Test #462
@@ -0,0 +1,547 @@
|
||||
# 가로형 서브 배너 백엔드 구현 계획
|
||||
|
||||
| 문서 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | P5 소프트 삭제·P6 추천 통합 구현 및 전체 검증 완료, 미배포 |
|
||||
| 작성일 | 2026-10-29 |
|
||||
| 요구사항 기준 | [prd.md](prd.md) |
|
||||
| API 기준 | [api-contract.md](api-contract.md), 실제 구현·HTTP 검증 계약 |
|
||||
| 현재 Phase | Phase 2~6 완료 |
|
||||
| 현재 활성 Goal | 없음 |
|
||||
|
||||
## 목표
|
||||
|
||||
두 placement의 서브 배너를 독립 테이블과 백엔드 API로 제공한다. 기존 문서 준비 Goal `DOC-T1`~`DOC-T5`는 완료된 역사적 문서 작업이다. 이후 사용자가 문서대로 백엔드 코드 및 DDL 생성을 승인했으며, 운영 DB 적용·배포는 범위 밖이다.
|
||||
|
||||
## 범위
|
||||
|
||||
### 포함
|
||||
|
||||
- `prd.md`의 요구사항 요약과 결정 상태 기록
|
||||
- `api-contract.md`의 실제 API 계약
|
||||
- 독립 `v2.subbanner` 기능 모듈·DDL·관리자 CRUD·소프트 삭제 및 기존 두 추천 GET 응답 합성
|
||||
- 현재 구현 작업은 신규 subbanner 코드·테스트·DDL 파일 생성과 실제 계약 문서 동기화를 포함한다. 이전 문서 전용 작업은 별도 완료 이력으로 보존한다.
|
||||
|
||||
### 제외
|
||||
|
||||
- **현재 제외:** 운영 DB DDL 적용·배포. 코드 변경이 포함된 이번 구현에서는 테스트·lint·빌드를 실행했다. 문서 전용 변경에 대한 Gradle 생략 규칙은 유지한다.
|
||||
- **향후 구현 범위 밖:** 앱/관리자 실제 UI, 기존 대형 배너 변경, scheduler/job, 순서 편집, 활성화 switch, analytics
|
||||
- 배포, 운영 DB 직접 변경, 기존 콘텐츠 배너 schema migration
|
||||
- 운영 배포가 완료됐다는 주장
|
||||
- 신규 dependency, global exception rewrite, 공통 utility wrapper
|
||||
- 앱/client network 통합은 backend 구현 후 별도 계약 검토와 제공이 끝날 때까지 차단한다. backend를 알 수 없는 외부 block으로 취급하지 않는다.
|
||||
|
||||
## 문서 변환 Goal
|
||||
|
||||
### `DOC-T5` 백엔드 계획 문서 변환
|
||||
|
||||
**Goal 실행 `DOC-T5`:** 사용자 승인 범위에서 PRD와 계획 문서만 backend 요구사항 및 향후 구현 계획으로 변환한다.
|
||||
|
||||
- **시작 조건:** `DOC-T1`~`DOC-T4`의 기존 문서 결과가 있고 사용자가 backend 문서 보완과 문서 검증 지침 수정을 승인했다.
|
||||
- **완료 증거:** 지정된 기존 문서 세 파일만 수정, 신규 backend Goal은 전부 미체크, 과거 DOC 체크박스/Gates/Progress와 이전 Gradle 증거 보존.
|
||||
- **범위 밖:** API 구현 승인, 코드·SQL 신규 파일 작성, Gradle/test/build 실행, 앱·관리자 prompt 변경.
|
||||
- **TDD 예외 사유:** 문서 전용 변경이다.
|
||||
- **대체 검증 방법:** PRD 요구 ID↔Goal 추적, 정확한 path/signature 일치, 과거 완료 기록과 미실행 검증 상태를 수동 대조한다.
|
||||
- [x] PRD와 backend Phase/Goal 초안 작성 완료.
|
||||
- [x] 부모 작업자가 문서 대조·공백 검사 및 범위를 확인했다. 아래 `DOC-T5` 최종 검증 기록을 따른다.
|
||||
|
||||
## 백엔드 구현 공통 제약
|
||||
|
||||
- 기술 기준은 저장소의 Kotlin·Java 17·Spring Boot 2.7.14·JPA/QueryDSL·JUnit 5와 Gradle Wrapper를 그대로 사용한다. 버전·dependency 변경은 계획에 포함하지 않는다.
|
||||
- 서브 배너 도메인·관리자는 `v2/subbanner`에 두고 기존 `v2/api/home` 및 `v2/api/content/recommendation`의 facade/DTO에서 응답을 합성한다. 대응 테스트 경로도 같은 책임을 따른다.
|
||||
- 데이터 계층은 한 개의 독립 기능 모듈로 유지한다. `SubBannerController`는 P6에서 제거했고, `AdminSubBannerController`와 `SubBannerService`·persistence·6필드 public DTO는 재사용한다. 단일 클래스 파일명은 ktlint 규칙을 따른다.
|
||||
- 현재 앱 조회는 `GET /api/v2/home/recommendations`, `GET /api/v2/audio/recommendations`의 `data.subBanners`다. 관리자 경로는 `/admin/sub-banners`를 유지한다. 이전 Phase의 standalone GET 기록은 당시 구현 이력이며 현재 계약은 [api-contract.md](api-contract.md)를 따른다.
|
||||
- 파일/링크/날짜 입력을 경계에서 검사한다. 오류는 기존 `ApiResponse`/`SodaExceptionHandler` 사실을 확인하고 global handler는 고치지 않는다.
|
||||
- DDL은 `docs/20261029_가로형서브배너/create-sub-banner.sql`이며 P5에서 is_active 기본 true를 추가했다. 테스트용 Flyway/자동 live migration을 만들지 않는다.
|
||||
- 기존 S3Uploader, 이미지 검증기 및 QueryDSL/JPA를 재사용한다. GIF는 허용하며 S3 object 자동 삭제/정리는 제공하지 않는다. 새 dependency는 추가하지 않았다.
|
||||
- 시간은 KST 분 입력→UTC 초 단위 DB 값→KST 분 응답이다. `TIMESTAMP` DDL precision을 확인하고 service에서 한번 얻은 `now`를 초 단위로 정규화한다.
|
||||
- 각 단계의 원래 명령은 계획이며 실제 실행 결과는 해당 Progress를 따른다. 문서 전용 변경은 Gradle을 실행하지 않고, 코드 변경의 Gate는 테스트·lint·빌드 증거로 판정한다.
|
||||
|
||||
## Historical Phase 1: 문서 작성 완료 기록
|
||||
|
||||
아래 `DOC-T1`~`DOC-T4`, Gate와 Progress는 기존 문서 작업 이력이다. 새 backend Phase의 완료 상태나 구현 착수 근거가 아니다.
|
||||
|
||||
**Phase 결과:** 요구사항, 제안 API와 두 독립 구현 프롬프트가 서로 일치하는 문서 묶음.
|
||||
|
||||
### Task 1.1 요구사항과 준비 계획
|
||||
|
||||
**Goal 실행 `DOC-T1`:** 샘플에 맞춰 PRD와 문서 작성 계획을 작성한다.
|
||||
|
||||
- **시작 조건:** 사용자 확정 요구, 프로젝트 문서 가이드 및 샘플 확인.
|
||||
- **완료 증거:** 정확한 파일 두 개 작성, `USER REQUIREMENTS`와 `AUTHOR PROPOSALS` 구분.
|
||||
- **범위 밖:** 코드 구현, 추가 계획 문서.
|
||||
- **TDD 예외 사유:** 문서 전용 변경으로 실행 동작이 없다.
|
||||
- **대체 검증 방법:** 요구사항 표와 문서 가이드 대조.
|
||||
|
||||
**Files:** `docs/20261029_가로형서브배너/prd.md`, `docs/20261029_가로형서브배너/plan-task.md`
|
||||
|
||||
- [x] 요구사항, 비목표, 외부 의존, 결정 기록 작성.
|
||||
- [x] 구현 계획을 문서 준비 범위로 한정.
|
||||
|
||||
### Task 1.2 계약 및 독립 프롬프트
|
||||
|
||||
**Goal 실행 `DOC-T2`:** API 제안과 독립 실행 가능한 앱·관리자 프롬프트를 작성한다.
|
||||
|
||||
- **시작 조건:** `DOC-T1` 문서 완료.
|
||||
- **완료 증거:** 요청된 세 파일 작성, API 필드/라우트/오류 상태가 PRD와 일치, 각 prompt에 필요한 계약을 자체 포함.
|
||||
- **범위 밖:** 서버 구현 지시, 실 API 연결, 특정 앱/admin 코드베이스 존재 가정.
|
||||
- **TDD 예외 사유:** 문서 전용 변경으로 실행 동작이 없다.
|
||||
- **대체 검증 방법:** PRD/API/prompt 사이 필드, 인증, 주소 처리, 차단 조건 교차 점검.
|
||||
|
||||
**Files:** `docs/20261029_가로형서브배너/api-contract.md`, `docs/20261029_가로형서브배너/app-prompt.md`, `docs/20261029_가로형서브배너/admin-prompt.md`
|
||||
|
||||
- [x] endpoint와 전송 형식을 하나의 최소 제안으로 고정한다.
|
||||
- [x] 두 prompt를 별도 사용 가능하게 만들고 백엔드 구현 지시를 제외한다.
|
||||
- [x] 콘솔에 prompt 본문을 출력하지 않는다.
|
||||
|
||||
### Task 1.3 출처 표기 및 독립 사용성 보정
|
||||
|
||||
**Goal 실행 `DOC-T3`:** 초안 검토에서 발견한 확정 요구·제안 혼용을 고치고 프롬프트의 독립 사용성을 검증한다.
|
||||
|
||||
- **시작 조건:** `DOC-T2` 작성 완료와 부모 검토.
|
||||
- **완료 증거:** 제안 상태, 응답 예시, 주소 조합표, URL 검증 및 순서 설명을 부모가 직접 대조.
|
||||
- **범위 밖:** 새로운 제품 결정, 서버 구현 및 실제 앱·관리자 연결.
|
||||
- **Files:** 같은 디렉터리의 `prd.md`, `api-contract.md`, `app-prompt.md`, `admin-prompt.md`.
|
||||
- **TDD 예외 사유:** 실행 코드가 없는 문서 보정.
|
||||
- **대체 검증 방법:** 아래 Progress의 직접 대조·독립 검토·공백 검사.
|
||||
- [x] 사용자 확정 범위와 작성자 제안을 분리하고 두 프롬프트의 계약·검증 사례를 보완했다.
|
||||
|
||||
### DOC-T4 후속: 관리자 노출 기간
|
||||
|
||||
**Goal 실행 `DOC-T4`:** 사용자 확인을 받은 콘텐츠 배너 동등 노출 기간 설정을 기존 다섯 문서에 일관되게 반영한다.
|
||||
|
||||
- **시작 조건:** 사용자가 관리자 페이지에서 콘텐츠 배너와 동일하게 노출 기간 설정이 필요하다고 확인했다.
|
||||
- **범위:** 기존 문서만 수정한다. 구현 계획의 TDD 예외는 문서 전용 변경으로 실행 동작이 없기 때문이다.
|
||||
- **Files:** `docs/20261029_가로형서브배너/prd.md`, `docs/20261029_가로형서브배너/plan-task.md`, `docs/20261029_가로형서브배너/api-contract.md`, `docs/20261029_가로형서브배너/app-prompt.md`, `docs/20261029_가로형서브배너/admin-prompt.md`.
|
||||
- **완료 기준:** 시작·종료 경계 의미, KST 입력/응답 형식, create/update 생략·null 규칙, 기간 역전 검증, 공개 응답 DTO 불변, 관리자 기간 무관 목록, 앱 기간 필터와 OS 필터가 모든 문서에서 일치한다. 관리자 프롬프트에 KST 입력, 경계 개별 해제, 오류/편집 보존 및 기간 외 기록 편집 기준을 포함한다.
|
||||
- **TDD 예외 사유:** 문서 전용 변경으로 실행 동작이 없다.
|
||||
- **대체 검증 방법:** 다섯 문서의 용어·JSON 예시·요청/응답 규칙 상호 대조와 공백 검사.
|
||||
- **부모 검증:** 아래 후속 `DOC-T4-GATE`와 부모 작업자의 `./gradlew tasks --all` 검증은 별도로 수행한다. 과거 `DOC-GATE` 완료 기록은 유지한다.
|
||||
- [x] 다섯 기존 문서에 후속 요구와 독립 프롬프트 세부사항을 반영한다.
|
||||
- [x] 부모가 문서 내용을 검토하고 후속 검증을 수행했다.
|
||||
|
||||
#### 후속 Gate `DOC-T4-GATE`
|
||||
|
||||
- **시작 조건:** `DOC-T4` 문서 보완 완료.
|
||||
- **완료 증거:** 기존 콘텐츠 배너 규칙과 다섯 문서 대조, 공백 검사, Gradle 문서 검증 명령 결과 기록.
|
||||
- **범위 밖:** 실제 API·관리자 화면 구현 및 실연동 완료 판정.
|
||||
- [x] 부모가 노출 기간 문서의 후속 검증을 완료했다.
|
||||
|
||||
### Phase 1 Gate `DOC-GATE`
|
||||
|
||||
- **시작 조건:** `DOC-T1`, `DOC-T2`, `DOC-T3` 작성 완료.
|
||||
- **완료 증거:** 요청된 파일만 관리했으며 문서 내용과 경로를 대조.
|
||||
- **범위 밖:** 제품 기능 구현 완료, API 제공·배포 완료 판정.
|
||||
- [x] 부모 작업자가 변경 파일과 요구사항 추적을 검증했다.
|
||||
|
||||
`DOC-GATE`의 기존 완료 기록은 `DOC-T3` 당시 검증 기록이다. `DOC-T4` 후속 반영은 별도 `DOC-T4-GATE`와 아래 부모 검증 기록으로 판정하며 과거 완료 기록으로 대체하지 않는다.
|
||||
|
||||
검증 명령: 부모 작업자가 `./gradlew tasks --all`을 실행했고 `BUILD SUCCESSFUL`, 종료 코드 0을 확인했다. 상세 증거는 아래에 누적한다.
|
||||
|
||||
## Historical DOC-T1~DOC-T4 변경 금지
|
||||
|
||||
아래 지침은 당시 문서-only 작업 범위의 기록이며 새 backend 계획에는 적용하지 않는다.
|
||||
|
||||
- 기존 다섯 파일 외의 파일을 변경하지 않는다.
|
||||
- backend, DB/schema/migration, architecture, 배포 구현 지시를 작성하지 않는다.
|
||||
- 제안 경로를 실제 배포 경로라고 표현하지 않는다.
|
||||
- 요청된 다섯 파일 이외의 파일을 만들지 않는다.
|
||||
|
||||
## Progress
|
||||
|
||||
### `DOC-T1` 1차 실행, 2026-10-29
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: PRD와 문서 준비 계획 작성.
|
||||
- 왜: 앱/관리자 구현 요청 전에 사용자 요구와 문서 범위를 분명히 하기 위해.
|
||||
- 어떻게: 프로젝트 작업절차·문서유지보수 가이드 및 샘플과 기존 API 근거를 읽고 요구사항을 분리해 기록.
|
||||
- 남은 항목: `DOC-T2`, 부모 최종 검증.
|
||||
- 다음 행동: 계약과 프롬프트 작성.
|
||||
|
||||
### `DOC-T2` 1차 실행, 2026-10-29
|
||||
|
||||
- 상태: 작성 완료, 부모 검증 대기
|
||||
- 무엇을: API 제안, 독립 앱 프롬프트, 독립 관리자 프롬프트 작성.
|
||||
- 왜: 별도 담당자가 현재 코드베이스나 prompt 원문 없이도 요구와 제안 계약을 이해하도록 하기 위해.
|
||||
- 어떻게: PRD의 요구사항 및 차단 경계를 계약 모델·endpoint·수용 기준에 대조.
|
||||
- 남은 항목: 부모 작업자의 문서 검토와 `./gradlew tasks --all`.
|
||||
- 다음 행동: 부모가 Gate 검증을 기록한다.
|
||||
|
||||
### `DOC-T3` 후속 보정 및 `DOC-GATE`, 2026-10-29
|
||||
|
||||
- 상태: 문서 보정·최종 검증 완료. 제품 기능과 신규 API는 미구현이다.
|
||||
- 무엇을: 사용자 확정 요구와 작성자 제안의 출처를 정정하고, 두 독립 프롬프트의 JSON 예시·8가지 주소 조합·오류 처리·multipart·null 삭제 규칙을 대조했다.
|
||||
- 왜: 실제로 제공되지 않은 API나 승인되지 않은 링크 우선순위를 확정 계약처럼 전달하지 않기 위해.
|
||||
- 어떻게: 부모가 다섯 파일 전체와 기존 추천/관리자 Controller·배너 DTO·공통 응답 근거를 직접 읽었다. 앱의 위험 URI scheme 표기를 `javascript`로 정정하고 대소문자 무관 검사를 명시했다.
|
||||
- 독립 검토: 첫 검토 에이전트는 모델 호환성 오류로 실행되지 않았다. 대체 검토는 완료됐으며 순서 충돌 후보 1건을 보고했다. 부모 대조 결과 서버의 id 오름차순과 클라이언트의 응답 순서 보존은 양립하므로 충돌은 아니다. 오해를 줄이도록 두 프롬프트에 서버 순서와 클라이언트 재정렬 금지를 함께 적었다. 그 외 중대한 누락은 보고되지 않았다.
|
||||
- 명령 검증: `./gradlew tasks --all` 재실행은 `BUILD SUCCESSFUL in 999ms`, 종료 코드 0이었다. 기존 Gradle deprecation 안내가 있었다.
|
||||
- 공백 검증: 다섯 파일 각각 `GIT_MASTER=1 /usr/bin/git diff --no-index --check -- /dev/null 파일경로` 실행, 출력 없음·종료 코드 0. 최초 셸 검사에서 zsh의 예약 변수 `path`로 명령 검색이 실패해 `doc_path`와 절대 git 경로로 고친 뒤 재실행했다. 파일 내용 문제는 아니었다.
|
||||
- 변경 범위: `GIT_MASTER=1 /usr/bin/git -c core.quotePath=false status --short --untracked-files=all`에서 해당 디렉터리의 새 Markdown 다섯 개만 확인했다. 기존 파일 변경, 커밋, 배포는 없다.
|
||||
- 생략·한계: 문서만 변경하여 전체 테스트·빌드 및 실제 앱/관리자 화면 QA는 실행하지 않았다. Markdown LSP 진단 도구가 제공되지 않아 직접 내용 대조와 공백 검사로 대체했다. 신규 API 실연동은 계약 합의·제공 후 별도 검증이 필요하다.
|
||||
|
||||
### `DOC-T4` 후속 반영, 2026-10-29
|
||||
|
||||
- 상태: 문서 수정 완료, 부모 검증 대기. 기존 `DOC-GATE`의 과거 완료 상태는 변경하지 않으며 이번 검증은 `DOC-T4-GATE`로 구분한다.
|
||||
- 무엇을: 콘텐츠 배너와 같은 기간 설정 요구를 기존 다섯 문서에 반영한다.
|
||||
- 왜: 관리자가 가로형 서브 배너에도 예약 노출 경계를 설정하고 목록에서 기간 외 레코드까지 관리할 수 있도록 계약 제안을 명확히 하기 위해.
|
||||
- 어떻게: 기존 콘텐츠 배너 예약 PRD와 구현을 근거로 KST 분 정밀도, 종료 분 inclusive 경계, 생성/수정 null 의미를 문서화한다. 공개 `SubBanner`는 유지하고 기간 필터는 앱 조회 보장으로만 정의한다.
|
||||
- 남은 항목: 부모 작업자의 문서 검토, `./gradlew tasks --all` 및 최종 문서 검증.
|
||||
- 변경 범위: 지정된 기존 다섯 Markdown만 수정. 코드·신규 파일·API 구현은 없다.
|
||||
- 생략·한계: 이 후속 문서 작업에서는 Gradle, 런타임 및 배포 검증을 수행하지 않았다. 신규 API는 제안 상태이며 구현·승인은 별도다.
|
||||
|
||||
### `DOC-T4-GATE` 부모 검증, 2026-10-29
|
||||
|
||||
- 상태: 문서 후속 검증 완료. 신규 API와 실제 관리자·앱 기능의 구현 완료를 뜻하지 않는다.
|
||||
- 무엇을·왜: 다섯 문서를 직접 읽고 기존 콘텐츠 배너의 PRD·Service·응답 formatter·노출 조건과 대조하여 날짜 계약의 차이를 방지했다.
|
||||
- 대조 결과: KST 분 입력/응답과 초 단위 포함 경계를 구분했고, null/null·한쪽 경계·동일 분·역전/잘못된 입력·수정 생략 유지와 null 해제 규칙이 일치한다. 관리자 목록은 기간 무관, 공개 목록은 기간 내 항목만 반환하며 공개 DTO와 기존 OS 링크 필터는 유지한다.
|
||||
- 보정: 관리자 응답의 기존 필드 타입을 보존하고 요청·응답 JSON 예시의 기간을 맞췄다. 기간만 수정할 때 기존 주소와 이미지를 보존하도록 명시했으며, 과거 Gate와 후속 Gate를 분리했다.
|
||||
- 명령: `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, 종료 코드 0. 다섯 파일 각각 `GIT_MASTER=1 /usr/bin/git diff --no-index --check -- /dev/null 파일경로`도 출력 없음·종료 코드 0이었다.
|
||||
- 범위: `GIT_MASTER=1 /usr/bin/git -c core.quotePath=false status --short --untracked-files=all`에서 기존 서브 배너 Markdown 다섯 개만 확인했다. 새 문서·코드 변경·커밋·배포는 없다.
|
||||
- 생략·한계: 문서 변경이므로 전체 테스트·빌드 및 실제 관리자/앱 UI·API 연동은 검증하지 않았다. Markdown LSP 도구 미제공으로 직접 계약 대조와 공백 검사로 대체했다. 신규 API는 계약 제안 상태를 유지한다.
|
||||
|
||||
## Phase 2: 데이터 모델 및 기간 규칙
|
||||
|
||||
**Phase 결과:** 새로운 독립 테이블과 도메인/persistence 계층에서 배너를 저장하고 placement, 주소, 기간 규칙을 보장한다.
|
||||
|
||||
**선행조건:** 부모가 backend 요구사항과 제안 API를 검토하고 구현 착수를 별도 승인. 과거 Phase 1/DOC Gate 완료는 backend 구현 승인이 아니다.
|
||||
|
||||
### Task 2.1 독립 데이터 모델, 기간 처리와 DDL
|
||||
|
||||
**Goal 실행 `P2-T1`:** `SubBanner` 저장 모델과 독립 SQL artifact를 정의하고 DB 왕복으로 초 정밀도 및 기간 규칙을 증명한다.
|
||||
|
||||
- **요구사항:** `SB-001`, `SB-002`, `SB-006`, `SB-010`.
|
||||
- **완료 증거:** 도메인 및 repository test, DDL의 테이블/컬럼 comment와 nullable UTC second-precision period, JPA flush/clear/reload 결과 기록.
|
||||
- **시작 조건:** backend 착수 승인. 첫 구현 checkpoint에서 GIF 허용 여부와 S3 object 삭제/교체 실패 시 정책을 검토하고 사용자 확인이 필요한 선택은 구현 전에 결정 기록에 추가한다. 권고안은 기존 Tika `image/*` 검증을 재사용하고 GIF 허용, 기존 DB 변경 없음, S3 자동 cleanup 미보장이다.
|
||||
- **범위 밖:** API/controller, multipart upload, 운영 DB 적용, 기존 content_banner migration.
|
||||
- **TDD 예외 사유:** 없음.
|
||||
|
||||
**Files (모두 PROPOSED):**
|
||||
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/domain/SubBanner.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/domain/SubBannerPlacement.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/port/out/SubBannerRepository.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/out/persistence/SubBannerEntity.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/out/persistence/DefaultSubBannerRepository.kt`
|
||||
- Create: `docs/20261029_가로형서브배너/create-sub-banner.sql`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/subbanner/domain/SubBannerTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/out/persistence/SubBannerPersistenceTest.kt`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Produces: `enum class SubBannerPlacement { HOME_RECOMMENDATION, CONTENT_RECOMMENDATION }`; `data class SubBanner(val id: Long? = null, val placement: SubBannerPlacement, val imagePath: String, val webUrl: String?, val androidDeepLink: String?, val iosDeepLink: String?, val startDate: LocalDateTime?, val endDate: LocalDateTime?)`; `interface SubBannerRepository { fun findById(id: Long): SubBanner?; fun findAllByPlacement(placement: SubBannerPlacement): List<SubBanner>; fun findVisibleByPlacement(placement: SubBannerPlacement, now: LocalDateTime): List<SubBanner>; fun save(subBanner: SubBanner): SubBanner; fun deleteById(id: Long) }`. `imagePath`는 S3 object key이며 DTO에서만 기존 `cloud.aws.cloud-front.host`와 조합해 `imageUrl`을 만든다.
|
||||
- Consumes: 기존 `BaseEntity`, QueryDSL/JPA와 프로젝트 DDL convention. 만든 repository의 `findVisibleByPlacement`에서 null 경계 독립 unbounded, 양끝 포함, id asc 정렬.
|
||||
|
||||
제안 테이블은 `sub_banner`이며 아래 매핑을 `SubBannerEntity` 및 향후 SQL에 함께 적용한다. 이는 검토용 저장 설계이지 기존 테이블이나 확정된 운영 스키마가 아니다.
|
||||
|
||||
| 컬럼 | 제안 MySQL 타입·규칙 | 매핑 |
|
||||
|---|---|---|
|
||||
| `id` | `BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY` | `BaseEntity.id` |
|
||||
| `placement` | `VARCHAR(32) NOT NULL` | `SubBannerPlacement`, enum 이름 저장 |
|
||||
| `image_path` | `LONGTEXT NOT NULL` | 서버가 생성한 S3 object key, 절대 URL 아님 |
|
||||
| `web_url`, `android_deep_link`, `ios_deep_link` | 각각 `LONGTEXT NULL` | 기존 URI를 255자 등으로 잘라 저장하지 않음 |
|
||||
| `start_date`, `end_date` | 각각 `TIMESTAMP NULL DEFAULT NULL` | UTC 초 단위, H2 검증 컬럼은 `TIMESTAMP(0)` |
|
||||
| `is_active` | `BOOLEAN NOT NULL DEFAULT TRUE` | `SubBannerEntity.isActive`; P5 후속 소프트 삭제 시 false |
|
||||
| `created_at` | `TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP` | `BaseEntity.createdAt` |
|
||||
| `updated_at` | `TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | `BaseEntity.updatedAt` |
|
||||
|
||||
모든 컬럼·테이블에 MySQL COMMENT를 붙이고 `(placement, id)` 인덱스를 둔다. `BaseEntity`와 전역 시간 설정은 변경하지 않는다. DDL은 신규 table CREATE 한 개이며 rollback 제안은 데이터 보존 확인 및 별도 승인 이후 해당 신규 table만 DROP, 자동 rollback 실행은 금지다. 삭제는 승인된 구현에서 해당 DB row만 제거하는 제안이며 S3 object는 자동 삭제하지 않는다.
|
||||
|
||||
- [x] **RED:** 링크 불변식·기간·목록·수정·삭제의 실패 테스트를 먼저 작성했다.
|
||||
- [x] **RED 확인:** 위 두 focused 클래스에서 26개 중 19개 assertion failure를 확인했다. 컴파일 실패는 RED 증거로 세지 않았다.
|
||||
- [x] **GREEN:** domain, Entity, QueryDSL repository와 신규 테이블 DDL을 구현했다.
|
||||
- [x] **GREEN 확인:** 같은 focused 테스트 26개 통과. 실제 H2 `TIMESTAMP(0)`의 flush/clear/reload, 긴 URI, 기간·placement·25개 초과 목록·수정·삭제를 검증했다.
|
||||
- [x] **REFACTOR:** 직접 영향 배너·creatorname 회귀를 포함한 50개 테스트와 `ktlintCheck` 통과. 부모의 별도 focused 재실행도 26개 통과했다.
|
||||
|
||||
### Phase 2 Gate
|
||||
|
||||
**Goal 실행 `P2-GATE`:** 독립 schema와 시간/persistence 경계를 backend feature로 확정한다.
|
||||
|
||||
- **시작 조건:** `P2-T1` 완료.
|
||||
- **완료 증거:** focused 테스트 통과, SQL review에서 신규 table 외 변경 없음, 결정 checkpoint 기록.
|
||||
- **범위 밖:** 운영 DB 적용, app/admin 구현.
|
||||
- **검증:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subbanner.*'` 및 `./gradlew ktlintCheck`, 둘 다 **구현 시 실행, 이번 작업 미실행**. 전체 test/build는 공통 보안/설정 영향 또는 final Gate 등 project guide 조건에서만 실행한다.
|
||||
- **Expected:** DB reload 뒤 날짜 정확도와 모든 허용 링크 조합 확인, ktlint 성공, live DDL 실행 0회.
|
||||
- [x] `P2-T1` 완료 증거 확인. 독립 P2 리뷰는 명세·품질 승인, 차단 사항 없음.
|
||||
- [x] DDL과 Entity의 테이블·컬럼·정밀도·index·COMMENT 일치 및 기존 데이터 비변경을 검토했다. 운영 적용·rollback은 별도 승인 대상이다.
|
||||
|
||||
| Goal | 선행조건 | 종속 산출물 |
|
||||
|---|---|---|
|
||||
| `P2-T1` | backend 착수 승인 | domain/repository 인터페이스, 신규 SQL artifact |
|
||||
| `P2-GATE` | `P2-T1` | Phase 3 착수 조건 |
|
||||
|
||||
## Phase 3: 관리자 API 및 multipart 이미지
|
||||
|
||||
**Phase 결과:** ADMIN만 multipart 기반 CRUD를 수행하고, 유효성 검사 실패가 image upload/DB 변경 없이 끝난다.
|
||||
|
||||
**선행조건:** `P2-GATE` 완료 및 관리자 API contract 검토.
|
||||
|
||||
### Task 3.1 관리자 CRUD
|
||||
|
||||
**Goal 실행 `P3-T1`:** 관리자 CRUD와 이미지 업로드를 구현하고 요청 검증, 병합 기간, 응답 envelope를 통합 테스트로 확인한다.
|
||||
|
||||
- **요구사항:** `SB-003`~`SB-007`, `SB-011`, `SB-012`.
|
||||
- **완료 증거:** create/list/update/delete 통합 테스트, DB 저장된 URI exact preservation, S3 object key의 기존 CDN URL 변환, 기간 KST exact roundtrip, 검증 실패 전 upload/save 없음.
|
||||
- **시작 조건:** `P2-GATE`, ADMIN route/API draft review. 이미지 정책은 `P2-T1` decision checkpoint에서 확정.
|
||||
- **범위 밖:** 공개 조회, ordering, cleanup scheduler, 공통 exception rewrite, 무단 400/404 보장.
|
||||
|
||||
**Files (Create는 신규 제안 경로, Reference는 기존 파일):**
|
||||
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/in/web/AdminSubBannerController.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/dto/SubBannerRequest.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/dto/AdminSubBannerResponse.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/application/SubBannerService.kt`
|
||||
- Reference: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`의 관리자 경로는 기존 `anyRequest().authenticated()` 유지, 별도 permitAll 추가나 이 Task의 설정 수정은 없다.
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/in/web/AdminSubBannerControllerIntegrationTest.kt`
|
||||
- 서비스 동작은 위 실제 HTTP→service→JPA 통합 테스트에서 검증한다. 같은 시나리오만 반복하는 별도 service unit test는 추가하지 않는다.
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `SubBannerRepository`, `SubBannerPlacement`, 기존 `S3Uploader.upload(inputStream: InputStream, bucket: String, filePath: String, metadata: ObjectMetadata? = null): String`의 반환 object key, 해당 key를 public `imageUrl`로 만드는 기존 CDN 변환 방식, `validateImage(file: MultipartFile, gifAllowed: Boolean)`.
|
||||
- Produces: `SubBannerService.findAllByPlacement(placement: SubBannerPlacement): List<SubBanner>`, `create(image: MultipartFile, request: String): SubBanner`, `update(id: Long, image: MultipartFile?, request: String): SubBanner`, `delete(id: Long): Unit`. 서비스는 raw JSON의 필드 존재 여부·타입을 검사한 뒤 DTO로 변환하여 생략된 날짜와 명시적 null을 잃지 않는다. 기본 4필드의 누락도 검증한다. Controller at `/admin/sub-banners`: `GET list(placement): ApiResponse<List<AdminSubBannerResponse>>`; `POST create(image: MultipartFile, request: String): ApiResponse<AdminSubBannerResponse>`; `PUT /{id} update(id: Long, image: MultipartFile?, request: String): ApiResponse<AdminSubBannerResponse>`; `DELETE /{id} delete(id: Long): ApiResponse<Any>`. DELETE는 서비스의 Unit을 data로 전달하지 않고 `data:null`인 성공 응답을 만든다. Controller class ADMIN preauthorization. `AdminSubBannerResponse` has `id, placement, imageUrl, webUrl, androidDeepLink, iosDeepLink, startDate, endDate`; create/update request wire fields follow `api-contract.md`. 공개 서비스 메서드는 `P4-T1`에서 추가한다.
|
||||
|
||||
- [x] **RED:** 실제 HTTP/JWT/H2/S3 대체 경계에서 CRUD·8개 주소 조합·JSON/URI/날짜 검증·생략/null·이미지 실패·기간 무관 목록 테스트를 먼저 작성했다.
|
||||
- [x] **RED:** 익명 및 모든 비관리자 역할 거부, 없는 id의 404·무변경 테스트도 작성했다.
|
||||
- [x] **RED 확인:** 신규 HTTP 테스트 39개에서 assertion failure 39개를 확인했다. 이후 DST gap 회귀에서도 43개 중 2개가 기대 400/실제 200으로 실패했다.
|
||||
- [x] **GREEN:** 관리자 controller·service·DTO를 구현했다. 검증 후에만 업로드/저장하며 트랜잭션·이미지 보존·row 삭제·실제 오류 계약을 적용했다.
|
||||
- [x] **GREEN 확인:** 최초 39/39, DST 회귀 수정 후 실제 HTTP 43/43 통과. POST/PUT 시작·종료 gap 거부·upload 0·DB 불변과 정상 과거/overlap 왕복을 확인했다.
|
||||
- [x] **REFACTOR:** 수정 전 직접 영향 회귀 161/161 및 수정 후 focused 43/43·`ktlintCheck` 통과. 파일명은 클래스명에 맞춰 단수형으로 정리했다.
|
||||
|
||||
### Phase 3 Gate
|
||||
|
||||
**Goal 실행 `P3-GATE`:** CRUD 및 업로드의 검증 순서와 계약 응답을 판정한다.
|
||||
|
||||
- **시작 조건:** `P3-T1` 완료.
|
||||
- **완료 증거:** multipart integration test, 저장 후 DB 확인, validation-failure 무변경 증거, 공통 응답 envelope 대조.
|
||||
- **범위 밖:** public GET, browser UI, S3 object cleanup 보장.
|
||||
- **검증:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subbanner.adapter.in.web.AdminSubBannerControllerIntegrationTest'`와 `./gradlew ktlintCheck`. 실제 security 공통 영향을 수정했을 때는 `./gradlew test`로 검증을 확대한다. 실행 증거는 아래 Progress에 기록한다.
|
||||
- **Expected:** 검증 실패는 upload/DB mutation 0회, 성공 POST/PUT 객체 및 DELETE null 응답, ADMIN만 변경.
|
||||
- [x] `P3-T1` 완료 증거 확인. 독립 P3 리뷰의 DST blocker를 RED→GREEN으로 수정하고 scoped 재검토 승인.
|
||||
- [x] 실제 400/404 Boot 오류 body, 익명 401, 비관리자·multipart 바인딩 오류의 기존 200 `success:false`를 HTTP로 확인했다. 소비자 문서의 최종 계약 동기화는 P4 최종 Gate에서 수행한다.
|
||||
|
||||
| Goal | 선행조건 | 종속 산출물 |
|
||||
|---|---|---|
|
||||
| `P3-T1` | `P2-GATE`와 API contract review | 관리자 CRUD endpoint 및 request/response DTO |
|
||||
| `P3-GATE` | `P3-T1` | Phase 4 착수 조건 |
|
||||
|
||||
## Phase 4: 공개 기간 조회 및 보안 Gate
|
||||
|
||||
**Phase 결과:** 정확한 공개 GET만 익명 허용하고 현재 기간의 전체 목록을 id asc로 반환한다. 관리자/인접 method 보호를 end-to-end 확인한다.
|
||||
|
||||
**선행조건:** `P3-GATE` 완료.
|
||||
|
||||
### Task 4.1 공개 GET, 기간 filtering과 권한 통합 검증
|
||||
|
||||
**Goal 실행 `P4-T1`:** 익명 공개 조회와 접근 정책을 구현하고 운영 경로에 실제 공개 범위 외 노출이 없음을 검증한다.
|
||||
|
||||
- **요구사항:** `SB-007`~`SB-009`, `SB-011`.
|
||||
- **완료 증거:** public/admin DTO 차이, placement/date/ordering, 정확한 GET 공개와 보호된 변형 경로 및 role test.
|
||||
- **시작 조건:** `P3-GATE` 완료, proposed endpoint contract 확인.
|
||||
- **범위 밖:** app OS link filtering, UI, pagination, active toggle, scheduler.
|
||||
|
||||
**Files (신규 경로 PROPOSED):**
|
||||
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/in/web/SubBannerController.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/application/SubBannerService.kt` (공개 조회 및 고정 시각 seam 추가)
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/dto/SubBannerResponse.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`, 정확한 `GET /api/v2/sub-banners`만 permitAll.
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/in/web/SubBannerControllerIntegrationTest.kt`
|
||||
- 보안 시나리오는 같은 실제 HTTP/JWT/H2 context의 `SubBannerControllerIntegrationTest`에 통합했다. 중복 HTTP fixture와 별도 security test 클래스는 만들지 않았다.
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `P2-T1`의 `SubBannerRepository.findVisibleByPlacement(placement: SubBannerPlacement, now: LocalDateTime): List<SubBanner>`와 `P3-T1`의 CDN URL 조립 규칙.
|
||||
- Produces: `SubBannerService.findVisibleByPlacement(placement: SubBannerPlacement): List<SubBanner>`와 `SubBannerController.getSubBanners(placement: SubBannerPlacement): ApiResponse<List<SubBannerResponse>>`. 서비스는 기존 `nowProvider: () -> LocalDateTime` 주입 패턴으로 UTC 시각을 한 번 얻어 초 단위 절삭 후 같은 값을 repository에 전달한다. public DTO는 정확히 `id, placement, imageUrl, webUrl, androidDeepLink, iosDeepLink`, 날짜 미포함. `SecurityConfig`에서 해당 GET만 익명 허용.
|
||||
|
||||
- [x] **RED:** 고정 시각·실제 DB·HTTP로 23개 공개 조회/권한 테스트를 작성했다. 두 placement, 전체 25개 목록, 빈 결과, nullable·초·소수초·자정 경계, 6필드 DTO 및 OS 비필터를 포함한다.
|
||||
- [x] **RED 확인:** 공개 경로 미구현 상태에서 23개 중 22개 assertion failure(기대 200/400, 실제 401)를 확인했다.
|
||||
- [x] **GREEN:** public controller/DTO·서비스 조회 및 정확한 GET matcher 한 줄을 추가했다. 기존 권한·추천 응답은 유지했다.
|
||||
- [x] **GREEN 확인:** 실제 HTTP 23/23 통과. GET 외 method·HEAD·인접 경로·관리자 보호와 요청 시각 1회 조회를 검증했다.
|
||||
- [x] **REFACTOR:** 기존 홈/오디오 추천 회귀 포함 96/96 및 ktlint 통과. 부모의 최종 전체 suite에서도 신규 92개를 포함한 2,676개가 통과했다.
|
||||
|
||||
### Phase 4 Gate
|
||||
|
||||
**Goal 실행 `P4-GATE`:** 신규 public/admin endpoint의 데이터·security·API 계약을 최종 검증한다.
|
||||
|
||||
- **시작 조건:** `P4-T1` 완료.
|
||||
- **완료 증거:** Phase 2~4 focused tests 통과, 신규 SQL review, security behavior와 실제 공통 오류 응답 기록.
|
||||
- **범위 밖:** API 배포/운영 DB 적용, 앱·관리자 client 연동 완료 주장.
|
||||
- **검증:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subbanner.*'` 및 관련 추천 회귀로 확인한 뒤 보안 설정 변경과 사용자 ulw 요구에 따라 부모가 `./gradlew test ktlintCheck bootJar --continue --console=plain`을 실행했다. 결과는 아래 최종 검증 기록을 따른다.
|
||||
- **직접 영향 회귀:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.HomeRecommendationResponseTest' --tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest'`는 **구현 시 실행, 이번 작업 미실행**이다. 기존 추천 응답이 바뀌지 않았는지 확인한다.
|
||||
- **Expected:** focused test 성공, backend artifact bootJar 성공, exact public GET만 anonymous, 관리자 CRUD ADMIN-only, 전체 8 조합 및 기간/DB 정밀도 규칙 통과. 운영 DDL 실행과 app integration 완료 판정 0건.
|
||||
- [x] `P4-T1` 및 모든 선행 Gate 증거 확인. 전체 테스트·lint·bootJar와 독립 최종 코드 리뷰 통과.
|
||||
- [x] API 계약·앱/관리자 프롬프트를 실제 구현에 맞추고 미배포·운영 DB 미적용 상태를 구분했다.
|
||||
|
||||
| Goal | 선행조건 | 종속 산출물 |
|
||||
|---|---|---|
|
||||
| `P4-T1` | `P3-GATE` | public GET, 기간 query, exact security matcher |
|
||||
| `P4-GATE` | `P2-GATE`, `P3-GATE`, `P4-T1` | backend 구현 검증, 별도 client integration 재개 조건 |
|
||||
|
||||
### 백엔드 실행 의존성
|
||||
|
||||
```text
|
||||
P2-T1 → P2-GATE → P3-T1 → P3-GATE → P4-T1 → P4-GATE
|
||||
```
|
||||
|
||||
backend checkbox는 실제 검증 후 갱신한다. 문서 작성/부모 검토 완료만으로 구현 완료를 표시하지 않는다. 사용자 구현 승인에 따라 순차적으로 한 개의 Goal만 active한다.
|
||||
|
||||
## Current Progress
|
||||
|
||||
### `DOC-T5` 문서 변환 초안, 2026-10-29
|
||||
|
||||
- 상태: 작성 초안 완료, 작성자 대조 및 부모 검증 대기.
|
||||
- 무엇을: 기존 `prd.md`와 `plan-task.md`에 backend requirements와 `P2`~`P4` 향후 implementation plan을 추가했다. DOC-T1~4 역사적 checkbox/Gate/Progress는 유지했다.
|
||||
- 왜: 앱·관리자 실제 구현 전에 서버 기능과 persistence를 codebase 근거에 맞게 검토할 수 있도록 하기 위해.
|
||||
- 어떻게: API draft, project guides 및 기존 security, S3, image validation, reservation, exception, JPA test patterns를 대조했다. API 전면 승인·배포·구현 상태로 표현하지 않았다.
|
||||
- 검증: PRD ID와 Goal, exact paths/signatures, proposed API shapes, 향후 DDL, historical checkboxes 및 미착수 backend checkboxes를 직접 대조했다. 이번 작업에서 Gradle/test/build를 실행하지 않았다.
|
||||
- 남은 항목: 작성자 문서 대조 및 부모 검토. backend 구현은 별도 승인 대기.
|
||||
- 다음 행동: 부모가 이 문서 Gate를 검증하고 반영한다.
|
||||
|
||||
### `DOC-T5` 부모 최종 문서 검증, 2026-10-29
|
||||
|
||||
- 상태: 백엔드 PRD·계획 문서 보완 및 문서 검증 지침 수정 완료. 백엔드 구현은 미착수이며 `P2-T1`과 모든 후속 구현 Task/Gate는 미완료다.
|
||||
- 무엇을·왜: 프롬프트 작성 계획을 실제 백엔드 개발에 사용할 수 있는 요구사항·파일·인터페이스·TDD·Gate 계획으로 보완하고, 문서만 바꿀 때 불필요한 Gradle 실행을 제거했다.
|
||||
- 문서 대조: `SB-001`~`SB-012`를 `P2-T1`·`P3-T1`·`P4-T1`과 연결했다. 사용자 확정과 작성자 제안을 구분하고, 8개 주소 존재 조합 중 모두 없는 경우만 거부·나머지 7개 허용으로 PRD/테스트 이름/계약을 맞췄다. 기간 형식·생략/null·노출 경계·관리자/공개 DTO는 기존 API 초안과 일치한다.
|
||||
- 파일·책임 대조: 신규 파일은 모두 제안 경로로 표시했다. 저장 object key인 `imagePath`와 응답 `imageUrl`을 구분하고 각 Phase의 서비스 제공 메서드, 변경 파일, 신규 테이블 매핑을 일치시켰다. 기존 구현과 회귀 테스트 경로를 직접 확인했다.
|
||||
- 이력 대조: `DOC-T1`~`DOC-T4`의 완료 체크박스와 과거 Gradle 실행 결과는 삭제하거나 새 작업 증거로 재사용하지 않았다. 새 백엔드 Task/Gate의 체크박스는 모두 미체크다.
|
||||
- 공백 검사: `GIT_MASTER=1 /usr/bin/git diff --check -- docs/agent-guides/문서유지보수.md`와 PRD/계획 각각의 `git diff --no-index --check -- /dev/null 파일경로`는 출력 없음·종료 코드 0이었다.
|
||||
- 범위 대조: 현재 승인 범위에서 수정한 파일은 이 PRD·계획 및 `docs/agent-guides/문서유지보수.md` 세 개다. 이전 작업에서 존재한 앱/관리자 prompt와 API 초안은 변경 대상에 넣지 않았다. 새 코드·SQL·빌드 설정·배포 변경은 없다.
|
||||
- 미실행·사유: 사용자 승인과 수정한 문서 전용 검증 지침에 따라 이번 작업에서는 `./gradlew tasks --all`·테스트·빌드를 실행하지 않았다. 위 구현 단계의 명령들은 향후 검증 계획이며 이번 실행 결과가 아니다. 실제 API 동작과 배포는 검증하지 않았다.
|
||||
- 다음 행동: PRD/API 제안을 검토하고 별도로 구현 착수를 승인한 뒤 `P2-T1`부터 재개한다. 이미지/GIF 및 S3 객체 보존 정책의 검토 checkpoint를 건너뛰지 않는다.
|
||||
|
||||
### 백엔드 구현 착수, 2026-10-29
|
||||
|
||||
- 승인: 사용자가 "문서대로 백엔드 코드, DDL 생성 진행"을 요청했다. 기존 설계의 독립 리소스·API·권한·기간 계약을 구현 기준으로 사용한다. 운영 DB 및 AWS에 검증용 요청을 보내지 않는다.
|
||||
- 실행 방식: 단일 구현 담당자에게 단계별 작업을 위임하고 부모가 파일·실행 증거를 검증한다. 기존 `test` 브랜치에서 진행하며 새 worktree를 자동 생성하거나 이전 문서 변경을 되돌리지 않는다.
|
||||
- 결정: GIF는 기존 이미지 검증기의 허용 옵션으로 지원하며, 삭제는 DB row에 한정하고 S3 object는 자동 삭제하지 않는다. 입력 오류/없는 리소스의 400/404는 신규 경계에서 처리하고 기존 공통 예외·권한 거부 정책은 변경하지 않는다.
|
||||
- 검증 시나리오: 관리자 실제 HTTP CRUD와 7개 유효 주소 조합, 모두 빈 주소·잘못된 형식의 무변경 거부, KST 생략/null 및 DB 왕복, 기간/placement 공개 조회와 20개 초과 무절단 목록, 익명/비관리자 권한 거부 및 기존 추천 회귀를 확인한다.
|
||||
- 도구 한계: 현재 host는 `lsp_diagnostics`를 제공하지 않는다. Kotlin 컴파일과 ktlint로 정적 오류를 검사하되 LSP 검증 완료로 표현하지 않는다.
|
||||
- 자원: 실행 임시 root는 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/subbanner-ulw.kY4Cw5`다. 생성한 test/QA 자원만 종료·정리하며 기존 공유 Gradle 캐시는 삭제하지 않는다.
|
||||
|
||||
### `P2-T1` 및 `P2-GATE`, 2026-10-29
|
||||
|
||||
- 완료: `SubBanner`·placement·저장소 port·JPA Entity·QueryDSL adapter와 `create-sub-banner.sql`, domain/persistence 테스트를 생성했다.
|
||||
- RED/GREEN: 두 신규 클래스 26 tests 중 의도한 assertion failure 19개 확인 후 26/26 통과. 기존 배너 baseline 19/19, 직접 영향 배너·creatorname 포함 Gate 50/50 및 전체 `ktlintCheck` 통과.
|
||||
- 부모 검증: 신규 두 클래스 focused 명령을 다시 실행해 `BUILD SUCCESSFUL in 16s`, XML domain 2 + persistence 24, failures/errors/skipped 모두 0을 확인했다. 전체 변경 소스·테스트·DDL을 읽었고 독립 리뷰도 P2 승인이다.
|
||||
- 결정: 기간 저장 경계는 repository에서 한 번 초 절삭한 값으로 저장/반환해 DB 반올림을 방지한다. 현재 UTC 시각도 repository query 경계에서 초 절삭한다. 서비스의 UTC 분 입력 정규화와 함께 사용한다.
|
||||
- 한계: 로컬 MySQL 실행 파일과 실행 중 Docker daemon이 없어 운영 MySQL 적용을 검증하지 않았다. H2 MySQL mode와 DDL/Entity 대조로 확인했으며 운영 DB에는 적용하지 않았다. 전체 suite/HTTP는 후속 Gate에서 수행한다.
|
||||
- 별도 DDL 직접 검증: task-owned Java/Hibernate driver가 작성한 SQL을 독립 H2에 적용한 뒤 실제 repository CRUD·초 정밀도·기간·placement·잘못된 주소 조합을 검증했다. `MANUAL QA PASS`, `BUILD SUCCESSFUL in 6s`. 초기 driver naming 설정 및 Java fluent setter 오류를 driver에서만 수정했으며 제품 코드는 변경하지 않았다.
|
||||
|
||||
### `P3-T1` 및 `P3-GATE`, 2026-10-29
|
||||
|
||||
- 완료: 관리자 CRUD·service·request/admin response DTO·실제 random-port JWT HTTP 테스트를 구현했다. 이미지 스트림은 use로 닫고 검증 전에 업로드/저장하지 않는다. 기본 4필드 필수 교체와 날짜 필드의 생략 유지/null 해제를 구분했다.
|
||||
- 증거: baseline 기존 HTTP/service 5/5; 신규 scaffold RED 39 assertion failures → GREEN 39/39. 기존 배너/creatorname 포함 직접 영향 회귀 161/161, full ktlint 통과. 별도 service unit test 대신 실제 HTTP→service→JPA 경로의 비중복 검증을 유지했다.
|
||||
- 리뷰 수정: `1988-05-08 02:30`의 Asia/Seoul DST gap 자동 보정을 JDK17과 실제 HTTP로 재현했다. gap 회귀 2 failures → 유효 offset 부재를 거부하도록 수정 → 43/43 및 ktlint 통과. 독립 scoped 재검토는 ADDRESSED/APPROVE다.
|
||||
- 실제 응답: DELETE HTTP200 `{"success":true,"message":null,"data":null,"errorProperty":null}`. JSON/URI/기간 검증 400과 없는 id 404는 Spring Boot 오류 body. 익명 401, 모든 비관리자와 필수 multipart part 누락은 기존 정책의 HTTP200 `success:false`다.
|
||||
- 저장 한계: MySQL TIMESTAMP가 표현 가능한 UTC `1970-01-01 00:00:01`~`2038-01-19 03:14:07` 밖 입력은 업로드 전에 400으로 거부한다. 이는 임의의 과거 날짜 제한이 아니다. S3 업로드 후 DB 실패 시 새 object 잔존 가능성은 보존하며 자동 정리를 추가하지 않았다.
|
||||
- 부모 확인: 실제 소스·테스트를 읽고 최종 XML 43 tests/failures0/errors0/skipped0을 확인했다. P4 후 부모가 전체 HTTP·회귀·빌드를 다시 실행한다.
|
||||
|
||||
### `P4-T1` 및 `P4-GATE` 최종 실행, 2026-10-29
|
||||
|
||||
- 완료: 공개 controller/6필드 DTO, UTC nowProvider를 한 번 읽는 read-only 서비스 조회, 정확한 `GET /api/v2/sub-banners` 허용 matcher 한 줄과 실제 HTTP 테스트를 구현했다. 기존 추천 API 응답·테이블·공통 예외 정책·dependency는 변경하지 않았다.
|
||||
- RED/GREEN: 공개 HTTP 23개 중 22개 assertion failures(기대 200/400, 실제 401) → 23/23 통과. JSON fixture의 LongNode/IntNode 차이는 기대 map의 serialize/readTree로 해소하고 전체 JSON tree 비교는 유지했다. 공개·관리자 보안 테스트는 동일 실제 HTTP context로 통합했다.
|
||||
- 관련 회귀: 신규 92개(domain 2, persistence 24, 관리자 HTTP 43, 공개 HTTP 23) + 기존 홈/오디오 추천 4개 = 96/96, ktlint 통과.
|
||||
- 부모 최종 명령: `./gradlew test ktlintCheck bootJar --continue --console=plain` → exit 0, `BUILD SUCCESSFUL in 12m 27s`. `build/reports/tests/test/index.html`의 총 2,676 tests, failures 0, ignored 0, 성공률 100%를 확인했다. `build/libs/sodalive-0.0.1-SNAPSHOT.jar` 생성 확인. 현재 코드로 전체 실제 HTTP 테스트도 재실행됐다.
|
||||
- 실제 HTTP 증거: 관리자 POST/PUT 200의 관리자 8필드, DELETE 200 `{"success":true,"message":null,"data":null,"errorProperty":null}`. 익명 공개 GET은 200의 6필드 배열/빈 배열, 누락·잘못된 placement는 400. 익명 POST/PUT/DELETE/HEAD/PATCH와 인접 GET은 401이며 비관리자 admin은 기존 200 `success:false`다. 원본 응답은 `build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.subbanner.adapter.in.web.*.xml`의 `HTTP EVIDENCE`에 기록돼 있다. Authorization/JWT는 출력하지 않았다.
|
||||
- 기간 증거: 고정 now의 시작 `:00`, 종료 `:59.999999999` 포함, 다음 초/다음 날 제외, 한쪽·양쪽 null, 두 placement 격리, 25개 무절단 id 순서, iOS 요청에도 Android-only 행을 반환하는 서버 OS 비필터를 실제 DB·HTTP로 확인했다.
|
||||
- 독립 최종 리뷰: production 11개·대응 테스트 4개·DDL·SecurityConfig diff·호출 경계를 검토한 결과 `APPROVE`, Must fix/Should fix 없음. 기존 P3 DST gap blocker의 RED→GREEN 및 재검토 승인도 확인했다. 부모가 리뷰와 별도로 전체 실행 결과를 확인했다.
|
||||
- 문서: API 계약과 앱/관리자 프롬프트를 실제 경로·shape·오류·날짜 허용 범위·이미지 정책으로 갱신했다. 문서 동기화 중 잘못 적힌 경로 ID 형식의 400 보장은 제거했으며 프레임워크 바인딩 오류까지 같은 상태라고 가정하지 않는다.
|
||||
- 남은 한계: 운영 DB DDL 적용·배포·실제 AWS 업로드·native MySQL 실행·앱/관리자 UI는 수행하지 않았다. DDL은 독립 H2에 실제 적용해 검증했다. host에 LSP 도구가 없어 Kotlin 컴파일/ktlint로 대체했으며 LSP clean으로 주장하지 않는다. S3 업로드 후 DB 실패 시 orphan 및 교체/삭제 후 기존 object 보존 가능성은 승인된 범위대로 남긴다.
|
||||
- 종료 확인: 변경 파일의 `git diff --check` 및 신규 파일 공백 검사 통과, PRD/plan 미완료 체크박스 없음. `pgrep`에서 Gradle Test Executor·embedded Redis·일회성 QA driver 잔존 없음. 검증 요약을 본 문서에 보존한 뒤 이번 소유 run root·child roots·QA driver·note·cache를 제거하고 경로 부재를 확인했다. 기존 공유 캐시·worktree·소스·빌드 결과/테스트 보고서는 보존했으며 커밋·푸시는 하지 않았다.
|
||||
|
||||
## Phase 5: is_active 소프트 삭제
|
||||
|
||||
앞선 하드 삭제 구현·검증은 당시 완료 이력이다. 이번 사용자 결정은 이를 대체하는 후속 변경이며 기존 완료 체크박스를 되돌리지 않는다.
|
||||
|
||||
### Task 5.1 저장·조회·삭제 경계 변경
|
||||
|
||||
**Goal `P5-T1`:** DELETE가 행을 보존하며 is_active만 false로 바꾸고, 삭제된 배너가 모든 소비자 조회와 수정 경로에서 제외된다.
|
||||
|
||||
- 시작 조건: `DEC-010` 사용자 승인 및 기존 P2~P4 완료.
|
||||
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/out/persistence/SubBannerEntity.kt`, `DefaultSubBannerRepository.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/subbanner/adapter/out/persistence/SubBannerPersistenceTest.kt`, `adapter/in/web/AdminSubBannerControllerIntegrationTest.kt`, `adapter/in/web/SubBannerControllerIntegrationTest.kt`; `docs/20261029_가로형서브배너/create-sub-banner.sql` 및 기존 PRD/API/프롬프트/계획.
|
||||
- 완료 증거: DB 행과 기존 필드 보존, false 갱신, findById/관리자 목록/공개 목록 제외, 수정·재삭제 404, 기본 true와 DTO 불변을 실제 persistence/HTTP 테스트로 확인한다.
|
||||
- 동시 경계: 수정 요청이 활성 entity를 로드한 뒤 다른 트랜잭션의 DELETE가 커밋하더라도, 먼저 시작한 수정이 삭제 상태를 true로 되돌리지 않아야 한다. 두 독립 EntityManager의 겹치는 트랜잭션과 새 조회 context로 재현·검증한다.
|
||||
- 범위 밖: 활성 토글·복원 API·isActive 요청/응답 필드·S3 삭제·기존 완료 이력 삭제·운영 DDL 적용. 아직 미배포인 신규 CREATE DDL을 보완하며 추측한 레거시 migration은 추가하지 않는다.
|
||||
- [x] **RED:** 행 보존/비활성/목록 제외·기간·placement·수정/재삭제 경계를 먼저 테스트했다. 추가 리뷰에서 발견한 stale managed entity도 두 트랜잭션으로 재현했다.
|
||||
- [x] **RED 확인:** 기존 하드 삭제/컬럼 부재 assertion 8개 실패, 이후 동시 상태 덮어쓰기 회귀 1개 실패를 각각 확인했다.
|
||||
- [x] **GREEN:** entity/DDL 기본 true, DELETE false 갱신, 모든 소비자 조회의 활성 필터와 entity-local DynamicUpdate를 구현했다.
|
||||
- [x] **GREEN 확인:** focused 99/99 및 실제 HTTP에서 행·필드·S3 보존/미노출/404·기존 DTO/권한/기간 규칙과 동시 수정의 비복원을 확인했다.
|
||||
- [x] **REFACTOR:** 직접 영향만 정리하고 ktlint·DDL 실제 H2 실행·독립 scoped 재검토를 통과했다. 전체 Gate는 별도 진행한다.
|
||||
|
||||
### Phase 5 Gate `P5-GATE`
|
||||
|
||||
- 시작 조건: `P5-T1` 완료.
|
||||
- 완료 증거: 실제 HTTP 시나리오·전체 테스트·ktlint·bootJar·독립 리뷰·자원 정리 기록.
|
||||
- [x] P6를 포함한 최종 전체 테스트 2,682개·lint·bootJar 및 독립 리뷰 통과. DDL의 is_active 기본 true와 entity 일치, 행 보존·조회 제외·동시 비복원을 검증했다. 자원 정리 영수증은 아래 최종 기록을 따른다.
|
||||
- 도구 한계: LSP 및 native MySQL 실행 환경은 미제공이다. 컴파일/ktlint와 H2 검증으로 대체하되 운영 적용 검증으로 표현하지 않는다.
|
||||
|
||||
### `P5-T1` 1차 검증 및 리뷰, 2026-10-29
|
||||
|
||||
- baseline: 기존 서브 배너 focused 92/92 통과. 회귀 테스트를 먼저 변경해 기존 하드 삭제와 컬럼 부재로 인한 assertion 8개 실패를 확인했다.
|
||||
- 1차 GREEN: entity 기본 true/nonnull, DELETE false 갱신, id/관리자/공개 활성 필터와 authored CREATE의 default true를 구현했다. focused 98/98 및 ktlint 통과. 실제 HTTP에서 DELETE 200/null, DB 행·payload·이미지·생성 시각 보존, 비활성 PUT/재DELETE404와 upload0을 확인했다.
|
||||
- 독립 리뷰: 순차 삭제 경계는 통과했으나, 삭제 전 true entity를 보관한 수정 트랜잭션의 Hibernate 전체 컬럼 UPDATE가 뒤늦게 is_active=true를 덮을 수 있다는 P1 blocker를 확인했다. 기존 flush/clear 후 stale-domain 테스트와 다른 경계이므로 두 트랜잭션 회귀 RED→GREEN을 추가한다. 전체 완료 판정은 수정·재검토 후 수행한다.
|
||||
|
||||
### `P5-T1` 동시 경계 수정 및 Gate 진행, 2026-10-29
|
||||
|
||||
- 수정: `@DynamicUpdate`를 SubBannerEntity에만 추가해 payload 수정이 변경하지 않은 is_active를 SQL에 포함하지 않도록 했다. 새 잠금·버전 컬럼·외부 API는 추가하지 않았다.
|
||||
- 재현: T1이 활성 managed entity를 유지한 상태에서 T2 DELETE 커밋 후 T1 payload UPDATE를 커밋했다. 제3 context에서 false를 기대했으나 true가 된 assertion RED 1건을 확인했다. 수정 뒤 persistence29/29와 focused99/99·ktlint가 통과했고 원래 stale-domain 회귀도 유지했다. 독립 재검토는 ADDRESSED/APPROVE다.
|
||||
- 실제 SQL: 삭제는 `set updated_at=?, is_active=? where id=?`, 일반 payload 변경은 `set updated_at=?, web_url=? where id=?`로 관찰돼 삭제 상태가 덮이지 않았다. 테스트의 독립 EntityManager 3개와 커밋된 fixture는 finally로 정리했다.
|
||||
- 전체 검증 1차: 부모의 `./gradlew test ktlintCheck bootJar --continue --console=plain`은 `bootJar` 뒤 `:test` 수행 중 실행 도구의 1,200,000ms 제한으로 종료됐다. 테스트 실패나 전체 성공을 판정할 출력은 없었다. 기존 99개 focused HTML을 전체 성공 증거로 재사용하지 않았다.
|
||||
- 조사·재시도: 종료 후 Gradle Test Executor/embedded Redis 잔존 없음을 확인했다. 코드·테스트·빌드 설정은 바꾸지 않고, 테스트별 진행만 기록하는 임시 init script를 사용해 동일 전체 명령을 2,400,000ms 제한으로 한 번 재실행 중이다. 이 기록은 최종 통과 주장이 아니다.
|
||||
|
||||
## Phase 6: 두 추천 API에 서브 배너 통합
|
||||
|
||||
### Task 6.1 비로그인 추천 응답 합성 및 기존 앱 GET 제거
|
||||
|
||||
**Goal `P6-T1`:** 홈·콘텐츠 추천 GET에서 한 요청으로 기존 추천 데이터와 `subBanners`를 받고, 별도 서브 배너 조회 endpoint는 제공하지 않는다.
|
||||
|
||||
- 시작 조건: 사용자 `DEC-011` 승인, P5 focused 99개 및 독립 재리뷰 승인. P5 전체 Gate는 이번 통합 이후 최종 코드로 함께 판정한다.
|
||||
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`, `dto/recommendation/HomeRecommendationResponse.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacade.kt`, `dto/AudioRecommendationsResponse.kt`; 기존 `v2/subbanner/adapter/in/web/SubBannerController.kt` 삭제 및 `configs/SecurityConfig.kt`의 해당 단일 permitAll 제거. 두 facade/DTO 직접 생성 테스트 호출자와 기존 실제 HTTP subbanner 테스트를 추천 통합 시나리오로 갱신한다.
|
||||
- 인터페이스: 두 facade는 기존 `SubBannerService.findVisibleByPlacement` 및 `SubBannerResponse.from`을 재사용한다. HOME/CONTENT placement는 해당 facade가 고정하며 클라이언트 placement query나 OS 조건을 추가하지 않는다. `data.subBanners: List<SubBannerResponse>`는 항상 존재하며 없으면 `[]`다.
|
||||
- 범위 밖: 관리자 API/DDL·소프트 삭제 변경, 기존 banners 교체, 새 fallback/cache/endpoint, 앱 UI 코드, 운영 배포. 기존 추천의 익명 GET 허용은 유지한다.
|
||||
- [x] **RED:** 두 추천 GET의 실제 익명 HTTP에서 subBanners·활성/기간·배열·legacy 보존·폐기 handler 부재를 검사했다.
|
||||
- [x] **RED 확인:** 29개 중 26 assertion failures를 확인했다. 통합 필드 부재와 폐기 대상 경로가 여전히 200인 것을 재현했다.
|
||||
- [x] **GREEN:** 두 facade/DTO에 필수 배열을 합성하고 모든 직접 생성 호출자를 수정했다. standalone controller와 해당 permitAll 한 줄을 제거했다.
|
||||
- [x] **GREEN 확인:** 같은 RED 명령 29/29 및 subbanner/home/audio 전체 관련 회귀 161/161 통과. 두 실제 추천 GET에 기존 소프트 삭제/기간/권한/무절단 시나리오를 이식했다.
|
||||
- [x] **REFACTOR:** API 계약·앱 프롬프트를 `data.subBanners`로 갱신했다. 기존 기능·보안 및 JSON assertion을 완화하지 않았고 ktlint와 독립 P6 리뷰를 통과했다.
|
||||
|
||||
### 통합 최종 Gate `P6-GATE`
|
||||
|
||||
- 시작 조건: P6-T1 및 P5 focused/리뷰 완료.
|
||||
- [x] 소프트 삭제·두 추천 통합 focused/전체 테스트·ktlint·bootJar·독립 리뷰를 통과하고 실제 HTTP 증거를 기록했다.
|
||||
- [x] 앱 프롬프트/API 문서·소프트 삭제 DDL·행 보존·비로그인 조회·폐기 endpoint 부재를 확인했다. 소유 QA 자원 정리는 최종 종료 기록에 남긴다.
|
||||
|
||||
### 전체 검증 범위 변경, 2026-10-29
|
||||
|
||||
- 사용자 추가 요청으로 변경 전 코드를 대상으로 실행 중이던 전체 검증 runner를 개별 취소했다. 소프트 삭제 구현은 유지하며 P6 통합 코드로 최종 검증을 새로 수행한다. 이는 전체 테스트 통과/실패 판정이 아니다.
|
||||
- 취소 당시 Gradle wrapper/test-worker PID를 확인한 뒤 후속 ps/lsof에서 해당 프로세스 부재를 확인했다. 공유 daemon·다른 작업 프로세스는 종료하지 않았다.
|
||||
|
||||
### `P6-T1` 구현·focused·독립 리뷰, 2026-10-29
|
||||
|
||||
- 구현: HomeRecommendationFacade는 HOME_RECOMMENDATION, AudioRecommendationFacade는 CONTENT_RECOMMENDATION을 고정해 기존 SubBannerService/6필드 DTO를 재사용한다. 기존 추천 DTO의 필수 `subBanners` 배열을 추가했고 모든 기존 필드/기존 banners 매핑을 그대로 유지했다. standalone 앱 controller와 정확한 GET 허용 matcher를 제거했다. 관리자 CRUD·DDL·소프트 삭제 저장소·DynamicUpdate는 변경하지 않았다.
|
||||
- TDD: 컴파일 후 RED 29개 중 26개 assertion failures → GREEN 29/29. 기존 소켓 테스트를 `RecommendationSubBannerIntegrationTest`로 이식했으며, 폐기된 placement query 검증은 두 화면의 고정 placement 및 handler 부재 검증으로 대체했다. 삭제된 테스트를 통과를 위한 우회로 사용하지 않았다.
|
||||
- focused: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subbanner.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*' --tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.*' ktlintCheck --console=plain` → 161/161, failures/errors/skipped 0, `BUILD SUCCESSFUL in 2m 25s`.
|
||||
- 실제 HTTP: 두 GET은 Authorization 없이 200이며 `data.subBanners:[]` 또는 정확한 6필드 목록을 반환했다. HOME의 기존 9개+신규 배열=10필드, AUDIO의 기존 8개+신규 배열=9필드 전체와 비어 있지 않은 legacy banners를 정확히 비교했다. 각 placement 25개 전체·기간 소수초/자정·삭제 행 9개 중 비활성 4개 보존·OS 비필터를 확인했다. 폐기된 경로는 인증 GET 404/익명 401이며 handler mapping에도 없다.
|
||||
- 독립 P6 리뷰: APPROVE, 차단 사항 없음. 직접 생성 테스트 호출자 6파일과 실제 HTTP 이식 전체를 확인했고 이전 P5 동시 삭제 비복원 회귀도 유지됨을 확인했다.
|
||||
- 부모 확인: facade/DTO 변경, 기존 테스트 diff 및 새 실제 HTTP 테스트 전체와 앱/API 문서를 읽었다. 통합 이후에는 별도 배너 fetch가 없으므로 앱 프롬프트의 오류 처리를 추천 API 오류와 개별 이미지/링크 오류로 구분했다.
|
||||
- 배포 조건: 추천 API도 신규 테이블/컬럼을 조회하므로 현재 DDL 준비가 서버 배포보다 선행해야 한다. 운영 DB 적용·배포는 수행하지 않는다.
|
||||
- 최종 실행: 안정된 P5+P6 코드로 테스트별 진행 로그만 추가한 임시 init script와 함께 전체 `test ktlintCheck bootJar --continue`를 실행 중이다. 이전 취소/timeout 결과나 focused 통과를 전체 통과로 대체하지 않는다.
|
||||
|
||||
### `P5-GATE`·`P6-GATE` 최종 전체 검증, 2026-10-29
|
||||
|
||||
- 결과: 안정된 최종 코드에서 `./gradlew test ktlintCheck bootJar --continue --console=plain`과 동작을 바꾸지 않는 임시 테스트 진행 로그 init script를 사용해 1회 실행했다. exit 0, `BUILD SUCCESSFUL in 15m 46s`. 전체 2,682 tests, failures/errors/skipped/ignored 모두 0이다. 부모가 fresh HTML 및 `QA_SUITE_RESULT tests=2682 failed=0 skipped=0` 원본 로그를 직접 확인했다.
|
||||
- 빌드/정적 검사: `bootJar`가 이번 실행에서 생성됐고 `build/libs/sodalive-0.0.1-SNAPSHOT.jar`를 확인했다. `ktlintCheck`는 변경 없는 소스의 선행 성공 결과를 사용한 UP-TO-DATE 상태이며 전체 명령은 성공했다. Kotlin 컴파일과 선행 focused lint도 통과했다. LSP 도구 미제공을 LSP 성공으로 표현하지 않는다.
|
||||
- 실제 HTTP: 최종 suite에도 `RecommendationSubBannerIntegrationTest` 24개가 실행됐다. 두 추천 GET의 Authorization 없는 200과 `data.subBanners`, 정확한 기존 root 필드/legacy banners 보존, 활성·기간·placement·25개 무절단, 폐기 경로의 handler 부재/인증 404·익명 401, admin 보호 및 삭제 행 보존을 확인했다. 이전 standalone 테스트 클래스는 source/build/report에서 사라졌고 새 클래스의 XML/HTML은 이번 실행에서 갱신됐다.
|
||||
- 소프트 삭제: 실제 DELETE는 200/data:null을 유지하며 row/is_active=false/기존 payload 보존, 삭제 후 수정·재삭제404, 관리자·추천 조회 제외를 확인했다. 두 EntityManager의 겹치는 트랜잭션에서도 삭제 상태가 true로 돌아가지 않는 회귀를 유지했다. 앱·관리자에 isActive 필드나 복구 API를 추가하지 않았다.
|
||||
- 리뷰: P5의 유일한 동시성 blocker는 재현 RED→DynamicUpdate GREEN 후 ADDRESSED/APPROVE. P6 독립 리뷰는 추가 차단 없이 APPROVE다. 각각의 검토와 별개로 부모가 전체 실행 결과·변경 파일·문서를 확인했다.
|
||||
- 증거 위치: `build/reports/tests/test/index.html`, `build/test-results/test/TEST-*.xml`. 실제 응답은 RecommendationSubBannerIntegrationTest/AdminSubBannerControllerIntegrationTest XML의 HTTP EVIDENCE/DB EVIDENCE, 동시 경계는 SubBannerPersistenceTest XML의 CONCURRENCY EVIDENCE에 남는다. 테스트 토큰은 출력하지 않았다.
|
||||
- 이전 중단 구분: P5 첫 전체 실행은 20분 도구 제한, 두 번째 진행 로그 실행은 사용자 P6 범위 변경 때문에 취소했다. 둘 다 통과 증거로 사용하지 않았으며 최종 P5+P6 전체 실행만 이번 완료 판정에 사용했다.
|
||||
- 운영 한계: DDL 파일만 생성·보완했으며 native MySQL·운영 DB·실제 AWS·앱/관리자 UI·배포는 수행하지 않았다. 서버 배포 전에 현재 sub_banner/is_active DDL 준비가 필요하다. 기존 테이블을 이미 별도로 생성한 환경은 배포 담당자가 스키마 차이를 확인해야 하며 자동 migration/운영 변경은 하지 않는다.
|
||||
- 종료 영수증: tracked/new 파일 공백 검사 통과, PRD/plan 미완료 체크박스 없음, production source에 폐기 `/api/v2/sub-banners` 경로 없음. 테스트 wrapper/worker·embedded Redis 잔존 없음을 확인한 뒤 이번 소유 `subbanner-soft-delete.wmREV2` root와 child root·note·진행 로그 스크립트·cache를 제거하고 경로 부재를 확인했다. 공유 Gradle daemon/cache·기존 worktree·소스·JAR·build 테스트 보고서는 보존했다. 커밋·푸시는 하지 않았다.
|
||||
|
||||
### 커밋 전 영향 범위 재검증, 2026-10-29
|
||||
|
||||
- 무엇을·왜: 커밋할 최종 변경의 서브 배너·홈·콘텐츠 추천 경계를 재검증했다. 제품 코드와 테스트는 수정하지 않았다.
|
||||
- 명령: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subbanner.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*' --tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.*' ktlintCheck bootJar --console=plain`.
|
||||
- 결과: 종료 코드 0, `BUILD SUCCESSFUL in 5m 5s`. 최신 HTML/XML에서 161 tests, failures/errors/skipped 0을 확인했다. 테스트는 재실행됐으며 lint·컴파일·bootJar는 입력 변경이 없어 UP-TO-DATE였다.
|
||||
- 전체 회귀 생략: 이전 최종 Gate의 전체 2,682개 통과 이후 제품 코드 변경이 없어 전체 suite를 반복하지 않았다. 이번 영향 범위 실행으로 `build/reports/tests/test/index.html`과 XML은 161개 결과로 갱신됐으며, 과거 전체 결과와 구분한다.
|
||||
- 운영 한계: 운영 DB·native MySQL·실제 AWS·앱/관리자 UI·배포는 이번에도 검증하지 않았다. 서버 배포 전에 현재 DDL 준비가 필요하다.
|
||||
Reference in New Issue
Block a user