diff --git a/docs/20261106_이벤트수정_누락필드보존/plan-task.md b/docs/20261106_이벤트수정_누락필드보존/plan-task.md new file mode 100644 index 00000000..270ba3fb --- /dev/null +++ b/docs/20261106_이벤트수정_누락필드보존/plan-task.md @@ -0,0 +1,88 @@ +# 이벤트 수정 누락 필드 보존 구현 계획 + +## 목표와 상태 +- 작성일: 2026-11-06 +- 요구사항: [prd.md](prd.md) +- 상태: 기능 범위 구현 및 검증 완료 +- 현재 활성 Goal: 없음 +- 기술: Kotlin 1.6.21, Java 17, Spring Boot 2.7.14, JUnit 5, MockMvc, H2. +- 구조: 기존 두 서비스의 대입 조건에 null 가드만 추가한다. API 시그니처와 명시값 처리 방식은 바꾸지 않는다. +- 실행: subagent-driven-development. 기존 작업 디렉터리를 사용하고 worktree는 생성하지 않는다. + +### Phase 1: 두 API의 누락 값 보존 + +- [x] **Task 1.1: 요청 누락 회귀 테스트와 최소 수정 (P1-T1)** + +**Objective:** 두 이벤트 수정 API가 누락된 link/isAdult를 보존하면서 명시값은 기존대로 저장한다. +- 시작 조건: EVENT-001~005 확정, 기존 코드·테스트 확인 완료. +- 완료 증거: RED assertion 실패, GREEN 통합 테스트 통과, DB 재조회 검증. +- 범위 밖: 다른 필드 처리 변경, 공개 스키마 변경, 운영 데이터 접근. + +**Files:** +- `src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt` +- `src/main/kotlin/kr/co/vividnext/sodalive/admin/event/banner/AdminEventBannerService.kt` +- `src/test/kotlin/kr/co/vividnext/sodalive/event/EventControllerIntegrationTest.kt` +- `src/test/kotlin/kr/co/vividnext/sodalive/admin/event/banner/AdminEventBannerControllerIntegrationTest.kt` + +- [x] **RED:** 기존 통합 테스트에 관리자 인증 PUT 요청과 DB 재조회 assertion을 추가한다. +- [x] **RED 확인:** 아래 focused 명령으로 기존 link/isAdult가 null로 바뀌어 실패하는지 확인한다. +- [x] **GREEN:** 두 서비스의 link/isAdult 대입 조건에 각 인자 `!= null` 가드를 추가한다. +- [x] **GREEN 확인:** 동일 focused 테스트에서 모든 시나리오가 통과한다. +- [x] **REFACTOR:** 불필요한 리팩터링 없이 기존 테스트를 포함한 직접 영향 회귀를 확인한다. + +```bash +export TMPDIR=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/event-omission.qeHSia/implementer.ZukrpN +export NODE_COMPILE_CACHE=$TMPDIR/node-compile-cache BUN_RUNTIME_TRANSPILER_CACHE_PATH=$TMPDIR/bun-transpiler +./gradlew test --no-daemon --tests 'kr.co.vividnext.sodalive.event.EventControllerIntegrationTest' --tests 'kr.co.vividnext.sodalive.admin.event.banner.AdminEventBannerControllerIntegrationTest' +``` + +**P1-T1 검증 결과 (2026-11-06):** +- RED: production 변경 전 위 명령 exit 1, 2분 23초. 23개 중 10개 assertion 실패, error/skip 0. 일반 클래스 12개 중 5개, 관리자 클래스 11개 중 5개 실패했다. 각 API의 제목만 수정한 true/false/null 3개 사례에서 기존 링크 대신 null이 저장됐으며 true/false도 null이 됐다. 빈 문자열/공백 링크 2개 사례에서는 링크의 기존 처리 방식은 맞았지만 생략한 isAdult가 true 대신 null이 됐다. 기존 테스트 9개와 명시값 변경 4개는 통과했다. +- GREEN: 네 비교문에만 `link != null &&`, `isAdult != null &&`를 추가한 뒤 동일 명령 exit 0, 3분 35초. 일반 12개와 관리자 11개, 총 23개 모두 통과하고 failure/error/skip은 0이었다. 기존 테스트를 삭제·변경하지 않았고 리팩터링하지 않았다. +- S1~S3는 실제 컨트롤러·보안·서비스와 기존 S3 mock을 사용했다. 모든 요청의 HTTP 200 및 `success=true`를 확인한 뒤 별도 repository 호출로 H2를 재조회했다. 제목만 수정하는 사례는 이미지 3종·isPopup·기간·lang·isActive까지 Event 스냅샷으로 완전 비교했다. 명시값은 Boolean을 반대 값에서 true/false로 각각 변경했고, 빈 문자열/공백 링크는 일반 API의 원문 저장과 관리자 API의 null 저장을 확인했다. 시각은 `2099-01-01T00:00:00`, `2099-01-02T23:59:59`로 고정했다. +- 임시 RED/GREEN 보고서: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/event-omission.qeHSia/implementer.ZukrpN/p1-t1-red-green.md`. GREEN XML에서도 각 클래스의 test/failure/error/skip 수를 확인했다. LSP 진단 도구가 제공되지 않아 실행하지 못했으며 Gradle의 production/test Kotlin 컴파일이 성공했다. 변경 파일의 컴파일 경고는 없고, 기존 다른 파일의 불필요한 `!!`·unchecked cast·미사용 인자 및 Gradle deprecation 경고는 남아 있다. +- 한계: MockMvc/H2 통합 검증이며 실제 네트워크·운영 MySQL 검증은 아니다. 전체 테스트는 미실행이며 사용자 실행 명령은 `./gradlew test`이다. QA·변경 범위 lint·빌드·리뷰·임시 리소스 정리는 상위 에이전트의 P1-GATE에 남겼다. 커밋·스테이징·푸시와 운영 앱/서비스 실행은 하지 않았다. + +## 시나리오 계약 +| 시나리오 | PASS 조건 | 검증 표면 | +|---|---|---| +| S1 누락 보존 | 제목만 수정해도 link와 isAdult=true/false/null 및 다른 생략 필드가 DB 재조회에서 유지된다. | 두 실제 컨트롤러의 MockMvc PUT + H2 | +| S2 명시값 반영 | 새 링크, isAdult=true/false가 저장되고 응답 success=true이다. | 두 컨트롤러 + H2 | +| S3 빈값 호환 | 빈 문자열·공백 link는 일반 API에서 원문, 관리자 API에서 null로 저장된다. isAdult 생략값은 유지된다. | 두 컨트롤러 + H2 | +| S4 인접 회귀 | 기존 이벤트 조회·언어·생성 테스트가 통과한다. | 기존 focused 테스트 | + +- [x] **Phase Gate (P1-GATE): 검증 및 리뷰** +- 시작 조건: P1-T1 완료. +- 완료 증거: focused 회귀, `./gradlew bootJar`, 변경 diff 리뷰, 검증 한계 기록, 임시 리소스 정리. +- 범위 밖: 전체 테스트 자동 실행, 관련 없는 lint 수정, 커밋/푸시. +- TDD 예외 사유: Gate는 구현이 아닌 검증 단계이다. +- 대체 검증: 실제 HTTP QA가 가능한 로컬 테스트 환경이면 endpoint 응답과 DB 결과를 확인한다. 불가하면 MockMvc와 실제 네트워크 검증을 구분해 기록한다. +- LSP 도구가 제공되지 않으면 그 사실을 명시하고 Kotlin 컴파일과 변경 범위 lint를 대체 증거로 남긴다. + +```bash +./gradlew test --tests 'kr.co.vividnext.sodalive.event.*' --tests 'kr.co.vividnext.sodalive.admin.event.banner.*' +./gradlew bootJar +``` + +## 종료 조건 +S1~S4의 관찰 가능한 동작과 focused 테스트가 통과하고, 빌드·변경 리뷰·임시 리소스 정리가 완료되면 즉시 종료한다. 전체 테스트는 저장소 규칙에 따라 사용자가 `./gradlew test`로 실행하며, 미검증 범위를 명시한다. + +## 검증 기록 +- 초기 조사: 두 서비스에서 link/isAdult만 누락 null을 대입하고, 기존 세 테스트 클래스에 update 검증이 없음을 확인했다. +- 계획 에이전트 후속 호출은 timeout으로 종료됐다. 기존 조사와 승인된 요구사항으로 위 최소 변경을 확정했다. + +### P1-GATE 검증 결과 (2026-11-06) +- 실제 HTTP QA: 임시 `EventOmissionHttpQaTest`를 별도 Gradle init script로 테스트 소스에 연결했다. `./gradlew --no-daemon --console=plain -I "$TMPDIR/http-qa.init.gradle" test --tests 'kr.co.vividnext.sodalive.event.EventOmissionHttpQaTest'` exit 0. RANDOM_PORT 서버와 TestRestTemplate, 실제 TokenProvider 관리자 JWT, H2·embedded Redis를 사용했다. MockMvc 요청으로 대체하지 않았다. +- 실제 네트워크: 두 경로 각각 기존 성인 여부 true/false/null에서 제목만 수정, 링크만 수정, isAdult=true, isAdult=false, 빈 링크, 공백 링크를 차례로 전송했다. 총 36개 PUT 모두 HTTP 200, `{"success":true,"message":"수정되었습니다.","data":{},"errorProperty":null}` 응답이며, 6개 조합 모두 DB 재조회 assertion을 통과했다. 제목 외 필드 보존·독립 필드 수정·빈값 호환을 확인했다. +- 1차 Gate: `./gradlew --no-daemon --console=plain test --tests 'kr.co.vividnext.sodalive.event.*' --tests 'kr.co.vividnext.sodalive.admin.event.banner.*' ktlintCheck bootJar --continue` exit 0, 1분 30초. 저장소 39개 및 앞선 임시 QA 컴파일 클래스 2개가 실행됐다. `ktlintCheck` 실제 실행 통과, `bootJar` 생성 및 `build/libs/sodalive-0.0.1-SNAPSHOT.jar` 확인. +- 임시 QA 컴파일 클래스가 일반 실행에도 남은 것을 확인해 해당 소유 class 파일만 제거했다. 이후 임시 init script 없이 아래 명령으로 저장소 테스트만 재실행했다. + +```bash +./gradlew --no-daemon --console=plain test --rerun --tests 'kr.co.vividnext.sodalive.event.*' --tests 'kr.co.vividnext.sodalive.admin.event.banner.*' ktlintCheck bootJar --continue +``` + +- 최종 결과: exit 0, 2분 57초. XML 5개에서 일반 컨트롤러 12 + 관리자 컨트롤러 11 + 언어 통합 7 + 저장소 5 + 서비스 4 = **39개**, 실패·오류·skip 모두 0. XML 시각은 `2026-11-06T04:14:52`~`04:15:19`. 임시 QA class는 최종 결과에 없다. Kotlin test 컴파일 및 테스트는 실제 실행됐고, 동일 입력의 lint·bootJar는 직전 통과 결과를 재사용한 UP-TO-DATE였다. +- 독립 리뷰: `bg_8220e32c`가 PRD EVENT-001~005, 실제 diff, 연결 코드와 테스트, HTTP/DB 원본을 대조해 **APPROVE**. 차단 사항 없음. [리뷰 기록](reviews/event-omission-review.md). +- 진단: LSP 도구가 제공되지 않아 미실행이며 Kotlin 컴파일·ktlintCheck로 대체했다. `git diff --check` 통과. 기존 무관 코드 경고와 테스트 context 종료 경고는 이번 수정 범위 밖이다. +- 검증 한계: 전체 테스트와 운영 MySQL·실제 관리자 UI는 미검증이다. 전체 검증은 사용자가 `./gradlew test`로 실행한다. 운영 데이터 복원·접속 및 커밋·푸시는 하지 않았다. +- 정리: Gradle wrapper/daemon/test worker·embedded Redis 프로세스가 남아 있지 않음을 확인했다. 이번 작업 소유 `event-omission.qeHSia` 루트와 두 자식 루트, 임시 HTTP QA 소스·init script·로그를 삭제했고, 루트·임시 class·임시 XML 부재 검사 `CLEANUP_PASS`를 확인했다. 앞서 기록된 임시 보고서 경로는 실행 당시 경로이며 현재는 위 문서와 리뷰에 요약 근거를 보존한다. 공유 캐시·기존 worktree·운영 리소스는 건드리지 않았다. diff --git a/docs/20261106_이벤트수정_누락필드보존/prd.md b/docs/20261106_이벤트수정_누락필드보존/prd.md new file mode 100644 index 00000000..e93029b4 --- /dev/null +++ b/docs/20261106_이벤트수정_누락필드보존/prd.md @@ -0,0 +1,27 @@ +# 이벤트 수정 누락 필드 보존 PRD + +## 문서 정보 +- 작성일: 2026-11-06 +- 상태: 구현 및 기능 범위 검증 완료 +- 결정권자: 사용자. 두 API 모두 파라미터 미전송 시 기존 값을 유지하도록 명시 승인. +- 구현 계획: [plan-task.md](plan-task.md) + +## 문제와 목표 +`PUT /event`, `PUT /admin/event/banner`에서 `link`, `isAdult`를 생략하면 기존 값이 null로 덮어써진다. 부분 수정 시 생략된 값을 보존하고, 명시적으로 전달한 값은 기존 방식으로 반영한다. + +## 요구사항과 수용 기준 +| ID | 상태 | 요구사항 | 수용 기준 | +|---|---|---|---| +| EVENT-001 | 확정 | 두 API의 link 생략은 변경하지 않는다. | 제목만 수정한 뒤 DB 재조회에서 기존 링크가 유지된다. | +| EVENT-002 | 확정 | 두 API의 isAdult 생략은 변경하지 않는다. | 기존 true, false, null 각각이 유지된다. | +| EVENT-003 | 확정 | 명시한 값의 기존 동작을 유지한다. | 새 링크 및 isAdult=true/false가 저장된다. | +| EVENT-004 | 확정 | 빈 링크의 기존 API별 동작을 유지한다. | /event는 빈 문자열·공백을 그대로 저장하고 관리자 API는 null로 저장한다. | +| EVENT-005 | 확정 | 다른 생략 필드와 API 계약을 유지한다. | 이미지·isPopup·기간·lang·isActive는 제목만 수정해도 유지되고 응답 성공 형식과 ADMIN 권한은 변하지 않는다. | + +## 범위 밖 +- 새 삭제 파라미터, JSON PATCH, DTO·스키마 변경, 서비스 통합/리팩터링. +- 기존 데이터 복원, 관리자 프런트엔드 변경, 운영 DB 접근, 날짜 처리 변경. +- isAdult를 null로 초기화하는 새 기능. 생략은 이제 보존이며 true/false 전송은 기존대로 지원한다. + +## 검증 경계 +기존 SpringBootTest/MockMvc와 H2 재조회로 요청 바인딩부터 저장까지 검증한다. S3는 기존 mock을 유지한다. 실제 관리자 UI 및 운영 MySQL 검증과 전체 테스트는 별도이며, 전체 테스트 실행 명령은 `./gradlew test`이다. diff --git a/docs/20261106_이벤트수정_누락필드보존/reviews/event-omission-review.md b/docs/20261106_이벤트수정_누락필드보존/reviews/event-omission-review.md new file mode 100644 index 00000000..fdf4ad16 --- /dev/null +++ b/docs/20261106_이벤트수정_누락필드보존/reviews/event-omission-review.md @@ -0,0 +1,29 @@ +# 이벤트 수정 누락 필드 보존 리뷰 + +## 리뷰 정보 +- 일자: 2026-11-06 +- 대상: 현재 working tree의 서비스 2개·통합 테스트 2개, P1-T1/P1-GATE +- 기준: [PRD](../prd.md), [구현 계획](../plan-task.md) +- 리뷰어: 독립 검토 에이전트 `bg_8220e32c` 및 상위 에이전트 실행 검증 +- 상태: 판정 완료, APPROVE + +## 검토 근거 +- `EventService.update`, `AdminEventBannerService.update`의 production 변경은 null 가드 네 곳뿐이다. +- 두 컨트롤러의 선택 파라미터, ADMIN 권한, 응답 구조, Event 엔티티·저장소를 대조했다. +- 기존 통합 테스트에 추가한 14개 사례는 실제 컨트롤러와 서비스 및 별도 H2 재조회로 누락·명시값·빈값 처리를 확인한다. +- nullable Boolean 세 상태 및 다른 생략 필드는 Event 스냅샷으로 비교한다. 명시한 false도 별도 값으로 반영한다. +- 빈 링크는 기존대로 일반 API에서 원문을 저장하고 관리자 API에서 null로 저장한다. + +## 실행 증거 +| 검증 | 결과 | +|---|---| +| TDD RED | 구현자 기록: 23개 중 의도한 assertion 실패 10개. 리뷰어가 RED 원본을 별도로 재실행하지는 않았다. | +| focused GREEN | 리뷰어가 원본 XML의 일반 12개·관리자 11개, 실패·오류·skip 0을 확인했다. | +| 실제 HTTP/DB | 부모가 실제 관리자 JWT로 36개 PUT·6개 DB 상태 조합을 실행했고 리뷰어가 QA 소스와 XML을 대조했다. 모두 PASS. | +| 최종 인접 회귀 | 부모가 임시 QA class 제거 후 저장소 5개 클래스 39개를 재실행, 실패·오류·skip 0 확인. | +| 빌드·정적 검사 | 부모의 Kotlin 컴파일, ktlintCheck, bootJar, diff 공백 검사 통과. | + +## 판정 +확정 발견 사항 없음. EVENT-001~005에 부합하고 API 스키마·권한 변경이나 범위 확장이 없다. 독립 리뷰의 명세 준수 및 코드 품질 판정은 APPROVE다. + +전체 테스트, 운영 MySQL, 실제 관리자 UI, LSP는 미검증이다. 전체 테스트는 사용자 실행 항목이며 위 검증을 전체 성공으로 표현하지 않는다.