AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@23a5ca8 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.
관련 소스 파일
이 페이지는 사용자가 챌린지 작성 화면에서 목표, 기간, 공개 범위, 참여 조건 등을 입력하고 챌린지를 생성하거나 기존 챌린지를 수정하는 흐름을 설명합니다. 클라이언트의 작성·수정 진입점과 서버의 ChallengeController, ChallengeService, Challenge 엔티티 사이에서 요청 데이터가 어떻게 전달되고 저장되는지 다룹니다.
생성 후 챌린지를 탐색하거나 다른 사용자가 참여하는 상세 흐름은 2.1 챌린지 탐색과 참여에서 다룹니다. 인증과 CurrentUserContext가 사용자 식별자를 제공하는 구조는 3.1 인증과 세션, 공통 REST 응답과 예외 형식은 3.2 API와 응답 구조, 이미지 업로드 전처리는 3.4 이미지 업로드, 챌린지 엔티티와 저장소 전반은 3.3 도메인 데이터 모델 및 5.1 서버 도메인 계층을 참고합니다. 관리자에서 공식 챌린지를 개설·관리하는 흐름은 4.2 관리자 콘솔에서 다룹니다.
이 원문에는 ChallengeCreateScreen과 ChallengeEditScreen 내부 구현, 클라이언트의 API 훅, ChallengeRequest 전체 선언이 포함되어 있지 않습니다. 따라서 이 페이지는 확인할 수 있는 라우팅 연결, 서버 요청 계약, 서비스 처리 및 엔티티 모델을 중심으로 설명합니다.
출처: page.tsx:1-9, page.tsx:1-14, ChallengeController.java:31-35
생성 페이지의 ChallengeCreatePage는 ChallengeCreateScreen을 반환하는 얇은 래퍼입니다. /challenge/create 경로에서 실제 입력 폼과 생성 요청을 처리하는 컴포넌트는 @feature/challenge/write/screen/ChallengeCreateScreen으로 연결됩니다.
수정 페이지의 ChallengeEditPage는 동적 경로 /challenge/[id]/edit에서 params를 비동기로 읽고, 경로의 id를 ChallengeEditScreen의 id prop으로 전달합니다. 따라서 수정 화면은 URL의 챌린지 식별자를 기준으로 대상 챌린지를 구분합니다.
두 페이지 파일에는 폼 필드, 제출 이벤트, API 훅 구현이 직접 나타나지 않습니다. 인증 여부는 페이지 컴포넌트가 아니라 src/app.module/middleware/auth.ts의 미들웨어가 처리한다고 주석에 명시되어 있습니다. 토큰 쿠키가 없으면 /login으로 리다이렉트합니다.
ChallengeController는 /challenges를 공통 경로로 사용합니다. 생성 요청은 ChallengeRequest, 수정 요청은 ChallengeEditRequest로 역직렬화하고 @Valid 검증을 거칩니다. 로그인한 사용자의 회원 ID는 CurrentUserContext.getCurrentMemberId()에서 가져옵니다.
ChallengeService는 회원 조회, 공개·비공개 유형 판정, 비밀번호 암호화, 주 횟수 검증, Challenge 저장, 최초 회차 생성, 호스트 참여자 생성, 목표 저장을 순서대로 조정합니다. 데이터베이스 접근은 ChallengeRepository, ParticipantRepository, ChallengeGoalRepository, FixedChallengeGoalRepository, ChallengeFixedDayRepository 등으로 분리되어 있습니다.
출처: page.tsx:1-8, page.tsx:1-14, ChallengeController.java:31-36, ChallengeService.java:97-130
서버의 Challenge는 challenge 테이블에 매핑됩니다. 제목·설명·카테고리·기간·공개 범위·참여 방식·목표 유형·참여 제한을 한 엔티티에 저장합니다.
| 분류 | Challenge 필드 | 설명 |
|---|---|---|
| 기본 정보 | title, description, category, thumbnailImage | 챌린지 제목, 설명, 카테고리, 썸네일 이미지 키입니다. |
| 기간 | startDate, endDate | 챌린지 시작일과 종료일입니다. |
| 공개 범위 | challengeType, password | PUBLIC, PRIVATE, OFFICIAL 유형과 비공개 챌린지 비밀번호를 저장합니다. 비밀번호는 생성 시 암호화된 값으로 저장됩니다. |
| 목표 | goalType, weeklyGoalCount, weekStartDay, fixedDays | 목표 유형, 주당 인증 횟수, 주 시작 요일, 작성 허용 요일을 나타냅니다. |
| 참여 조건 | participationType, maxParticipantsCnt, allowMidJoin | 개인·단체 형태, 최대 참여 인원, 중도 참여 허용 여부입니다. |
| 기록 조건 | photoRequired, diaryTemplate, bookRequired, postEndWriteAllowed | 인증 이미지 필요 여부, 일지 양식, 참여 시 책 선택 조건, 종료 후 작성 허용 여부입니다. |
| 소유자·상태 | hostMember, deletedAt, deletedReason, createdAt | 호스트, 소프트 삭제 시각·사유, 생성 시각입니다. |
Challenge에는 participants, diaries, likes와 같은 연관 목록도 있습니다. 생성 시 입력한 목표 문자열은 ChallengeGoal 또는 FixedChallengeGoal에 별도로 저장되며, 챌린지 자체의 단일 문자열 필드에 합쳐지지 않습니다.
challengeType은 서버 설명상 PUBLIC(공개), PRIVATE(비공개), OFFICIAL(공식) 중 하나입니다. 요청에서 생략하면 생성 시 ChallengeType.PUBLIC을 기본값으로 사용합니다. PRIVATE를 선택하면 비밀번호가 비어 있지 않아야 하며, 통과한 비밀번호는 passwordEncoder.encode()로 암호화됩니다.
participationType이 GROUP인 경우 최대 참여 인원 제한을 지정할 수 있지만, 제한값이 2보다 작으면 생성이 거부됩니다. 제한이 없으면 maxParticipantCnt를 NULL로 보내도록 API 설명에 명시되어 있으며, 엔티티의 maxParticipantsCnt가 이를 저장합니다.
출처: Challenge.java:23-169, ChallengeService.java:176-203, ChallengeController.java:39-72
작성 화면의 내부 제출 함수 이름과 클라이언트 훅은 원문에서 확인되지 않지만, 서버에 도달한 생성 요청의 처리 순서는 다음과 같습니다.
ChallengeCreateScreen에서 제목, 카테고리, 설명, 시작일·종료일, 목표, 목표 유형, 참여 방식, 공개 범위와 참여 조건을 입력합니다. 썸네지를 사용하는 경우 이미지 업로드 후 반환된 키를 thumbnailImage에 넣습니다.POST /challenges로 ChallengeRequest JSON을 전송합니다. 서버의 ChallengeController.createChallenge()는 @RequestBody @Valid로 요청을 받고 CurrentUserContext.getCurrentMemberId()로 현재 회원 ID를 구합니다.ChallengeService.createChallenge(challengeRequest, memberId)가 MemberRepository.findById()로 호스트 회원을 조회합니다. 회원이 없으면 MEMBER_NOT_FOUND 예외가 발생합니다.challengeType이 생략되면 PUBLIC으로 정하고, 비공개인데 비밀번호가 없으면 PRIVATE_CHALLENGE로 거부합니다.weeklyGoalCount가 생략되면 Challenge.DAYS_IN_WEEK인 7을 사용하고, weekStartDay가 생략되면 MONDAY를 사용합니다. 이후 WeeklyChallengePolicy.validate()가 주 횟수, 시작일·종료일, 고정 요일을 검증합니다.Challenge.builder()를 구성합니다. allowMidJoin, photoRequired, bookRequired, postEndWriteAllowed, diaryTemplate 등도 이 단계에서 엔티티에 반영됩니다.challenge.replaceFixedDays()가 고정 요일 목록을 ChallengeFixedDay 연관 데이터로 교체하고, challengeRepository.save(challenge)가 챌린지를 저장합니다.challengeOccurrenceService.createFirstOccurrence(challenge)가 최초 회차를 만듭니다. 서비스 주석에 따르면 개인·1회성 챌린지를 포함해 모든 챌린지가 회차 하나를 갖습니다.ParticipantStatus.HOST로 저장합니다. 생성 즉시 호스트가 참여자가 되며, 참여자에는 최초 회차 ID와 현재 날짜의 joinedAt이 기록됩니다.goalType이 FIXED이면 목표를 FixedChallengeGoal에 챌린지 원본으로 저장한 뒤 호스트의 ChallengeGoal에도 복제합니다. 그 외에는 호스트의 ChallengeGoal만 저장합니다.challengeBookService.replaceChallengeBooks()가 함께 읽는 책 목록을 저장하고, toChallengeSummary()가 생성 결과를 ChallengeSummaryResponse로 변환합니다.ApiResponse.success(Message.CREATE_CHALLENGE, ...)로 성공 응답을 반환합니다.출처: ChallengeController.java:117-123, ChallengeService.java:176-279
수정 화면은 /challenge/[id]/edit의 id를 ChallengeEditScreen에 전달합니다. 서버 수정 API는 PATCH /challenges/{challengeId}이며, ChallengeEditRequest는 변경하려는 필드만 포함하는 부분 수정 형태입니다.
Optional 필드는 필드를 생략하면 기존 값을 유지하고, 포함하면 전달한 값으로 업데이트합니다. maxParticipantCnt는 JsonNullable<Long>을 사용하므로 미정의(undefined)와 명시적 null을 구분할 수 있습니다. API 설명에 따르면 null은 최대 인원 제한을 삭제한다는 의미입니다. thumbnailImage도 같은 방식으로 변경·유지·삭제를 구분합니다.
수정 요청에서 사용할 수 있는 필드는 다음과 같습니다.
| 요청 필드 | 자료형 | 수정 의미 |
|---|---|---|
title | Optional<String> | 제목을 변경합니다. |
thumbnailImage | Optional<String> | 이미지 키를 변경하거나 null로 삭제합니다. |
category | Optional<Category> | 카테고리를 변경합니다. |
description | Optional<String> | 설명을 변경합니다. |
diaryTemplate | Optional<String> | 일지 양식을 변경하며 null은 양식 해제입니다. 최대 길이는 50,000자입니다. |
allowMidJoin | Optional<Boolean> | 중도 참여 허용 여부를 변경합니다. |
maxParticipantCnt | JsonNullable<Long> | 최대 인원을 변경하거나 null로 제한을 제거합니다. |
goals | Optional<List<String>> | 목표 목록을 변경합니다. |
bookRequired | Optional<Boolean> | 자유 목표에서 참여자의 책 선택 필수 여부를 변경합니다. |
weeklyGoalCount | Optional<Integer> | 주당 목표 횟수입니다. 생성 후 다른 값으로 바꿀 수 없습니다. |
weekStartDay | Optional<WeekStartDay> | 주 시작 요일입니다. 생성 후 변경 불가이며 현재 지원 값은 MONDAY입니다. |
fixedDaysOfWeek | Optional<List<DayOfWeek>> | 고정 작성 요일입니다. 생성 후 다른 값으로 바꿀 수 없습니다. |
ChallengeEditScreen에서 기존 챌린지의 값을 수정합니다.PATCH /challenges/{challengeId}로 변경 필드만 전송합니다.ChallengeController.editChallenge()가 경로의 challengeId, 요청 본문 ChallengeEditRequest, CurrentUserContext의 회원 ID를 challengeService.editChallenge()에 전달합니다.updateTitle(), updateCategory(), updateDescription(), updateAllowMidJoin() 등의 메서드로 반영합니다. 고정 요일은 replaceFixedDays()로 연관 행을 통째로 교체합니다.weeklyGoalCount, weekStartDay, fixedDaysOfWeek가 생성 당시와 다른 값이면 CHALLENGE_039로 거부됩니다. 이 제한은 이미 참여한 사용자의 스트릭과 진행률이 기존 규칙과 달라지는 것을 막기 위한 서버 설명으로 기록되어 있습니다.ChallengeSummaryResponse를 Message.EDIT_CHALLENGE와 함께 반환합니다.ChallengeService.java 원문은 editChallenge() 메서드의 선언부까지만 제공되므로, 수정 시 소유자 검증, 각 필드의 실제 업데이트 순서, 목표 목록 교체 방식은 해당 메서드 전체를 추가 확인해야 합니다.
출처: page.tsx:4-14, ChallengeEditRequest.java:13-68, ChallengeController.java:125-176, Challenge.java:177-303
weeklyGoalCount는 1부터 7까지의 주당 인증 횟수입니다. 7이면 별도 enum이 아니라 Challenge.isDaily()가 참이 되는 매일형으로 처리합니다. 생략 시에도 7이므로 기존 요청은 매일형으로 동작합니다.
fixedDaysOfWeek를 지정하면 해당 요일에만 일지를 쓸 수 있습니다. Challenge.isWritableDayOfWeek()는 고정 요일이 없으면 모든 날짜를 허용하고, 고정 요일이 있으면 날짜의 요일이 목록에 포함되는지 확인합니다. 고정 요일 개수는 weeklyGoalCount와 같아야 하며, 매일형인 7회 목표에는 고정 요일을 지정할 수 없습니다.
주당 인증 횟수가 1~6인 경우 weekStartDay는 MONDAY만 허용됩니다. 시작일은 월요일이어야 하고 종료일은 마지막 주의 일요일이어야 합니다. 단, 무기한 챌린지는 종료일 제약에서 제외됩니다. 매일 인증인 7회 목표에는 이 주 단위 기간 제약을 적용하지 않으므로 20일과 같은 기간도 사용할 수 있습니다.
startDate와 endDate는 Challenge에 저장되며, 챌린지 회차가 전환될 때 mirrorOccurrencePeriod()로 현재 회차 기간을 반영합니다. 이 페이지의 생성 흐름에서는 최초 회차가 생성되지만, 반복 회차 전환 자체는 챌린지 생성 범위를 벗어납니다.
출처: ChallengeController.java:52-71, ChallengeService.java:204-243, Challenge.java:71-97, Challenge.java:244-279
| 메서드 | 엔드포인트 | 인증·입력 | 성공 응답 |
|---|---|---|---|
POST | /challenges | 인증된 회원, @Valid ChallengeRequest | ApiResponse<ChallengeSummaryResponse> |
PATCH | /challenges/{challengeId} | 인증된 회원, @Valid ChallengeEditRequest | ApiResponse<ChallengeSummaryResponse> |
생성 API는 200 성공, 401 인증되지 않은 접근, 404 회원을 찾을 수 없음, 400 주 횟수제 설정 오류를 문서화합니다. 주 횟수제 오류 예시는 목표 횟수 범위 밖, 시작일이 주 시작 요일과 불일치, 종료일이 주 마지막 날과 불일치, 고정 요일 수 불일치, 매일 인증과 고정 요일 동시 사용입니다.
서비스 구현에서 확인되는 추가 생성 거부 조건은 GROUP인데 최대 인원이 2 미만인 경우, PRIVATE인데 비밀번호가 없거나 공백인 경우입니다. 수정 API는 401, 404 응답을 문서화하며, 기록 빈도 설정을 다른 값으로 수정할 때 CHALLENGE_039가 사용됩니다.
이미지 자체를 생성·수정 API가 업로드하는 것은 아닙니다. API 설명은 먼저 /image/presigned-url을 통해 썸네일을 업로드한 뒤 반환된 키를 thumbnailImage에 담아 보내도록 안내합니다. 따라서 작성 폼은 이미지 업로드 결과와 챌린지 저장 요청을 별도 단계로 연결해야 합니다.
출처: ChallengeController.java:39-176, ChallengeService.java:183-218
생성 화면은 /challenge/create에서 ChallengeCreatePage를 통해 ChallengeCreateScreen을 표시합니다. 페이지 파일에는 별도의 id가 없으므로 생성 대상 챌린지를 미리 지정하지 않습니다. 수정 화면은 /challenge/{id}/edit에서 URL의 id를 ChallengeEditScreen으로 전달하므로, 화면 구현은 이 값을 사용해 대상 챌린지를 조회하고 수정 요청의 {challengeId}로 연결해야 합니다. 다만 조회 훅과 폼 초기화 코드 자체는 제공된 원문에서 확인되지 않습니다.
작성 폼의 서버 연결은 다음 데이터 흐름으로 정리할 수 있습니다.
ChallengeCreateScreen이 사용자의 기본 정보·기간·목표·공개 범위·참여 조건을 수집합니다./image/presigned-url 업로드 결과의 키를 thumbnailImage에 포함합니다.POST /challenges가 서버의 ChallengeController.createChallenge()로 연결됩니다.ChallengeService가 챌린지 본체, 고정 요일, 최초 회차, 호스트 참여자, 목표와 책을 저장합니다.ChallengeSummaryResponse를 반환하면 화면은 생성 완료 결과를 사용합니다.수정은 다음과 같이 연결됩니다.
ChallengeEditPage가 params.id를 해석합니다.ChallengeEditScreen id={id}가 해당 챌린지의 편집 화면을 구성합니다.PATCH /challenges/{challengeId} 요청에 넣습니다.ChallengeSummaryResponse입니다.출처: page.tsx:1-8, page.tsx:4-14, ChallengeController.java:117-176
ChallengeCreateScreen과 ChallengeEditScreen은 실제로 어떤 폼 컴포넌트와 훅을 사용하는지 확인이 필요합니다. 생성 시 POST /challenges, 수정 시 PATCH /challenges/{challengeId}를 호출하는 클라이언트 코드의 경로와 함수명은 무엇입니까?ChallengeRequest 전체 필드와 제약 조건을 확인해야 합니다. 특히 title, startDate, endDate, goals, goalType, participationType, photoRequired, books의 필수 여부와 기본값은 무엇입니까?ChallengeService.editChallenge() 전체 구현을 확인해야 합니다. 수정 권한 검사, 시작 후 목표·책 수정 제한, 목표 목록 삭제·재생성 방식, password와 challengeType 수정 가능 여부는 어떻게 처리합니까?visibleFrom, 반복 설정인 cycleWeeks·recruitDays를 사용자에게 노출하는지 확인이 필요합니다. 현재 제공된 ChallengeRequest 선언과 폼 구현이 없어 화면 지원 여부를 판단할 수 없습니다.ChallengeSummaryResponse의 실제 응답 필드와 생성·수정 성공 후 클라이언트의 이동 경로는 무엇입니까?@Valid 실패와 CustomException이 최종적으로 어떤 ErrorResponse JSON 구조와 HTTP 상태 코드로 변환되는지는 3.2 API와 응답 구조의 관련 서버 코드 확인이 필요합니다.출처: page.tsx:1-9, page.tsx:1-14, ChallengeService.java:176-283