AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@23a5ca8 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.
관련 소스 파일
이 페이지는 1D1S-client에서 사용자가 챌린지를 목록으로 탐색하고, 특정 챌린지의 공개 정보와 상세 정보를 확인하는 화면 흐름을 설명합니다. 또한 1D1S-server-v2의 챌린지 조회 API, 참여자 목록 조회, 챌린지와 회차(ChallengeOccurrence)의 관계, 참여 상태와 자동 참여 처리 구조를 함께 다룹니다.
이 원문에서 확인되는 범위는 다음과 같습니다.
/challenge 챌린지 목록 화면과 카테고리별 초기 데이터 주입/challenge/[id] 챌린지 상세 화면과 공개 미리보기GET /challenges/{challengeId}/preview, GET /challenges/{challengeId}, GET /challenges/{challengeId}/participants APIChallenge, ChallengeOccurrence, Participant, ChallengeAutoRejoin을 중심으로 한 데이터 흐름다만 참여·나가기 버튼이 실제로 어떤 클라이언트 훅을 사용하고, 참여 신청 또는 자진 탈퇴를 어떤 HTTP 엔드포인트로 호출하는지는 제공된 원문에 전체 코드가 포함되어 있지 않습니다. 챌린지 생성·수정은 이 페이지의 주 흐름이 아니므로 2.2 챌린지 만들기에서 다루며, 서버 계층의 전체 패키지 구조는 5.1 서버 도메인 계층, 회차·참여자 용어의 종합 정리는 7.1 핵심 용어집을 참조합니다.
출처: 1D1S-client/src/app/challenge/(board)/page.tsx:1-42, 1D1S-client/src/app/challenge/[id]/page.tsx:1-107, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:178-266
ChallengeListPage는 /challenge 라우트의 서버 컴포넌트입니다. 먼저 fetchPublicChallengeSections()를 호출해 공개 챌린지 섹션 데이터를 가져옵니다. 반환된 항목을 sectionId 기준의 Map으로 만든 뒤, CHALLENGE_SECTIONS의 정의 순서에 맞춰 initialSections를 구성합니다.
initialSections는 각 섹션의 정의와 해당 섹션의 챌린지 항목을 함께 보유합니다. 서버에 시드 데이터가 없는 섹션은 제거합니다. 주석에 따르면 화면은 시드가 없는 줄을 아직 로딩되지 않은 줄로 간주하므로, 빈 섹션을 임의로 채우지 않는 것이 중요합니다.
구성한 데이터는 ChallengeBoardScreen의 initialSections prop으로 전달됩니다. 렌더링 중 서버 조회가 지연되면 ChallengeCategorySectionsSkeleton이 Suspense의 대체 화면으로 표시됩니다.
ChallengeDetail은 URL의 id를 받아 fetchPublicChallenge(id)로 공개 챌린지 정보를 조회합니다. 조회 성공 시 buildChallengeJsonLd와 JsonLd를 이용해 제목, 설명, 썸네일, 시작일, 종료일, 참여자 수를 구조화 데이터로 제공합니다.
실제 상세 화면은 ChallengeDetailScreen이 담당합니다. 서버에서 읽은 공개 데이터를 initialChallenge로 전달하므로, 인증이 필요한 React Query 조회가 완료되기 전에도 제목·설명·기간 같은 공개 정보가 화면에 표시될 수 있습니다. 대기 중에는 공개 데이터가 있으면 ChallengeDetailPreview, 없으면 ChallengeDetailSkeleton을 사용합니다.
ChallengeController는 /challenges를 공통 경로로 사용하는 REST 컨트롤러입니다. ChallengeService가 생성·수정·상세·통계·참여자 조회를 수행하며, 현재 로그인한 회원은 CurrentUserContext에서 읽습니다. 로그인하지 않은 사용자를 허용하는 조회에서는 getCurrentMemberIdOrNull()을 사용합니다.
출처: 1D1S-client/src/app/challenge/(board)/page.tsx:28-40, 1D1S-client/src/app/challenge/[id]/page.tsx:70-104, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:31-37
챌린지 자체와 특정 회차의 활동 데이터가 분리되어 있습니다. Challenge는 시리즈의 공통 설정과 공개 정보를 보유하고, ChallengeOccurrence는 그 시리즈의 한 회차를 나타냅니다. 참여와 일지는 회차에 귀속됩니다.
| 엔티티·테이블 | 주요 필드 | 역할 |
|---|---|---|
Challenge / challenge | id, title, category, startDate, endDate, challengeType, goalType, participationType | 챌린지 시리즈의 기본 정보와 참여 정책을 보유합니다. |
Challenge / challenge | cycleWeeks, recruitDays, allowMidJoin, maxParticipantsCnt | 반복 회차와 모집·중간 참여·인원 제한을 표현합니다. |
Challenge / challenge | weeklyGoalCount, weekStartDay, fixedDays | 주간 인증 횟수와 작성 가능 요일을 표현합니다. |
Challenge / challenge | visibleFrom, deletedAt, deletedReason | 공식 챌린지의 예약 노출과 소프트 삭제를 표현합니다. |
ChallengeOccurrence / challenge_occurrence | occurrenceNo, startDate, endDate, recruitStartDate, recruitEndDate | 챌린지의 특정 회차 기간과 모집 창을 보유합니다. |
ChallengeOccurrence / challenge_occurrence | status, startNotifiedAt, createdAt | 회차 상태와 시작 알림 처리 상태를 보유합니다. |
Participant | member, 회차 참조, status | 특정 회원이 특정 회차에 참여하는 상태를 나타냅니다. |
ChallengeAutoRejoin / challenge_auto_rejoin | challenge, member, createdAt, disabledAt, disabledReason | 사용자×챌린지 단위의 다음 회차 자동 참여 설정을 저장합니다. |
ChallengeFixedDay / challenge_fixed_day | challenge, dayOfWeek | 요일 고정 챌린지의 작성 가능 요일을 저장합니다. |
ChallengeGoal / challenge_goal | content, participant | 참여자별 목표를 저장하고 일지 목표와 연결합니다. |
ChallengeOccurrence.first()는 첫 회차 번호를 FIRST_OCCURRENCE_NO, 즉 1로 설정하고, 챌린지의 startDate와 endDate를 회차에 그대로 복사합니다. 회차의 기간은 스트릭 보호 구간과 연결되므로, 회차가 챌린지 기간을 임의로 보정하지 않습니다.
Challenge의 cycleWeeks가 null이면 반복하지 않는 챌린지이며, 값이 있으면 isRecurring()이 true를 반환합니다. weeklyGoalCount는 1~7의 주간 인증 횟수이고 7이면 isDaily()가 true가 됩니다. fixedDays가 비어 있으면 모든 요일에 작성할 수 있고, 값이 있으면 isWritableDayOfWeek()가 해당 날짜의 작성 가능 여부를 판정합니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/Challenge.java:23-175, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeOccurrence.java:14-97, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeFixedDay.java:11-38, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeGoal.java:11-30
/challenge에 진입합니다.ChallengeListPage가 서버에서 fetchPublicChallengeSections()를 호출합니다.sectionId와 items의 Map으로 변환합니다.CHALLENGE_SECTIONS의 순서를 기준으로 서버에서 제공된 섹션만 initialSections에 포함합니다.ChallengeBoardScreen이 initialSections를 받아 카테고리별 챌린지 레일을 표시합니다.ChallengeCategorySectionsSkeleton이 표시됩니다.제공된 목록 페이지 원문에는 fetchPublicChallengeSections()의 내부 HTTP URL이나 목록 API의 서버 컨트롤러 메서드가 포함되어 있지 않습니다. 따라서 이 페이지에서 실제 목록 엔드포인트의 경로, 페이지 크기, 정렬 방식은 확인할 수 없습니다.
/challenge/{id}로 이동합니다.ChallengeDetail이 params에서 문자열 id를 읽습니다.fetchPublicChallenge(id)를 호출해 공개 정보를 읽습니다.JsonLd를 생성합니다.ChallengeDetailScreen에 id와 initialChallenge를 전달합니다.useChallengeDetail이 가져옵니다.ChallengeDetailPreview가 제목·설명·기간 등의 공개 정보를 보여줍니다.ChallengeDetailSkeleton이 사용되고, 메타데이터는 기본값으로 폴백합니다.서버의 GET /challenges/{challengeId}는 비공개 챌린지에서 참여자 접근을 요구합니다. 비공개 챌린지에 접근할 수 없을 때 서버 설명은 POST /challenges/{challengeId}/verify-password를 호출해 참여 절차를 진행하도록 안내합니다. 다만 비밀번호 확인 요청의 DTO와 실제 참여 API 구현은 제공된 범위에 없습니다.
출처: 1D1S-client/src/app/challenge/(board)/page.tsx:28-40, 1D1S-client/src/app/challenge/[id]/page.tsx:44-68, 1D1S-client/src/app/challenge/[id]/page.tsx:70-104, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:178-266
ChallengeOccurrence는 OccurrenceStatus를 저장하며, 모집 기간이 있는 회차는 활동 시작 전에 만들어질 수 있습니다. 이 경우 초기 상태는 SCHEDULED일 수 있고, open()이 호출되면 OPEN으로 전환됩니다. CLOSED 회차는 다시 열리지 않습니다.
| 상태·판정 | 의미 |
|---|---|
SCHEDULED | 회차가 예정되어 있지만 활동 시작 전인 상태입니다. 모집 창이 있으면 모집 신청을 받을 수 있습니다. |
OPEN | 회차 활동이 진행 중인 상태입니다. |
CLOSED | 회차가 종료되어 더 이상 참여할 수 없는 상태입니다. |
isJoinableOn(today) | 현재 날짜 기준으로 해당 회차에 참여 가능한지 판정합니다. |
isRecruitingOn(date) | recruitStartDate와 recruitEndDate 사이인지 판정합니다. |
isJoinableOn()은 먼저 OccurrencePhase를 계산합니다. 단계가 CLOSED이면 참여할 수 없고, SCHEDULED이더라도 모집 창이 없으면 참여를 막지 않습니다. 반대로 모집 창이 존재하는 예정 회차는 그 창 안에서만 참여할 수 있습니다.
참여자 목록 API 설명에서 확인되는 상태는 HOST, PARTICIPANT, PENDING입니다. 호스트 또는 관리자는 참여 신청 대기자까지 조회할 수 있지만, 일반 사용자와 비로그인 사용자는 HOST, PARTICIPANT만 조회합니다.
강퇴 흐름에서는 ParticipantStatus.KICKED도 사용됩니다. ChallengeKickService.kick()은 현재 회차의 대상 참여자를 찾아 PARTICIPANT인지 확인한 뒤 KICKED로 전환합니다. 신청 대기자(PENDING)는 강퇴 대상이 아니라 별도의 거절 처리 대상이라고 주석에 명시되어 있습니다.
ParticipantStatus | 원문에서 확인되는 의미 |
|---|---|
HOST | 챌린지의 호스트입니다. 참여자 목록에 포함됩니다. |
PARTICIPANT | 현재 참여 중인 회원입니다. |
PENDING | 참여 신청 대기자입니다. 호스트·관리자 조회에서만 포함될 수 있습니다. |
KICKED | 호스트 또는 관리자가 내보낸 참여자입니다. |
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeOccurrence.java:77-82, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/entity/ChallengeOccurrence.java:105-165, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:284-321, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeKickService.java:75-97
제공된 컨트롤러 원문에서 확인되는 챌린지 관련 엔드포인트는 다음과 같습니다.
| 메서드 | 경로 | 인증 | 응답·역할 |
|---|---|---|---|
GET | /challenges/{challengeId}/preview | 불필요 | ChallengePreviewResponse를 반환합니다. 비공개 챌린지 참여 전에 필요한 최소 정보이며 goalType 확인에 사용됩니다. |
GET | /challenges/{challengeId} | 선택적 조회 문맥 | ChallengeResponse를 반환합니다. 공개 챌린지는 접근 가능하고, 비공개 챌린지는 참여자만 접근할 수 있습니다. |
GET | /challenges/{challengeId}/statistics | 상세 조회 정책과 동일 | ChallengeStatisticsResponse를 반환합니다. occurrenceId를 생략하면 현재 회차를 사용합니다. |
GET | /challenges/{challengeId}/participants | 조회 권한에 따라 결과 제한 | OffsetPagination<ParticipantResponse>를 반환합니다. status 다중 선택과 sort를 지원합니다. |
POST | /challenges | 필요 | ChallengeSummaryResponse를 반환하며 챌린지를 생성합니다. |
PATCH | /challenges/{challengeId} | 필요 | 챌린지를 수정합니다. |
참여자 목록 조회의 status 파라미터는 HOST, PARTICIPANT, PENDING을 받을 수 있으며 status=HOST&status=PARTICIPANT처럼 여러 번 전달할 수 있습니다. sort는 PARTICIPATION 또는 RANK이며 기본값은 PARTICIPATION입니다. PENDING은 호스트와 관리자만 조회할 수 있고, 권한 밖의 상태 필터는 조용히 제외됩니다.
통계 API는 occurrenceId를 선택적으로 받습니다. 이 값이 없으면 현재 회차를 사용하므로, 회차가 여러 개인 반복 챌린지에서 특정 회차의 통계를 명시적으로 조회할 수 있습니다.
참여 자체에 대해서는 컨트롤러 앞부분에서 verify-password가 설명에 언급되지만, 실제 참여 신청 엔드포인트는 제공된 줄 범위에 나타나지 않습니다. 서버 내부에서는 ChallengeService.joinOccurrence()가 자동 참여 등록 경로로 사용되며, 이것이 외부 HTTP API의 직접적인 경로인지는 확인되지 않습니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:178-210, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:212-281, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:284-321, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeAutoRejoinWriter.java:62-79
참여 API의 전체 컨트롤러 메서드는 원문에 없지만, 자동 참여 경로는 참여 규칙을 재사용합니다.
ChallengeAutoRejoinRollover.processOpenedToday()가 오늘 열린 회차를 찾습니다.ChallengeAutoRejoin 행을 조회합니다.Participant와 일지 작성 날짜를 회원별로 모읍니다.AutoRejoinActivityPolicy.isLowActivity()로 활동 저조 여부를 판단합니다.ChallengeAutoRejoinWriter.disableForLowActivity()로 옵션을 끄고 알림을 보냅니다.ChallengeAutoRejoinWriter.rejoin()이 직전 참여자의 목표와 책을 복사합니다.rejoin()은 ChallengeService.joinOccurrence()를 호출해 일반 참여와 같은 규칙으로 다음 회차 참여를 처리합니다.ChallengeAutoRejoinService.enable()은 사용자가 해당 챌린지의 활성 참여자였는지 확인한 뒤 옵션을 켭니다. 게스트 또는 옵션이 없는 사용자의 상태는 AutoRejoinResponse.off()로 표현됩니다. 자동 참여는 삭제되지 않은 반복 공식 챌린지에서만 사용 가능하며, 참여하지 않은 사용자가 먼저 옵션을 켜는 것은 허용되지 않습니다.
자진 탈퇴 API의 구현은 제공되지 않았지만, 호스트가 다른 참여자를 내보내는 흐름은 확인됩니다.
ChallengeKickService.kick()을 호출합니다.HOST이거나 이미 PARTICIPANT가 아니면 예외를 발생시킵니다.KICKED로 변경합니다.ChatRoomService.leaveRoom()으로 채팅방에서도 내보냅니다.ChallengeKick에 챌린지, 강퇴 회원, 실행 호스트, 사유를 저장합니다.NotificationService.notifyChallengeKicked()로 알림을 보냅니다.이 흐름은 참여 자격을 제거하지만 일지를 삭제하지는 않습니다. 강퇴된 회원의 일지는 챌린지 일지 목록과 통계에 남도록 처리됩니다. 강퇴 사유는 HtmlSanitizer로 정화한 뒤 공백 여부를 다시 검사하며, 정화 후 내용이 없으면 CHALLENGE_KICK_REASON_REQUIRED가 발생합니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeAutoRejoinService.java:41-92, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeAutoRejoinRollover.java:57-169, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeKickService.java:50-126
챌린지 상세 조회는 다음과 같은 접근 결과를 구분합니다.
| 상황 | 처리 |
|---|---|
| 공개 챌린지 조회 | GET /challenges/{challengeId}가 상세를 반환합니다. |
| 비공개 챌린지에 권한 없이 접근 | 403으로 거부되며, 비밀번호 확인 절차가 안내됩니다. |
| 인증되지 않은 상세 접근 | API 명세상 401 응답이 정의되어 있습니다. |
| 챌린지 또는 회원이 없음 | 404 응답이 정의되어 있습니다. |
| 공개 미리보기 대상 없음 | GET /challenges/{challengeId}/preview가 404를 반환합니다. |
| 존재하지 않는 참여자 대상 강퇴 | PARTICIPANT_NOT_FOUND가 발생합니다. |
| 호스트가 아닌 사용자의 강퇴 | CHALLENGE_KICK_NOT_HOST가 발생합니다. |
| 자기 자신 또는 호스트 강퇴 | 각각 CHALLENGE_KICK_SELF, CHALLENGE_KICK_HOST가 발생합니다. |
| 이미 참여 중이 아닌 대상 강퇴 | CHALLENGE_KICK_NOT_PARTICIPANT가 발생합니다. |
비활성 챌린지는 ChallengeAutoCloseScheduler가 매일 한국 시간 04:00에 실행하는 closeInactiveChallenges()를 통해 정리됩니다. ChallengeAutoCloseService는 14일간 활동이 없는 진행 중 챌린지를 조회해 종료하고, 같은 대상의 ChallengeOccurrence도 closeByChallengeIds()로 함께 닫습니다. 챌린지 기간과 회차 기간을 함께 닫는 것은 두 기간이 어긋나지 않도록 하기 위한 처리입니다.
관리자 목록에서는 AdminChallengeService가 ONGOING, UPCOMING, ENDED 상태 필터를 정규화합니다. 정렬은 latest 또는 likes만 허용하며, 소프트 삭제된 챌린지도 관리자에게는 삭제 여부·시각·사유와 함께 표시됩니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:223-266, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeKickService.java:68-85, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeAutoCloseScheduler.java:11-19, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeAutoCloseService.java:15-55, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/AdminChallengeService.java:40-81
ChallengeBoardScreen이 초기 목록 이후 추가로 호출하는 실제 목록 API 경로와 React Query 훅 이름은 무엇입니까?ChallengeDetailScreen 내부의 useChallengeDetail 구현과 상세 데이터의 실제 요청 경로는 어디에 있습니까?POST /challenges/{challengeId}/verify-password 요청 이후 참여가 자동으로 완료되는지, 아니면 별도의 참여 API를 다시 호출하는지 확인이 필요합니다.ParticipantStatus 전이, 채팅방·일지 처리 순서는 무엇입니까?ChallengeService.joinOccurrence()가 PENDING, PARTICIPANT, HOST를 어떤 챌린지 유형별로 생성하는지 확인이 필요합니다.ChallengeOccurrenceService.currentOrCreate()가 현재 회차를 선택하거나 생성하는 기준일과 모집 기간 처리 규칙은 무엇입니까?RANK 정렬이 어떤 점수·스트릭·완료율을 기준으로 하는지 제공된 원문만으로는 확인할 수 없습니다.ChallengeStatisticsResponse의 참여율과 일지 추이에서 참여자 수·회차·탈퇴자·강퇴자의 포함 기준을 확인할 필요가 있습니다.출처: 1D1S-client/src/app/challenge/[id]/page.tsx:44-59, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/controller/ChallengeController.java:178-222, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeAutoRejoinService.java:20-31, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/challenge/service/ChallengeKickService.java:27-35