Skip to content

[Feat] 로드맵 생성 데이터 다국어 지원 기능 구현#215

Merged
jeong1112 merged 13 commits intodevelopfrom
feat/multilingual
Mar 27, 2026
Merged

[Feat] 로드맵 생성 데이터 다국어 지원 기능 구현#215
jeong1112 merged 13 commits intodevelopfrom
feat/multilingual

Conversation

@jeong1112
Copy link
Copy Markdown
Collaborator

@jeong1112 jeong1112 commented Mar 27, 2026

Related issue 🛠

Work Description 📝

  • 로드맵 데이터를 다국어 지원 가능하게 구현했습니다.
  • 온보딩 과정에서의 데이터와 다르게, 로드맵 데이터는 요청에 의해 동적으로 생성되므로 다른 구조로 구현했습니다.
  • 로드맵 데이터 생성은 기본적으로 프롬프트 내용이 한글이라 한글로 생성하되, 로드맵 생성 완료와 동시에 비동기 • 병렬적으로 번역 API를 통해 데이터를 번역하여 번역 DB에 저장합니다.
  • Phase, PhaseAction, ActionItem 조회 시 요청 헤더에 담긴 언어에 따라 다른 언어로 조회될 수 있도록 조회 로직을 수정하였습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 로드맵, 단계, 작업 가이드, 액션 아이템에 대한 다국어 번역 파이프라인 추가 (Google 번역 연동)
    • 로드맵 생성 시 자동으로 여러 언어로 번역해 저장 및 제공
  • 리팩토링

    • 로드맵 생성 흐름을 동기화하고 번역 처리를 생성 흐름에 통합
    • AI 시스템 프롬프트 생성 방식 개선
  • 문서/설정

    • 번역 API 설정 추가 및 관련 파일 무시 규칙 업데이트

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 664f9081-e6d5-4ce8-ac19-431d32a9a47b

📥 Commits

Reviewing files that changed from the base of the PR and between e1d9942 and 186eb23.

📒 Files selected for processing (3)
  • src/main/java/org/sopt/kareer/global/external/google/exception/GoogleTranslationErrorCode.java
  • src/main/java/org/sopt/kareer/global/external/google/exception/GoogleTranslationException.java
  • src/main/java/org/sopt/kareer/global/external/google/service/GoogleTranslationService.java

📝 Walkthrough

Walkthrough

로드맵 생성에 다국어 번역을 도입했습니다. 영문/베트남어/중국어 대상으로 Google Translate를 호출해 번역을 생성·저장하고, 기존 서비스들을 로케일 기반 조회로 업데이트했으며 비동기 RoadmapAsyncService를 제거했습니다.

Changes

Cohort / File(s) Summary
컨트롤러 변경
src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java
RoadmapAsyncService 제거, RoadmapTranslationService 주입 및 generateRoadmap에서 번역 호출 추가.
퍼시스트 및 서비스 흐름
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java, src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java
로드맵 저장 메서드가 RoadmapTranslationTarget을 반환하도록 변경하여 번역 메타데이터를 캡처하고 반환 흐름에 반영.
번역 페이로드
src/main/java/org/sopt/kareer/domain/roadmap/dto/translation/RoadmapTranslationTarget.java
로드맵 계층 구조를 표현하는 새로운 record 타입(Phase/Action/Guideline/Mistake/ActionItem) 추가.
번역 엔티티
src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseTranslation.java, PhaseActionTranslation.java, PhaseActionGuidelineTranslation.java, PhaseActionMistakeTranslation.java, ActionItemTranslation.java
번역 저장용 JPA 엔티티 5개 추가(언어 필드, 고유 제약, ManyToOne 관계, 팩토리 메서드).
번역 저장소(Repository)
src/main/java/org/sopt/kareer/domain/roadmap/repository/...
위 엔티티들에 대응하는 Spring Data JPA 리포지토리 5개 추가(언어별 조회/삭제, 일부 JPQL 포함).
번역 서비스
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationService.java, RoadmapTranslationPersistService.java
다중 언어 비동기 번역 조율 서비스 및 번역 결과를 DB에 일괄 교체 저장하는 퍼시스트 서비스 추가.
Google 번역 통합
src/main/java/org/sopt/kareer/global/external/google/service/GoogleTranslationService.java, .../google/exception/*
Google Translate 호출 구현 추가(텍스트 평탄화→API 호출→재조립). 예외/에러코드 및 CustomException 래핑 추가.
기존 서비스 로케일 적용
src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java, PhaseActionService.java, ActionItemService.java
LocaleContextHolder 기반 현재 언어 조회를 사용해 번역 레포지토리에서 내용을 가져와 응답에 적용하도록 변경.
비동기 서비스 제거 및 테스트 수정
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapAsyncService.java (삭제), src/test/.../ControllerTestSupport.java
RoadmapAsyncService 클래스 제거 및 테스트 지원에서 해당 @MockBean 제거.
AI 프롬프트/외부 AI 호출 변경
src/main/java/org/sopt/kareer/global/external/ai/prompt/..., src/main/java/org/sopt/kareer/global/external/ai/service/OpenAiService.java
프롬프트 상수들을 템플릿+빌더 메서드(buildSystemPrompt())로 전환, Roadmap 프롬프트에서 출력 언어 요구를 한국어로 변경.
설정/빌드/기타
src/main/resources/application.yml, build.gradle, .gitignore
Google Translate 설정(key/url) 추가, Prometheus dep 포맷 변경(공백), .gitignoreCLAUDE.md 추가.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant MemberController
    participant RoadMapService
    participant RoadMapPersistService
    participant Database
    participant RoadmapTranslationService
    participant GoogleTranslationService
    participant RoadmapTranslationPersistService

    Client->>MemberController: POST /roadmap/generate
    MemberController->>RoadMapService: createRoadmap(memberId)
    RoadMapService->>RoadMapPersistService: saveRoadMap(member, response)
    RoadMapPersistService->>Database: save phases/actions/guidelines/mistakes/actionItems
    RoadMapPersistService-->>RoadMapService: RoadmapTranslationTarget
    RoadMapService-->>MemberController: RoadmapTranslationTarget

    par 번역 비동기 처리
        MemberController->>RoadmapTranslationService: translateAllLanguages(target)
        loop 각 언어 (en, vi, zh-CN)
            RoadmapTranslationService->>GoogleTranslationService: translate(target, language)
            GoogleTranslationService->>GoogleTranslationService: call Google Translate API
            GoogleTranslationService-->>RoadmapTranslationService: translated Target
            RoadmapTranslationService->>RoadmapTranslationPersistService: saveTranslations(translated, language)
            RoadmapTranslationPersistService->>Database: delete old translations, insert new translations
        end
    end

    MemberController-->>Client: HTTP 200
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60분

Possibly related PRs

Suggested reviewers

  • eraser502
  • hyomee2

Poem

"🐰 번역 깡총깡총, 텍스트 모아 번역해요,
키를 눌러 영어·베트남·중국어로 춤춰요.
DB에 쏙쏙 저장하고, 서비스는 웃음,
다국어 로드맵, 모두 함께 홧팅! 🌈"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '[Feat] 로드맵 생성 데이터 다국어 지원 기능 구현'은 변경 사항의 주요 내용인 로드맵 생성 데이터의 다국어 지원 기능을 명확하고 간결하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR은 #214 이슈의 '로드맵 생성 기능에서 다국어 지원 추가' 요구사항을 충족합니다. 한국어 기본 생성, 비동기 번역 API 호출, 번역 데이터 DB 저장, 언어별 데이터 조회 기능이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 프롬프트 수정(한국어 명시), RoadmapAsyncService 제거 등의 변경사항들은 모두 로드맵 다국어 지원 기능 구현과 직접 관련되어 있으며 범위 내에 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multilingual

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java (1)

45-49: ⚠️ Potential issue | 🟡 Minor

불필요한 null 체크 (데드 코드)

orElseThrow()가 호출되면 visanull일 수 없습니다. 45번 라인에서 예외를 던지므로 47-49번 라인의 null 체크는 절대 true가 될 수 없는 데드 코드입니다.

🐛 제안된 수정
 MemberVisa visa = memberVisaRepository.findActiveByMemberId(memberId).orElseThrow(() -> new MemberException(MemberErrorCode.VISA_NOT_FOUND));

-List<Document> visaDocs = (visa == null)
-        ? List.of()
-        : requiredRetriever.retrieveVisaAll(visa);
+List<Document> visaDocs = requiredRetriever.retrieveVisaAll(visa);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java`
around lines 45 - 49, The null-check after calling
memberVisaRepository.findActiveByMemberId(...).orElseThrow(...) is dead code
because orElseThrow guarantees visa is non-null; remove the ternary/conditional
using visa and instead directly call requiredRetriever.retrieveVisaAll(visa)
after obtaining MemberVisa visa (reference symbols: MemberVisa,
memberVisaRepository.findActiveByMemberId, requiredRetriever.retrieveVisaAll,
variable visa) so the code is simplified and the unreachable branch is
eliminated.
🧹 Nitpick comments (7)
src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseActionMistakeTranslation.java (1)

23-24: 언어 태그 컬럼 길이가 다국어 확장에 다소 타이트합니다.

Line 23의 length = 10은 일부 locale tag 확장 케이스에서 부족할 수 있습니다. 스키마를 조금 넉넉히 잡아두는 편이 안전합니다.

🔧 제안 변경안
-    `@Column`(nullable = false, length = 10)
+    `@Column`(nullable = false, length = 35)
     private String language;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseActionMistakeTranslation.java`
around lines 23 - 24, The language column on PhaseActionMistakeTranslation is
too short (length = 10) for some RFC-style locale tags; update the `@Column` on
the field named "language" in class PhaseActionMistakeTranslation to use a
larger length (e.g., length = 35) to safely accommodate extended locale tags
while keeping nullable = false.
src/main/java/org/sopt/kareer/global/external/ai/prompt/JobPostingRecommendPrompt.java (1)

33-35: 불필요한 .formatted() 호출

템플릿 문자열에 포맷 지정자가 없으므로 .formatted() 호출이 불필요합니다. 단순히 상수를 반환하거나, 향후 동적 값 삽입을 위해 의도적으로 유지하는 것이라면 주석으로 명시하는 것이 좋습니다.

♻️ 제안된 수정
 public static String buildSystemPrompt() {
-    return JOB_POSTING_SYSTEM_PROMPT_TEMPLATE.formatted();
+    return JOB_POSTING_SYSTEM_PROMPT_TEMPLATE;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/kareer/global/external/ai/prompt/JobPostingRecommendPrompt.java`
around lines 33 - 35, The method buildSystemPrompt unnecessarily calls
.formatted() on JOB_POSTING_SYSTEM_PROMPT_TEMPLATE which has no format
specifiers; remove the .formatted() and return
JOB_POSTING_SYSTEM_PROMPT_TEMPLATE directly in buildSystemPrompt(), or if you
intentionally keep .formatted() for future interpolation, add a clarifying
comment above buildSystemPrompt() referencing JOB_POSTING_SYSTEM_PROMPT_TEMPLATE
to indicate that intent.
src/main/java/org/sopt/kareer/domain/roadmap/service/ActionItemService.java (1)

69-69: 불필요한 변수 재할당입니다.

translationMap은 이미 effectively final이므로 finalTranslationMap으로 복사할 필요가 없습니다.

♻️ 불필요한 변수 제거
-        final Map<Long, ActionItemTranslation> finalTranslationMap = translationMap;
-
         List<ActionItemResponse> visaActionItems = activeActionItems.stream()
                 .filter(item -> item.getActionsType() == ActionItemType.VISA)
-                .map(item -> toResponse(item, finalTranslationMap))
+                .map(item -> toResponse(item, translationMap))
                 .toList();

         List<ActionItemResponse> careerActionItems = activeActionItems.stream()
                 .filter(item -> item.getActionsType() == ActionItemType.CAREER)
-                .map(item -> toResponse(item, finalTranslationMap))
+                .map(item -> toResponse(item, translationMap))
                 .toList();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/ActionItemService.java`
at line 69, Remove the unnecessary reassignment to finalTranslationMap in
ActionItemService: use the existing effectively-final translationMap directly
wherever finalTranslationMap is referenced, delete the finalTranslationMap
declaration/assignment, and ensure no subsequent code relies on that duplicate
variable name (update references to translationMap if needed).
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java (1)

62-84: 개별 save() 호출을 batch 저장으로 최적화할 수 있습니다.

현재 PhaseActionGuideline, PhaseActionMistake, ActionItem을 반복문 내에서 개별적으로 저장하고 있습니다. 엔티티 수가 많아지면 N+1 형태의 INSERT 쿼리가 발생할 수 있습니다.

♻️ saveAll()을 사용한 batch 저장 제안

각 PhaseAction별로 guideline, mistake, actionItem 리스트를 모아서 한 번에 저장하는 방식을 고려해 보세요:

// 예시: guidelines 저장 최적화
List<PhaseActionGuideline> guidelinesToSave = new ArrayList<>();
for (String g : Optional.ofNullable(phaseActionPlan.guideline()).orElse(Collections.emptyList())) {
    guidelinesToSave.add(PhaseActionGuideline.create(g, savedPhaseAction));
}
List<PhaseActionGuideline> savedGuidelines = phaseActionGuidelineRepository.saveAll(guidelinesToSave);
List<RoadmapTranslationTarget.GuidelineTarget> guidelineTargets = savedGuidelines.stream()
    .map(saved -> new RoadmapTranslationTarget.GuidelineTarget(saved.getId(), saved.getContent()))
    .toList();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java`
around lines 62 - 84, Collect the entities for each PhaseAction before
persisting and replace the per-item repository.save calls with batch saves using
saveAll: build lists of PhaseActionGuideline, PhaseActionMistake, and ActionItem
(using PhaseActionGuideline.create(...), PhaseActionMistake.create(...),
ActionItem.create(...)) for the current savedPhaseAction/member, call
phaseActionGuidelineRepository.saveAll(...),
phaseActionMistakeRepository.saveAll(...), and
actionItemRepository.saveAll(...), then map the returned saved entities to
RoadmapTranslationTarget.GuidelineTarget, MistakeTarget, and ActionItemTarget
(using saved.getId() and the original content/title) so you still populate
guidelineTargets, mistakeTargets, and actionItemTargets without per-item
INSERTs.
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationService.java (1)

38-38: CompletableFuture.allOf()의 결과가 사용되지 않습니다.

현재 fire-and-forget 패턴으로 동작하며, 이것이 의도된 것이라면 괜찮습니다. 하지만 코드의 의도를 명확히 하기 위해 결과를 명시적으로 무시하거나 주석을 추가하는 것이 좋습니다.

♻️ 의도를 명확히 하는 방법
-        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
+        // Fire-and-forget: 비동기 번역은 응답에 영향을 주지 않음
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

또는 완료를 기다려야 한다면:

-        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationService.java`
at line 38, The call to CompletableFuture.allOf(futures.toArray(new
CompletableFuture[0])) in RoadmapTranslationService is currently fire-and-forget
and its result is discarded; either explicitly handle it (e.g., assign to a
CompletableFuture variable and call join()/get() or use
.thenApply/.exceptionally to handle completion/errors) or make the intent
explicit by adding a comment or using CompletableFuture.allOf(...).join() to
block until all futures in the futures collection complete; locate the call in
class RoadmapTranslationService where the futures variable is used and update it
accordingly to either wait for completion or clearly document the intentional
non-blocking behavior.
src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java (1)

133-133: 불필요한 변수 재할당입니다.

translationMap은 이미 effectively final입니다.

♻️ 불필요한 변수 제거
-        final Map<Long, PhaseActionTranslation> finalTranslationMap = translationMap;
         List<HomePhaseDetailResponse.HomePhaseActionResponse> actionResponses = phaseActions.stream()
                 .map(pa -> {
-                    PhaseActionTranslation t = finalTranslationMap.get(pa.getId());
+                    PhaseActionTranslation t = translationMap.get(pa.getId());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java` at
line 133, The assignment creating finalTranslationMap from translationMap in
PhaseService is unnecessary because translationMap is already effectively final;
remove the redundant finalTranslationMap declaration and update subsequent
references to use translationMap directly (search for finalTranslationMap usages
in the PhaseService method and replace them with translationMap).
src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationPersistService.java (1)

47-74: 대량 저장은 saveAll/batching으로 묶는 편이 좋습니다.

지금은 phase/action/guideline/mistake/action-item마다 개별 save()를 호출해서 번역 수만큼 insert round-trip이 생깁니다. 언어별 저장을 병렬로 태우는 흐름이면 이 메서드가 DB write hot path가 되기 쉬워서, 타입별로 엔티티를 모아 saveAll()과 JDBC batching을 같이 쓰는 쪽을 권합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationPersistService.java`
around lines 47 - 74, The loop currently calls repository.save() for each
translation (PhaseTranslation, PhaseActionTranslation,
PhaseActionGuidelineTranslation, PhaseActionMistakeTranslation,
ActionItemTranslation), causing many DB round-trips; instead, accumulate
translations into typed lists (e.g., List<PhaseTranslation> phaseTranslations,
List<PhaseActionTranslation> actionTranslations, etc.) while iterating over
target.phases()/actions(), then call
phaseTranslationRepository.saveAll(phaseTranslations),
phaseActionTranslationRepository.saveAll(actionTranslations),
guidelineTranslationRepository.saveAll(...),
mistakeTranslationRepository.saveAll(...), and
actionItemTranslationRepository.saveAll(...) once each; ensure JDBC batching is
enabled in config (hibernate.jdbc.batch_size) so these saveAll calls are
executed as batched inserts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/sopt/kareer/domain/roadmap/service/ActionItemService.java`:
- Around line 62-67: The code uses
LocaleContextHolder.getLocale().toLanguageTag() which yields BCP-47 tags (e.g.,
"en-US") that don't match the short codes in TARGET_LANGUAGES, causing
actionItemTranslationRepository.findAllByActionItem_IdInAndLanguage(...) to
return nothing; change how you derive the language before calling that
repository by normalizing the locale (e.g., use locale.getLanguage() for most
languages and special-case Chinese to "zh-CN" or other mappings used by
RoadmapTranslationService.TARGET_LANGUAGES), add a small helper like
normalizeLanguage(Locale) and replace the current language assignment so
actionItemTranslationRepository.findAllByActionItem_IdInAndLanguage(itemIds,
language) queries the expected codes.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseActionService.java`:
- Around line 61-65: The code in PhaseActionService uses
LocaleContextHolder.getLocale().toLanguageTag() which produces tags like "en-US"
that don't match DB keys (which are "en", "vi", "zh-CN"); create and use a
common normalizer (e.g., LocaleUtils.normalizeLanguageCode(Locale)) that returns
locale.getLanguage() by default and maps "zh" to "zh-CN", then replace
LocaleContextHolder.getLocale().toLanguageTag() with
LocaleUtils.normalizeLanguageCode(LocaleContextHolder.getLocale()) in the call
to
phaseActionTranslationRepository.findByPhaseAction_IdAndLanguage(phaseActionId,
language) so translations resolve consistently (same fix applied as in
ActionItemService).

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java`:
- Around line 148-150: The currentLanguage() helper returns
LocaleContextHolder.getLocale().toLanguageTag() (e.g., "en-US") which doesn't
match DB keys ("en","vi","zh-CN"); change it to a shared utility (e.g.,
LocaleUtils.getNormalizedLanguage()) and update PhaseService, ActionItemService,
and PhaseActionService to use it: implement getNormalizedLanguage() to return
locale.getLanguage() for most languages, and special-case Chinese to return
"zh-CN" when locale.getCountry() equals "CN" (or other mappings your DB uses),
ensuring returned values exactly match the stored translation keys.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationPersistService.java`:
- Around line 34-45: The deletion scope is too
narrow—actionItemTranslationRepository.deleteAllByActionItem_IdInAndLanguage
currently deletes only actionItemIds present in the new target, leaving orphaned
translations when the snapshot shrinks; change the cleanup to delete action-item
translations by the broader actionIds (use actionIds to remove all action items
under those actions) or clear the roadmap/parent scope first, i.e. replace the
actionItem delete call to use actionIds (or add an additional
deleteAllByActionItem_PhaseAction_IdInAndLanguage using actionIds) and ensure
the sequence with
guidelineTranslationRepository.deleteAllByGuideline_PhaseAction_IdInAndLanguage,
mistakeTranslationRepository.deleteAllByMistake_PhaseAction_IdInAndLanguage,
phaseActionTranslationRepository.deleteAllByPhaseAction_IdInAndLanguage and
phaseTranslationRepository.deleteAllByPhase_IdInAndLanguage empties the parent
scope before re-inserting translations.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationService.java`:
- Around line 27-38: The async translation tasks started from
RoadmapTranslationService (TARGET_LANGUAGES ->
googleTranslationService.translate -> persistService.saveTranslations) must run
after the parent transaction (e.g., saveRoadMap) commits to avoid
EntityNotFoundException from getReferenceById; fix by moving the invocation out
of the transactional boundary — either register a TransactionSynchronization in
the caller (after saveRoadMap returns) to call the service method that schedules
the CompletableFutures, or convert the scheduling to an application event
handled by a listener annotated with `@TransactionalEventListener`(phase =
AFTER_COMMIT) so translateAllLanguages/translation scheduling only runs after
commit.

In `@src/main/java/org/sopt/kareer/global/external/ai/prompt/RoadmapPrompt.java`:
- Around line 56-57: In RoadmapPrompt, the Korean language directive currently
risks forcing enum-like keys (e.g., status/type/actionsType) into Korean; change
the instruction so it explicitly limits Korean output to
natural-language/content fields only and specify that structural/enum
identifiers (status, type, actionsType, etc.) must remain in English; locate the
Korean-language lines in the RoadmapPrompt class and reword them to say "Only
natural-language content fields should be written in Korean; all structural keys
and enum-like fields (status, type, actionsType, etc.) must be in English,"
ensuring the prompt clearly separates natural-language values from
enum/identifier fields.

In
`@src/main/java/org/sopt/kareer/global/external/google/service/GoogleTranslationService.java`:
- Around line 73-107: The reassemble method risks IndexOutOfBoundsException
because it repeatedly calls translated.get(idx[0]++) without validating bounds;
add a defensive check by creating and using a helper like safeGet(List<String>
translated, int[] idx) inside GoogleTranslationService.reassemble that logs a
warning when idx[0] >= translated.size() and returns a sensible fallback (empty
string or original text) instead of calling get, or alternatively validate total
expected count against translated.size() before the loops (use method names
reassemble and safeGet to locate code and update all translated.get(idx[0]++)
occurrences to call the safe accessor).
- Around line 61-70: The code in GoogleTranslationService that calls
restClient.post(...).retrieve().body(Map.class) assumes the response structure
and can cause NPEs; update the translate call to defensively check the returned
Map for null and expected keys before casting (inspect the variables response,
data, translations and the "translatedText" value), handle HTTP errors from the
REST call (e.g., map non-2xx to a custom exception or wrap the error) and return
a safe fallback (empty list) or throw a clear service exception when keys are
missing or types are unexpected; ensure the method documents/propagates errors
appropriately rather than allowing NullPointerException.

---

Outside diff comments:
In `@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java`:
- Around line 45-49: The null-check after calling
memberVisaRepository.findActiveByMemberId(...).orElseThrow(...) is dead code
because orElseThrow guarantees visa is non-null; remove the ternary/conditional
using visa and instead directly call requiredRetriever.retrieveVisaAll(visa)
after obtaining MemberVisa visa (reference symbols: MemberVisa,
memberVisaRepository.findActiveByMemberId, requiredRetriever.retrieveVisaAll,
variable visa) so the code is simplified and the unreachable branch is
eliminated.

---

Nitpick comments:
In
`@src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseActionMistakeTranslation.java`:
- Around line 23-24: The language column on PhaseActionMistakeTranslation is too
short (length = 10) for some RFC-style locale tags; update the `@Column` on the
field named "language" in class PhaseActionMistakeTranslation to use a larger
length (e.g., length = 35) to safely accommodate extended locale tags while
keeping nullable = false.

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/ActionItemService.java`:
- Line 69: Remove the unnecessary reassignment to finalTranslationMap in
ActionItemService: use the existing effectively-final translationMap directly
wherever finalTranslationMap is referenced, delete the finalTranslationMap
declaration/assignment, and ensure no subsequent code relies on that duplicate
variable name (update references to translationMap if needed).

In `@src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java`:
- Line 133: The assignment creating finalTranslationMap from translationMap in
PhaseService is unnecessary because translationMap is already effectively final;
remove the redundant finalTranslationMap declaration and update subsequent
references to use translationMap directly (search for finalTranslationMap usages
in the PhaseService method and replace them with translationMap).

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java`:
- Around line 62-84: Collect the entities for each PhaseAction before persisting
and replace the per-item repository.save calls with batch saves using saveAll:
build lists of PhaseActionGuideline, PhaseActionMistake, and ActionItem (using
PhaseActionGuideline.create(...), PhaseActionMistake.create(...),
ActionItem.create(...)) for the current savedPhaseAction/member, call
phaseActionGuidelineRepository.saveAll(...),
phaseActionMistakeRepository.saveAll(...), and
actionItemRepository.saveAll(...), then map the returned saved entities to
RoadmapTranslationTarget.GuidelineTarget, MistakeTarget, and ActionItemTarget
(using saved.getId() and the original content/title) so you still populate
guidelineTargets, mistakeTargets, and actionItemTargets without per-item
INSERTs.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationPersistService.java`:
- Around line 47-74: The loop currently calls repository.save() for each
translation (PhaseTranslation, PhaseActionTranslation,
PhaseActionGuidelineTranslation, PhaseActionMistakeTranslation,
ActionItemTranslation), causing many DB round-trips; instead, accumulate
translations into typed lists (e.g., List<PhaseTranslation> phaseTranslations,
List<PhaseActionTranslation> actionTranslations, etc.) while iterating over
target.phases()/actions(), then call
phaseTranslationRepository.saveAll(phaseTranslations),
phaseActionTranslationRepository.saveAll(actionTranslations),
guidelineTranslationRepository.saveAll(...),
mistakeTranslationRepository.saveAll(...), and
actionItemTranslationRepository.saveAll(...) once each; ensure JDBC batching is
enabled in config (hibernate.jdbc.batch_size) so these saveAll calls are
executed as batched inserts.

In
`@src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationService.java`:
- Line 38: The call to CompletableFuture.allOf(futures.toArray(new
CompletableFuture[0])) in RoadmapTranslationService is currently fire-and-forget
and its result is discarded; either explicitly handle it (e.g., assign to a
CompletableFuture variable and call join()/get() or use
.thenApply/.exceptionally to handle completion/errors) or make the intent
explicit by adding a comment or using CompletableFuture.allOf(...).join() to
block until all futures in the futures collection complete; locate the call in
class RoadmapTranslationService where the futures variable is used and update it
accordingly to either wait for completion or clearly document the intentional
non-blocking behavior.

In
`@src/main/java/org/sopt/kareer/global/external/ai/prompt/JobPostingRecommendPrompt.java`:
- Around line 33-35: The method buildSystemPrompt unnecessarily calls
.formatted() on JOB_POSTING_SYSTEM_PROMPT_TEMPLATE which has no format
specifiers; remove the .formatted() and return
JOB_POSTING_SYSTEM_PROMPT_TEMPLATE directly in buildSystemPrompt(), or if you
intentionally keep .formatted() for future interpolation, add a clarifying
comment above buildSystemPrompt() referencing JOB_POSTING_SYSTEM_PROMPT_TEMPLATE
to indicate that intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 447274d5-dd24-46fd-a44c-1ade792d0404

📥 Commits

Reviewing files that changed from the base of the PR and between 1776c70 and e1d9942.

📒 Files selected for processing (29)
  • .gitignore
  • build.gradle
  • src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java
  • src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java
  • src/main/java/org/sopt/kareer/domain/roadmap/dto/translation/RoadmapTranslationTarget.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/ActionItemTranslation.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseActionGuidelineTranslation.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseActionMistakeTranslation.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseActionTranslation.java
  • src/main/java/org/sopt/kareer/domain/roadmap/entity/PhaseTranslation.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/ActionItemTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionGuidelineTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionMistakeTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseActionTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/repository/PhaseTranslationRepository.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/ActionItemService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseActionService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/PhaseService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapPersistService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadMapService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapAsyncService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationPersistService.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapTranslationService.java
  • src/main/java/org/sopt/kareer/global/external/ai/prompt/JobPostingRecommendPrompt.java
  • src/main/java/org/sopt/kareer/global/external/ai/prompt/RoadmapPrompt.java
  • src/main/java/org/sopt/kareer/global/external/ai/service/OpenAiService.java
  • src/main/java/org/sopt/kareer/global/external/google/service/GoogleTranslationService.java
  • src/main/resources/application.yml
  • src/test/java/org/sopt/kareer/support/ControllerTestSupport.java
💤 Files with no reviewable changes (2)
  • src/test/java/org/sopt/kareer/support/ControllerTestSupport.java
  • src/main/java/org/sopt/kareer/domain/roadmap/service/RoadmapAsyncService.java

@jeong1112 jeong1112 merged commit 3de04b4 into develop Mar 27, 2026
1 of 2 checks passed
@jeong1112 jeong1112 deleted the feat/multilingual branch March 27, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 로드맵, 채용 공고 추천 다국어 지원 기능 구현

1 participant