Skip to content

[Feat] 다국어 처리 구조 구현 #212

Merged
eraser502 merged 11 commits intodevelopfrom
feat/#205-i18n-support
Mar 26, 2026
Merged

[Feat] 다국어 처리 구조 구현 #212
eraser502 merged 11 commits intodevelopfrom
feat/#205-i18n-support

Conversation

@eraser502
Copy link
Copy Markdown
Collaborator

@eraser502 eraser502 commented Mar 26, 2026

Related issue 🛠

Work Description 📝

  • localized_onboard_category 테이블에 온보딩/프로필에서 사용하는 모든 코드(국가, 전공, 학교, 학위, 영어 수준, 관심분야 등)를 저장하고, 자식 테이블인 localized_onboard_category_translation에서 언어별 라벨을 관리해요.

  • 서비스 계층(LocalizedOnboardQueryService)에서 요청의 Locale을 읽어
    언어 태그 → 언어 코드 → 기본값(en) 순으로 라벨을 조회하며,
    라벨이 없을 경우 첫 번역값 또는 빈 문자열을 반환해요.

  • 회원 정보는 DB에 항상 코드만 저장하고, 조회 시에만 해당 서비스를 통해 라벨로 변환하여 응답 DTO를 구성하는 구조에요.

ScreenShots 📷

To Reviewers 📢

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 요청 언어 헤더(X-Preferred-Language) 기반 국제화와 로케일 처리 추가
    • 온보딩 항목(대학/국가/전공/분야)이 코드+레이블 형태로 지역화되어 제공
  • 개선사항

    • 회원 온보딩·프로필 입력을 코드 기반 필드로 전환
    • 회원 정보·마이페이지에서 지역화된 레이블로 표시
    • 정적 상수 목록 제거 및 데이터 기반의 지역화 항목 사용
    • 비자 포인트 입력/검증 제거

@eraser502 eraser502 requested review from hyomee2 and jeong1112 March 26, 2026 19:00
@eraser502 eraser502 self-assigned this Mar 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Warning

Rate limit exceeded

@eraser502 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0f4cbed0-eb55-420d-a7f7-1b982a3c52c6

📥 Commits

Reviewing files that changed from the base of the PR and between fc9ffd9 and 0f21276.

📒 Files selected for processing (2)
  • src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java
  • src/main/java/org/sopt/kareer/domain/member/service/MemberService.java
📝 Walkthrough

Walkthrough

데이터베이스 기반 다국어 온보드 목록과 로케일 해석을 추가하고, 멤버 도메인을 enum 기반 필드에서 code(문자열) 기반 필드로 전환했습니다. 정적 상수 클래스들이 제거되고 관련 DTO/서비스/컨트롤러/설정이 현지화 로직으로 대체되었습니다.

Changes

Cohort / File(s) Summary
다국어 쿼리 서비스 및 저장소
src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java, src/main/java/org/sopt/kareer/domain/member/repository/LocalizedOnboardCategoryRepository.java
새 서비스와 리포지토리 추가: DB에서 카테고리+번역을 조회하고 요청 로케일에 따라 label을 결정하는 API(getFields/getMajors/getUniversities/getCountries)와 resolveLabelByCode 제공.
다국어 카테고리 엔티티
src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java, src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java
카테고리 및 번역을 저장하는 JPA 엔티티 추가(일대다, 생성/수정 타임스탬프, 번역 추가/업데이트 로직 포함).
로케일 처리 구성
src/main/java/org/sopt/kareer/global/config/InternationalizationConfig.java, src/main/java/org/sopt/kareer/global/config/locale/KareerLocaleResolver.java
KareerLocaleResolver 추가: X-Preferred-Language 헤더/요청 속성 기반 로케일 결정, LocaleResolver 빈 등록.
컨트롤러/서비스 연동
src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java, src/main/java/org/sopt/kareer/domain/member/service/MemberService.java
Controller가 LocalizedOnboardQueryService 주입 및 onboard 엔드포인트를 서비스로 대체. MemberService는 코드값을 레이블로 변환해 DTO에 주입하도록 변경.
멤버 도메인 리팩토링
src/main/java/org/sopt/kareer/domain/member/entity/Member.java, src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java
Member 엔티티의 enum 필드들을 code 기반 문자열 필드로 변경(여러 코드 필드 추가) 및 updateInfoV2 추가. MemberVisa에서 visaPoint 제거.
요청/커맨드 DTO 변경
src/main/java/org/sopt/kareer/domain/member/dto/request/MemberOnboardV2Request.java, src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java, src/main/java/org/sopt/kareer/domain/member/service/dto/request/MypageCommand.java
클라이언트로부터 받는 필드를 enum/값에서 *Code 문자열로 변경(universityCode, countryCode, primaryMajorCode 등). visaPoint 제거.
응답 DTO 변경
src/main/java/org/sopt/kareer/domain/member/dto/response/LocalizedItemResponse.java, src/main/java/org/sopt/kareer/domain/member/dto/response/Onboard*.java, src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java, src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java
온보드 응답 요소 타입이 List<String>List<LocalizedItemResponse(code,label)>로 변경. Member/Mypage 응답은 레이블 파라미터를 받도록 팩토리 시그니처 변경.
상수 클래스 제거
src/main/java/org/sopt/kareer/domain/member/entity/constants/University.java, .../Major.java, .../Field.java
정적 하드코딩 리스트(University, Major, Field) 클래스 전부 삭제.
국제화 관련 설정(문서/스웨거)
src/main/java/org/sopt/kareer/global/config/swagger/SwaggerConfig.java
모든 OpenAPI operation에 X-Preferred-Language 헤더 파라미터를 자동 추가하도록 커스터마이즈.
외부/AI 빌더 변경
src/main/java/org/sopt/kareer/global/external/ai/builder/.../MemberContextBuilder.java, .../PolicyQueryBuilder.java
AI 컨텍스트/쿼리 빌더가 Member의 code 필드를 사용하도록 변경(visaPoint 제거 반영 포함).
테스트 및 기타 변경
src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java, src/main/java/.../enums/*.java
테스트가 code 기반 값으로 업데이트됨. Degree/EnglishLevel description 문자열 포맷 변경, 새 enum LocalizedOnboardCategoryType 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MemberController
    participant LocalizedOnboardQueryService
    participant LocalizedOnboardCategoryRepository
    participant Database
    participant KareerLocaleResolver

    Client->>MemberController: GET /onboard/universities (X-Preferred-Language: ko)
    MemberController->>KareerLocaleResolver: resolveLocale(request)
    KareerLocaleResolver-->>MemberController: Locale("ko")
    MemberController->>LocalizedOnboardQueryService: getUniversities()
    LocalizedOnboardQueryService->>LocalizedOnboardCategoryRepository: findAllByTypeOrderByUseOrderAscIdAsc(UNIVERSITY)
    LocalizedOnboardCategoryRepository->>Database: query with translations (EntityGraph)
    Database-->>LocalizedOnboardCategoryRepository: entities + translations
    LocalizedOnboardCategoryRepository-->>LocalizedOnboardQueryService: List<LocalizedOnboardCategory>
    LocalizedOnboardQueryService->>LocalizedOnboardQueryService: resolveLabelByCode(type, code) per item (locale fallbacks)
    LocalizedOnboardQueryService-->>MemberController: OnboardUniversitiesResponse(List<LocalizedItemResponse>)
    MemberController-->>Client: 200 OK {universities: [{code,label}, ...]}
Loading
sequenceDiagram
    participant Client
    participant MemberService
    participant MemberRepository
    participant LocalizedOnboardQueryService
    participant MemberInfoResponse

    Client->>MemberService: GET /members/{id}
    MemberService->>MemberRepository: findById(id)
    MemberRepository-->>MemberService: Member (countryCode, primaryMajorCode, ...)
    MemberService->>LocalizedOnboardQueryService: resolveLabelByCode(COUNTRY, countryCode)
    LocalizedOnboardQueryService-->>MemberService: countryLabel
    MemberService->>LocalizedOnboardQueryService: resolveLabelByCode(MAJOR, primaryMajorCode)
    LocalizedOnboardQueryService-->>MemberService: majorLabel
    MemberService->>MemberInfoResponse: of(member, memberVisa, countryLabel, majorLabel, ...)
    MemberInfoResponse-->>MemberService: MemberInfoResponse
    MemberService-->>Client: 200 OK {...}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • hyomee2
  • jeong1112

Poem

🐇 안녕! 정적 리스트야, 안녕히 가렴
코드와 라벨이 춤추는 DB 정원에서
언어를 입은 단어들이 피어나는구나
X-Preferred-Language로 길을 찾고
토끼가 기뻐 뛰어오르네 🌱💫

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.39% 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 연결된 이슈 #205는 다국어 처리 구조 구현을 목표로 하며, PR의 모든 주요 변경사항(LocalizedOnboardCategory 엔티티, LocalizedOnboardQueryService, Locale 처리, 코드 기반 필드 변환 등)이 이를 충족합니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 다국어 처리 구조 구현의 범위 내에 있으며, visaPoint 제거도 계획 변경에 따른 것으로 범위 내 변경입니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#205-i18n-support

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.

@eraser502 eraser502 merged commit 1776c70 into develop Mar 26, 2026
1 check passed
@eraser502 eraser502 deleted the feat/#205-i18n-support branch March 26, 2026 19:13
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 (2)
src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java (1)

40-52: ⚠️ Potential issue | 🔴 Critical

visaPoint 제거가 불완전하여 테스트 컴파일이 실패합니다.

MemberVisa 엔티티에서 visaPoint 필드가 완전히 제거되었으나, 테스트 코드가 여전히 이를 참조하고 있습니다:

  • MemberVisaFixture.java 라인 17, 28, 39: .visaPoint(...) 빌더 메서드 호출 → 메서드 없음 (컴파일 실패)
  • MemberServiceTest.java 라인 101: .getVisaPoint() 게터 호출 → 메서드 없음 (컴파일 실패)
  • MemberOnboardRequest.java: 여전히 visaPoint 필드 존재하고 D10 비자에 대해 필수 검증
  • MemberService.java: createMemberVisa() 호출 시 visaPoint 전달하지 않음 (요청값 버려짐)

요청 DTO와 테스트 코드도 동일한 PR에서 함께 정리해야 합니다.

🤖 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/member/entity/MemberVisa.java` around
lines 40 - 52, MemberVisa still has visaPoint removed but tests and DTOs
reference it; remove all remaining visaPoint usages and align API/signatures:
delete the visaPoint field and any D10-specific validation from
MemberOnboardRequest, update MemberService.createMemberVisa call sites to match
the new signature (no visaPoint argument), and update test fixtures and
assertions—remove .visaPoint(...) builder calls in MemberVisaFixture, remove
.getVisaPoint() usages in MemberServiceTest and any assertions depending on it;
ensure MemberService and MemberOnboardRequest constructors/mapper logic no
longer expect or propagate visaPoint and rebuild tests to assert on existing
fields (e.g., visaType, visaStatus, visaStartDate, visaExpiredAt).
src/main/java/org/sopt/kareer/domain/member/service/MemberService.java (1)

121-150: ⚠️ Potential issue | 🟠 Major

preparationStatuses가 null이면 여기서 NPE가 발생합니다.

MemberOnboardV2Request에서 preparationStatuses는 검증 어노테이션이 없어 null이 가능한 반면, 라인 145의 String.join은 null이 전달되면 즉시 NullPointerException을 던집니다. 요청에서 이 필드를 빠뜨리기만 해도 온보딩이 500 에러로 실패합니다. 빈 리스트 기본값 처리 또는 @NotNull 검증을 추가해 주세요.

🛠️ null-safe 처리 예시
         String fieldOfInterest = String.join(",", request.fieldsOfInterests());
-        String preparationStatus = String.join(",", request.preparationStatuses());
+        String preparationStatus = request.preparationStatuses() == null
+                ? ""
+                : String.join(",", request.preparationStatuses());
🤖 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/member/service/MemberService.java`
around lines 121 - 150, onboardMemberV2 currently calls String.join on
request.preparationStatuses() (and similarly fieldsOfInterests), which throws
NPE if those lists are null; update MemberService.onboardMemberV2 to defensively
handle nulls by defaulting to empty lists before joining (e.g., replace
String.join(",", request.preparationStatuses()) with a null-safe expression that
uses an empty list or empty string when request.preparationStatuses() is null)
or alternatively add `@NotNull` to MemberOnboardV2Request.preparationStatuses()
(and fieldsOfInterests if desired) so the DTO is validated; ensure to update
both preparationStatuses and fieldsOfInterests usages and keep the rest of
member.updateInfoV2 call unchanged.
🧹 Nitpick comments (5)
src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java (1)

54-59: 동일 타입 파라미터가 너무 많아 호출 순서 실수에 취약합니다.

String 라벨 인자가 5개 연속이라 호출부에서 순서를 바꿔도 컴파일로 못 잡습니다. 라벨 묶음을 별도 record/VO로 감싸거나 builder에서 직접 채우는 쪽이 더 안전합니다.

🤖 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/member/dto/response/MypageResponse.java`
around lines 54 - 59, The static factory MypageResponse.of currently takes five
consecutive String label parameters (countryLabel, primaryMajorLabel,
universityLabel, degreeLabel, englishLevelLabel) which is error-prone; refactor
by introducing a compact value object/record (e.g., MypageLabels or
LabelsRecord) to hold those label fields and change MypageResponse.of to accept
that single Labels type (or add a builder on MypageResponse that accepts/sets
labels via a Labels object); update all call sites that invoke MypageResponse.of
to construct and pass the new Labels instance (or use the builder) and adjust
MemberVisa/Member usages accordingly so parameter order mistakes are prevented.
src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java (2)

5-6: 와일드카드 import 사용

와일드카드 import (.*)는 어떤 클래스가 실제로 사용되는지 파악하기 어렵게 만들고, 이름 충돌 가능성을 높입니다. 명시적 import를 사용하면 가독성과 유지보수성이 향상됩니다.

♻️ 명시적 import 제안
-import org.sopt.kareer.domain.member.entity.*;
-import org.sopt.kareer.domain.member.entity.enums.*;
+import org.sopt.kareer.domain.member.entity.Member;
+import org.sopt.kareer.domain.member.entity.MemberVisa;
+import org.sopt.kareer.domain.member.entity.enums.LanguageLevel;
+import org.sopt.kareer.domain.member.entity.enums.VisaType;
🤖 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/member/dto/response/MemberInfoResponse.java`
around lines 5 - 6, Replace the wildcard imports in MemberInfoResponse by
listing the exact classes used from org.sopt.kareer.domain.member.entity and
org.sopt.kareer.domain.member.entity.enums; update the import statements
referenced in MemberInfoResponse (the class name MemberInfoResponse) to
explicitly import only the entity classes and enum types referenced in its
fields/methods (e.g., specific entity classes and enum names) instead of using
org.sopt.kareer.domain.member.entity.* and
org.sopt.kareer.domain.member.entity.enums.* to improve clarity and avoid name
collisions.

61-86: 파라미터가 많은 팩토리 메서드

of() 메서드가 7개의 파라미터를 받고 있으며, 그 중 5개가 String 타입의 레이블입니다. 동일한 타입의 파라미터가 연속되면 호출 시 순서를 잘못 전달할 위험이 있습니다.

향후 파라미터가 더 늘어날 경우, Builder 패턴이나 전용 파라미터 객체(예: MemberLabels record)를 고려해 보세요.

🤖 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/member/dto/response/MemberInfoResponse.java`
around lines 61 - 86, The MemberInfoResponse.of factory method takes many
parameters (several consecutive Strings) which risks ordering bugs; refactor by
introducing a parameter object or builder: create a MemberLabels record or a
MemberInfoResponse.Builder and change MemberInfoResponse.of(Member, MemberVisa,
String... ) to accept the new MemberLabels (or use builder setters) and update
callers to pass the labels via that object or builder; adjust MemberInfoResponse
constructor/fields as needed and keep MemberInfoResponse.of(Member, MemberVisa,
MemberLabels) (or remove the static of in favor of Builder.build()) to make
parameter grouping explicit and prevent ordering mistakes.
src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java (1)

61-71: 중복된 Locale 추출 로직

Lines 48-50의 resolveLabelByCode와 Lines 62-64의 toItemResponses에서 동일한 Locale 추출 로직이 반복됩니다. 헬퍼 메서드로 추출하여 중복을 제거하면 유지보수성이 향상됩니다.

♻️ Locale 추출 헬퍼 메서드 제안
+    private record LocaleInfo(String languageTag, String language) {}
+
+    private LocaleInfo extractLocaleInfo() {
+        Locale locale = LocaleContextHolder.getLocale();
+        return new LocaleInfo(locale.toLanguageTag(), locale.getLanguage());
+    }
+
     private List<LocalizedItemResponse> toItemResponses(List<LocalizedOnboardCategory> categories) {
-        Locale locale = LocaleContextHolder.getLocale();
-        String languageTag = locale != null ? locale.toLanguageTag() : null;
-        String language = locale != null ? locale.getLanguage() : null;
+        LocaleInfo localeInfo = extractLocaleInfo();
 
         return categories.stream()
                 .map(category -> new LocalizedItemResponse(
                         category.getCode(),
-                        resolveLabel(category, languageTag, language)))
+                        resolveLabel(category, localeInfo.languageTag(), localeInfo.language())))
                 .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/member/service/LocalizedOnboardQueryService.java`
around lines 61 - 71, Extract the duplicated Locale extraction into a private
helper used by both resolveLabelByCode and toItemResponses: create a method
(e.g., getLocaleLanguageInfo or resolveLocaleInfo) that calls
LocaleContextHolder.getLocale(), computes languageTag and language (handling
null), and returns them in a small holder (e.g., a simple record/DTO or
String[]); then replace the inline Locale/ languageTag/ language logic inside
resolveLabelByCode and toItemResponses with a call to this new helper and use
its returned values when calling resolveLabel.
src/main/java/org/sopt/kareer/domain/member/entity/Member.java (1)

117-149: updateInfoV2()의 입력 검증 부재

새로운 updateInfoV2() 메서드가 문자열 코드를 직접 받아 저장합니다. 유효하지 않은 코드가 전달될 경우 데이터 무결성 문제가 발생할 수 있습니다.

서비스 레이어에서 코드 유효성을 검증하거나, 도메인 로직에서 검증을 추가하는 것이 좋습니다.

🤖 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/member/entity/Member.java` around lines
117 - 149, The updateInfoV2 method on Member accepts raw string codes
(countryCode, universityCode, englishLevelCode, degreeCode, primaryMajorCode,
etc.) without validation, risking data integrity; add domain-level validation
inside Member.updateInfoV2 (or helper methods it calls) to validate/convert
those code strings to the appropriate enums/typed values (e.g., Country,
University, EnglishLevel, Degree, Major) or lookup services and throw a clear
domain exception (e.g., InvalidMemberDataException) on invalid codes, and only
set the fields (fieldsOfInterest, preparationStatus, languageLevel, etc.) after
successful validation; ensure assertPendingStatus() remains and keep setting
status to MemberStatus.ACTIVE only when all validations pass.
🤖 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/member/dto/request/MemberOnboardV2Request.java`:
- Around line 16-20: The fields universityCode and countryCode in
MemberOnboardV2Request are annotated with `@NotNull` which allows empty or
whitespace strings; change their validation annotation to `@NotBlank` so
empty/blank values are rejected and label lookup keys remain valid. Update the
annotations on the universityCode and countryCode fields in the
MemberOnboardV2Request class (same style as primaryMajorCode, name, targetJob)
to use `@NotBlank` with an appropriate message.

In `@src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java`:
- Around line 22-35: MypageRequest has inconsistent validation and misleading
schema examples: change countryCode's annotation from `@NotNull` to `@NotBlank` (and
keep or harmonize validation messages) so empty/blank strings are rejected like
universityCode and primaryMajorCode, and update all `@Schema` example values for
countryCode, universityCode, and primaryMajorCode to show actual code formats
(e.g., "KR" or "AF" for countryCode, a university code token for universityCode,
and a major code token for primaryMajorCode) so API consumers see the expected
code strings; locate and modify the fields countryCode, universityCode, and
primaryMajorCode in class MypageRequest to apply these changes.

In `@src/main/java/org/sopt/kareer/domain/member/entity/enums/EnglishLevel.java`:
- Around line 9-11: The enum EnglishLevel was changed to use lowercase
descriptions which will break compatibility with existing DB values like
"Beginner"/"Intermediate"/"Advanced"; revert the enum descriptions in
EnglishLevel (the description strings passed to BEGINNER/INTERMEDIATE/ADVANCED)
to the original casing, and/or add a mapping method on EnglishLevel (e.g.,
fromDescription(String) or normalizeDescriptionForDb()) that accepts both
capitalized and lowercase forms and returns the correct enum so Member
persistence (which calls getDescription()) remains compatible; update usages
that serialize/deserialize englishLevelCode to use this mapping.

In
`@src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java`:
- Around line 25-42: Add a unique constraint for the combination of fields type
and code on the LocalizedOnboardCategory entity and in the DB migration: update
the `@Table` annotation on class LocalizedOnboardCategory to declare a unique
constraint over columns "type" and "code" (e.g., uniqueConstraints =
`@UniqueConstraint`(columnNames = {"type","code"}, name =
"uk_localized_onboard_type_code")), and create a corresponding migration that
adds the unique index/constraint on the localized_onboard_category table
(resolving or deduplicating any existing duplicate rows beforehand so the
migration can apply cleanly); this will ensure findByTypeAndCode() returns a
single deterministic row.

In
`@src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java`:
- Around line 17-34: Add a DB-level unique constraint on (category_id, language)
to prevent duplicates: update the JPA entity LocalizedOnboardCategoryTranslation
by adding a uniqueConstraints entry on `@Table` for the (category_id, language)
pair, and create a database migration that (a) resolves or deduplicates any
existing duplicate rows for localized_onboard_category_translation and (b) adds
a UNIQUE constraint or unique index on category_id and language. Also review
callers like LocalizedOnboardCategory.updateTranslation(), addTranslation() and
any repository usages that assume multiple rows per language to ensure they
handle the uniqueness guarantee after migration.

In `@src/main/java/org/sopt/kareer/domain/member/entity/Member.java`:
- Around line 192-212: The updateProfile method mixes enum parameters (Degree,
EnglishLevel) with string codes for other fields which is inconsistent; change
the updateProfile signature to accept degreeCode and englishLevelCode as String
(instead of Degree and EnglishLevel), assign them directly to this.degreeCode
and this.englishLevelCode, and update all callers to pass the corresponding code
strings; keep the other parameters (countryCode, universityCode,
primaryMajorCode) unchanged so the API uniformly uses string codes.

In
`@src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java`:
- Around line 31-33: In MemberContextBuilder the university field is incorrectly
populated using member.getCountryCode(), causing "country" and "university" to
share the same value; update the appendLine call that sets "university" to use
member.getUniversityCode() instead (locate the appendLine(sb, "university", ...)
call and replace the getter), leaving the "country" and "primaryMajor" lines
unchanged.

In `@src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java`:
- Line 41: The test asserts the wrong getter: replace the call to
member.getUniversityCode() with the English level getter
member.getEnglishLevelCode() so the assertion compares the stored EnglishLevel
(e.g., EnglishLevel.BEGINNER) code value; update the assertion in MemberTest
(currently using getUniversityCode) to use getEnglishLevelCode() to validate
English level correctly.

---

Outside diff comments:
In `@src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java`:
- Around line 40-52: MemberVisa still has visaPoint removed but tests and DTOs
reference it; remove all remaining visaPoint usages and align API/signatures:
delete the visaPoint field and any D10-specific validation from
MemberOnboardRequest, update MemberService.createMemberVisa call sites to match
the new signature (no visaPoint argument), and update test fixtures and
assertions—remove .visaPoint(...) builder calls in MemberVisaFixture, remove
.getVisaPoint() usages in MemberServiceTest and any assertions depending on it;
ensure MemberService and MemberOnboardRequest constructors/mapper logic no
longer expect or propagate visaPoint and rebuild tests to assert on existing
fields (e.g., visaType, visaStatus, visaStartDate, visaExpiredAt).

In `@src/main/java/org/sopt/kareer/domain/member/service/MemberService.java`:
- Around line 121-150: onboardMemberV2 currently calls String.join on
request.preparationStatuses() (and similarly fieldsOfInterests), which throws
NPE if those lists are null; update MemberService.onboardMemberV2 to defensively
handle nulls by defaulting to empty lists before joining (e.g., replace
String.join(",", request.preparationStatuses()) with a null-safe expression that
uses an empty list or empty string when request.preparationStatuses() is null)
or alternatively add `@NotNull` to MemberOnboardV2Request.preparationStatuses()
(and fieldsOfInterests if desired) so the DTO is validated; ensure to update
both preparationStatuses and fieldsOfInterests usages and keep the rest of
member.updateInfoV2 call unchanged.

---

Nitpick comments:
In
`@src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java`:
- Around line 5-6: Replace the wildcard imports in MemberInfoResponse by listing
the exact classes used from org.sopt.kareer.domain.member.entity and
org.sopt.kareer.domain.member.entity.enums; update the import statements
referenced in MemberInfoResponse (the class name MemberInfoResponse) to
explicitly import only the entity classes and enum types referenced in its
fields/methods (e.g., specific entity classes and enum names) instead of using
org.sopt.kareer.domain.member.entity.* and
org.sopt.kareer.domain.member.entity.enums.* to improve clarity and avoid name
collisions.
- Around line 61-86: The MemberInfoResponse.of factory method takes many
parameters (several consecutive Strings) which risks ordering bugs; refactor by
introducing a parameter object or builder: create a MemberLabels record or a
MemberInfoResponse.Builder and change MemberInfoResponse.of(Member, MemberVisa,
String... ) to accept the new MemberLabels (or use builder setters) and update
callers to pass the labels via that object or builder; adjust MemberInfoResponse
constructor/fields as needed and keep MemberInfoResponse.of(Member, MemberVisa,
MemberLabels) (or remove the static of in favor of Builder.build()) to make
parameter grouping explicit and prevent ordering mistakes.

In
`@src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java`:
- Around line 54-59: The static factory MypageResponse.of currently takes five
consecutive String label parameters (countryLabel, primaryMajorLabel,
universityLabel, degreeLabel, englishLevelLabel) which is error-prone; refactor
by introducing a compact value object/record (e.g., MypageLabels or
LabelsRecord) to hold those label fields and change MypageResponse.of to accept
that single Labels type (or add a builder on MypageResponse that accepts/sets
labels via a Labels object); update all call sites that invoke MypageResponse.of
to construct and pass the new Labels instance (or use the builder) and adjust
MemberVisa/Member usages accordingly so parameter order mistakes are prevented.

In `@src/main/java/org/sopt/kareer/domain/member/entity/Member.java`:
- Around line 117-149: The updateInfoV2 method on Member accepts raw string
codes (countryCode, universityCode, englishLevelCode, degreeCode,
primaryMajorCode, etc.) without validation, risking data integrity; add
domain-level validation inside Member.updateInfoV2 (or helper methods it calls)
to validate/convert those code strings to the appropriate enums/typed values
(e.g., Country, University, EnglishLevel, Degree, Major) or lookup services and
throw a clear domain exception (e.g., InvalidMemberDataException) on invalid
codes, and only set the fields (fieldsOfInterest, preparationStatus,
languageLevel, etc.) after successful validation; ensure assertPendingStatus()
remains and keep setting status to MemberStatus.ACTIVE only when all validations
pass.

In
`@src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java`:
- Around line 61-71: Extract the duplicated Locale extraction into a private
helper used by both resolveLabelByCode and toItemResponses: create a method
(e.g., getLocaleLanguageInfo or resolveLocaleInfo) that calls
LocaleContextHolder.getLocale(), computes languageTag and language (handling
null), and returns them in a small holder (e.g., a simple record/DTO or
String[]); then replace the inline Locale/ languageTag/ language logic inside
resolveLabelByCode and toItemResponses with a call to this new helper and use
its returned values when calling resolveLabel.
🪄 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: 103e04a2-f4c5-4d4e-81d2-0697449f5c1c

📥 Commits

Reviewing files that changed from the base of the PR and between 5bdab6b and fc9ffd9.

📒 Files selected for processing (30)
  • src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java
  • src/main/java/org/sopt/kareer/domain/member/dto/request/MemberOnboardV2Request.java
  • src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java
  • src/main/java/org/sopt/kareer/domain/member/dto/response/LocalizedItemResponse.java
  • src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java
  • src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java
  • src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardCountriesResponse.java
  • src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardFieldsResponse.java
  • src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardMajorsResponse.java
  • src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardUniversitiesResponse.java
  • src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java
  • src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java
  • src/main/java/org/sopt/kareer/domain/member/entity/Member.java
  • src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java
  • src/main/java/org/sopt/kareer/domain/member/entity/constants/Field.java
  • src/main/java/org/sopt/kareer/domain/member/entity/constants/Major.java
  • src/main/java/org/sopt/kareer/domain/member/entity/constants/University.java
  • src/main/java/org/sopt/kareer/domain/member/entity/enums/Degree.java
  • src/main/java/org/sopt/kareer/domain/member/entity/enums/EnglishLevel.java
  • src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java
  • src/main/java/org/sopt/kareer/domain/member/repository/LocalizedOnboardCategoryRepository.java
  • src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java
  • src/main/java/org/sopt/kareer/domain/member/service/MemberService.java
  • src/main/java/org/sopt/kareer/domain/member/service/dto/request/MypageCommand.java
  • src/main/java/org/sopt/kareer/global/config/InternationalizationConfig.java
  • src/main/java/org/sopt/kareer/global/config/locale/KareerLocaleResolver.java
  • src/main/java/org/sopt/kareer/global/config/swagger/SwaggerConfig.java
  • src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java
  • src/main/java/org/sopt/kareer/global/external/ai/builder/query/PolicyQueryBuilder.java
  • src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java
💤 Files with no reviewable changes (3)
  • src/main/java/org/sopt/kareer/domain/member/entity/constants/Field.java
  • src/main/java/org/sopt/kareer/domain/member/entity/constants/University.java
  • src/main/java/org/sopt/kareer/domain/member/entity/constants/Major.java

Comment on lines +16 to +20
@NotNull(message = "대학교 코드는 필수 입력값입니다.")
String universityCode,

@NotNull(message = "국가는 필수 입력값입니다.")
Country country,
@NotNull(message = "국가 코드는 필수 입력값입니다.")
String countryCode,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/java/org/sopt/kareer/domain/member/dto/request/MemberOnboardV2Request.java

Repository: team-kareer/kareer-server

Length of output: 2327


🏁 Script executed:

rg -A 3 "universityCode|countryCode" src/main/java/org/sopt/kareer --type java | head -50

Repository: team-kareer/kareer-server

Length of output: 4773


🏁 Script executed:

rg "@NotBlank|@NotNull" src/main/java/org/sopt/kareer/domain/member --type java -B 1 | head -40

Repository: team-kareer/kareer-server

Length of output: 3237


코드 문자열은 @NotBlank로 검증해야 합니다.

universityCodecountryCodeString인데 @NotNull만 사용하고 있어서 "" 또는 공백 문자열이 통과합니다. 같은 레코드의 다른 코드 필드들(primaryMajorCode, name, targetJob)은 모두 @NotBlank를 사용하고 있습니다. 이 두 필드는 이후 라벨 조회 키로 사용되므로 빈 값이 저장되면 매핑이 깨집니다.

수정 예시
-        `@NotNull`(message = "대학교 코드는 필수 입력값입니다.")
+        `@NotBlank`(message = "대학교 코드는 필수 입력값입니다.")
         String universityCode,

-        `@NotNull`(message = "국가 코드는 필수 입력값입니다.")
+        `@NotBlank`(message = "국가 코드는 필수 입력값입니다.")
         String countryCode,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@NotNull(message = "대학교 코드는 필수 입력값입니다.")
String universityCode,
@NotNull(message = "국가는 필수 입력값입니다.")
Country country,
@NotNull(message = "국가 코드는 필수 입력값입니다.")
String countryCode,
`@NotBlank`(message = "대학교 코드는 필수 입력값입니다.")
String universityCode,
`@NotBlank`(message = "국가 코드는 필수 입력값입니다.")
String countryCode,
🤖 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/member/dto/request/MemberOnboardV2Request.java`
around lines 16 - 20, The fields universityCode and countryCode in
MemberOnboardV2Request are annotated with `@NotNull` which allows empty or
whitespace strings; change their validation annotation to `@NotBlank` so
empty/blank values are rejected and label lookup keys remain valid. Update the
annotations on the universityCode and countryCode fields in the
MemberOnboardV2Request class (same style as primaryMajorCode, name, targetJob)
to use `@NotBlank` with an appropriate message.

Comment on lines +22 to +35
@NotNull(message = "국가코드는 필수 입력값입니다.")
String countryCode,

@Schema(description = "학위", example = "DOMESTIC_ASSOCIATE")
@NotNull(message = "학위는 필수 입력값입니다.")
Degree degree,

@Schema(description = "대학", example = "Konkuk University")
@NotBlank(message = "대학은 필수 입력값입니다.")
String university,
@NotBlank(message = "대학코드는 필수 입력값입니다.")
String universityCode,

@Schema(description = "전공", example = "Computer Science")
@NotBlank(message = "전공은 필수 입력값입니다.")
String primaryMajor,
@NotBlank(message = "전공코드는 필수 입력값입니다.")
String primaryMajorCode,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "MypageRequest.java" | head -5

Repository: team-kareer/kareer-server

Length of output: 145


🏁 Script executed:

cat -n src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java | head -50

Repository: team-kareer/kareer-server

Length of output: 2152


코드형 요청 계약을 일관되게 맞춰 주세요.

countryCode@NotNull이라 ""/공백 문자열이 통과하는 반면, universityCodeprimaryMajorCode@NotBlank로 보호됩니다. 또한 세 필드의 @Schema example이 모두 표시명("Afghanistan", "Konkuk University", "Computer Science")이라 실제 코드 형식을 명시하지 않아 API 소비자 혼동을 야기합니다.

🧩 수정 예시
-        `@Schema`(description = "국가", example = "Afghanistan")
-        `@NotNull`(message = "국가코드는 필수 입력값입니다.")
+        `@Schema`(description = "국가 코드", example = "afghanistan")
+        `@NotBlank`(message = "국가코드는 필수 입력값입니다.")
         String countryCode,
@@
-        `@Schema`(description = "대학", example = "Konkuk University")
+        `@Schema`(description = "대학 코드", example = "konkuk-university")
         `@NotBlank`(message = "대학코드는 필수 입력값입니다.")
         String universityCode,
@@
-        `@Schema`(description = "전공", example = "Computer Science")
+        `@Schema`(description = "전공 코드", example = "computer-science")
         `@NotBlank`(message = "전공코드는 필수 입력값입니다.")
         String primaryMajorCode,
🤖 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/member/dto/request/MypageRequest.java`
around lines 22 - 35, MypageRequest has inconsistent validation and misleading
schema examples: change countryCode's annotation from `@NotNull` to `@NotBlank` (and
keep or harmonize validation messages) so empty/blank strings are rejected like
universityCode and primaryMajorCode, and update all `@Schema` example values for
countryCode, universityCode, and primaryMajorCode to show actual code formats
(e.g., "KR" or "AF" for countryCode, a university code token for universityCode,
and a major code token for primaryMajorCode) so API consumers see the expected
code strings; locate and modify the fields countryCode, universityCode, and
primaryMajorCode in class MypageRequest to apply these changes.

Comment on lines +9 to +11
BEGINNER("beginner"),
INTERMEDIATE("intermediate"),
ADVANCED("advanced")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

기존 englishLevelCode 데이터 호환성 깨짐 위험이 있습니다.

description 포맷을 소문자로 바꾸면, 기존에 저장된 "Beginner", "Intermediate", "Advanced" 값과 불일치가 발생할 수 있습니다. Member 저장 로직이 getDescription()을 그대로 쓰고 있으므로, 배포 시점에 DB 정규화 마이그레이션(기존 값 소문자 변환) 또는 조회 시 하위호환 매핑이 필요합니다.

🤖 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/member/entity/enums/EnglishLevel.java`
around lines 9 - 11, The enum EnglishLevel was changed to use lowercase
descriptions which will break compatibility with existing DB values like
"Beginner"/"Intermediate"/"Advanced"; revert the enum descriptions in
EnglishLevel (the description strings passed to BEGINNER/INTERMEDIATE/ADVANCED)
to the original casing, and/or add a mapping method on EnglishLevel (e.g.,
fromDescription(String) or normalizeDescriptionForDb()) that accepts both
capitalized and lowercase forms and returns the correct enum so Member
persistence (which calls getDescription()) remains compatible; update usages
that serialize/deserialize englishLevelCode to use this mapping.

Comment on lines +25 to +42
@Table(name = "localized_onboard_category")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LocalizedOnboardCategory {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LocalizedOnboardCategoryType type;

@Column(nullable = false)
private String code;

@Column(name = "use_order", nullable = false)
private int useOrder;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find all references to LocalizedOnboardCategory
rg "LocalizedOnboardCategory" --type java -A 3 -B 1

Repository: team-kareer/kareer-server

Length of output: 20055


🏁 Script executed:

# Look for migration files related to this table
fd ".*onboard.*" --type f | grep -i migration

Repository: team-kareer/kareer-server

Length of output: 51


🏁 Script executed:

# Check the repository or query methods for this entity
rg "localizedOnboardCategory|LocalizedOnboardCategory" --type java | head -30

Repository: team-kareer/kareer-server

Length of output: 5452


🏁 Script executed:

# Search for any unique constraint definitions in existing migrations
rg "uniqueConstraint|UNIQUE|uk_" --type sql --type java | head -20

Repository: team-kareer/kareer-server

Length of output: 808


🏁 Script executed:

# Look for Flyway or Liquibase migration files
find . -type f \( -name "*.sql" -o -name "*.xml" \) | grep -i "migration\|flyway\|liquibase" | head -20

Repository: team-kareer/kareer-server

Length of output: 51


🏁 Script executed:

# Search for migration files that might contain onboard_category table definition
rg "localized_onboard_category|onboard_category" --type sql

Repository: team-kareer/kareer-server

Length of output: 217


🏁 Script executed:

# Read complete LocalizedOnboardCategory entity definition
cat -n src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java

Repository: team-kareer/kareer-server

Length of output: 3033


🏁 Script executed:

# Search for migration files more broadly
find . -type f -name "*.sql" | head -30

Repository: team-kareer/kareer-server

Length of output: 51


🏁 Script executed:

# Also check for migration directories
find . -type d -name "*migration*" -o -type d -name "*flyway*" -o -type d -name "*liquibase*"

Repository: team-kareer/kareer-server

Length of output: 51


🏁 Script executed:

# Check the resolveLabelByCode method to see how it handles the Optional
rg "resolveLabelByCode" -A 15 --type java

Repository: team-kareer/kareer-server

Length of output: 8086


typecode 조합에 유니크 제약을 추가하세요.

findByTypeAndCode() 메서드로 단건 조회를 하는데, 현재 제약이 없으면 같은 조합의 행이 여러 개 존재할 때 임의의 행이 반환되어 잘못된 라벨이 매핑될 수 있습니다. 엔티티의 @Table 어노테이션과 데이터베이스 마이그레이션에 유니크 제약을 추가하세요.

🛠️ 제약 추가 예시
+import jakarta.persistence.UniqueConstraint;
 ...
-@Table(name = "localized_onboard_category")
+@Table(
+        name = "localized_onboard_category",
+        uniqueConstraints = `@UniqueConstraint`(
+                name = "uk_localized_onboard_category_type_code",
+                columnNames = {"type", "code"}
+        )
+)
 public class LocalizedOnboardCategory {
🤖 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/member/entity/LocalizedOnboardCategory.java`
around lines 25 - 42, Add a unique constraint for the combination of fields type
and code on the LocalizedOnboardCategory entity and in the DB migration: update
the `@Table` annotation on class LocalizedOnboardCategory to declare a unique
constraint over columns "type" and "code" (e.g., uniqueConstraints =
`@UniqueConstraint`(columnNames = {"type","code"}, name =
"uk_localized_onboard_type_code")), and create a corresponding migration that
adds the unique index/constraint on the localized_onboard_category table
(resolving or deduplicating any existing duplicate rows beforehand so the
migration can apply cleanly); this will ensure findByTypeAndCode() returns a
single deterministic row.

Comment on lines +17 to +34
@Table(name = "localized_onboard_category_translation")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LocalizedOnboardCategoryTranslation {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private LocalizedOnboardCategory category;

@Column(nullable = false)
private String language;

@Column(nullable = false)
private String label;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine the LocalizedOnboardCategoryTranslation entity
find . -type f -name "LocalizedOnboardCategoryTranslation.java" | head -5

Repository: team-kareer/kareer-server

Length of output: 162


🏁 Script executed:

# Find the parent entity LocalizedOnboardCategory
find . -type f -name "LocalizedOnboardCategory.java" | head -5

Repository: team-kareer/kareer-server

Length of output: 151


🏁 Script executed:

# Search for updateTranslation and addTranslation methods
rg -A 10 "updateTranslation|addTranslation" --type java

Repository: team-kareer/kareer-server

Length of output: 1687


🏁 Script executed:

# Check for any migration files or constraints definitions
find . -type f \( -name "*.sql" -o -name "*migration*" -o -name "*Migration*.java" \) | grep -i "localized\|onboard" | head -10

Repository: team-kareer/kareer-server

Length of output: 51


🏁 Script executed:

# Search for UniqueConstraint usage in the codebase
rg "UniqueConstraint" --type java -B 2 -A 2

Repository: team-kareer/kareer-server

Length of output: 1726


🏁 Script executed:

# Read the full LocalizedOnboardCategoryTranslation entity
cat src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java

Repository: team-kareer/kareer-server

Length of output: 1498


🏁 Script executed:

# Read the full LocalizedOnboardCategory entity to see the complete context
cat src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java

Repository: team-kareer/kareer-server

Length of output: 2522


🏁 Script executed:

# Check if there are any constraints defined elsewhere for this translation table
rg "localized_onboard_category_translation" --type sql,java

Repository: team-kareer/kareer-server

Length of output: 101


🏁 Script executed:

# Search for migration files or database schema definitions
find . -type f \( -name "*.sql" -o -name "*Migration.java" -o -name "*V*.sql" \) | xargs rg "localized_onboard_category_translation" 2>/dev/null || echo "No migration files found with this table"

Repository: team-kareer/kareer-server

Length of output: 216


🏁 Script executed:

# Search across the codebase for any unique constraint definitions or migrations
rg "localized_onboard_category_translation" -t java

Repository: team-kareer/kareer-server

Length of output: 216


(category_id, language) 고유성을 데이터베이스 레벨에서 강제해야 합니다.

상위 엔티티의 updateTranslation()은 언어당 번역 1개를 전제로 findFirst()로 동작하는데, 현재 (category_id, language) 중복이 DB에서 허용됩니다. addTranslation()이 중복 체크 없이 단순 추가만 수행하므로, 의도치 않은 중복 호출 시 어떤 라벨이 조회될지 비결정적입니다. 엔티티 선언과 마이그레이션 모두에서 유니크 제약으로 고정하는 것이 안전합니다.

제약 추가 예시
+import jakarta.persistence.UniqueConstraint;
 ...
-@Table(name = "localized_onboard_category_translation")
+@Table(
+        name = "localized_onboard_category_translation",
+        uniqueConstraints = `@UniqueConstraint`(
+                name = "uk_localized_onboard_category_translation_category_language",
+                columnNames = {"category_id", "language"}
+        )
+)
 public class LocalizedOnboardCategoryTranslation {
🤖 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/member/entity/LocalizedOnboardCategoryTranslation.java`
around lines 17 - 34, Add a DB-level unique constraint on (category_id,
language) to prevent duplicates: update the JPA entity
LocalizedOnboardCategoryTranslation by adding a uniqueConstraints entry on
`@Table` for the (category_id, language) pair, and create a database migration
that (a) resolves or deduplicates any existing duplicate rows for
localized_onboard_category_translation and (b) adds a UNIQUE constraint or
unique index on category_id and language. Also review callers like
LocalizedOnboardCategory.updateTranslation(), addTranslation() and any
repository usages that assume multiple rows per language to ensure they handle
the uniqueness guarantee after migration.

Comment on lines 192 to 212
public void updateProfile(
String targetJob,
LocalDate birthDate,
Country country,
String countryCode,
Degree degree,
String university,
String primaryMajor,
String universityCode,
String primaryMajorCode,
String secondaryMajor,
LanguageLevel languageLevel,
EnglishLevel englishLevel
) {
this.targetJob = targetJob;
this.birthDate = birthDate;
this.country = country;
this.degree = degree;
this.university = university;
this.primaryMajor = primaryMajor;
this.countryCode = countryCode;
this.degreeCode = degree.getDescription();
this.universityCode = universityCode;
this.primaryMajorCode = primaryMajorCode;
this.secondaryMajor = secondaryMajor;
this.languageLevel = languageLevel;
this.englishLevel = englishLevel;
this.englishLevelCode = englishLevel.getDescription();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

updateProfile() 메서드의 일관성 없는 파라미터 타입

updateProfile()에서 countryCode, universityCode, primaryMajorCode는 문자열 코드로 받지만, DegreeEnglishLevel은 여전히 enum으로 받아 내부에서 getDescription()으로 변환합니다.

이는 호출자에게 혼란을 줄 수 있으며, i18n 전환의 일관성을 해칩니다. 모든 필드를 문자열 코드로 통일하거나, 명확한 API 계약을 문서화하는 것이 좋습니다.

♻️ 문자열 코드로 통일하는 제안
     public void updateProfile(
             String targetJob,
             LocalDate birthDate,
             String countryCode,
-            Degree degree,
+            String degreeCode,
             String universityCode,
             String primaryMajorCode,
             String secondaryMajor,
             LanguageLevel languageLevel,
-            EnglishLevel englishLevel
+            String englishLevelCode
     ) {
         this.targetJob = targetJob;
         this.birthDate = birthDate;
         this.countryCode = countryCode;
-        this.degreeCode = degree.getDescription();
+        this.degreeCode = degreeCode;
         this.universityCode = universityCode;
         this.primaryMajorCode = primaryMajorCode;
         this.secondaryMajor = secondaryMajor;
         this.languageLevel = languageLevel;
-        this.englishLevelCode = englishLevel.getDescription();
+        this.englishLevelCode = englishLevelCode;
     }
🤖 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/member/entity/Member.java` around lines
192 - 212, The updateProfile method mixes enum parameters (Degree, EnglishLevel)
with string codes for other fields which is inconsistent; change the
updateProfile signature to accept degreeCode and englishLevelCode as String
(instead of Degree and EnglishLevel), assign them directly to this.degreeCode
and this.englishLevelCode, and update all callers to pass the corresponding code
strings; keep the other parameters (countryCode, universityCode,
primaryMajorCode) unchanged so the API uniformly uses string codes.

Comment on lines +31 to +33
appendLine(sb, "country", member.getCountryCode() != null ? member.getCountryCode() : "");
appendLine(sb, "university", member.getCountryCode());
appendLine(sb, "primaryMajor", member.getPrimaryMajorCode());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

university에 국가 코드를 넣고 있습니다.

Line 32가 member.getCountryCode()를 다시 사용해서 대학 정보가 사라지고 countryuniversity가 같은 값으로 내려갑니다. 여기서는 member.getUniversityCode()를 써야 합니다.

🔧 수정 예시
-        appendLine(sb, "university", member.getCountryCode());
+        appendLine(sb, "university", member.getUniversityCode());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
appendLine(sb, "country", member.getCountryCode() != null ? member.getCountryCode() : "");
appendLine(sb, "university", member.getCountryCode());
appendLine(sb, "primaryMajor", member.getPrimaryMajorCode());
appendLine(sb, "country", member.getCountryCode() != null ? member.getCountryCode() : "");
appendLine(sb, "university", member.getUniversityCode());
appendLine(sb, "primaryMajor", member.getPrimaryMajorCode());
🤖 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/builder/context/MemberContextBuilder.java`
around lines 31 - 33, In MemberContextBuilder the university field is
incorrectly populated using member.getCountryCode(), causing "country" and
"university" to share the same value; update the appendLine call that sets
"university" to use member.getUniversityCode() instead (locate the
appendLine(sb, "university", ...) call and replace the getter), leaving the
"country" and "primaryMajor" lines unchanged.

assertThat(member.getSecondaryMajor()).isEqualTo("Statistic");
assertThat(member.getLanguageLevel()).isEqualTo(LanguageLevel.LEVEL_3);
assertThat(member.getEnglishLevel()).isEqualTo(EnglishLevel.BEGINNER);
assertThat(member.getUniversityCode()).isEqualTo("beginner");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

영어 레벨 검증 getter가 잘못되었습니다.

Line 41은 getUniversityCode()를 다시 확인하고 있어 EnglishLevel.BEGINNER를 넣어도 테스트가 실패합니다. 여기서는 getEnglishLevelCode()를 검증해야 합니다.

🧪 수정 예시
-        assertThat(member.getUniversityCode()).isEqualTo("beginner");
+        assertThat(member.getEnglishLevelCode()).isEqualTo("beginner");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assertThat(member.getUniversityCode()).isEqualTo("beginner");
assertThat(member.getEnglishLevelCode()).isEqualTo("beginner");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java` at line
41, The test asserts the wrong getter: replace the call to
member.getUniversityCode() with the English level getter
member.getEnglishLevelCode() so the assertion compares the stored EnglishLevel
(e.g., EnglishLevel.BEGINNER) code value; update the assertion in MemberTest
(currently using getUniversityCode) to use getEnglishLevelCode() to validate
English level correctly.

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