diff --git a/docs/20261028_최근공지사항_v2/plan-task.md b/docs/20261028_최근공지사항_v2/plan-task.md new file mode 100644 index 00000000..bf670e5a --- /dev/null +++ b/docs/20261028_최근공지사항_v2/plan-task.md @@ -0,0 +1,165 @@ +# 최근 공지사항 v2 구현 계획 + +> 실행 방식: 승인된 설계를 기존 작업 디렉터리에서 TDD로 구현하고 독립 검토한다. 새 worktree·commit·배포는 수행하지 않는다. 전체 테스트는 최종 Gate에서 실행한다. + +| 항목 | 내용 | +|---|---| +| 작성일 | 2026-10-28 | +| 상태 | P1-T1 완료, P1-GATE 전체 회귀 시간 초과로 미완료, 미배포 | +| 요구사항 | [prd.md](prd.md) | +| API 기준 | [api-contract.md](api-contract.md) | +| 현재 Phase / 활성 Goal | Phase 1 / P1-GATE 전체 회귀 결과 확보 필요 | + +**Goal:** 기존 v1·홈을 유지하면서 누구나 최근 활성 공지를 최대 3개 조회한다. + +**Architecture:** 새 web controller → 읽기 전용 application service → 기존 repository 목록 쿼리. 쿼리·DTO·공통 JSON 정책은 유지한다. + +**Tech Stack:** Kotlin 1.6.21, Java 17, Spring Boot 2.7.14, JUnit 5, H2, embedded Redis, TestRestTemplate. + +## 전역 제약과 리뷰 초점 + +- 새 GET 한 경로만 permitAll로 허용한다. 주변 경로나 다른 메서드를 공개하지 않는다. +- `NoticeItem(id: Long, title: String, content: String, date: String)` 및 UTC→Asia/Seoul `yyyy-MM-dd`를 보존한다. +- createdAt이 id 순서와 반대여도 id로 정렬하며 비활성 최신 id를 제외한다. +- 빈 배열, 1/2개 배열, 전체 본문, KST 자정, v1 객체/null을 HTTP로 검증한다. +- 외부 서비스·운영 앱·스키마·의존성·UI·캐시·신규 추상화는 범위 밖이다. + +### Phase 1: 최근 공지 v2 수직 슬라이스 + +- [x] **Task 1.1: P1-T1 실제 HTTP 조회 구현** + +**Objective:** 익명 GET의 배열 계약과 기존 호환성을 실제 HTTP/DB 왕복으로 검증한다. + +**시작 조건:** 사용자 승인 설계, PRD와 이 계획 작성 완료. + +**완료 증거:** 의도한 RED, focused GREEN, 실응답 관찰, 계약과 검증 기록, 소유 임시 리소스 정리. + +**범위 밖:** 전체 테스트·빌드·독립 리뷰는 P1-GATE 소유이며 배포는 전체 작업 범위 밖이다. + +**Files:** + +- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/notice/application/LatestNoticeService.kt` +- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/notice/adapter/in/web/LatestNoticeController.kt` +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`의 기존 notice 규칙 바로 옆 GET matcher 한 줄 +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/notice/LatestNoticeControllerIntegrationTest.kt` +- Docs: 이 폴더의 `prd.md`, `plan-task.md`, `api-contract.md`, `client-prompt.md` + +**Interfaces:** + +- Consumes: `ServiceServiceNoticeRepository.getNoticeList(pageable: Pageable): List` +- Produces: `LatestNoticeService.getLatestNotices(): List`는 `PageRequest.of(0, 3)` 사용 +- HTTP: `LatestNoticeController.getLatestNotices()`는 `ApiResponse.ok(service.getLatestNotices())` 반환 + +- [x] **RED:** RANDOM_PORT/TestRestTemplate/실제 repository·SecurityConfig, 고유 H2, 기존 날짜 alias, EmbeddedRedisInitializer, AFTER_CLASS DirtiesContext로 테스트 작성. 아직 없는 controller 타입은 참조하지 않는다. +- [x] **RED 확인:** 아래 focused 명령으로 미허용·미구현 GET의 `expected 200, actual 401` 같은 HTTP assertion 실패를 확인하고 기록한다. 컴파일/인프라 실패는 RED로 세지 않는다. +- [x] **GREEN:** production 2개 파일과 정확한 GET matcher 한 줄만 추가한다. +- [x] **GREEN 확인:** 동일 focused 명령으로 4+활성·비활성·id 정렬·본문·KST 경계·0/1/2·v1·권한 테스트가 통과하는지 확인한다. +- [x] **REFACTOR:** 불필요한 정리 없이 focused 회귀와 diff 점검, HTTP 출력 및 문서 정합성 기록을 남긴다. + +```bash +./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.api.notice.LatestNoticeControllerIntegrationTest +./gradlew --no-daemon tasks --all +git diff --check +``` + +**Expected:** focused 테스트 실패 0, HTTP 200 + data 배열, 익명 POST/인접 경로 401, v1 객체/null, 명령 유효성 검증 exit 0. + +#### Phase 1 Gate: P1-GATE + +**Objective:** 최종 전체 회귀·빌드·독립 리뷰로 전달 가능 상태를 판정한다. + +**시작 조건:** P1-T1 증거와 리소스 정리 완료. + +**완료 증거:** 전체 테스트·빌드 exit 0, 독립 리뷰 결과와 잔여 위험 기록. + +**범위 밖:** 실패를 숨기는 test 삭제·완화, 무관한 수정·배포. + +- [ ] **[blocked] 전체 회귀 완료:** `./gradlew --no-daemon build --continue`의 전체 `test`가 20분 제한으로 종료됐다. 충분한 실행 시간이 확보된 환경에서 전체 테스트 exit 0을 확인하기 전 Gate는 완료하지 않는다. +- [x] `./gradlew --no-daemon bootJar ktlintCheck`로 패키징과 전체 lint를 검증한다. +- [x] 공지 HTTP 11개와 기존 배포 엔드포인트 HTTP 4개를 함께 재실행해 15개 통과를 확인한다. +- [x] [독립 리뷰](reviews/implementation-review.md)의 규격 적합·코드/테스트 품질 승인과 확정 발견 사항 없음을 기록한다. 이는 전체 회귀 Gate 통과나 배포 승인이 아니다. +- [x] 미배포임을 명시한다. 배포 자체는 작업 범위 밖이다. + +## 실행 자원과 정리 의무 + +- 소유 임시 root: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/notice-v2-impl.thzdr5` +- 모든 shell은 `TMPDIR`를 위 root로, `NODE_COMPILE_CACHE=$TMPDIR/node-compile-cache`, `BUN_RUNTIME_TRANSPILER_CACHE_PATH=$TMPDIR/bun-transpiler`로 export한다. +- Gradle은 `--no-daemon`을 사용한다. 테스트 context는 AFTER_CLASS에 닫고 embedded Redis는 기존 initializer의 JVM 종료 hook으로 종료한다. +- [x] 보고 전 이 실행의 소유 프로세스가 종료됐는지 확인하고 위 root만 삭제한다. 공유 캐시·다른 실행자의 파일과 프로세스는 건드리지 않는다. +- LSP 도구는 현재 host에 없어 실행할 수 없다. focused Kotlin 컴파일 결과를 기록하되 LSP 검증으로 표현하지 않는다. + +## Progress + +### P1-T1 시작: 2026-10-28 + +- `test` 브랜치 초기 `git status --short --branch`: 변경 없음. +- 루트 AGENTS, 필수 agent guides, PRD/계획 샘플, TDD `writing-good-tests.md` 및 기존 쿼리·v1·홈·HTTP 테스트를 확인했다. +- 첫 baseline: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.deployment.DeploymentEndpointIntegrationTest`는 120초 tool timeout으로 종료. 테스트 결과 XML이 없어 성공/RED로 판정하지 않는다. 원인 관찰을 위해 `--info`와 600초 제한으로 재시도한다. +- 이 단계에서 production 코드 변경은 없다. + +### P1-T1 baseline 및 RED 작성: 2026-10-28 + +- `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.deployment.DeploymentEndpointIntegrationTest --info`: exit 0, 4 tests / 0 failures / 0 errors, 2m33s. 기존 HTTP/보안 테스트 기반 동작 확인. +- 관찰: 기존 translation/charge 스케줄러가 H2에서 `for update skip locked` 문법 오류를 로그로 남긴다. baseline에서도 발생했으며 이 범위에서 수정하지 않는다. +- fixture는 실제 repository로 저장·flush한 뒤 별도 JDBC 갱신으로 createdAt을 고정한다. BaseEntity의 PrePersist가 현재 시각으로 덮어쓰므로 이 순서가 필요하다. 서버의 조회는 별도 HTTP thread/DB 왕복이다. +- 전체 JSON tree를 literal 기대값과 비교해 null 필드 포함 여부·필드 이름·본문·날짜까지 확인한다. 생성된 id만 실제 저장 결과를 사용한다. + +### P1-T1 RED 확인: 2026-10-28 + +- production 코드 추가 전 `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.api.notice.LatestNoticeControllerIntegrationTest` 실행: exit 1, 3m42s, 11 tests / 7 failures / 0 errors / 0 skipped. +- 7개 v2 GET 검증이 모두 `expected: <200 OK> but was: <401 UNAUTHORIZED>` HTTP assertion으로 실패했다. Kotlin 컴파일과 로컬 H2/Redis/서버 초기화는 성공했다. +- 기존 v1 객체/null 및 익명 POST/인접 경로 거부의 나머지 4개 검증은 통과했다. +- RED 실응답: `HTTP GET /api/v2/notices/latest -> 401 {"timestamp":"2026-10-28T06:18:18.170+00:00","status":401,"error":"Unauthorized","path":"/api/v2/notices/latest"}`. +- 당시 XML: `build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.notice.LatestNoticeControllerIntegrationTest.xml`. 다음 Gradle test 실행이 덮어쓰므로 실패 수와 핵심 응답을 이 문서에 보존한다. + +### P1-T1 GREEN 및 HTTP 응답 관찰: 2026-10-28 + +- `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.api.notice.LatestNoticeControllerIntegrationTest`: exit 0, 9m11s, 11 tests / 0 failures / 0 errors / 0 skipped. production/test Kotlin 컴파일도 실행·성공했다. 이 결과는 전체 `build` 성공을 뜻하지 않는다. +- 실제 HTTP 출력은 테스트의 `println`과 XML ``에 남긴다. 확인한 artifact: `build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.notice.LatestNoticeControllerIntegrationTest.xml`, HTML: `build/reports/tests/test/index.html`. 다음 테스트 실행으로 덮어쓸 수 있어 아래 핵심 응답을 보존한다. +- 익명 GET의 3개 응답은 id 12/11/10이며 date는 2026-10-28/2026-10-28/2026-10-29여서 createdAt 최신순이 아님을 확인했다. 더 큰 비활성 id는 제외됐다. HTML 모양 텍스트·인용부호·줄바꿈을 포함한 본문도 전체 일치했다. +- UTC 14:59:59/15:00:00의 KST 날짜, 0/1/2개, 전부 비활성, v1 객체/null, 익명 POST/인접 GET의 401을 모두 실서버 응답으로 확인했다. message/errorProperty의 명시적 null 포함도 JSON tree 완전 비교로 확정했다. +- 코드 정리로 바꿀 중복이 없어 REFACTOR production 수정은 하지 않았다. `git diff`에서 기존 notice/home 변경은 없고 SecurityConfig는 GET matcher 한 줄뿐이다. + +```text +HTTP GET /api/v2/notices/latest -> 200 {"success":true,"message":null,"data":[{"id":12,"title":"최근 서비스 공지","content":"서비스 공지\n두 번째 줄: 전체 본문 & \"인용\"","date":"2026-10-28"},{"id":11,"title":"세 번째 공지","content":"본문 3","date":"2026-10-28"},{"id":10,"title":"두 번째 공지","content":"본문 2","date":"2026-10-29"}],"errorProperty":null} +HTTP GET /api/v2/notices/latest -> 200 {"success":true,"message":null,"data":[],"errorProperty":null} +HTTP GET /notice/latest -> 200 {"success":true,"message":null,"data":null,"errorProperty":null} +HTTP POST /api/v2/notices/latest -> 401 {"timestamp":"2026-10-28T06:28:16.011+00:00","status":401,"error":"Unauthorized","path":"/api/v2/notices/latest"} +HTTP GET /api/v2/notices/private -> 401 {"timestamp":"2026-10-28T06:28:16.112+00:00","status":401,"error":"Unauthorized","path":"/api/v2/notices/private"} +``` + +## 검증 기록 + +전체 테스트·빌드·독립 리뷰는 사용자 지정 역할 분담에 따라 이 실행에서 생략하고 P1-GATE에 남긴다. 테스트 클래스 추가에 대한 단일 실행 예시는 위 focused 명령을 사용한다. + +- `./gradlew --no-daemon tasks --all`: exit 0, 31s. 문서의 `test`/`build`/관련 Gradle 명령이 등록됐음을 확인했다. +- `git diff --check`: exit 0. 신규 파일은 read 도구로 확인하고 변경 범위 목록을 `git status --short --untracked-files=all`로 점검했다. +- PRD의 NOTICE-001~005는 모두 P1-T1의 HTTP 검증으로 연결된다. v1/home는 코드 diff가 없고 v1은 HTTP 회귀도 확인했다. 홈 전체 응답을 호출하는 별도 회귀는 이 실행에서 수행하지 않았다. +- LSP 진단 도구 미제공, lint·전체 테스트·전체 빌드·독립 리뷰·운영 MySQL·배포 검증은 미실행이다. +- 기존 관찰 사항: H2에서 translation/charge 스케줄러의 `skip locked` 오류 로그가 GREEN에도 남는다(테스트 실패는 0). 기존 다른 테스트의 불필요 `!!`/unchecked cast 컴파일 경고, Netty macOS resolver 및 Gradle deprecation 경고도 이 범위에서 수정하지 않았다. + +### P1-T1 정리 영수증 및 인계: 2026-10-28 + +- GREEN XML에서 Tomcat 종료와 HikariPool shutdown completed를 확인했다. Gradle test JVM도 종료됐고 프로세스 목록에 이 실행의 test worker, Redis, Kotlin compile daemon, Gradle 8.1.1 daemon은 남아 있지 않다. +- 별도 실행자의 기존 Gradle 8.14.3 daemon(pid 49992)은 소유 대상이 아니므로 종료하지 않았다. +- 소유 root가 빈 디렉터리임을 확인하고 `rmdir "$TMPDIR" && test ! -e "$TMPDIR"` 실행: exit 0. 삭제한 root는 위 `notice-v2-impl.thzdr5` 하나뿐이며 공유 캐시는 변경하지 않았다. +- P1-T1은 focused 증거와 자원 정리까지 완료했다. 전체 테스트·빌드·독립 리뷰와 운영 배포 판단은 부모 실행자의 P1-GATE로 남긴다. + +### P1-GATE 실행: 2026-10-28 + +- `./gradlew --no-daemon build --continue`: `bootJar`/`assemble` 완료 후 전체 `:test` 단계에서 도구의 1,200,000ms 제한으로 종료됐다. 전체 테스트 수·성공 여부를 판정할 최종 결과는 없으며 전체 build 성공으로 표시하지 않는다. 원인이 특정 테스트 실패인지 단순 소요 시간인지 이 실행으로 확정하지 못했다. +- `./gradlew --no-daemon bootJar ktlintCheck`: exit 0, `BUILD SUCCESSFUL in 59s`. 산출물은 `build/libs/sodalive-0.0.1-SNAPSHOT.jar`이며 main/test/script 전체 ktlint를 통과했다. +- `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.api.notice.LatestNoticeControllerIntegrationTest --tests kr.co.vividnext.sodalive.deployment.DeploymentEndpointIntegrationTest`: exit 0, `BUILD SUCCESSFUL in 3m 29s`. 두 XML에서 11+4=15 tests, failures/errors/skipped 모두 0을 확인했다. +- 최신 공지 HTTP XML timestamp는 `2026-10-28T06:57:06`, 기존 배포 HTTP XML timestamp는 `2026-10-28T06:56:23`이다. `TestRestTemplate`가 RANDOM_PORT 실서버에 요청했으며, 공지 응답의 id 12/11/10 순서·전체 필드·빈 배열·KST 날짜와 v1 객체/null, 익명 POST/인접 GET 401을 출력 및 전체 JSON 비교로 재검증했다. +- XML에서 세 테스트 서버의 `Stopping service [Tomcat]`를 확인했다. 최종 소유 자원 부재 확인은 아래 정리 기록에 남긴다. +- LSP 도구는 제공되지 않아 실행하지 못했다. Kotlin 컴파일·ktlint 성공을 LSP 실행으로 대신 표기하지 않는다. 운영 MySQL·배포·클라이언트 앱 검증은 범위 밖이다. +- 최초 독립 리뷰는 완료 알림 후 결과 조회 오류와 세션 미발견으로 판정 내용을 복구하지 못했다. 근거 없는 승인으로 처리하지 않고, 대체 독립 리뷰를 파일로 기록하도록 요청했다. +- 대체 독립 리뷰 결과: 규격 적합, 코드/테스트 품질 승인, 확정 발견 사항 없음. [implementation-review.md](reviews/implementation-review.md)에 검토 근거와 한계를 기록했다. 이후 production/test 변경은 없으며 위 15개 HTTP 재실행 결과를 이 계획에 보완했다. +- 최종 `git diff --check`: exit 0. 전체 회귀 제한을 숨기기 위한 테스트 변경·삭제·skip은 없다. commit/push/worktree 생성/배포는 수행하지 않았다. + +### P1-GATE 소유 자원 정리: 2026-10-28 + +- 최신 HTTP XML에서 Tomcat 종료를 확인했고, 실행 후 프로세스 목록에 이 작업의 Gradle/test worker/Redis는 남아 있지 않았다. 별도 실행자의 기존 Java 프로세스는 종료하지 않았다. +- 빈 하위 `opencode/`와 검증 root `notice-v2.xbE00p`를 `rmdir`로 삭제했다. `test ! -e`로 검증 root, 구현 root `notice-v2-impl.thzdr5`, 작업 노트의 부재를 확인했다(exit 0). +- 승인된 임시 상위 디렉터리를 다시 읽어 `notice-v2` 실행/리뷰 root가 남아 있지 않음을 확인했다. 공유 캐시와 다른 작업의 디렉터리는 보존했다. 대체 리뷰어는 shell/임시 자원을 생성하지 않았다. +- 최종 상태: 구현과 로컬 HTTP 15개·패키징·lint·독립 코드 리뷰는 완료. **전체 회귀는 시간 초과로 미검증이며 P1-GATE는 보류**다. 재개 시 이미 완료한 구현을 반복하지 말고 충분한 실행 시간의 환경에서 전체 테스트 종료 결과를 확보한다. diff --git a/docs/20261028_최근공지사항_v2/reviews/implementation-review.md b/docs/20261028_최근공지사항_v2/reviews/implementation-review.md new file mode 100644 index 00000000..e822aaa5 --- /dev/null +++ b/docs/20261028_최근공지사항_v2/reviews/implementation-review.md @@ -0,0 +1,62 @@ +# 최근 공지사항 v2 구현 리뷰 + +## 1. 리뷰 정보와 범위 + +| 항목 | 내용 | +|---|---| +| 대상 | 2026-10-28 작업 트리의 P1-T1 구현 및 P1-GATE 독립 리뷰 부분 | +| 리뷰어 / 상태 | 독립 리뷰 에이전트 / 판정 완료 | +| 기준 | [PRD](../prd.md)의 NOTICE-001~005, [계획](../plan-task.md), [API 계약](../api-contract.md), [클라이언트 전달문](../client-prompt.md), [리뷰 샘플](../../sample/sample-review.md) | +| 방법 | 신규 controller/service, SecurityConfig, 통합 테스트, 기존 repository/DTO/v1 및 홈 호출 경로를 읽기 전용 대조 | +| 제외 | 코드·테스트 수정, 명령·테스트·실서버 재실행, 운영 MySQL·배포 검증 | + +이전 리뷰 결과는 회수되지 않아 재사용하지 않았다. 이번 판정은 현재 파일을 직접 읽은 정적 검토와 부모 실행자가 전달한 실행 증거를 구분한다. Git SHA/diff는 이번 리뷰에서 독립 확인하지 않았으며, 기존 파일의 변경 없음은 부모의 diff 관찰을 함께 근거로 삼았다. + +## 2. 판정 기준과 계약 대조 + +확정 계약 위반·보안 노출·주요 회귀는 수정 필요 항목으로 판정한다. 재현 조건과 코드·테스트 근거가 없는 추측이나 공개된 환경 한계는 코드 결함으로 올리지 않는다. 아래 경로는 저장소 루트 기준이며 숫자는 줄 번호다. + +| 기준 | 판정 및 근거 | +|---|---| +| NOTICE-001: 익명 GET 및 배열 envelope | 적합. `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/notice/adapter/in/web/LatestNoticeController.kt:10`의 경로와 `:13`의 `ApiResponse.ok`가 목록을 직접 반환한다. `src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt:3`의 기존 네 필드를 재사용한다. | +| NOTICE-002: 활성·id DESC·DB 최대 3개 | 적합. `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/notice/application/LatestNoticeService.kt:12`가 `getNoticeList(PageRequest.of(0, 3))`를 호출한다. `src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt:44`의 활성 필터, `:45`의 offset, `:46`의 DB limit, `:47`의 id 역순을 그대로 사용한다. 전체 조회 후 메모리 절단이나 totalCount 조회가 없다. | +| NOTICE-003: 필드·전체 본문·KST | 적합. `src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt:10`의 `NoticeItem(id: Long, title: String, content: String, date: String)`을 재사용한다. `ServiceServiceNoticeRepository.kt:36`은 원래 제목·본문을 projection하고 `:67`은 UTC→Asia/Seoul 및 `%Y-%m-%d`를 유지한다. 신규 변환·축약이 없다. | +| NOTICE-004: 0/1/2/3+ 및 전부 비활성 | 적합. service의 non-null 목록을 그대로 반환하므로 빈 목록도 `[]`이고 패딩·반복이 없다. 통합 테스트 `:59`, `:101`, `:110`, `:125`, `:142`가 활성 4개와 더 큰 비활성 id, 0/1/2개, 전부 비활성을 검증한다. | +| NOTICE-005: v1·홈·권한 보존 | 적합. `src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt:39` → `ServiceNoticeService.kt:51` → `ServiceServiceNoticeRepository.kt:51`은 단일/null 경로다. `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt:52`와 `GetContentMainTabHomeResponse.kt:13`도 기존 `NoticeItem?`를 유지한다. `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt:194`는 정확한 GET 한 경로만 허용하고 `:214`의 나머지 인증 규칙을 유지한다. | + +## 3. 코드·테스트 품질 + +- **품질 판정: 적합.** 읽기 전용 트랜잭션 service와 얇은 controller만 추가하며 기존 repository·DTO·envelope를 재사용한다. 신규 port·facade·캐시·페이지네이션·의존성·오류 fallback 등 불필요한 추상화가 없다. +- 테스트 기준 파일은 `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/notice/LatestNoticeControllerIntegrationTest.kt`다. `:26`의 RANDOM_PORT, `:33`의 embedded Redis, `:35`의 실제 repository/JDBC와 TestRestTemplate을 사용하며 controller/service를 mock하지 않는다. 파라미터 확장 포함 11건이다. +- `:193`은 실제 저장·flush 후 JDBC로 고정 시각을 저장한다. 이는 `src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt:29`의 PrePersist 덮어쓰기를 피하고 별도 HTTP 요청이 DB를 읽게 한다. `:185`는 상태와 전체 JSON tree를 비교하므로 null 필드 누락·추가 필드·본문 변형·배열 순서도 검출한다. +- `:59`의 id와 createdAt 역순 fixture, `:84`의 UTC 14:59:59/15:00:00 KST 경계, `:154`/`:171`의 v1 객체/null, `:176`의 POST·인접 GET 거부가 핵심 회귀를 덮는다. H2 날짜 alias는 기존 `src/test/kotlin/kr/co/vividnext/sodalive/support/H2MysqlDateFunctions.kt:10`을 사용하므로 운영 MySQL 검증과는 구분한다. +- 클라이언트 전달문도 배열·서버 순서·날짜 재변환 금지·미배포 시 명시적 mock·자동 fallback 금지를 계약과 일치하게 안내한다. + +## 4. 실행 증거와 검증 한계 + +아래 실행 결과는 **부모 실행자가 실제 관찰하여 전달한 증거**다. 리뷰어는 명령, XML 또는 HTTP 호출을 재검증하지 않았다. + +| 전달된 검증 | 결과와 한계 | +|---|---| +| focused RED | 11 tests / 7 failures. 미구현·미허용 v2 GET이 HTTP 401로 실패했으며 컴파일·인프라 실패를 RED로 세지 않았다. | +| focused GREEN XML | timestamp `2026-10-28T06:28:11`, 11 tests / 0 failures / 0 errors / 0 skipped. 전체 테스트 성공의 증거는 아니다. | +| 실제 HTTP 관찰 | v2 HTTP 200의 id 12/11/10 배열과 `[]`, v1 null/객체, 익명 POST 및 인접 GET의 401을 확인했다. 전체 필드·KST 경계는 위 focused 테스트와 계획의 응답 기록으로 연결된다. | +| `./gradlew --no-daemon bootJar ktlintCheck` | exit 0, 59초. 패키징·lint 성공이며 전체 `build` 성공으로 확대하지 않는다. | +| `./gradlew --no-daemon build --continue` | `:test`에서 1,200,000ms 후 타임아웃. **전체 테스트 및 전체 build 검증은 pending/blocked이며 P1-GATE 완료 불가.** 타임아웃 후 focused HTTP 재실행은 부모 담당 예정으로, 이 리뷰 시점에는 결과 미확인이다. | + +LSP 도구는 미제공이다. 운영 MySQL의 실제 timezone 처리·배포 및 홈 전체 HTTP 응답 회귀는 미검증이다. 이 한계만을 이유로 무관한 코드 수정을 요구하지 않는다. 이번 리뷰는 shell·런타임 자원·임시 root를 생성하지 않았다. + +## 5. 발견 사항과 후속 전환 + +**확정 발견 사항 없음.** 승인 계약과 검토 코드·테스트에서 수정이 필요한 결함을 확인하지 못했다. 신규 회귀 수정 Task/goal 전환 항목 없음. + +## 6. 리뷰 종료 판정 + +**규격 판정: 적합. 코드·테스트 품질 판정: 승인(검토 범위 내). 독립 리뷰는 완료하되 최종 P1-GATE는 전체 회귀·전체 build 미확인으로 보류한다. 배포 승인을 뜻하지 않는다.** + +남은 담당은 부모 실행자다. 타임아웃 후 focused HTTP 재실행 결과와 전체 회귀·전체 build의 종료 결과를 별도로 기록하고, 기존 문서의 Gate 상태·역할 표현을 최종 증거에 맞춰 정리한다. 이 리뷰에서는 다른 파일이나 완료 체크박스를 변경하지 않는다. + +## 7. 실행자 후속 증거 + +- 2026-10-28: 리뷰와 독립적으로 공지·기존 배포 엔드포인트 HTTP 테스트를 함께 재실행했다. `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.api.notice.LatestNoticeControllerIntegrationTest --tests kr.co.vividnext.sodalive.deployment.DeploymentEndpointIntegrationTest`가 exit 0, 3m29s로 끝났으며 XML은 11+4=15 tests, failures/errors/skipped 모두 0이다. +- production/test 수정 없이 문서의 현재 상태를 갱신했다. 전체 회귀의 시간 초과와 P1-GATE 보류는 그대로 유지한다. 이 기록은 리뷰어의 재실행 주장이 아니라 실행자의 추가 증거다.