diff --git a/docs/check-in.md b/docs/check-in.md index 4690e5e..07aad6e 100644 --- a/docs/check-in.md +++ b/docs/check-in.md @@ -20,6 +20,16 @@ DB·노션 접근이나 데이터 변경은 없으며 실제 ACTIVE·시간 조 샘플 조회 후 실제 체크인 API를 호출하면 운영 행사에 제출되므로 화면 테스트에 연결하지 않는다. 실서비스 화면에서는 `/events/active`를 사용해야 한다. +### 화면 연동용 타임테이블 + +`GET /dev/events/active/timetable`는 세미나 예시 일정 6개를 고정 반환한다. +인증·행사 ACTIVE 여부·체크인 시작 시각과 관계없이 `200 / SUCCESS`로 응답하며 DB·노션에 접근하지 않는다. +응답은 아래 실제 타임테이블 조회 예시와 같은 `data` 배열이며 `timestamp`만 응답 시각에 따라 달라진다. +`endTime: null`은 종료 미정으로, 프론트에서 `18:00 ~`로 표시한다. +`https://checkin.ject.kr`의 CORS 조회를 허용하며 인증정보 포함 옵션은 사용하지 않는다. +기존 행사 샘플(`/dev/events/active`)은 온보딩 예시이며 이 API는 별도의 세미나 예시다. +운영 DB 일정이 바뀌어도 샘플은 자동 갱신되지 않는다. 실서비스 화면은 `/events/active/timetable`을 사용한다. + ## 행사 타임테이블 조회 관련 이슈: [#19](https://github.com/JECT-Study/JECT-Official-Event-QR-CheckIn-Server/issues/19) diff --git a/src/main/java/ject/official_qr_checkin_server/domain/event/controller/PreviewEventController.java b/src/main/java/ject/official_qr_checkin_server/domain/event/controller/PreviewEventController.java index 949c6e1..cae3fdc 100644 --- a/src/main/java/ject/official_qr_checkin_server/domain/event/controller/PreviewEventController.java +++ b/src/main/java/ject/official_qr_checkin_server/domain/event/controller/PreviewEventController.java @@ -2,7 +2,9 @@ import io.swagger.v3.oas.annotations.Operation; import java.time.LocalDateTime; +import java.util.List; import ject.official_qr_checkin_server.domain.event.dto.ActiveEventResponse; +import ject.official_qr_checkin_server.domain.event.dto.TimetableItemResponse; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -10,10 +12,26 @@ @RestController public class PreviewEventController { + private static final List TIMETABLE_SAMPLE = List.of( + new TimetableItemResponse("13:30", "14:00", "체크인"), + new TimetableItemResponse("14:10", "15:00", "젝트 사용 툴 세미나"), + new TimetableItemResponse("15:10", "15:30", "쉬는시간"), + new TimetableItemResponse("15:30", "17:30", "집중 협업 시간"), + new TimetableItemResponse("17:30", "18:00", "공지 & 만족도 조사 & 파트별 단체사진"), + new TimetableItemResponse("18:00", null, "퇴장") + ); + @Operation(summary = "화면 연동용 행사 조회", description = "인증 없이 고정 샘플을 반환합니다. DB·노션에 접근하지 않으며 실제 체크인과 연결되지 않습니다.") @GetMapping("/dev/events/active") public ActiveEventResponse getPreviewEvent() { return new ActiveEventResponse("[테스트] 온보딩", LocalDateTime.of(2026, 9, 19, 12, 30)); } + + @Operation(summary = "화면 연동용 타임테이블 조회", + description = "행사 상태·시각·인증과 관계없이 세미나 예시 6개를 반환합니다. DB·노션에 접근하지 않습니다. 실제 조회와 동일한 응답 형식이며 종료 미정은 null입니다.") + @GetMapping("/dev/events/active/timetable") + public List getPreviewTimetable() { + return TIMETABLE_SAMPLE; + } } diff --git a/src/test/java/ject/official_qr_checkin_server/domain/event/EventTimetableIntegrationTests.java b/src/test/java/ject/official_qr_checkin_server/domain/event/EventTimetableIntegrationTests.java index 1828892..43f4756 100644 --- a/src/test/java/ject/official_qr_checkin_server/domain/event/EventTimetableIntegrationTests.java +++ b/src/test/java/ject/official_qr_checkin_server/domain/event/EventTimetableIntegrationTests.java @@ -7,6 +7,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -52,6 +53,53 @@ void setTime() { at(START); } + @ParameterizedTest + @ValueSource(longs = {-86400, 0, 86400}) + void previewReturnsFixedSampleRegardlessOfTimeWithoutEvents(long seconds) throws Exception { + at(START.plusSeconds(seconds)); + mvc.perform(get("/dev/events/active/timetable").header("Origin", ORIGIN)) + .andExpect(status().isOk()) + .andExpect(header().string("Access-Control-Allow-Origin", ORIGIN)) + .andExpect(jsonPath("$.data", hasSize(6))) + .andExpect(jsonPath("$.timestamp").isNotEmpty()) + .andExpect(jsonPath("$.data[0].id").doesNotExist()) + .andExpect(jsonPath("$.data[0].eventId").doesNotExist()) + .andExpect(content().json(""" + {"status":"SUCCESS","data":[ + {"startTime":"13:30","endTime":"14:00","schedule":"체크인"}, + {"startTime":"14:10","endTime":"15:00","schedule":"젝트 사용 툴 세미나"}, + {"startTime":"15:10","endTime":"15:30","schedule":"쉬는시간"}, + {"startTime":"15:30","endTime":"17:30","schedule":"집중 협업 시간"}, + {"startTime":"17:30","endTime":"18:00","schedule":"공지 & 만족도 조사 & 파트별 단체사진"}, + {"startTime":"18:00","endTime":null,"schedule":"퇴장"} + ]} + """)); + assertThat(events.count()).isZero(); + assertThat(timetable.count()).isZero(); + mvc.perform(get(PATH)).andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value("EVENT-003")); + } + + @Test + void previewIgnoresInactiveEventAndRealTimetable() throws Exception { + item(event(EventStatus.INACTIVE), "09:00", "10:00", "실제 DB 일정"); + mvc.perform(get("/dev/events/active/timetable")) + .andExpect(status().isOk()).andExpect(jsonPath("$.data", hasSize(6))) + .andExpect(jsonPath("$.data[0].schedule").value("체크인")); + assertThat(timetable.count()).isEqualTo(1); + mvc.perform(get(PATH)).andExpect(status().isConflict()) + .andExpect(jsonPath("$.status").value("CHECKIN-001")); + } + + @Test + void previewAllowsCorsPreflightWithoutCredentials() throws Exception { + mvc.perform(options("/dev/events/active/timetable").header("Origin", ORIGIN) + .header("Access-Control-Request-Method", "GET")) + .andExpect(status().isOk()) + .andExpect(header().string("Access-Control-Allow-Origin", ORIGIN)) + .andExpect(header().doesNotExist("Access-Control-Allow-Credentials")); + } + @Test void returnsSeminarInTimeOrderWithoutIdsOrAuthentication() throws Exception { Event event = event(EventStatus.ACTIVE); @@ -143,7 +191,8 @@ void permitsCorsPreflight() throws Exception { @Test void publishesSwaggerOperation() throws Exception { mvc.perform(get("/v3/api-docs/check-in-api")).andExpect(status().isOk()) - .andExpect(jsonPath("$.paths['/events/active/timetable'].get").exists()); + .andExpect(jsonPath("$.paths['/events/active/timetable'].get").exists()) + .andExpect(jsonPath("$.paths['/dev/events/active/timetable'].get").exists()); } private Event event(EventStatus status) {