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
Expand Up @@ -37,7 +37,7 @@ public class Answer extends BaseEntity {
@JoinColumn(name = "question_id", nullable = false)
private Question question;

@Column(name = "content", nullable = false, length = 2_000)
@Column(name = "content", nullable = false, length = 10_000)
private String content;

@Column(name = "answer_rank", nullable = false)
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/com/samhap/kokomen/category/domain/Category.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.samhap.kokomen.category.domain;

import com.samhap.kokomen.global.constant.AwsConstant;
import java.util.Arrays;
import java.util.List;
import lombok.Getter;

Expand Down Expand Up @@ -71,7 +72,14 @@ public enum Category {
자바스크립트는 웹 브라우저와 서버에서 실행되는 동적 프로그래밍 언어로, 현대 웹 개발의 핵심 기술입니다.
주로 자바스크립트의 언어에 대한 이해도를 묻는 질문과 자바스크립트를 동작시키는 엔진, 추가적으로 정적 분석을 위한 타입스크립트에 대한 질문 또한 일부 출제됩니다.
""",
"kokomen-javascript-typescript.png");
"kokomen-javascript-typescript.png"),
LIVE_CODING("라이브 코테",
"""
라이브 코테는 실제 코딩 인터뷰처럼 코딩 문제를 풀고, 제출한 코드에 대해 면접관과 대화하며 진행됩니다.
문제를 해결하는 코드를 작성한 뒤에는 시간·공간 복잡도, 최적화 방안, 엣지 케이스, 자료구조 선택 근거 등에 대한 꼬리 질문이 이어집니다.
단순히 정답 코드를 작성하는 것을 넘어, 자신의 풀이를 설명하고 개선할 수 있는 능력을 평가하는 영역입니다.
""",
"kokomen-live-coding.png");

private static final String BASE_URL = AwsConstant.CLOUD_FRONT_DOMAIN_URL + "category-image/";

Expand All @@ -85,7 +93,9 @@ public enum Category {
this.imageUrl = BASE_URL + imageUrl;
}

private static final List<Category> CATEGORIES = List.of(values());
private static final List<Category> CATEGORIES = Arrays.stream(values())
.filter(category -> category != LIVE_CODING)
.toList();

public static List<Category> getCategories() {
return CATEGORIES;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.samhap.kokomen.interview.service.dto.proceedstate.InterviewProceedStateResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -34,7 +33,8 @@ public ResponseEntity<Void> proceedInterviewBlockAsync(
@Authentication(required = false) MemberAuth memberAuth,
ClientIp clientIp
) {
interviewProceedFacadeService.proceedInterviewByBedrockFlow(interviewId, curQuestionId, answerRequest, memberAuth,
interviewProceedFacadeService.proceedInterviewByBedrockFlow(interviewId, curQuestionId, answerRequest,
memberAuth,
clientIp);
return ResponseEntity.noContent().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ public Interview(Member member, RootQuestion rootQuestion, Integer maxQuestionCo
InterviewType.CATEGORY_BASED, null, null, 0L, 0L, null, null);
}

public Interview(Member member, RootQuestion rootQuestion, Integer maxQuestionCount, InterviewMode interviewMode,
InterviewType interviewType) {
this(null, member, rootQuestion, maxQuestionCount, InterviewState.IN_PROGRESS, interviewMode,
interviewType, null, null, 0L, 0L, null, null);
}

public Interview(Member member, GeneratedQuestion generatedQuestion, Integer maxQuestionCount,
InterviewMode interviewMode) {
this(null, member, null, maxQuestionCount, InterviewState.IN_PROGRESS, interviewMode,
Expand Down Expand Up @@ -178,6 +184,10 @@ public boolean isResumeBased() {
return this.interviewType == InterviewType.RESUME_BASED;
}

public boolean isLiveCoding() {
return this.interviewType == InterviewType.LIVE_CODING;
}

public String getDisplayCategory() {
if (isResumeBased()) {
return RESUME_BASED_DISPLAY_CATEGORY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public enum InterviewType {
CATEGORY_BASED,
RESUME_BASED
RESUME_BASED,
LIVE_CODING
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public AnswerFeedbackBedrockClient(

public String requestAnswerFeedback(QuestionAndAnswers questionAndAnswers, AnswerRank curAnswerRank) {
ConverseResponse response = converseClient.converse(
InterviewBedrockRequestFactory.createAnswerFeedbackSystem(curAnswerRank),
InterviewBedrockRequestFactory.createAnswerFeedbackSystem(
questionAndAnswers.getInterview().getInterviewType(), curAnswerRank),
InterviewBedrockRequestFactory.createAnswerFeedbackMessages(questionAndAnswers),
InterviewBedrockRequestFactory.createAnswerFeedbackToolConfig(),
properties.answerFeedbackMaxTokens(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public BedrockConverseResponse requestToBedrock(QuestionAndAnswers questionAndAn

private BedrockConverseResponse requestProceed(QuestionAndAnswers questionAndAnswers) {
ConverseResponse response = converseClient.converse(
InterviewBedrockRequestFactory.createProceedSystem(),
InterviewBedrockRequestFactory.createProceedSystem(questionAndAnswers.getInterview().getInterviewType()),
InterviewBedrockRequestFactory.createProceedMessages(questionAndAnswers),
InterviewBedrockRequestFactory.createProceedToolConfig(),
properties.proceedMaxTokens(),
Expand All @@ -47,7 +47,7 @@ private BedrockConverseResponse requestProceed(QuestionAndAnswers questionAndAns

private BedrockConverseResponse requestEnd(QuestionAndAnswers questionAndAnswers) {
ConverseResponse response = converseClient.converse(
InterviewBedrockRequestFactory.createEndSystem(),
InterviewBedrockRequestFactory.createEndSystem(questionAndAnswers.getInterview().getInterviewType()),
InterviewBedrockRequestFactory.createProceedMessages(questionAndAnswers),
InterviewBedrockRequestFactory.createEndToolConfig(),
properties.endMaxTokens(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.samhap.kokomen.answer.domain.Answer;
import com.samhap.kokomen.answer.domain.AnswerRank;
import com.samhap.kokomen.interview.domain.InterviewType;
import com.samhap.kokomen.interview.domain.Question;
import com.samhap.kokomen.interview.tool.CodingInterviewBedrockSystemMessageConstant;
import com.samhap.kokomen.interview.tool.InterviewBedrockSystemMessageConstant;
import com.samhap.kokomen.interview.tool.QuestionAndAnswers;
import java.util.ArrayList;
Expand All @@ -28,22 +30,32 @@ public final class InterviewBedrockRequestFactory {
private InterviewBedrockRequestFactory() {
}

public static List<SystemContentBlock> createProceedSystem() {
public static List<SystemContentBlock> createProceedSystem(InterviewType interviewType) {
String prompt = interviewType == InterviewType.LIVE_CODING
? CodingInterviewBedrockSystemMessageConstant.CODING_IN_PROGRESS_RANK_AND_NEXT_QUESTION_PROMPT
: InterviewBedrockSystemMessageConstant.IN_PROGRESS_RANK_AND_NEXT_QUESTION_PROMPT;
return List.of(SystemContentBlock.builder()
.text(InterviewBedrockSystemMessageConstant.IN_PROGRESS_RANK_AND_NEXT_QUESTION_PROMPT)
.text(prompt)
.build());
}

public static List<SystemContentBlock> createEndSystem() {
public static List<SystemContentBlock> createEndSystem(InterviewType interviewType) {
String prompt = interviewType == InterviewType.LIVE_CODING
? CodingInterviewBedrockSystemMessageConstant.CODING_END_PROMPT
: InterviewBedrockSystemMessageConstant.END_PROMPT;
return List.of(SystemContentBlock.builder()
.text(InterviewBedrockSystemMessageConstant.END_PROMPT)
.text(prompt)
.build());
}

public static List<SystemContentBlock> createAnswerFeedbackSystem(AnswerRank curAnswerRank) {
public static List<SystemContentBlock> createAnswerFeedbackSystem(InterviewType interviewType,
AnswerRank curAnswerRank) {
String prompt = interviewType == InterviewType.LIVE_CODING
? CodingInterviewBedrockSystemMessageConstant.CODING_ANSWER_FEEDBACK_PROMPT
: InterviewBedrockSystemMessageConstant.ANSWER_FEEDBACK_PROMPT;
return List.of(
SystemContentBlock.builder()
.text(InterviewBedrockSystemMessageConstant.ANSWER_FEEDBACK_PROMPT)
.text(prompt)
.build(),
SystemContentBlock.builder()
.text("<context>대상 답변 rank: " + curAnswerRank.name() + "</context>")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ Optional<RootQuestion> findRootQuestionByCategoryAndStateAndQuestionOrder(Catego
List<RootQuestion> findAllByCategoryAndState(Category category, RootQuestionState state);

List<RootQuestion> findAllByState(RootQuestionState state);

List<RootQuestion> findAllByStateAndCategoryNot(RootQuestionState state, Category category);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.samhap.kokomen.interview.service;

import com.samhap.kokomen.category.domain.Category;
import com.samhap.kokomen.global.dto.ClientIp;
import com.samhap.kokomen.global.dto.MemberAuth;
import com.samhap.kokomen.global.exception.BadRequestException;
Expand All @@ -8,6 +9,7 @@
import com.samhap.kokomen.interview.domain.GeneratedQuestion;
import com.samhap.kokomen.interview.domain.Interview;
import com.samhap.kokomen.interview.domain.InterviewMode;
import com.samhap.kokomen.interview.domain.InterviewType;
import com.samhap.kokomen.interview.domain.Question;
import com.samhap.kokomen.interview.domain.ResumeQuestionGeneration;
import com.samhap.kokomen.interview.domain.RootQuestion;
Expand Down Expand Up @@ -56,13 +58,15 @@ public class InterviewStartFacadeService {
@Transactional
public InterviewStartResponse startInterview(InterviewRequest interviewRequest, MemberAuth memberAuth) {
InterviewMode interviewMode = interviewRequest.mode();
validateModeSupportedForCategory(interviewRequest.category(), interviewMode);
int requiredTokenCount = interviewRequest.maxQuestionCount() * interviewMode.getRequiredTokenCount()
- TOKEN_NOT_REQUIRED_FOR_ROOT_QUESTION_VOICE;
tokenFacadeService.validateEnoughTokens(memberAuth.memberId(), requiredTokenCount);
Member member = memberService.readById(memberAuth.memberId());
RootQuestion rootQuestion = rootQuestionService.findNextRootQuestionForMember(member, interviewRequest);
Interview interview = interviewService.saveInterview(
new Interview(member, rootQuestion, interviewRequest.maxQuestionCount(), interviewMode));
new Interview(member, rootQuestion, interviewRequest.maxQuestionCount(), interviewMode,
resolveInterviewType(rootQuestion.getCategory())));
Question question = questionService.saveQuestion(new Question(interview, rootQuestion.getContent()));

if (interviewMode == InterviewMode.VOICE) {
Expand Down Expand Up @@ -99,13 +103,15 @@ public static String createGuestInterviewStartedLockKey(ClientIp clientIp) {
public InterviewStartResponse startRootQuestionCustomInterview(RootQuestionCustomInterviewRequest request,
MemberAuth memberAuth) {
InterviewMode interviewMode = request.mode();
RootQuestion rootQuestion = rootQuestionService.readRootQuestion(request.rootQuestionId());
validateModeSupportedForCategory(rootQuestion.getCategory(), interviewMode);
int requiredTokenCount = request.maxQuestionCount() * interviewMode.getRequiredTokenCount()
- TOKEN_NOT_REQUIRED_FOR_ROOT_QUESTION_VOICE;
tokenFacadeService.validateEnoughTokens(memberAuth.memberId(), requiredTokenCount);
Member member = memberService.readById(memberAuth.memberId());
RootQuestion rootQuestion = rootQuestionService.readRootQuestion(request.rootQuestionId());
Interview interview = interviewService.saveInterview(
new Interview(member, rootQuestion, request.maxQuestionCount(), interviewMode));
new Interview(member, rootQuestion, request.maxQuestionCount(), interviewMode,
resolveInterviewType(rootQuestion.getCategory())));
Question question = questionService.saveQuestion(new Question(interview, rootQuestion.getContent()));

if (interviewMode == InterviewMode.VOICE) {
Expand Down Expand Up @@ -154,4 +160,14 @@ private void validateGenerationCompleted(ResumeQuestionGeneration generation) {
throw new BadRequestException("질문 생성이 완료되지 않았습니다.");
}
}

private InterviewType resolveInterviewType(Category category) {
return category == Category.LIVE_CODING ? InterviewType.LIVE_CODING : InterviewType.CATEGORY_BASED;
}

private void validateModeSupportedForCategory(Category category, InterviewMode interviewMode) {
if (category == Category.LIVE_CODING && interviewMode == InterviewMode.VOICE) {
throw new BadRequestException("라이브 코테는 음성 모드를 지원하지 않습니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private InterviewResultResponse findGuestInterviewResult(Long interviewId, Clien
}

private List<RootQuestionReferenceAnswer> getReferenceAnswers(Interview interview) {
if (interview.isResumeBased()) {
if (interview.isResumeBased() || interview.isLiveCoding()) {
return List.of();
}
return findRootQuestionReferenceAnswers(interview.getRootQuestion().getId(), interview.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.hibernate.validator.constraints.Length;

public record AnswerRequestV2(
@Length(max = 2000, message = "answer는 최대 2000자까지 입력할 수 있습니다.")
@Length(max = 10000, message = "answer는 최대 10000자까지 입력할 수 있습니다.")
@NotBlank(message = "answer는 비어있을 수 없습니다.")
String answer,
@NotNull(message = "mode는 null일 수 없습니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class RootQuestionService {
private final QuestionVoicePathResolver questionVoicePathResolver;

public RootQuestion readRandomActiveRootQuestion() {
List<RootQuestion> rootQuestions = rootQuestionRepository.findAllByState(RootQuestionState.ACTIVE);
List<RootQuestion> rootQuestions =
rootQuestionRepository.findAllByStateAndCategoryNot(RootQuestionState.ACTIVE, Category.LIVE_CODING);
if (rootQuestions.isEmpty()) {
throw new NotFoundException("활성화된 루트 질문이 존재하지 않습니다.");
}
Expand Down
Loading
Loading