Test #462

Merged
klaus merged 13 commits from test into main 2026-10-30 03:15:43 +00:00
2 changed files with 227 additions and 0 deletions
Showing only changes of commit 8345cc25ad - Show all commits
@@ -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<NoticeItem>`
- Produces: `LatestNoticeService.getLatestNotices(): List<NoticeItem>`는 `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 `<system-out>`에 남긴다. 확인한 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두 번째 줄: <b>전체 본문</b> & \"인용\"","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는 보류**다. 재개 시 이미 완료한 구현을 반복하지 말고 충분한 실행 시간의 환경에서 전체 테스트 종료 결과를 확보한다.
@@ -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 보류는 그대로 유지한다. 이 기록은 리뷰어의 재실행 주장이 아니라 실행자의 추가 증거다.