Pangea
⏎
자동 분석 · 1달 전
로그인
데모 · 1D1S 프로젝트를 읽기 전용으로 보고 있습니다. 편집·AI 실행은 할 수 없습니다.내 레포로 시작하기 →
1D1S
홈브리핑진전이슈8일정화면API지도 탐색플로우문서변경

문서
루트/AI 역문서/2. 핵심 기능

AI 초안 · 검토 전노근 · 1달 전 수정

2.4 좋아요와 댓글

AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@f69a4eb 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.

관련 소스 파일

  • 1D1S-client/src/app.feature/diary/detail/components/DiaryActionToolbar.tsx
  • 1D1S-client/src/app.feature/diary/detail/components/DiaryCommentSection.tsx
  • 1D1S-client/src/app.feature/diary/detail/api/diaryCommentApi.ts
  • 1D1S-client/src/app.feature/challenge/detail/hooks/useChallengeLikeToggle.ts
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/diary/entity/DiaryLike.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeLike.java
목적과 범위

이 페이지는 1D1S-client와 1D1S-server-v2 원문에서 확인되는 좋아요와 댓글 기능의 연결 구조를 설명합니다. 범위는 다음과 같습니다.

  • 일지 상세에서 좋아요 상태와 좋아요 수를 표시하고 토글하는 화면 동작
  • 일지 댓글의 목록 조회, 원댓글 작성, 대댓글 작성, 삭제, 신고 진입 흐름
  • 챌린지 상세 카드에서 좋아요를 낙관적으로 반영하는 클라이언트 훅
  • 일지 좋아요와 챌린지 좋아요를 저장하는 서버 엔티티
  • 댓글 신고와 좋아요 조회가 화면, API 클라이언트, 서버 모델에서 어떻게 분리되어 있는지

이 원문에는 서버의 컨트롤러·서비스·리포지터리 구현, 일지 좋아요 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, 서버 엔티티로 나뉩니다.

영역구성 요소확인되는 역할
일지 좋아요 화면DiaryActionToolbardiaryData.likedByMe, diaryData.likeCount를 사용해 좋아요 상태와 수를 표시하고 onLikeToggle을 호출합니다.
일지 댓글 화면DiaryCommentSection댓글 트리 조회 결과를 표시하고 댓글·대댓글 작성, 삭제, 신고 동작을 연결합니다.
일지 댓글 데이터 계층diaryCommentApi댓글 목록, 대댓글 목록, 작성, 삭제, 신고 요청을 HTTP 요청으로 노출합니다.
댓글 상태 훅useCommentTreethreadComments, deletedCommentIds, totalCommentCount 등 댓글 화면에 필요한 상태를 제공합니다.
댓글 변경 훅useCreateDiaryComment, useCreateCommentReply, useDeleteComment일지 댓글 생성·대댓글 생성·댓글 삭제 mutation을 제공합니다.
챌린지 좋아요 훅useChallengeLikeToggle로그인 확인, 낙관적 상태 변경, 좋아요·좋아요 취소 mutation 호출을 담당합니다.
서버 일지 좋아요 모델DiaryLikemember와 diary를 연결하는 diary_like 엔티티입니다.
서버 챌린지 좋아요 모델ChallengeLikemember와 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에는 같은 이름의 판정 메서드가 원문에 없습니다.

모델테이블회원 연결대상 연결현재 사용자 판정
DiaryLikediary_likeMember memberDiary diarylikedByMe(Member me) 제공
ChallengeLikechallenge_likeMember memberChallenge challenge원문에서 별도 메서드 확인 안 됨
댓글 API 응답 정규화

댓글 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로 사용합니다. 루트 댓글이 삭제된 경우에는 대댓글을 등록하지 않습니다.

구체적인 흐름은 다음과 같습니다.

  1. DiaryCommentSection이 diaryId, currentMemberId, currentUserNickname을 사용해 useCommentTree를 호출합니다.
  2. useCommentTree는 threadComments, flatCommentAuthors, flatCommentMeta, deletedCommentIds, replyTargetRootIdMap, totalCommentCount, isCommentsLoading, isCommentsError를 반환합니다.
  3. 댓글 목록이 로딩 중이면 DiaryCommentsSkeleton을 렌더링합니다. 오류이면 댓글을 불러오지 못했습니다.를 표시하고, 목록이 비어 있으면 첫 댓글을 남겨보세요.를 표시합니다.
  4. 댓글이 있으면 CommentThread에 threadComments와 현재 사용자 ID를 전달합니다. currentMemberId가 있으면 문자열로 변환하고, 없으면 undefined를 전달합니다.
  5. 사용자가 원댓글을 입력하면 commentContent.trim()으로 공백을 제거합니다. 빈 문자열이거나 mutation이 진행 중이면 제출하지 않습니다.
  6. 로그인 상태가 아니면 onRequireLogin()을 호출하고 API mutation을 실행하지 않습니다.
  7. 로그인 상태이면 useCreateDiaryComment(diaryId)가 제공하는 mutation에 { content }를 전달합니다. 성공 시 입력값을 비우고, 완료 시 중복 제출 방지용 isCommentSubmittingRef를 해제합니다.
  8. 사용자가 답글을 제출하면 handleReplySubmit이 댓글 ID를 숫자로 변환하고, replyTargetRootIdMap에서 루트 댓글 ID를 찾습니다.
  9. 루트 댓글이 삭제되지 않았으면 useCreateCommentReply(diaryId)에 { commentId: rootCommentId, content: trimmedContent }를 전달합니다.
  10. 댓글 삭제는 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입니다.

따라서 원문에서 확인되는 일지 좋아요의 화면 책임은 다음과 같습니다.

  1. 상위 화면이 일지 상세 데이터로 likedByMe, likeCount를 구성합니다.
  2. DiaryActionToolbar가 두 값을 읽어 현재 상태와 수를 렌더링합니다.
  3. 사용자가 좋아요 버튼을 누르면 onLikeToggle()을 호출합니다.
  4. 상위 화면 또는 연결된 훅이 실제 좋아요 변경 요청을 처리합니다. 해당 훅과 API 경로는 제공된 원문에 없습니다.
  5. 요청 중에는 isLikePending으로 같은 버튼을 비활성화합니다.
챌린지 좋아요

챌린지에서는 useChallengeLikeToggle이 좋아요 흐름을 직접 관리합니다.

  1. challengeId가 undefined이면 아무 동작도 하지 않습니다.
  2. useIsLoggedIn()이 false이면 loginOpen을 true로 바꾸고 로그인 안내용 상태를 반환합니다.
  3. 이미 useLikeChallenge 또는 useUnlikeChallenge mutation이 진행 중이면 중복 요청을 막습니다.
  4. 현재 화면 상태와 반대인 값을 override에 즉시 저장합니다.
  5. 현재 좋아요 상태가 true이면 unlike.mutate(challengeId)를 호출하고, false이면 like.mutate(challengeId)를 호출합니다.
  6. 요청 실패 시 notifyApiError를 실행합니다.
  7. 요청 완료 시 override를 null로 되돌립니다.
  8. 화면에 보이는 좋아요 수는 서버 수에 낙관적 변화량을 더하고, 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 엔드포인트

댓글 신고는 댓글 목록에 포함된 일반적인 작성·삭제 흐름과 별도의 상태 및 API로 분리됩니다.

화면에서 handleReportComment는 댓글 ID를 숫자로 변환하고, 유효하지 않거나 삭제된 댓글이면 중단합니다. 로그인하지 않은 사용자는 onRequireLogin()을 거칩니다. 로그인된 사용자가 신고를 선택하면 reportTargetCommentId에 댓글 ID를 저장하고 CommentReportDialog를 엽니다. 다만 제공된 DiaryCommentSection 원문에는 CommentReportDialog의 제출 callback 연결 코드가 보이지 않습니다.

diaryCommentApi.reportComment는 댓글 ID와 CreateCommentReportRequest를 받아 다음 요청을 노출합니다.

기능메서드클라이언트 경로요청 데이터반환
일지 댓글 목록getDiaryCommentsGET /comments/diaries/:diaryIdpage, size 쿼리DiaryCommentListResponse
대댓글 목록getCommentRepliesGET /comments/:commentId/repliespage, size 쿼리DiaryCommentListResponse
댓글 작성createDiaryCommentPOST /comments/diaries/:diaryIdCreateCommentRequestvoid
대댓글 작성createCommentReplyPOST /comments/:commentId/repliesCreateCommentRequestvoid
댓글 삭제deleteCommentDELETE /comments/:commentId없음void
댓글 신고reportCommentPOST /comments/:commentId/reportCreateCommentReportRequestboolean

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

확인 필요

다음 항목은 제공된 원문만으로는 확인할 수 없습니다.

  • 일지 좋아요의 실제 mutation 훅과 API 엔드포인트는 무엇입니까? DiaryActionToolbar의 onLikeToggle을 연결하는 상위 컴포넌트 경로가 필요합니다.
  • 챌린지 좋아요의 useLikeChallenge, useUnlikeChallenge가 호출하는 실제 API 엔드포인트와 서버 컨트롤러는 무엇입니까?
  • 일지 상세 조회 응답에서 likedByMe, likeCount, totalCommentCount를 구성하는 DTO와 서버 서비스는 무엇입니까?
  • DiaryLike와 ChallengeLike의 중복 좋아요 방지 제약, 삭제 방식, repository 조회 쿼리는 무엇입니까?
  • CommentReportDialog가 diaryCommentApi.reportComment를 직접 호출하는지, 별도 훅을 거치는지, 신고 사유의 CreateCommentReportRequest 필드는 무엇입니까?
  • 서버에 댓글 신고 엔티티·테이블·관리자 처리 API가 있습니까?
  • 챌린지에도 댓글 작성·조회·대댓글·신고 기능이 존재합니까? 제공된 원문에서는 일지 댓글 API와 화면만 확인됩니다.
  • 댓글 mutation 성공 뒤 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

속성
분류스펙
폴더AI 역문서 / 2. 핵심 기능
#AI 역문서
연결된 화면·API0
아직 없음
이 문서를 언급한 문서
2
0 목차스펙
2.3 일지 작성과 조회스펙
#AI 역문서