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/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; 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; +} 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() + )); + } }