Files

24 KiB

자막 Phase 1 Spring 연동 구현 계획

Goal: 기존 업로드와 권한을 보존하면서 자막 작업 등록, 네 BOT 내부 API, 언어별 안전한 자막 조회를 제공한다.

Architecture: 기존 Kotlin/Spring/JPA와 member JWT를 재사용한다. 신규 자막 기능은 v2.subtitle 하위에 두고 기존 콘텐츠 서비스의 업로드 및 상세에만 연결한다. 원본 S3 쓰기 이전 commit과 callback receipt/status/metadata의 원자성을 보장한다.

Tech Stack: Kotlin 1.6.21, Java 17, Spring Boot 2.7.14, JPA, MySQL 운영 DDL, H2 테스트.

Spec: prd.md. 사용자 승인 범위이며 재생성 기능은 제외한다.

현재 상태

  • Phase 1/2 완료 이력 유지. Phase3 컬럼 매핑 수정·신규 테스트·독립 리뷰·소유 임시 자원 정리 완료. 활성 로컬 Goal 없음. 운영 DB/외부 게이트는 별도 검증 대상이다.
  • 실행: subagent-driven-development, 공유 Kotlin 구현은 단일 담당자가 순차 처리한다. 엔티티 계약이 확보된 additive DDL·운영 문서는 별도 담당자가 병렬 준비하고 마지막에 대조한다.
  • 새 worktree는 만들지 않는다. 기존 test 작업 트리는 조사 시 clean이며 main/master가 아니다.
  • commit/push/배포/DB 적용은 이 계획의 실행 항목이 아니다.

전역 제약

  • PRD의 네 endpoint/payload/상태/경로와 BOT 인증을 그대로 구현한다. 공개 재생성 API와 직접 START 호출은 금지한다.
  • 기존 upload/MediaConvert/preview/알림/JWT/텍스트 번역 worker를 보존한다. 기존 테스트를 삭제하거나 완화하지 않는다.
  • additive DDL만 작성한다. 적용 migration 편집/삭제와 데이터 DROP 금지.
  • 이번 작업에서 새로 작성한 테스트만 실행하며 bootJar로 빌드를 별도 확인한다. 기존 클래스에 추가한 새 테스트는 메서드 selector로 지정한다. 기존 focused 회귀도 실행하지 않는다. 전체 테스트 명령은 사용자용 ./gradlew test다.
  • AWS/live/mixed/2h/운영 DB/consumer 미검증을 성공으로 보고하지 않는다.

Review Focus

  1. 외부 호출자가 트랜잭션을 가지고 있어도 S3 이벤트 시점에는 job/source가 commit되어 있어야 한다.
  2. UUID 파일명 변경 후 기존 MediaConvert 완료 콜백을 수락하되 다른 콘텐츠/경로를 허용하지 않아야 한다.
  3. callback 중복/경합 때 receipt와 상태 및 자막이 원자 반영되고 older 결과가 최신을 덮지 않아야 한다.
  4. JSON 숫자 coercion/누락 nullable 필드/추가 필드 및 raw-body 재직렬화로 계약이 약화되지 않아야 한다.
  5. preview-only 사용자에게 전체 자막이 노출되거나 BOT 변경으로 기존 역할 권한이 넓어지지 않아야 한다.

Phase 1: 업로드부터 자막 조회까지

Task 1.1: 자막 backend 연결

Goal P1-T1: 기존 콘텐츠 흐름에 durable source/job 등록과 네 내부 API, 안전한 자막 조회가 연결된다.

  • 시작 조건: 재생성 제외 승인, PRD 작성 완료, 업로드 transaction 경계 검토 완료.
  • 완료 증거: 아래 RED/GREEN 기록과 5개 기능 그룹을 검증하는 새 테스트 통과. 기존 콘텐츠/로그인 테스트는 코드 대조만 하고 미실행으로 보고.
  • 범위 밖: Lambda 코드, 앱 코드, 인프라, 재생성, 자동 복구.
  • 신규 구현 파일은 src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/ 아래 application, domain, adapter, dto 책임별로 배치한다. 과도한 port/wrapper는 만들지 않는다.

Files:

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/domain/SubtitleJobStatus.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleJob.kt, SubtitleJobRepository.kt, SubtitleCallbackReceipt.kt, SubtitleCallbackReceiptRepository.kt 및 필요한 최소 subtitle metadata 저장 타입.
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/Subtitle.kt, SubtitleRepository.kt. 결과 행은 auto-increment Long PK와 jobId(36), language(35), format(16), s3Bucket(255), s3Key(255)를 저장하며 (jobId, language) unique와 job FK를 둔다. findAllByJobId(jobId) 및 findByJobIdAndLanguage(jobId, language)로 callback/조회에서 재사용한다. 언어별 컬럼을 제거하되 아직 미배포인 이번 draft만 교체하고 기존 적용 schema/data는 건드리지 않는다.
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/application/SubtitleJobService.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleJobController.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/dto/SubtitleJobDtos.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt, AudioContent.kt, GetAudioContentDetailResponse.kt 및 필요한 기존 호출자.
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt와 필요한 자막 전용 설정.
  • Create: docs/20261110_자막Phase1연동/subtitle-schema.sql.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/subtitle/의 기능 통합 테스트 및 기존 content/AudioContentServiceTest.kt, member/MemberLoginResponseContractTest.kt, jwt/TokenProviderTest.kt 관련 회귀.

Interfaces:

  • Consumes: 기존 AudioContent, S3Uploader, AudioContentCloudFront, LangContext, member JWT/role 및 PRD의 exact Lambda HTTP 계약.

  • Produces: PRD의 네 내부 HTTP endpoint; 기존 콘텐츠 상세의 nullable 자막 정보. source lookup은 최초 업로드 binding에 고정한다.

  • 내부 함수/저장 형태는 기존 패턴과 transaction 검토에 맞추며 실제 파일/이름은 구현 기록에 갱신한다. 공개 계약은 변경하지 않는다.

  • RED: 기존 관련 테스트의 의도를 먼저 읽고 새 실제 HTTP 테스트를 작성했다. 지시 이후 기존 baseline은 실행하지 않았다.

  • RED 확인: 실제 로그인 후 403 기대/404 실제, source 확장자 보존 실패 및 기존 eventId/unknown job의 422 기대/404 실제를 확인했다. compile/setup 실패는 RED로 인정하지 않았다.

  • GREEN: 최소 persistence/업로드 commit 경계/API/상세 연결 및 additive DDL을 구현했다. 언어별 결과는 Subtitle 행이다.

  • GREEN 확인: 부모 직접 실행에서 신규 SubtitleHttpIntegrationTest 13개 통과, 실패·오류·skip 0.

  • REFACTOR: Kotlin/JPA·직접 영향 test compile, 변경 파일 ktlint 및 bootJar 성공을 확인했다. 기존 회귀 case는 실행하지 않았다.

검증 상세: implementation-report.md. 부모 재실행 parent-verification.log는 BUILD SUCCESSFUL in 2m 21s, XML timestamp 2026-11-10T14:33:59, 13/0/0/0이다.

Task 1.2: 실제 HTTP와 독립 리뷰

Goal P1-T2: 구현 결과를 실제 HTTP 표면과 독립 검토로 확인하고 외부 미검증을 구분한다.

  • 시작 조건: P1-T1 구현 및 focused test 완료.
  • 완료 증거: 실제 HTTP status/body assertion, bootJar, 변경 범위 lint/compile, 독립 리뷰의 blocker 해소.
  • 범위 밖: 외부 AWS 환경에 대한 실제 운영 검증 및 전체 테스트.
  • TDD 예외 사유: 검증/보고 task다. 대체 검증은 HTTP 실행과 코드/DDL/테스트 대조다.
  • 실제 localhost HTTP로 EMAIL BOT 로그인, 네 endpoint, 중복/경합/오류 및 기존 콘텐츠 상세를 실행했다. H2와 외부 S3/서명 경계 대체를 사용했다.
  • bootJar, 변경 파일 ktlintCheck, 신규 HTTP 테스트를 실행했다. LSP 미제공이며 Kotlin compile/lint를 대체 증거로 기록했다.
  • 독립 리뷰에서 HTTP 오류 경계 1건을 발견·수정하고, 제한 delta 재검토에서 ADDRESSED 및 로컬 backend APPROVE를 받았다.
  • final-report.md에 실제 수정·삭제·보존 경로, 명령/결과, DB 적용 및 consumer 상태, fixture 출처, live/mixed/2h BLOCKED를 기록했다.

Task 1.R1: 리뷰 회귀 수정

Goal P1-R1: 자막 API의 빈 body/저장 실패가 HTTP 200으로 숨겨지지 않는다.

  • 시작 조건: 독립 리뷰 P1 finding 및 SodaExceptionHandler.kt의 기존 HTTP200 변환 확인.
  • 완료 증거: 새 실제 HTTP 테스트에서 빈 callback/started 422 및 DB 실패 5xx, receipt/status/results rollback 확인.
  • 범위 밖: 기존 공통 exception handler나 다른 API 응답 변경.
  • Files: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleExceptionHandler.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/subtitle/SubtitleHttpIntegrationTest.kt.
  • RED: 빈 body와 receipt 저장 실패의 실제 HTTP 테스트 2개를 추가했다.
  • RED 확인: [422,422] 기대에 [200,200], 500 기대에 200 실패를 확인했다.
  • GREEN: 자막 전용 advice에서 body 읽기 오류는 422, 예상치 못한 서버 오류는 일반화된 500으로 반환한다.
  • GREEN 확인: 신규 자막 15개 통과, 부모의 추가 두 HTTP 사례 재실행도 통과했다. 기존 서비스 직접 호출 rollback 테스트를 유지했다.
  • REFACTOR: 변경 lint/build 및 부모 HTTP 재검증 후 제한 delta 리뷰가 승인했다. 원 reviewer session 유실로 대체 reviewer가 기존 판정과 수정분만 재검토했다.

Phase 1 Gate

Goal P1-GATE: 로컬 backend 완료와 운영 배포 준비 상태가 증거로 구분된다.

  • P1-T1, P1-T2, P1-R1 완료 증거 확인.
  • 임시 QA 프로세스/listener 부재와 작업 소유 build/cache/log root 제거를 확인했다. 공유 프로세스·캐시는 변경하지 않았다.
  • 재생성 URI/worker/HMAC/파괴적 SQL/외부 프로젝트 변경이 없음을 diff 확인했다.
  • 운영 반영은 consumer inventory, DB additive DDL 검토, 조정 배포, controlled live ko/ja/mixed와 2h 측정이 필요하다고 보고했다.

Phase 2: generate-url 자막 응답

Task 2.1: 기존 URL 권한을 유지한 자막 제공

Goal P2-T1: 구매자의 generate-url 응답에 상세와 동일한 요청 언어 자막이 포함되고 기존 contentUrl 계약이 보존된다.

  • 시작 조건: PRD §7 사용자 요청 확정, 기존 generateUrl/공유 subtitle 조회 및 새 HTTP fixture 확인.
  • 범위 밖: 기존 구매 판정 확대, URI/DB/Lambda 변경, 재생성, 기존 테스트/전체 테스트 실행.
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/GenerateUrlResponse.kt.
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt의 generateUrl만.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/subtitle/SubtitleHttpIntegrationTest.kt에 이번 후속용 HTTP 사례 추가.
  • Consumes: SubtitleJobService.subtitle(content, lang, expirationTime), 기존 LangContext, 구매 검사와 AudioContentCloudFront.
  • Produces: data.contentUrl 유지 + nullable data.subtitle의 language/format/url.
  • 완료 증거: 아래 세 시나리오의 새 HTTP 사례 RED/GREEN, 컴파일/변경 lint, 범위 한정 독립 리뷰와 임시 자원 정리.

시나리오:

  1. 구매·공개 완료 콘텐츠: Accept-Language: ko/ja 각각에 해당 SRT 정보와 기존 음원 URL, 기존 envelope가 반환된다.
  2. 미완료 job/해당 언어 없음/미지원 언어/자막 미등록: 음원 URL은 유지하고 subtitle은 명시적 null이다.
  3. 미구매자(소유자·무료 포함)는 기존 빈 contentUrl과 null 자막을 받는다. 예약 공개 구매자의 subtitle은 공개 전 null이며 공개 후 제공된다. 기존 URL 반환은 그대로다.
  • RED: 기존 HTTP fixture를 재사용한 새 메서드 3개만 추가했다.
  • RED 확인: 실제 응답의 has("subtitle")에서 3개 모두 assertion 실패함을 확인했다.
  • GREEN: DTO field와 generateUrl의 공유 자막 조회 연결만 구현했다.
  • GREEN 확인: 이번 새 HTTP 메서드 3개 통과, 부모 직접 재실행도 3/0/0/0. 이전 15개와 클래스 전체는 실행하지 않았다.
  • REFACTOR: 컴파일/bootJar/테스트 lint 및 제한 독립 리뷰 APPROVE를 확인했다. production lint의 기존 EOF 빈 줄1건을 별도 기록하고 dirty 변경을 보존했다. 자원 정리는 Phase2 Gate에서 확인한다.

Phase 2 Gate

Goal P2-GATE: 추가 응답의 HTTP·권한·호환성 증거가 확보된다.

  • 새 HTTP3개 통과와 독립 리뷰 APPROVE 확인.
  • 기존 코드 변경은 DTO와 generateUrl 본문뿐임을 실제 전후 diff로 대조했다.
  • 결과 보고를 보존하고 소유 테스트 PID/listener 및 임시 build/cache/root 부재를 확인했다.

Phase 3: Schema-validation 회귀 수정

Task 3.R1: s3 컬럼 물리명 일치

Goal P3-R1: 기존 subtitle.s3_bucket/s3_key 테이블을 Hibernate가 검증하고 정상 저장·조회한다.

  • 시작 조건: 실제 사용자 에러, 명시적 이름이 없는 엔티티와 DDL 차이 확인.
  • Files: src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/Subtitle.kt.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/subtitle/SubtitleSchemaValidationTest.kt 신규.
  • 범위 밖: DDL 수정·운영 DB 접속/컬럼 추가·자동 update·전역 naming strategy·JSON/property rename·기존 테스트 실행.
  • 시나리오: 독립적으로 만든 s3_bucket/s3_key 테이블에 실제 Boot 기본 naming strategy를 사용한 Hibernate validate 성공, 두 값의 persist/reload/JDBC column 확인 성공.
  • RED: 엔티티 자동 DDL 생성 없이 기존 컬럼명의 H2 table을 준비하고 실제 schema validate를 수행하는 새 테스트를 작성했다.
  • RED 확인: 사용자 보고와 동일한 missing s3bucket 에러로 1개 실패를 확인했다. 컴파일 오류가 아니다.
  • GREEN: @Column(name = "s3_bucket"), @Column(name = "s3_key")를 기존 속성에 명시했다.
  • GREEN 확인: 새 테스트1개만 실행해 validation 및 Hibernate 저장/새 Session 조회/JDBC 두 컬럼 조회 성공을 확인했다.
  • REFACTOR: 변경 compile/bootJar/lint와 독립 리뷰 APPROVE, 검증 기록 보존 및 소유 임시 자원 제거·부재 확인을 완료했다.

이전 기능 실행 검증 이력

./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subtitle.*'
./gradlew bootJar

기존 클래스에 추가한 새 테스트는 실제 메서드명을 --tests에 지정한다. 기존 테스트 클래스 전체는 실행하지 않는다. 수동 전체 검증은 사용자 실행 ./gradlew test다. 실제 명령/추가 영향 테스트/결과는 아래에 누적한다.

Progress

  • 2026-11-11 P3-R1 종료: 작업 root를 참조하는 프로세스가 없고 실제 디렉터리/symlink 아님을 확인한 뒤 소유 subtitle-schema-fix.XcftjN만 제거했다. test ! -e exit0, 공유캐시 미변경. 결과/명령/리뷰는 문서로 보존했다. 운영 재시작/DB 변경/커밋은 하지 않았다.

  • 2026-11-11 P3-R1 독립 리뷰 APPROVE: 명세/품질 blocker 없음. 두@Column만 바뀌었고 기존 SQL/JSON 계약은 유지된다. 독립DDL+actualvalidate+새Session/JDBC 조회와 RED/GREEN 증거가 확인됐다. 실제MySQL/전체Spring검증은 별도 게이트다.

  • 2026-11-11 P3-R1 RED/GREEN: 실제 Hibernate validation의 missing[s3bucket] 실패를 수정 전 재현했다(1fail, 3m49s). 두@Column만 수정한 뒤 신규 테스트1/0/0/0, Kotlin compile/bootJar/변경2파일 lint 성공(3m29s). 실제 운영 DB는 조회·변경하지 않았다. schema-validation-report.md에 원인과 기존 create-drop 검증 공백을 기록했다. 독립 한정 리뷰 진행 중.

  • 2026-11-11 P3-R1 등록: 사용자 보고 missing [s3bucket] 재현/수정. 기존 기본 명명전략이 숫자-대문자 경계에서 underscore를 넣는다고 가정한 정적 대조와 엔티티 생성 H2 테스트의 검증 공백을 기록한다. 기존 DDL은 유지하고 실제 validate 회귀 검증으로 보완한다.

  • 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-T1 독립 리뷰 APPROVE: 기존 contentUrl/envelope/활성 구매 정책과 서명 만료 인자 유지, 자막만 공개 시점·언어·최신 결과 조건 적용, 새 HTTP3개 증거를 확인했다. 이번 delta blocker/새 회귀 없음. production EOF 공백은 작업 시작 전 dirty 상태의 잔여사항이며 전체 lint clean을 주장하지 않는다.

  • 2026-11-11 P2-T1 구현: DTO nullable field 및 generateUrl 본문만 수정. 새 HTTP3개 RED/GREEN 후 부모가 정확히 새3method selector로 직접 실행해 3 tests/0 failures/errors/skipped, BUILD SUCCESSFUL in 37s, XML UTC 2026-11-10T22:12:51 확인. 실제 Tomcat PID17759/port50914. Kotlin compile/bootJar 및 테스트 lint 성공. 두 production 파일 lint에서는 작업 전부터 있던 서비스 EOF 빈 줄1건만 실패하므로 다른 dirty 변경으로 보존했다. 상세는 generate-url-report.md. 범위 한정 독립 리뷰 진행 중.

  • 2026-11-11 후속 요청: generate-url에도 상세와 같은 자막 응답 추가. GenerateUrlResponse는 현재 contentUrl만 있고 기존 URL 권한은 구매 검사다. P2-T1에서 additive field/공유 조회만 연결한다. 기존 Phase1 완료·검증 기록은 유지한다. 시작 시 추가 dirty 파일(기존 관리자 controller/query 테스트 등)을 발견했으며 다른 작업으로 보고 변경하지 않는다.

  • 2026-11-11 종료 정리: 소유 root subtitle-phase1.wiRcAJ가 실제 디렉터리이며 symlink가 아님을 확인한 후 제거했고 test ! -e exit0으로 부재 확인했다. 작업 소유 테스트 PID/HTTP port의 종료는 먼저 확인했다. 결과·명령·실패 이력·리뷰 판정은 저장소 문서에 보존했으며 임시 log/init/JAR 경로는 과거 검증 위치다. 모든 로컬 Goal 완료, 외부 배포 게이트는 별도 BLOCKED로 유지한다.

  • 2026-11-11 최종 제한 리뷰: REV-P1-001 ADDRESSED, 로컬 backend gate APPROVE. 신규 blocker 없음. 원 reviewer session 유실 때문에 기존 전체 리뷰 판정과 오류 advice/두 테스트 delta만 대체 reviewer가 확인했으며 전체 리뷰를 반복하지 않았다. 재생성·AWS·운영 DB·live·2h는 구현/승인 범위에서 분리했다.

  • 2026-11-11 P1-R1 검증: 2개 HTTP RED 후 수정, 신규 자막 15개 전체와 bootJar/변경 lint 통과 (p1-green-15-build-lint.log, 7m44s). 부모는 수정한 두 새 HTTP 사례만 재실행해 2/0/0/0 및 BUILD SUCCESSFUL in 4m 55s 확인. 실제 Tomcat port 59977/PID44707, XML timestamp 2026-11-10T15:34:43. 기존 reviewer session continuation이 유실돼 이전 판정과 수정 delta만 전달한 단일 재리뷰를 진행 중이다. 같은 기능을 재설계하거나 전체 리뷰를 반복하지 않는다.

  • 2026-11-10 독립 gate 리뷰: REQUEST_CHANGES 1건. 자막 advice가 ResponseStatusException만 처리하므로 빈 body/저장 실패가 기존 공통 advice에서 HTTP200으로 변환될 수 있다. 부모가 실제 공통 handler를 확인했고 전용 advice+새 HTTP 사례의 P1-R1로 수정한다. 나머지 저장/권한/언어별 구조/업로드/DDL 범위는 reviewer가 부합 판정했다.

  • 2026-11-10 부모 검증: 격리 build/cache에서 신규 HTTP 13개를 up-to-date 무시하고 직접 재실행, 13 passed/0 failed/0 errors/0 skipped, BUILD SUCCESSFUL in 2m 21s. 같은 코드의 bootJar와 ktlint는 up-to-date 확인이며 구현 담당의 실제 실행 로그 build-lint.log는 BUILD SUCCESSFUL in 1m 28s다. 로컬 mapping의 sourceUploadErrorAt TIMESTAMP 차이는 해소했다. 기존 독립 reviewer 결과 회수 실패 및 session 유실로 승인을 주장하지 않고 대체 단일 reviewer를 실행 중이다.

  • 2026-11-10 실제 ZIP 계약 대조: unzip -p .../start-subtitle-transcription.zip common/transcribe.py로 확장자 없는 원본 key가 UNSUPPORTED_MEDIA임을 확인했다. common/runtime.py::source_content는 UUID 뒤 확장자를 허용한다. 원본의 안전한 확장자를 보존하도록 등록 seam/새 테스트를 수정한다. 이는 기존 artifact의 read-only 계약 확인이며 Lambda 변경·실행·AWS 호출이 아니다. 확장자는 형식 보장이 아니며 기존 업로드/MediaConvert 동작을 미지원 자막 형식 때문에 막지 않는다.

  • 2026-11-10 후속 설계 조정: 사용자에게 향후 언어 추가 가능성이 확인되어 고정 koKey/jaKey 대신 Subtitle 결과 행을 둔다. 현재 API 허용 언어/상태 판정은 ko/ja 고정 그대로다. 구현 담당과 DDL 담당을 잠시 중단하고 같은 session으로 갱신 계약을 전달한다. callback 원자성/terminal replay/조회 권한 새 테스트를 같은 구조로 갱신한다. 아래 Oracle의 고정 컬럼 권고는 당시 두 언어 고정 가정의 기록이며 이 결정으로 대체한다.

  • 2026-11-10 후속 지시: 테스트는 이번에 작성한 것만 실행. 지시 반영을 위해 실행 중 implementer를 일시 중단하고 같은 session으로 재개한다. Kotlin 공유 파일은 기존 담당자가 소유하고 DDL/운영 문서만 병렬 분리한다. 이전 baseline 실행 이력은 아래에 보존하되 재실행하지 않는다.

  • 2026-11-10: 현재 상태 조사 완료. Spring 자막/recovery 구현 없음, 기존 BOT 인증/업로드 보존. Phase 1 Lambda ZIP hash 대조 완료, AWS 상태는 미확인. 사용자 결정으로 재생성 전체 제외.

  • 2026-11-10: PRD/계획 준비. 아직 production 코드 수정 및 테스트 실행 전이며 성공 결과를 주장하지 않는다.

  • 2026-11-10: Oracle 경계 검토 완료. create orchestration은 NOT_SUPPORTED, 실제 등록은 필수 주입 TransactionTemplate(REQUIRES_NEW)로 분리한다. AI 관리자 create 바깥 transaction도 조정한다. content/source/job/최신 선택값을 한 번 저장한 뒤 S3 원본 업로드하고 후속 input 경로 재대입은 제거한다. 업로드 예외는 객체 생성 여부가 불명확하므로 처리 상태를 terminal로 덮지 않고 별도 업로드 오류만 기록해 수동 확인 대상으로 남긴다.

  • 2026-11-10: 고정 두 언어 metadata는 job의 nullable 컬럼으로 최소 저장하고 별도 번역/checkpoint 구조는 만들지 않는다. receipt eventId는 case-sensitive unique, callback/started는 동일 job lock을 사용한다. 다른 job 사이 eventId 경합은 rollback 후 receipt 재조회로 판정한다.

  • 2026-11-10: UUID 파일명 변경은 외부 MediaConvert의 ID 추출/output 명명 규칙 확인 전 배포 BLOCKED다. 현재 저장소에 외부 consumer의 운영 계약 증거가 없으며, 임의 prefix 완화로 숨기지 않는다. 자막 S3 bucket의 private CloudFront origin/서명 접근도 운영 확인 게이트다.

  • 2026-11-10: baseline 명령 ./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest' --tests 'kr.co.vividnext.sodalive.member.MemberLoginResponseContractTest' --tests 'kr.co.vividnext.sodalive.jwt.TokenProviderTest'는 kaptGenerateStubsKotlin 단계에서 200초 도구 제한으로 종료. 테스트 결과 없음. 기존 다른 Gradle 작업이 관찰되어 타 작업을 종료하지 않고 이 작업 전용 build/cache 경로로 검증을 격리한다.

Schema-validation 커밋 전 재검증

  • 2026-11-11 별도 /commit-with-validation 요청으로 ./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleSchemaValidationTest'를 실행했다.
  • 결과: BUILD SUCCESSFUL in 46s, 테스트 1개, 실패/오류/건너뜀 0개. 기존 컬럼명의 Hibernate validation과 저장·재조회를 재확인했다.
  • 전체 테스트와 실제 MySQL 기동은 실행하지 않았다. 사용자 전체 테스트 명령은 ./gradlew test다.