docs(notice): v2 최근 공지 구현 검증과 리뷰를 기록한다
This commit is contained in:
@@ -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 보류는 그대로 유지한다. 이 기록은 리뷰어의 재실행 주장이 아니라 실행자의 추가 증거다.
|
||||||
Reference in New Issue
Block a user