193 lines
21 KiB
Markdown
193 lines
21 KiB
Markdown
# 자막 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다.
|