From 9d3bffdd812e69d16d0be3d14030720e8e386093 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 01:55:18 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[feat]=20#205=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EC=A0=95=EC=A0=81=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B2=88=EC=97=AD=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/LocalizedOnboardCategory.java | 73 +++++++++++++++++++ .../LocalizedOnboardCategoryTranslation.java | 52 +++++++++++++ .../enums/LocalizedOnboardCategoryType.java | 8 ++ .../LocalizedOnboardCategoryRepository.java | 17 +++++ 4 files changed, 150 insertions(+) create mode 100644 src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java create mode 100644 src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java create mode 100644 src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java create mode 100644 src/main/java/org/sopt/kareer/domain/member/repository/LocalizedOnboardCategoryRepository.java diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java b/src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java new file mode 100644 index 0000000..f477650 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategory.java @@ -0,0 +1,73 @@ +package org.sopt.kareer.domain.member.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.sopt.kareer.domain.member.entity.enums.LocalizedOnboardCategoryType; + +@Entity +@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; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "category", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private List translations = new ArrayList<>(); + + public LocalizedOnboardCategory(LocalizedOnboardCategoryType type, String code, int useOrder) { + this.type = type; + this.code = code; + this.useOrder = useOrder; + } + + public void addTranslation(String language, String label) { + LocalizedOnboardCategoryTranslation translation = new LocalizedOnboardCategoryTranslation(language, label); + translation.assignCategory(this); + this.translations.add(translation); + } + + public void updateTranslation(String language, String label) { + this.translations.stream() + .filter(t -> t.hasLanguage(language)) + .findFirst() + .ifPresentOrElse(t -> t.updateLabel(label), () -> addTranslation(language, label)); + } +} diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java b/src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java new file mode 100644 index 0000000..a62231c --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/entity/LocalizedOnboardCategoryTranslation.java @@ -0,0 +1,52 @@ +package org.sopt.kareer.domain.member.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@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; + + public LocalizedOnboardCategoryTranslation(String language, String label) { + this.language = language; + this.label = label; + } + + void assignCategory(LocalizedOnboardCategory category) { + this.category = category; + } + + boolean hasLanguage(String targetLanguage) { + return this.language.equalsIgnoreCase(targetLanguage); + } + + void updateLabel(String label) { + this.label = label; + } +} diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java b/src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java new file mode 100644 index 0000000..54d3479 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java @@ -0,0 +1,8 @@ +package org.sopt.kareer.domain.member.entity.enums; + +public enum LocalizedOnboardCategoryType { + FIELD, + MAJOR, + UNIVERSITY, + COUNTRY +} diff --git a/src/main/java/org/sopt/kareer/domain/member/repository/LocalizedOnboardCategoryRepository.java b/src/main/java/org/sopt/kareer/domain/member/repository/LocalizedOnboardCategoryRepository.java new file mode 100644 index 0000000..b781be8 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/repository/LocalizedOnboardCategoryRepository.java @@ -0,0 +1,17 @@ +package org.sopt.kareer.domain.member.repository; + +import java.util.List; +import java.util.Optional; +import org.sopt.kareer.domain.member.entity.LocalizedOnboardCategory; +import org.sopt.kareer.domain.member.entity.enums.LocalizedOnboardCategoryType; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LocalizedOnboardCategoryRepository extends JpaRepository { + + @EntityGraph(attributePaths = "translations") + List findAllByTypeOrderByUseOrderAscIdAsc(LocalizedOnboardCategoryType type); + + @EntityGraph(attributePaths = "translations") + Optional findByTypeAndCode(LocalizedOnboardCategoryType type, String code); +} From fdeabe0b788ed166c0d02e8cc0846aad00096de9 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 01:56:47 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[feat]=20#205=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EA=B4=80=EB=A0=A8=20=EC=A1=B0=ED=9A=8C=20API?= =?UTF-8?q?=EB=A5=BC=20DB=20=EA=B8=B0=EB=B0=98=20=EB=8B=A4=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/LocalizedItemResponse.java | 3 + .../response/OnboardCountriesResponse.java | 4 +- .../dto/response/OnboardFieldsResponse.java | 4 +- .../dto/response/OnboardMajorsResponse.java | 4 +- .../response/OnboardUniversitiesResponse.java | 4 +- .../service/LocalizedOnboardQueryService.java | 78 +++++++++++++++++++ 6 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/sopt/kareer/domain/member/dto/response/LocalizedItemResponse.java create mode 100644 src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/LocalizedItemResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/LocalizedItemResponse.java new file mode 100644 index 0000000..2e6c312 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/LocalizedItemResponse.java @@ -0,0 +1,3 @@ +package org.sopt.kareer.domain.member.dto.response; + +public record LocalizedItemResponse(String code, String label) {} diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardCountriesResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardCountriesResponse.java index c535992..247769d 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardCountriesResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardCountriesResponse.java @@ -3,9 +3,9 @@ import java.util.List; public record OnboardCountriesResponse( - List countries + List countries ) { - public static OnboardCountriesResponse from(List countries) { + public static OnboardCountriesResponse of(List countries) { return new OnboardCountriesResponse(countries); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardFieldsResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardFieldsResponse.java index 7a35c50..ee44439 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardFieldsResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardFieldsResponse.java @@ -3,9 +3,9 @@ import java.util.List; public record OnboardFieldsResponse( - List fields + List fields ) { - public static OnboardFieldsResponse from(List fields) { + public static OnboardFieldsResponse of(List fields) { return new OnboardFieldsResponse(fields); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardMajorsResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardMajorsResponse.java index 7e759d9..4adcd85 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardMajorsResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardMajorsResponse.java @@ -3,9 +3,9 @@ import java.util.List; public record OnboardMajorsResponse( - List majors + List majors ) { - public static OnboardMajorsResponse from(List majorList) { + public static OnboardMajorsResponse of(List majorList) { return new OnboardMajorsResponse(majorList); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardUniversitiesResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardUniversitiesResponse.java index 911e137..ec814a6 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardUniversitiesResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/OnboardUniversitiesResponse.java @@ -3,9 +3,9 @@ import java.util.List; public record OnboardUniversitiesResponse( - List universities + List universities ) { - public static OnboardUniversitiesResponse from(List universities) { + public static OnboardUniversitiesResponse of(List universities) { return new OnboardUniversitiesResponse(universities); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java b/src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java new file mode 100644 index 0000000..6831a6a --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java @@ -0,0 +1,78 @@ +package org.sopt.kareer.domain.member.service; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.sopt.kareer.domain.member.dto.response.LocalizedItemResponse; +import org.sopt.kareer.domain.member.dto.response.OnboardCountriesResponse; +import org.sopt.kareer.domain.member.dto.response.OnboardFieldsResponse; +import org.sopt.kareer.domain.member.dto.response.OnboardMajorsResponse; +import org.sopt.kareer.domain.member.dto.response.OnboardUniversitiesResponse; +import org.sopt.kareer.domain.member.entity.*; +import org.sopt.kareer.domain.member.entity.enums.LocalizedOnboardCategoryType; +import org.sopt.kareer.domain.member.repository.LocalizedOnboardCategoryRepository; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LocalizedOnboardQueryService { + + private static final String DEFAULT_LANGUAGE = "en"; + private static final String EMPTY_LABEL = ""; + + private final LocalizedOnboardCategoryRepository categoryRepository; + + + public OnboardFieldsResponse getFields() { + return OnboardFieldsResponse.of(toItemResponses(fetchByType(LocalizedOnboardCategoryType.FIELD))); + } + + public OnboardMajorsResponse getMajors() { + return OnboardMajorsResponse.of(toItemResponses(fetchByType(LocalizedOnboardCategoryType.MAJOR))); + } + + public OnboardUniversitiesResponse getUniversities() { + return OnboardUniversitiesResponse.of(toItemResponses(fetchByType(LocalizedOnboardCategoryType.UNIVERSITY))); + } + + public OnboardCountriesResponse getCountries() { + return OnboardCountriesResponse.of(toItemResponses(fetchByType(LocalizedOnboardCategoryType.COUNTRY))); + } + + private List fetchByType(LocalizedOnboardCategoryType type) { + return categoryRepository.findAllByTypeOrderByUseOrderAscIdAsc(type); + } + + private List toItemResponses(List categories) { + Locale locale = LocaleContextHolder.getLocale(); + String languageTag = locale != null ? locale.toLanguageTag() : null; + String language = locale != null ? locale.getLanguage() : null; + + return categories.stream() + .map(category -> new LocalizedItemResponse( + category.getCode(), + resolveLabel(category, languageTag, language))) + .toList(); + } + + private String resolveLabel(LocalizedOnboardCategory category, String preferredTag, String preferredLanguage) { + return findLabel(category, preferredTag) + .or(() -> findLabel(category, preferredLanguage)) + .or(() -> findLabel(category, DEFAULT_LANGUAGE)) + .or(() -> category.getTranslations().stream().findFirst().map( + LocalizedOnboardCategoryTranslation::getLabel)) + .orElse(EMPTY_LABEL); + } + + private Optional findLabel(LocalizedOnboardCategory category, String language) { + if (language == null || language.isBlank()) { + return Optional.empty(); + } + return category.getTranslations().stream() + .filter(t -> t.getLanguage().equalsIgnoreCase(language)) + .map(LocalizedOnboardCategoryTranslation::getLabel) + .findFirst(); + } +} From abd39a81dc9a0dfdd5088d7a9c578b6fd7dd4241 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 02:23:45 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[feat]=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 39 ++++--------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java index 3b70ea5..36e5acb 100644 --- a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java +++ b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java @@ -1,6 +1,8 @@ package org.sopt.kareer.domain.member.controller; +import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.*; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; @@ -10,10 +12,7 @@ import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; import org.sopt.kareer.domain.member.dto.request.MypageRequest; import org.sopt.kareer.domain.member.dto.response.*; -import org.sopt.kareer.domain.member.entity.constants.Field; -import org.sopt.kareer.domain.member.entity.constants.Major; -import org.sopt.kareer.domain.member.entity.constants.University; -import org.sopt.kareer.domain.member.entity.enums.Country; +import org.sopt.kareer.domain.member.service.LocalizedOnboardQueryService; import org.sopt.kareer.domain.member.service.MemberService; import org.sopt.kareer.domain.roadmap.dto.response.RoadmapTestResponse; import org.sopt.kareer.domain.roadmap.service.RoadMapService; @@ -23,13 +22,9 @@ import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; import org.sopt.kareer.global.response.BaseResponse; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import static org.sopt.kareer.global.config.swagger.SwaggerResponseDescription.*; @RestController @RequiredArgsConstructor @@ -41,6 +36,7 @@ public class MemberController { private final RoadMapService roadMapService; private final RoadmapAsyncService roadmapAsyncService; private final AuthService authService; + private final LocalizedOnboardQueryService localizedOnboardQueryService; @GetMapping("/me") @Operation(summary = "회원 정보 조회", description = "로그인한 회원의 정보를 조회합니다.") @@ -67,7 +63,7 @@ public ResponseEntity> onboardMember(@AuthenticationPrincipal public ResponseEntity> getOnboardUniversities() { return ResponseEntity .status(HttpStatus.OK) - .body(BaseResponse.ok(OnboardUniversitiesResponse.from(University.UNIVERSITY_LIST), + .body(BaseResponse.ok(localizedOnboardQueryService.getUniversities(), "온보딩 대학교 목록 조회에 성공하였습니다.")); } @@ -77,7 +73,7 @@ public ResponseEntity> getOnboardCountrie return ResponseEntity .status(HttpStatus.OK) .body(BaseResponse.ok( - OnboardCountriesResponse.from(Country.getCountries()), + localizedOnboardQueryService.getCountries(), "온보딩 국가 목록 조회에 성공하였습니다.")); } @@ -86,8 +82,7 @@ public ResponseEntity> getOnboardCountrie public ResponseEntity> getOnboardMajors() { return ResponseEntity .status(HttpStatus.OK) - .body(BaseResponse.ok(OnboardMajorsResponse.from(Major.MAJOR_LIST), "온보딩 전공 목록 조회에 성공하였습니다.") - ); + .body(BaseResponse.ok(localizedOnboardQueryService.getMajors(), "온보딩 전공 목록 조회에 성공하였습니다.")); } @GetMapping("/onboard/fields") @@ -95,8 +90,7 @@ public ResponseEntity> getOnboardMajors() { public ResponseEntity> getOnboardFields() { return ResponseEntity .status(HttpStatus.OK) - .body(BaseResponse.ok(OnboardFieldsResponse.from(Field.FIELD_LIST), "온보딩 관심 분야 목록 조회에 성공하였습니다.") - ); + .body(BaseResponse.ok(localizedOnboardQueryService.getFields(), "온보딩 관심 분야 목록 조회에 성공하였습니다.")); } @Operation(summary = "AI 로드맵 생성 API", description = "사용자가 온보딩에 입력한 정보를 통해 로드맵을 생성합니다.") @@ -159,21 +153,4 @@ public ResponseEntity> deleteMember(@AuthenticationPrincipal .body(BaseResponse.ok("회원 탈퇴에 성공하였습니다.")); } - @Operation(summary = "온보딩 비자 OCR API", description = "온보딩 과정에서 유저의 비자 문서를 분석하여 정보를 추출합니다.") - @PostMapping(value = "/onboard/ocr/visa", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> getVisaInfo( - @RequestPart("file") MultipartFile file){ - return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(memberService.getVisaOcr(file), "사용자 비자 정보 추출에 성공했습니다.")); - } - - @Operation(summary = "온보딩 여권 OCR API", description = "온보딩 과정에서 유저의 여권을 분석하여 정보를 추출합니다.") - @PostMapping(value = "/onboard/ocr/passport", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> getPassportInfo( - @RequestPart("file") MultipartFile file){ - return ResponseEntity.status(HttpStatus.OK) - .body(BaseResponse.ok(memberService.getPassportOcr(file), "사용자 여권 정보 추출에 성공했습니다.")); - } - - } From cf8fb3d13abf6604964fc4263e2809021330440d Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 01:57:31 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[feat]=20#205=20X-Preferred-Language=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20Locale=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?Swagger=20=ED=97=A4=EB=8D=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/InternationalizationConfig.java | 17 ++++++ .../config/locale/KareerLocaleResolver.java | 61 +++++++++++++++++++ .../global/config/swagger/SwaggerConfig.java | 23 +++++++ 3 files changed, 101 insertions(+) create mode 100644 src/main/java/org/sopt/kareer/global/config/InternationalizationConfig.java create mode 100644 src/main/java/org/sopt/kareer/global/config/locale/KareerLocaleResolver.java diff --git a/src/main/java/org/sopt/kareer/global/config/InternationalizationConfig.java b/src/main/java/org/sopt/kareer/global/config/InternationalizationConfig.java new file mode 100644 index 0000000..2314996 --- /dev/null +++ b/src/main/java/org/sopt/kareer/global/config/InternationalizationConfig.java @@ -0,0 +1,17 @@ +package org.sopt.kareer.global.config; + +import java.util.Locale; +import org.sopt.kareer.global.config.locale.KareerLocaleResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class InternationalizationConfig implements WebMvcConfigurer { + + @Bean + public LocaleResolver localeResolver() { + return new KareerLocaleResolver(Locale.ENGLISH); + } +} diff --git a/src/main/java/org/sopt/kareer/global/config/locale/KareerLocaleResolver.java b/src/main/java/org/sopt/kareer/global/config/locale/KareerLocaleResolver.java new file mode 100644 index 0000000..e6885e4 --- /dev/null +++ b/src/main/java/org/sopt/kareer/global/config/locale/KareerLocaleResolver.java @@ -0,0 +1,61 @@ +package org.sopt.kareer.global.config.locale; + +import static org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Locale; +import org.springframework.context.i18n.LocaleContext; +import org.springframework.web.servlet.i18n.AbstractLocaleContextResolver; + +public class KareerLocaleResolver extends AbstractLocaleContextResolver { + + private static final String HEADER_NAME = "X-Preferred-Language"; + private final Locale defaultLocale; + + public KareerLocaleResolver(Locale defaultLocale) { + this.defaultLocale = defaultLocale; + } + + @Override + public Locale resolveLocale(HttpServletRequest request) { + return determineLocale(request); + } + + @Override + public LocaleContext resolveLocaleContext(HttpServletRequest request) { + Locale locale = determineLocale(request); + return () -> locale; + } + + @Override + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + if (locale == null) { + request.removeAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); + } else { + request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, locale); + } + } + + @Override + public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) { + if (localeContext == null || localeContext.getLocale() == null) { + request.removeAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); + return; + } + setLocale(request, response, localeContext.getLocale()); + } + + private Locale determineLocale(HttpServletRequest request) { + Object attribute = request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); + if (attribute instanceof Locale locale) { + return locale; + } + + String header = request.getHeader(HEADER_NAME); + if (header != null && !header.isBlank()) { + return Locale.forLanguageTag(header); + } + return defaultLocale; + } +} diff --git a/src/main/java/org/sopt/kareer/global/config/swagger/SwaggerConfig.java b/src/main/java/org/sopt/kareer/global/config/swagger/SwaggerConfig.java index 51cfea7..cf44c70 100644 --- a/src/main/java/org/sopt/kareer/global/config/swagger/SwaggerConfig.java +++ b/src/main/java/org/sopt/kareer/global/config/swagger/SwaggerConfig.java @@ -8,6 +8,8 @@ import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityRequirement; @@ -38,6 +40,7 @@ @Configuration public class SwaggerConfig { + private static final String HEADER_LANGUAGE = "X-Preferred-Language"; private final String securitySchemaName = "JWT"; @Bean @@ -70,6 +73,8 @@ private SecurityRequirement setSecurityItems() { public OperationCustomizer customize() { return (Operation operation, HandlerMethod handlerMethod) -> { + addLanguageHeader(operation); + CustomExceptionDescription customExceptionDescription = handlerMethod.getMethodAnnotation( CustomExceptionDescription.class); @@ -82,6 +87,24 @@ public OperationCustomizer customize() { }; } + private void addLanguageHeader(Operation operation) { + boolean exists = operation.getParameters() != null && + operation.getParameters().stream() + .anyMatch(parameter -> HEADER_LANGUAGE.equalsIgnoreCase(parameter.getName())); + if (exists) { + return; + } + + Parameter parameter = new Parameter() + .in("header") + .name(HEADER_LANGUAGE) + .required(false) + .description("Preferred language code (e.g., en, ko, zh-CN, vi)") + .schema(new StringSchema()._default("en")); + + operation.addParametersItem(parameter); + } + private void generateErrorCodeResponseExample( Operation operation, SwaggerResponseDescription type) { From 644ee44339b07e18d790a73570e66c96c24ad67d Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 03:05:02 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[feat]=20#205=20=EB=8B=A4=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20=EC=B2=98=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=98=A8=EB=B3=B4=EB=94=A9=20API=20=EB=B0=8F=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=ED=9A=8D=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EB=B9=84=EC=9E=90=20=EC=A0=90=EC=88=98=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/MemberOnboardV2Request.java | 26 +++------ .../dto/response/MemberInfoResponse.java | 22 ++++---- .../member/dto/response/MypageResponse.java | 11 ++-- .../kareer/domain/member/entity/Member.java | 53 +++++++++++++++---- .../domain/member/entity/MemberVisa.java | 31 +---------- .../service/LocalizedOnboardQueryService.java | 13 +++++ .../domain/member/service/MemberService.java | 30 ++++++++--- .../builder/context/MemberContextBuilder.java | 22 ++++---- 8 files changed, 115 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/request/MemberOnboardV2Request.java b/src/main/java/org/sopt/kareer/domain/member/dto/request/MemberOnboardV2Request.java index e6770c3..12321f4 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/request/MemberOnboardV2Request.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/request/MemberOnboardV2Request.java @@ -13,11 +13,11 @@ public record MemberOnboardV2Request( @NotNull(message = "생년월일은 필수 입력값입니다.") LocalDate birthDate, - @NotNull(message = "대학교는 필수 입력값입니다.") - String university, + @NotNull(message = "대학교 코드는 필수 입력값입니다.") + String universityCode, - @NotNull(message = "국가는 필수 입력값입니다.") - Country country, + @NotNull(message = "국가 코드는 필수 입력값입니다.") + String countryCode, @NotNull(message = "언어 능력은 필수 입력값입니다.") LanguageLevel languageLevel, @@ -40,11 +40,8 @@ public record MemberOnboardV2Request( @NotNull(message = "비자 만료일은 필수 입력값입니다.") LocalDate visaExpiredAt, - @Schema(description = "비자 점수, D10 비자인 경우만", example = "50") - Integer visaPoint, - - @NotBlank(message = "제1전공은 필수 입력값입니다.") - String primaryMajor, + @NotBlank(message = "제1전공 코드는 필수 입력값입니다.") + String primaryMajorCode, String secondaryMajor, @@ -63,15 +60,4 @@ public record MemberOnboardV2Request( @Size(max = 1000, message = "개인 배경은 최대 1000자까지 입력할 수 있습니다.") String personalBackground ) { - @AssertTrue(message = "visaPoint는 D10 비자인 경우에만 입력할 수 있습니다.") - @Schema(hidden = true) - public boolean isVisaPointValid() { - if (visaType == null) { - return visaPoint == null; - } - if (visaType == VisaType.D10) { - return visaPoint != null; - } - return visaPoint == null; - } } diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java index 117bdf2..3c00854 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java @@ -2,11 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; -import org.sopt.kareer.domain.member.entity.Member; -import org.sopt.kareer.domain.member.entity.MemberVisa; -import org.sopt.kareer.domain.member.entity.enums.Country; -import org.sopt.kareer.domain.member.entity.enums.LanguageLevel; -import org.sopt.kareer.domain.member.entity.enums.VisaType; +import org.sopt.kareer.domain.member.entity.*; +import org.sopt.kareer.domain.member.entity.enums.*; @Schema(description = "회원 정보 응답") public record MemberInfoResponse( @@ -26,7 +23,7 @@ public record MemberInfoResponse( LocalDate birthDate, @Schema(description = "거주 국가") - Country country, + String country, @Schema(description = "주 전공", example = "Computer Science") String primaryMajor, @@ -37,6 +34,9 @@ public record MemberInfoResponse( @Schema(description = "타겟 직무", example = "Backend Engineer") String targetJob, + @Schema(description = "대학교", example = "Seoul National University") + String university, + @Schema(description = "졸업일", example = "2026-02-11") LocalDate graduationDate, @@ -55,17 +55,21 @@ public record MemberInfoResponse( @Schema(description = "비자 유형") VisaType visaType ) { - public static MemberInfoResponse from(Member member, MemberVisa memberVisa) { + public static MemberInfoResponse of(Member member, MemberVisa memberVisa, + String countryLabel, + String primaryMajorLabel, + String universityLabel) { return new MemberInfoResponse( member.getId(), member.getName(), member.getEmail(), member.getProfileImageUrl(), member.getBirthDate(), - member.getCountry(), - member.getPrimaryMajor(), + countryLabel, + primaryMajorLabel, member.getSecondaryMajor(), member.getTargetJob(), + universityLabel, member.getGraduationDate(), member.getExpectedGraduationDate(), member.getLanguageLevel(), diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java index 13f5a75..b8c37e4 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java @@ -51,16 +51,19 @@ public record MypageResponse( String englishLevel ) { - public static MypageResponse from(Member member, MemberVisa memberVisa) { + public static MypageResponse of(Member member, MemberVisa memberVisa, + String countryLabel, + String primaryMajorLabel, + String universityLabel) { return MypageResponse.builder() .name(member.getName()) .profileImageUrl(member.getProfileImageUrl()) .targetJob(member.getTargetJob()) .birthDate(member.getBirthDate()) - .country(member.getCountry().getCountryName()) + .country(countryLabel) .degree(member.getDegree().getDescription()) - .university(member.getUniversity()) - .primaryMajor(member.getPrimaryMajor()) + .university(universityLabel) + .primaryMajor(primaryMajorLabel) .secondaryMajor(member.getSecondaryMajor()) .visaType(memberVisa.getVisaType().getDescription()) .visaExpiredAt(memberVisa.getVisaExpiredAt()) diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java index 31d4c7e..d62bc8c 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java @@ -45,10 +45,9 @@ public class Member extends BaseEntity { private LocalDate birthDate; - @Enumerated(EnumType.STRING) - private Country country; + private String countryCode; - private String primaryMajor; + private String primaryMajorCode; private String secondaryMajor; @@ -60,7 +59,7 @@ public class Member extends BaseEntity { private String personalBackground; - private String university; + private String universityCode; @Enumerated(EnumType.STRING) private EnglishLevel englishLevel; @@ -100,15 +99,49 @@ public void updateInfo(String name, assertPendingStatus(); this.name = name; this.birthDate = birthDate; - this.country = country; - this.university = university; + this.countryCode = country.getCountryName(); + this.universityCode = university; + this.englishLevel = englishLevel; + this.fieldsOfInterest = fieldsOfInterests; + this.preparationStatus = preparationStatuses; + this.languageLevel = languageLevel; + this.degree = degree; + this.expectedGraduationDate = expectedGraduationDate; + this.primaryMajorCode = primaryMajor; + this.secondaryMajor = secondaryMajor; + this.targetJob = targetJob; + this.targetJobSkill = targetJobSkill; + this.status = MemberStatus.ACTIVE; + this.personalBackground = personalBackground; + } + + public void updateInfoV2(String name, + LocalDate birthDate, + String countryCode, + String universityCode, + EnglishLevel englishLevel, + String fieldsOfInterests, + String preparationStatuses, + LanguageLevel languageLevel, + Degree degree, + LocalDate expectedGraduationDate, + String primaryMajorCode, + String secondaryMajor, + String targetJob, + String targetJobSkill, + String personalBackground) { + assertPendingStatus(); + this.name = name; + this.birthDate = birthDate; + this.countryCode = countryCode; + this.universityCode = universityCode; this.englishLevel = englishLevel; this.fieldsOfInterest = fieldsOfInterests; this.preparationStatus = preparationStatuses; this.languageLevel = languageLevel; this.degree = degree; this.expectedGraduationDate = expectedGraduationDate; - this.primaryMajor = primaryMajor; + this.primaryMajorCode = primaryMajorCode; this.secondaryMajor = secondaryMajor; this.targetJob = targetJob; this.targetJobSkill = targetJobSkill; @@ -170,10 +203,10 @@ public void updateProfile( ) { this.targetJob = targetJob; this.birthDate = birthDate; - this.country = country; + this.countryCode = country.getCountryName(); this.degree = degree; - this.university = university; - this.primaryMajor = primaryMajor; + this.universityCode = university; + this.primaryMajorCode = primaryMajor; this.secondaryMajor = secondaryMajor; this.languageLevel = languageLevel; this.englishLevel = englishLevel; diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java b/src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java index 1c8c3f4..27961ae 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/MemberVisa.java @@ -1,15 +1,11 @@ package org.sopt.kareer.domain.member.entity; import jakarta.persistence.*; +import java.time.LocalDate; import lombok.*; -import org.sopt.kareer.domain.member.entity.enums.VisaStatus; -import org.sopt.kareer.domain.member.entity.enums.VisaType; -import org.sopt.kareer.domain.member.exception.MemberErrorCode; -import org.sopt.kareer.domain.member.exception.MemberException; +import org.sopt.kareer.domain.member.entity.enums.*; import org.sopt.kareer.global.entity.BaseEntity; -import java.time.LocalDate; - @Table(name = "member_visas") @Entity @Getter @@ -38,8 +34,6 @@ public class MemberVisa extends BaseEntity { @Column(nullable = false) private LocalDate visaExpiredAt; - private Integer visaPoint; - @Column(nullable = false) private LocalDate visaStartDate; @@ -47,7 +41,6 @@ public static MemberVisa createMemberVisa( Member member, VisaType visaType, LocalDate visaExpiredAt, - Integer visaPoint, LocalDate visaStartDate ) { return MemberVisa.builder() @@ -55,30 +48,10 @@ public static MemberVisa createMemberVisa( .visaType(visaType) .visaStatus(VisaStatus.ACTIVE) .visaExpiredAt(visaExpiredAt) - .visaPoint(visaPoint) .visaStartDate(visaStartDate) .build(); } - @PrePersist - @PreUpdate - private void validateVisaPoint() { - if (visaType == null) { - return; - } - - if (visaType == VisaType.D10) { - if (visaPoint == null) { - throw new MemberException(MemberErrorCode.INVALID_VISA_POINT); - } - return; - } - - if (visaPoint != null) { - throw new MemberException(MemberErrorCode.INVALID_VISA_POINT); - } - } - public void updateVisa( VisaType visaType, LocalDate visaExpiredAt diff --git a/src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java b/src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java index 6831a6a..c4df850 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/LocalizedOnboardQueryService.java @@ -41,6 +41,19 @@ public OnboardCountriesResponse getCountries() { return OnboardCountriesResponse.of(toItemResponses(fetchByType(LocalizedOnboardCategoryType.COUNTRY))); } + public String resolveLabelByCode(LocalizedOnboardCategoryType type, String code) { + if (code == null || code.isBlank()) { + return null; + } + Locale locale = LocaleContextHolder.getLocale(); + String languageTag = locale != null ? locale.toLanguageTag() : null; + String language = locale != null ? locale.getLanguage() : null; + + return categoryRepository.findByTypeAndCode(type, code) + .map(category -> resolveLabel(category, languageTag, language)) + .orElse(code); + } + private List fetchByType(LocalizedOnboardCategoryType type) { return categoryRepository.findAllByTypeOrderByUseOrderAscIdAsc(type); } diff --git a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java index 061dec1..21e6697 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java @@ -7,6 +7,7 @@ import org.sopt.kareer.domain.member.entity.Member; import org.sopt.kareer.domain.member.entity.MemberVisa; import org.sopt.kareer.domain.member.entity.enums.MemberStatus; +import org.sopt.kareer.domain.member.entity.enums.LocalizedOnboardCategoryType; import org.sopt.kareer.domain.member.exception.MemberErrorCode; import org.sopt.kareer.domain.member.exception.MemberException; import org.sopt.kareer.domain.member.repository.MemberRepository; @@ -33,6 +34,7 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberVisaRepository memberVisaRepository; private final MemberDeletionService memberDeletionService; + private final LocalizedOnboardQueryService localizedOnboardQueryService; private final DocumentProcessingService documentProcessingService; private final VisaOcrParser visaOcrParser; private final PassportOcrParser passportOcrParser; @@ -69,7 +71,17 @@ public MemberInfoResponse getMemberInfo(Long memberId) { Member member = getById(memberId); MemberVisa memberVisa = memberVisaRepository.findActiveByMemberId(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.VISA_NOT_FOUND)); - return MemberInfoResponse.from(member, memberVisa); + String countryLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.COUNTRY, member.getCountryCode()); + String primaryMajorLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.MAJOR, member.getPrimaryMajorCode()); + String universityLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.UNIVERSITY, member.getUniversityCode()); + + return MemberInfoResponse.of( + member, + memberVisa, + countryLabel, + primaryMajorLabel, + universityLabel + ); } @Transactional @@ -97,7 +109,6 @@ public void onboardMember(MemberOnboardRequest request, Long memberId) { member, request.visaType(), request.visaExpiredAt(), - request.visaPoint(), request.visaStartDate() ); memberVisaRepository.save(memberVisa); @@ -109,18 +120,18 @@ public void onboardMemberV2(MemberOnboardV2Request request, Long memberId) { String fieldOfInterest = String.join(",", request.fieldsOfInterests()); String preparationStatus = String.join(",", request.preparationStatuses()); - member.updateInfo( + member.updateInfoV2( request.name(), request.birthDate(), - request.country(), - request.university(), + request.countryCode(), + request.universityCode(), request.englishLevel(), fieldOfInterest, preparationStatus, request.languageLevel(), request.degree(), request.expectedGraduationDate(), - request.primaryMajor(), + request.primaryMajorCode(), request.secondaryMajor(), request.targetJob(), request.targetJobSkill(), @@ -131,7 +142,6 @@ public void onboardMemberV2(MemberOnboardV2Request request, Long memberId) { member, request.visaType(), request.visaExpiredAt(), - request.visaPoint(), request.visaStartDate() ); memberVisaRepository.save(memberVisa); @@ -152,7 +162,11 @@ public MypageResponse getMypage(Long memberId) { Member member = getById(memberId); MemberVisa memberVisa = memberVisaRepository.findActiveByMemberId(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.VISA_NOT_FOUND)); - return MypageResponse.from(member, memberVisa); + String countryLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.COUNTRY, member.getCountryCode()); + String primaryMajorLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.MAJOR, member.getPrimaryMajorCode()); + String universityLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.UNIVERSITY, member.getUniversityCode()); + + return MypageResponse.of(member, memberVisa, countryLabel, primaryMajorLabel, universityLabel); } @Transactional diff --git a/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java b/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java index b9b1e5b..479601a 100644 --- a/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java +++ b/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java @@ -1,19 +1,16 @@ package org.sopt.kareer.global.external.ai.builder.context; +import static org.sopt.kareer.domain.member.exception.MemberErrorCode.MEMBER_NOT_FOUND; + +import java.time.LocalDate; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.entity.Member; -import org.sopt.kareer.domain.member.entity.MemberVisa; +import org.sopt.kareer.domain.member.entity.*; import org.sopt.kareer.domain.member.entity.enums.VisaStatus; import org.sopt.kareer.domain.member.exception.MemberException; -import org.sopt.kareer.domain.member.repository.MemberRepository; -import org.sopt.kareer.domain.member.repository.MemberVisaRepository; +import org.sopt.kareer.domain.member.repository.*; import org.springframework.stereotype.Component; -import java.time.LocalDate; -import java.util.List; - -import static org.sopt.kareer.domain.member.exception.MemberErrorCode.MEMBER_NOT_FOUND; - @Component @RequiredArgsConstructor public class MemberContextBuilder { @@ -31,9 +28,9 @@ public MemberAndContext load(Long memberId) { appendLine(sb, "name", member.getName()); appendLine(sb, "email", member.getEmail()); appendLine(sb, "birthDate", member.getBirthDate()); - appendLine(sb, "country", member.getCountry() != null ? member.getCountry().name() : ""); - appendLine(sb, "university", member.getUniversity()); - appendLine(sb, "primaryMajor", member.getPrimaryMajor()); + appendLine(sb, "country", member.getCountryCode() != null ? member.getCountryCode() : ""); + appendLine(sb, "university", member.getCountryCode()); + appendLine(sb, "primaryMajor", member.getPrimaryMajorCode()); appendLine(sb, "secondaryMajor", member.getSecondaryMajor()); appendLine(sb, "targetJob", member.getTargetJob()); appendLine(sb, "targetJobSkill", member.getTargetJobSkill()); @@ -52,7 +49,6 @@ public MemberAndContext load(Long memberId) { .append(", visaStatus: ").append(v.getVisaStatus().name()) .append(", visaStartDate: ").append(v.getVisaStartDate()) .append(", visaExpiredAt: ").append(v.getVisaExpiredAt()) - .append(", visaPoint: ").append(v.getVisaPoint() != null ? v.getVisaPoint() : "") .append("\n"); } From 91f7b439a391b28ec3ddae4eced6366890df0b7b Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 03:06:54 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[feat]=20#205=20=EB=8B=A4=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20=EC=B2=98=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/entity/constants/Field.java | 72 ----- .../domain/member/entity/constants/Major.java | 75 ------ .../member/entity/constants/University.java | 251 ------------------ 3 files changed, 398 deletions(-) delete mode 100644 src/main/java/org/sopt/kareer/domain/member/entity/constants/Field.java delete mode 100644 src/main/java/org/sopt/kareer/domain/member/entity/constants/Major.java delete mode 100644 src/main/java/org/sopt/kareer/domain/member/entity/constants/University.java diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/constants/Field.java b/src/main/java/org/sopt/kareer/domain/member/entity/constants/Field.java deleted file mode 100644 index f93140b..0000000 --- a/src/main/java/org/sopt/kareer/domain/member/entity/constants/Field.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.sopt.kareer.domain.member.entity.constants; - -import java.util.List; - -public class Field { - - private Field() { - } - - public static final List FIELD_LIST = List.of( - "Automotive", - "Aerospace", - "Energy", - "Oil & Gas", - "Renewable Energy", - "Manufacturing", - "Construction", - "Engineering", - "Healthcare", - "Pharmaceuticals", - "Biotechnology", - "Medical Devices", - "Education", - "EdTech", - "Finance", - "Banking", - "Insurance", - "FinTech", - "Investment & Asset Management", - "Retail", - "E-commerce", - "Consumer Goods", - "Telecommunications", - "Media", - "Entertainment", - "Gaming", - "Agriculture", - "Food & Beverage", - "Hospitality", - "Travel & Tourism", - "Logistics", - "Supply Chain", - "Real Estate", - "Consulting", - "Legal", - "Government & Public Sector", - "Nonprofit & NGO", - "Information Technology", - "Software Development", - "Artificial Intelligence", - "Data & Analytics", - "Cybersecurity", - "Cloud Computing", - "Blockchain", - "Internet of Things (IoT)", - "Robotics", - "Semiconductors", - "Electronics", - "Hardware", - "Design", - "Marketing & Advertising", - "Human Resources", - "Sports", - "Fashion & Apparel", - "Beauty & Cosmetics", - "Environment", - "Sustainability", - "Smart City", - "Defense", - "Space Industry" - ); -} diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/constants/Major.java b/src/main/java/org/sopt/kareer/domain/member/entity/constants/Major.java deleted file mode 100644 index 595e919..0000000 --- a/src/main/java/org/sopt/kareer/domain/member/entity/constants/Major.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.sopt.kareer.domain.member.entity.constants; - -import java.util.List; - -public final class Major { - - private Major() { - } - - public static final List MAJOR_LIST = List.of( - // Engineering & CS - "Computer Science", - "Software Engineering", - "Computer Engineering", - "Information Technology", - "Information Systems", - "Information Security", - "Data Science", - "Artificial Intelligence", - "Electrical Engineering", - "Electronics Engineering", - "Mechanical Engineering", - "Civil Engineering", - "Industrial Engineering", - "Chemical Engineering", - "Biomedical Engineering", - "Environmental Engineering", - - // Natural Sciences - "Mathematics", - "Statistics", - "Physics", - "Chemistry", - "Biology", - "Biotechnology", - "Environmental Science", - - // Business & Economics - "Business Administration", - "Management", - "Economics", - "Finance", - "Accounting", - "Marketing", - "International Business", - - // Social Sciences - "Political Science", - "International Relations", - "Sociology", - "Psychology", - "Communications", - "Public Administration", - - // Arts & Humanities - "English", - "Linguistics", - "History", - "Philosophy", - "Design", - "Graphic Design", - "Fine Arts", - - // Education & Health - "Education", - "Early Childhood Education", - "Nursing", - "Public Health", - - // Law & Misc - "Law", - "Criminal Justice", - "Social Work" - ); -} diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/constants/University.java b/src/main/java/org/sopt/kareer/domain/member/entity/constants/University.java deleted file mode 100644 index edc88af..0000000 --- a/src/main/java/org/sopt/kareer/domain/member/entity/constants/University.java +++ /dev/null @@ -1,251 +0,0 @@ -package org.sopt.kareer.domain.member.entity.constants; - -import java.util.List; - -public class University { - - private University() { - } - - public static final List UNIVERSITY_LIST = List.of( - "Kunsan National University", - "Kongju National University", - "Gyeongguk National University", - "Gangneung Wonju National University", - "Kwangju Women’S University", - "Gwangju University", - "Gwangju National University Of Education", - "Gwangju Institute Of Science And Technology", - "Gwangju Catholic University", - "Kwangwoon University", - "Kwangshin University", - "Gongju National University Of Education", - "Kosin University", - "The Cyber University Of Korea", - "Korea University Sejong Campus", - "Korea University", - "Keimyung University", - "Kyung Hee Cyber University", - "Kyung Hee University", - "Kyungil University", - "Gyeongin National University Of Education", - "Kyungwoon University", - "Kyungsung University", - "Gyeongsang National University", - "Kyungpook National University", - "Kyungdong University", - "Kyungnam University", - "Gyeongnam National University Of Science And Technology", - "Kyonggi University", - "Konyang Cyber University", - "Konyang University", - "Konkuk University", - "Kangwon National University", - "GANGSEO UNIVERSITY", - "Kangnam University", - "Methodist Theological University", - "The Catholic University Of Korea", - "Catholic Kkottongnae University", - "Catholic Kwandong University", - "Gachon University", - "Kaya University", - "Gyeonggi University Of Science And Technology", - "Geoje University", - "Gangwon State University", - "Kangwon Tourism College", - "Gangneung Yeongdong University", - "Gangdong University", - "Catholic Sangji College", - "ICT Polytech Institute of Korea", - "Hwashin Cyber University", - "HWASUNG MEDI-SCIENCE UNIVERSITY", - "Hongik University", - "Howon University", - "Hoseo University", - "Honam Theological University & Seminary", - "Honam University", - "Hyupsung University", - "Hanzhong University", - "Haniluniv.&Presb.Theol.Sem.", - "Hanyang Cyber University", - "Hanyang University Erica Campus", - "Hanyang University", - "Hanshin University", - "Hansei University", - "Hansung University", - "Hanseo University", - "Hanbuk University", - "Hanbat National University", - "Hallym University", - "Hanlyo University", - "Halla University", - "Handong Global University", - "Hannam University", - "Korea Aerospace University", - "Korea Baptist Theological University/Seminary", - "Korea National Sport University", - "Korea National University Of Cultural Heritage", - "Hankuk University Of Foreign Studies", - "Korea National University Of Arts", - "Open Cyber University Of Korea (OCU)", - "Korea institute of Energy Technology", - "Korean Bible University", - "Korea National Open University", - "Korea University of Technology and Education", - "Korea National University Of Education", - "Korea Advanced Institute of Science And Technology(KAIST)", - "Tech University of Korea", - "Hankyong National University", - "Pohang University Of Science And Technology (Postech)", - "Pyeongtaek University", - "TAEJAE UNIVERSITY", - "Tamna University", - "Calvin University", - "Chungbuk National University", - "Chungnam National University", - "Chuncheon National University Of Education", - "Chugye University For The Arts", - "Chongshin University", - "Chodang University", - "Cheongju University", - "Chongju National University Of Education", - "Chungwoon University", - "Changshin University", - "CHA University", - "Jinju National University", - "Chinju National University Of Education", - "Jungwon University", - "Joong-Ang Sangha University", - "Chung-Ang University", - "Joongbu University", - "Chosun University", - "Jeju National University", - "Jeju International University", - "Jungseok Institute Of Technology", - "Jeonju University", - "Jeonju National University Of Education", - "Jeonbuk National University", - "Chonnam National University", - "Presbyterian University & Theological Seminary", - "Inha University", - "Incheon National University", - "Incheon Catholic University", - "Inje University", - "Ewha Womans University", - "Eulji University", - "U1 University", - "Uiduk University", - "Wonkwang Digital University", - "Wonkwang University", - "University Of Ulsan", - "Ulsan National Institute of Science and Technology", - "Woosong University", - "Woosuk University", - "Yong In University", - "Yewon Arts University", - "Jesus University", - "Youngsan University Of Seon Studies", - "Youngsan University", - "Youngnam Theological University & Seminary", - "Yeungnam University", - "Yonsei University MIRAE Campus", - "Yonsei University", - "Anyang University", - "Ajou University", - "ACTS University", - "Shinhan University", - "Silla University", - "SinGyeongju University", - "Korea Soongsil Cyber University", - "Soongsil University", - "Soonchunhyang University", - "Korea Christian College", - "Sookmyung Women’S University", - "The University Of Suwon", - "Suwon Catholic University", - "Songwon University", - "Sehan University", - "Sejong Cyber University", - "Sejong University", - "Semyung University", - "Sungshin Women’S University", - "Sungkyunkwan University", - "Sungkonghoe University", - "Sungkyul University", - "Sun Moon University", - "Seowon University", - "Seoul Hanyoung University", - "Seoul Jangsin University", - "Seoul Women’s University", - "Seoul Theological University", - "University Of Seoul", - "Seoul Cyber University", - "Seoul Digital University", - "Seoul National University", - "Seoul Christian University", - "Seoul National University Of Education", - "Seoul National University Of Science And Technology", - "Seonam University", - "Seokyeong University", - "Sogang University", - "Sangji University", - "Sangmyung University", - "Sangmyung University (Seoul)", - "Sahmyook University", - "Cyber Hankuk University Of Foreign Studies", - "Busan Presbyterian University", - "Busan University Of Foreign Studies", - "Busandigital University", - "Pusan National University", - "Busan National University Of Education", - "Catholic University Of Pusan", - "Baekseok University", - "Pai Chai University", - "Mokpo Catholic University", - "Mokwon University", - "Myongji University Social Science Campus", - "Myongji University Natural Science Campus", - "Luther University", - "Digital Seoul Culture Arts University", - "Dong-Eui University", - "Dongyang University", - "Dong-A University", - "Dongshin University", - "Dongseo University", - "Tongmyong University", - "Dongduk Women’s University", - "Dongguk University", - "Duksung Women’S University", - "Daejin University", - "Daejeon Theological University", - "Daejeon University", - "Daejeoncatholic University", - "Daeshin University", - "Daegu Haany University", - "Daegu University Of Foreign Studies", - "Daegu Arts University", - "Daegu Cyber University", - "Daegu University", - "Daegu National University Of Education", - "DGIST", - "Daegu Catholic University", - "Dankook University", - "Namseoul University", - "Nambu University", - "Korea Nazarene University", - "Gimcheon University", - "Geumgang University", - "Global Cyber University", - "Far East University", - "Gukje Cyber University", - "Kookmin University", - "Korea Maritime & Ocean University", - "Korea National University Of Transportation", - "Changwon National University", - "Sunchon National University", - "Pukyong National University", - "Mokpo National Maritime University", - "Mokpo National University", - "Kumoh National Institute Of Technology" - ); -} From f3dd06c644aa5003c160bc0391075820f12149ee Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 03:49:53 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[feat]=20#205=20=ED=95=99=EC=9C=84,=20?= =?UTF-8?q?=EC=98=81=EC=96=B4=20=EC=88=98=EC=A4=80=20=EB=8B=A4=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/MemberInfoResponse.java | 10 ++- .../member/dto/response/MypageResponse.java | 8 ++- .../kareer/domain/member/entity/Member.java | 23 +++---- .../domain/member/entity/enums/Degree.java | 14 ++-- .../member/entity/enums/EnglishLevel.java | 6 +- .../enums/LocalizedOnboardCategoryType.java | 4 +- .../domain/member/service/MemberService.java | 67 +++++++++++-------- .../builder/context/MemberContextBuilder.java | 4 +- .../ai/builder/query/PolicyQueryBuilder.java | 2 +- 9 files changed, 80 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java index 3c00854..05fcb9d 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/MemberInfoResponse.java @@ -49,6 +49,9 @@ public record MemberInfoResponse( @Schema(description = "학위", example = "Bachelor") String degree, + @Schema(description = "영어 수준", example = "Upper Intermediate") + String englishLevel, + @Schema(description = "타겟 직무 스킬", example = "Java, Spring") String targetJobSkill, @@ -58,7 +61,9 @@ public record MemberInfoResponse( public static MemberInfoResponse of(Member member, MemberVisa memberVisa, String countryLabel, String primaryMajorLabel, - String universityLabel) { + String universityLabel, + String degreeLabel, + String englishLevelLabel) { return new MemberInfoResponse( member.getId(), member.getName(), @@ -73,7 +78,8 @@ public static MemberInfoResponse of(Member member, MemberVisa memberVisa, member.getGraduationDate(), member.getExpectedGraduationDate(), member.getLanguageLevel(), - member.getDegree().getDescription(), + degreeLabel, + englishLevelLabel, member.getTargetJobSkill(), memberVisa.getVisaType() ); diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java b/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java index b8c37e4..0620adc 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/response/MypageResponse.java @@ -54,21 +54,23 @@ public record MypageResponse( public static MypageResponse of(Member member, MemberVisa memberVisa, String countryLabel, String primaryMajorLabel, - String universityLabel) { + String universityLabel, + String degreeLabel, + String englishLevelLabel) { return MypageResponse.builder() .name(member.getName()) .profileImageUrl(member.getProfileImageUrl()) .targetJob(member.getTargetJob()) .birthDate(member.getBirthDate()) .country(countryLabel) - .degree(member.getDegree().getDescription()) + .degree(degreeLabel) .university(universityLabel) .primaryMajor(primaryMajorLabel) .secondaryMajor(member.getSecondaryMajor()) .visaType(memberVisa.getVisaType().getDescription()) .visaExpiredAt(memberVisa.getVisaExpiredAt()) .languageLevel(member.getLanguageLevel()) - .englishLevel(member.getEnglishLevel().getDescription()) + .englishLevel(englishLevelLabel) .build(); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java index d62bc8c..aedc27e 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java @@ -61,14 +61,12 @@ public class Member extends BaseEntity { private String universityCode; - @Enumerated(EnumType.STRING) - private EnglishLevel englishLevel; + private String englishLevelCode; @Enumerated(EnumType.STRING) private LanguageLevel languageLevel; - @Enumerated(EnumType.STRING) - private Degree degree; + private String degreeCode; @Column(length = 1000) private String targetJobSkill; @@ -81,6 +79,7 @@ public class Member extends BaseEntity { @Column(nullable = false) private RoadmapStatus roadmapStatus; + // 프론트 온보딩 구현 완료 후 삭제 예정 public void updateInfo(String name, LocalDate birthDate, Country country, @@ -101,11 +100,11 @@ public void updateInfo(String name, this.birthDate = birthDate; this.countryCode = country.getCountryName(); this.universityCode = university; - this.englishLevel = englishLevel; + this.englishLevelCode = englishLevel.getDescription(); this.fieldsOfInterest = fieldsOfInterests; this.preparationStatus = preparationStatuses; this.languageLevel = languageLevel; - this.degree = degree; + this.degreeCode = degree.getDescription(); this.expectedGraduationDate = expectedGraduationDate; this.primaryMajorCode = primaryMajor; this.secondaryMajor = secondaryMajor; @@ -119,11 +118,11 @@ public void updateInfoV2(String name, LocalDate birthDate, String countryCode, String universityCode, - EnglishLevel englishLevel, + String englishLevelCode, String fieldsOfInterests, String preparationStatuses, LanguageLevel languageLevel, - Degree degree, + String degreeCode, LocalDate expectedGraduationDate, String primaryMajorCode, String secondaryMajor, @@ -135,11 +134,11 @@ public void updateInfoV2(String name, this.birthDate = birthDate; this.countryCode = countryCode; this.universityCode = universityCode; - this.englishLevel = englishLevel; + this.englishLevelCode = englishLevelCode; this.fieldsOfInterest = fieldsOfInterests; this.preparationStatus = preparationStatuses; this.languageLevel = languageLevel; - this.degree = degree; + this.degreeCode = degreeCode; this.expectedGraduationDate = expectedGraduationDate; this.primaryMajorCode = primaryMajorCode; this.secondaryMajor = secondaryMajor; @@ -204,11 +203,11 @@ public void updateProfile( this.targetJob = targetJob; this.birthDate = birthDate; this.countryCode = country.getCountryName(); - this.degree = degree; + this.degreeCode = degree.getDescription(); this.universityCode = university; this.primaryMajorCode = primaryMajor; this.secondaryMajor = secondaryMajor; this.languageLevel = languageLevel; - this.englishLevel = englishLevel; + this.englishLevelCode = englishLevel.getDescription(); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/enums/Degree.java b/src/main/java/org/sopt/kareer/domain/member/entity/enums/Degree.java index 7caa53f..1c08f3c 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/enums/Degree.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/enums/Degree.java @@ -6,13 +6,13 @@ @Getter @AllArgsConstructor public enum Degree { - DOMESTIC_ASSOCIATE("South Korea•Associate"), - DOMESTIC_BACHELORS("South Korea•Bachelor"), - DOMESTIC_MASTERS("South Korea•Master"), - DOMESTIC_DOCTORATE("South Korea•Doctorate"), - OVERSEAS_BACHELORS("Outside Korea•Bachelor"), - OVERSEAS_MASTERS("Outside Korea•Master"), - OVERSEAS_DOCTORATE("Outside Korea•Doctorate") + DOMESTIC_ASSOCIATE("south-korea-associate"), + DOMESTIC_BACHELORS("south-korea-bachelor"), + DOMESTIC_MASTERS("south-korea-master"), + DOMESTIC_DOCTORATE("south-korea-doctorate"), + OVERSEAS_BACHELORS("outside-korea-bachelor"), + OVERSEAS_MASTERS("outside-korea-master"), + OVERSEAS_DOCTORATE("outside-korea-doctorate") ; private final String description; diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/enums/EnglishLevel.java b/src/main/java/org/sopt/kareer/domain/member/entity/enums/EnglishLevel.java index 2e4059e..77d0175 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/enums/EnglishLevel.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/enums/EnglishLevel.java @@ -6,9 +6,9 @@ @Getter @AllArgsConstructor public enum EnglishLevel { - BEGINNER("Beginner"), - INTERMEDIATE("Intermediate"), - ADVANCED("Advanced") + BEGINNER("beginner"), + INTERMEDIATE("intermediate"), + ADVANCED("advanced") ; private final String description; diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java b/src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java index 54d3479..871a916 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/enums/LocalizedOnboardCategoryType.java @@ -4,5 +4,7 @@ public enum LocalizedOnboardCategoryType { FIELD, MAJOR, UNIVERSITY, - COUNTRY + COUNTRY, + DEGREE, + ENGLISH_LEVEL } diff --git a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java index 21e6697..ef3557a 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java @@ -1,22 +1,15 @@ package org.sopt.kareer.domain.member.service; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request; +import org.sopt.kareer.domain.member.dto.request.*; import org.sopt.kareer.domain.member.dto.response.*; -import org.sopt.kareer.domain.member.entity.Member; -import org.sopt.kareer.domain.member.entity.MemberVisa; -import org.sopt.kareer.domain.member.entity.enums.MemberStatus; -import org.sopt.kareer.domain.member.entity.enums.LocalizedOnboardCategoryType; -import org.sopt.kareer.domain.member.exception.MemberErrorCode; -import org.sopt.kareer.domain.member.exception.MemberException; -import org.sopt.kareer.domain.member.repository.MemberRepository; -import org.sopt.kareer.domain.member.repository.MemberVisaRepository; +import org.sopt.kareer.domain.member.entity.*; +import org.sopt.kareer.domain.member.entity.enums.*; +import org.sopt.kareer.domain.member.exception.*; +import org.sopt.kareer.domain.member.repository.*; import org.sopt.kareer.domain.member.service.dto.request.MypageCommand; -import org.sopt.kareer.domain.member.util.PassportOcrParser; -import org.sopt.kareer.domain.member.util.VisaOcrParser; -import org.sopt.kareer.global.document.exception.DocumentErrorCode; -import org.sopt.kareer.global.document.exception.DocumentException; +import org.sopt.kareer.domain.member.util.*; +import org.sopt.kareer.global.document.exception.*; import org.sopt.kareer.global.document.service.DocumentProcessingService; import org.sopt.kareer.global.exception.customexception.GlobalException; import org.sopt.kareer.global.exception.errorcode.GlobalErrorCode; @@ -71,19 +64,29 @@ public MemberInfoResponse getMemberInfo(Long memberId) { Member member = getById(memberId); MemberVisa memberVisa = memberVisaRepository.findActiveByMemberId(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.VISA_NOT_FOUND)); - String countryLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.COUNTRY, member.getCountryCode()); - String primaryMajorLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.MAJOR, member.getPrimaryMajorCode()); - String universityLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.UNIVERSITY, member.getUniversityCode()); + String countryLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.COUNTRY, + member.getCountryCode()); + String primaryMajorLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.MAJOR, + member.getPrimaryMajorCode()); + String universityLabel = localizedOnboardQueryService.resolveLabelByCode( + LocalizedOnboardCategoryType.UNIVERSITY, member.getUniversityCode()); + String degreeLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.DEGREE, + member.getDegreeCode()); + String englishLevelLabel = localizedOnboardQueryService.resolveLabelByCode( + LocalizedOnboardCategoryType.ENGLISH_LEVEL, member.getEnglishLevelCode()); return MemberInfoResponse.of( member, memberVisa, countryLabel, primaryMajorLabel, - universityLabel + universityLabel, + degreeLabel, + englishLevelLabel ); } + // 프론트 온보딩 구현 완료 후 삭제 예정 @Transactional public void onboardMember(MemberOnboardRequest request, Long memberId) { Member member = getById(memberId); @@ -125,11 +128,11 @@ public void onboardMemberV2(MemberOnboardV2Request request, Long memberId) { request.birthDate(), request.countryCode(), request.universityCode(), - request.englishLevel(), + request.englishLevel().getDescription(), fieldOfInterest, preparationStatus, request.languageLevel(), - request.degree(), + request.degree().getDescription(), request.expectedGraduationDate(), request.primaryMajorCode(), request.secondaryMajor(), @@ -162,11 +165,20 @@ public MypageResponse getMypage(Long memberId) { Member member = getById(memberId); MemberVisa memberVisa = memberVisaRepository.findActiveByMemberId(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.VISA_NOT_FOUND)); - String countryLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.COUNTRY, member.getCountryCode()); - String primaryMajorLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.MAJOR, member.getPrimaryMajorCode()); - String universityLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.UNIVERSITY, member.getUniversityCode()); + String countryLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.COUNTRY, + member.getCountryCode()); + String primaryMajorLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.MAJOR, + member.getPrimaryMajorCode()); + String universityLabel = localizedOnboardQueryService.resolveLabelByCode( + LocalizedOnboardCategoryType.UNIVERSITY, member.getUniversityCode()); + String degreeLabel = localizedOnboardQueryService.resolveLabelByCode(LocalizedOnboardCategoryType.DEGREE, + member.getDegreeCode()); + String englishLevelLabel = localizedOnboardQueryService.resolveLabelByCode( + LocalizedOnboardCategoryType.ENGLISH_LEVEL, member.getEnglishLevelCode()); + + return MypageResponse.of(member, memberVisa, countryLabel, primaryMajorLabel, universityLabel, degreeLabel, + englishLevelLabel); - return MypageResponse.of(member, memberVisa, countryLabel, primaryMajorLabel, universityLabel); } @Transactional @@ -197,7 +209,7 @@ public void deleteMember(Long memberId) { } - public OcrVisaResponse getVisaOcr(MultipartFile file){ + public OcrVisaResponse getVisaOcr(MultipartFile file) { try { String text = documentProcessingService.extractText(file); VisaOcrParser.VisaInfo visaInfo = visaOcrParser.parse(text); @@ -205,7 +217,7 @@ public OcrVisaResponse getVisaOcr(MultipartFile file){ return OcrVisaResponse.from(visaInfo); } catch (DocumentException e) { throw e; - } catch(Exception e) { + } catch (Exception e) { throw new DocumentException( DocumentErrorCode.OCR_PROCESSING_FAILED ); @@ -220,10 +232,11 @@ public OcrPassportResponse getPassportOcr(MultipartFile file) { return OcrPassportResponse.from(passportInfo); } catch (DocumentException e) { throw e; - } catch(Exception e) { + } catch (Exception e) { throw new DocumentException( DocumentErrorCode.OCR_PROCESSING_FAILED ); } } + } diff --git a/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java b/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java index 479601a..0e71708 100644 --- a/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java +++ b/src/main/java/org/sopt/kareer/global/external/ai/builder/context/MemberContextBuilder.java @@ -38,8 +38,8 @@ public MemberAndContext load(Long memberId) { appendLine(sb, "preparationStatus", member.getPreparationStatus()); appendLine(sb, "personalBackground", member.getPersonalBackground()); appendLine(sb, "languageLevel", member.getLanguageLevel() != null ? member.getLanguageLevel().name() : ""); - appendLine(sb, "englishLevel", member.getEnglishLevel() != null ? member.getEnglishLevel().name() : ""); - appendLine(sb, "degree", member.getDegree() != null ? member.getDegree().name() : ""); + appendLine(sb, "englishLevel", member.getEnglishLevelCode() != null ? member.getEnglishLevelCode() : ""); + appendLine(sb, "degree", member.getDegreeCode() != null ? member.getDegreeCode() : ""); appendLine(sb, "graduationDate", member.getGraduationDate()); appendLine(sb, "expectedGraduationDate", member.getExpectedGraduationDate()); diff --git a/src/main/java/org/sopt/kareer/global/external/ai/builder/query/PolicyQueryBuilder.java b/src/main/java/org/sopt/kareer/global/external/ai/builder/query/PolicyQueryBuilder.java index f3fb8b0..509273f 100644 --- a/src/main/java/org/sopt/kareer/global/external/ai/builder/query/PolicyQueryBuilder.java +++ b/src/main/java/org/sopt/kareer/global/external/ai/builder/query/PolicyQueryBuilder.java @@ -10,7 +10,7 @@ public static String buildPolicyQuery(Member member, MemberVisa visa) { "Korea visa policy and employment rules", "visaType=" + (visa == null ? "" : visa.getVisaType().name()), "targetJob=" + nullSafe(member.getTargetJob()), - "degree=" + (member.getDegree() == null ? "" : member.getDegree().name()), + "degree=" + (member.getDegreeCode() == null ? "" : member.getDegreeCode()), "graduation=" + (member.getExpectedGraduationDate() == null ? "" : member.getExpectedGraduationDate().toString()) ); } From f7c50ea93d9b67257145d18acb42e5378170e08e Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 03:53:48 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[fix]=20#205=20OCR=20API=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java index 36e5acb..692dd80 100644 --- a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java +++ b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java @@ -5,26 +5,22 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.*; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; -import org.sopt.kareer.domain.member.dto.request.MypageRequest; +import org.sopt.kareer.domain.member.dto.request.*; import org.sopt.kareer.domain.member.dto.response.*; -import org.sopt.kareer.domain.member.service.LocalizedOnboardQueryService; -import org.sopt.kareer.domain.member.service.MemberService; +import org.sopt.kareer.domain.member.service.*; import org.sopt.kareer.domain.roadmap.dto.response.RoadmapTestResponse; -import org.sopt.kareer.domain.roadmap.service.RoadMapService; -import org.sopt.kareer.domain.roadmap.service.RoadmapAsyncService; +import org.sopt.kareer.domain.roadmap.service.*; import org.sopt.kareer.global.annotation.CustomExceptionDescription; import org.sopt.kareer.global.auth.service.AuthService; import org.sopt.kareer.global.config.swagger.SwaggerResponseDescription; import org.sopt.kareer.global.response.BaseResponse; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -153,4 +149,20 @@ public ResponseEntity> deleteMember(@AuthenticationPrincipal .body(BaseResponse.ok("회원 탈퇴에 성공하였습니다.")); } + @Operation(summary = "온보딩 비자 OCR API", description = "온보딩 과정에서 유저의 비자 문서를 분석하여 정보를 추출합니다.") + @PostMapping(value = "/onboard/ocr/visa", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> getVisaInfo( + @RequestPart("file") MultipartFile file){ + return ResponseEntity.status(HttpStatus.OK) + .body(BaseResponse.ok(memberService.getVisaOcr(file), "사용자 비자 정보 추출에 성공했습니다.")); + } + + @Operation(summary = "온보딩 여권 OCR API", description = "온보딩 과정에서 유저의 여권을 분석하여 정보를 추출합니다.") + @PostMapping(value = "/onboard/ocr/passport", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> getPassportInfo( + @RequestPart("file") MultipartFile file){ + return ResponseEntity.status(HttpStatus.OK) + .body(BaseResponse.ok(memberService.getPassportOcr(file), "사용자 여권 정보 추출에 성공했습니다.")); + } + } From fc9ffd9ec2cbc969240b71d8fce2026bc34c7e4c Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 03:59:43 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[feat]=20#205=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=88=98=EC=A0=95=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EA=B5=AD=EA=B0=80,=20=EB=8C=80=ED=95=99,=20?= =?UTF-8?q?=EC=A0=84=EA=B3=B5=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/dto/request/MypageRequest.java | 16 +++++----- .../kareer/domain/member/entity/Member.java | 12 +++---- .../domain/member/service/MemberService.java | 6 ++-- .../service/dto/request/MypageCommand.java | 6 ++-- .../domain/member/entity/MemberTest.java | 31 ++++++++----------- 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java b/src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java index 025a64d..d3c347d 100644 --- a/src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java +++ b/src/main/java/org/sopt/kareer/domain/member/dto/request/MypageRequest.java @@ -19,20 +19,20 @@ public record MypageRequest( LocalDate birthDate, @Schema(description = "국가", example = "Afghanistan") - @NotNull(message = "국가는 필수 입력값입니다.") - Country country, + @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, @Schema(description = "부전공", example = "Statistic") @NotBlank(message = "부전공은 필수 입력값입니다.") @@ -55,8 +55,8 @@ public record MypageRequest( EnglishLevel englishLevel ) { public MypageCommand toCommand(){ - return new MypageCommand(this.targetJob, this.birthDate, this.country, - this.degree, this.university, this.primaryMajor, this.secondaryMajor, this.visaType, + return new MypageCommand(this.targetJob, this.birthDate, this.countryCode, + this.degree, this.universityCode, this.primaryMajorCode, this.secondaryMajor, this.visaType, this.visaExpiredAt, this.languageLevel, this.englishLevel); } } diff --git a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java index aedc27e..dcfae8b 100644 --- a/src/main/java/org/sopt/kareer/domain/member/entity/Member.java +++ b/src/main/java/org/sopt/kareer/domain/member/entity/Member.java @@ -192,20 +192,20 @@ public void assertCanStartRoadmap() { 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.countryCode = country.getCountryName(); + this.countryCode = countryCode; this.degreeCode = degree.getDescription(); - this.universityCode = university; - this.primaryMajorCode = primaryMajor; + this.universityCode = universityCode; + this.primaryMajorCode = primaryMajorCode; this.secondaryMajor = secondaryMajor; this.languageLevel = languageLevel; this.englishLevelCode = englishLevel.getDescription(); diff --git a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java index ef3557a..66a6078 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java @@ -190,10 +190,10 @@ public void updateMypage(Long memberId, MypageCommand command) { member.updateProfile( command.targetJob(), command.birthDate(), - command.country(), + command.countryCode(), command.degree(), - command.university(), - command.primaryMajor(), + command.universityCode(), + command.primaryMajorCode(), command.secondaryMajor(), command.languageLevel(), command.englishLevel() diff --git a/src/main/java/org/sopt/kareer/domain/member/service/dto/request/MypageCommand.java b/src/main/java/org/sopt/kareer/domain/member/service/dto/request/MypageCommand.java index a9bff20..fe0c758 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/dto/request/MypageCommand.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/dto/request/MypageCommand.java @@ -9,13 +9,13 @@ public record MypageCommand( LocalDate birthDate, - Country country, + String countryCode, Degree degree, - String university, + String universityCode, - String primaryMajor, + String primaryMajorCode, String secondaryMajor, diff --git a/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java b/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java index 6fbef9c..f0b8b98 100644 --- a/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java +++ b/src/test/java/org/sopt/kareer/domain/member/entity/MemberTest.java @@ -1,16 +1,11 @@ package org.sopt.kareer.domain.member.entity; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.sopt.kareer.domain.member.entity.enums.Country; -import org.sopt.kareer.domain.member.entity.enums.Degree; -import org.sopt.kareer.domain.member.entity.enums.EnglishLevel; -import org.sopt.kareer.domain.member.entity.enums.LanguageLevel; -import org.sopt.kareer.domain.member.fixture.MemberFixture; +import static org.assertj.core.api.Assertions.assertThat; import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.*; +import org.sopt.kareer.domain.member.entity.enums.*; +import org.sopt.kareer.domain.member.fixture.MemberFixture; class MemberTest { @@ -25,10 +20,10 @@ void updateProfile(){ member.updateProfile( "Developer", newBirthDate, - Country.AFGHANISTAN, + "afghanistan", Degree.DOMESTIC_ASSOCIATE, - "Konkuk University", - "Computer Science", + "konkuk-university", + "computer-science", "Statistic", LanguageLevel.LEVEL_3, EnglishLevel.BEGINNER @@ -37,12 +32,12 @@ void updateProfile(){ //then assertThat(member.getTargetJob()).isEqualTo("Developer"); assertThat(member.getBirthDate()).isEqualTo(newBirthDate); - assertThat(member.getCountry()).isEqualTo(Country.AFGHANISTAN); - assertThat(member.getDegree()).isEqualTo(Degree.DOMESTIC_ASSOCIATE); - assertThat(member.getUniversity()).isEqualTo("Konkuk University"); - assertThat(member.getPrimaryMajor()).isEqualTo("Computer Science"); + assertThat(member.getCountryCode()).isEqualTo("afghanistan"); + assertThat(member.getDegreeCode()).isEqualTo("south-korea-associate"); + assertThat(member.getUniversityCode()).isEqualTo("konkuk-university"); + assertThat(member.getPrimaryMajorCode()).isEqualTo("computer-science"); assertThat(member.getSecondaryMajor()).isEqualTo("Statistic"); assertThat(member.getLanguageLevel()).isEqualTo(LanguageLevel.LEVEL_3); - assertThat(member.getEnglishLevel()).isEqualTo(EnglishLevel.BEGINNER); + assertThat(member.getUniversityCode()).isEqualTo("beginner"); } -} \ No newline at end of file +} From 0f21276395520cfd6484dcae47d438a2c207f538 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Fri, 27 Mar 2026 04:06:49 +0900 Subject: [PATCH 10/10] =?UTF-8?q?[chore]=20#205=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=EB=AC=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kareer/domain/member/controller/MemberController.java | 1 - .../sopt/kareer/domain/member/service/MemberService.java | 8 -------- 2 files changed, 9 deletions(-) diff --git a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java index 5cc11da..150511b 100644 --- a/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java +++ b/src/main/java/org/sopt/kareer/domain/member/controller/MemberController.java @@ -8,7 +8,6 @@ import jakarta.servlet.http.*; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.dto.request.*; import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; import org.sopt.kareer.domain.member.dto.request.MemberTermsRequest; import org.sopt.kareer.domain.member.dto.request.MypageRequest; diff --git a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java index 16de665..2bcfd4c 100644 --- a/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java +++ b/src/main/java/org/sopt/kareer/domain/member/service/MemberService.java @@ -1,19 +1,12 @@ package org.sopt.kareer.domain.member.service; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.member.dto.request.*; import org.sopt.kareer.domain.member.dto.response.*; -import org.sopt.kareer.domain.member.entity.*; import org.sopt.kareer.domain.member.entity.enums.*; -import org.sopt.kareer.domain.member.exception.*; -import org.sopt.kareer.domain.member.repository.*; import org.sopt.kareer.domain.member.service.dto.request.MypageCommand; -import org.sopt.kareer.domain.member.util.*; -import org.sopt.kareer.global.document.exception.*; import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest; import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request; import org.sopt.kareer.domain.member.dto.request.MemberTermsRequest; -import org.sopt.kareer.domain.member.dto.response.*; import org.sopt.kareer.domain.member.entity.Member; import org.sopt.kareer.domain.member.entity.MemberTerm; import org.sopt.kareer.domain.member.entity.MemberVisa; @@ -23,7 +16,6 @@ import org.sopt.kareer.domain.member.repository.MemberRepository; import org.sopt.kareer.domain.member.repository.MemberTermRepository; import org.sopt.kareer.domain.member.repository.MemberVisaRepository; -import org.sopt.kareer.domain.member.service.dto.request.MypageCommand; import org.sopt.kareer.domain.member.util.PassportOcrParser; import org.sopt.kareer.domain.member.util.VisaOcrParser; import org.sopt.kareer.domain.term.entity.Term;