Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

/**
* 면접 응답에 붙는 대표 면접관. 목록·상세 화면이 성향·난이도·면접관 이름을 여기서 읽는다.
*
* <p>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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,23 +25,26 @@ public class InterviewResponse {
private Long personaId;
// N:1 면접관 목록. 진행 순서대로다.
private List<Long> 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<Long> personaIds) {
/**
* @param personaIds N:1 면접관 목록. 1:1은 빈 목록이다.
* @param persona 대표 면접관. 없으면 null이다.
*/
public static InterviewResponse from(InterviewEntity interview, List<Long> personaIds, PersonaEntity persona) {
return new InterviewResponse(
interview.getInterviewId(),
interview.getUserId(),
interview.getMode(),
interview.getPersonaId(),
personaIds,
InterviewPersonaSummary.of(persona),
interview.getSessionId(),
interview.getStatus(),
interview.getCreatedAt(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,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);
}

/**
Expand Down Expand Up @@ -138,7 +138,9 @@ private InterviewResponse createMultiInterview(Long userId, List<Long> 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());
}

/**
Expand Down Expand Up @@ -261,17 +263,73 @@ private static void requireSupportedJob(AuthUser authUser) {
}
}

/**
* 사용자의 면접 목록. 면접마다 대표 면접관을 붙인다.
*
* <p>N:1 면접관 명단과 면접관은 면접마다 따로 읽지 않고 한 번에 읽어 면접별로 나눈다.
* 그러지 않으면 목록이 길어지는 만큼 쿼리가 늘어난다.
*/
public List<InterviewResponse> getAllInterviewsByUserId(Long userId) {
return interviewRepository.findAllByUserId(userId).stream()
.map(interview -> InterviewResponse.from(interview, personaIdsOf(interview)))
List<InterviewEntity> interviews = interviewRepository.findAllByUserId(userId);

List<Long> multiInterviewIds = interviews.stream()
.filter(interview -> interview.getMode() == InterviewMode.MULTI)
.map(InterviewEntity::getInterviewId)
.toList();
Map<Long, List<Long>> personaIdsByInterview = multiInterviewIds.isEmpty()
? Map.of()
: interviewPersonaRepository
.findAllByInterviewIdInOrderByInterviewIdAscPersonaOrderAsc(multiInterviewIds).stream()
.collect(Collectors.groupingBy(InterviewPersonaEntity::getInterviewId,
Collectors.mapping(InterviewPersonaEntity::getPersonaId, Collectors.toList())));

Map<Long, Long> 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<Long, PersonaEntity> 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();
}

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<Long> 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<Long> multiPersonaIds) {
if (interview.getMode() == InterviewMode.MULTI) {
return multiPersonaIds.isEmpty() ? null : multiPersonaIds.getFirst();
}
return interview.getPersonaId();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "샘플커머스"));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
}
}
Loading
Loading