From 7a15351b34f4677820de4fd02824473baf8e18df Mon Sep 17 00:00:00 2001 From: gituser Date: Mon, 19 Oct 2026 13:27:19 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=A9=B4=EC=A0=91=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=97=90=20=EB=8C=80=ED=91=9C=20=EB=A9=B4=EC=A0=91?= =?UTF-8?q?=EA=B4=80(persona)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InterviewResponse에 persona { id, personaName, personaRole, type, level, tone, imageUrl }를 더한다. 1:1은 그 면접의 면접관, N:1은 진행 순서 맨 앞 면접관, 없으면 null. 이름은 저장값 그대로다. 목록 조회는 N:1 명단과 면접관을 면접마다 읽지 않고 한 번에 읽는다. Co-Authored-By: Claude Opus 5.5 --- .../dto/response/InterviewPersonaSummary.java | 40 +++++++++++ .../dto/response/InterviewResponse.java | 14 ++-- .../interview/service/InterviewService.java | 68 +++++++++++++++++-- 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewPersonaSummary.java diff --git a/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewPersonaSummary.java b/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewPersonaSummary.java new file mode 100644 index 0000000..69bb324 --- /dev/null +++ b/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewPersonaSummary.java @@ -0,0 +1,40 @@ +package repit.repit_api_server.domain.userdata.interview.dto.response; + +import repit.repit_api_server.domain.userdata.persona.entity.PersonaEntity; +import repit.repit_api_server.domain.userdata.persona.entity.enums.InterviewTone; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Level; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Role; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Type; + +/** + * 면접 응답에 붙는 대표 면접관. 목록·상세 화면이 성향·난이도·면접관 이름을 여기서 읽는다. + * + *

1:1은 그 면접의 면접관, N:1은 진행 순서 맨 앞 면접관이다. 피드백 응답의 성향·난이도와 + * 같은 규칙이다. 이름은 저장된 값 그대로다 — 1:1 면접관 이름 끝의 유일성 접미사는 화면이 뗀다. + */ +public record InterviewPersonaSummary( + Long id, + String personaName, + Role personaRole, + Type type, + Level level, + InterviewTone tone, + String imageUrl +) { + + /** 면접관이 없으면(지워졌거나 걸려 있지 않으면) null이다. */ + public static InterviewPersonaSummary of(PersonaEntity persona) { + if (persona == null) { + return null; + } + return new InterviewPersonaSummary( + persona.getPersonaId(), + persona.getPersonaName(), + persona.getRole(), + persona.getType(), + persona.getLevel(), + persona.getTone(), + persona.getImageUrl() + ); + } +} diff --git a/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponse.java b/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponse.java index d811834..faac148 100644 --- a/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponse.java +++ b/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponse.java @@ -8,6 +8,7 @@ import repit.repit_api_server.domain.userdata.interview.entity.InterviewEntity; import repit.repit_api_server.domain.userdata.interview.entity.enums.InterviewMode; import repit.repit_api_server.domain.userdata.interview.entity.enums.Status; +import repit.repit_api_server.domain.userdata.persona.entity.PersonaEntity; import java.time.LocalDateTime; import java.util.List; @@ -24,23 +25,26 @@ public class InterviewResponse { private Long personaId; // N:1 면접관 목록. 진행 순서대로다. private List personaIds; + // 대표 면접관. 1:1은 그 면접관, N:1은 진행 순서 맨 앞 면접관이다. 면접관이 없으면 비어 있다. + private InterviewPersonaSummary persona; private String sessionId; private Status status; private LocalDateTime createdAt; // 면접을 만들 때 고른 기업. 고르지 않았으면 비어 있다. private CompanyBadge company; - public static InterviewResponse from(InterviewEntity interview) { - return from(interview, List.of()); - } - - public static InterviewResponse from(InterviewEntity interview, List personaIds) { + /** + * @param personaIds N:1 면접관 목록. 1:1은 빈 목록이다. + * @param persona 대표 면접관. 없으면 null이다. + */ + public static InterviewResponse from(InterviewEntity interview, List personaIds, PersonaEntity persona) { return new InterviewResponse( interview.getInterviewId(), interview.getUserId(), interview.getMode(), interview.getPersonaId(), personaIds, + InterviewPersonaSummary.of(persona), interview.getSessionId(), interview.getStatus(), interview.getCreatedAt(), diff --git a/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java b/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java index 2a5c67e..bd0e721 100644 --- a/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java +++ b/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java @@ -89,7 +89,7 @@ public InterviewResponse createInterview(Long userId, CreateInterviewRequest req .build(); InterviewEntity saved = interviewRepository.save(interview); - return InterviewResponse.from(saved); + return InterviewResponse.from(saved, List.of(), persona); } /** @@ -134,7 +134,9 @@ private InterviewResponse createMultiInterview(Long userId, List requested } interviewPersonaRepository.saveAll(members); - return InterviewResponse.from(saved, ordered.stream().map(PersonaEntity::getPersonaId).toList()); + // 대표 면접관은 진행 순서 맨 앞이다. 조회 응답과 같은 규칙이다. + return InterviewResponse.from(saved, ordered.stream().map(PersonaEntity::getPersonaId).toList(), + ordered.getFirst()); } /** 고른 기업을 지금 모습 그대로 복사해 둔다. 공개 기업이 아니면 404다. 고르지 않았으면 비어 있다. */ @@ -254,9 +256,47 @@ private static void requireSupportedJob(AuthUser authUser) { } } + /** + * 사용자의 면접 목록. 면접마다 대표 면접관을 붙인다. + * + *

N:1 면접관 명단과 면접관은 면접마다 따로 읽지 않고 한 번에 읽어 면접별로 나눈다. + * 그러지 않으면 목록이 길어지는 만큼 쿼리가 늘어난다. + */ public List getAllInterviewsByUserId(Long userId) { - return interviewRepository.findAllByUserId(userId).stream() - .map(interview -> InterviewResponse.from(interview, personaIdsOf(interview))) + List interviews = interviewRepository.findAllByUserId(userId); + + List multiInterviewIds = interviews.stream() + .filter(interview -> interview.getMode() == InterviewMode.MULTI) + .map(InterviewEntity::getInterviewId) + .toList(); + Map> personaIdsByInterview = multiInterviewIds.isEmpty() + ? Map.of() + : interviewPersonaRepository + .findAllByInterviewIdInOrderByInterviewIdAscPersonaOrderAsc(multiInterviewIds).stream() + .collect(Collectors.groupingBy(InterviewPersonaEntity::getInterviewId, + Collectors.mapping(InterviewPersonaEntity::getPersonaId, Collectors.toList()))); + + Map representativeIdByInterview = new HashMap<>(); + for (InterviewEntity interview : interviews) { + Long personaId = representativePersonaId(interview, + personaIdsByInterview.getOrDefault(interview.getInterviewId(), List.of())); + if (personaId != null) { + representativeIdByInterview.put(interview.getInterviewId(), personaId); + } + } + Map personaById = representativeIdByInterview.isEmpty() + ? Map.of() + : personaRepository.findAllById(new LinkedHashSet<>(representativeIdByInterview.values())).stream() + .collect(Collectors.toMap(PersonaEntity::getPersonaId, Function.identity())); + + return interviews.stream() + .map(interview -> { + Long representativeId = representativeIdByInterview.get(interview.getInterviewId()); + return InterviewResponse.from( + interview, + personaIdsByInterview.getOrDefault(interview.getInterviewId(), List.of()), + representativeId == null ? null : personaById.get(representativeId)); + }) .toList(); } @@ -264,7 +304,25 @@ public InterviewResponse getInterviewById(Long userId, Long interviewId) { InterviewEntity interview = interviewRepository.findById(interviewId) .orElseThrow(() -> BusinessException.notFound("면접을 찾을 수 없습니다")); verifyOwner(interview, userId); - return InterviewResponse.from(interview, personaIdsOf(interview)); + List personaIds = personaIdsOf(interview); + Long representativeId = representativePersonaId(interview, personaIds); + PersonaEntity persona = representativeId == null + ? null + : personaRepository.findById(representativeId).orElse(null); + return InterviewResponse.from(interview, personaIds, persona); + } + + /** + * 면접을 대표하는 면접관 id. 1:1은 그 면접의 면접관, N:1은 진행 순서 맨 앞 면접관이다. + * 피드백 응답이 성향·난이도를 대표 한 명으로 읽는 것과 같은 규칙이다. 걸린 면접관이 없으면 null이다. + * + * @param multiPersonaIds 진행 순서대로 정렬된 N:1 면접관 명단. 1:1은 빈 목록이다. + */ + private static Long representativePersonaId(InterviewEntity interview, List multiPersonaIds) { + if (interview.getMode() == InterviewMode.MULTI) { + return multiPersonaIds.isEmpty() ? null : multiPersonaIds.getFirst(); + } + return interview.getPersonaId(); } /** From f712d19950804f94e2d596cb55f8c7afe2f0ec47 Mon Sep 17 00:00:00 2001 From: gituser Date: Mon, 19 Oct 2026 13:27:19 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test:=20=EB=A9=B4=EC=A0=91=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20persona=20=EC=A1=B0=ED=9A=8C=C2=B7=EC=A7=81?= =?UTF-8?q?=EB=A0=AC=ED=99=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../InterviewResponseSerializationTest.java | 83 +++++++ .../service/InterviewServicePersonaTest.java | 227 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 src/test/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponseSerializationTest.java create mode 100644 src/test/java/repit/repit_api_server/domain/userdata/interview/service/InterviewServicePersonaTest.java diff --git a/src/test/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponseSerializationTest.java b/src/test/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponseSerializationTest.java new file mode 100644 index 0000000..54edafb --- /dev/null +++ b/src/test/java/repit/repit_api_server/domain/userdata/interview/dto/response/InterviewResponseSerializationTest.java @@ -0,0 +1,83 @@ +package repit.repit_api_server.domain.userdata.interview.dto.response; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import repit.repit_api_server.domain.userdata.interview.entity.InterviewEntity; +import repit.repit_api_server.domain.userdata.interview.entity.enums.InterviewMode; +import repit.repit_api_server.domain.userdata.interview.entity.enums.Status; +import repit.repit_api_server.domain.userdata.persona.entity.PersonaEntity; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Gender; +import repit.repit_api_server.domain.userdata.persona.entity.enums.InterviewTone; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Level; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Role; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Type; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** 웹이 읽는 persona 키 이름이 그대로 나가야 한다. 이름이 어긋나면 화면은 조용히 "미제공"이 된다. */ +class InterviewResponseSerializationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)); + + private static final InterviewEntity INTERVIEW = InterviewEntity.builder() + .interviewId(56L) + .userId(7L) + .personaId(56L) + .mode(InterviewMode.SOLO) + .sessionId("sess-56") + .status(Status.COMPLETED) + .build(); + + @Test + void 대표_면접관을_persona_객체로_내보낸다() { + PersonaEntity persona = PersonaEntity.builder() + .personaId(56L) + .personaName("김도윤-4898e87d") + .role(Role.TECH) + .type(Type.FRIENDLY) + .level(Level.EASY) + .tone(InterviewTone.GENTLE) + .career(3) + .gender(Gender.MALE) + .imageUrl("/img/56.svg") + .build(); + + contextRunner.run(context -> { + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + JsonNode json = objectMapper.valueToTree(InterviewResponse.from(INTERVIEW, List.of(), persona)); + JsonNode node = json.get("persona"); + + assertThat(node.propertyNames()).containsExactlyInAnyOrder( + "id", "personaName", "personaRole", "type", "level", "tone", "imageUrl"); + assertThat(node.get("id").asLong()).isEqualTo(56L); + assertThat(node.get("personaName").asString()).isEqualTo("김도윤-4898e87d"); + assertThat(node.get("personaRole").asString()).isEqualTo("TECH"); + assertThat(node.get("type").asString()).isEqualTo("FRIENDLY"); + assertThat(node.get("level").asString()).isEqualTo("EASY"); + assertThat(node.get("tone").asString()).isEqualTo("GENTLE"); + assertThat(node.get("imageUrl").asString()).isEqualTo("/img/56.svg"); + // 기존 필드는 그대로 남는다. + assertThat(json.get("personaId").asLong()).isEqualTo(56L); + }); + } + + @Test + void 면접관이_없으면_persona는_null이다() { + contextRunner.run(context -> { + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + JsonNode json = objectMapper.valueToTree(InterviewResponse.from(INTERVIEW, List.of(), null)); + + assertThat(json.has("persona")).isTrue(); + assertThat(json.get("persona").isNull()).isTrue(); + }); + } +} diff --git a/src/test/java/repit/repit_api_server/domain/userdata/interview/service/InterviewServicePersonaTest.java b/src/test/java/repit/repit_api_server/domain/userdata/interview/service/InterviewServicePersonaTest.java new file mode 100644 index 0000000..856c950 --- /dev/null +++ b/src/test/java/repit/repit_api_server/domain/userdata/interview/service/InterviewServicePersonaTest.java @@ -0,0 +1,227 @@ +package repit.repit_api_server.domain.userdata.interview.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import repit.repit_api_server.domain.userdata.answer.repository.AnswerRepository; +import repit.repit_api_server.domain.userdata.interview.dto.response.InterviewPersonaSummary; +import repit.repit_api_server.domain.userdata.interview.dto.response.InterviewResponse; +import repit.repit_api_server.domain.userdata.interview.entity.InterviewEntity; +import repit.repit_api_server.domain.userdata.interview.entity.InterviewPersonaEntity; +import repit.repit_api_server.domain.userdata.interview.entity.enums.InterviewMode; +import repit.repit_api_server.domain.userdata.interview.entity.enums.Status; +import repit.repit_api_server.domain.userdata.interview.repository.InterviewPersonaRepository; +import repit.repit_api_server.domain.userdata.interview.repository.InterviewRepository; +import repit.repit_api_server.domain.userdata.persona.entity.PersonaEntity; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Gender; +import repit.repit_api_server.domain.userdata.persona.entity.enums.InterviewTone; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Level; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Role; +import repit.repit_api_server.domain.userdata.persona.entity.enums.Type; +import repit.repit_api_server.domain.userdata.persona.repository.PersonaRepository; +import repit.repit_api_server.domain.userdata.question.repository.QuestionRepository; +import repit.repit_api_server.domain.userdata.question.service.QuestionTailorService; +import repit.repit_api_server.global.client.ChatServerClient; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyIterable; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 면접 조회 응답의 대표 면접관(persona). + * + *

웹 목록·상세는 성향·난이도·면접관 이름을 여기서 읽는다. 피드백이 아직 없는 면접도 + * 면접관은 정해져 있으므로 값이 나와야 한다. 1:1은 그 면접관, N:1은 진행 순서 맨 앞, + * 면접관이 없으면 null이다. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class InterviewServicePersonaTest { + + @Mock + private InterviewRepository interviewRepository; + @Mock + private QuestionRepository questionRepository; + @Mock + private ChatServerClient chatServerClient; + @Mock + private AnswerRepository answerRepository; + @Mock + private PersonaRepository personaRepository; + @Mock + private QuestionTailorService questionTailorService; + @Mock + private InterviewPersonaRepository interviewPersonaRepository; + + private InterviewService service; + + private static final Long USER_ID = 7L; + + @BeforeEach + void setUp() { + service = new InterviewService(interviewRepository, questionRepository, chatServerClient, + answerRepository, personaRepository, questionTailorService, + interviewPersonaRepository, null, null); + } + + @Test + void 일대일_단건은_그_면접의_면접관을_싣는다() { + when(interviewRepository.findById(3L)).thenReturn(Optional.of(solo(3L, 56L))); + when(personaRepository.findById(56L)).thenReturn(Optional.of( + persona(56L, "김도윤-4898e87d", Role.TECH, Type.FRIENDLY, Level.EASY, InterviewTone.GENTLE))); + + InterviewPersonaSummary persona = service.getInterviewById(USER_ID, 3L).getPersona(); + + // 이름은 저장값 그대로다. 접미사는 화면이 뗀다. + assertThat(persona).isEqualTo(new InterviewPersonaSummary(56L, "김도윤-4898e87d", Role.TECH, + Type.FRIENDLY, Level.EASY, InterviewTone.GENTLE, "/img/56.svg")); + } + + @Test + void N대1_단건은_진행_순서_맨_앞_면접관을_싣는다() { + when(interviewRepository.findById(4L)).thenReturn(Optional.of(multi(4L))); + when(interviewPersonaRepository.findAllByInterviewIdOrderByPersonaOrderAsc(4L)).thenReturn(List.of( + member(4L, 11L, 0), member(4L, 12L, 1))); + when(personaRepository.findById(11L)).thenReturn(Optional.of( + persona(11L, "박기술", Role.TECH, Type.METICULOUS, Level.HARD, InterviewTone.DIRECT))); + + InterviewResponse response = service.getInterviewById(USER_ID, 4L); + + assertThat(response.getPersonaIds()).containsExactly(11L, 12L); + assertThat(response.getPersona()).isEqualTo(new InterviewPersonaSummary(11L, "박기술", Role.TECH, + Type.METICULOUS, Level.HARD, InterviewTone.DIRECT, "/img/11.svg")); + verify(personaRepository, never()).findById(12L); + } + + @Test + void 면접관이_걸려_있지_않으면_null이다() { + when(interviewRepository.findById(3L)).thenReturn(Optional.of(solo(3L, null))); + when(interviewRepository.findById(4L)).thenReturn(Optional.of(multi(4L))); + when(interviewPersonaRepository.findAllByInterviewIdOrderByPersonaOrderAsc(4L)).thenReturn(List.of()); + + assertThat(service.getInterviewById(USER_ID, 3L).getPersona()).isNull(); + assertThat(service.getInterviewById(USER_ID, 4L).getPersona()).isNull(); + verify(personaRepository, never()).findById(any()); + } + + @Test + void 면접관이_지워졌으면_null이다() { + when(interviewRepository.findById(3L)).thenReturn(Optional.of(solo(3L, 56L))); + when(personaRepository.findById(56L)).thenReturn(Optional.empty()); + + assertThat(service.getInterviewById(USER_ID, 3L).getPersona()).isNull(); + } + + /** + * 목록은 면접관을 면접마다 읽지 않고 한 번에 읽는다. N:1 명단도 마찬가지다. + * 1:1·N:1·면접관 없음이 섞여 있어도 각자 맞는 값이 붙어야 한다. + */ + @Test + void 목록은_면접관을_한_번에_읽어_면접마다_대표_면접관을_싣는다() { + when(interviewRepository.findAllByUserId(USER_ID)).thenReturn(List.of( + solo(1L, 56L), multi(2L), solo(3L, null), multi(4L))); + when(interviewPersonaRepository.findAllByInterviewIdInOrderByInterviewIdAscPersonaOrderAsc(List.of(2L, 4L))) + .thenReturn(List.of(member(2L, 11L, 0), member(2L, 12L, 1))); + Map stored = Map.of( + 56L, persona(56L, "김도윤-4898e87d", Role.TECH, Type.FRIENDLY, Level.EASY, InterviewTone.GENTLE), + 11L, persona(11L, "박기술", Role.TECH, Type.METICULOUS, Level.HARD, InterviewTone.DIRECT), + 12L, persona(12L, "이인사", Role.HR, Type.METICULOUS, Level.HARD, InterviewTone.DIRECT)); + when(personaRepository.findAllById(anyIterable())).thenAnswer(invocation -> { + Iterable ids = invocation.getArgument(0); + return StreamSupport.stream(ids.spliterator(), false) + .map(stored::get) + .filter(java.util.Objects::nonNull) + .toList(); + }); + + Map byId = service.getAllInterviewsByUserId(USER_ID).stream() + .collect(Collectors.toMap(InterviewResponse::getInterviewId, Function.identity())); + + assertThat(byId.get(1L).getPersona().id()).isEqualTo(56L); + assertThat(byId.get(1L).getPersona().level()).isEqualTo(Level.EASY); + assertThat(byId.get(1L).getPersonaIds()).isEmpty(); + assertThat(byId.get(2L).getPersona().id()).isEqualTo(11L); + assertThat(byId.get(2L).getPersonaIds()).containsExactly(11L, 12L); + assertThat(byId.get(3L).getPersona()).isNull(); + // 명단이 비어 있는 N:1도 깨지지 않고 비어 나간다. + assertThat(byId.get(4L).getPersona()).isNull(); + assertThat(byId.get(4L).getPersonaIds()).isEmpty(); + + verify(personaRepository, times(1)).findAllById(anyIterable()); + verify(personaRepository, never()).findById(any()); + verify(interviewPersonaRepository, times(1)) + .findAllByInterviewIdInOrderByInterviewIdAscPersonaOrderAsc(any()); + verify(interviewPersonaRepository, never()).findAllByInterviewIdOrderByPersonaOrderAsc(any()); + } + + @Test + void 대표_면접관이_하나도_없으면_면접관을_읽지_않는다() { + when(interviewRepository.findAllByUserId(USER_ID)).thenReturn(List.of(solo(1L, null))); + + assertThat(service.getAllInterviewsByUserId(USER_ID)) + .singleElement() + .extracting(InterviewResponse::getPersona) + .isNull(); + verify(personaRepository, never()).findAllById(anyIterable()); + verify(interviewPersonaRepository, never()) + .findAllByInterviewIdInOrderByInterviewIdAscPersonaOrderAsc(any()); + } + + private static InterviewEntity solo(Long interviewId, Long personaId) { + return InterviewEntity.builder() + .interviewId(interviewId) + .userId(USER_ID) + .personaId(personaId) + .mode(InterviewMode.SOLO) + .sessionId("sess-" + interviewId) + .status(Status.COMPLETED) + .build(); + } + + private static InterviewEntity multi(Long interviewId) { + return InterviewEntity.builder() + .interviewId(interviewId) + .userId(USER_ID) + .mode(InterviewMode.MULTI) + .sessionId("sess-" + interviewId) + .status(Status.COMPLETED) + .build(); + } + + private static InterviewPersonaEntity member(Long interviewId, Long personaId, int order) { + return InterviewPersonaEntity.builder() + .interviewId(interviewId) + .personaId(personaId) + .personaOrder(order) + .build(); + } + + private static PersonaEntity persona(Long id, String name, Role role, Type type, Level level, InterviewTone tone) { + return PersonaEntity.builder() + .personaId(id) + .personaName(name) + .role(role) + .type(type) + .level(level) + .tone(tone) + .career(3) + .gender(Gender.MALE) + .imageUrl("/img/" + id + ".svg") + .build(); + } +} From d27e93f1c54b0c354631ae9ec11ae2527a04ec46 Mon Sep 17 00:00:00 2001 From: gituser Date: Mon, 19 Oct 2026 13:36:48 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EA=B8=B0=EC=97=85=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=20=ED=86=B5=ED=95=A9=20=ED=9B=84=20=EB=A9=B4=EC=A0=91?= =?UTF-8?q?=EA=B4=80=20=EC=9D=91=EB=8B=B5=20=EC=83=9D=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=ED=9A=8C=EA=B7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interview/controller/InterviewControllerCreateTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/repit/repit_api_server/domain/userdata/interview/controller/InterviewControllerCreateTest.java b/src/test/java/repit/repit_api_server/domain/userdata/interview/controller/InterviewControllerCreateTest.java index 1c6d052..2094886 100644 --- a/src/test/java/repit/repit_api_server/domain/userdata/interview/controller/InterviewControllerCreateTest.java +++ b/src/test/java/repit/repit_api_server/domain/userdata/interview/controller/InterviewControllerCreateTest.java @@ -96,7 +96,7 @@ void tearDown() { } private static InterviewResponse response(Long interviewId) { - return new InterviewResponse(interviewId, USER_ID, InterviewMode.SOLO, 1L, List.of(), "session", + return new InterviewResponse(interviewId, USER_ID, InterviewMode.SOLO, 1L, List.of(), null, "session", Status.IN_PROGRESS, LocalDateTime.now(), new CompanyBadge(12L, "샘플커머스")); }