Files

19 KiB
Raw Permalink Blame History

일반용 이벤트 상세 조회 구현 계획

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 실행: 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 단건 조회

  • 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; /** 금지.

  • RED: 먼저 실제 HTTP 정상 요청 테스트를 작성하고, 조회 불가·성인 3상태·언어·관리자·응답 변환·인접 보안 시나리오를 추가한다.

  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.event.*' --console=plain에서 미구현 route/security로 정상 요청이 실패함을 기록한다.

  • GREEN: ID 단건 로드, 양수 검사, 활성/현재 시각/언어/성인 필터, EventItem 변환, 좁은 보안 허용을 구현한다.

  • GREEN 확인: 동일 focused test로 모든 시나리오 통과를 확인한다.

  • 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

  • Goal P1-GATE: 동작·회귀·빌드·리뷰·정리 완료
  • 시작 조건: P1-T1 완료.
  • 완료 증거: 아래 명령과 HTTP 증거, 독립 reviewer 승인, 자원 정리 확인.
  • 범위 밖: 기존 전체 테스트 실패/린트 위반의 무관한 수정.
  • TDD 예외 사유: 읽기 전용 완료 검증이며 테스트 추가가 아닌 실행·리뷰 단계다.
./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가 덮어쓸 수 있어 핵심 응답을 아래 보존한다.
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, APPROVE, 확정 발견 사항 없음. 부모가 리뷰 파일을 읽고 실제 코드·HTTP·전체 테스트 근거와 대조했다. 리뷰 환경은 임시 자원/프로세스 생성 0건이다.
  • 클라이언트용 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 자원이 정리되면 종료한다. 실행 불가능하거나 기존 실패인 항목은 근거와 함께 명시하고 완료로 가장하지 않는다.