Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/check-in.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,36 @@

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;

/** 화면 연동 전용. 실제 행사 상태 및 체크인 처리와 연결하지 않는다. */
@RestController
public class PreviewEventController {

private static final List<TimetableItemResponse> 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<TimetableItemResponse> getPreviewTimetable() {
return TIMETABLE_SAMPLE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
Loading