Test #462
@@ -0,0 +1,152 @@
|
||||
# 일반용 이벤트 상세 조회 구현 계획
|
||||
|
||||
**Goal:** 일반 사용자가 기존 노출 정책을 통과한 이벤트 한 건을 v2 API로 조회한다.
|
||||
**Architecture:** 기존 EventRepository의 ID 조회와 EventItem을 재사용한다. 신규 controller와 read-only service는 v2 아래 배치하고 기존 controller/service는 변경하지 않는다.
|
||||
**Tech Stack:** Kotlin 1.6.21, Java 17, Spring Boot 2.7.14, JPA, JUnit 5, H2.
|
||||
**Spec:** [prd.md](prd.md)
|
||||
**실행:** subagent-driven-development; 커밋/푸시는 별도 승인 없이 하지 않는다.
|
||||
|
||||
## 현재 상태
|
||||
- 상태: 구현 및 검증 완료 (운영 배포 제외)
|
||||
- 현재 Phase: 1
|
||||
- 활성/다음 Goal: 없음 (P1-T1, P1-GATE 완료)
|
||||
- 순서: P1-T1 → P1-GATE. 동시에 하나만 실행한다.
|
||||
|
||||
## Global Constraints
|
||||
- 승인된 GET `/api/v2/events/{id}`만 추가. 관리자 상세/legacy 변경/DB·의존성 변경 제외.
|
||||
- 기존 성인/언어 정책 및 이미지 변환을 그대로 유지한다.
|
||||
- 모든 신규 API/하위 구현은 v2 패키지에 둔다. 불필요한 추상화/port는 만들지 않는다.
|
||||
- 기존 테스트 삭제·완화, 비밀값 기록 금지. 소유 임시 자원만 정리한다.
|
||||
|
||||
## Review Focus
|
||||
- 인증된 성인에게 `isAdult=false` 이벤트를 추가로 노출하지 않는지 확인한다.
|
||||
- 관리자도 언어·기간·활성 조건을 지키는지 확인한다.
|
||||
- 익명 허용이 쓰기/collection/추가 하위 경로로 확장되지 않는지 확인한다.
|
||||
- null 이미지, 외부 HTTPS 이미지, 외부 link를 변경하거나 관리 엔티티를 수정하지 않는지 확인한다.
|
||||
- 미존재와 비노출 오류가 동일하고 상세 정보를 누출하지 않는지 확인한다.
|
||||
|
||||
### Phase 1: v2 단건 조회
|
||||
|
||||
- [x] **Task 1.1: 일반 이벤트 상세 조회와 보안 회귀 검증**
|
||||
|
||||
**Goal P1-T1:** 기존 목록과 같은 노출 정책을 적용한 단일 EventItem을 반환한다.
|
||||
- 시작 조건: PRD 확정, 기존 event/admin focused test 성공.
|
||||
- 완료 증거: RED/GREEN 기록, S1~S3 테스트 성공, 실제 HTTP 응답 증거.
|
||||
- 범위 밖: 전체 빌드/독립 리뷰는 P1-GATE; legacy 리팩터링과 추가 기능.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/adapter/in/web/EventDetailController.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/application/EventDetailService.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/event/EventDetailControllerIntegrationTest.kt`
|
||||
- 필요한 서비스 경계 테스트는 같은 v2/api/event 패키지에 통합 테스트로 추가한다.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `EventRepository`, `AuthRepository.getAuthIdByMemberId`, `MemberContentPreferenceService.resolveCountryCode`, `EventItem`.
|
||||
- Produces: `EventDetailService.getEvent(id: Long, member: Member?): EventItem`.
|
||||
- Controller: `/api/v2/events`, `@GetMapping("/{id}")`, nullable `@AuthenticationPrincipal`.
|
||||
- Security: GET `/api/v2/events/*`만 permitAll; `/**` 금지.
|
||||
|
||||
- [x] **RED:** 먼저 실제 HTTP 정상 요청 테스트를 작성하고, 조회 불가·성인 3상태·언어·관리자·응답 변환·인접 보안 시나리오를 추가한다.
|
||||
- [x] **RED 확인:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.event.*' --console=plain`에서 미구현 route/security로 정상 요청이 실패함을 기록한다.
|
||||
- [x] **GREEN:** ID 단건 로드, 양수 검사, 활성/현재 시각/언어/성인 필터, EventItem 변환, 좁은 보안 허용을 구현한다.
|
||||
- [x] **GREEN 확인:** 동일 focused test로 모든 시나리오 통과를 확인한다.
|
||||
- [x] **REFACTOR:** 새 중복만 필요 시 정리하고 기존 event/admin 회귀를 다시 실행한다.
|
||||
|
||||
**검증 세부:**
|
||||
- 실제 DB에 저장한 KO/JA 및 nullable adult 필드를 비교한다. 인증 일반 회원의 auth 기록은 실제 DB로 준비한다.
|
||||
- `isAdult` null/false/true 각각을 익명·미인증·인증·관리자에 대해 기존 `/event`와 비교한다.
|
||||
- 미래/종료 테스트는 현재 시각에서 충분히 떨어진 값을 사용해 flake를 피한다. 기간 조건의 양끝 포함은 코드 및 가능한 기존 seam으로 검증한다.
|
||||
- 이미지/링크 null, 상대 경로, HTTPS, 기존 http 문자열 보정 및 모든 응답 필드를 확인한다.
|
||||
- missing/zero/negative/nonnumeric/overflow ID, non-GET, 하위 추가 경로, legacy 목록/팝업/admin 접근을 검증한다.
|
||||
- RANDOM_PORT/TestRestTemplate의 상태/본문을 test output으로 남긴다. 인증은 동일 컨텍스트 MockMvc로도 검증 가능하다.
|
||||
|
||||
### Phase 1 Gate
|
||||
- [x] **Goal P1-GATE: 동작·회귀·빌드·리뷰·정리 완료**
|
||||
- 시작 조건: P1-T1 완료.
|
||||
- 완료 증거: 아래 명령과 HTTP 증거, 독립 reviewer 승인, 자원 정리 확인.
|
||||
- 범위 밖: 기존 전체 테스트 실패/린트 위반의 무관한 수정.
|
||||
- TDD 예외 사유: 읽기 전용 완료 검증이며 테스트 추가가 아닌 실행·리뷰 단계다.
|
||||
|
||||
```bash
|
||||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.event.*' --tests 'kr.co.vividnext.sodalive.event.*' --tests 'kr.co.vividnext.sodalive.admin.event.banner.*' --console=plain
|
||||
./gradlew test --console=plain
|
||||
./gradlew bootJar ktlintCheck --console=plain
|
||||
./gradlew tasks --all --console=plain
|
||||
```
|
||||
|
||||
- LSP 도구가 노출되어 있으면 변경 파일 진단, 없으면 Kotlin compile/ktlint 결과와 미실행 사실을 기록한다.
|
||||
- 사용자 ulw 지시 및 보안 matcher 추가로 전체 테스트를 실행한다. 실패가 기존 상태에서도 재현되면 별도 제한으로 기록한다.
|
||||
- 고위험 reviewer 1명이 목표·S1~S3·diff·실행 증거를 확인한다. 범위 내 blocker만 수정한다.
|
||||
- 소유 임시 폴더/QA 프로세스가 정리되었음을 확인한다. 공유 Gradle 캐시는 삭제하지 않는다.
|
||||
- 후속 사용자 요청의 전달물: `api-client-prompt.md`에 구현된 요청/응답/오류/노출 정책을 기록하고 최종 응답에도 프롬프트로 제공한다. 문서 작업의 TDD는 해당 없으며 실제 HTTP 응답 및 코드와 대조한다.
|
||||
|
||||
## Progress
|
||||
### 사전 검증 — 2026-10-28
|
||||
- worktree: clean.
|
||||
- `./gradlew test --tests 'kr.co.vividnext.sodalive.event.*' --tests 'kr.co.vividnext.sodalive.admin.event.banner.*' --console=plain`: BUILD SUCCESSFUL, 1m 1s. 구현 전 기존 목록/팝업/관리자 테스트 기준선 확보.
|
||||
- 부모 세션에는 lsp_diagnostics 도구가 노출되지 않았다. 진단 성공으로 보고하지 않는다.
|
||||
|
||||
### P1-T1 RED — 2026-10-28
|
||||
- 새 production 타입을 참조하지 않는 RANDOM_PORT/TestRestTemplate + 같은 컨텍스트 MockMvc 통합 테스트를 먼저 작성했다. S3 외 노출 정책·repository·인증 기록은 mock 없이 실제 H2로 검증한다.
|
||||
- 명령: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.event.*' --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`.
|
||||
- 첫 실행은 도구의 120초 제한으로 test compile 중 중단되어 RED 근거로 사용하지 않았다. 같은 명령을 600초 제한으로 재실행: BUILD FAILED, 8m 59s, 28 tests completed / 15 failed / 0 errors. compile 성공 후 실제 HTTP assertion에서 expected 200 OK / actual 401 UNAUTHORIZED로 실패했다. 기존 보호 경로 13건은 통과했다.
|
||||
- 정상 요청 RED 원문: `HTTP GET /api/v2/events/19 country=null -> 401 {"timestamp":"2026-10-28T09:03:37.443+00:00","status":401,"error":"Unauthorized","path":"/api/v2/events/19"}`. `shouldReturnAnonymousEventDetailWithLegacyFieldsWithoutMutatingTheEntity`의 HTTP 상태 assertion 실패이며 missing-class compile 실패가 아니다.
|
||||
- RED 원본은 소유 임시 폴더의 `red-retry.log` 및 당시 `build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.event.EventDetailControllerIntegrationTest.xml`에서 확인했다. XML은 다음 실행으로 갱신되므로 핵심 결과와 응답을 이 기록에 보존한다.
|
||||
- 실행 자원: `--no-daemon`과 in-process Kotlin compile을 사용했다. 소유 임시 root는 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/event-detail-p1-t1.xH3EUF`이며 작업 종료 시 삭제하고 공유 캐시/다른 세션 프로세스는 건드리지 않는다.
|
||||
|
||||
### P1-T1 GREEN 및 인접 회귀 — 2026-10-28
|
||||
- 변경 파일은 신규 `EventDetailController.kt`, `EventDetailService.kt`, 신규 `EventDetailControllerIntegrationTest.kt`, `SecurityConfig.kt`의 GET matcher 한 줄, 이 계획 문서뿐이다. 기존 엔티티/서비스/컨트롤러와 PRD는 변경하지 않았다.
|
||||
- `EventRepository.findByIdOrNull`로 단건을 로드하고 read-only 서비스에서 기존 목록과 동일한 정책을 확인한다. 전체 목록 조회, 새 port/DTO/의존성, 별도 parser/exception handler는 추가하지 않았다. 관리자 우회는 성인 조건에만 적용한다.
|
||||
- focused GREEN 명령: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.event.*' --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process` → BUILD SUCCESSFUL, 8m 26s, 28 tests / 0 failures / 0 errors / 0 skipped.
|
||||
- 인접 회귀 명령: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.event.*' --tests 'kr.co.vividnext.sodalive.event.*' --tests 'kr.co.vividnext.sodalive.admin.event.banner.*' --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process` → BUILD SUCCESSFUL, 3m 45s. XML 합계 46 tests / 0 failures / 0 errors / 0 skipped: 신규 28, 기존 EventController 5 / EventRepository 5 / EventService 4 / AdminEventBannerController 4.
|
||||
- S1: RANDOM_PORT 실제 HTTP로 익명 KO/JP/기타 국가/헤더 누락, 5개 필드 전체, nullable 이미지·link, HTTPS 보존, 상대/http 이미지 CDN 접두사, 외부 link 보존을 확인했다. `/event` 항목과 상세를 완전 비교하고 DB 재조회로 이미지 필드가 변하지 않음을 확인했다.
|
||||
- S2: 실제 H2 auth 기록이 있는/없는 일반 회원과 관리자는 같은 컨텍스트 MockMvc로 검증했다. KO/JA × adult null/false/true × 익명/미인증/인증/관리자 24조합의 상세와 기존 목록을 비교했다. 인증 성인은 false를 제외하고 관리자는 false/true/null을 허용한다. 비활성/미래/종료/언어 불일치는 관리자에게도 미존재와 동일한 실패 응답이다. 양수 검사·미존재는 HTTP 200 invalid_request 및 실제 SodaException messageKey, malformed/overflow는 기존 MethodArgumentTypeMismatchException과 공통 unknown 응답을 확인했다.
|
||||
- 기간은 현재 시각에서 하루 이상 떨어진 값으로 테스트했다. 정확히 같은 시각의 포함 경계는 기존 주입 seam이 없어 시계를 새로 추상화하지 않고 코드 비교로 확인했다: legacy `startDate.loe(now)` / `endDate.goe(now)`, 신규는 `startDate > now` / `endDate < now`만 거부하여 양끝을 포함한다.
|
||||
- S3: 실제 HTTP 보안 13건에서 익명 POST/PUT/DELETE, collection, 추가 하위 경로, legacy 팝업/쓰기, 관리자 목록/단건 경로 모두 401을 확인했다. 익명 기존 `/event` 목록은 계속 200이며 기존 목록/팝업/관리자 회귀도 통과했다.
|
||||
- 검증 중 제한: 첫 GREEN은 테스트 소스 compile 중 600초 도구 제한, 첫 인접 회귀는 180초 제한으로 종료되어 통과 근거로 사용하지 않았다. 제한을 늘려 재실행했다. 중간 GREEN의 2건 실패는 MockMvc `contentAsString`의 기본 문자셋으로 한글을 잘못 해석한 테스트 문제였다. JSON은 원본 bytes로 파싱하고 로그만 명시적 UTF-8로 읽도록 수정했으며 기대값/정책 assertion은 완화하지 않았다.
|
||||
- 진단: 이 세션에도 `lsp_diagnostics` 도구가 없어 미실행. Gradle의 production/test Kotlin compile은 성공했다. 변경 파일에서 compiler 경고는 없었고 기존 다른 파일의 불필요한 `!!`/unchecked cast/미사용 인자 및 Gradle deprecated 경고는 수정하지 않았다. 전체 suite, bootJar, ktlintCheck, tasks, 독립 리뷰는 부모 담당 P1-GATE로 남겨 실행하지 않았다.
|
||||
- HTTP 원본 artifact: `build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.event.EventDetailControllerIntegrationTest.xml`의 `system-out`에 상태/본문을 기록했다. HTML은 `build/reports/tests/test/classes/kr.co.vividnext.sodalive.v2.api.event.EventDetailControllerIntegrationTest.html`. 다음 Gradle test가 덮어쓸 수 있어 핵심 응답을 아래 보존한다.
|
||||
|
||||
```text
|
||||
HTTP GET /api/v2/events/22 country=null -> 200 {"success":true,"message":null,"data":{"id":22,"thumbnailImageUrl":"https://cdn.test/thumbnail.png","detailImageUrl":null,"popupImageUrl":null,"link":"https://external.test/promotion?from=soda&target=event#detail"},"errorProperty":null}
|
||||
HTTP GET /api/v2/events/17 country=JP -> 200 {"success":true,"message":null,"data":{"id":17,"thumbnailImageUrl":"https://cdn.test/thumbnail.png","detailImageUrl":"https://cdn.test/detail.png","popupImageUrl":"https://cdn.test/popup.png","link":"https://external.test/event"},"errorProperty":null}
|
||||
HTTP GET /api/v2/events/0 country=null -> 200 {"success":false,"message":"잘못된 요청입니다.","data":null,"errorProperty":null}
|
||||
HTTP GET /api/v2/events/1/extra -> 401 {"timestamp":"2026-10-28T09:37:20.055+00:00","status":401,"error":"Unauthorized","path":"/api/v2/events/1/extra"}
|
||||
HTTP GET /event/popup -> 401 {"timestamp":"2026-10-28T09:37:20.121+00:00","status":401,"error":"Unauthorized","path":"/event/popup"}
|
||||
```
|
||||
|
||||
### P1-T1 Teardown Receipt — 2026-10-28
|
||||
- 마지막 테스트 executor PID 18510 종료와 소유 root에 열린 파일 없음(`lsof -nP +D`)을 확인했다. mktemp root 소유 표식을 읽고 허용 parent의 canonical path·직접 하위 이름·비심볼릭링크 조건을 확인한 뒤 이 세션의 `event-detail-p1-t1.xH3EUF`만 삭제했다. macOS `/var`의 canonical path가 `/private/var`여서 첫 경로 가드는 삭제를 거부했고, 양쪽 parent를 동일하게 정규화한 후 삭제했다.
|
||||
- 실제 반환: `TEARDOWN OK: owned event-detail-p1-t1.xH3EUF removed; test executor exited; no open files; shared caches untouched`. RED/GREEN의 핵심 결과와 HTTP 응답은 위에 보존했으며 Gradle XML/HTML은 worktree build 폴더에 남아 있다.
|
||||
- P1-T1만 완료했다. P1-GATE 체크박스·기준·상단 실행 상태는 부모 세션 소유로 유지하며 커밋/푸시/worktree/다른 에이전트/전체 suite·build·review는 실행하지 않았다.
|
||||
|
||||
### P1-GATE 부모 검증 — 2026-10-28
|
||||
- 부모가 새 controller/service와 전체 통합 테스트, SecurityConfig의 GET matcher 한 줄을 직접 검토했다. 기존 API 변경은 없다.
|
||||
- `./gradlew bootJar --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`: BUILD SUCCESSFUL, 1m 9s.
|
||||
- 최초 ktlintCheck는 새 테스트 CsvSource의 줄바꿈 3건을 지적했다. 데이터/기대값은 유지하고 각 인자를 별도 줄로 배치했다.
|
||||
- `./gradlew ktlintCheck --console=plain --no-daemon`: BUILD SUCCESSFUL, 1m 33s. 전체 Kotlin main/test/script 검사 통과.
|
||||
- 첫 전체 테스트는 사용자 중단으로 끝났으며 성공 근거에서 제외했다. 재개 후 `./gradlew test --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`: BUILD SUCCESSFUL, 19m 29s.
|
||||
- 최종 XML 집계: 326 suites, 2,584 tests, failures=0, errors=0, skipped=0. 새 EventDetailControllerIntegrationTest 28건과 기존 event/admin 테스트도 포함한다.
|
||||
- 실제 HTTP 표면은 RANDOM_PORT의 TestRestTemplate 호출이다. 부모 전체 테스트 실행에서도 성공/비노출/인접 보안 응답을 확인했다. 인증 사용자 행렬은 실제 DB fixture와 MockMvc로 검증했다.
|
||||
- 최종 HTTP artifact: `build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.event.EventDetailControllerIntegrationTest.xml`, timestamp `2026-10-28T10:19:56`.
|
||||
- 대표 응답: `/api/v2/events/22` → HTTP 200, success=true, id=22, thumbnailImageUrl=https://cdn.test/thumbnail.png, detailImageUrl=null, popupImageUrl=null, 외부 link 보존. `/api/v2/events/0` → HTTP 200, success=false, data=null. `/event/popup` 익명 → HTTP 401.
|
||||
- `./gradlew tasks --all --console=plain`: BUILD SUCCESSFUL. 문서의 Gradle 명령 존재 확인.
|
||||
- 이전 리뷰 작업은 완료 알림이 있었으나 중단 후 background_output/session_read 및 정확한 로컬 session ID 조회로 결과를 회수하지 못했다. 이를 승인으로 간주하지 않고 동일 범위의 대체 리뷰 하나만 요청했다.
|
||||
- `lsp_diagnostics`는 부모/구현 세션 모두 도구 미제공으로 미실행. Kotlin 컴파일·ktlint 성공을 대체 증거로 기록한다. 운영 배포와 운영 DB 호출은 수행하지 않았다.
|
||||
|
||||
### P1-GATE 최종 판정 및 전달 — 2026-10-28
|
||||
- `./gradlew bootJar ktlintCheck tasks --all --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`: 최종 재확인 BUILD SUCCESSFUL, 17s. `git diff --check`도 통과했다.
|
||||
- 독립 대체 리뷰: [reviews/event-detail-review.md](reviews/event-detail-review.md), APPROVE, 확정 발견 사항 없음. 부모가 리뷰 파일을 읽고 실제 코드·HTTP·전체 테스트 근거와 대조했다. 리뷰 환경은 임시 자원/프로세스 생성 0건이다.
|
||||
- 클라이언트용 [api-client-prompt.md](api-client-prompt.md)를 실제 endpoint, 5개 응답 필드, nullable 값, HTTP 200 실패 envelope, JWT/언어·국가 처리와 대조했다. 기존 API와 관리자 범위는 그대로다.
|
||||
- 부모 최종 `pgrep`에서 GradleWrapperMain/Gradle Test Executor/소유 root 관련 프로세스가 없었다. 부모 root는 빈 opencode 폴더만 남아 `rmdir`로 제거했다. 소유 notepad와 구현 root도 부재를 확인했다.
|
||||
- 정리 read-back: `TEARDOWN OK: parent root, implementation root and notepad absent; shared caches untouched`. 공유 캐시나 다른 세션의 파일/프로세스는 삭제하지 않았다.
|
||||
- 완료 범위: 승인 API·문서·시나리오 S1~S3·전체 테스트·빌드·린트·리뷰·소유 자원 정리. 제한: LSP 미제공으로 미실행, 기간 동일 시각 경계는 코드 대조, 인증 viewer는 MockMvc 검증. 운영 배포·운영 DB 검증·커밋·푸시는 하지 않았다.
|
||||
|
||||
### 커밋 전 재검증 — 2026-10-29
|
||||
- `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.event.*' --tests 'kr.co.vividnext.sodalive.event.*' --tests 'kr.co.vividnext.sodalive.admin.event.banner.*' bootJar ktlintCheck --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`: BUILD SUCCESSFUL, 3m 55s. 테스트 5 suites / 46 tests / failures=0 / errors=0 / skipped=0. bootJar·ktlintCheck는 변경 없음으로 UP-TO-DATE다.
|
||||
- `git diff --check` 및 구현·테스트의 `git diff --staged --check` 통과. 커밋 대상의 비밀값·credential artifact 포함 여부를 확인했다.
|
||||
- 전체 회귀는 P1-GATE의 2,584건 성공 이후 구현 변경이 없어 재실행하지 않았다. 대신 신규 상세·기존 이벤트·관리자 배너 회귀를 실행했다.
|
||||
|
||||
## 종료 기준
|
||||
승인 endpoint의 정상/경계/회귀가 실제 요청으로 검증되고, 테스트·빌드·사용 가능한 진단 및 독립 리뷰를 마치고 소유 QA 자원이 정리되면 종료한다. 실행 불가능하거나 기존 실패인 항목은 근거와 함께 명시하고 완료로 가장하지 않는다.
|
||||
@@ -0,0 +1,62 @@
|
||||
# 일반 이벤트 상세 조회 최종 코드 리뷰
|
||||
|
||||
**판정: APPROVE. 확정 발견 사항 없음.**
|
||||
|
||||
## 1. 리뷰 정보와 범위
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 리뷰 대상 | P1-T1 구현 및 P1-GATE의 코드·검증 증거 |
|
||||
| 기준 상태 | 현재 working tree의 신규 파일 직접 열람 및 SecurityConfig의 GET matcher |
|
||||
| 리뷰 일자 / 리뷰어 | 2026-10-28 / Sisyphus-Junior |
|
||||
| 기준 문서 | [prd.md](../prd.md) EVT-001~005, [plan-task.md](../plan-task.md) P1-T1·P1-GATE |
|
||||
| 리뷰 상태 | 판정 완료. 호스트 중단 전 리뷰의 판정은 승계하지 않음 |
|
||||
|
||||
변경 내용: 익명 접근 가능한 `GET /api/v2/events/{id}`가 기존 목록의 노출 정책을 적용해 단일 `EventItem`을 반환한다. 관리자 상세나 추가 응답 필드는 만들지 않는다.
|
||||
|
||||
신규 controller/service/test, 보안 matcher와 공통 인증·예외 경로, 기존 event Service/Repository/DTO 및 인접 회귀 결과를 검토했다. 제품·테스트·계획 문서 수정, 테스트·빌드 재실행, 별도 HTTP 실행, 에이전트·커밋은 제외했다. 다중 사용자 요청을 전제로 단건 조회와 상태 변경 여부를 확인했으며 부하 측정은 하지 않았다.
|
||||
|
||||
## 2. 요구사항 대조
|
||||
|
||||
| 기준 | 판정 및 코드·테스트 근거 |
|
||||
|---|---|
|
||||
| EVT-001: 일반용 단건 GET만 추가 | [EventDetailController.kt:12](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/adapter/in/web/EventDetailController.kt:12)의 단일 GET mapping, nullable principal, `ApiResponse.ok` 확인. 관리자 endpoint는 추가하지 않음. [EventDetailService.kt:25](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/application/EventDetailService.kt:25)는 read-only이며 ID로 조회하고 전체 목록을 검색하지 않음. |
|
||||
| EVT-002: 활성·기간·언어 | [EventDetailService.kt:31](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/application/EventDetailService.kt:31)의 `startDate > now` / `endDate < now` 거부는 [EventRepository.kt:21](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt:21)의 `loe` / `goe`와 동일하게 양끝을 포함함. [EventService.kt:92](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt:92)와 같은 country resolver로 JP→JA, 그 외→KO. 활성·기간·언어 검사는 관리자 분기보다 먼저 수행됨. |
|
||||
| EVT-002: 성인 3상태 및 관리자 우회 | [EventDetailService.kt:38](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/application/EventDetailService.kt:38)는 실제 auth 기록으로 판단함. 익명·미인증은 null/false, 인증 일반 회원은 null/true만 허용하므로 인증 성인에게 false를 노출하지 않음. 관리자는 성인 조건만 생략함. [EventService.kt:56](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt:56)와 [EventRepository.kt:26](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt:26)에 일치. 신규 테스트의 KO/JA × 3상태 × 4종 viewer 비교 및 관리자 비노출 검증 확인. |
|
||||
| EVT-003: 정확히 5개 필드·이미지·link | [EventDetailService.kt:45](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/application/EventDetailService.kt:45)는 [GetEventResponse.kt:13](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt:13)의 `id`, `thumbnailImageUrl`, `detailImageUrl`, `popupImageUrl`, `link`만 반환함. HTTPS 보존, 그 외 CDN 접두사는 기존 Service와 같고 nullable 이미지·link를 보존함. 외부 link에 HTTP 요청·리다이렉트하지 않으며 엔티티를 수정하지 않음. |
|
||||
| EVT-004·005: 실패 envelope·보안 경계 | 양수 아님·미존재·비노출은 같은 `SodaException(common.error.invalid_request)`를 사용하며 [SodaExceptionHandler.kt:26](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt:26)의 HTTP 200 실패 envelope 유지. 비숫자·Long 초과는 기존 바인딩·공통 오류 처리 유지. [SecurityConfig.kt:191](/Users/klaus/Develop/sodalive/Server/sodalive_/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt:191)는 GET과 단일 segment `*`만 허용하며 쓰기·collection·추가 하위 경로·팝업·관리자 경로는 기존 인증 규칙 적용. JWT 필터를 생략하는 설정이 아니므로 기존 잘못된 JWT 처리 경로도 유지됨. |
|
||||
|
||||
핵심 테스트: [EventDetailControllerIntegrationTest.kt:74](/Users/klaus/Develop/sodalive/Server/sodalive_/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/event/EventDetailControllerIntegrationTest.kt:74)의 응답 완전 비교·DB 재조회, `:165`의 성인 행렬, `:212`의 관리자 비노출, `:244`의 ID 오류, `:271`의 인접 보안 검증을 읽었다. RANDOM_PORT/TestRestTemplate은 실제 HTTP, 인증 MockMvc는 동일 컨텍스트와 실제 H2 member/auth/event fixture를 사용한다. 서비스·repository·인증 기록을 mock으로 대체하지 않으며 S3Uploader만 mock이다.
|
||||
|
||||
## 3. 검토한 실행 증거
|
||||
|
||||
아래는 기존 실행 결과의 확인이며 이 리뷰에서 실행한 테스트가 아니다.
|
||||
|
||||
| 출처 | 결과 및 확인 범위 |
|
||||
|---|---|
|
||||
| [신규 suite XML:2](/Users/klaus/Develop/sodalive/Server/sodalive_/build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.event.EventDetailControllerIntegrationTest.xml:2) 직접 열람 | 2026-10-28T10:19:56, 28 tests, failures/errors/skipped 모두 0. testcase 목록과 실제 HTTP·MockMvc `system-out` 확인. |
|
||||
| 같은 XML의 실제 응답 직접 열람 | `:10378` 익명 200·정확한 5필드·nullable 이미지·외부 link 보존. `:6484`, `:7407` 인증 일반 회원의 false 이벤트 거부. `:13745` 이후 관리자 비활성·미래·종료·언어 불일치 모두 동일 실패. `:10026`~`:10326`의 인접 보안 13건 모두 401. `:4568` 양수 아닌 ID도 HTTP 200, `success=false`, `data=null`, 기존 메시지 확인. |
|
||||
| 기존 회귀 XML 직접 열람 | `EventControllerIntegrationTest` 5, `EventRepositoryTest` 5, `EventServiceTest` 4, `AdminEventBannerControllerIntegrationTest` 4건. 각 `build/test-results/test/TEST-kr.co.vividnext.sodalive.<클래스 전체 이름>.xml`의 testsuite/testcase를 확인했으며 합계 18건 모두 failures/errors/skipped 0. |
|
||||
| 부모 세션 제공 전체 suite 증거 | `./gradlew test --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`: BUILD SUCCESSFUL, 19m 29s, 326 suites / 2584 tests / failures/errors/skips 0. 전체 집계는 부모 제공 증거이며 이 리뷰에서 재집계하지 않음. |
|
||||
| 부모 세션 제공 빌드·린트 증거 | `bootJar` 성공, `ktlintCheck`는 CsvSource 줄바꿈 수정 후 성공. Kotlin production/test compile 성공을 대체 진단 증거로 사용하며 LSP 성공으로 표현하지 않음. |
|
||||
|
||||
## 4. 발견 사항과 검증 한계
|
||||
|
||||
**확정 발견 사항 없음.** 승인된 EVT-001~005를 위반하는 구체 사례나 차단할 회귀를 발견하지 못했다. 회귀 수정 Task/goal 전환 항목 없음.
|
||||
|
||||
- 정확히 같은 시각의 기간 경계는 고정 시계 실행 테스트가 아니라 기존 QueryDSL 포함 비교와 신규 거부 조건의 코드 대조로 확인했다. 계획에서 허용한 검증 방식이며 현재 비교 연산에 결함은 없다.
|
||||
- 인증 viewer는 JWT 발급·전송을 포함한 별도 실제 HTTP가 아니라 인증 MockMvc와 실제 DB fixture로 검증됐다. 이는 승인된 S2 검증 표면이며 기존 JWT 필터 경로는 코드로 확인했다.
|
||||
- `lsp_diagnostics`는 부모와 이 리뷰 환경에 도구가 없어 미실행이다. 이는 진단 도구의 제한이지 확인된 코드 결함이 아니다. 테스트·빌드·린트 성공은 위에 출처를 구분해 기록했으며 재실행하지 않았다.
|
||||
|
||||
## 5. 리뷰 종료 판정과 정리
|
||||
|
||||
| 항목 | 결과 |
|
||||
|---|---|
|
||||
| 승인 요구사항·구현·보안·테스트 범위 확인 | 충족 |
|
||||
| 발견 사항 판정 / plan 전환 | 확정 발견 사항 없음 / 해당 없음 |
|
||||
| 실행 증거와 미검증 부분 구분 | 충족 |
|
||||
| 최종 결론 | **APPROVE**. 이 구현의 반영을 차단할 항목 없음 |
|
||||
|
||||
이 판정은 구현과 제공된 증거에 대한 승인이다. 부모 소유의 P1-GATE 체크박스·상단 상태나 부모 QA 자원의 최종 정리 상태를 대신 갱신·인증하지 않는다.
|
||||
|
||||
**Teardown Receipt:** 이 리뷰에서 shell·테스트·빌드·서버·에이전트를 실행하지 않았고 소유 임시 root/프로세스를 생성하지 않았다. 정리 대상 0, 남은 리뷰 소유 자원 0. 공유 캐시 삭제 없음. 저장소 변경은 이 리뷰 파일뿐이다.
|
||||
Reference in New Issue
Block a user