Files
sodalive-backend-spring-boot/docs/20261110_자막Phase1연동/prd.md
T

13 KiB

자막 Phase 1 Spring 연동 PRD

문서 정보

  • 작성일: 2026-11-10
  • 상태: 재생성 제외 로컬 backend 구현·검증 완료. 운영 배포/외부 게이트는 별도 차단 상태.
  • 구현 계획: plan-task.md
  • 기준: 사용자 제공 Phase 1 계약 및 후속 결정 "이번 스코프에 재생성 기능은 추가하지 않고 나머지만 진행"

1. 목표와 범위

기존 콘텐츠 업로드에 자막 원본 binding과 PENDING 작업을 연결하고, 기존 BOT JWT로 보호한 네 내부 API를 통해 Lambda 결과를 저장한다. 기존 콘텐츠 상세에서 요청 언어와 원본 열람 권한에 맞는 자막 정보만 반환한다.

Spring은 업로드/작업 등록/결과 저장/콘텐츠 접근 제어를 맡는다. Lambda의 S3 이벤트, Transcribe, 번역, SRT 생성은 이미 별도 구현되어 있으므로 다시 구현하지 않는다. 앱 플레이어 구현은 이 Spring 저장소의 변경 대상이 아니며 실제 앱 표시 검증은 외부 게이트다.

제외

  • 사용자 결정에 따라 자막 재생성 전체: 새 공개 API, 새 job 생성 business action, START 직접 비동기 호출 모두 제외.
  • Scheduler discovery, recovery/lease/fence/checkpoint/outbox/자동 복구, HMAC 인증, 신규 로그인/refresh 계약.
  • 외부 프로젝트 수정, AWS 설정/배포/실데이터 변경, DB DROP, 적용 migration 편집/삭제.
  • MediaConvert/AAC/preview/기존 알림의 기능 변경과 2GB 다운로드/probe pipeline.

2. 실제 현재 상태

영역 확인 결과 처리
Spring 작업 트리 test, 조사 기준 fe620aa9, 변경 없음 현 작업 트리에서 최소 변경
로그인/JWT/BOT MemberService.login, MemberRole.BOT, JwtFilter, TokenProvider 존재 재사용, 공유 인증 약화 금지
콘텐츠 AudioContent의 numeric PK, 기존 multipart 업로드와 상세/서명 URL 존재 보존 및 필요한 연결만 추가
업로드 AudioContentService.createAudioContent 트랜잭션 안에서 S3 업로드 S3 이벤트 전에 committed PENDING/source가 보이도록 조정
업로드 완료 uploadComplete가 파일명의 contentId prefix 검사 후 경로/duration/알림 처리 기존 파일은 유지, 새 UUID 원본과의 정확한 연결 검증 필요
자막/recovery 자막 API/job/worker/HMAC 및 자막 migration 검색 결과 없음 실제 삭제 대상 없음, 공유 텍스트 TranslationJob/JWT/Agora 보존
DB 관리 Flyway/Liquibase 의존성 없음, 기존 문서 SQL 관례 additive MySQL DDL 문서만 작성, 운영 적용 여부 미확인
재생성 기존 action 없음. generate-url은 재생 URL 발급 이번 범위 제외

Lambda handoff 확인

참조는 /Users/klaus/Develop/sodalive/Lambda/generate-subtitle/aws-subtitle-service/docs/FINAL_REPORT.md다. 실제 로컬 ZIP hash가 보고서 및 manifest와 일치함을 조사했다.

  • start-subtitle-transcription.zip: a0d0b19b2ecb24c0f89caf528bf09fd039558563a99063f082ac64418704abfd
  • generate-subtitle.zip: bfa8742ca436bace853ec2540fc5f70daa24cbab49e75315565d980189fbf209

이는 AWS 배포 상태의 증거가 아니다. 현재 운영 consumer/in-flight/Scheduler/DB 상태는 미확인이다. 이 저장소에 삭제할 legacy 자막 consumer는 발견되지 않았으며 외부 consumer 삭제/중지/교체는 하지 않는다.

3. 고정 계약

식별자와 저장 경로

  • contentId: 기존 Long PK, JSON number. jobId: Spring 생성 UUID string.
  • 원본: input/{contentId}/{UUID filename}, 같은 key 덮어쓰기 금지. VersionId 생성/사용 없음.
  • 원본 파일의 안전한 확장자를 UUID 뒤에 보존한다(예: {UUID}.mp3). 실제 Phase 1 START ZIP의 common/transcribe.py::validate_binding은 amr/flac/m4a/mp3/mp4/ogg/webm/wav 확장자를 요구하며, 확장자 없는 key는 UNSUPPORTED_MEDIA다. 확장자는 형식 힌트일 뿐 실제 미디어 검증을 대신하지 않는다. 기존 업로드 허용 동작을 임의로 축소하지 않고 미지원 형식은 자막 실패와 기존 업로드를 구분한다.
  • transcript: transcripts/{contentId}/{jobId}/transcript.json.
  • 자막: subtitles/{contentId}/{jobId}/ko.srt, subtitles/{contentId}/{jobId}/ja.srt.
  • 상태: PENDING, PROCESSING, COMPLETED, PARTIAL_COMPLETED, FAILED.
  • 등록과 원본 binding은 S3 원본 업로드 전에 commit한다. 업로드 실패를 성공으로 숨기지 않으며 이미 commit된 job의 실패를 기록한다. 자동 복구는 추가하지 않는다.

내부 API: 정확히 네 개

모든 요청은 기존 /member/login에서 발급된 JWT의 BOT 권한을 검증한다. 일반 USER/CREATOR/ADMIN만으로 허용하지 않는다. unknown job/source는 404이며 조회 중 job을 만들지 않는다.

Method Path 계약
GET /internal/api/subtitle-jobs/resolve?bucket=...&key=... 업로드의 정확한 source binding 조회
GET /internal/api/subtitle-jobs/{jobId} UUID job 조회
POST /internal/api/subtitle-jobs/{jobId}/started 정확히 transcribeJobName, status
POST /internal/api/subtitle-jobs/callback 정확히 아래 8개 필드

조회 응답은 envelope 없이 jobId, numeric contentId, status, source:{bucket,key,versionId:null}다. recovery/provider/lease 정보는 노출하지 않는다.

started의 이름은 subtitle-{jobId}, status는 PROCESSING만 허용한다. PENDING만 PROCESSING으로 전이하고 같은 PROCESSING 및 terminal은 no-op이다. 성공은 200 {} 또는 204 empty다.

callback은 eventId, jobId, numeric contentId, transcribeJobName, status, subtitles, errorCode, errorMessage만 받는다. eventId는 비어 있지 않은 문자열로 UUID 제한을 하지 않는다. subtitle 항목은 language, format, s3Bucket, s3Key만 받는다. 언어는 ko-KR/ja-JP, format은 SRT, 언어 중복은 금지한다. 이름/job/content/configured bucket allowlist/정확한 자막 key를 검증한다.

  • 2개 자막이면 COMPLETED, 1개면 PARTIAL_COMPLETED, 0개면 FAILED만 허용한다.
  • COMPLETED의 errorCode/errorMessage는 명시적 null, partial/failed는 구체적 비어 있지 않은 오류를 요구한다. nullable 필드도 요청에서 생략하지 않는다.
  • started 전 callback도 허용한다. callback 검증은 SRT 내용 검사로 위장하지 않는다. 실제 빈 파일/무음 방지는 기존 Lambda 및 live 검증 책임이다.
  • receipt의 eventId와 원래 HTTP body bytes의 SHA-256을 상태/자막 metadata와 같은 DB 트랜잭션으로 저장한다. hash는 인증이 아니다.
  • 같은 eventId/hash는 200, 같은 eventId/다른 hash는 422, terminal job의 다른 결과는 409다. 회귀 및 부분 반영 금지.
  • 오래된 job의 결과는 자체 저장하되 콘텐츠 최신 선택 결과를 대체하지 않는다. 재생성 API가 제외되어도 이 저장 불변식은 유지한다.

로그인/권한/개인정보

기존 login 요청의 isAdmin:false, isCreator:false와 기존 data.token 응답을 바꾸지 않는다. 활성 EMAIL BOT의 DB role과 발급 JWT를 실제 인증 경로 테스트로 확인한다. Lambda의 success envelope/401 1회 갱신/403 갱신 금지 구현은 재작성하지 않는다.

credentials/JWT/body를 코드/로그/보고서에 출력하지 않는다. BOT 운영 계정 준비는 운영자 책임이다. 기존 JWT 등록/회원 권한/다른 role 경로는 그대로 보존한다.

언어별 저장 구조

추후 언어 추가 가능성을 반영해 SubtitleJob에 koKey/jaKey 같은 언어별 컬럼을 두지 않는다. Subtitle 결과 행에 jobId, language, format, s3Bucket, s3Key를 저장하고 (jobId, language) unique로 중복을 방지한다. 작업 상태/원본/오류는 SubtitleJob, callback 중복 방지는 receipt가 담당한다. callback transaction에서 결과 행도 함께 저장한다.

이번 Phase 1의 외부 계약은 그대로 유지한다. callback은 ko-KR/ja-JP만 허용하며 2개/1개/0개 결과의 상태 판정도 변경하지 않는다. 다른 언어 활성화, 언어 관리 API, 동적 provider 설정은 구현하지 않는다. 새 언어는 향후 계약·Lambda·조회 정책 변경이 필요하지만 언어별 DB 컬럼 추가는 필요 없도록 한다.

콘텐츠 상세 응답

기존 상세 API 응답에 nullable 자막 정보를 additive하게 제공한다. 기존 LangContext의 한국어/일본어에 맞는 완료 자막만 선택한다. 미지원 언어/미완료/없는 언어는 null이며 원문 복사 fallback을 만들지 않는다.

원본 재생이 가능한 구매/소유자/무료 조건과 공개 시점 등 기존 접근 제한을 통과한 경우에만 서명된 SRT URL을 제공한다. 유료 preview만 볼 수 있는 사용자는 전체 자막을 받지 않는다. 새로운 공개 download URI를 만들지 않는다.

4. 검증 시나리오

그룹 통과 조건 증거
1. 등록/조회 S3 upload 경계에서 별도 transaction으로 PENDING/source 조회 가능, UUID/숫자 JSON 및 404 정확 Spring DB/HTTP 테스트, upload 경계 assertion
2. started 단조 전이, duplicate/terminal no-op, 잘못된 이름/status 거부 DB/HTTP 테스트
3. callback complete/partial/fail, exact body hash replay, 422/409, callback-before-started, atomic rollback, older/latest guard DB/HTTP 및 동시성 테스트
4. 로그인/보안 실제 active EMAIL BOT 로그인/JWT로 네 API 허용, 다른 role/익명 거부, 기존 envelope 보존 기존 로그인/JWT 회귀 및 통합 테스트
5. 콘텐츠 ko/ja 선택 및 구매/소유자/무료/preview 권한 유지, 기존 업로드 완료/알림 회귀 없음 기존 콘텐츠 테스트와 추가 회귀

실제 서버 HTTP 요청으로 핵심 endpoint를 검증하고, 이번 작업에서 새로 작성한 테스트와 bootJar를 분리 실행한다. 기존 테스트는 읽어 계약을 보존하되 실행하지 않는다. 기존 클래스에 새 테스트를 추가한 경우 해당 메서드만 지정한다. 전체 테스트는 사용자 실행 ./gradlew test로 남긴다. LSP 도구가 제공되지 않으면 Kotlin compile/lint 대체 결과와 미실행 사실을 명시한다.

5. 외부 게이트와 미검증

  • AWS 접근/배포는 승인 없이 하지 않는다. 기존 consumer 운영 여부가 미확인이므로 삭제/전환/재개는 BLOCKED다.
  • live ko/ja/mixed S3 -> callback -> 콘텐츠/앱 검증은 별도 승인 및 환경 필요. 실제 word/item 언어 evidence 없는 mixed 결과는 통과 불가. synthetic fixture는 대체 증거가 아니다.
  • 2시간은 입력 목표이며 SLA/지원 보장이 아니다. 기존 신뢰 가능한 duration의 가용성과 검증 경계를 확인한다. 구형 또는 검증 불가능한 원본은 수동 게이트로 남기고 임의 client 값을 신뢰하지 않는다.
  • 대표 2시간 처리 예산은 미측정이며 지원 검증 BLOCKED. 분할/상한 하향/Translate 50-call 제한을 추가하지 않는다.
  • 운영 DB 적용 상태 미확인. DDL 적용/기존 데이터/미확인 환경에 대한 삭제는 하지 않는다.

6. Decision Log

날짜 결정 근거
2026-11-10 자막 재생성 기능 전체 제외, 나머지 진행 사용자 명시 결정
2026-11-10 현재 Spring 중심, 외부 참조는 명시된 Lambda handoff에 한정 사용자 범위 확인
2026-11-10 이미 있는 로그인/JWT/Lambda 재구현 금지, 자막 recovery 삭제 없음 실제 코드/ZIP 조사
2026-11-10 실행 테스트는 이번 작업에서 새로 작성한 사례만. 기존 focused 회귀 및 전체 테스트 실행 금지 사용자 후속 지시
2026-11-10 향후 언어 추가를 고려해 언어별 결과 행으로 저장, 이번 API는 ko/ja만 유지 사용자 확장 가능성 제시 및 중단 없이 진행 지시에 따른 최소 저장 구조 조정

7. 후속 범위: generate-url 자막 응답

2026-11-11 사용자 요청에 따라 기존 GET /audio-content/{id}/generate-url 응답에도 콘텐츠 상세와 동일한 nullable subtitle을 추가한다. 기존 ApiResponse envelope와 data.contentUrl 이름·타입·URL 생성 동작은 유지한다.

  • GenerateUrlResponse에 subtitle: ContentSubtitleResponse? = null을 추가한다. 필드는 language, format, url이다.
  • 기존 orderRepository.isExistOrdered 검사를 유지한다. 구매가 없으면 기존처럼 contentUrl은 빈 문자열이며 subtitle은 null이다.
  • 상세와 달리 현재 generate-url은 소유자/무료 콘텐츠만으로 구매 검사를 우회하지 않는다. 이 요청으로 기존 URL 권한을 확대하지 않는다.
  • 구매가 확인된 사용자의 자막은 상세와 같은 공개 시점 조건(미지정/도래 또는 소유자)을 적용한다. 기존 contentUrl 동작은 바꾸지 않는다.
  • SubtitleJobService.subtitle을 재사용해 요청 LangContext의 ko/ja, 최신 job, 완료/부분 완료 및 해당 언어 존재 여부를 처리한다.
  • 자막 미완료·누락·미지원 언어면 null이며 원문 fallback이나 새 자막 생성은 하지 않는다. 음원과 자막 서명 URL에는 같은 만료 기간을 전달한다.
  • 컨트롤러 URI/인증·DB 스키마·Lambda·재생성은 변경하지 않는다. 검증은 이번 후속 작업에서 새로 작성한 HTTP 테스트만 실행한다.