Files
T

91 KiB

크리에이터 커뮤니티 게시물 본문 번역 구현 계획

문서 항목 내용
상태 기존 로컬 Gate 기록 유지 · P1-R4 구현·자동 회귀·수정 후 리뷰 완료 · 테스트 서버 수동 검증 대기
작성일 2026-09-10
요구사항 / API 기준 PRD, 특히 §3 요구사항과 §5 API 계약
기준 템플릿 docs/sample/sample-plan-task.md
현재 Phase / 활성 Goal P1-R4 및 P1/P2 영향 회귀 완료 / 활성 구현 Goal 없음
다음 Goal 테스트 브랜치 배포 후 실제 MySQL/Papago/HTTP 수동 검증

목표·범위·제약

본문 원문을 보존하면서 한국어·영어·일본어 번역을 저장하고, 상세·목록·미리보기 전체에 일관되게 제공한다. 신규/본문 수정은 전체 목표 언어를 처리하고, 기존 게시물의 누락 번역은 상세에서 요청 언어만 예약한다.

기존 Kotlin, Java 17, Spring Boot 2.7.14, Gradle Wrapper, JUnit 5, QueryDSL/JPA, Papago를 사용한다. 새 큐·스케줄러·외부 의존성·공개 API 필드를 추가하지 않는다. 원문과 번역을 분리하며 구매/차단/성인 정책을 유지한다. 새 재사용 기능은 v2/creator/channel/community/translation 아래에 두고 기존 공통 번역 연결부만 확장한다. 현재 PRD의 제외 범위는 모든 Goal에 적용한다. 신규 파일 경로는 아래 표에서 생성으로 구분한다.

로컬 검증은 Gradle 자동 테스트와 ktlint으로 한정한다. DDL·HTTP·실제 MySQL 동시성은 로컬에서 실행하지 않고, 테스트 브랜치 배포 후 이 문서의 수동 체크리스트에서 확인한다.

현재 상태와 실행 순서

Phase 결과 상태 완료 Task 다음 Goal
1 현재 본문에 대응하는 번역 저장·감지 기반 P1-R4 포함 로컬 자동 검증 완료 6/6 테스트 서버 수동 검증
2 작성·수정·상세의 비동기 연결과 폴백 완료 2/2 테스트 서버 수동 검증
3 모든 목록·홈 적용과 전체 사용자 흐름 검증 로컬 Gate 완료 2/2 테스트 서버 수동 검증

실행 순서: P1-T1 → P1-T2 → P1-GATE → P1-R1 → P1-GATE 재검증 → P1-R2 → P1-GATE 재검증 → P1-R3 → P1-GATE 재검증 → P2-T1 → P2-T2 → P2-GATE → P3-T1 → P3-T2 → P3-GATE → P3-R1 → P3-GATE 재검증 → P1-R4 → P1/P2 영향 회귀 → 수정 후 리뷰. Task 또는 Gate 하나만 활성 Goal로 운용한다. 각 Goal의 시작 조건·체크박스·완료 증거·검증 기록을 모두 충족해야 완료한다. 과거 완료 기록은 되돌리지 않고 후속 문제를 별도 수정 Task로 누적한다.

2026-09-10 후속 리뷰: Phase 1은 수정 1건, Phase 2와 Phase 3는 추가 확정 발견 사항 없음. 당시 후속 실행 순서: P1-R4 → P1-GATE 영향 범위 재검토 → P2 상세 경로 영향 재검토 → 기존 테스트 서버 수동 검증. 당시 정적 리뷰는 사용자 지시에 따라 테스트를 재실행하지 않았다. 이후 P1-R4의 RED/GREEN, P1/P2 영향 회귀와 수정 후 리뷰를 완료했다. 기존 Task/Gate 기록은 유지하며 남은 단계는 테스트 서버 수동 검증이다.

설계 계약

데이터와 공통 파이프라인

  • CreatorCommunity.languageCode: String?, contentRevision: Long을 추가한다. 기존 행은 NULL/0, 신규 행도 0부터 시작한다. 실제 원문 문자열 변경 때만 개정 번호를 증가시키고 언어를 NULL로 초기화한다. 동시 수정 시 개정 번호가 충돌하지 않도록 기존 소유권 조건을 유지한 행 잠금 조회를 사용한다.
  • 신규 CreatorCommunityTranslation에는 creatorCommunityId, locale, content, sourceRevision, sourceHash, sourceLanguage와 기존 BaseEntity 시각 필드를 둔다. 게시물 ID와 locale 조합을 유일하게 만든다.
  • LanguageDetectTargetType과 LanguageTranslationTargetType에 CREATOR_COMMUNITY를 추가한다. 번역 필드 키는 content 하나다. 다른 리소스 분기는 수정하지 않는다.
  • TranslationSource에 커뮤니티 원문의 개정 번호를 전달할 선택 필드를 추가하고 기존 호출은 기본값으로 유지한다. TranslationSourceExtractor는 활성 커뮤니티의 본문·언어·개정 번호를 같은 스냅샷으로 추출한다.
  • materializer는 추출된 개정 번호에 대응하는 메모리만 사용한다. 저장 시 현재 행의 개정 번호·언어·활성을 다시 검사하고 동일한 짧은 DB 트랜잭션 안에서 upsert한다. 오래된 값이 새 번역을 덮어쓰지 않도록 개정 검사와 쓰기를 직렬화한다. 외부 Papago 호출은 해당 잠금 밖에서 기존 워커가 처리한다.
  • 기존 translation_job 유일 키와 메모리 키는 유지한다. 이미 완료한 원문으로 복원되는 경우에는 메모리에서 현재 개정 번호의 조회 모델을 먼저 materialize하고, 부족한 경우에만 기존 scheduler를 호출한다.

신규 공유 서비스 경계

CreatorCommunityTranslationService 하나에 다음의 명확히 분리된 진입점을 둔다. 단순 내부 호출을 위한 별도 port는 만들지 않는다.

인터페이스 책임 / 호출 조건
findDisplayContents(postIds: List<Long>, locale: String): Map<Long, CreatorCommunityDisplayContent> 읽기 전용. 현재 게시물과 번역을 일괄 조회해 표시 본문과 유효 번역 여부를 반환한다. 감지·예약·materialize 금지. 조회 권한 판정은 호출자가 먼저 수행한다.
requestTranslations(postId: Long, targetLanguage: String? = null) 별도 쓰기 트랜잭션. NULL이면 원문 외 지원 언어 전체, 값이 있으면 그 언어만 처리한다. 언어가 없으면 감지 이벤트를 발행하고, 있으면 메모리 복원 후 누락 작업을 예약한다.

감지 이벤트에는 커뮤니티용 sourceRevision: Long? = null, targetLanguage: String? = null 정보를 추가한다. 기존 이벤트 발행자는 기본값으로 동작한다. 커뮤니티 핸들러는 감지 요청 당시 개정과 현재 개정을 비교한다. 감지 결과 저장은 개정 번호·활성 상태 조건을 포함한 갱신으로 처리하고, 저장 커밋 후 공유 서비스를 호출한다. 상세에서 넘어온 targetLanguage를 유지해야 기존 글의 첫 감지가 전체 언어 번역으로 확대되지 않는다.

이미 감지된 본문에는 감지를 반복하지 않고 해당 언어로 예약을 이어간다. 다중 상세 요청에서 감지 API의 동시 중복 호출을 완전히 제거하는 신규 분산 잠금은 추가하지 않는다. 기존 감지 캐시를 재사용하고 작업 중복/유일성 경합은 기존 DB 키와 커뮤니티 예약 경계에서 제어한다. 유일 키 경합으로 정상 상세 응답이 실패하지 않도록 통합 검증한다.

트랜잭션과 표시 순서

  • 작성·수정 커밋 후 처리에는 기존 커밋 후 실행 패턴을 재사용한다. 감지·예약은 롤백된 원문을 소비하지 않는다.
  • 레거시 서비스와 v2 facade/query service는 읽기 전용 트랜잭션이 있으므로 상세의 감지/예약은 별도 Spring bean의 REQUIRES_NEW 진입점을 통해 처리한다. 자기 호출로 트랜잭션 프록시를 우회하지 않는다.
  • 권한/노출 판정 → 현재 원문/유효 번역 선택 → 기존 유료 마스킹 → 응답 조립 순서로 적용한다.
  • 목록·채널 홈·홈 추천·팔로잉은 일괄 읽기 메서드만 호출한다. 추천 스냅샷에는 언어별 본문을 넣지 않고 기존 후보 ID로 얻은 상세 레코드의 본문만 응답 조립 전에 치환한다.
  • 원문을 담은 관리자 편집 응답, 댓글·답글, 푸시 이벤트와 정산 조회는 그대로 둔다.

Phase 1: 번역 저장과 원문 언어 감지 기반

구현 항목

  • Task 1.1: 개정 번호에 대응하는 본문 번역 저장·조회·메모리 복원

Goal ID / objective: P1-T1 — 현재 본문과 일치하는 번역만 저장·조회할 수 있다.

  • 시작 조건: PRD CCT-001, CCT-007, CCT-010 확인, 사용자 구현 요청.
  • 완료 증거: 아래 파일과 local automatic gate, 원문 복원/오래된 번역 차단 검증 기록. 실제 MySQL DDL·HTTP·동시성 확인은 테스트 브랜치 배포 후 수동 체크리스트로 이관한다.
  • 범위 밖: 언어 감지 이벤트 연결, HTTP 진입점 변경.
  • 생산 인터페이스: 설계 계약의 findDisplayContents, 언어 확정 상태의 requestTranslations, 커뮤니티 source/materializer 분기.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunity.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityRepository.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageTranslationEvent.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/TranslationSourceExtractor.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/TranslationReadModelMaterializer.kt
생성 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/adapter/out/persistence/CreatorCommunityTranslation.kt
생성 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/adapter/out/persistence/CreatorCommunityTranslationRepository.kt
생성 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/application/CreatorCommunityTranslationService.kt
생성 src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/CreatorCommunityTranslationServiceTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/TranslationJobWorkerTest.kt
생성 docs/20260910_크리에이터커뮤니티게시물본문번역/schema.sql
  • RED: 번역 없음/동일 언어/현재 개정 일치/이전 개정/다른 언어 결과의 표시 테스트와 본문 A→B→A 복원 테스트를 작성한다. materializer는 작업 실행 도중 원문 변경·비활성화 시 오래된 번역을 저장/노출하지 않는 테스트로 보호한다.
  • RED 확인: 아래 focused 명령에서 해당 동작의 assertion 실패를 확인한다. 컴파일 오류만으로 RED를 완료하지 않는다.
  • GREEN: 기존 메모리/워커를 연결하고 일괄 읽기, 개정 검사, 현재 메모리 복원을 최소 구현한다. MySQL DDL은 기존 행 NULL/0 보존, locale 유일 키, 모든 컬럼 COMMENT, 테이블 COMMENT를 포함한다. 시각은 TIMESTAMP, created/updated 기본값은 docs/agent-guides/문서유지보수.md를 따른다.
  • GREEN 확인: focused test에서 원문 보존·중복 키·동시 upsert·이전 결과 차단·캐시 복원 통과를 확인한다.
  • REFACTOR: 이번 변경의 중복만 정리하고 focused test와 공통 번역 회귀를 실행한다. 자동 검증 결과와 배포 후 DDL 확인 항목을 이 Task 아래에 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationJobWorkerTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.i18n.translation.*'

DDL은 테스트 브랜치 배포 후 격리된 MySQL 테스트 DB에 적용해 기존 행의 언어 NULL/개정 0, 원문 유지, locale 유일 키를 확인한다. 운영 DB 적용은 이 Task의 검증으로 실행하지 않는다.

P1-T1 검증 기록 — 2026-09-10

  • RED: CreatorCommunityTranslationServiceTest.shouldRequireCreatorCommunityTranslationPersistenceContract를 먼저 작성했다. ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationJobWorkerTest'는 exit 1로 실패했고, CreatorCommunityTranslationServiceTest.kt:13의 languageCode 누락 assertion이 원인이었다.
  • GREEN/REFACTOR: CreatorCommunity의 nullable 언어·0 시작 개정, 현재 개정/해시/언어/활성 상태를 잠금으로 재확인하는 materializer, 메모리 우선 복원 후 기존 scheduler를 쓰는 서비스, 일괄 표시 조회와 locale 유일 번역 저장을 추가했다. 같은 focused 명령은 exit 0, BUILD SUCCESSFUL로 통과했다.
  • 동시성: H2 MySQL 모드 @DataJpaTest에서 두 worker를 같은 원문 메모리 재료화 지점에 동시에 진입시켜, 게시물 PESSIMISTIC_WRITE 잠금과 locale 유일 키 후 번역 행 1개·오류 0개를 확인했다.
  • 공통 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' — exit 0, BUILD SUCCESSFUL. ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL.
  • DDL: schema.sql에 기존 행의 language_code NULL 및 content_revision 0 기본값, locale 유일 키, 전 컬럼/테이블 COMMENT, BaseEntity 시각 컬럼 규칙을 기록했다. 운영 DB에는 적용하지 않았다.
  • 전체 회귀/수동 HTTP/Papago/MySQL 적용 검증은 이 Task의 지정 focused/common Gradle 검증 범위를 넘어 실행하지 않았다.

P1-T1 검토 수정 — 2026-09-10

  • 검토 확인 RED: CreatorCommunityTranslationServiceTest.shouldRejectStaleManagedCreatorCommunityAfterConcurrentCommit를 실제 JPA transaction으로 추가했다. focused 명령은 exit 1, 20개 테스트 중 1개 실패였고, stale managed CreatorCommunity로 이전 번역을 저장한 CreatorCommunityTranslationServiceTest.kt:358 assertion이 원인이었다.

  • GREEN: EntityManager.refresh(..., PESSIMISTIC_WRITE)로 잠긴 게시물의 최신 상태를 다시 읽고, 번역 행도 findByCreatorCommunityIdAndLocaleForUpdate의 잠금 현재 읽기로 조회하게 했다. provider 호출은 기존처럼 materializer transaction 밖의 worker에 남는다. 같은 focused 명령은 exit 0, BUILD SUCCESSFUL로 통과했다.

  • 테스트 보강: 번역 없음·원문 언어 불일치·원문 해시 불일치의 원문 폴백, 실제 JPA stale persistence-context 차단, A→B→A 기존 번역 행 갱신, repeatable-read snapshot 뒤 동시 upsert를 확인했다.

  • H2 설정: @AutoConfigureTestDatabase(replace = NONE)와 jdbc:h2:mem:creator-community-translation;MODE=MySQL을 사용한다. 이는 H2 호환 모드일 뿐 실제 MySQL DDL·REPEATABLE READ 검증 증거가 아니다.

  • 기록 정정: 이전 검증 기록의 H2 MySQL 모드 표기는 datasource replacement를 끄기 전에는 성립하지 않았다. 현재 H2 설정을 바로잡았지만, 과거 결과를 실제 MySQL 결과로 소급하지 않는다.

  • 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' — exit 0, BUILD SUCCESSFUL. ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL.

  • 사용자 검증 정책 변경: local acceptance는 automatic Gradle test와 ktlint으로 한정한다. 위 자동 검증이 통과했으므로 Task 1.1과 GREEN 확인/REFACTOR 체크박스를 완료로 표시한다. Docker, 서버, HTTP, MySQL은 local에서 실행하지 않는다.

  • 배포 후 보류: schema.sql 적용, 기존 행 NULL/0, locale 유일 키, 실제 MySQL REPEATABLE READ 동작은 테스트 브랜치 배포 후 아래 수동 체크리스트에서만 확인하며 아직 통과로 기록하지 않는다.

  • Task 1.2: 미확정 원문 언어의 비동기 감지와 목표 언어 전달

Goal ID / objective: P1-T2 — 본문 언어가 없으면 현재 본문을 감지하고 요청 범위의 번역으로 이어진다.

  • 시작 조건: P1-T1 완료.
  • 완료 증거: 한국어/영어/일본어 감지, 단일/전체 목표 언어 전달, 지연 감지·실패 테스트 통과.
  • 범위 밖: 사용자 언어 입력 필드, 다른 리소스의 감지 정책 변경.
  • 소비/생산: 기존 LanguageDetectionCacheService.detectWithCache를 소비하고 커뮤니티 감지 분기와 완성된 requestTranslations를 제공한다.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityRepository.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/application/CreatorCommunityTranslationService.kt
생성 src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectTest.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionCacheService.kt
확인 src/test/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionCacheServiceTest.kt
  • RED: 언어 NULL인 본문 감지 후 targetLanguage=ja는 일본어만, NULL은 원문 외 두 언어로 이어지는 실패 테스트를 작성한다. 빈 본문·미지원 원문 언어·감지 실패·감지 중 본문 수정·중복 이벤트도 포함한다.
  • RED 확인: 아래 focused 명령으로 목표 언어 유실과 오래된 감지 결과 적용의 assertion 실패를 확인한다.
  • GREEN: 기존 감지 이벤트에 기본값이 있는 커뮤니티 메타데이터를 추가하고 개정 조건부 저장·커밋 후 예약을 연결한다. 다른 리소스 이벤트와 동작은 유지한다.
  • GREEN 확인: 외부 감지 provider 경계만 통제한 통합 테스트로 실제 커밋 후 이벤트/별도 트랜잭션을 확인한다. 테스트를 감싼 트랜잭션이 커밋되지 않아 AFTER_COMMIT이 실행되지 않는 문제를 피한다.
  • REFACTOR: focused test와 기존 감지 캐시/번역 scheduler 회귀 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationJobSchedulerTest'

P1-T2 검증 기록 — 2026-09-10

  • RED: CreatorCommunityLanguageDetectTest.shouldKeepLegacyDefaultsAndDeclareCreatorCommunityMetadata를 먼저 작성했다. ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'는 exit 1, CreatorCommunityLanguageDetectTest.kt:14의 CREATOR_COMMUNITY 누락 assertion으로 실패했다.
  • 행위 RED: 커밋 후 단일/전체 목표 언어 전달, 빈 본문·미지원 감지값·provider 실패·stale 본문·중복 이벤트를 추가했다. 같은 명령은 exit 1, CreatorCommunityLanguageDetectTest.kt:85, :106, :179의 CreatorCommunityTranslationService.requestTranslations(...) WantedButNotInvoked assertion으로 실패했다.
  • GREEN: LanguageDetectEvent의 기본값 있는 커뮤니티 메타데이터, 언어 미확정 본문의 현재 content/revision 이벤트 발행, 감지 외부 호출 전 무잠금 상태 확인과 잠금 후 active/revision/content 재확인, 저장 커밋 뒤 기존 번역 서비스 호출을 연결했다. 감지 언어가 지원 범위 밖이거나 비어 있으면 원문·작업 상태를 유지한다.
  • AFTER_COMMIT: @Transactional(propagation = Propagation.NOT_SUPPORTED) 테스트와 명시적 TransactionTemplate 커밋으로 이벤트를 발행했다. 테스트용 감지 provider 경계만 제어하고 실제 행 언어 저장 뒤 requestTranslations(postId, "ja") 또는 requestTranslations(postId, null) 호출을 검증했다.
  • focused: 위 P1-T2 명령 — exit 0, BUILD SUCCESSFUL.
  • 범위: local automatic 정책에 따라 Docker, HTTP, 실제 Papago, MySQL DDL/동시성은 실행하지 않았다.

Phase 1 Gate

  • Goal P1-GATE: 현재 본문 기반의 감지·번역 저장 경계 판정

  • 시작 조건: P1-T1, P1-T2 체크박스와 검증 기록 완료.

  • 완료 증거: 아래 명령 exit 0, DDL·엔티티 정적 대조, 개정/언어 불일치 미노출 기록.

  • 범위 밖: API 연결 구현. TDD 예외 사유: 이미 작성한 동작과 DDL의 Gate 검증이며 별도 제품 코드가 아니다.

  • 대체 검증: 기존 통합 테스트를 실행하고 DB의 현재 개정/번역 개정/언어를 대조한다.

  • 확인 파일: 두 Task의 source/test 파일 및 docs/20260910_크리에이터커뮤니티게시물본문번역/schema.sql.

./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*'

P1-GATE 검증 기록 — 2026-09-10

  • 자동 Gate: 위 명령 — exit 0, BUILD SUCCESSFUL. 현재 개정 번역 저장/메모리 복원/작업 예약과 커밋 후 감지 경계를 함께 회귀했다.
  • 형식: ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL.
  • 보류: 사용자 정책에 따라 실제 MySQL DDL·격리 수준, HTTP, Docker, 실제 Papago 검증은 실행하지 않았다. 테스트 브랜치 배포 후 수동 체크리스트는 모두 미완료 상태로 유지한다.

Task 1.R: P1-T2 리뷰 회귀 수정

Goal ID / objective: P1-R1 — 감지 커밋 후 독립 트랜잭션에서 현재 본문만 요청 범위대로 번역 작업으로 예약한다.

  • 시작 조건: reviews/p1-t2-review.md의 REV-P1-T2-001~004 확정, P1-T2 및 P1-GATE 기존 기록 확인.
  • 완료 증거: 실제 Spring 감지 listener와 scheduler를 사용하는 실패 재현 test, 최소 수정 후 focused test와 P1 Gate 재검증, 검증 기록.
  • 범위 밖: 다른 리소스 감지 정책, 공개 API, Docker·HTTP·실제 Papago·MySQL 수동 검증.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/application/CreatorCommunityTranslationService.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectTest.kt
생성 docs/20260910_크리에이터커뮤니티게시물본문번역/reviews/p1-t2-review.md
  • RED: REQUIRES_NEW, 실제 translation_job 저장, stale 본문 차단, known-language 다중 target continuation을 실제 Spring 경계로 재현한다.
  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'에서 7개 중 5개 assertion 실패를 확인한다.
  • GREEN: 감지 listener의 잠금 현재 읽기와 커밋 후 독립 번역 요청 트랜잭션을 최소 수정한다.
  • GREEN 확인: 같은 focused test가 성공하는지 확인한다.
  • REFACTOR: P1-T2 직접 영향 회귀, P1 Gate, ktlint을 실행하고 실제 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationJobSchedulerTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*'
./gradlew ktlintCheck

P1-R1 진행 기록 — 2026-09-10

  • RED: 새 CreatorCommunityLanguageDetectTest는 초기 Kotlin generic 추론과 AudioContentThemeQueryRepository test bean 누락을 바로잡은 뒤 실행했다. focused 명령은 exit 1, 7개 중 5개 assertion 실패였다. REQUIRES_NEW annotation 부재, 커밋 뒤 job 미저장, stale 감지 결과의 언어 저장, 이미 언어가 있는 두 target 이벤트의 job 미저장을 확인했다.
  • GREEN: requestTranslations를 REQUIRES_NEW로 분리하고, listener는 잠금 조회 뒤 EntityManager.refresh(..., PESSIMISTIC_WRITE)로 현재 본문을 확인한다. 이미 언어가 있는 이벤트도 감지 없이 after-commit에서 원 target을 요청한다.
  • GREEN 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' — exit 0, BUILD SUCCESSFUL. 실제 Spring listener와 scheduler를 통해 단일/전체 target job 저장, stale 본문 차단, known-language 두 target continuation을 확인했다.
  • REFACTOR/Phase Gate 재검증: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*' ktlintCheck bootJar — exit 0, BUILD SUCCESSFUL. focused/공통 번역 회귀, ktlint, bootJar를 함께 확인했다.
  • 범위: 사용자 정책에 따라 Docker, HTTP, 실제 Papago, MySQL 수동 동시성 검증은 실행하지 않았다.

Task 1.R3: P1-T2 동일 게시물 작업 예약 직렬화

Goal ID / objective: P1-R3 — 같은 게시물·target의 동시 requestTranslations가 누락 메모리에서도 하나의 translation job만 예약한다.

  • 시작 조건: reviews/p1-t2-review.md의 REV-P1-T2-006 확정, P1-R2 완료 기록 확인.
  • 완료 증거: 실제 Spring/JPA service와 job repository의 concurrent missing-memory RED test, 최소 lock 수정 후 두 호출 정상 종료·job 1개, P1 Gate 재검증.
  • 범위 밖: 공통 scheduler 동작, 다른 resource type, 분산 lock·의존성, P2/P3, Docker·HTTP·실제 Papago·MySQL 수동 검증.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/application/CreatorCommunityTranslationService.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/translation/CreatorCommunityTranslationServiceTest.kt
수정 docs/20260910_크리에이터커뮤니티게시물본문번역/reviews/p1-t2-review.md
  • RED: 같은 언어 확정 게시물·target의 두 요청을 동시에 시작해 실제 translation_job unique insert 경합을 재현한다.
  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest'에서 unique-key 오류 또는 정상 완료/job 1개 위반을 확인한다.
  • GREEN: REQUIRES_NEW request transaction 시작에 active post write lock과 refresh를 추가한다.
  • GREEN 확인: 같은 focused test가 두 호출 정상 종료와 job 1개를 확인한다.
  • REFACTOR: P1-T2 직접 영향 회귀, P1 Gate, ktlint을 실행하고 실제 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationJobSchedulerTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*'
./gradlew ktlintCheck

P1-R3 진행 기록 — 2026-09-10

  • 원인: requestTranslations가 post lock 없이 memory lookup과 TranslationJobScheduler의 select-then-insert를 수행해, 동일 post·target의 두 request transaction이 unique key에 경합할 수 있다.
  • RED: actual JPA translation_job 저장을 사용하는 두 REQUIRES_NEW transaction의 missing-job 조회 창을 동기화했다. focused 명령은 exit 1이었고, H2 SQLState 23505의 uk_translation_job_resource_field_target_hash unique index 충돌을 확인했다.
  • GREEN: requestTranslations 시작에서 active post를 findByIdAndIsActiveTrueForUpdate로 잠그고 EntityManager.refresh(..., PESSIMISTIC_WRITE)한 뒤 source extraction, memory lookup, job 조회·저장을 진행한다.
  • GREEN 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' — exit 0, BUILD SUCCESSFUL; 두 호출이 정상 종료하고 job 1개만 저장됨을 확인했다.
  • REFACTOR/Phase Gate 재검증: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationJobSchedulerTest' — exit 0, BUILD SUCCESSFUL. ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*' — exit 0, BUILD SUCCESSFUL. ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL.
  • 전체 ./gradlew test는 private scheduling service의 직접 영향 회귀와 P1 Gate가 통과했으므로 실행하지 않았다. Docker, HTTP, 실제 Papago, MySQL 수동 동시성 검증도 사용자 정책에 따라 실행하지 않았다.

Task 1.R2: P1-T2 동시 감지 target 보존

Goal ID / objective: P1-R2 — 두 언어 미확정 감지가 같은 현재 본문에서 경합해도 각 이벤트의 요청 target을 커밋 후 예약한다.

  • 시작 조건: reviews/p1-t2-review.md의 REV-P1-T2-005 확정, P1-R1 완료 기록 확인.
  • 완료 증거: 두 감지가 언어 NULL에서 시작한 뒤 한 감지만 먼저 커밋하는 실제 Spring failure test, 최소 수정 후 focused test와 P1 Gate 재검증, 검증 기록.
  • 범위 밖: 다른 리소스 감지 정책, 공개 API, Docker·HTTP·실제 Papago·MySQL 수동 검증.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectTest.kt
수정 docs/20260910_크리에이터커뮤니티게시물본문번역/reviews/p1-t2-review.md
  • RED: 두 감지가 languageCode=NULL에서 시작하고 첫 감지 커밋 뒤 두 번째가 lock을 얻는 실제 ja/en target 보존 test를 작성한다.
  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'에서 두 번째 target job 미저장 assertion 실패를 확인한다.
  • GREEN: lock/refresh 뒤 현재 revision/content가 유효하고 언어만 이미 설정된 경우 after-commit에 원 target을 등록한다.
  • GREEN 확인: 같은 focused test가 성공하는지 확인한다.
  • REFACTOR: P1-T2 직접 영향 회귀, P1 Gate, ktlint을 실행하고 실제 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationJobSchedulerTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*'
./gradlew ktlintCheck

P1-R2 진행 기록 — 2026-09-10

  • 원인: handleCreatorCommunityLanguageDetect는 lock과 refresh 뒤 다른 감지가 저장한 languageCode를 발견하면, 현재 revision/content가 일치해도 return하여 두 번째 이벤트의 target을 잃는다.
  • RED: shouldPreserveSecondTargetAfterConcurrentDetectionSetsLanguage는 두 감지가 languageCode NULL에서 시작한 뒤, 첫 감지의 ja 예약·커밋 후 두 번째 감지의 en 진행을 순서대로 제어했다. focused 명령은 exit 1, 8개 중 1개 assertion 실패였고, translation_job의 en 누락이 원인이었다.
  • GREEN: lock/refresh 뒤 revision/content가 같고 languageCode만 이미 설정되어 있으면 이를 덮어쓰지 않고, 기존 requestTranslationsAfterCommit에 두 번째 이벤트의 원 target을 등록한다. lock 보유 중 REQUIRES_NEW 호출은 하지 않는다.
  • GREEN 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' — exit 0, BUILD SUCCESSFUL. 실제 listener/scheduler/repository에서 ja와 en job 저장 및 source language ko 유지를 확인했다.
  • REFACTOR/Phase Gate 재검증: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*' ktlintCheck — exit 0, BUILD SUCCESSFUL.
  • 범위: 사용자 정책에 따라 Docker, HTTP, 실제 Papago, MySQL 수동 동시성 검증은 실행하지 않았다.

Task 1.R4: 최초 언어 감지 캐시 경합에서도 요청 target 보존

  • Goal P1-R4: 동일 본문의 최초 감지 캐시 저장이 경합해도 각 이벤트의 목표 언어 번역을 예약한다

  • 시작 조건: Phase 1 리뷰의 REV-P1-007 확인 및 후속 구현 요청. 기존 P1-R1~R3 완료 기록을 유지한다.

  • 관련 요구사항: CCT-002, CCT-005, CCT-010, 기존 P1-R2의 요청 target 보존.

  • 완료 증거: 실제 감지 캐시 INSERT 경합의 실패 회귀 테스트 → 최소 수정 → focused 성공 → P1 감지/번역 및 P2 상세 영향 회귀 → 실행 결과와 리뷰 재판정 기록.

  • 범위 밖: 외부 감지 중 게시물 행 잠금, 새 분산 잠금/큐/스케줄러, 번역 지원 언어·공개 API 변경, 감지 provider 실패를 무조건 성공으로 처리, 다른 리소스의 제품 정책 변경, 운영 DB 작업.

작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionCacheService.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionResultRepository.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionResult.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt
생성 src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectionCacheConcurrencyTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionCacheServiceTest.kt
확인 src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectTest.kt
기록 docs/20260910_크리에이터커뮤니티게시물본문번역/reviews/phase-1-review.md
기록 docs/20260910_크리에이터커뮤니티게시물본문번역/plan-task.md

설계 기준:

  • 캐시 miss → detector → 무조건 INSERT를 같은 키의 원자적 저장으로 바꾸고 저장된 결과로 수렴시켰다. MySQL ON DUPLICATE KEY UPDATE id=id와 scalar SELECT ... FOR UPDATE를 사용한다. DB 예외를 잡아 무시하지 않는다.

  • 동일 해시/provider/normalizationVersion의 승자 결과를 사용하며, 반복 읽기 격리에서도 최초 miss 스냅샷에 갇히지 않도록 저장 후 current read를 보장한다. 기본 언어를 임의로 지정하거나 감지 결과를 최신 본문에 무조건 적용하지 않는다.

  • 이미 rollback-only가 된 같은 JPA 트랜잭션에서 예외만 잡고 target 예약을 계속하는 방식은 금지한다. 공통 캐시 저장 수정이 다른 리소스의 기존 감지/캐시 재사용 동작을 바꾸지 않도록 회귀 검증한다.

  • 새 테스트는 실제 LanguageDetectionCacheService와 repository를 사용한다. detectWithCache 전체를 override하지 않고 감지 HTTP 응답/provider 경계만 통제해 두 트랜잭션이 모두 캐시 miss를 읽는 순서를 제어한다.

  • RED: shouldPreserveBothTargetsWhenInitialDetectionCacheWritesRace를 추가한다. 언어 NULL/캐시 없음의 동일 게시물에서 ja, en 감지를 동시에 시작하고 두 cache miss 이후 첫 저장/커밋, 후발 저장 순서로 진행한다. 두 흐름의 정상 완료, 캐시 1행, ja/en 작업 모두 존재를 assertion으로 검증한다.

  • RED 확인: 아래 focused 명령으로 후발 중복 키 예외 또는 target 누락의 의도한 실패를 확인한다. 두 이벤트가 애초에 cache miss에 진입하지 않은 테스트를 경합 재현으로 인정하지 않는다.

  • GREEN: 원자적 캐시 저장과 저장 결과 재사용만 최소 수정한다. 감지 후 기존 개정/본문 검사와 커밋 후 요청 target 전달을 유지한다.

  • GREEN 확인: 같은 focused 테스트를 통과시키고, 같은 본문의 서로 다른 게시물 생성도 캐시 1행을 공유하면서 각 게시물의 원문 외 두 언어 작업이 보존되는지 검증한다.

  • REFACTOR: 감지 캐시·listener·scheduler·materializer 및 실제 상세 프록시 영향 회귀를 실행한다. 선택한 DB 저장 문법의 MySQL 동작과 H2 테스트 지원 범위를 구분하고 실제 MySQL 미검증은 기존 수동 체크리스트에 남긴다. 관련 없는 변경 없이 아래 명령 결과와 REV-P1-007 수정 판정을 누적한다.

실행한 focused/영향 회귀 명령이다. focused의 fresh 재실행에는 --rerun-tasks --no-parallel을 추가했다.

./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectionCacheConcurrencyTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.i18n.translation.*'
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.*'
./gradlew ktlintCheck

공통 캐시 동작 변경의 영향이 위 회귀로 판정되지 않으면 전체 테스트로 확장한다. 전체 테스트 생략 시 대체 명령과 근거를 기록한다. 현재 상태: RED/GREEN·영향 회귀·수정 후 리뷰 완료. 실제 MySQL 검증은 미완료다.

P1-R4 실행 기록 · 2026-09-10

  • RED: 위 focused 명령은 두 provider 진입을 모두 확인한 뒤 exit 1로 실패했다. ExecutionException → DataIntegrityViolationException → H2 23505로 감지 캐시 유일 키 충돌을 재현했다.
  • GREEN: native MySQL ON DUPLICATE KEY UPDATE id=id로 승자 행을 보존하고 scalar SELECT ... FOR UPDATE로 저장 결과를 현재 읽기한다. native INSERT의 audit 시각을 명시하며 DB 예외를 잡고 계속 진행하지 않는다.
  • GREEN 확인: 실제 캐시 service/repository를 사용해 같은 게시물의 ja/en 이중 miss 및 서로 다른 게시물의 동일 본문 경합을 검증했다. 캐시 1행으로 수렴하며 각 요청의 목표 언어 작업을 보존한다.
  • focused fresh 재실행: 위 첫 명령 + --rerun-tasks --no-parallel, BUILD SUCCESSFUL (5분 19초).
  • P1 listener/scheduler/materializer 회귀: 위 두 번째 명령, BUILD SUCCESSFUL (1분 09초).
  • P2 상세/API 회귀: 위 세 번째 명령, BUILD SUCCESSFUL (1분 42초).
  • 형식: ./gradlew ktlintCheck, BUILD SUCCESSFUL (44초).
  • 수정 후 리뷰: spec PASS (ses_f7657d350ffe3YUYhEHvHPuikK), quality APPROVED (ses_f7656994dffeR2odTLswaw6RTo). REV-P1-007은 로컬 자동 검증 범위에서 수정 완료로 재판정했다.
  • 전체 ./gradlew test는 생략했다. 공통 캐시 계약, 실제 repository 경합, 모든 직접 listener/scheduler/materializer 테스트와 P2 상세/API 소비자가 위 대체 명령으로 통과했고, 미해결 상태로 남은 별도의 공통 경계가 없었다.
  • 범위: H2 MODE=MySQL은 실제 MySQL 8/InnoDB 검증이 아니다. 실제 MySQL/Papago/HTTP, Docker, 배포는 실행하지 않았다. 상세 명령·리뷰 출처는 후속 증거에 누적한다.

Phase 2: 작성·수정·상세에서 번역 처리

구현 항목

  • Task 2.1: 작성 커밋과 실제 본문 변경에 감지·번역 연결

Goal ID / objective: P2-T1 — 생성과 본문 수정이 현재 원문 기준의 전체 목표 언어 번역으로 이어진다.

  • 시작 조건: P1-GATE 완료, CCT-003~004, CCT-007 확인.
  • 완료 증거: 신규·실제 변경·동일 본문·롤백·관리자 공용 경로·동시 수정 검증.
  • 범위 밖: 이미지·오디오 업로드, 알림 발행, 소유권 정책 변경.
  • 소비 인터페이스: requestTranslations(postId, targetLanguage = null).
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityRepository.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt
확인/테스트 보강 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt
확인/테스트 보강 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt
  • RED: 생성 커밋 후 감지, 본문 변경 때만 언어 NULL/개정 증가, 롤백 시 작업 없음의 실패 테스트를 작성한다. 수정 후 새 요청에서 이전 번역 미노출, 실제 언어 변경, 관리자 공용 경로도 포함한다.
  • RED 확인: 아래 focused 명령으로 번역 연결 부재/개정 미변경의 assertion 실패를 확인한다.
  • GREEN: 기존 커밋 후 실행 패턴으로 공유 서비스를 연결하고 본문 문자열이 다른 경우에만 언어·개정 상태를 갱신한다. 소유권 조건을 유지한 잠금 조회로 동시 본문 수정의 개정을 직렬화한다.
  • GREEN 확인: 테스트를 다시 실행하고 동일 본문/본문 외 변경은 감지·번역 요청 0건인지 확인한다.
  • REFACTOR: 공용 서비스 호출자 회귀를 확인한다. 관리자 facade의 중복 번역 연결은 추가하지 않는다.
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostConcurrencyTest'

P2-T1 검증 기록 — 2026-09-10

  • RED: focused 명령은 exit 1이었다. 생성 뒤 번역 요청 누락, 본문 변경 뒤 개정/언어 상태 미갱신의 assertion 실패를 CreatorCommunityServiceTest.kt:333, AiCharacterAdminCommunityPostCreateTest.kt:93, AiCharacterAdminCommunityPostUpdateTest.kt:91에서 확인했다.
  • GREEN: CreatorCommunityService는 생성과 실제 본문 변경 뒤 기존 after-commit 패턴으로 requestTranslations(postId, null)을 요청한다. 본문 변경은 languageCode를 NULL로 초기화하고 contentRevision을 증가시키며, 수정 조회는 소유권 조건의 PESSIMISTIC_WRITE 잠금 후 refresh한다.
  • 동시성: 실제 H2/JPA 비관적 잠금 조회를 latch로 제어한 관리자 MockMvc 요청 두 건이 모두 200으로 종료하고, 최종 revision 2와 번역 요청 2건을 확인했다.
  • focused: 위 명령과 LegacyCommunityPostCharacterizationTest를 포함한 실행 — exit 0, BUILD SUCCESSFUL. 생성·수정·동일 본문·롤백·관리자 공용 경로·동시 수정과 변경된 수동 생성자 호출을 함께 확인했다.
  • P1 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*' — exit 0, BUILD SUCCESSFUL.
  • 형식: ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL.
  • 범위: local acceptance 정책에 따라 Docker, 서버 HTTP, 실제 Papago, MySQL 수동 동시성 검증은 실행하지 않았다.

Task 2.R: P2-T1 spec-review 실제 트랜잭션 증거

Goal ID / objective: P2-R1 — 실제 Spring transaction에서 생성 롤백과 본문 변경 뒤 stale 번역 폴백·감지 예약을 증명한다.

  • 시작 조건: P2-T1 완료, spec-review의 실제 transaction 증거 보강 요구 확인.
  • 완료 증거: proxied CreatorCommunityService 생성 롤백 뒤 post/job/detection 미영속과, 한글→영문 수정 커밋 뒤 revision·stale 번역 폴백·영문 감지·새 target job을 별도 transaction에서 확인한다.
  • 범위 밖: P2-T2 상세/목록 API, production 리팩터링, Docker·HTTP·실제 Papago/MySQL 검증.
작업 정확한 파일 경로
수정 src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectTest.kt
수정 docs/20260910_크리에이터커뮤니티게시물본문번역/plan-task.md
확인 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt
  • TEST-FIRST: 실제 outer transaction rollback 뒤 생성 post와 감지/번역 작업이 남지 않는 test를 작성한다. 기존 한글 translation 행이 있는 post의 실제 영문 수정 뒤, 감지 전 stale 번역 폴백과 감지 후 언어·target job을 test한다.
  • TEST-FIRST 확인: 첫 실행에서 async 감지 완료 전 languageCode를 읽은 assertion 실패를 확인하고, 기존 await 방식의 완료 조건으로 test 경계를 바로잡는다.
  • GREEN: test가 production 결함을 드러내면 실패 test를 유지한 최소 수정만 적용하고, 아니면 실제 경계 증거만 보강한다.
  • GREEN 확인: 실제 post-commit 경로가 완료될 때까지 latch/새 transaction으로 대기한 assertion을 통과시킨다.
  • REFACTOR: P2 focused, P1 번역/감지 회귀, ktlint 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostConcurrencyTest'

P2-R1 검증 기록 — 2026-09-10

  • test-first: CreatorCommunityLanguageDetectTest에 실제 CreatorCommunityService bean을 등록하고 AOP proxy를 확인했다. JPA repository, listener, translation service, scheduler는 실제 bean을 사용하며 S3·FCM 연관 의존성과 external detector만 test 경계로 제어했다.
  • 생성 rollback: 실제 outer TransactionTemplate에서 createCommunityPost를 호출하고 rollback-only로 종료했다. 새 transaction에서 생성 post 부재, 해당 post ID의 translation read-model 및 translation_job 0건, detector query 0건을 확인했다.
  • 영문 수정: 기존 한글 원문과 유효한 이전 영어 translation 행을 만들고 실제 modifyCommunityPost를 commit했다. 차단된 detector가 시작된 뒤 새 transaction에서 languageCode=NULL, revision 1, 현재 영문 원문 반환과 이전 영어 translation 행의 미사용을 확인했다. detector 해제 뒤 languageCode=en, ko·ja job 예약을 대기해 확인했다.
  • 첫 실행: rollback test는 통과했고 수정 test는 async @TransactionalEventListener 완료 전 state를 읽어 expected en but was null로 실패했다. 이는 production 결함이 아니라 outer service future가 async 감지 완료를 기다리지 않는 test synchronization 문제였으며, 기존 조건 기반 await로 post-commit 완료를 기다리도록 바로잡았다. production 파일은 변경하지 않았다.
  • focused: 위 P2-R1 명령에 --rerun-tasks를 추가해 재실행 — exit 0, BUILD SUCCESSFUL.
  • P1 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*' 에 --rerun-tasks를 추가해 재실행 — exit 0, BUILD SUCCESSFUL.
  • 형식: ./gradlew ktlintCheck --rerun-tasks — exit 0, BUILD SUCCESSFUL.
  • 범위: Docker, 서버 HTTP, detail/list API, 실제 Papago/MySQL 검증은 실행하거나 변경하지 않았다.

Task 2.R: P2-T1 quality-review 고정 상태 경쟁 회귀

Goal ID / objective: P2-R2 — 관리자/레거시 고정 상태 변경이 최신 본문·개정·언어 상태를 덮어쓰지 않는다.

  • 시작 조건: P2-T1 완료, quality-review의 stale flush 및 test pseudo-lock 지적 확정.
  • 완료 증거: stale admin read 뒤 동시 본문 변경을 commit하고 고정+본문 요청을 재개하는 실제 JPA race test에서 최신 본문·revision·language와 fixed 상태를 확인한다. test는 production 소유권 조건 PESSIMISTIC_WRITE query를 사용한다.
  • 범위 밖: P2-T2/P3, API schema, @DynamicUpdate/@Version, dependency, Docker·HTTP·실제 MySQL 검증.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostConcurrencyTest.kt
수정 docs/20260910_크리에이터커뮤니티게시물본문번역/plan-task.md
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityRepository.kt
  • RED: stale admin read 뒤 별도 transaction의 본문/개정/언어 변경을 commit하고, 재개한 fixed+content 요청이 최신 상태에서 적용돼야 한다는 실제 JPA race test를 작성한다.
  • RED 확인: 현재 구현으로 최신 state 보존 assertion이 실패하는지 확인한다.
  • GREEN: updateCommunityPostFixed가 소유권 조건 post lock과 refresh를 통해 최신 행을 얻은 뒤에만 고정 상태를 변경하도록 최소 수정한다. 관리자 흐름의 member→post lock 순서는 유지한다.
  • GREEN 확인: production repository lock query를 사용하는 test에서 두 번째 요청의 pre-lock/lock 도착을 latch로 제어하고 executor 종료까지 확인한다.
  • REFACTOR: P2 focused, P1 번역/감지 회귀, ktlint, diff check 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostConcurrencyTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*'
./gradlew ktlintCheck

P2-R2 검증 기록 — 2026-09-10

  • RED: shouldApplyFixedAndContentUpdateAfterConcurrentContentCommit는 admin facade의 활성 post read 뒤 첫 요청을 멈추고, 별도 worker의 실제 CreatorCommunityService.modifyCommunityPost가 production findByIdAndMemberIdForUpdate 경로로 after concurrent 본문·revision 1·language NULL을 commit한 뒤 mixed isFixed=true/after fixed 요청을 재개했다. 현재 구현에서 contentRevision 2 assertion은 실제 1로 실패했다.

  • GREEN: updateCommunityPostFixed의 비잠금 조회를 소유권 조건 findByIdAndMemberIdForUpdate와 EntityManager.refresh(..., PESSIMISTIC_WRITE)로 교체했다. facade의 기존 member lock 뒤 post lock 순서는 바꾸지 않았다.

  • 동시성 test: 기존 본문 race의 test-only JPQL lock 대체를 제거하고 두 worker start gate와 실제 production repository query를 사용한다. 새 race는 concurrent content worker가 실제 lock 호출 직전임을 latch로 알리고 commit 완료 뒤 mixed request를 재개한다. 모든 executor는 release 뒤 shutdown과 awaitTermination으로 종료를 확인한다.

  • focused: P2 명령에 --rerun-tasks를 추가해 재실행 — 67 tests, exit 0, BUILD SUCCESSFUL.

  • P1 회귀: P1 명령에 --rerun-tasks를 추가해 재실행 — exit 0, BUILD SUCCESSFUL.

  • 형식: ./gradlew ktlintCheck --rerun-tasks — exit 0, BUILD SUCCESSFUL.

  • diff: GIT_MASTER=1 git diff --check — exit 0.

  • 범위: P2-T2/P3, API schema, @DynamicUpdate/@Version, dependency, Docker, HTTP, 실제 MySQL 검증은 변경하거나 실행하지 않았다.

  • Task 2.2: 레거시·v2 상세의 저장 번역 표시와 누락 예약

Goal ID / objective: P2-T2 — 상세에서 원문을 즉시 제공하고 요청 언어의 누락 번역만 예약한다.

  • 시작 조건: P2-T1 완료, PRD §5 상세 계약 확인.
  • 완료 증거: 두 상세 API의 기존 응답 구조·언어 처리·권한·별도 쓰기 트랜잭션·비동기 폴백 테스트 통과.
  • 범위 밖: 목록 연결, 댓글 API에서 번역 예약, 번역 상태/원문 필드 추가.
  • 소비 인터페이스: 권한 확인 후 findDisplayContents를 조회하고, 유효 번역이 없을 때만 requestTranslations(postId, langContext.lang.code)를 호출한다.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/SelectCommunityPostResponse.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicy.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt
생성 src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityTranslationEndToEndTest.kt
  • RED: Accept-Language: ja에서 기존 언어 미확정 글의 최초 상세는 원문, 감지 후 일본어만 예약, 완료 후 일본어 제공을 테스트한다. 같은 언어·반복 상세·차단·비활성·미구매·댓글 API도 포함한다.
  • RED 확인: 아래 focused 명령에서 저장 번역 미사용/누락 예약 부재를 확인한다.
  • GREEN: 기존 권한 확인 뒤 상세에서 유효 번역이 없을 때만 공유 쓰기 진입점을 호출한다. 읽기 전용 부모 트랜잭션과 분리하고 번역 선택은 기존 마스킹 전에 DTO/레코드 copy(content = ...)로 반영한다. 원문 엔티티는 조회 중 변경하지 않는다.
  • GREEN 확인: 실제 HTTP 계약 테스트와 중복 상세 경합에서 정상 응답·중복 작업 방지·원문 즉시 응답을 확인한다.
  • REFACTOR: 같은 내부 조회를 사용하는 댓글/답글이 예약 진입점을 호출하지 않는지 회귀 검사한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityTranslationEndToEndTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest'

P2-T2 검증 기록 — 2026-09-10

  • RED: 상세 경로에 번역 선택과 예약 연결이 없던 상태에서 legacy/v2 focused test의 저장 번역 미사용과 누락 예약 assertion 실패를 확인했다. 컴파일 오류는 RED 증거로 사용하지 않았으며, 새 통합 test의 viewerId named argument를 실제 memberId 계약에 맞춰 바로잡았다.
  • GREEN: CreatorCommunityService와 CreatorChannelCommunityQueryService는 권한/차단 확인 뒤 findDisplayContents(...)[postId] ?: content를 선택하고 requestTranslations(postId, 요청 언어)를 호출한다. 선택한 본문은 기존 유료 마스킹 전 DTO/record copy(content = ...)에만 반영하며, 엔티티 원문은 변경하지 않는다.
  • 통합: CreatorCommunityTranslationEndToEndTest는 저장된 번역의 유료 마스킹 전 선택과, 언어 NULL 원문의 즉시 폴백 및 targetLanguage=ja 단일 감지 이벤트를 실제 JPA 경계에서 확인했다.
  • HTTP 계약: legacy/v2 detail test는 번역 존재/누락, 구매자·소유자의 전체 본문, 미구매자의 기존 마스킹, 차단·비활성 경로, 댓글·답글의 예약 비진입을 확인했다. 같은 언어 예약 없음과 동시 예약의 유일성은 기존 CreatorCommunityTranslationServiceTest/통합 test로 회귀했다.
  • focused: 위 명령 — exit 0, BUILD SUCCESSFUL.
  • P1 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceIntegrationTest' — exit 0, BUILD SUCCESSFUL.
  • 호출자 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest' — exit 0, BUILD SUCCESSFUL.

Task 2.R: P2-T2 실제 상세 파이프라인 증거 보강

Goal ID / objective: P2-R3 — 실제 Spring 프록시 상세 조회가 감지, 단일 작업 예약, 재료화와 유효 번역 재사용을 올바르게 연결한다.

  • 시작 조건: P2-T2 완료, 상세 경로의 실제 transaction 증거 보강 요구 확인.
  • 완료 증거: 원문 즉시 반환, 감지 후 요청 언어 하나의 job, 반복·동시 상세의 단일 job, memory 재료화 후 번역 반환, 유효 번역 상세의 예약 미호출을 확인한다.
  • 범위 밖: 목록/홈, 외부 감지·번역 API, Docker, live HTTP, 실제 MySQL 검증.
작업 정확한 파일 경로
수정 src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectTest.kt
확인 src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
확인 src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt
  • RED: 병렬 상세 응답 본문 검증은 ExecutorService.submit { ... }가 Runnable 오버로드를 선택해 Future.get()이 null을 반환하며 실패하는 것을 확인한다. 이는 production 동작이 아닌 수용 테스트의 반환값 폐기 문제다.
  • GREEN: Callable을 명시해 두 병렬 상세의 실제 원문 반환과 translation job 1개를 검증한다.
  • GREEN 확인: 실제 CreatorCommunityService, LanguageDetectListener, ResourceTranslationJobScheduler, TranslationReadModelMaterializer 경로에서 원문 즉시 반환, ko job 하나, memory 재료화 후 번역 반환을 확인한다. legacy/v2 unit test는 유효 번역의 문자열이 원문과 같아도 requestTranslations를 호출하지 않음을 확인한다.
  • REFACTOR: P2 focused, P1 번역/감지 회귀, Phase 2 Gate를 실행한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityTranslationEndToEndTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceIntegrationTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*'
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'

P2-R3 검증 기록 — 2026-09-10

  • RED: 병렬 lambda의 반환값을 확인하는 assertion은 expected: [detail source content, detail source content] but was: [null, null]로 실패했다. submit의 Runnable 오버로드가 반환값을 폐기한 것이 원인이었고, production 상세 응답이나 DB 본문 손실은 아니었다.
  • GREEN: Callable 명시 뒤 실제 두 상세 응답은 원문을 반환하고, 감지 완료 후 ko translation job은 하나만 유지됐다. 수동으로 저장한 translation memory를 실제 materializer가 적용한 뒤 상세는 번역문을 반환했다.
  • 유효 저장 번역: legacy/v2 상세 unit test는 CreatorCommunityDisplayContent.hasValidTranslation으로 판정하므로, 번역문이 원문과 같아도 requestTranslations를 호출하지 않는다. 실제 상세 파이프라인 test는 재료화 뒤 번역문을 반환하고 job 수가 증가하지 않음을 확인했다.
  • focused: 첫 번째 명령 — exit 0, BUILD SUCCESSFUL (41초).
  • P1 회귀: 두 번째 명령 — exit 0, BUILD SUCCESSFUL (26초).
  • Phase 2 Gate: 세 번째 명령 — exit 0, BUILD SUCCESSFUL (1분 30초).
  • latch RED: CreatorCommunityLanguageDetectTest.shouldNotCompleteNextTaskWhenPreviousTaskFinishesAfterLatchIsReassigned에서 A를 멈춘 뒤 B가 다음 task의 latch를 준비하게 했다. ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest.shouldNotCompleteNextTaskWhenPreviousTaskFinishesAfterLatchIsReassigned'는 exit 1, expected: <false> but was: <true>로 실패했다. A 종료가 completion.get()으로 B의 latch를 감소시킨 것이 원인이었다.
  • latch GREEN: decorator 생성 시점의 taskCompletion을 캡처해 해당 task 종료에만 사용하도록 했고, latch regression과 상세 파이프라인 detector는 finally에서 release하도록 정리했다. ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' — exit 0, BUILD SUCCESSFUL (20초).
  • P2 재회귀: 첫 번째 명령 — exit 0, BUILD SUCCESSFUL (44초).
  • 형식: ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL (17초).

Phase 2 Gate

  • Goal P2-GATE: 생성·수정·기존 글 상세 흐름 판정

  • 시작 조건: Phase 2 두 Task 완료.

  • 완료 증거: 생성→커밋→감지→번역 저장과 기존 글 상세→요청 언어만 예약의 실제 통합 결과.

  • 범위 밖: 목록/홈 구현. TDD 예외 사유: 앞선 Task의 사용자 흐름 검증 Gate다.

  • 대체 검증: 테스트 HTTP 요청과 실제 커밋 후 DB 상태를 대조하고 롤백 케이스에 작업이 없는지 확인한다.

  • 확인 파일: Phase 2의 두 EndToEndTest, 서비스 테스트, PRD §4~5.

./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'

P2-GATE 검증 기록 — 2026-09-10

  • gate: 위 명령 — exit 0, BUILD SUCCESSFUL. 생성·수정·기존 글 상세, 레거시 서비스/JPA 및 v2 MockMvc 계약, 관리자 공용 경로를 함께 확인했다.
  • 형식: ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL.
  • diff: GIT_MASTER=1 git diff --check — exit 0.
  • 범위: local automatic 정책에 따라 Docker, live server/curl, 실제 Papago, 실제 MySQL DDL·동시성은 실행하지 않았다. HTTP 동작은 자동 MockMvc 통합 테스트로만 검증했다.

Phase 3: 목록·홈·미리보기 번역과 최종 검증

구현 항목

  • Task 3.1: 레거시 목록과 v2 커뮤니티·채널 홈에 저장 번역 적용

Goal ID / objective: P3-T1 — 커뮤니티 목록과 채널 홈이 작업 예약 없이 저장된 번역을 제공한다.

  • 시작 조건: P2-GATE 완료.
  • 완료 증거: 언어별 목록·최신 목록·고정/일반 채널 홈, 유료 마스킹, 추가 N+1 없음, 감지/작업 증가 0건.
  • 범위 밖: 추천/팔로잉 소식, 정렬·페이지·고정 정책 변경.
  • 소비 인터페이스: findDisplayContents만 호출. 레거시 DTO와 v2 레코드를 표시 직전에 복사해 기존 마스킹을 재사용한다.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityTranslationEndToEndTest.kt
  • RED: 번역 존재/누락·이전 개정 혼합 페이지와 무료·유료·소유자·구매자 응답을 테스트한다. 이모지 포함 유료 본문에서 기존 코드포인트 기준 미리보기를 확인한다.
  • RED 확인: 아래 명령으로 번역 미사용 assertion 실패를 확인한다.
  • GREEN: 페이지별 번역 일괄 조회 후 기존 마스킹 전에 반영한다. 채널 홈은 이미 호출하는 findHomeCommunityPosts 공통 경로를 통해 고정/일반 게시물을 모두 처리한다.
  • GREEN 확인: 목록만 호출했을 때 감지 이벤트/번역 작업 생성 0건, 페이지·개수·순서 유지, 페이지 크기에 비례한 추가 번역 SELECT 증가 없음까지 확인한다.
  • REFACTOR: 기존 권한·홈 응답 회귀와 focused test 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest'
실행·검증 기록 — 2026-09-10
  • 무엇을: 레거시 목록·최신 목록, v2 커뮤니티 탭, 채널 홈의 고정/일반 게시물에 저장된 표시 번역을 한 번에 조회해 본문에 복사했다. 상세 경로의 작업 예약 정책은 변경하지 않았고, 목록/홈에서는 findDisplayContents만 호출한다.

  • 왜: 목록 응답은 번역 누락 시 원문으로 안전하게 폴백하면서도 감지·번역 작업을 만들지 않아야 한다.

  • RED 확인: 새 테스트 6개를 대상으로 ./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest.shouldUseStoredTranslationsOnceForLegacyCommunityListBeforePaidMasking' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest.shouldUseStoredTranslationsForVisibleLatestCommunityPostsWithoutScheduling' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest.shouldUseStoredTranslationsOnceForCommunityTabBeforePaidMasking' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest.shouldUseStoredTranslationsForPinnedAndNormalHomeCommunityPostsWithoutScheduling' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest.shouldUseStoredTranslationsForCommunityTabWithoutScheduling' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest.shouldUseStoredTranslationsForPinnedAndNormalHomeCommunityPosts'를 실행했다. exit 1의 기대된 assertion 실패로 번역 적용 전 원문이 반환됨을 확인했다.

  • GREEN/영향 범위 회귀: 위 focused 명령 — exit 0, BUILD SUCCESSFUL (1분 3초). 저장 번역·원문 폴백·이전 개정 무시, 유료 미구매자/구매자/소유자 마스킹, 차단 필터 후 일괄 조회, 목록/홈 호출의 감지·번역 작업 미생성을 단위·MockMvc 통합 테스트로 확인했다.

  • 형식: ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL (45초).

  • 전체 회귀와 수동 HTTP: 공통 번역 API 또는 모든 Phase를 변경하지 않았으므로 직접 영향받은 package/API 회귀까지만 실행했다. 운영과 분리된 DB·Redis·S3·Papago 및 테스트 계정이 준비되지 않아 live server/curl·실제 Papago 검증은 실행하지 않았고, HTTP 응답은 MockMvc 통합 테스트로 검증했다.

  • 추가 실제 통합 증거: CreatorCommunityTranslationEndToEndTest는 실제 CreatorCommunityTranslationService와 H2 저장소를 사용한다. 영속성 컨텍스트와 Hibernate 통계를 매 배치 전 초기화한 뒤 1개와 4개 ID를 각각 읽어, 게시글 IN 조회 1회와 creator_community_translation 조회 1회의 합계인 prepared statement 2회를 동일하게 확인했다. 따라서 페이지 크기는 번역 읽기 SELECT 수를 늘리지 않는다.

  • 추가 부작용 증거: 같은 실제 읽기 서비스를 레거시 목록의 번역 누락 게시물에 연결하고, 실제 translation_job 행 수의 전후 동일성과 ApplicationEventPublisher·재료화기·resource scheduler의 무상호작용을 확인했다. 읽기 서비스 자체는 mock으로 대체하지 않았다. v2 탭의 mock 경계 테스트는 별도로 반환 50개 ID만 전달하고 51번째 fetchLimit + 1 ID를 제외함을 확인한다.

  • TDD 예외: 이번 추가는 이미 올바른 production 동작을 직접 측정하는 증거 테스트뿐이므로 RED assertion을 만들 수 없었다. 새 테스트는 즉시 통과했으며 production P3-T1 파일은 변경하지 않았다.

  • 재검증: ./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest' — exit 0, BUILD SUCCESSFUL (1분 18초). ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL (17초).

  • Task 3.2: 홈 추천과 팔로잉 소식 본문에 저장 번역 적용

Goal ID / objective: P3-T2 — 홈 추천·팔로잉 소식의 커뮤니티 본문과 파생 미리보기가 요청 언어로 표시된다.

  • 시작 조건: P3-T1 완료.
  • 완료 증거: 두 API의 번역/원문 폴백·일괄 조회·작업 미생성·다른 소식 유형 불변 테스트.
  • 범위 밖: 추천 스냅샷/순위 변경, FCM·알림 이력·오디오 소식 번역.
  • 소비 인터페이스: LangContext의 언어와 findDisplayContents. 추천 상세 레코드/팔로잉 도메인의 커뮤니티 항목만 복사한다.
작업 정확한 파일 경로
수정 src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt
수정 src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt
확인 src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt
수정 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
  • RED: 일본어·영어 요청의 인기 커뮤니티/팔로잉 소식, 누락·이전 개정 폴백, 비커뮤니티 항목 유지, 조회만으로 작업이 늘지 않는 테스트를 작성한다.
  • RED 확인: 아래 focused 명령으로 해당 응답이 원문에 머무르는 assertion 실패를 확인한다.
  • GREEN: 이미 필터링된 게시물 ID를 모아 공유 읽기 메서드로 처리한다. 팔로잉 서비스에 기존 LangContext를 전달한다. 본문은 DB 게시물의 현재 내용에서 가져오므로 과거 소식 발행 문자열을 번역 소스로 쓰지 않는다.
  • GREEN 확인: 언어 전환·익명 허용 추천·인증된 팔로잉 흐름을 검증하고 기존 필터/추천 순서를 대조한다.
  • REFACTOR: 변경된 생성자 호출 테스트를 갱신하고 두 기능의 직접 영향 회귀 결과를 기록한다.
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest' --tests 'kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest'
실행·검증 기록 — 2026-09-10
  • 무엇을: 인기 커뮤니티 추천의 최종 노출 게시물과 팔로잉 최근 소식의 커뮤니티 게시물만 요청 언어의 저장 본문으로 복사했다. 추천 중복 제거·limit와 팔로잉 필터·순서·다른 소식 유형은 변경하지 않았다.
  • RED 확인: 서비스·API 테스트 4개가 번역 연결 전 원문 본문 assertion으로 실패함을 확인했다.
  • GREEN/회귀: 관련 5개 테스트 클래스에 --rerun-tasks를 적용해 exit 0, BUILD SUCCESSFUL (2분 38초)을 확인했다. 일본어 비회원 추천, 영어 인증 팔로잉, 현재 원문 폴백, 최종 ID 단일 배치, 비커뮤니티 불변을 검증했다.
  • 리뷰: 명세 리뷰 APPROVED (ses_f7757315dffeoDl3Ef2bhCO684), 품질 리뷰 APPROVED (ses_f774b8bf4ffehbjDvQWo6yE84S).
  • 형식/diff: ./gradlew ktlintCheck --rerun-tasks — exit 0, BUILD SUCCESSFUL (26초). GIT_MASTER=1 git diff --check — exit 0.
  • 범위: Docker, live HTTP, 실제 Papago/MySQL은 실행하지 않았으며 테스트 브랜치 배포 후 수동 체크리스트에서 확인한다.

Task P3-R1: DDL 해시 컬럼 타입 정합화

  • Goal P3-R1: source_hash DDL을 Hibernate 엔티티 매핑과 일치시킨다

  • 시작 조건: P3-GATE 코드 품질 리뷰에서 CHAR(64)와 VARCHAR(64) 불일치가 차단 이슈로 확정됨.

  • 완료 증거: schema.sql의 source_hash VARCHAR(64) 확인, ./gradlew tasks --all, git diff --check, 해당 delta 코드 품질 재검토 통과.

  • 범위 밖: 엔티티 매핑 변경, 실제 MySQL DDL 적용, 비차단 로그·테스트 종료 개선.

  • RED: rg -n 'source_hash VARCHAR\(64\)' schema.sql이 일치 항목 없이 실패하는지 확인한다.

  • GREEN: DDL 한 줄만 VARCHAR(64)로 변경하고 동일 명령이 성공하는지 확인한다.

  • REFACTOR: 추가 구조 변경 없이 문서 상태와 Gate 증거만 동기화한다.

  • 수정 파일: docs/20260910_크리에이터커뮤니티게시물본문번역/schema.sql, prd.md, plan-task.md.

실행·검증 기록 — 2026-09-10
  • RED: rg -n 'source_hash VARCHAR\(64\)' schema.sql — 일치 항목 없이 exit 1.
  • GREEN: source_hash를 엔티티의 String + @Column(length = 64)와 같은 VARCHAR(64)로 변경했고 동일 rg가 11행을 반환했다.
  • 회귀: ./gradlew tasks --all — exit 0, BUILD SUCCESSFUL; GIT_MASTER=1 git diff --check — exit 0.
  • delta 품질 재검토: PASS (ses_f772f064cffeKa532hsf1Pa3GU). 실제 MySQL 적용과 ddl-auto: validate 기동 확인은 수동 검증에 유지한다.

Phase 3 Gate

  • Goal P3-GATE: 전체 노출 표면과 비동기 정합성 최종 판정

  • 시작 조건: P3-T1, P3-T2 및 이전 Gate의 증거 완료.

  • 완료 증거: 아래 자동 검증, PRD CCT-001~010 대조 및 5개 리뷰 레인 통과. HTTP·실제 MySQL/Papago는 배포 후 수동 체크리스트로 분리한다.

  • 범위 밖: 테스트 삭제/완화, 운영 데이터 번역/배포, 다른 기능의 결함 수정.

  • TDD 예외 사유: 기능 추가가 아닌 전체 수용 기준 검증 Gate다.

  • 대체 검증: 자동 회귀, 격리된 실행 서버의 HTTP 응답/DB 작업 수 대조, 문서와 실제 동작 대조.

  • 확인 파일: 모든 Task의 변경 파일, prd.md, plan-task.md, schema.sql.

./gradlew test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.recommendation.*' --tests 'kr.co.vividnext.sodalive.v2.home.following.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'
./gradlew ktlintCheck
./gradlew bootJar

기본 최종 검증은 위 직접 영향 범위다. 이번 변경이 공통 번역 API의 기존 리소스 동작까지 바꾸거나, 위 범위로 판정할 수 없는 실패가 발견되거나, 사용자/릴리스 기준이 전체 검증을 요구하면 ./gradlew test를 추가한다. 전체 테스트 생략 시 공통 번역 및 영향받은 각 기능을 위 명령으로 검증했다는 근거와 실제 결과를 기록한다.

최종 Gate 기록 — 2026-09-10
  • 직접 영향 테스트 — clean 후 --no-parallel 순차 실행, exit 0, BUILD SUCCESSFUL (5분 20초). 53개 클래스 493개 테스트, 실패·오류·skip 0을 XML에서 확인했다.
  • ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL (20초). ./gradlew bootJar — exit 0, BUILD SUCCESSFUL (1분 31초).
  • GIT_MASTER=1 git diff --check — exit 0. 최초 병렬 Gradle의 kapt 출력 경합은 clean 후 순차 성공으로 대체했다.
  • 최종 리뷰: 목표·제약 PASS, 자동 QA PASS, 보안 PASS, 문맥 PASS. 코드 품질의 DDL 타입 차단 이슈는 P3-R1로 수정 후 delta 재검토 PASS.
  • 비차단 관찰: 번역 예약 실패 로그 분류, 일부 경합 테스트 executor 종료 확인, 언어 감지 완료 전 반복 상세 요청의 호출 증폭은 이번 범위에서 변경하지 않았다.
  • 간접 소비자: LiveApiService, ExplorerService도 기존 목록 서비스를 통해 저장 번역을 사용하며 추가 예약은 하지 않는다.
  • 로컬 자동 Gate는 완료했다. 실제 DDL·MySQL 격리 수준·HTTP·Papago 검증은 아래 테스트 서버 수동 체크리스트에서 미완료로 유지한다.

테스트 브랜치 배포 후 수동 체크리스트

수동 HTTP 검증은 운영과 분리된 DB·Redis·S3/Papago 설정 및 테스트 계정이 준비된 테스트 서버에서 실행한다. 기존 ./gradlew bootRun을 사용하며 토큰·키·본문 전문은 기록물에서 제거한다.

  1. schema.sql을 테스트 DB에 적용한다. 기존 행은 language_code NULL, content_revision 0, 원문이 유지되고, creator_community_id + locale 유일 키가 생성됐는지 확인한다.
  2. 같은 게시물·locale의 번역 요청을 동시에 보낸다. duplicate-key 오류나 API 실패 없이, 현재 원문과 일치하는 번역 행이 정확히 1개인지 확인한다.
  3. 언어 NULL인 기존 무료 게시물로 PRD §5의 모든 목록/홈 API를 조회한다. 원문 표시, 감지/번역 작업 증가 0건을 확인한다.
  4. 같은 글을 Accept-Language: ja로 상세 조회한다. 최초 원문 응답, 감지 후 일본어 작업만 예약, 워커 완료 후 상세/목록/채널 홈/추천/팔로잉에서 저장 번역 사용을 확인한다.
  5. 한·영·일 원문의 신규 게시물과 실제 본문 수정을 실행한다. 다른 두 언어 생성, 수정 직후 현재 원문 폴백, 지연된 이전 감지/번역 결과 미노출, A→B→A 메모리 복원을 확인한다.
  6. 유료 글을 미구매자/구매자/소유자로 조회하고 번역 선택 후 기존 마스킹이 적용되는지 확인한다. 차단·성인·비활성·댓글/답글 노출과 미디어 URL 규칙은 기존 결과와 대조한다.
  7. 감지/번역 provider 실패와 반복 상세 요청을 재현한다. 원문 응답, 기존 재시도 정책, 작업 중복 방지, 페이지 크기 증가 시 추가 번역 조회 수가 선형으로 늘지 않음을 확인한다.

P1-R4 실제 MySQL 수동 확인 · 2026-09-10 추가

  • 테스트 서버가 실제 MySQL 8/InnoDB인지 확인하고 버전·테이블 엔진·세션 격리 수준을 기록한다. 감지 캐시의 해시/provider/normalizationVersion 유일 키와 audit 컬럼의 타입·NULL/default 조건을 확인한다. native INSERT 후 생성/수정 시각이 채워지고 중복 upsert가 승자 감지값과 생성 시각을 덮어쓰지 않는지 대조한다.
  • 언어 NULL이고 해당 캐시 키가 없는 동일 한국어 게시물에 ja/en 상세 요청을 보낸다. 테스트용 provider 제어로 두 miss와 provider 진입을 확인한 뒤 첫 감지 저장·커밋·ja 예약을 완료하고 후발 감지를 해제한다. 실제 REPEATABLE READ에서 예외/rollback 없이 두 흐름이 끝나고 캐시 1행, ja/en 작업이 모두 남는지 확인한다. 단순 동시 HTTP 요청만으로 이 실행 순서를 증명했다고 보지 않는다.
  • 서로 다른 게시물을 동일 본문으로 생성하고 캐시 miss 두 건의 저장 순서를 같은 방식으로 제어한다. 공통 캐시 1행과 승자 결과 재사용, 각 게시물의 원문 외 두 언어 작업 보존을 확인한다.
  • provider 감지 응답을 차단한 동안 별도 연결에서 해당 게시물 본문 수정을 커밋할 수 있는지 확인한다. 수정 후 개정 증가·언어 초기화와 현재 원문 폴백을 확인하고 이전 감지를 해제한다. 오래된 결과가 새 본문·개정·언어·번역을 덮어쓰거나 이전 원문의 작업을 새 개정에 적용하지 않는지 확인한다.

필요한 테스트 환경이 없으면 수동 검증은 미완료로 기록한다. MockMvc/provider 대체 테스트를 실제 Papago 검증으로 표기하지 않는다. 실제 Papago 확인은 비민감한 테스트 문장을 사용하며 수동 검증 환경에서만 수행한다.

Progress와 검증 기록

문서 작성 — 2026-09-10

  • 상태: PRD·구현 계획 작성. 구현 Task/Gate는 모두 미실행.
  • 무엇을: 사용자 인터뷰 결정과 레거시/v2 생성·상세·목록·홈 경로를 이 계획의 6개 Task에 연결했다.
  • 왜: 구현 전 모호한 범위를 없애고 정확한 변경 위치·수용 기준·TDD 순서를 제공하기 위해서다.
  • 실행 확인: ./gradlew tasks --all — exit 0, BUILD SUCCESSFUL. test, bootRun, bootJar, ktlintCheck task를 확인했다.
  • 관찰: 기존 Gradle 설정에서 Gradle 9.0 비호환 예정 deprecation 안내가 출력됐다. 이번 문서 범위에서 변경하지 않는다.
  • 미실행: 코드 구현, 신규 테스트 작성/실행, 서버 실행, Papago 호출, DDL 작성/적용, 운영 작업.
  • 테스트 생략 근거: 제품 코드를 변경하지 않는 문서 작업이다. 미래 테스트 명령을 계획에 적은 것은 테스트 통과 증거가 아니다.
  • 남은 실행 조건: 사용자의 별도 구현 요청과 각 Task의 검증 환경 확보.

문서 검증 — 2026-09-10

  • ./gradlew tasks --all — 문서 생성 후 재실행, exit 0, BUILD SUCCESSFUL.
  • 문서 경로·링크·템플릿 잔여값·공백 검사 — exit 0, Document validation: PASS. 기존 파일 참조가 존재하며 미래 생성 파일 7개는 생성 항목으로 구분했다.
  • 요구사항/계획 대조 — 요구사항 10개, 구현 Task 6개, Gate 3개가 연결되어 있고 구현 완료 체크는 없다.
  • git diff --check — exit 0. 신규 미추적 문서의 공백은 별도 문서 검사로 확인했다.
  • git -c core.quotePath=false status --short --untracked-files=all — 이 작업 폴더의 prd.md, plan-task.md 두 파일만 추가됨을 확인했다.

P2-R3 최종 검증 — 2026-09-10

  • ./gradlew tasks --all — exit 0, BUILD SUCCESSFUL; test, ktlintCheck, build task가 유효함을 확인했다.
  • ./gradlew ktlintCheck — exit 0, BUILD SUCCESSFUL. P2 변경 테스트의 import 순서와 Callable 호출 줄바꿈을 정정했다.
  • ./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest.shouldRunDetailTranslationPipelineThroughProxyListenerAndScheduler' — exit 0, BUILD SUCCESSFUL. 실제 Spring 상세 파이프라인에서 원문 즉시 반환, 단일 job, 재료화 후 번역 반환을 확인했다.
  • ./gradlew build — exit 0, BUILD SUCCESSFUL (8분 16초). 컴파일, 전체 테스트, ktlint, 패키징을 포함한다.
  • GIT_MASTER=1 git diff --check — exit 0. Docker, live HTTP, 실제 Papago, 실제 MySQL 검증은 실행하지 않았다.

Phase별 후속 정적 리뷰 — 2026-09-10

  • 기준: 인터뷰를 반영한 prd.md와 이 계획, 현재 source/test/DDL. HEAD와 작업 트리 지문은 리뷰 증거에 기록했다.
  • Phase 1: 감지 캐시 최초 INSERT 경합으로 후발 target 예약이 누락되는 정적 경로 1건 확정. REV-P1-007을 신규 P1-R4로 추가했다. 기존 완료 기록과 이전 리뷰는 유지했다.
  • Phase 2: 생성·실제 본문 수정·상세 단일 언어 예약·권한/마스킹 기준 충족. 자체 추가 수정 사항 없음. Phase 1 결함의 상세 영향은 P1-R4에서 처리한다.
  • Phase 3: 목록·채널 홈·추천·팔로잉 저장 번역 및 비예약 기준 충족. 신규 Task 없음.
  • 실행 제약: 사용자 요청대로 테스트·컴파일·서버·HTTP·Papago·DB 재현은 실행하지 않았다. 기존 XML 성공 기록은 과거 실행 증거로만 읽었다.
  • 검증 한계: 실제 캐시 저장 경합은 새 Task의 RED로 재현할 예정이다. 한·영·일 전체 생성→워커 통합 검증과 실제 MySQL/Papago/HTTP는 기존 테스트 서버 수동 확인 범위로 남겼다. 구성요소 테스트 통과를 실환경 전체 흐름 통과로 확대하지 않는다.
  • 문서 검증: ./gradlew tasks --all — exit 0(task 목록 조회만 수행, 테스트·컴파일 미실행). git diff --check와 문서 링크/공백 검사 통과. 리뷰 전후 source/test/DDL 32개 파일 지문이 같음을 확인했다.

P1-R4 문서 동기화 · 2026-09-10

  • 완료된 RED/GREEN·P1/P2 영향 회귀·spec/quality 리뷰를 P1-R4 실행 기록과 리뷰 문서 두 곳에 누적했다. 기존 정적 리뷰 지문과 과거 기록은 당시 증거로 보존하며 실제 MySQL 수동 확인 네 항목은 미완료로 추가했다.
  • 문서 검증: ./gradlew tasks --all, exit 0, BUILD SUCCESSFUL (2초). test, ktlintCheck task를 확인했다. rg로 P1-R4 Goal 및 RED/GREEN/REFACTOR 완료 체크, 실행 시간·리뷰 출처·실환경 검증 보류 표기를 대조했다.
  • 이번 작업은 지정된 계획·리뷰 문서 3개만 수정했다. PRD·source·test·schema 변경이나 제품 테스트 재실행은 하지 않았다.

변경 관리

기술 선택은 PRD DEC-009~010, 제품 결정은 DEC-001~008을 따른다. 후속 요구사항은 PRD 결정 기록 → 요구사항/API 계약 → 이 계획의 파일·검증·체크박스 순서로 갱신한다. 미래 구현에서는 각 Task 아래에 실행일·무엇/왜/어떻게·정확한 명령·결과·남은 항목을 누적한다. 새 테스트 클래스를 만들 때는 이 문서의 focused 실행 예시가 실제 클래스와 일치하는지도 갱신한다.