docs(subtitle): 자막 연동 검증과 배포 조건을 기록한다
This commit is contained in:
@@ -0,0 +1,141 @@
|
|||||||
|
# 자막 Phase 1 배포 게이트
|
||||||
|
|
||||||
|
**현재 판정: 배포 BLOCKED.** 이 문서는 로컬 저장 계약과 배포 전 확인 항목이며 DB 적용·AWS 배포 증거가 아니다.
|
||||||
|
기준일은 2026-11-10이며 [PRD](prd.md), [구현 계획](plan-task.md), [DDL](subtitle-schema.sql)을 함께 본다.
|
||||||
|
|
||||||
|
## 1. 검토 기준과 파일 경계
|
||||||
|
|
||||||
|
- persistence 기준 경로: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/`.
|
||||||
|
- 실제 읽은 파일: `SubtitleJob.kt`, `Subtitle.kt`, `SubtitleCallbackReceipt.kt` 및 세 repository.
|
||||||
|
- 콘텐츠 기준: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt`, `common/BaseEntity.kt`.
|
||||||
|
- `src/main/resources/application.yml`의 `MySQL8Dialect`·`ddl-auto: validate`는 로컬 설정 근거일 뿐 운영 DB 증거가 아니다.
|
||||||
|
- 문서 담당은 이 문서와 DDL만 작성한다. 병렬 Kotlin 구현의 최종 엔티티 대조는 상위 구현 게이트에서 다시 수행한다.
|
||||||
|
|
||||||
|
## 2. 저장 계약 대조
|
||||||
|
|
||||||
|
물리명은 기존 Spring snake_case 매핑 기준이다. 길이 미지정 `String`은 JPA 기본 `VARCHAR(255)`를 따른다.
|
||||||
|
`jobId`는 UUID **문자열**이며 `BINARY(16)`으로 바꾸지 않는다. `N`은 NOT NULL, `Y`는 NULL 허용이다.
|
||||||
|
|
||||||
|
| `SubtitleJob.kt` 속성 | DDL 컬럼·타입 | NULL |
|
||||||
|
|---|---|---|
|
||||||
|
| `jobId` | `job_id VARCHAR(36)` PK | N |
|
||||||
|
| `contentId` | `content_id BIGINT` signed | N |
|
||||||
|
| `sourceBucket`, `sourceKey` | `source_bucket`, `source_key VARCHAR(255)`; 전체 두 컬럼 unique | N |
|
||||||
|
| `outputKey` | `output_key VARCHAR(255)` | N |
|
||||||
|
| `status` | `status VARCHAR(32)`; enum STRING | N |
|
||||||
|
| `errorCode` | `error_code VARCHAR(255)` | Y |
|
||||||
|
| `errorMessage` | `error_message TEXT` | Y |
|
||||||
|
| `sourceUploadErrorCode` | `source_upload_error_code VARCHAR(255)` | Y |
|
||||||
|
| `sourceUploadErrorAt` | `source_upload_error_at TIMESTAMP`; 명시적 JPA TIMESTAMP | Y |
|
||||||
|
| `createdAt`, `updatedAt` | `created_at`, `updated_at TIMESTAMP` | N |
|
||||||
|
|
||||||
|
| `Subtitle.kt` 속성 | DDL 컬럼·타입 | NULL |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `id BIGINT` signed PK AUTO_INCREMENT | N |
|
||||||
|
| `jobId` | `job_id VARCHAR(36)` FK → `subtitle_job.job_id` | N |
|
||||||
|
| `language` | `language VARCHAR(35)`; `(job_id, language)` unique | N |
|
||||||
|
| `format` | `format VARCHAR(16)` | N |
|
||||||
|
| `s3Bucket`, `s3Key` | `s3_bucket`, `s3_key VARCHAR(255)` | N |
|
||||||
|
|
||||||
|
| 기존 엔티티 속성 | DDL 컬럼·타입 | NULL |
|
||||||
|
|---|---|---|
|
||||||
|
| `SubtitleCallbackReceipt.eventId` | `event_id VARCHAR(255)` PK; UUID 제한 없음 | N |
|
||||||
|
| `SubtitleCallbackReceipt.jobId` | `job_id VARCHAR(36)` | N |
|
||||||
|
| `SubtitleCallbackReceipt.bodyHash` | `body_hash VARCHAR(64)` | N |
|
||||||
|
| `SubtitleCallbackReceipt.createdAt` | `created_at TIMESTAMP` | N |
|
||||||
|
| `AudioContent.latestSubtitleJobId` | `content.latest_subtitle_job_id VARCHAR(36)` | Y |
|
||||||
|
|
||||||
|
### 미확정 차이와 적용 전 대조
|
||||||
|
|
||||||
|
1. 마지막 정적 대조에서 `Subtitle.kt`의 테이블명·6개 컬럼·길이·nullable·IDENTITY PK·unique가 합의 계약과 일치했다.
|
||||||
|
`SubtitleJob.kt`에서도 `koKey`/`jaKey` 제거를 확인했다. `Subtitle.jobId`는 scalar이고 JPA 관계/FK 선언은 없다.
|
||||||
|
DDL의 `subtitle.job_id` FK는 합의 계약에 따른 DB 제약이며 ORM 관계 선언 확인으로 보고하지 않는다.
|
||||||
|
`subtitle_job` CREATE에 `ko_key`/`ja_key`는 넣지 않는다. 미출시 초안 교체이며 기존 데이터 이관·컬럼 삭제 SQL은 없다.
|
||||||
|
2. 최초 대조에서는 `sourceUploadErrorAt`에 `columnDefinition`이 없어 차이로 기록했다.
|
||||||
|
최종 구현은 `@Column(columnDefinition = "TIMESTAMP")`와 초 절삭을 명시했고 부모가 DDL과 다시 대조했다.
|
||||||
|
이 로컬 mapping 차이는 해소됐지만 실제 MySQL schema 적용·왕복 검증은 여전히 운영 게이트다.
|
||||||
|
3. 나머지 시각은 엔티티의 명시적 `TIMESTAMP`와 초 절삭을 따른다. DDL 소수초 정밀도는 모두 0이다.
|
||||||
|
`created_at`은 `DEFAULT CURRENT_TIMESTAMP`, job의 `updated_at`은 같은 기본값과 `ON UPDATE CURRENT_TIMESTAMP`다.
|
||||||
|
`Subtitle.kt`에는 시각이나 `BaseEntity` 상속이 없으므로 결과 행에 임의 시각 컬럼을 추가하지 않는다.
|
||||||
|
UTC 생성값과 JDBC/session timezone 및 실제 MySQL 왕복은 미검증이다.
|
||||||
|
4. `AudioContent`는 `content` 테이블이며 `BaseEntity.id: Long?`의 IDENTITY를 상속한다.
|
||||||
|
신규 `content_id`는 signed BIGINT로 맞췄지만 운영 `content.id`의 실제 타입·부호·엔진은 확인하지 않았다.
|
||||||
|
합의된 `subtitle.job_id` FK만 둔다. receipt의 jobId 및 콘텐츠 연결은 현재 scalar 매핑이므로 FK를 임의 추가하지 않는다.
|
||||||
|
FK cascade는 없으며 기본 RESTRICT다. `(job_id, language)` unique가 결과 FK와 작업별 조회 인덱스를 겸한다.
|
||||||
|
5. 현재 callback 활성화는 `ko-KR`/`ja-JP`, 형식은 `SRT`, 결과 2/1/0개의 상태 판정은 그대로다.
|
||||||
|
저장 구조만 언어별 행으로 바꾼다. 새 언어 활성화·재생성·새 관리 API·recovery 상태/테이블은 추가하지 않는다.
|
||||||
|
|
||||||
|
### 식별자와 인덱스 전제
|
||||||
|
|
||||||
|
- 세 신규 테이블과 최신 job 컬럼은 `utf8mb4_0900_bin`을 쓴다. 대소문자·후행 공백을 구별하는 NO PAD 비교다.
|
||||||
|
eventId 및 bucket/key의 정확한 식별에 case-insensitive collation이나 PAD SPACE인 `utf8mb4_bin`을 대신 쓰지 않는다.
|
||||||
|
- MySQL 8.0.17 이상 및 해당 collation의 binary/NO PAD 속성, InnoDB DYNAMIC·3072-byte 키 지원을 운영자가 확인한다.
|
||||||
|
16 KiB 이상 InnoDB page를 전제로 하며 4/8 KiB page 환경의 적합성을 주장하지 않는다.
|
||||||
|
- source unique 최대 키 데이터는 `(255 + 255) × 4 = 2040` bytes다. eventId PK는 `255 × 4 = 1020` bytes다.
|
||||||
|
결과 unique는 `(36 + 35) × 4 = 284` bytes다. 보조 인덱스의 PK 포함분도 3072 bytes 미만이다.
|
||||||
|
prefix unique나 해시 식별자로 원래 bucket/key를 대체하지 않는다.
|
||||||
|
- 경로 컬럼은 현재 UUID 기반 계약에 맞는 `VARCHAR(255)`이며 일반 S3 최대 길이 전체 지원을 뜻하지 않는다.
|
||||||
|
job의 `(content_id, created_at)` 인덱스는 현재 `findFirstByContentIdOrderByCreatedAtAsc` 조회에 대응한다.
|
||||||
|
|
||||||
|
## 3. Lambda handoff와 실제 환경 구분
|
||||||
|
|
||||||
|
아래 SHA-256은 **PRD §2의 기존 조사 기록 인용**이다. 문서 담당이 외부 저장소·ZIP·AWS를 다시 조회한 결과가 아니다.
|
||||||
|
원본 보고서 참조는 PRD에 적힌 `aws-subtitle-service/docs/FINAL_REPORT.md`다.
|
||||||
|
|
||||||
|
| 정확한 artifact | SHA-256 |
|
||||||
|
|---|---|
|
||||||
|
| `start-subtitle-transcription.zip` | `a0d0b19b2ecb24c0f89caf528bf09fd039558563a99063f082ac64418704abfd` |
|
||||||
|
| `generate-subtitle.zip` | `bfa8742ca436bace853ec2540fc5f70daa24cbab49e75315565d980189fbf209` |
|
||||||
|
|
||||||
|
로컬 artifact 일치 기록은 운영 함수 버전/alias·event routing·consumer·in-flight·Scheduler·DB 적용 증거가 아니다.
|
||||||
|
자격 증명/JWT/raw body 값은 이 문서나 증거 보고서에 기록하지 않는다.
|
||||||
|
|
||||||
|
## 4. 운영 차단 항목
|
||||||
|
|
||||||
|
| 게이트 | 현재 | 해제에 필요한 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 최종 엔티티·DB | BLOCKED | 위 차이 해소, 운영 기존 스키마·버전·collation·인덱스 확인, 별도 DDL 승인·적용 기록 |
|
||||||
|
| consumer·조정 배포 | BLOCKED | 운영자 inventory, 실행 중일 때만 pause/drain, backend·정확한 두 Lambda의 조정 배포 기록 |
|
||||||
|
| MediaConvert·CloudFront | BLOCKED | UUID 입력의 ID 추출/출력 확장자 및 private origin·서명 URL 실환경 검증 |
|
||||||
|
| controlled live | BLOCKED | ko/ja/mixed의 실제 S3 → callback → 콘텐츠/앱 결과와 word/item 언어 증거 |
|
||||||
|
| 대표 2시간 입력 | BLOCKED·미측정 | 신뢰 가능한 duration 근거 및 대표 입력의 전체 처리 예산 실측 |
|
||||||
|
|
||||||
|
### 운영자 실행 순서: 별도 승인 후에만
|
||||||
|
|
||||||
|
1. 운영 consumer·event routing·함수 버전/alias·in-flight·기존 DB 상태를 inventory로 확정한다.
|
||||||
|
실행 중인 consumer가 있을 때만 신규 유입을 pause하고 기존 작업·callback을 drain하는 전환 구간을 조율한다.
|
||||||
|
실행 중인 대상이 없으면 그 증거와 함께 pause/drain을 생략한다. 미확인 상태를 비가동으로 간주하지 않는다.
|
||||||
|
2. 최종 JPA와 실제 `SHOW CREATE TABLE`·collation·인덱스 전제를 대조하고 DB 변경을 별도 승인받는다.
|
||||||
|
기존 동명 테이블/컬럼이 있으면 중단하고 차이를 검토한다. `IF NOT EXISTS`로 불일치를 숨기지 않는다.
|
||||||
|
additive DDL 승인·적용 확인과 Phase 1 backend/두 Lambda의 배포를 같은 전환 계획으로 조율한다.
|
||||||
|
구·신 consumer의 중복 실행이나 호환성 미확인 in-flight를 남긴 채 재개하지 않는다.
|
||||||
|
3. UUID 원본 `input/{contentId}/{UUID filename}`으로 외부 MediaConvert의 contentId 추출 규칙을 확인한다.
|
||||||
|
등록은 안전한 실제 파일 확장자를 UUID 뒤에 보존한다. 실제 START ZIP은 지원 확장자를 검사하므로
|
||||||
|
누락/미지원 확장자의 자막 성공을 주장하지 않는다. 이를 위해 기존 업로드를 거부하거나 파일을 허위 변환하지 않는다.
|
||||||
|
출력 경로·실제 확장자·완료 callback·기존 preview/알림 흐름의 호환 증거가 필요하다.
|
||||||
|
로컬 `matchesOutput`의 허용 확장자는 `aac`/`m4a`/`mp3`이나 실제 외부 출력 계약이 확인된 것은 아니다.
|
||||||
|
자막 bucket/key가 기존 private CloudFront origin/path에 정확히 매핑되고 권한 있는 사용자만 서명 URL로
|
||||||
|
열람하는지 확인한다. 두 항목 모두 현재 미검증이며 임의 prefix 허용이나 공개 S3 URL로 우회하지 않는다.
|
||||||
|
현재 Spring의 자막 bucket allowlist는 기존 `cloud.aws.s3.content-bucket` 한 개다.
|
||||||
|
Lambda `MEDIA_BUCKET`과 `SUBTITLE_BUCKET`도 이 값에 맞춰야 하며, 별도 자막 bucket을 쓰려면
|
||||||
|
allowlist와 CloudFront origin 계약을 먼저 변경·검증해야 한다. 현재 구현이 다중 bucket을 지원한다고 보지 않는다.
|
||||||
|
4. 일반 유입을 재개하기 전에 controlled ko/ja/mixed live를 수행한다.
|
||||||
|
S3 source binding, Transcribe 실제 word/item 언어, 결과 SRT, BOT callback, 언어별 콘텐츠/앱 표시를 연결해 기록한다.
|
||||||
|
mixed는 실제 word/item 언어 증거가 필수이며 synthetic fixture나 HTTP metadata 검증으로 대체할 수 없다.
|
||||||
|
유료 preview-only 사용자의 전체 자막 차단과 private 접근도 확인한다.
|
||||||
|
5. 신뢰 가능한 기존 duration의 가용성과 입력 검증 경계를 확인하고 대표 2시간 입력의 처리 예산을 실측한다.
|
||||||
|
구형/검증 불가능 원본은 수동 게이트로 남기며 임의 client duration을 신뢰하지 않는다.
|
||||||
|
2시간은 입력 목표이지 SLA/지원 보장이 아니다. 미측정 상태에서 지원 또는 재개 승인을 하지 않는다.
|
||||||
|
모든 차단 항목의 증거와 승인자를 기록한 뒤에만 일반 유입을 재개한다.
|
||||||
|
|
||||||
|
purge·데이터 정리·적용 migration 수정·Scheduler fallback·자동 복구·Translate 50-call 정책은 이 절차에 없다.
|
||||||
|
재생성 전체와 새 언어 활성화 역시 제외한다. 미해결 작업을 삭제하거나 성공 처리해 drain을 가장하지 않는다.
|
||||||
|
|
||||||
|
## 5. 문서 담당 검증 경계
|
||||||
|
|
||||||
|
문서 간 계약·실제 엔티티 속성·컬럼별 COMMENT·테이블 COMMENT·SQL 종류·공백을 read/rg로 정적으로 대조했다.
|
||||||
|
DDL은 CREATE TABLE 3개와 ALTER TABLE ADD COLUMN 1개뿐이며 23개 신규 컬럼·3개 신규 테이블에 COMMENT가 있다.
|
||||||
|
후행 공백·탭·CR·130자 초과 행은 발견하지 않았다. SQL 파싱/실행이나 물리 스키마 호환성을 검증한 결과가 아니다.
|
||||||
|
이 담당은 테스트·빌드·DB 실행·AWS 접근·외부 저장소 조회를 하지 않으며 성공 결과를 주장하지 않는다.
|
||||||
|
상위 구현 검증도 이번 작업에서 새로 작성한 테스트만 실행하도록 승인되어 있다. 기존 테스트 실행은 승인 범위가 아니다.
|
||||||
|
로컬 HTTP·새 테스트·빌드 결과는 구현 담당의 별도 증거이며, 그 성공도 위 운영 게이트를 해제하지 않는다.
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# 자막 Phase 1 Spring 구현 검증 기록
|
||||||
|
|
||||||
|
> 상태: **재생성 제외 로컬 backend 구현·신규 테스트 15개·독립 리뷰 완료**. 운영 배포 완료가 아니다.
|
||||||
|
|
||||||
|
## 범위와 결정
|
||||||
|
|
||||||
|
- 이번 범위: 기존 업로드의 PENDING/source 선등록, 네 BOT 내부 endpoint, raw-body receipt, 기존 상세의 언어·권한별 자막 제공.
|
||||||
|
- 재생성은 사용자 결정으로 제외했다. 새 공개 재생성 API, 직접 START 호출, Scheduler/outbox를 만들지 않는다.
|
||||||
|
- 후속 요구로 언어별 결과는 `Subtitle` 행에 저장한다. 이번 callback 허용 언어/완료 판정은 ko/ja만 유지하며 다른 언어를 활성화하지 않는다.
|
||||||
|
- 테스트 실행은 사용자 후속 지시 이후 **이번 작업에서 새로 작성한 테스트만** 대상으로 한다. 기존 클래스에 추가한 사례도 메서드 selector로 한정한다.
|
||||||
|
|
||||||
|
## 실제 조사와 보존
|
||||||
|
|
||||||
|
- 현재 Spring에 subtitle/recovery/자막 HMAC/재생성 action이 발견되지 않았다. 제거할 해당 코드가 없으므로 실제 삭제 없음.
|
||||||
|
- 공유 member login/JWT/MemberRole.BOT, Agora HMAC, 텍스트 TranslationJob/worker, S3Uploader와 기존 MediaConvert 관련 외부 코드/설정은 보존 대상이다.
|
||||||
|
- 기존 MediaConvert 완료 API의 ADMIN/BOT 권한은 유지한다. 새 원본 UUID에 대한 source-bound 경로 검증과 기존 파일 검증을 구분한다. 실제 consumer ID 추출·출력 규칙 호환성은 운영 확인 전 배포 BLOCKED다.
|
||||||
|
- Flyway/Liquibase 의존성 및 자막 migration은 현재 프로젝트에서 발견되지 않았다. 운영/공유 환경의 적용 schema/data 상태는 조회하지 않았다. additive SQL 문서만 준비하며 DB에는 적용하지 않는다.
|
||||||
|
|
||||||
|
## Lambda와 운영 전환
|
||||||
|
|
||||||
|
명시된 `aws-subtitle-service/docs/FINAL_REPORT.md`와 실제 로컬 ZIP을 대조했다.
|
||||||
|
|
||||||
|
| 산출물 | SHA-256 |
|
||||||
|
|---|---|
|
||||||
|
| `start-subtitle-transcription.zip` | `a0d0b19b2ecb24c0f89caf528bf09fd039558563a99063f082ac64418704abfd` |
|
||||||
|
| `generate-subtitle.zip` | `bfa8742ca436bace853ec2540fc5f70daa24cbab49e75315565d980189fbf209` |
|
||||||
|
|
||||||
|
파일 hash 일치는 AWS 배포 증거가 아니다. 운영 legacy consumer/caller/in-flight/Scheduler/배포 버전은 미확인이다. AWS 호출·설정·배포·중지·삭제·재개를 수행하지 않았다. 운영자는 inventory 후 실제 가동 중인 경우에만 pause/drain하고 Phase 1 backend/Lambda를 조정 배포해야 한다. S3/DB purge나 legacy fake work API는 금지한다.
|
||||||
|
|
||||||
|
## 실행 기록
|
||||||
|
|
||||||
|
| 시점 | 명령/검증 | 관찰 결과 |
|
||||||
|
|---|---|---|
|
||||||
|
| 테스트 범위 변경 지시 전 | 기존 `AudioContentServiceTest`, `MemberLoginResponseContractTest`, `TokenProviderTest` focused baseline | 부모 실행은 kapt 단계 200초 제한 종료, 테스트 판정 없음 |
|
||||||
|
| 테스트 범위 변경 지시 전 | 구현 담당자의 격리 focused 시도 | `red-focused.log`: 41 cases 중 새 사례의 JWT key setup 1건 실패. 기능 RED로 인정하지 않음. 기존 테스트 실행은 이후 중단 |
|
||||||
|
| RED | 새 `SubtitleHttpIntegrationTest.shouldRejectActualNonBotLoginJwt` | `red-http.log`: 실제 로그인한 non-BOT의 자막 접근 차단 실패를 재현. 최종 GREEN 아님 |
|
||||||
|
| 1차 새 테스트/HTTP | 부모가 신규 `SubtitleHttpIntegrationTest` 직접 재실행 | **13 passed, failures/errors/skipped 0**. XML timestamp `2026-11-10T14:33:59`, `BUILD SUCCESSFUL in 2m 21s` |
|
||||||
|
| bootJar/변경 범위 lint | 구현 담당 실제 실행 및 부모 같은 코드 재확인 | `build-lint.log`: `BUILD SUCCESSFUL in 1m 28s`; 부모 재확인은 UP-TO-DATE |
|
||||||
|
| 리뷰 수정 RED | 신규 HTTP 2개 | 빈 body 422 기대/200 실제, DB 실패 500 기대/200 실제 확인 |
|
||||||
|
| 최종 새 테스트/빌드/lint | `P1-R1` 수정 후 신규 자막 클래스 전체 | **15 passed, failures/errors/skipped 0**, bootJar/변경 lint 실제 실행 성공, 7m44s |
|
||||||
|
| 부모 최종 HTTP 재검증 | 추가된 새 2개 메서드만 직접 실행 | **2 passed**, 422/500 및 전체 rollback 확인. XML timestamp `2026-11-10T15:34:43`, 4m55s |
|
||||||
|
| 독립 리뷰 | 1차 REQUEST_CHANGES 1건 수정 후 delta 재검토 | **REV-P1-001 ADDRESSED / 로컬 backend APPROVE**, 추가 blocker 없음 |
|
||||||
|
|
||||||
|
테스트 fixture는 로컬 합성 데이터와 H2, 실제 localhost HTTP, 외부 S3/서명 경계 대체를 사용한다. 실제 음성 또는 live S3 품질 검증이 아니다. LSP diagnostics 도구가 제공되지 않아 실제 Kotlin compile/lint 결과를 대체 증거로 기록했으며 LSP 성공을 주장하지 않는다.
|
||||||
|
|
||||||
|
실제 검증한 HTTP 결과: BOT 허용 및 non-BOT 403/익명 401, unknown 404, callback 수락/replay 200,
|
||||||
|
같은 eventId의 변경 body 422, terminal 다른 결과 409, 동시 receipt 경합 200/422 및 loser rollback.
|
||||||
|
기존 콘텐츠 상세에서 preview-only의 자막 null, 구매/소유자/무료 및 공개 시점별 ko/ja 선택을 검증했다.
|
||||||
|
동일 job 결과는 언어별 행 ID를 보존하며, 이전 job 완료가 최신 선택값을 바꾸지 않는 것을 확인했다.
|
||||||
|
|
||||||
|
실제 HTTP 증거는 테스트 JVM PID `23390`, Tomcat `127.0.0.1:59074`, XML timestamp
|
||||||
|
`2026-11-10T14:33:59`다. 실행 후 `ps -p 23390,20950,22207`, `lsof -iTCP:59074 -sTCP:LISTEN`,
|
||||||
|
작업 root를 대상으로 한 `pgrep`에서 남은 프로세스/listener가 없음을 확인했다. 임시 산출물은 리뷰 후 정리했다.
|
||||||
|
|
||||||
|
후속 HTTP 검증은 PID `44707`, Tomcat port `59977`에서 실행했다. 빈 callback/started는 422이고,
|
||||||
|
receipt 저장 실패는 `{"error":"Subtitle request failed"}`와 500을 반환하며 상태/오류/결과/receipt가 rollback된다.
|
||||||
|
구현 담당의 첫 GREEN은 컴파일 600초 제한으로 종료됐으나 캐시 재사용 후 성공했다. 실패 이력은 삭제하지 않았다.
|
||||||
|
후속 PID `44707`/`28638`/`37727`/`39291`, port `59977`, 작업 root 참조 프로세스의 부재도 확인했다.
|
||||||
|
|
||||||
|
## 내부 API와 저장 구조
|
||||||
|
|
||||||
|
| Method | Path |
|
||||||
|
|---|---|
|
||||||
|
| GET | `/internal/api/subtitle-jobs/resolve` |
|
||||||
|
| GET | `/internal/api/subtitle-jobs/{jobId}` |
|
||||||
|
| POST | `/internal/api/subtitle-jobs/{jobId}/started` |
|
||||||
|
| POST | `/internal/api/subtitle-jobs/callback` |
|
||||||
|
|
||||||
|
네 API는 기존 로그인 JWT와 활성 DB BOT role을 요구한다. 조회는 최소 DTO이며 callback은 원문 bytes hash를 사용한다.
|
||||||
|
`subtitle_job`, 언어별 `subtitle`, `subtitle_callback_receipt`와 `content.latest_subtitle_job_id`의
|
||||||
|
additive SQL만 작성했다. 실제 DB 적용은 하지 않았다. 미래 언어는 결과 행으로 저장할 수 있지만
|
||||||
|
현재 허용 언어는 ko/ja뿐이고 향후 Lambda·API·조회 정책 변경 없이 새 언어가 자동 활성화되지는 않는다.
|
||||||
|
|
||||||
|
공유 로그인/JWT/업로드/MediaConvert는 보존했다. 현재 저장소에는 삭제할 자막 recovery code가 없어서 삭제 파일은 0개다.
|
||||||
|
외부 프로젝트, AWS 리소스, 적용 migration 및 운영 데이터는 변경하지 않았다. commit/push도 수행하지 않았다.
|
||||||
|
|
||||||
|
전체 변경 경로·신규 case 이름·재현 명령·실패/수정 이력은 [implementation-report.md](implementation-report.md)에 있다.
|
||||||
|
로컬 mapping의 업로드 오류 시각 TIMESTAMP 불일치는 수정·대조했으나 운영 MySQL 실행 증거는 아니다.
|
||||||
|
테스트 전체 Spring context에서 기존 worker의 H2/MySQL 구문 차이 로그가 발생했다. 해당 worker 성공을 주장하지 않는다.
|
||||||
|
|
||||||
|
## 외부 차단 항목
|
||||||
|
|
||||||
|
| 항목 | 상태와 재개 조건 |
|
||||||
|
|---|---|
|
||||||
|
| 운영 DB | BLOCKED: 기존 schema 확인 및 additive DDL 승인·적용 필요 |
|
||||||
|
| old consumer coordination | BLOCKED: 원격 배포/사용/in-flight 조사와 운영자 조정 필요 |
|
||||||
|
| MediaConvert/CloudFront | BLOCKED: UUID ID 추출·출력 key 규칙 및 자막 private origin/서명 접근 확인 필요 |
|
||||||
|
| live ko/ja/mixed | BLOCKED: 승인된 실제 S3 입력부터 callback/콘텐츠/앱 표시 검증 필요. mixed는 실제 word/item 언어 evidence 필수 |
|
||||||
|
| 2시간 입력 목표 | BLOCKED: 실제 처리 시간/예산 미측정. 상한을 내리거나 SLA/지원 성공을 주장하지 않음 |
|
||||||
|
|
||||||
|
전체 테스트는 실행하지 않는다. 사용자가 별도로 전체 검증을 원하면 `./gradlew test`를 실행할 수 있다. 기존 회귀 미실행에 따른 잔여 위험은 새 기능 테스트 성공과 구분한다.
|
||||||
|
|
||||||
|
## 종료 정리와 재현
|
||||||
|
|
||||||
|
2026-11-11 리뷰 승인 후 이번 작업 소유 임시 root `subtitle-phase1.wiRcAJ`와 그 안의 build/cache/log/JAR를 제거했다.
|
||||||
|
`test ! -e` exit0으로 부재를 확인했고 공유 캐시·기존 worker는 변경하지 않았다. 위 임시 경로는 과거 검증 위치이며,
|
||||||
|
검증 결과와 실패 이력은 이 문서 및 구현/리뷰 보고서에 보존했다.
|
||||||
|
|
||||||
|
정상 개발 환경에서 이번에 작성한 테스트만 다시 실행하는 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 후속 완료: generate-url 자막 응답
|
||||||
|
|
||||||
|
2026-11-11 요청에 따라 `GET /audio-content/{id}/generate-url`에도 상세와 동일한 nullable `subtitle`을 추가했다.
|
||||||
|
기존 `contentUrl`과 활성 구매 검사/음원 URL 동작은 유지하며 자막만 요청 언어·완료 상태·공개 시점 조건을 따른다.
|
||||||
|
이번 후속에서 새로 작성한 HTTP3개만 실행했고 부모 재실행도 3/0/0/0으로 통과했다. 독립 리뷰 APPROVE다.
|
||||||
|
컴파일/bootJar/테스트 lint는 성공했으나 production lint는 시작 전부터 있던 서비스 EOF 공백1건을 지적해 보존했다.
|
||||||
|
이 결과를 기존15개를 포함한 클래스 전체 재실행 성공으로 표현하지 않는다.
|
||||||
|
상세 명령/선택자/응답 예시/잔여 게이트는 [generate-url-report.md](generate-url-report.md)에 있다.
|
||||||
|
|
||||||
|
## 커밋 검증 후속 기록
|
||||||
|
|
||||||
|
2026-11-11 별도 사용자 `/commit-with-validation` 요청으로 변경을 커밋했다.
|
||||||
|
기능 연동 커밋은 pre-commit `ktlintCheck`에서 위에 기록된 서비스 EOF 빈 줄 1건으로 처음 거부됐다.
|
||||||
|
해당 빈 줄만 제거한 뒤 재시도하여 staged Kotlin 대상 lint, `git diff --staged --check` 및
|
||||||
|
`work/scripts/check-commit-message-rules.sh`의 커밋 직전·직후 검증이 통과했다.
|
||||||
|
기존 EOF 미해결 기록은 당시 상태이며 이 커밋 준비 과정에서 해소됐다.
|
||||||
|
|
||||||
|
이번 커밋 작업에서는 기능 테스트·전체 테스트·bootJar를 재실행하지 않았다.
|
||||||
|
기존 테스트 결과는 위 실행 시점의 기록이며 운영 배포 BLOCKED는 유지한다.
|
||||||
|
사용자가 전체 테스트를 실행할 때의 명령은 `./gradlew test`다.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# generate-url 자막 후속 구현 검증
|
||||||
|
|
||||||
|
## 범위와 결과
|
||||||
|
|
||||||
|
- 기준: `prd.md` §7, `plan-task.md` `P2-T1`. Phase 1은 재구현하지 않았다.
|
||||||
|
- 이번 production 변경은 `content/GenerateUrlResponse.kt`의 기존 DTO import/nullable 필드 추가와 `content/AudioContentService.kt`의 `generateUrl` 본문뿐이다. DTO는 `GenerateUrlResponse(contentUrl: String, subtitle: ContentSubtitleResponse? = null)`이다.
|
||||||
|
- 기존 `orderRepository.isExistOrdered`를 그대로 사용한다. 구매 없는 소유자/무료, 비활성 KEEP, 만료 RENTAL은 빈 `contentUrl`과 명시적 null `subtitle`이다. 활성 구매의 기존 음원 경로/만료 기간/envelope는 유지했다.
|
||||||
|
- 구매자는 하나의 `expirationTime`을 음원 서명과 `SubtitleJobService.subtitle(audioContent, langContext.lang, expirationTime)`에 전달한다. 자막만 미지정/도래한 공개일 또는 소유자 조건을 적용한다. 예약 구매자의 기존 음원 URL은 유지한다.
|
||||||
|
- 기존 `SubtitleHttpIntegrationTest`의 15개 메서드/도우미를 보존하고 HTTP 테스트 3개와 세 테스트가 공용으로 사용하는 요청 도우미 1개만 추가했다. 다른 dirty 파일, PRD/plan/final-report, controller/DB/Lambda는 수정하지 않았다.
|
||||||
|
|
||||||
|
## 새 HTTP 테스트
|
||||||
|
|
||||||
|
클래스: `kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest`. 실행은 아래 세 메서드 selector만 사용했다.
|
||||||
|
|
||||||
|
1. `shouldIncludeRequestedSubtitleInGenerateUrl`: 활성 KEEP 구매/공개 완료 ko·ja, 정확한 `{contentUrl,subtitle}` 및 자막 3필드, 상세와 동일한 자막, 음원/SRT 경로와 같은 14,400,000ms 서명 인자, 기존 success envelope.
|
||||||
|
2. `shouldReturnNullSubtitleInGenerateUrlWhenUnavailable`: PENDING/최신 job 미등록/부분 완료 ko 누락/미지원 en의 명시적 null과 음원 URL 보존, 부분 완료 ja 정상 제공.
|
||||||
|
3. `shouldPreserveGenerateUrlPurchaseAndReleaseRestrictions`: 미구매자·소유자·무료 및 비활성/만료 주문 차단, 활성 RENTAL 구매자의 예약 음원 URL 보존/자막 차단, 활성 구매 소유자의 예약 예외, 공개일 null/과거 허용, 익명 401.
|
||||||
|
|
||||||
|
실제 EMAIL 로그인/JWT, RANDOM_PORT Tomcat HTTP, H2와 기존 fixture/callback 도우미를 사용했다. 외부 S3/CloudFront 경계만 기존 mock을 재사용했다. 서명 URL은 전달 경로/만료 인자 검증이며 실제 CloudFront 암호학적 서명이나 다운로드 검증은 아니다.
|
||||||
|
|
||||||
|
## 명령과 증거
|
||||||
|
|
||||||
|
소유 root는 아래 `VERIFY_ROOT`다. build/project-cache/TMPDIR/Node/Bun 캐시를 격리했으며 init script는 production 소스를 필터링하지 않는다. 테스트 compile만 이 클래스와 필요한 support 3파일로 한정한다. `test.outputs.upToDateWhen { false }`로 실제 실행을 강제한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export VERIFY_ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/generate-url-subtitle.fSBkJM/implementer
|
||||||
|
export TMPDIR="$VERIFY_ROOT/tmp" NODE_COMPILE_CACHE="$VERIFY_ROOT/node-cache"
|
||||||
|
export npm_config_cache="$VERIFY_ROOT/node-cache" BUN_INSTALL_CACHE_DIR="$VERIFY_ROOT/bun-cache"
|
||||||
|
export AWS_EC2_METADATA_DISABLED=true
|
||||||
|
G=(./gradlew --offline --no-daemon --max-workers=2
|
||||||
|
-Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g
|
||||||
|
--project-cache-dir "$VERIFY_ROOT/project-cache"
|
||||||
|
--init-script "$VERIFY_ROOT/verification.init.gradle")
|
||||||
|
S=(--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldIncludeRequestedSubtitleInGenerateUrl'
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReturnNullSubtitleInGenerateUrlWhenUnavailable'
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldPreserveGenerateUrlPurchaseAndReleaseRestrictions')
|
||||||
|
|
||||||
|
# RED: production 변경 전
|
||||||
|
"${G[@]}" test "${S[@]}" > "$VERIFY_ROOT/red-assertions.log" 2>&1
|
||||||
|
# 첫 GREEN
|
||||||
|
"${G[@]}" test "${S[@]}" ktlintCheck bootJar > "$VERIFY_ROOT/green-build-lint.log" 2>&1
|
||||||
|
# 입력 3파일을 고정/확인한 production+test lint
|
||||||
|
"${G[@]}" verifyLintScope ktlintCheck bootJar > "$VERIFY_ROOT/final-verified-scope-lint-build.log" 2>&1
|
||||||
|
# 기존 dirty EOF 보존 후 최종 HTTP/컴파일/테스트 lint/bootJar
|
||||||
|
"${G[@]}" test "${S[@]}" verifyLintScope ktlintTestSourceSetCheck bootJar > "$VERIFY_ROOT/final-green-build-test-lint.log" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
| 검증 | 결과/증거 |
|
||||||
|
|---|---|
|
||||||
|
| RED | exit 1, 50초, 3 tests/3 failures/0 errors/0 skipped. 모두 `has("subtitle")`에서 expected true/actual false. `red-assertions.log`, `red-summary.log` |
|
||||||
|
| 첫 GREEN | exit 0, `BUILD SUCCESSFUL in 3m 35s`, HTTP 3개 통과 및 compile/bootJar. lint의 정확한 입력 증거는 이후 고정한 실행을 기준으로 한다. |
|
||||||
|
| 정확한 변경 lint | 입력은 production 2파일+이 테스트 1파일, Kotlin script 0개임을 `verifyLintScope` assertion으로 확인. production lint는 **기존 EOF 빈 줄 1건 실패**: `AudioContentService.kt:1482:1 Needless blank line(s)`. 그 외 지적 없음. `final-verified-scope-lint-build.log` |
|
||||||
|
| 최종 GREEN | exit 0, `BUILD SUCCESSFUL in 3m 30s`, compileKotlin/compileTestKotlin/bootJar 실제 실행, 테스트 lint 통과. HTTP XML 3 tests/0 failures/0 errors/0 skipped, timestamp `2026-11-10T22:07:38` (UTC). `final-green-build-test-lint.log`, `green-summary.log` |
|
||||||
|
|
||||||
|
최초 임시 init의 상대 source 경로로 `test NO-SOURCE`가 된 `red.log`는 RED 증거가 아니다. `project.file`로 수정 후 실제 assertion RED를 확인했다. `final-scope-lint-build.log`도 lint scope introspection assertion 실패 기록이며 HTTP 실패가 아니다. 이후 init에서 lint 입력 파일을 명시적으로 고정하고 검증했다. 이 이력과 로그를 삭제하지 않았다.
|
||||||
|
|
||||||
|
## 보존 사항과 남은 게이트
|
||||||
|
|
||||||
|
- 시작 전 `git diff`에도 있던 서비스 EOF 빈 줄은 타 dirty 변경 보존 지시에 따라 유지했다. `git diff --check` 역시 같은 EOF 1건을 지적하므로 lint 전체 clean을 주장하지 않는다. 테스트/새 DTO lint 오류는 없다.
|
||||||
|
- LSP 도구는 제공되지 않았다. Kotlin compile과 실제 3파일 ktlint 결과로 대체했다. 기존 Kotlin 경고와 테스트 종료 중 Redis reconnect 경고가 있으나 새 테스트 실패/오류는 없다.
|
||||||
|
- 기존 15개 테스트/기타 기존 테스트/전체 test/check/build는 실행하지 않았다. 사용자 전체 테스트 명령은 `./gradlew test`이며 이번 승인된 자동 검증 범위 밖이다.
|
||||||
|
- RED PID 14462/HTTP 50806, 첫 GREEN PID 15417/HTTP 50833, 최종 GREEN PID 16741/HTTP 50879는 테스트 종료로 정상 종료했다. 세 PID와 HTTP/Redis port 50806/50833/50879/50803/50831/50877에 `ps`/`lsof` 조회 결과가 없었다. 공유 프로세스를 종료하거나 task 취소를 호출하지 않았다. 부모 재검증을 위해 소유 root/init/log/JAR는 남겼다.
|
||||||
|
- AWS/외부 저장소/운영 DB/배포/live ko·ja·mixed/2h 처리/앱/실제 CloudFront 다운로드는 미검증이다. 부모 재검증과 독립 리뷰도 이 보고서의 완료 주장에 포함하지 않는다. credential/JWT/request body는 로그/보고서에 출력하지 않았으며 최종 XML과 로그의 관련 패턴 검색 결과도 없었다.
|
||||||
|
|
||||||
|
## 부모 재검증
|
||||||
|
|
||||||
|
- 같은 격리 init 및 정확히 새 메서드 3개 selector로 직접 재실행했다. `parent-verification.log`의 결과는 exit0,
|
||||||
|
`BUILD SUCCESSFUL in 37s`이며 test는 실제 실행, bootJar/테스트 lint는 동일 코드의 UP-TO-DATE 확인이다.
|
||||||
|
- XML: 3 tests/0 failures/0 errors/0 skipped, UTC timestamp `2026-11-10T22:12:51`. 실제 Tomcat PID17759, HTTP port50914다.
|
||||||
|
- 부모가 DTO·generateUrl의 전후 diff 및 추가 테스트 전체를 읽었다. 공유 자막 조회와 기존 구매 판정·음원 URL이 유지된다.
|
||||||
|
- 독립 리뷰와 임시 자원 정리는 아래 후속 기록으로 남긴다. 기존 production EOF lint1건은 성공으로 덮어쓰지 않는다.
|
||||||
|
|
||||||
|
## 독립 리뷰 및 사용 계약
|
||||||
|
|
||||||
|
- 제한 독립 리뷰 `ses_e387b1e8fffensjb6dnoRk82ST`: **APPROVE**. 이번 production delta의 명세·품질 blocker나 새 회귀 없음.
|
||||||
|
- `data.contentUrl`과 기존 envelope는 유지하며 `data.subtitle`만 추가된다. 요청 언어는 기존 `Accept-Language`/`LangContext`를 따른다.
|
||||||
|
- 자막 응답 예시:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contentUrl": "https://cdn.example.com/audio-signed-url",
|
||||||
|
"subtitle": {
|
||||||
|
"language": "ko-KR",
|
||||||
|
"format": "SRT",
|
||||||
|
"url": "https://cdn.example.com/subtitle-signed-url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 구매/공개 조건을 충족하지 못하거나 미완료·해당 언어 자막이 없으면 `subtitle`은 명시적 null이다. 기존 URL 권한을 확대하지 않는다.
|
||||||
|
- 부모 HTTP PID17759 및 자식 PID14462/15417/16741, port50914, 소유 root 참조 프로세스는 `ps`/`lsof`/`pgrep`에서 부재를 확인했다.
|
||||||
|
- 리뷰 승인/결과 문서 보존 후 작업 소유 root `generate-url-subtitle.fSBkJM`과 하위 build/cache/log/JAR를 제거했고 `test ! -e` exit0으로 확인했다. 위 임시 log/init 경로는 과거 검증 위치이며 공유 캐시는 건드리지 않았다.
|
||||||
|
|
||||||
|
일반 개발 환경에서 이번 후속 테스트만 실행하는 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldIncludeRequestedSubtitleInGenerateUrl' \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReturnNullSubtitleInGenerateUrlWhenUnavailable' \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldPreserveGenerateUrlPurchaseAndReleaseRestrictions'
|
||||||
|
```
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# 자막 Phase 1 Kotlin 구현 보고
|
||||||
|
|
||||||
|
## 결과와 검증 경계
|
||||||
|
|
||||||
|
- 재생성을 제외한 기존 업로드 등록, 네 BOT 내부 API, callback 원자 저장, 기존 콘텐츠 상세의 권한별 ko/ja 자막 조회를 구현했다.
|
||||||
|
- 새로 작성한 `SubtitleHttpIntegrationTest` 13개는 실제 RANDOM_PORT localhost HTTP/H2에서 전부 통과했다. 외부 AWS 클라이언트와 서명 URL 생성만 대체하고, 회원 서비스·로그인·비밀번호 검증·JWT·DB principal/role은 실제 경로를 사용했다.
|
||||||
|
- `bootJar`와 변경 Kotlin 파일에 한정한 `ktlintCheck`가 성공했다. LSP 도구는 제공되지 않아 실행하지 않았으며 실제 Kotlin/JPA 생성 코드·테스트 컴파일과 lint로 대체했다.
|
||||||
|
- PRD/계획과 SQL/deployment 문서는 병렬 담당자 소유다. 이 담당자는 해당 문서를 수정하지 않았으며 전체 Phase gate/독립 리뷰 완료를 대신 선언하지 않는다.
|
||||||
|
|
||||||
|
## 구현 선택
|
||||||
|
|
||||||
|
1. `AudioContentService.createAudioContent` 두 overload와 AI 관리자 create orchestration은 `NOT_SUPPORTED`다. 필수 주입된 transaction manager의 `TransactionTemplate(REQUIRES_NEW)`가 content·tags·cover·source/job·latest pointer를 commit한 뒤 원본 S3 업로드를 시작한다. 업로드 후 content의 input 경로를 다시 대입하지 않으므로 조기 MediaConvert 완료 경로를 되돌리지 않는다. 성공 후 기존 언어 이벤트는 별도의 짧은 실제 쓰기 트랜잭션에서 발행한다.
|
||||||
|
2. 원본은 `input/{contentId}/{canonical UUID}.{safe actual extension}`이다. multipart filename의 경로는 버리고 영문/숫자 1~16자리 실제 확장자만 소문자로 보존한다. 예를 들어 `C:\client\dir/file.MP3`는 UUID `.mp3`로 등록된다. 확장자가 없거나 안전하게 보존할 수 없으면 UUID만 사용한다. 업로드 자체를 새로 거부하거나 `.mp3`로 허위 변경하지 않는다. 확장자는 형식 hint일 뿐 실제 오디오 검증 증거가 아니다.
|
||||||
|
3. `SubtitleJob.outputKey`는 확장자를 제외한 `output/{contentId}/{same UUID}` stem이다. 새 등록의 upload-complete는 이 정확한 stem과 `aac`/`m4a`/`mp3` 확장자 조합만 수락한다. 다른 콘텐츠, 다른 UUID, 추가 suffix, 임의 디렉터리, 비오디오 확장자는 거부한다. source가 없는 기존 콘텐츠의 동작은 기존 prefix 검증을 유지한다. 실제 consumer의 basename·ID 추출 호환성은 여전히 배포 전 확인 대상이다.
|
||||||
|
4. 언어 결과는 `Subtitle` 행에 저장한다. `jobId` scalar와 `unique(job_id, language)`, 실제 `language`·`format`·`s3Bucket`·`s3Key`를 보존한다. callback 허용 언어는 여전히 ko-KR/ja-JP이며 상태 규칙은 2/1/0개 그대로다. 미래 언어 활성화/새 provider framework는 추가하지 않았다.
|
||||||
|
5. callback은 원래 HTTP `ByteArray` SHA-256을 receipt와 함께 저장한다. `REQUIRES_NEW`/`READ_COMMITTED`에서 receipt를 먼저 조회하고 job lock 뒤 다시 조회한다. 기존 eventId로 unknown job을 보낸 변경 body도 422다. receipt는 JPA `Persistable.isNew=true`로 insert하여 merge가 경합한 다른 receipt를 update하지 못하게 했다. unique 경합은 rollback 종료 후 새 트랜잭션에서 receipt를 읽어 200/422를 판정한다. terminal의 동일 결과는 행 ID를 보존하고, 다른 결과는 409다.
|
||||||
|
|
||||||
|
## 변경 경로
|
||||||
|
|
||||||
|
### 신규 production
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/domain/SubtitleJobStatus.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/dto/SubtitleJobDtos.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleJob.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleJobRepository.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/Subtitle.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleRepository.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleCallbackReceipt.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleCallbackReceiptRepository.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/application/SubtitleJobService.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleJobController.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleRequestParser.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleExceptionHandler.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 기존 production 연결
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt
|
||||||
|
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 테스트와 보고
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/test/kotlin/kr/co/vividnext/sodalive/v2/subtitle/SubtitleHttpIntegrationTest.kt
|
||||||
|
src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt
|
||||||
|
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt
|
||||||
|
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt
|
||||||
|
docs/20261110_자막Phase1연동/implementation-report.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- 기존 수동 `AudioContentService` 생성자는 필수 자막 서비스/transaction manager 인자를 반영했다.
|
||||||
|
- 기존 생성 통합 테스트의 성공 fixture는 등록 전에 실제 commit되도록 조정했다. audio 업로드/후속 이벤트 실패 테스트는 이미 commit된 등록을 보존한다는 새 승인 계약에 맞췄다. 실패 테스트를 삭제하거나 성공 assertion을 느슨하게 하지 않았다.
|
||||||
|
- 이 기존 테스트들은 후속 사용자 지시 이후 **실행하지 않았다**. 관련 기존 5개 클래스는 새 테스트 실행의 컴파일 입력에 포함했지만 실행 selector는 신규 자막 패키지뿐이다.
|
||||||
|
- production 파일 삭제, 기존 JWT/member/텍스트 TranslationJob worker 변경, 외부 프로젝트 변경, 신규 AWS SDK 의존성, 재생성 API/START 호출은 없다.
|
||||||
|
|
||||||
|
## 새 테스트 selector
|
||||||
|
|
||||||
|
전체 신규 클래스만 실행하는 selector:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest'
|
||||||
|
```
|
||||||
|
|
||||||
|
개별 selector는 아래 클래스명 뒤에 각 메서드를 붙인다. 모두 이번 작업에서 새로 작성한 case다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRejectActualNonBotLoginJwt
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldUseRealBotJwtAndStoreCallbackBeforeStartedWithExactByteReplay
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldStartMonotonicallyAndAcceptPartialAndFailedResults
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReturn404WithoutDiscoveryAndCheckCurrentDatabaseRole
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRejectMalformedContractsWithoutPersistingAnything
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldSerializeConcurrentCallbacksAndRollbackLosingJob
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRollbackResultsAndStatusWhenReceiptInsertFails
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldKeepLatestPointerWhenOlderJobCompletes
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldCommitRegistrationBeforeUploadAndNeverRevertEarlyMediaConvertOutput
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldKeepAcceptedSourceAndTerminalResultWhenUploadReturnFails
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldPreserveOnlySafeActualSourceExtensionsWithoutClaimingConversion
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldExposeLanguageSubtitleOnlyWithReleasedOriginalAccessInExistingDetail
|
||||||
|
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReadOnlyPersistedLanguageOfMatchingLatestCompletedJob
|
||||||
|
```
|
||||||
|
|
||||||
|
## 재현 명령과 아티팩트
|
||||||
|
|
||||||
|
작업 전용 root는 삭제하지 않고 부모 검증에 인계한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/subtitle-phase1.wiRcAJ/backend.HX0j0B
|
||||||
|
export TMPDIR="$ROOT"
|
||||||
|
export NODE_COMPILE_CACHE="$ROOT/node"
|
||||||
|
export BUN_RUNTIME_TRANSPILER_CACHE_PATH="$ROOT/bun"
|
||||||
|
|
||||||
|
./gradlew --no-daemon --max-workers=2 \
|
||||||
|
-Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
|
||||||
|
--project-cache-dir "$ROOT/gradle-cache" -I "$ROOT/isolation.gradle" \
|
||||||
|
test --tests 'kr.co.vividnext.sodalive.v2.subtitle.*'
|
||||||
|
|
||||||
|
./gradlew --no-daemon --max-workers=2 \
|
||||||
|
-Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
|
||||||
|
--project-cache-dir "$ROOT/gradle-cache" -I "$ROOT/isolation.gradle" \
|
||||||
|
bootJar ktlintCheck
|
||||||
|
```
|
||||||
|
|
||||||
|
- 임시 init script는 build 출력과 Gradle project cache를 공유 출력에서 격리한다. 테스트 Kotlin 컴파일 입력은 신규 자막/직접 영향 기존 5개 클래스/support이며, lint filter도 변경 Kotlin 파일만 대상으로 한다. 전역 설정이나 저장소 build 설정은 변경하지 않았다.
|
||||||
|
- 성공 테스트: `$ROOT/green-new-tests-cached.log`, `BUILD SUCCESSFUL in 1m 32s`. 임시 script의 사용하지 않는 JWT 기본값을 제거한 뒤 다시 실행한 최종 `$ROOT/final-new-tests.log`도 `BUILD SUCCESSFUL in 2m 25s`다. XML: `$ROOT/build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.xml`, `tests=13`, `failures=0`, `errors=0`, `skipped=0`.
|
||||||
|
- 실제 두 job의 동일 eventId 경합에서 H2 SQLState `23505` unique 위반이 관찰됐고, 최종 HTTP 200/422와 loser job의 PENDING/결과 행 없음 assertion이 통과했다. 별도의 receipt 쓰기 실패 주입에서도 identity 생성으로 flush된 결과 행까지 rollback됨을 확인했다.
|
||||||
|
- 성공 빌드/lint: `$ROOT/build-lint.log`, `BUILD SUCCESSFUL in 1m 28s`. 실행 가능한 boot JAR은 `$ROOT/build/libs/sodalive-0.0.1-SNAPSHOT.jar`다. `jar tf`에서 새 컨트롤러/서비스/엔티티/repository/DTO가 `BOOT-INF/classes`에 포함된 것을 확인했다.
|
||||||
|
- 검증 완료 후 root를 참조하는 작업 소유 Gradle/HTTP/Redis 프로세스는 관찰되지 않았다. 기존 공유 worker PID `55393`과 그 Redis PID `55452`는 이 작업 소유가 아니며 종료하지 않았다. 테스트 실행 중 관찰된 작업 소유 executor PID `20950`과 최종 executor PID `22207`은 종료됐다.
|
||||||
|
|
||||||
|
## RED와 실패 이력
|
||||||
|
|
||||||
|
- 첫 baseline/신규 RED 명령은 테스트 컴파일 중 600초 제한으로 종료됐다(`red.log`). 테스트 결과가 아니며 RED로 인정하지 않았다.
|
||||||
|
- `red-focused.log`는 이전 지시에 따른 기존 3개 회귀 클래스와 신규 1개 case 실행이다. 총 41개 중 1개가 테스트 JWT key setup에서 실패했다. 이를 기능 RED로 인정하지 않았다. 이 명령은 기존 테스트 실행 금지라는 후속 사용자 지시 전의 기록이며 현재 구현의 회귀 성공 증거로 사용하지 않는다.
|
||||||
|
- 테스트 전용 `DynamicPropertySource`로 유효 길이의 비운영 JWT key를 지정한 뒤 `red-http.log`에서 실제 로그인 성공 후 `403 기대 / 404 실제`를 확인했다. BOT 전용 경로가 없던 동작의 실제 RED다.
|
||||||
|
- 초기 `red-binding.log`는 JPA unique constraint의 logical/physical column 이름 오류로 context가 뜨지 못했다. 이를 RED로 인정하지 않고 엔티티에 명시적 column 이름을 붙였다.
|
||||||
|
- `red-binding-assertions.log`에서 새 두 case가 실제 assertion으로 실패했다: source 확장자 보존 실패, 기존 eventId의 unknown job 변경에 대해 `422 기대 / 404 실제`. 확장자 전달과 receipt 선조회/명시적 isolation 변경 후 같은 두 case를 포함한 13개 전체 신규 case가 GREEN이다.
|
||||||
|
- `green-first.log`는 사용자 지시 갱신을 위한 중단으로 완료 결과가 없다. `green-new-tests.log`는 compile을 통과한 뒤 test 시작 시점에 600초 도구 제한으로 종료됐다. 공유 프로세스를 종료하지 않고 같은 격리 경로의 캐시를 재사용한 `green-new-tests-cached.log`에서 완료 결과를 얻었다.
|
||||||
|
- `format.log`의 첫 변경 파일 format 실행은 길이 위반을 보고했다. 해당 새 줄만 정리한 뒤 최종 `ktlintCheck`가 통과했다.
|
||||||
|
|
||||||
|
## 외부 게이트와 남은 미검증
|
||||||
|
|
||||||
|
1. 실제 MediaConvert consumer가 기존 contentId filename prefix를 읽는지, UUID basename·출력 확장자·디렉터리를 어떻게 구성하는지 운영 증거가 없다. 로컬에서는 정확한 UUID stem과 AAC/M4A/MP3 경로 검증만 확인했다. consumer의 운영 호환성을 확인하기 전 배포 전환은 BLOCKED다.
|
||||||
|
2. 부모가 승인된 ZIP에서 확인한 Transcribe 지원 확장자는 amr/flac/m4a/mp3/mp4/ogg/webm/wav다. Spring은 AAC/opus 등 기존 업로드를 임의로 거부하지 않는다. 이들 또는 누락/불안전 확장자에 대한 자막 성공은 보장하지 않으며 Lambda의 명시적 unsupported 결과/수동 확인 대상이다. 테스트의 한 바이트 파일은 source binding/transaction fixture일 뿐 실제 미디어 증거가 아니다.
|
||||||
|
3. S3 putObject 수락 후 getUrl 실패는 성공으로 숨기지 않는다. 원래 예외를 호출자에게 전파하고 `SOURCE_UPLOAD_UNCERTAIN` 및 초 정밀도 TIMESTAMP를 별도 기록한다. 이미 완료된 자막 상태/metadata와 content를 삭제하거나 회귀시키지 않는다. 실제 객체/MediaConvert 상태는 운영자 수동 확인이 필요하다.
|
||||||
|
4. 운영 MySQL DDL 적용, case-sensitive/NO PAD collation 및 FK/인덱스 검증, private CloudFront origin과 실제 SRT 서명 접근, 실서버 권한/앱 표시, live ko/ja/mixed 및 실제 duration/2시간 처리 예산은 미검증이다. AWS 호출·배포·DB 적용은 수행하지 않았다.
|
||||||
|
5. 전체 테스트와 후속 지시 이후의 기존 회귀 case 실행은 수행하지 않았다. H2 전체 Spring context의 기존 charge/translation scheduler는 MySQL `FOR UPDATE SKIP LOCKED` 구문 때문에 로그 오류를 남겼다. 이번 신규 case는 모두 통과했지만 그 worker 기능의 성공 증거는 아니다. 사용자가 별도로 전체 테스트를 원할 때만 `./gradlew test`를 실행한다.
|
||||||
|
|
||||||
|
## 20261111 P1 후속 수정: body/DB 실패의 HTTP 200 오인 방지
|
||||||
|
|
||||||
|
### 수정 범위와 계약
|
||||||
|
|
||||||
|
- `SubtitleExceptionHandler.kt`의 기존 `SubtitleJobController` 전용 scope와 우선순위를 유지했다. `HttpMessageNotReadableException`은 HTTP 422와 기존 일반화된 거절 body로, 그 외 예상하지 못한 `Exception`은 HTTP 500과 `{"error":"Subtitle request failed"}`로 응답한다.
|
||||||
|
- 기존 `ResponseStatusException`의 404/409/422 처리는 변경하지 않았다. 공통 `SodaExceptionHandler`, JWT, 컨트롤러, 서비스 및 DB 저장 로직은 변경하지 않았으며 예외 원문·요청·토큰 로그도 추가하지 않았다.
|
||||||
|
- 수정 파일은 자막 advice, `SubtitleHttpIntegrationTest.kt` 및 이 보고서의 추가 기록뿐이다. 기존 직접 서비스 롤백 테스트 `shouldRollbackResultsAndStatusWhenReceiptInsertFails`와 기존 assertion은 그대로 보존했다. 부모 소유 PRD/계획/final-report는 수정하지 않았다.
|
||||||
|
|
||||||
|
### 새 HTTP 두 사례
|
||||||
|
|
||||||
|
1. `shouldRejectEmptyCallbackAndStartedBodiesWithoutPersistingAnything`: 실제 BOT 로그인/JWT로 빈 callback·started body를 모두 전송해 422를 확인한다. job/receipt/결과 행 수, PENDING 상태, 갱신 시각 및 오류 필드가 변경되지 않고 해당 job 결과도 없음을 검증한다.
|
||||||
|
2. `shouldReturn500AndRollbackWhenReceiptInsertFailsOverHttp`: 실제 HTTP callback에서 기존 receipt repository SpyBean의 `saveAndFlush`에 DB 예외를 주입한다. ko-KR partial callback을 사용하여 상태뿐 아니라 오류 필드도 변경 대상에 포함하고, 일반화된 500 body 및 receipt·결과 부재, PENDING/갱신 시각/오류 필드 롤백을 DB 재조회로 확인한다.
|
||||||
|
|
||||||
|
### RED/GREEN 명령과 증거
|
||||||
|
|
||||||
|
위 격리 `ROOT`와 `TMPDIR`/Node/Bun cache 환경을 그대로 사용했다. 외부 의존성 다운로드를 막는 `--offline`과 부모의 `verification.gradle`을 추가하여 테스트만 강제로 다시 실행했다. `clean` 및 무필터 test/check/build는 실행하지 않았다.
|
||||||
|
|
||||||
|
advice 수정 전 RED와 수정 후 두 사례 GREEN에 사용한 동일 selector/명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew --offline --no-daemon --max-workers=2 \
|
||||||
|
-Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
|
||||||
|
--project-cache-dir "$ROOT/gradle-cache" \
|
||||||
|
-I "$ROOT/isolation.gradle" -I "$ROOT/../verification.gradle" \
|
||||||
|
test \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRejectEmptyCallbackAndStartedBodiesWithoutPersistingAnything' \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReturn500AndRollbackWhenReceiptInsertFailsOverHttp'
|
||||||
|
```
|
||||||
|
|
||||||
|
- RED: `$ROOT/p1-red-two-http.log`, `BUILD FAILED in 4m 16s`, 정확히 2개 실행/2개 assertion 실패다. `$ROOT/p1-red-assertions.log`에 XML의 실패 증거를 보존했다. 빈 body는 `expected [422, 422] / actual [200, 200]`, DB 실패는 `expected 500 / actual 200`이었다.
|
||||||
|
- 첫 GREEN 시도 `$ROOT/p1-green-two-http.log`는 `compileKotlin` 도중 600초 도구 제한으로 종료됐다. 테스트가 실행되지 않아 GREEN으로 계산하지 않았다. 작업 소유 프로세스가 남지 않은 것을 확인한 후 같은 캐시와 1200초 제한으로 재실행했다.
|
||||||
|
- 두 사례 GREEN: `$ROOT/p1-green-two-http-retry.log`, `BUILD SUCCESSFUL in 13m 29s`. `$ROOT/p1-green-two-assertions.log`는 `tests=2`, `failures=0`, `errors=0`, `skipped=0`이다.
|
||||||
|
|
||||||
|
이후 자막 신규 클래스 15개 전체와 기존 격리 script의 변경 Kotlin 파일 lint 및 boot JAR를 함께 검증했다:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew --offline --no-daemon --max-workers=2 \
|
||||||
|
-Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
|
||||||
|
--project-cache-dir "$ROOT/gradle-cache" \
|
||||||
|
-I "$ROOT/isolation.gradle" -I "$ROOT/../verification.gradle" \
|
||||||
|
test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest' \
|
||||||
|
bootJar ktlintCheck
|
||||||
|
```
|
||||||
|
|
||||||
|
- 최종 `$ROOT/p1-green-15-build-lint.log`: `BUILD SUCCESSFUL in 7m 44s`. production/test 컴파일은 `UP-TO-DATE`였으며 test, bootJar, main/test 변경 파일 lint는 실제 실행됐다.
|
||||||
|
- 최종 XML `$ROOT/build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.xml`: `tests=15`, `failures=0`, `errors=0`, `skipped=0`. 실제 RANDOM_PORT HTTP에서 새 422/500과 기존 404/409/422 및 직접 서비스 롤백 case가 모두 통과했다. 기존 회귀 클래스는 컴파일 입력일 뿐 실행하지 않았다.
|
||||||
|
- `$ROOT/build/libs/sodalive-0.0.1-SNAPSHOT.jar`에 수정된 `SubtitleExceptionHandler.class`가 포함됨을 `jar tf`로 확인했다. LSP 도구는 제공되지 않아 사용하지 못했으며 Kotlin 컴파일과 변경 파일 lint를 확인했다.
|
||||||
|
- RED/GREEN/최종 테스트 executor PID `28638`/`37727`/`39291`은 모두 종료됐다(`$ROOT/p1-test-process-status.log`는 header만 존재). 완료 후 작업 root를 사용하는 Gradle/HTTP/Redis 프로세스는 관찰되지 않았다. 기존 공유 PID `55393`/`55452`는 그대로이며 어떤 task도 취소하거나 프로세스를 수동 종료하지 않았다. root와 로그는 부모 재검증용으로 보존했다.
|
||||||
|
|
||||||
|
### 남은 검증 경계
|
||||||
|
|
||||||
|
- 기존 Kotlin 경고, Gradle deprecation, H2의 기존 charge/translation scheduler SQL 오류와 Redis 종료 시 재접속 경고는 관찰됐으나 수정 범위 밖이다. 신규 자막 15개 assertion은 전부 통과했다.
|
||||||
|
- 기존 회귀/전체 테스트, 운영 MySQL, AWS·MediaConvert·Lambda 실연동 및 배포 검증은 수행하지 않았다. 앞 절의 외부 게이트는 그대로 남으며, 전체 테스트 사용자 실행 명령은 `./gradlew test`다.
|
||||||
|
|
||||||
|
## 부모 최종 검증·정리 추가 기록
|
||||||
|
|
||||||
|
- 부모가 처음 신규13개 및 P1의 두 HTTP 사례를 각각 직접 재실행해 모두 통과했고, 독립 리뷰의 유일 finding은 ADDRESSED 및 로컬 backend APPROVE 판정을 받았다.
|
||||||
|
- 부모의 P1 재실행은 `BUILD SUCCESSFUL in 4m 55s`, 실제 XML2/0/0/0, timestamp `2026-11-10T15:34:43`이다.
|
||||||
|
- 2026-11-11 작업 소유 PID/listener 부재를 확인한 후 위 임시 ROOT 전체를 제거했다. 따라서 ROOT 기반 로그·init script·JAR 경로는 과거 검증 위치이며 현재 파일로 남아 있지 않다. 명령/결과/실패 이력은 본 보고서와 final-report에 보존했다.
|
||||||
|
- 일반 개발 환경의 재현 명령은 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest'`다. 기존 테스트는 실행하지 않는 selector다.
|
||||||
@@ -152,6 +152,8 @@
|
|||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
|
- 2026-11-11 별도 사용자 `/commit-with-validation` 요청으로 커밋 진행. 기능 연동 커밋의 pre-commit `ktlintCheck`가 기존 `AudioContentService.kt` EOF 빈 줄 1건으로 실패하여 해당 빈 줄만 제거했다. 재시도에서 staged Kotlin 대상 lint와 `git diff --staged --check`, 커밋 직전·직후 메시지 검증이 통과했다. 이번 커밋 작업에서는 기능/전체 테스트 및 bootJar를 재실행하지 않았으며 기존 실행 이력과 외부 배포 BLOCKED는 유지한다. 전체 테스트 사용자 명령은 `./gradlew test`다.
|
||||||
|
|
||||||
- 2026-11-11 `P2-GATE` 종료: 부모 직접 HTTP3개 재실행 성공과 독립 승인 후 소유 root `generate-url-subtitle.fSBkJM`의 디렉터리/symlink 여부를 확인해 제거했다. `test ! -e` exit0이며 공유 캐시/다른 dirty 파일은 건드리지 않았다. 원래 EOF lint1건은 별도 미해결 관찰로 남긴다. 검증 로그 경로는 과거 위치이며 결과/명령/실패 이력은 보고서에 보존했다.
|
- 2026-11-11 `P2-GATE` 종료: 부모 직접 HTTP3개 재실행 성공과 독립 승인 후 소유 root `generate-url-subtitle.fSBkJM`의 디렉터리/symlink 여부를 확인해 제거했다. `test ! -e` exit0이며 공유 캐시/다른 dirty 파일은 건드리지 않았다. 원래 EOF lint1건은 별도 미해결 관찰로 남긴다. 검증 로그 경로는 과거 위치이며 결과/명령/실패 이력은 보고서에 보존했다.
|
||||||
|
|
||||||
- 2026-11-11 `P2-T1` 독립 리뷰 APPROVE: 기존 contentUrl/envelope/활성 구매 정책과 서명 만료 인자 유지, 자막만 공개 시점·언어·최신 결과 조건 적용, 새 HTTP3개 증거를 확인했다. 이번 delta blocker/새 회귀 없음. production EOF 공백은 작업 시작 전 dirty 상태의 잔여사항이며 전체 lint clean을 주장하지 않는다.
|
- 2026-11-11 `P2-T1` 독립 리뷰 APPROVE: 기존 contentUrl/envelope/활성 구매 정책과 서명 만료 인자 유지, 자막만 공개 시점·언어·최신 결과 조건 적용, 새 HTTP3개 증거를 확인했다. 이번 delta blocker/새 회귀 없음. production EOF 공백은 작업 시작 전 dirty 상태의 잔여사항이며 전체 lint clean을 주장하지 않는다.
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# 자막 Phase 1 Backend Gate 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
- 일자: 2026-11-10
|
||||||
|
- 대상: `test` working tree의 신규 `v2.subtitle` 및 기존 업로드/상세/보안 연결
|
||||||
|
- 기준: [PRD](../prd.md), [계획](../plan-task.md), [구현 보고](../implementation-report.md)
|
||||||
|
- 리뷰어: 독립 Oracle (`ses_e3a1e039bffeXbBb7Nx0V7Hvk8`)
|
||||||
|
- 상태: 회귀 수정 검증 완료, 로컬 backend APPROVE
|
||||||
|
- 검토 제외: AWS/운영 DB/실음성/2시간 성능, 기존 전체 회귀 실행
|
||||||
|
|
||||||
|
## 2. 근거와 판정
|
||||||
|
|
||||||
|
부모 직접 실행의 실제 localhost HTTP 신규 테스트 13개가 0 failure/error/skip으로 통과했다.
|
||||||
|
reviewer는 코드·신규 테스트·실행 로그·DDL을 읽었고 테스트를 재실행하지 않았다.
|
||||||
|
저장/권한/언어별 결과/업로드/스키마는 부합 판정이나 HTTP 오류 경계 한 건으로 REQUEST_CHANGES다.
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 내용 | 수정 Goal |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| REV-P1-001 | High | 수정 완료 / ADDRESSED | 자막 빈 body/저장 실패가 공통 advice에서 HTTP200으로 변환 | P1-R1 |
|
||||||
|
|
||||||
|
## 3. REV-P1-001
|
||||||
|
|
||||||
|
- `SubtitleExceptionHandler.kt`는 ResponseStatusException만 처리한다.
|
||||||
|
- 기존 `common/SodaExceptionHandler.kt:73`과 `:94`는 DB 예외/일반 예외에 HTTP 상태 없이 ApiResponse.error를 반환한다.
|
||||||
|
- 필수 body가 비면 strict parser 이전에 HttpMessageNotReadableException이 발생한다.
|
||||||
|
- receipt 실패의 기존 새 테스트는 서비스 직접 호출 rollback만 검증하므로 HTTP200 문제를 포착하지 못했다.
|
||||||
|
- 영향: 결과가 저장되지 않았어도 Lambda가 HTTP 성공으로 해석할 수 있다.
|
||||||
|
|
||||||
|
최소 수정은 자막 controller에 한정된 advice에서 body 읽기 오류를 422, 예상하지 못한 서버 오류를
|
||||||
|
민감정보 없는 500으로 반환하는 것이다. 기존 404/409/422 및 공유 API 동작을 보존한다.
|
||||||
|
|
||||||
|
## 4. 수정 검증 조건
|
||||||
|
|
||||||
|
1. 빈 callback/started 요청이 200이 되는 RED를 실제 HTTP로 확인한다.
|
||||||
|
2. receipt 실패 callback이 200이 되는 RED와 전체 DB rollback을 확인한다.
|
||||||
|
3. 수정 후 422/500 및 무부분반영을 검증하고 이번 작성 테스트만 재실행한다.
|
||||||
|
4. 같은 reviewer가 수정 delta와 새 증거를 재검토한다.
|
||||||
|
|
||||||
|
## 5. 판정 기록
|
||||||
|
|
||||||
|
- 1차: REQUEST_CHANGES 1건. 부모가 공통 handler 코드를 직접 확인하고 `P1-R1`에 반영했다.
|
||||||
|
- 2026-11-11 수정 검증: 전용 advice만 수정하고 신규 HTTP 두 사례의 RED/GREEN을 확인했다. 이후 신규 자막15개·bootJar·변경 lint가 통과했고, 부모 직접 두 HTTP 사례 재실행도 2/0/0/0으로 통과했다. 기존 공통 handler와 직접 서비스 rollback 테스트는 보존했다. reviewer session continuation 유실로 기존 판정과 delta만 전달한 단일 재검토를 진행 중이다.
|
||||||
|
- 운영 배포는 코드 리뷰와 별개이며 [배포 게이트](../deployment-gates.md)의 차단 상태를 유지한다.
|
||||||
|
- 최종 제한 delta 리뷰 (`ses_e39e64c42ffeCOGUZjgeM4P1jf`): **REV-P1-001 ADDRESSED / 로컬 backend APPROVE**.
|
||||||
|
전용 advice의 422/500, 기존 404/409/422 유지, 실제 HTTP 및 rollback RED/GREEN 증거를 확인했다.
|
||||||
|
수정 delta에서 신규 버그/blocker는 발견하지 못했다. 추가 코드 수정 불필요 판정이며 전체 리뷰 반복이나 기존 테스트 실행은 하지 않았다.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# generate-url 자막 응답 제한 리뷰
|
||||||
|
|
||||||
|
- 일자: 2026-11-11
|
||||||
|
- 범위: `P2-T1`, `GenerateUrlResponse`와 `AudioContentService.generateUrl`, 새 HTTP 테스트3개.
|
||||||
|
- 기준: PRD §7, plan-task P2, 실제 메서드/DTO 전후 diff 및 `generate-url-report.md`의 RED/GREEN 증거.
|
||||||
|
- 리뷰어: `ses_e387b1e8fffensjb6dnoRk82ST`, read-only. 기존 Phase1 전체는 다시 리뷰하지 않았다.
|
||||||
|
- 판정: **APPROVE**, 이번 범위의 확정 finding/blocker/새 회귀 없음.
|
||||||
|
|
||||||
|
확인한 기준:
|
||||||
|
1. contentUrl과 ApiResponse envelope 유지, 공유 nullable subtitle만 additive 추가.
|
||||||
|
2. 활성 구매(KEEP/유효 RENTAL) 정책 유지, 소유자·무료만으로 우회하지 않음.
|
||||||
|
3. 음원 URL은 기존대로이며 자막에만 상세와 동일한 공개일/소유자 조건 적용.
|
||||||
|
4. 동일 expirationTime 전달 및 공유 latest/언어/상태별 자막 조회 재사용.
|
||||||
|
5. 새 HTTP3개 RED → GREEN 및 부모 재실행3/0/0/0, 실제 localhost HTTP/JWT/H2 증거 확인.
|
||||||
|
|
||||||
|
잔여사항: production lint의 기존 서비스 EOF 빈 줄1건은 보존했다. 기존 테스트/전체 테스트/LSP 및 실제 AWS 서명·다운로드/운영 배포는 이번 검증이 아니다. 수정 Task로 전환할 새로운 발견 사항은 없다.
|
||||||
Reference in New Issue
Block a user