AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@23a5ca8 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.
관련 소스 파일
이 페이지는 사용자가 챌린지의 달성 기록을 일지로 작성하고, 작성 과정에서 감정·본문·이미지를 저장한 뒤 일지 목록과 상세 화면에서 기록을 조회하는 흐름을 설명합니다. 프론트엔드의 일지 목록·상세·작성 라우트와 서버의 DiaryController, DiaryService, Diary 및 하위 엔티티를 함께 다룹니다.
포함 범위는 다음과 같습니다.
feeling, 이미지 URL, 썸네일, 공개 여부와 같은 일지 필드 저장다음 내용은 이 페이지의 직접 범위에서 제외합니다.
출처: page.tsx:5-12, page.tsx:8-20, DiaryController.java:34-40
일지 기능은 라우트 페이지가 화면 컴포넌트를 호출하고, 화면 컴포넌트가 React Query 기반 조회 로직 또는 작성 로직을 통해 서버 API와 연결되는 형태입니다. 제공된 원문에서 화면 내부 구현은 확인되지 않으므로, 화면 컴포넌트와 조회 훅의 구체적인 요청 구성은 확인 필요입니다.
| 구성 요소 | 역할 |
|---|---|
DiaryListPage | /diary 경로에서 DiaryListScreen을 Suspense 경계 안에 배치합니다. |
DiaryListScreen | 일지 목록 화면을 제공합니다. 주석상 목록 데이터 프리페치는 클라이언트 React Query의 useDiaryList로 이관되었습니다. |
DiaryDetailPage | /diary/[id]의 id를 숫자로 변환해 DiaryDetailScreen에 전달합니다. |
DiaryDetailScreen | useDiaryDetail을 통해 상세 데이터를 조회하는 화면입니다. 인증 상태에 따라 LoginRequiredDialog가 상세 내용을 가릴 수 있습니다. |
DiaryCreatePage | /diary/create에서 DiaryCreateScreen을 Suspense 안에 배치합니다. |
DiaryController | /diaries 하위 REST API를 노출하고 현재 로그인 회원을 CurrentUserContext에서 가져옵니다. |
DiaryService | 일지 생성·수정·조회 과정에서 회원, 챌린지, 참여자, 회차, 목표, 이미지와 부가 보상 처리를 조정합니다. |
Diary | diary 테이블에 일지 본문과 상태를 저장하고 DiaryGoal, DiaryImage, DiaryLike, DiaryReport를 연결합니다. |
목록 라우트는 서버에서 쿠키를 읽어 데이터를 미리 가져오지 않습니다. 따라서 페이지 셸은 Link 프리페치로 미리 준비될 수 있고, 실제 목록 데이터는 useDiaryList의 QueryClient 캐시에서 제공됩니다. 상세 라우트도 같은 방식으로 useDiaryDetail이 클라이언트에서 상세를 조회합니다.
출처: page.tsx:6-18, page.tsx:11-26, page.tsx:1-9, DiaryService.java:55-86
핵심 엔티티는 Diary입니다. Diary는 작성 회원과 챌린지를 ManyToOne으로 연결하고, 목표·이미지·좋아요·신고를 하위 컬렉션으로 보유합니다. 데이터베이스 테이블 이름은 diary이며, BaseTimeEntity를 상속하므로 공통 시간 필드를 포함합니다.
| 엔티티·테이블 | 주요 필드 또는 관계 | 설명 |
|---|---|---|
Diary / diary | id, title, content | 일지 식별자, 제목, 본문입니다. 본문은 TEXT 컬럼으로 저장됩니다. |
Diary / diary | completedDate | 달성 날짜입니다. 작성 시각과 구분되며 회차 귀속과 XP 판정의 기준으로 사용됩니다. |
Diary / diary | feeling | Feeling enum 값을 문자열로 저장합니다. |
Diary / diary | isPublic, isAllGoalsCompleted, isDeleted | 공개 여부, 전체 목표 달성 여부, 소프트 삭제 여부입니다. 기본값은 각각 true, false, false입니다. |
Diary / diary | thumbnailUrl | DiaryImage.url 중 하나를 대표 썸네일로 저장합니다. 이미지가 없으면 null일 수 있습니다. |
Diary / diary | member, challenge | 작성 회원과 챌린지에 대한 지연 로딩 관계입니다. |
Diary / diary | occurrenceId | 달성 날짜에 대응하는 회차 ID입니다. 아직 nullable이며 챌린지가 없는 일지는 회차도 없습니다. |
DiaryGoal / diary_goal | diary, challengeGoal, isCompleted | 해당 일지에서 챌린지 목표를 달성했는지 저장합니다. |
DiaryImage / diary_image | url, diary | 일지에 연결된 이미지 URL을 저장합니다. |
DiaryLike / diary_like | member, diary | 어떤 회원이 어떤 일지를 좋아요 했는지 나타냅니다. |
DiaryReport / diary_report | type, content, member, diary | 일지 신고 유형과 내용을 저장합니다. |
Diary.replaceImages는 이미지 목록을 전체 교체하는 방식입니다. imageUrls == null이면 기존 이미지를 유지하고, 빈 목록이면 기존 이미지를 모두 삭제합니다. 목록이 전달되면 기존 DiaryImage를 비운 뒤 각 URL로 새 항목을 구성합니다. 생성·수정 서비스는 이 메서드와 updateThumbnailUrl을 함께 사용합니다.
일지의 occurrenceId는 작성 시각이 아니라 completedDate로 계산됩니다. 따라서 과거 날짜를 달성 날짜로 지정한 일지는 해당 날짜의 회차에 귀속됩니다. 수정 시에는 챌린지와 달성 날짜를 모두 바꿀 수 있으므로 회차도 다시 계산합니다.
출처: Diary.java:19-105, Diary.java:107-164, DiaryGoal.java:10-37, DiaryImage.java:9-25
일지 생성은 작성 화면에서 전달한 DiaryRequest를 서버가 검증하고, 챌린지의 참여자 목표를 기반으로 DiaryGoal을 구성한 뒤 이미지와 부가 활동을 저장하는 순서입니다.
DiaryCreateScreen에서 챌린지, 달성 날짜, 제목, 본문, 감정, 공개 여부, 이미지 URL, 썸네일 URL, 달성 목표 ID를 입력합니다. 화면 내부의 실제 입력 컴포넌트와 요청 생성 코드는 제공된 원문에서 확인되지 않습니다.POST /diaries를 호출합니다. 서버의 DiaryController.createDiary는 CurrentUserContext.getCurrentMemberId()로 현재 회원을 구합니다.DiaryService.createDiary가 본문을 sanitizeContent로 처리하고 회원과 챌린지를 조회합니다.validateDiaryWritable이 챌린지 종료 후 작성 가능 여부를 확인합니다. 종료 후 허용되는 유예 기간은 KST 기준 POST_END_WRITE_GRACE_DAYS = 2입니다.validateFixedDayOfWeek가 고정 요일 챌린지의 달성 날짜를 검증하고, validatePhotoRequired가 인증샷 필수 챌린지의 이미지 존재 여부를 검증합니다.ChallengeOccurrenceService.occurrenceIdFor가 챌린지 ID와 달성 날짜로 회차를 계산합니다. 같은 회차의 Participant를 조회하지 못하면 PARTICIPANT_NOT_FOUND가 발생합니다.Diary를 생성하고 diaryRepository.save로 저장합니다. 이때 completedDate, feeling, isPublic, content, challenge, member, occurrenceId가 설정됩니다.validateImageUrls로 검증하고 replaceImages로 DiaryImage를 생성합니다. 첫 이미지 또는 요청된 썸네일은 thumbnailUrl로 반영됩니다.ChallengeGoal 목록과 요청된 achievedGoalIds를 비교해 각 DiaryGoal의 isCompleted를 구성합니다. 참여자의 목표에 없는 ID가 들어오면 일지를 생성하지 않고 DIARY_NOT_CREATED를 발생시킵니다.isAllGoalsCompleted에 저장합니다.DiaryXpService를 통해 XP를 지급하고, StreakService.recalculate로 스트릭을 다시 계산합니다. 친구 목록에는 notificationService.notifyFriendDiaryCreated가 일지 생성 알림을 보냅니다.DiaryResponse를 생성해 챌린지 요약, 작성자 프로필 이미지 URL, 현재 레벨, 좋아요 기본값, 댓글 수 0, 챌린지 도서 정보와 함께 반환합니다.PATCH /diaries/{id}를 호출합니다.DiaryController.updateDiary가 회원 ID와 경로의 diaryId를 DiaryService.updateDiary에 전달합니다.DIARY_NOT_ACCESS를 발생시킵니다.imageUrls가 null이면 기존 이미지를 유지합니다. 배열이 전달되면 해당 배열을 최종 이미지 목록으로 간주해 전체 교체합니다.Diary.updateDiary가 제목, 감정, 본문, 공개 여부, 달성 날짜, 챌린지, 회차와 목표 목록을 갱신합니다.출처: DiaryController.java:81-86, DiaryService.java:88-224, DiaryService.java:226-322
모든 응답은 ApiResponse로 감싸집니다. 생성·수정·단건 조회는 DiaryResponse를 사용하고, 챌린지별 목록은 OffsetPagination<DiaryResponse>, 전체 공개 목록은 Pagination<DiaryResponse>를 사용합니다.
| HTTP 메서드 | 경로 | 컨트롤러 메서드 | 요청·응답 |
|---|---|---|---|
POST | /diaries | createDiary | DiaryRequest를 받아 DiaryResponse를 반환합니다. 현재 로그인 회원 기준으로 생성합니다. |
PATCH | /diaries/{id} | updateDiary | diaryId와 DiaryRequest를 받아 수정된 DiaryResponse를 반환합니다. |
GET | /diaries/{id} | getDiary | diaryId로 단건 DiaryResponse를 조회합니다. |
GET | /diaries/challenges/{challengeId} | getChallengeDiaries | 특정 챌린지의 공개 일지를 OffsetPagination으로 조회합니다. |
GET | /diaries | getAllDiaryBySize | size, cursor를 사용해 공개 일지를 커서 기반 Pagination으로 조회합니다. |
GET | /diaries/all | getAllDiary | 모든 공개 일지를 List<DiaryResponse>로 조회합니다. 테스트용 일괄 조회 API로 주석에 기록되어 있습니다. |
GET | /diaries/random | randomDiary | size개만큼 공개 일지를 무작위 조회합니다. |
GET /diaries/challenges/{challengeId}는 선택적으로 date를 받습니다. date가 있으면 completedDate가 해당 날짜인 일지만 조회하고, 없으면 전체를 조회합니다. page의 기본값은 0, size의 기본값은 10이며 createdAt 내림차순과 id 내림차순으로 정렬합니다.
GET /diaries는 첫 호출에서 cursor를 생략하고, 다음 호출에서 응답의 pageInfo.nextCursor를 전달하는 구조로 설명되어 있습니다. size는 기본값이 10이고 최대 100까지 허용된다고 API 문서에 기재되어 있습니다.
출처: DiaryController.java:81-86, DiaryController.java:126-169, DiaryController.java:171-210, DiaryController.java:213-303
일지 관련 Next.js 라우트는 세 화면으로 나뉩니다.
| 화면 | 라우트 파일 | 호출 컴포넌트 | 데이터 동작 |
|---|---|---|---|
| 일지 목록 | src/app/diary/(board)/page.tsx | DiaryListScreen | Suspense 안에서 표시되며 DiaryBoardSkeleton을 로딩 화면으로 사용합니다. 목록 데이터는 useDiaryList로 클라이언트에서 조회합니다. |
| 일지 상세 | src/app/diary/[id]/page.tsx | DiaryDetailScreen | URL의 id를 Number(id)로 변환해 전달합니다. 상세 데이터는 useDiaryDetail로 조회합니다. |
| 일지 작성 | src/app/diary/create/page.tsx | DiaryCreateScreen | Suspense 안에서 표시하며 fallback은 null입니다. |
목록 페이지는 DiaryListPage 자체에서 서버 프리페치를 수행하지 않습니다. 주석에 따르면 데이터 프리페치는 useDiaryList로 이관되었고, 서버가 쿠키를 읽지 않기 때문에 라우트는 dynamic 강제를 사용하지 않습니다. 캐시가 fresh하면 목록과 상세 사이를 이동할 때 스켈레톤 없이 QueryClient 캐시가 즉시 제공될 수 있습니다.
상세 페이지도 useDiaryDetail이 클라이언트에서 데이터를 조회합니다. 페이지 레벨에서 hasServerSession을 확인하지 않고, 미들웨어가 access 토큰 또는 세션 힌트를 기준으로 인증 가드를 처리합니다. 만료된 힌트만 있는 경우에는 DiaryDetailScreen의 useIsLoggedIn과 LoginRequiredDialog가 화면을 가립니다.
다만 제공된 원문에는 useDiaryList, useDiaryDetail, DiaryCreateScreen 내부에서 실제로 어떤 HTTP 클라이언트를 사용하고 어떤 요청 파라미터를 조합하는지 포함되어 있지 않습니다. 따라서 화면에서 위 API를 호출하는 구체적인 함수명과 요청 시점은 해당 구현 확인이 필요합니다.
출처: page.tsx:1-20, page.tsx:1-27, page.tsx:1-9
일지에는 별도의 상태 enum이 정의되어 있지 않으며, isPublic, isAllGoalsCompleted, isDeleted 등의 Boolean 필드로 주요 상태를 표현합니다.
| 상태·조건 | 코드 표현 | 처리 |
|---|---|---|
| 공개 일지 | isPublic = true | 공개 일지 목록 조회 대상이 될 수 있습니다. |
| 비공개 일지 | isPublic = false | 단건 조회에서 접근 권한 검사가 적용될 수 있으며, 문서상 비공개 접근 실패는 403입니다. |
| 목표 전체 달성 | isAllGoalsCompleted = true | 요청된 목표 ID가 참여자의 모든 ChallengeGoal을 포함할 때 설정됩니다. |
| 소프트 삭제 | isDeleted = true | softDelete가 값을 바꾸며 수정 서비스는 findByIdAndIsDeletedFalse로 삭제된 일지를 제외합니다. |
| 인증 실패 | 401 | 생성·수정 API 문서에 인증 필요 오류로 명시되어 있습니다. |
| 대상 없음 | 404 또는 도메인 예외 | 회원, 챌린지, 참여자 또는 일지가 없을 때 발생합니다. |
| 접근 권한 없음 | 403 또는 DIARY_NOT_ACCESS | 비공개 일지 접근이나 다른 회원의 일지 수정에서 문제가 됩니다. |
| 잘못된 요청 | 400 | 잘못된 ID 형식, 검증 실패, 챌린지·참여자 조건 위반 등이 해당합니다. |
생성 시 본문은 HtmlSanitizer를 통해 정리되고, 이미지 URL은 validateImageUrls로 검증됩니다. 고정 요일 챌린지는 달성 날짜가 지정된 요일인지 검사하며, 인증샷 필수 챌린지는 최종 이미지가 최소 한 장인지 검사합니다.
유효한 일지 저장 후 DiaryXpService.grantForValidDiary는 계정 최초 유효 일지, 일일 첫 일지, 다른 챌린지의 추가 일지, 주간 활동, 챌린지 주간 약속, 복귀 활동에 대한 XP 지급을 시도합니다. 지급은 XpGrantService.grantConfirmed와 멱등 키를 사용하며, 실제 지급된 항목만 XpGainResult에 포함됩니다. 이 처리와 배지 지급은 일지 저장 트랜잭션에 함께 연결됩니다.
출처: Diary.java:42-68, DiaryService.java:88-118, DiaryXpService.java:48-74
일지 도메인에는 일반 사용자 서비스와 별도로 AdminDiaryService가 있습니다. 이 서비스는 CurrentUserContext에서 현재 회원을 가져온 뒤 MemberRole.ADMIN인지 확인하는 requireAdmin을 먼저 실행합니다.
관리자 일지 조회는 DiaryRepository로 일지를 찾고 toDiaryResponse에서 다음 데이터를 조합합니다.
ChallengeService.toChallengeSummaryDiaryLikeRepositoryImageService.getFileUrlMemberGamificationService.currentLevelOfCommentRepositoryChallengeBookService.booksForDiary관리자 목록은 latest, author, challenge, likes 기준으로 필터링합니다. 작성자 닉네임과 챌린지 ID의 조합이 필터 종류와 맞지 않으면 INVALID_DIARY_ADMIN_FILTER가 발생합니다. 관리자 삭제는 삭제 사유가 비어 있지 않은지 확인한 뒤 Diary.updateDeletedReason과 Diary.softDelete를 실행하고, 작성자의 스트릭을 다시 계산합니다.
제공된 원문에는 AdminDiaryService를 호출하는 관리자 컨트롤러의 경로가 포함되어 있지 않으므로, 관리자 REST 엔드포인트의 정확한 URL은 확인 필요입니다.
출처: AdminDiaryService.java:37-72, AdminDiaryService.java:74-143, AdminDiaryService.java:181-199
DiaryListScreen의 useDiaryList가 실제로 GET /diaries, GET /diaries/challenges/{challengeId} 중 어떤 API를 호출하는지 확인해야 합니다.DiaryDetailScreen의 useDiaryDetail가 GET /diaries/{id}를 호출하는 실제 클라이언트 코드와 인증 실패 처리 기준을 확인해야 합니다.DiaryCreateScreen이 POST /diaries에 전달하는 DiaryRequest의 정확한 필드 목록과 이미지 presigned URL 업로드 선행 흐름을 확인해야 합니다.DiaryRequest의 feeling에 허용되는 Feeling enum 값과 클라이언트에서 표시하는 감정 선택지를 확인해야 합니다.DiaryController 원문이 deleteDiary 선언 중간에서 끝나므로, 일반 사용자 삭제 API의 HTTP 메서드와 정확한 경로를 확인해야 합니다.DiaryService의 수정 메서드 후반부가 제공되지 않았으므로, 수정 시 XP·스트릭·알림이 생성과 동일하게 재계산되는지 확인해야 합니다.DiaryService.getDiary 구현을 확인해야 합니다.AdminDiaryService를 연결하는 관리자 컨트롤러의 엔드포인트와 1D1S-admin 화면의 실제 호출 훅을 확인해야 합니다.출처: page.tsx:11-16, DiaryController.java:305-321, DiaryService.java:313-324