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 @@ -71,7 +71,14 @@ public enum Category {
자바스크립트는 웹 브라우저와 서버에서 실행되는 동적 프로그래밍 언어로, 현대 웹 개발의 핵심 기술입니다.
주로 자바스크립트의 언어에 대한 이해도를 묻는 질문과 자바스크립트를 동작시키는 엔진, 추가적으로 정적 분석을 위한 타입스크립트에 대한 질문 또한 일부 출제됩니다.
""",
"kokomen-javascript-typescript.png");
"kokomen-javascript-typescript.png"),
PERSONALITY("인성 면접",
"""
인성 면접은 지원자의 가치관, 협업 태도, 문제 해결 방식 등 소프트 스킬을 평가하는 면접입니다.
실제 경험을 바탕으로 한 행동 기반(STAR) 질문이 출제되며, 상황·본인의 역할·행동·결과를 구체적으로 설명하는 것이 중요합니다.
기술 역량을 넘어 팀워크, 소통, 자기 성찰, 직무 적합성을 보여줄 수 있는지를 중점적으로 살펴봅니다.
""",
"kokomen-personality.png");

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ public boolean isLiveCoding() {
return this.interviewType == InterviewType.LIVE_CODING;
}

public boolean isPersonality() {
return this.interviewType == InterviewType.PERSONALITY;
}

public String getDisplayCategory() {
if (isResumeBased()) {
return RESUME_BASED_DISPLAY_CATEGORY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum InterviewType {
CATEGORY_BASED,
RESUME_BASED,
LIVE_CODING
LIVE_CODING,
PERSONALITY
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,8 @@ public String createInitialQuestionContent() {
public boolean isCode() {
return this.questionType == RootQuestionType.CODE;
}

public boolean isPersonality() {
return this.category == Category.PERSONALITY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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.PersonalityInterviewBedrockSystemMessageConstant;
import com.samhap.kokomen.interview.tool.QuestionAndAnswers;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -31,28 +32,37 @@ private InterviewBedrockRequestFactory() {
}

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;
String prompt = switch (interviewType) {
case LIVE_CODING ->
CodingInterviewBedrockSystemMessageConstant.CODING_IN_PROGRESS_RANK_AND_NEXT_QUESTION_PROMPT;
case PERSONALITY ->
PersonalityInterviewBedrockSystemMessageConstant.PERSONALITY_IN_PROGRESS_RANK_AND_NEXT_QUESTION_PROMPT;
case CATEGORY_BASED, RESUME_BASED ->
InterviewBedrockSystemMessageConstant.IN_PROGRESS_RANK_AND_NEXT_QUESTION_PROMPT;
};
return List.of(SystemContentBlock.builder()
.text(prompt)
.build());
}

public static List<SystemContentBlock> createEndSystem(InterviewType interviewType) {
String prompt = interviewType == InterviewType.LIVE_CODING
? CodingInterviewBedrockSystemMessageConstant.CODING_END_PROMPT
: InterviewBedrockSystemMessageConstant.END_PROMPT;
String prompt = switch (interviewType) {
case LIVE_CODING -> CodingInterviewBedrockSystemMessageConstant.CODING_END_PROMPT;
case PERSONALITY -> PersonalityInterviewBedrockSystemMessageConstant.PERSONALITY_END_PROMPT;
case CATEGORY_BASED, RESUME_BASED -> InterviewBedrockSystemMessageConstant.END_PROMPT;
};
return List.of(SystemContentBlock.builder()
.text(prompt)
.build());
}

public static List<SystemContentBlock> createAnswerFeedbackSystem(InterviewType interviewType,
AnswerRank curAnswerRank) {
String prompt = interviewType == InterviewType.LIVE_CODING
? CodingInterviewBedrockSystemMessageConstant.CODING_ANSWER_FEEDBACK_PROMPT
: InterviewBedrockSystemMessageConstant.ANSWER_FEEDBACK_PROMPT;
String prompt = switch (interviewType) {
case LIVE_CODING -> CodingInterviewBedrockSystemMessageConstant.CODING_ANSWER_FEEDBACK_PROMPT;
case PERSONALITY -> PersonalityInterviewBedrockSystemMessageConstant.PERSONALITY_ANSWER_FEEDBACK_PROMPT;
case CATEGORY_BASED, RESUME_BASED -> InterviewBedrockSystemMessageConstant.ANSWER_FEEDBACK_PROMPT;
};
return List.of(
SystemContentBlock.builder()
.text(prompt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,7 @@ Optional<RootQuestion> findRootQuestionByCategoryAndStateAndQuestionOrder(Catego
List<RootQuestion> findAllByState(RootQuestionState state);

List<RootQuestion> findAllByStateAndQuestionType(RootQuestionState state, RootQuestionType questionType);

List<RootQuestion> findAllByStateAndQuestionTypeAndCategoryNot(RootQuestionState state, RootQuestionType questionType,
Category category);
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,13 @@ public InterviewStartResponse startRootQuestionCustomInterview(RootQuestionCusto
}

private InterviewType resolveInterviewType(RootQuestion rootQuestion) {
return rootQuestion.isCode() ? InterviewType.LIVE_CODING : InterviewType.CATEGORY_BASED;
if (rootQuestion.isCode()) {
return InterviewType.LIVE_CODING;
}
if (rootQuestion.isPersonality()) {
return InterviewType.PERSONALITY;
}
return InterviewType.CATEGORY_BASED;
}

private void validateLiveCodingNotVoice(InterviewRequest interviewRequest, InterviewMode interviewMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public class RootQuestionService {
private final QuestionVoicePathResolver questionVoicePathResolver;

public RootQuestion readRandomActiveRootQuestion() {
List<RootQuestion> rootQuestions = rootQuestionRepository.findAllByStateAndQuestionType(
RootQuestionState.ACTIVE, RootQuestionType.GENERAL);
List<RootQuestion> rootQuestions = rootQuestionRepository.findAllByStateAndQuestionTypeAndCategoryNot(
RootQuestionState.ACTIVE, RootQuestionType.GENERAL, Category.PERSONALITY);
if (rootQuestions.isEmpty()) {
throw new NotFoundException("활성화된 루트 질문이 존재하지 않습니다.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,77 @@ public final class GptSystemMessageConstant {
InterviewPromptFragments.FEEDBACK_TONE_BY_RANK
);

public static final String PERSONALITY_PROCEED_SYSTEM_MESSAGE = """
<role>
%s
</role>

<task>
아래는 인성 면접의 대화 흐름이다. assistant 메시지는 면접관의 인성 질문, user 메시지는 면접자가 본인의 경험·태도·생각을 서술한 답변이다.
가장 최근 질문에 대한 답변(가장 마지막 user 메시지)만 평가하고, 그 답변에 대한 피드백과 다음 꼬리 질문을 생성하라.
</task>

%s
%s
%s
%s
%s
%s
%s

<output>
반드시 제공된 함수(generate_feedback)를 호출하여 다음 네 필드를 함께 제출하라.
- reasoning : answer_analysis(rubric 항목별 평가 근거)와 question_planning(follow_up_question_algorithm 단계별 사고 과정)을 한 단락으로 작성. 사용자에게 노출되지 않으므로 자유 형식이지만 두 항목을 모두 포함.
- rank : 위 평가 기준과 랭크 매핑에 따라 산출된 A/B/C/D/F 중 한 글자.
- feedback : 가장 최근 답변에 대한 3-4문장 피드백. answer_rank에 맞는 톤. 존댓말, 점수/랭크 미언급, 개행 없이 한 단락.
- next_question : 위 단일 질문 제약을 모두 충족하는 꼬리 질문 1문장.
</output>
""".formatted(
PersonalityInterviewPromptFragments.PERSONA,
PersonalityInterviewPromptFragments.SECURITY_RULES,
PersonalityInterviewPromptFragments.LENGTH_NEUTRAL,
PersonalityInterviewPromptFragments.RUBRIC,
InterviewPromptFragments.RANK_MAPPING,
InterviewPromptFragments.FEEDBACK_TONE_BY_RANK,
PersonalityInterviewPromptFragments.FOLLOW_UP_QUESTION_ALGORITHM,
PersonalityInterviewPromptFragments.SINGLE_QUESTION_CONSTRAINT
);

public static final String PERSONALITY_END_SYSTEM_MESSAGE = """
<role>
%s
</role>

<task>
아래는 인성 면접의 전체 대화 흐름이다. assistant 메시지는 면접관의 인성 질문, user 메시지는 면접자가 본인의 경험·태도·생각을 서술한 답변이다.
가장 최근 답변에 대한 rank와 feedback, 그리고 면접 전체에 대한 종합 평가(strengths, improvements, learning_direction)를 작성하라.
</task>

%s
%s
%s
%s
%s

<output>
반드시 제공된 함수(generate_total_feedback)를 호출하여 다음 여섯 필드를 함께 제출하라.
- reasoning : last_answer_analysis(가장 최근 답변에 대한 rubric 평가 근거)와 전체 인성 면접의 강점/개선/학습 방향 정리를 한 단락으로 작성. 사용자에게 노출되지 않음.
- rank : 가장 최근 답변에 대한 랭크. A, B, C, D, F 중 한 글자. 전체 답변 누적이 아닌 가장 최근 답변만을 기준으로 평가한다.
- feedback : 가장 최근 답변에 대한 3-4문장 피드백. answer_rank에 맞는 톤. 존댓말, 점수/랭크 미언급, 개행 없이 한 단락.
- strengths : 면접자의 강점 1-2문장. 존댓말, 점수/랭크 미언급.
- improvements : 보완·개선 영역 1-2문장. 존댓말, 점수/랭크 미언급.
- learning_direction : 향후 보완 방향 1-2문장. 존댓말, 점수/랭크 미언급.
strengths/improvements/learning_direction 세 필드는 서버에서 한 단락으로 합성되므로 각 항목은 독립적인 한두 문장으로 자연스럽게 이어지게 작성한다. 인사·점수·랭크 언급 금지.
</output>
""".formatted(
PersonalityInterviewPromptFragments.PERSONA,
PersonalityInterviewPromptFragments.SECURITY_RULES,
PersonalityInterviewPromptFragments.LENGTH_NEUTRAL,
PersonalityInterviewPromptFragments.RUBRIC,
InterviewPromptFragments.RANK_MAPPING,
InterviewPromptFragments.FEEDBACK_TONE_BY_RANK
);

private GptSystemMessageConstant() {
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.samhap.kokomen.interview.tool;

import com.samhap.kokomen.answer.domain.Answer;
import com.samhap.kokomen.interview.domain.InterviewType;
import com.samhap.kokomen.interview.domain.Question;
import com.samhap.kokomen.interview.external.dto.request.GptMessage;
import java.util.ArrayList;
Expand All @@ -13,9 +12,11 @@ private InterviewMessagesFactory() {
}

public static List<GptMessage> createGptProceedMessages(QuestionAndAnswers questionAndAnswers) {
String systemMessage = isLiveCoding(questionAndAnswers)
? GptSystemMessageConstant.CODING_PROCEED_SYSTEM_MESSAGE
: GptSystemMessageConstant.PROCEED_SYSTEM_MESSAGE;
String systemMessage = switch (questionAndAnswers.getInterview().getInterviewType()) {
case LIVE_CODING -> GptSystemMessageConstant.CODING_PROCEED_SYSTEM_MESSAGE;
case PERSONALITY -> GptSystemMessageConstant.PERSONALITY_PROCEED_SYSTEM_MESSAGE;
case CATEGORY_BASED, RESUME_BASED -> GptSystemMessageConstant.PROCEED_SYSTEM_MESSAGE;
};
List<GptMessage> gptMessages = new ArrayList<>();
gptMessages.add(new GptMessage("system", systemMessage));
addGptMessages(questionAndAnswers, gptMessages);
Expand All @@ -24,20 +25,18 @@ public static List<GptMessage> createGptProceedMessages(QuestionAndAnswers quest
}

public static List<GptMessage> createGptEndMessages(QuestionAndAnswers questionAndAnswers) {
String systemMessage = isLiveCoding(questionAndAnswers)
? GptSystemMessageConstant.CODING_END_SYSTEM_MESSAGE
: GptSystemMessageConstant.END_SYSTEM_MESSAGE;
String systemMessage = switch (questionAndAnswers.getInterview().getInterviewType()) {
case LIVE_CODING -> GptSystemMessageConstant.CODING_END_SYSTEM_MESSAGE;
case PERSONALITY -> GptSystemMessageConstant.PERSONALITY_END_SYSTEM_MESSAGE;
case CATEGORY_BASED, RESUME_BASED -> GptSystemMessageConstant.END_SYSTEM_MESSAGE;
};
List<GptMessage> gptMessages = new ArrayList<>();
gptMessages.add(new GptMessage("system", systemMessage));
addGptMessages(questionAndAnswers, gptMessages);

return gptMessages;
}

private static boolean isLiveCoding(QuestionAndAnswers questionAndAnswers) {
return questionAndAnswers.getInterview().getInterviewType() == InterviewType.LIVE_CODING;
}

private static void addGptMessages(QuestionAndAnswers questionAndAnswers, List<GptMessage> gptMessages) {
List<Question> questions = questionAndAnswers.getQuestions();
List<Answer> prevAnswers = questionAndAnswers.getPrevAnswers();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.samhap.kokomen.interview.tool;

public final class PersonalityInterviewBedrockSystemMessageConstant {

public static final String PERSONALITY_IN_PROGRESS_RANK_AND_NEXT_QUESTION_PROMPT = """
<role>
%s
</role>

<task>
아래는 인성 면접의 대화 흐름이다. assistant 메시지는 면접관의 인성 질문, user 메시지는 면접자가 본인의 경험·태도·생각을 서술한 답변이다.
가장 최근 질문에 대한 답변(가장 마지막 user 메시지)만 평가하고, 그 답변을 토대로 다음 꼬리 질문 한 개를 생성하라.
</task>

%s
%s
%s
%s
%s
%s

<output>
반드시 제공된 도구를 호출해 reasoning, rank, next_question 세 필드를 함께 제출하라.
- reasoning : answer_analysis(rubric 항목별 답변 평가 근거)와 question_planning(follow_up_question_algorithm 단계별 사고 과정)을 한 단락으로 작성. 사용자에게 노출되지 않으므로 자유 형식이지만 두 항목을 모두 포함.
- rank : 가장 최근 답변에 대한 평가 등급. A, B, C, D, F 중 한 글자.
- next_question : single_question_constraint를 만족하는 꼬리 질문 1문장.
</output>
""".formatted(
PersonalityInterviewPromptFragments.PERSONA,
PersonalityInterviewPromptFragments.SECURITY_RULES,
PersonalityInterviewPromptFragments.LENGTH_NEUTRAL,
PersonalityInterviewPromptFragments.RUBRIC,
InterviewPromptFragments.RANK_MAPPING,
PersonalityInterviewPromptFragments.FOLLOW_UP_QUESTION_ALGORITHM,
PersonalityInterviewPromptFragments.SINGLE_QUESTION_CONSTRAINT
);

public static final String PERSONALITY_END_PROMPT = """
<role>
%s
</role>

<task>
아래는 인성 면접의 전체 대화 흐름이다. assistant 메시지는 면접관의 인성 질문, user 메시지는 면접자가 본인의 경험·태도·생각을 서술한 답변이다.
가장 최근 답변에 대한 rank와 feedback, 그리고 면접 전체에 대한 종합 평가(strengths, improvements, learning_direction)를 작성하라.
</task>

%s
%s
%s
%s
%s

<output>
제공된 도구를 호출해 reasoning, rank, feedback, strengths, improvements, learning_direction 여섯 필드를 함께 제출하라.
- reasoning : last_answer_analysis(가장 최근 답변에 대한 rubric 평가 근거)와 전체 인성 면접의 강점/개선/학습 방향 정리를 한 단락으로 작성. 사용자에게 노출되지 않음.
- rank : 가장 최근 답변에 대한 랭크. A, B, C, D, F 중 한 글자. 전체 답변 누적이 아닌 가장 최근 답변만을 기준으로 평가한다.
- feedback : 가장 최근 답변에 대한 3-4문장의 정중한 피드백. 존댓말 사용, 점수/랭크 미언급, 개행 없이 한 단락.
- strengths : 면접자의 강점 1-2문장. 존댓말, 점수/랭크 미언급.
- improvements : 보완·개선 영역 1-2문장. 존댓말, 점수/랭크 미언급.
- learning_direction : 향후 보완 방향 1-2문장. 존댓말, 점수/랭크 미언급.
strengths/improvements/learning_direction 세 필드는 서버에서 한 단락으로 합성되므로 각 항목은 독립적인 한두 문장으로 자연스럽게 이어지게 작성한다. 인사·점수·랭크 언급 금지.
</output>
""".formatted(
PersonalityInterviewPromptFragments.PERSONA,
PersonalityInterviewPromptFragments.SECURITY_RULES,
PersonalityInterviewPromptFragments.LENGTH_NEUTRAL,
PersonalityInterviewPromptFragments.RUBRIC,
InterviewPromptFragments.RANK_MAPPING,
InterviewPromptFragments.FEEDBACK_TONE_BY_RANK
);

public static final String PERSONALITY_ANSWER_FEEDBACK_PROMPT = """
<role>
%s
</role>

<task>
아래는 인성 면접의 대화 흐름이며, 가장 최근 답변에 매겨진 answer_rank는 system context 영역에 별도로 제공된다.
너의 작업은 가장 최근 질문에 대해 면접자가 서술한 답변에 대한 피드백을 작성하는 것이다.
</task>

%s
%s

<evaluation_criteria note="참고용, 점수는 매기지 말 것">
구체성 및 경험 근거, 자기 인식 및 성찰, 협업 및 소통 태도, 가치관 및 직무 적합성
</evaluation_criteria>

%s

<output>
제공된 도구를 호출해 feedback 필드에 3-4문장의 정중한 피드백을 작성하라.
answer_rank에 맞는 톤으로 작성하되, 점수나 랭크 자체는 절대 언급하지 마라.
존댓말 사용, 개행 없이 한 단락으로 작성한다.
</output>
""".formatted(
PersonalityInterviewPromptFragments.PERSONA,
PersonalityInterviewPromptFragments.SECURITY_RULES,
PersonalityInterviewPromptFragments.LENGTH_NEUTRAL,
InterviewPromptFragments.FEEDBACK_TONE_BY_RANK
);

private PersonalityInterviewBedrockSystemMessageConstant() {
}
}
Loading
Loading