Skip to content

feat: N:1 면접 스키마 도입 및 분석 콜백·SSE 정합성 수정 - #36

Merged
kimwuju merged 9 commits into
mainfrom
feat/multi-interview
Aug 25, 2026
Merged

feat: N:1 면접 스키마 도입 및 분석 콜백·SSE 정합성 수정#36
kimwuju merged 9 commits into
mainfrom
feat/multi-interview

Conversation

@kimwuju

@kimwuju kimwuju commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

연관 이슈 없음

Task

  • N:1 면접(면접관 3인) 스키마를 도입했습니다. 페르소나에 직책 축(role: TECH/HR/CEO)을 신설하고, major(BACKEND/FRONTEND)는 기술 면접관의 세부 전공으로 좁혔습니다. 말투(type)와 직책은 독립이라 CEO가 압박형일 수도 친화형일 수도 있습니다. 면접에는 mode(SOLO/MULTI)가 생겼고, N:1 면접관 목록은 interview_persona가 진행 순서(TECH → HR → CEO)와 함께 들고 있습니다. 1:1은 기존 interview.persona_id 경로가 그대로 돕니다.
  • 면접관별 종합 피드백을 저장합니다. feedback_persona 테이블과 feedback_item.persona_id를 추가했습니다. 문항이 페르소나당 2~3개뿐이라 3지표(total/intent/reliability) 대신 점수 하나만 둡니다. 1:1 콜백에는 personas가 실리지 않고, 그때는 아무것도 저장하지 않습니다.
  • N:1은 채팅 서버가 면접 종료 시점에 분석 서버를 직접 호출합니다. 이 서버는 요청을 접수한 적이 없어 대응하는 feedback 행이 없으므로, 콜백의 sessionId로 면접을 찾아 그 시점에 만듭니다.
  • 페르소나 난이도(level)를 값 집합이 고정된 형태로 다시 넣고, 채팅 서버로 전달합니다. V7에서 제거했던 자유 문자열 컬럼과 달리 EASY/NORMAL/HARD로 CHECK를 겁니다.
  • role·level의 기본값 폴백을 걷어냈습니다. null → TECH, null → NORMAL이 남아 있으면 프론트가 값을 누락해도 드러나지 않고, 저장된 뒤에는 요청값과 기본값을 구분할 수 없습니다. 비어 있으면 422로 거부합니다.
  • 실패 콜백이 이미 저장된 분석 결과를 지우던 문제를 고쳤습니다. /api/v1/ai/callback은 성공·실패가 같은 경로로 들어오는데 status를 보지 않고 result를 덮어써서, 실패 콜백 한 번에 성공 결과가 null이 됐습니다.
  • 구독이 콜백보다 늦게 붙어도 결과를 받습니다. 기존 SSE는 그 순간 붙어 있는 emitter에게만 보내고 없으면 버려서, 이미 끝난 작업을 구독하면 connect만 받고 영영 대기했습니다. analysis_datastatus·error_status_code·error_message를 두고(V10), 구독 시 끝난 작업이면 즉시 결과를 보내고 스트림을 닫습니다.
  • 분석 작업 소유자(user_id) 기록 실패가 조용히 넘어가지 않게 했습니다. job_id가 없으면 502로 끊고, 사용자 조회 실패는 로그를 남기되 요청 전체는 살립니다(그 시점엔 분석 서버가 이미 작업을 접수한 뒤라, 여기서 던지면 클라이언트가 jobId를 못 받은 채 작업만 떠돕니다).
  • 마이그레이션 V7~V10이 포함됩니다. 테스트 통과합니다.

마이그레이션 순서

V7  persona.level 제거          ← 코드에 없던 잔여 컬럼 정리
V8  N:1 면접 스키마             ← role/mode/interview_persona/feedback_persona
V9  persona.level 재도입        ← EASY/NORMAL/HARD로 값 집합 고정
V10 analysis_data 상태·실패사유  ← SSE 되짚기의 전제

V7에서 지운 levelV9에서 다시 넣습니다. 같은 이름이지만 다른 컬럼입니다 — 전자는 코드 어디에서도 쓰지 않던 자유 문자열 잔여물이었고, 후자는 CHECK로 값이 고정된 새 컬럼입니다.

리뷰 시 봐주셨으면 하는 것

N:1 면접 시작은 아직 막혀 있습니다. prepareInterviewMULTI면 422로 거부합니다. 1:1용 질문 재작성을 그대로 태우면 인사·CEO 질문 없이 면접이 열리기 때문입니다. 분석 서버의 multi tailor 스펙이 확정되면 열어야 합니다.

analysis_data.status에 DEFAULT를 뒀습니다. 배포 중 구버전 인스턴스가 콜백을 받아 INSERT할 때 NOT NULL로 깨지지 않게 하기 위해서입니다. 기존 행은 result 유무로 백필했습니다.

user_id가 여전히 채워지지 않고 있습니다. 8/20 이후 analysis_data에 쌓인 행은 전부 user_id가 비어 있는데, AI 서버 응답(0.45초, job_id 정상)도 getUser()도 정상이라 코드 경로에서 원인을 특정하지 못했습니다. 이번에 추가한 ERROR 로그로 다음 호출에서 드러날 예정입니다. 영향은 findTopByUserIdAndResultIsNotNullOrderByCreatedAtDesc가 최신 분석 결과를 못 찾아 질문 재작성이 예전 결과를 집는 것입니다.

/subscribe/{jobId}/callback에 인증이 없습니다. jobId만 알면 남의 분석 결과를 구독하거나 결과를 밀어넣을 수 있습니다. 이번 PR 범위 밖이라 두었습니다.

프론트도 함께 확인이 필요합니다. SSE는 구독 즉시 event: connect를 보냅니다. 결과는 question-generated / question-generation-failed로 오므로 각각 addEventListener로 분기하고, 받은 뒤 eventSource.close()를 호출해야 재연결이 반복되지 않습니다.

kimwuju and others added 9 commits August 25, 2026 20:18
엔티티에서 쓰지 않는 persona.level 컬럼을 제거한다.
베이스라인 이전에 생성된 DB에만 남아 있을 수 있어 IF EXISTS로 처리했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/generate 콜백은 성공/실패가 같은 경로로 들어오고 실패 콜백에는 result가 없다.
status를 보지 않고 덮어쓰고 있어 실패 콜백 한 번에 이미 받아둔 결과가 날아갔다.
성공 콜백일 때만 결과를 저장하고, 실패는 사유를 로그로 남긴다.

registerJob도 행 전체를 저장하면 콜백이 먼저 채운 result를 덮어쓸 수 있어
소유자 컬럼만 갱신하도록 바꿨다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
소유자(user_id)를 남기지 못하면 사용자의 최신 분석 결과를 되찾을 수 없어
질문 재작성이 예전 결과를 집어 든다. 그런데 job_id가 비었거나 사용자 조회가
실패해도 아무 흔적이 남지 않아 원인을 알 수 없었다.

- 분석 서버 응답에 job_id가 없으면 502로 끊는다. jobId 없이 성공을 돌려줘도
  클라이언트는 구독도 조회도 할 수 없다.
- 사용자 조회 실패는 로그로 남기되 요청 전체를 실패시키지 않는다. 이 시점엔
  분석 서버가 이미 작업을 접수해서, 여기서 던지면 jobId를 못 받은 채 작업만
  떠돈다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
페르소나에 직책(TECH/HR/CEO) 축을 신설한다. 말투(type)와 독립이라
CEO가 압박형일 수도 친화형일 수도 있다. 전공(major)은 기술 면접관에게만
있는 값이라 NOT NULL을 풀고 CHECK로 대신 묶었다.

면접관이 여럿인 면접은 interview.persona_id 한 칸에 담기지 않아
interview_persona로 분리했다. 진행 순서는 요청 순서가 아니라 직책 순서로
정한다 — 꼬리질문이 부모 질문 뒤에 삽입되므로 한 면접관의 질문 묶음이
끝나야 다음 면접관으로 넘어간다.

피드백에는 면접관별 종합 계층을 더했다. 면접관당 문항이 2~3개뿐이라
3지표는 전체 계층에만 두고 여기는 점수 하나만 둔다. N:1은 채팅 서버가
면접 종료 시점에 분석 서버를 직접 호출해 이 서버에 접수 기록이 없으므로,
콜백에서 세션으로 면접을 찾아 그때 행을 만든다.

N:1 면접 시작(prepare)은 막아뒀다. 1:1용 재작성을 태우면 인사·CEO 질문
없이 면접이 열린다. multi tailor 스펙이 확정되면 푼다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
V7에서 제거했던 자유 문자열 컬럼과 달리 값 집합을 EASY/NORMAL/HARD로
고정한다. 말투(type)·직책(role)과 독립된 축이라, 압박형 CEO가 쉬울 수도
친화형 기술 면접관이 어려울 수도 있다.

기존 행과 난이도를 안 보내는 기존 요청은 전부 NORMAL이 된다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
난이도를 안 넘기면 채팅 서버가 꼬리질문을 얼마나 깊게 파고들지 판단할
근거가 없다. 면접 데이터를 넘길 때 페르소나에 실어 보낸다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SSE는 그 순간 붙어 있는 구독자에게만 결과를 흘렸다. 콜백이 먼저 도착하면
이벤트는 그대로 버려지고, 나중에 붙은 클라이언트는 연결 확인용 connect만 받은 채
타임아웃까지 매달려 있었다. EventSource가 그때마다 다시 붙어 재연결만 반복하니
결과는 오지 않는데 SSE는 계속 오는 것처럼 보였다.

구독 시점에 저장된 결과를 먼저 확인해, 이미 끝난 작업이면 붙는 즉시 돌려주고 끊는다.

되짚으려면 "실패한 작업"과 "아직 끝나지 않은 작업"을 구분해야 하는데 result 유무로는
그게 안 됐다. analysis_data에 status와 실패 사유를 남긴다. 기본값을 둬서 상태를
모르는 구버전이 콜백을 받아도 저장이 깨지지 않는다.

이미 성공한 작업에 뒤늦은 실패 콜백이 오면 무시한다. 받아둔 결과를 잃을 이유가 없다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
프론트가 role을 보내지 않던 시절의 null -> TECH 기본값이 남아 있어,
인사·CEO 면접관을 만들어도 기술 면접관으로 저장될 여지가 있었다.
직책은 저장되고 나면 되돌릴 근거가 없으므로 비어 있으면 422로 거부한다.

엔티티의 @Builder.Default도 걷어냈다. 안 채우고 저장하면 TECH로 묻히는
대신 NOT NULL로 걸리는 편이 낫다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
직책과 같은 이유다. null -> NORMAL 기본값이 남아 있으면 프론트가 보낸
난이도가 누락돼도 드러나지 않고, 저장된 뒤에는 요청값과 기본값을
구분할 수 없다. 비어 있으면 422로 거부하고 엔티티 기본값도 걷어냈다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kimwuju
kimwuju merged commit 4ea6221 into main Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant