Files

16 KiB

최근 공지사항 v2 구현 계획

실행 방식: 승인된 설계를 기존 작업 디렉터리에서 TDD로 구현하고 독립 검토한다. 새 worktree·commit·배포는 수행하지 않는다. 전체 테스트는 최종 Gate에서 실행한다.

항목 내용
작성일 2026-10-28
상태 P1-T1 완료, P1-GATE 전체 회귀 시간 초과로 미완료, 미배포
요구사항 prd.md
API 기준 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 수직 슬라이스

  • 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()) 반환

  • RED: RANDOM_PORT/TestRestTemplate/실제 repository·SecurityConfig, 고유 H2, 기존 날짜 alias, EmbeddedRedisInitializer, AFTER_CLASS DirtiesContext로 테스트 작성. 아직 없는 controller 타입은 참조하지 않는다.

  • RED 확인: 아래 focused 명령으로 미허용·미구현 GET의 expected 200, actual 401 같은 HTTP assertion 실패를 확인하고 기록한다. 컴파일/인프라 실패는 RED로 세지 않는다.

  • GREEN: production 2개 파일과 정확한 GET matcher 한 줄만 추가한다.

  • GREEN 확인: 동일 focused 명령으로 4+활성·비활성·id 정렬·본문·KST 경계·0/1/2·v1·권한 테스트가 통과하는지 확인한다.

  • REFACTOR: 불필요한 정리 없이 focused 회귀와 diff 점검, HTTP 출력 및 문서 정합성 기록을 남긴다.

./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는 완료하지 않는다.
  • ./gradlew --no-daemon bootJar ktlintCheck로 패키징과 전체 lint를 검증한다.
  • 공지 HTTP 11개와 기존 배포 엔드포인트 HTTP 4개를 함께 재실행해 15개 통과를 확인한다.
  • 독립 리뷰의 규격 적합·코드/테스트 품질 승인과 확정 발견 사항 없음을 기록한다. 이는 전체 회귀 Gate 통과나 배포 승인이 아니다.
  • 미배포임을 명시한다. 배포 자체는 작업 범위 밖이다.

실행 자원과 정리 의무

  • 소유 임시 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으로 종료한다.
  • 보고 전 이 실행의 소유 프로세스가 종료됐는지 확인하고 위 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 한 줄뿐이다.
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에 검토 근거와 한계를 기록했다. 이후 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는 보류다. 재개 시 이미 완료한 구현을 반복하지 말고 충분한 실행 시간의 환경에서 전체 테스트 종료 결과를 확보한다.