AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@f69a4eb 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.
관련 소스 파일
이 페이지는 1D1S-client와 1D1S-server-v2 원문에서 확인되는 좋아요와 댓글 기능의 연결 구조를 설명합니다. 범위는 다음과 같습니다.
이 원문에는 서버의 컨트롤러·서비스·리포지터리 구현, 일지 좋아요 API와 챌린지 좋아요 API의 실제 경로, 챌린지 댓글 화면 및 댓글 API가 포함되어 있지 않습니다. 따라서 해당 부분은 구현 사실로 단정하지 않고 3.2 API와 응답 구조, 3.3 도메인 데이터 모델, 5.1 서버 도메인 계층에서 확인할 항목으로 남깁니다. 일지 상세 전체 흐름은 2.3 일지 작성과 조회, 챌린지 상세와 참여 흐름은 2.1 챌린지 탐색과 참여와 연결됩니다.
출처: 1D1S-client/src/app.feature/diary/detail/components/DiaryActionToolbar.tsx:6-18, 1D1S-client/src/app.feature/diary/detail/api/diaryCommentApi.ts:109-177, 1D1S-client/src/app.feature/challenge/detail/hooks/useChallengeLikeToggle.ts:24-63
기능은 화면 컴포넌트, 클라이언트 훅·API, 서버 엔티티로 나뉩니다.
| 영역 | 구성 요소 | 확인되는 역할 |
|---|---|---|
| 일지 좋아요 화면 | DiaryActionToolbar | diaryData.likedByMe, diaryData.likeCount를 사용해 좋아요 상태와 수를 표시하고 onLikeToggle을 호출합니다. |
| 일지 댓글 화면 | DiaryCommentSection | 댓글 트리 조회 결과를 표시하고 댓글·대댓글 작성, 삭제, 신고 동작을 연결합니다. |
| 일지 댓글 데이터 계층 | diaryCommentApi | 댓글 목록, 대댓글 목록, 작성, 삭제, 신고 요청을 HTTP 요청으로 노출합니다. |
| 댓글 상태 훅 | useCommentTree | threadComments, deletedCommentIds, totalCommentCount 등 댓글 화면에 필요한 상태를 제공합니다. |
| 댓글 변경 훅 | useCreateDiaryComment, useCreateCommentReply, useDeleteComment | 일지 댓글 생성·대댓글 생성·댓글 삭제 mutation을 제공합니다. |
| 챌린지 좋아요 훅 | useChallengeLikeToggle | 로그인 확인, 낙관적 상태 변경, 좋아요·좋아요 취소 mutation 호출을 담당합니다. |
| 서버 일지 좋아요 모델 | DiaryLike | member와 diary를 연결하는 diary_like 엔티티입니다. |
| 서버 챌린지 좋아요 모델 | ChallengeLike | member와 challenge를 연결하는 challenge_like 엔티티입니다. |
일지 댓글 화면은 CommentThread에 onReplySubmit, onDelete, onReport를 전달합니다. 즉, 디자인 시스템의 댓글 표현과 일지 기능의 mutation·신고 상태 관리가 DiaryCommentSection을 경계로 결합됩니다. 반면 좋아요는 일지에서 DiaryActionToolbar가 콜백을 받는 구조이고, 챌린지에서는 useChallengeLikeToggle이 직접 mutation 훅을 사용합니다.
출처: 1D1S-client/src/app.feature/diary/detail/components/DiaryActionToolbar.tsx:20-29, 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx:112-141, 1D1S-client/src/app.feature/challenge/detail/hooks/useChallengeLikeToggle.ts:1-6, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/diary/entity/DiaryLike.java:10-28, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeLike.java:10-28
서버에는 좋아요를 별도의 엔티티로 저장합니다. DiaryLike는 diary_like 테이블에 매핑되며 Member와 Diary를 각각 member_id, diary_id 외래 키로 참조합니다. ChallengeLike도 같은 구조로 challenge_like 테이블에 매핑되고 Member와 Challenge를 참조합니다.
DiaryLike.likedByMe(Member me)는 저장된 좋아요의 member와 현재 회원 me가 모두 존재하고, 두 회원의 ID가 같을 때 true를 반환합니다. 어느 한쪽의 회원 또는 ID가 null이면 false입니다. 이 메서드는 일지 좋아요 엔티티를 현재 사용자의 좋아요 여부로 판정하는 서버 모델 동작을 보여줍니다. ChallengeLike에는 같은 이름의 판정 메서드가 원문에 없습니다.
| 모델 | 테이블 | 회원 연결 | 대상 연결 | 현재 사용자 판정 |
|---|---|---|---|---|
DiaryLike | diary_like | Member member | Diary diary | likedByMe(Member me) 제공 |
ChallengeLike | challenge_like | Member member | Challenge challenge | 원문에서 별도 메서드 확인 안 됨 |
댓글 API 응답은 서버 필드 이름의 차이를 클라이언트 모델로 통일합니다. 댓글 ID는 id가 없으면 commentId를 사용하고, 답글 수는 replyCount가 없으면 replyCnt를 사용합니다. 삭제 여부도 isDeleted가 없으면 deleted를 사용합니다. 작성자 역시 authorInfoDto, author, member 순으로 확인하고, ID·닉네임·프로필 이미지에 개별 fallback 필드를 적용합니다.
페이지 정보가 없으면 normalizePageInfo가 요청 시 사용한 page, size를 기본값으로 사용하고, totalElements, totalPages, hasNextPage는 각각 0, 0, false로 채웁니다. 이 정규화는 화면 컴포넌트가 여러 응답 형태를 직접 처리하지 않도록 diaryCommentApi 내부에 배치되어 있습니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/diary/entity/DiaryLike.java:18-38, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeLike.java:18-28, 1D1S-client/src/app.feature/diary/detail/api/diaryCommentApi.ts:18-107
일지 댓글은 원댓글과 대댓글을 분리된 API로 다루지만 화면에서는 CommentThread로 트리 형태를 표시합니다. 대댓글 작성 시 사용자가 클릭한 노드가 대댓글이어도 replyTargetRootIdMap에서 루트 댓글 ID를 찾아 /comments/:id/replies 요청의 대상 ID로 사용합니다. 루트 댓글이 삭제된 경우에는 대댓글을 등록하지 않습니다.
구체적인 흐름은 다음과 같습니다.
DiaryCommentSection이 diaryId, currentMemberId, currentUserNickname을 사용해 useCommentTree를 호출합니다.useCommentTree는 threadComments, flatCommentAuthors, flatCommentMeta, deletedCommentIds, replyTargetRootIdMap, totalCommentCount, isCommentsLoading, isCommentsError를 반환합니다.DiaryCommentsSkeleton을 렌더링합니다. 오류이면 댓글을 불러오지 못했습니다.를 표시하고, 목록이 비어 있으면 첫 댓글을 남겨보세요.를 표시합니다.CommentThread에 threadComments와 현재 사용자 ID를 전달합니다. currentMemberId가 있으면 문자열로 변환하고, 없으면 undefined를 전달합니다.commentContent.trim()으로 공백을 제거합니다. 빈 문자열이거나 mutation이 진행 중이면 제출하지 않습니다.onRequireLogin()을 호출하고 API mutation을 실행하지 않습니다.useCreateDiaryComment(diaryId)가 제공하는 mutation에 { content }를 전달합니다. 성공 시 입력값을 비우고, 완료 시 중복 제출 방지용 isCommentSubmittingRef를 해제합니다.handleReplySubmit이 댓글 ID를 숫자로 변환하고, replyTargetRootIdMap에서 루트 댓글 ID를 찾습니다.useCreateCommentReply(diaryId)에 { commentId: rootCommentId, content: trimmedContent }를 전달합니다.window.confirm('댓글을 삭제하시겠습니까?') 확인 뒤 useDeleteComment(diaryId)에 대상 댓글 ID를 전달합니다.출처: 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx:124-229, 1D1S-client/src/app.feature/diary/detail/api/diaryCommentApi.ts:109-167
DiaryActionToolbar는 좋아요 mutation 자체를 구현하지 않습니다. 상위 화면이 onLikeToggle을 전달하고, toolbar는 이를 클릭 이벤트에 연결합니다. 버튼은 isLikePending이 true이면 비활성화되며, diaryData.likedByMe에 따라 색상과 하트 채움 상태를 바꿉니다. 표시되는 수는 diaryData.likeCount입니다.
따라서 원문에서 확인되는 일지 좋아요의 화면 책임은 다음과 같습니다.
likedByMe, likeCount를 구성합니다.DiaryActionToolbar가 두 값을 읽어 현재 상태와 수를 렌더링합니다.onLikeToggle()을 호출합니다.isLikePending으로 같은 버튼을 비활성화합니다.챌린지에서는 useChallengeLikeToggle이 좋아요 흐름을 직접 관리합니다.
challengeId가 undefined이면 아무 동작도 하지 않습니다.useIsLoggedIn()이 false이면 loginOpen을 true로 바꾸고 로그인 안내용 상태를 반환합니다.useLikeChallenge 또는 useUnlikeChallenge mutation이 진행 중이면 중복 요청을 막습니다.override에 즉시 저장합니다.true이면 unlike.mutate(challengeId)를 호출하고, false이면 like.mutate(challengeId)를 호출합니다.notifyApiError를 실행합니다.override를 null로 되돌립니다.Math.max(0, ...)로 음수가 되지 않게 합니다.여기서 “좋아요 조회”는 화면에서 좋아요 여부와 수를 읽는 것과, 서버 모델이 좋아요 레코드를 저장하는 것으로 나뉩니다. DiaryActionToolbar와 useChallengeLikeToggle은 각각 화면 상태를 소비하거나 임시로 덮어쓰지만, 실제 조회 API 경로는 제공된 원문에 없습니다. DiaryLike와 ChallengeLike는 저장 모델이지만 조회용 repository나 controller는 포함되어 있지 않습니다.
출처: 1D1S-client/src/app.feature/diary/detail/components/DiaryActionToolbar.tsx:35-55, 1D1S-client/src/app.feature/challenge/detail/hooks/useChallengeLikeToggle.ts:16-60, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/diary/entity/DiaryLike.java:22-38, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeLike.java:22-28
댓글 신고는 댓글 목록에 포함된 일반적인 작성·삭제 흐름과 별도의 상태 및 API로 분리됩니다.
화면에서 handleReportComment는 댓글 ID를 숫자로 변환하고, 유효하지 않거나 삭제된 댓글이면 중단합니다. 로그인하지 않은 사용자는 onRequireLogin()을 거칩니다. 로그인된 사용자가 신고를 선택하면 reportTargetCommentId에 댓글 ID를 저장하고 CommentReportDialog를 엽니다. 다만 제공된 DiaryCommentSection 원문에는 CommentReportDialog의 제출 callback 연결 코드가 보이지 않습니다.
diaryCommentApi.reportComment는 댓글 ID와 CreateCommentReportRequest를 받아 다음 요청을 노출합니다.
| 기능 | 메서드 | 클라이언트 경로 | 요청 데이터 | 반환 |
|---|---|---|---|---|
| 일지 댓글 목록 | getDiaryComments | GET /comments/diaries/:diaryId | page, size 쿼리 | DiaryCommentListResponse |
| 대댓글 목록 | getCommentReplies | GET /comments/:commentId/replies | page, size 쿼리 | DiaryCommentListResponse |
| 댓글 작성 | createDiaryComment | POST /comments/diaries/:diaryId | CreateCommentRequest | void |
| 대댓글 작성 | createCommentReply | POST /comments/:commentId/replies | CreateCommentRequest | void |
| 댓글 삭제 | deleteComment | DELETE /comments/:commentId | 없음 | void |
| 댓글 신고 | reportComment | POST /comments/:commentId/report | CreateCommentReportRequest | boolean |
getDiaryComments와 getCommentReplies는 기본 page를 0, 기본 size를 10으로 사용합니다. 두 조회 응답은 normalizeListResponse를 거쳐 items와 pageInfo가 항상 클라이언트 모델 형태가 되도록 변환됩니다.
이 표의 경로는 apiClient에 전달되는 클라이언트 요청 경로입니다. 원문에는 서버 컨트롤러의 @RequestMapping이나 신고 엔티티가 없으므로, 서버에서 어떤 컨트롤러·서비스·테이블이 처리하는지는 확인할 수 없습니다.
출처: 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx:231-245, 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx:299-307, 1D1S-client/src/app.feature/diary/detail/api/diaryCommentApi.ts:109-177
일지 상세의 액션 영역은 DiaryActionToolbar에서 좋아요, 댓글 수, 공유를 한 줄의 pill 형태로 표시합니다. 댓글 카운터에 onCommentTap이 전달되면 버튼으로 렌더링되어 해당 callback을 호출합니다. callback이 없으면 같은 숫자를 span으로 표시하므로 클릭 동작이 없습니다. 일지 상세에서 댓글 섹션으로 이동하려면 상위 화면이 onCommentTap을 전달해야 합니다.
댓글 섹션은 데스크톱과 모바일 입력을 나눕니다. 데스크톱·큰 화면 입력은 diary-comment-content라는 TextField와 등록 버튼으로 구성되고, 모바일 입력은 DiaryMobileCommentBar의 MobileBottomActionBar 안에 있습니다. 모바일에서 대댓글 입력이 열리면 [data-reply-input] 존재 여부를 MutationObserver로 감지하여 메인 입력 바를 숨깁니다.
입력과 mutation의 방어 동작은 다음과 같습니다.
| 상황 | 동작 |
|---|---|
| 비로그인 사용자의 댓글·답글·삭제·신고 | onRequireLogin() 호출 |
| 공백만 입력 | 제출하지 않음 |
| mutation 진행 중 | 제출 버튼 또는 처리 함수를 차단 |
| 같은 틱의 중복 제출 | isCommentSubmittingRef로 차단 |
| 잘못된 댓글 ID | 숫자 변환 후 유효하지 않으면 중단 |
| 삭제된 루트 댓글에 답글 | 답글 mutation을 실행하지 않음 |
| 댓글 목록 로딩 | DiaryCommentsSkeleton 표시 |
| 댓글 목록 조회 실패 | 오류 문구 표시 |
| 댓글 없음 | 첫 댓글 안내 표시 |
| 챌린지 좋아요 요청 실패 | notifyApiError 실행 후 서버 상태로 복귀 |
댓글 입력 포커스 시에는 모바일 키보드에 가려지지 않도록 visualViewport를 관찰합니다. 인라인 답글 입력은 scrollIntoView({ block: 'center', behavior: 'smooth' })를 사용하고, 고정된 메인 댓글 입력에는 같은 보정을 적용하지 않습니다. 네이티브 앱에서는 visualViewport resize 후 한 번 스크롤하고, 일반 환경에서는 resize를 requestAnimationFrame으로 합칩니다.
출처: 1D1S-client/src/app.feature/diary/detail/components/DiaryActionToolbar.tsx:30-88, 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx:24-100, 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx:261-327, 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx:340-415
다음 항목은 제공된 원문만으로는 확인할 수 없습니다.
DiaryActionToolbar의 onLikeToggle을 연결하는 상위 컴포넌트 경로가 필요합니다.useLikeChallenge, useUnlikeChallenge가 호출하는 실제 API 엔드포인트와 서버 컨트롤러는 무엇입니까?likedByMe, likeCount, totalCommentCount를 구성하는 DTO와 서버 서비스는 무엇입니까?DiaryLike와 ChallengeLike의 중복 좋아요 방지 제약, 삭제 방식, repository 조회 쿼리는 무엇입니까?CommentReportDialog가 diaryCommentApi.reportComment를 직접 호출하는지, 별도 훅을 거치는지, 신고 사유의 CreateCommentReportRequest 필드는 무엇입니까?useCommentTree가 어떤 query key를 무효화하고 목록을 다시 조회하는지 확인이 필요합니다.DiaryLike.setDiary(Diary diary)는 diary.getLikes().add(this)만 수행합니다. 양방향 연관관계의 반대편 설정, 저장 시점, 좋아요 취소 로직은 추가 원문이 필요합니다.출처: 1D1S-client/src/app.feature/diary/detail/components/DiaryActionToolbar.tsx:8-17, 1D1S-client/src/app.feature/challenge/detail/hooks/useChallengeLikeToggle.ts:5-6, 1D1S-client/src/app.feature/diary/detail/api/diaryCommentApi.ts:169-177, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/diary/entity/DiaryLike.java:30-38