From acb27385ade3f2ecc828a86d91134ee36fafa4cf Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 1 Apr 2026 21:53:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[feat]=20#226=20Term=20=EB=B2=88=EC=97=AD?= =?UTF-8?q?=20=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 --- .../domain/term/entity/TermTranslation.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/java/org/sopt/kareer/domain/term/entity/TermTranslation.java diff --git a/src/main/java/org/sopt/kareer/domain/term/entity/TermTranslation.java b/src/main/java/org/sopt/kareer/domain/term/entity/TermTranslation.java new file mode 100644 index 0000000..023ef06 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/term/entity/TermTranslation.java @@ -0,0 +1,36 @@ +package org.sopt.kareer.domain.term.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.sopt.kareer.global.entity.BaseEntity; + +@Entity +@Table(name = "term_translations", + uniqueConstraints = { + @UniqueConstraint(name = "uk_term_translation_language", + columnNames = {"term_id", "language_code"}) + }) +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TermTranslation extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "term_translation_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "term_id", nullable = false) + private Term term; + + @Column(name = "language_code", nullable = false, length = 10) + private String languageCode; + + @Column(nullable = false) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; +} From 0b20d28e259614e90ae3d1876527f5167a5db5b4 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 1 Apr 2026 21:56:18 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[feat]=20#226=20=EC=95=BD=EA=B4=80=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../term/dto/response/TermsResponse.java | 6 +- .../repository/TermTranslationRepository.java | 12 +++ .../domain/term/service/TermService.java | 90 ++++++++++++++++++- 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/sopt/kareer/domain/term/repository/TermTranslationRepository.java diff --git a/src/main/java/org/sopt/kareer/domain/term/dto/response/TermsResponse.java b/src/main/java/org/sopt/kareer/domain/term/dto/response/TermsResponse.java index 8464213..92c4b6d 100644 --- a/src/main/java/org/sopt/kareer/domain/term/dto/response/TermsResponse.java +++ b/src/main/java/org/sopt/kareer/domain/term/dto/response/TermsResponse.java @@ -27,11 +27,11 @@ public record TermResponse( @Schema(description = "ν•„μˆ˜ μ—¬λΆ€", example="true") boolean required ) { - public static TermResponse from(Term term) { + public static TermResponse of(Term term, String title, String content) { return new TermResponse( term.getId(), - term.getTitle(), - term.getContent(), + title, + content, term.isRequired() ); } diff --git a/src/main/java/org/sopt/kareer/domain/term/repository/TermTranslationRepository.java b/src/main/java/org/sopt/kareer/domain/term/repository/TermTranslationRepository.java new file mode 100644 index 0000000..0749552 --- /dev/null +++ b/src/main/java/org/sopt/kareer/domain/term/repository/TermTranslationRepository.java @@ -0,0 +1,12 @@ +package org.sopt.kareer.domain.term.repository; + +import org.sopt.kareer.domain.term.entity.TermTranslation; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TermTranslationRepository extends JpaRepository { + + List findAllByTerm_IdInAndLanguageCode(List termIds, String languageCode); + +} diff --git a/src/main/java/org/sopt/kareer/domain/term/service/TermService.java b/src/main/java/org/sopt/kareer/domain/term/service/TermService.java index 7d15fb2..6978694 100644 --- a/src/main/java/org/sopt/kareer/domain/term/service/TermService.java +++ b/src/main/java/org/sopt/kareer/domain/term/service/TermService.java @@ -1,28 +1,57 @@ package org.sopt.kareer.domain.term.service; import lombok.RequiredArgsConstructor; -import org.sopt.kareer.domain.term.entity.Term; import org.sopt.kareer.domain.term.dto.response.TermsResponse; +import org.sopt.kareer.domain.term.entity.Term; +import org.sopt.kareer.domain.term.entity.TermTranslation; import org.sopt.kareer.domain.term.repository.TermRepository; +import org.sopt.kareer.domain.term.repository.TermTranslationRepository; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class TermService { + private static final String DEFAULT_LANGUAGE = "ko"; + private final TermRepository termRepository; + private final TermTranslationRepository termTranslationRepository; public TermsResponse getTerms() { + List languagePreferences = resolveLanguagePreferences(); List terms = termRepository.findByActiveTrue(); + if (terms.isEmpty()) { + return TermsResponse.from(List.of()); + } + + List termIds = terms.stream() + .map(Term::getId) + .toList(); + + Map> translationsByLanguage = loadTranslationsByLanguage(termIds, languagePreferences); List termResponses = terms.stream() .sorted(Comparator.comparing(term -> term.getType().getOrder())) - .map(TermsResponse.TermResponse::from) + .map(term -> { + TermTranslation translation = findTranslation(term.getId(), translationsByLanguage, languagePreferences); + + String title = translation != null ? translation.getTitle() : ""; + String content = translation != null ? translation.getContent() : ""; + return TermsResponse.TermResponse.of(term, title, content); + }) .toList(); return TermsResponse.from(termResponses); @@ -31,4 +60,61 @@ public TermsResponse getTerms() { public List getActiveTerms() { return termRepository.findByActiveTrue(); } + + private static List resolveLanguagePreferences() { + Locale locale = LocaleContextHolder.getLocale(); + List preferences = new ArrayList<>(); + + addLanguage(preferences, locale != null ? locale.toLanguageTag() : null); + addLanguage(preferences, locale != null ? locale.getLanguage() : null); + addLanguage(preferences, DEFAULT_LANGUAGE); + + return List.copyOf(preferences); + } + + private static void addLanguage(List target, String language) { + if (language == null || language.isBlank()) { + return; + } + String normalized = language.toLowerCase(Locale.ROOT); + if (!target.contains(normalized)) { + target.add(normalized); + } + } + + private Map> loadTranslationsByLanguage(List termIds, + List languagePreferences) { + Map> translationsByLanguage = new LinkedHashMap<>(); + for (String language : languagePreferences) { + List translations = termTranslationRepository + .findAllByTerm_IdInAndLanguageCode(termIds, language); + translationsByLanguage.put(language, groupByTermId(translations)); + } + return translationsByLanguage; + } + + private static TermTranslation findTranslation(Long termId, + Map> translationsByLanguage, + List languagePreferences) { + for (String language : languagePreferences) { + Map translations = translationsByLanguage.get(language); + if (translations == null) { + continue; + } + TermTranslation translation = translations.get(termId); + if (translation != null) { + return translation; + } + } + return null; + } + + private static Map groupByTermId(List translations) { + return translations.stream() + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableMap( + translation -> translation.getTerm().getId(), + Function.identity() + )); + } } From e0f97863998509481fbf1a331df952b2bfe23c3e Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 1 Apr 2026 21:56:32 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[feat]=20#226=20=EB=B2=88=EC=97=AD=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EA=B5=AC=ED=98=84=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=ED=95=84=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/sopt/kareer/domain/term/entity/Term.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/org/sopt/kareer/domain/term/entity/Term.java b/src/main/java/org/sopt/kareer/domain/term/entity/Term.java index cb5b903..087c432 100644 --- a/src/main/java/org/sopt/kareer/domain/term/entity/Term.java +++ b/src/main/java/org/sopt/kareer/domain/term/entity/Term.java @@ -18,12 +18,6 @@ public class Term extends BaseEntity { @Column(name = "term_id") private Long id; - @Column(nullable = false) - private String title; - - @Column(nullable = false, columnDefinition = "TEXT") - private String content; - @Column(nullable = false) @Enumerated(EnumType.STRING) private TermType type;