Skip to content

feat: 면접에 기업을 골라 재작성·꼬리질문·피드백에 기업 정보를 싣게 함 - #97

Merged
kimwuju merged 4 commits into
mainfrom
feat/company
Oct 15, 2026
Merged

kimwuju merged 4 commits into
mainfrom
feat/company

Conversation

@kimwuju

@kimwuju kimwuju commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

개요

기업 맞춤 면접 구현 명세 04-api v2(1단계)입니다. main(bd6663e) 기준입니다. 공통 계약은 00-overview §6, 데이터 형식은 01-company-data를 따릅니다.

면접을 만들 때 기업 하나를 고를 수 있습니다. 고른 기업 정보는 면접에 스냅샷으로 남습니다. 이 스냅샷이 질문 재작성(1:1·다대일), Chat 꼬리질문, 피드백의 companyFit에 실립니다. 점수는 바뀌지 않습니다.

기업 데이터는 시드 마이그레이션으로 넣지 않습니다. API가 기동할 때 데이터 패키지(JSON)를 읽어 반영합니다. COMPANY_DATA_LOCATION을 비우면 기업이 0개입니다. 그러면 Web이 기업 선택 섹션을 숨기므로, 운영에 먼저 배포해도 화면은 지금과 같습니다.

변경

1. 스키마 (V34__add_company.sql)

  • 테이블 3개를 추가했습니다: company, company_source, company_fact.
  • 컬럼을 추가했습니다: interview.company_id·company_snapshot(jsonb), feedback.company_fit(jsonb).
  • 기업 행은 지우지 않습니다. 면접이 가리키고 있으므로, 패키지에서 빠진 기업은 HIDDEN으로만 바꿉니다.

2. 기업 데이터 가져오기 (domain/company/importer)

  • CompanyDataImporter가 app.company.data-location의 *.json을 읽습니다. 웹 서버가 요청을 받기 전에 끝나도록 SmartInitializingSingleton에서 돕니다. 명세의 ApplicationRunner는 내장 서버가 뜬 뒤에 돌아, 그 사이 요청이 반쯤 반영된 데이터를 볼 수 있습니다.
    • 파일 검증(CompanyDataValidator, 01 §3 규칙 1~7, 9)과 묶음 유일성(규칙 8)을 확인합니다.
    • 기업 하나를 트랜잭션 하나로 반영합니다.
    • 파일 SHA-256이 같고 공개 상태면 건너뜁니다. 내용이 바뀌었으면 fact·source를 지우고 다시 넣습니다.
    • 이번에 읽은 파일에 아예 없는 공개 기업은 숨깁니다. 숨김은 반영보다 먼저 합니다. 숨긴 기업이 들고 있던 DART 고유번호·사업자등록번호를 이번 묶음이 쓰면 놓게 한 뒤 반영합니다.
    • 반영에 실패한 기업은 나머지를 반영한 뒤 한 번 더 시도합니다. 묶음 안에서 식별자를 옮겨도 처리 순서와 상관없이 들어갑니다.
    • 모회사를 연결합니다(규칙 10). 묶음에 아예 없는 모회사는 연결하지 않고 경고합니다. 모회사 파일이 묶음에 있는데 실패했으면 기존 연결을 그대로 둡니다.
  • 기동을 실패시키지 않습니다. 문제가 있는 파일만 건너뛰고 오류 로그를 남깁니다. 마지막에 요약 로그를 남깁니다: 기업 데이터 가져오기: 새로 n, 갱신 u, 그대로 s, 숨김 h, 건너뜀 e
  • 패키지 위치
    • company-data/sample/: 샘플 기업 3개(01 §5 문장 그대로)
    • company-data/prod/: 실제 데이터 자리(.gitkeep)
  • 상한값은 분석 서버 계약과 같은 값을 CompanyLimits 한 곳에 모았습니다. 글자 수는 분석 서버(Python)와 맞춰 코드포인트로 셉니다.

3. GET /api/companies?q=&category=

  • 공개 기업 전체를 읽어 메모리에서 거릅니다. 캐시는 없습니다.
  • 표시 이름·법인 이름·별칭에서 찾고, 공백과 대소문자는 무시합니다.
  • 정렬은 분류 순서, 그다음 이름 가나다순입니다.
  • talentKeywords는 TALENT fact의 title을 최대 3개 담습니다. legalSize가 미확인이면 null입니다.
  • 검색어가 2~30자를 벗어나면 422입니다.

4. 면접 생성과 전달

  • CreateInterviewRequest.companyId는 선택입니다. 있으면 CompanySnapshotFactory가 공개 기업을 스냅샷으로 만들어 저장합니다(1:1·다대일). 숨긴 기업이나 없는 기업이면 404입니다.
  • 이후 경로는 스냅샷만 읽고, 기업 테이블은 다시 조회하지 않습니다.
    • 재작성 요청(QuestionTailorRequest·QuestionTailorMultiRequest)과 피드백 요청(FeedbackSoloRequest·FeedbackMultiRequest)에 company 스냅샷을 그대로 싣습니다.
    • Chat 준비 요청에는 company { displayName, interviewerBrief }만 싣습니다.
    • 기업이 없으면 company 키를 아예 보내지 않습니다(@JsonInclude(NON_NULL)).
  • 피드백 콜백의 companyFit은 CompanyFitReader가 읽습니다. 모양이 틀리면 warn을 남기고 비우며, 피드백은 그대로 저장합니다.
  • 응답 변경
    • InterviewResponse에 company { companyId, displayName } | null을 추가했습니다(생성·단건·목록).
    • FeedbackResponse에 company와 companyFit을 추가했습니다.

외부에서 보이는 동작 변화

요청 이전 이후
GET /api/companies 없음 공개 기업 목록. 데이터 위치가 비어 있으면 []
POST /api/interviews/create, companyId 없음 — 같음(응답에 company: null)
POST /api/interviews/create, 공개 기업 companyId — 스냅샷 저장, 응답에 company
POST /api/interviews/create, 숨김·없는 기업 — 404 "선택한 기업을 찾을 수 없습니다."
기업 없는 면접의 Analysis·Chat 요청 본문 — 지금과 같음(company 키 없음)

배포 순서 (중요)

00 §7: Analysis → Chat → API → Web.

  • Analysis는 모르는 필드를 무시합니다. Chat의 준비 요청은 ignoreUnknown입니다.
  • 그래서 API를 먼저 배포해도 에러는 나지 않지만, 기업 정보가 조용히 빠집니다.
  • 운영 env에 COMPANY_DATA_LOCATION을 넣지 않으면 기업이 0개입니다. 개발·스테이징은 classpath:company-data/sample/*.json입니다.

테스트

  • ./gradlew test 전체 682개가 통과했습니다(신규 66개).
  • 로컬 Postgres에서 V34를 적용했습니다. 샘플 위치로 기동했을 때 로그는 새로 3, 갱신 0, 그대로 0, 숨김 0, 건너뜀 0이었습니다.
  • 신규 테스트
    • CompanyDataValidatorTest: 규칙별 위반 1건씩, 샘플 3개 오류 없음.
    • CompanyDataImporterTest(@SpringBootTest, 롤백, 실제 배포되는 company-data/sample을 복사해 사용)
      • 새로 반영, 그대로, 한 기업만 갱신(fact_id 새로), 빠진 파일 숨김·복귀
      • 검증 실패·깨진 JSON의 기존 상태 유지, code 중복 둘 다 건너뜀, 위치 비움·파일 0개
      • 모회사 파일 실패 시 연결 유지, 빠지면 끊음
    • CompanyDataImporterIsolationTest(@SpringBootTest, 실제 트랜잭션 경계): 한 기업의 DB 오류는 그 기업만 건너뜁니다. 묶음 안에서 옮긴 DART 고유번호, 숨긴 기업에서 넘겨받는 식별자도 확인합니다.
    • CompanySnapshotFactoryTest, CompanyQueryServiceTest
    • FeedbackServiceCompanyTest(@SpringBootTest): 요청에 company, companyFit jsonb 저장·조회, null·형식 오류.
  • 기존 테스트에 추가한 케이스
    • InterviewServiceCreateTest: SOLO·MULTI 스냅샷, 기업 없음, 404.
    • QuestionTailorServiceRequestTest: solo·multi company, 기업 없으면 키 없음.
    • ChatInterviewHandoffServiceTest: 기업 요약 두 칸만, 기업 없으면 키 없음.

참고

  • 목록 조회(/api/interviews/getAll, 피드백 목록)는 배지 하나를 만들려고 company_snapshot jsonb를 함께 읽습니다. 빼려면 저장 구조를 별도 테이블이나 지연 로딩으로 바꿔야 해서 이번에는 두었습니다. 면접당 수 KB 수준입니다.
  • A-1 P-2(fix: 테일러 콜백이 접수 응답보다 먼저 와도 결과를 잃지 않게 함(P-2) #98) 병합 후 main을 받아 충돌을 정리했습니다. 충돌은 QuestionTailorService 요청 빌더 두 곳뿐이었습니다. 두 곳 모두 .company(...)와 P-2의 withTailorId(...) 콜백 주소를 함께 남겼습니다. 병합 결과로 전체 701개 테스트가 통과했습니다. 리뷰 반영(71faefb) 뒤 전체 711개가 통과합니다.
  • 기존 테스트 수정: 7개 파일 27곳에서 생성자 인자 수만 맞췄습니다(null 추가). 대상은 CreateInterviewRequest, FeedbackCallbackRequest.Result, InterviewService입니다. 단언문은 바꾸지 않았습니다.
  • 명세를 해석한 부분
    • JSON이 깨진 파일은 code를 읽을 수 없습니다. 그래서 파일 이름(<code>.json)도 "읽은 기업"으로 쳐서 그 기업을 숨기지 않습니다(§3-4-7 "검증에 실패한 파일의 기업은 기존 상태 유지").
    • 모회사 연결은 내용이 그대로인 기업에도 다시 적용합니다. 모회사가 나중에 묶음에 들어와도 연결되게 하려는 것입니다.
    • fact·source 삭제는 JPQL 한 문장으로 합니다. 하나씩 지우면 Hibernate가 삽입을 먼저 내보내 (company_id, source_key) 유일 제약에 걸립니다.

🤖 Generated with Claude Code

기업 맞춤 면접 1단계 API(명세 04-api v2). 기업 데이터는 시드 마이그레이션이 아니라
기동할 때 데이터 패키지(JSON)를 읽어 반영한다. 위치를 비우면 기업이 0개라 운영 화면은 그대로다.

- V34: company·company_source·company_fact, interview.company_id·company_snapshot, feedback.company_fit
- CompanyDataImporter: COMPANY_DATA_LOCATION의 *.json을 파일 검증(01 §3 규칙 1~7, 9)·묶음 유일성 확인 후
  기업 하나 = 트랜잭션 하나로 반영. 내용 해시가 같으면 건너뛰고, 빠진 기업은 HIDDEN. 기동은 막지 않는다
- 샘플 기업 패키지 3개(company-data/sample), 운영용 자리(company-data/prod)
- GET /api/companies: 이름·법인명·별칭 검색(공백·대소문자 무시), 분류 필터, 분류 순서 → 이름순
- 면접 생성: companyId(선택)가 있으면 공개 기업을 스냅샷으로 저장. 숨김·없는 기업은 404
- 재작성(1:1·다대일)·피드백 요청에 company 스냅샷, Chat 준비 요청에 company {displayName, interviewerBrief}.
  기업이 없으면 키 자체를 보내지 않는다
- 피드백 콜백 companyFit 저장(모양이 틀리면 비우고 피드백은 저장), 피드백·면접 응답에 company

A-1 P-2보다 먼저 main 위에서 만들었다. QuestionTailorService 변경은 빌더 두 줄이다.
기존 테스트는 생성자 인자 수만 맞췄다(null 추가).

배포는 Analysis → Chat → API → Web 순서다. 먼저 배포하면 기업 정보가 조용히 빠진다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@danjee2009 danjee2009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검토 범위: 기업 스냅샷을 Analysis 및 Chat 요청으로 전달하는 API 변경과 소비자 호환성을 확인했습니다. GitHub CI의 테스트 체크는 통과했습니다.

공개 기업이 없으면 면접에 기업을 고를 수 없어 company가 나가지 않는다. 분석·채팅 서버는 모르는 필드를
조용히 버리므로, COMPANY_DATA_LOCATION이 기업 기능의 배포 스위치라는 점을 설정 옆에 남긴다(PR #97 리뷰).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kimwuju and others added 2 commits October 15, 2026 15:59
# Conflicts:
#	src/main/java/repit/repit_api_server/domain/userdata/question/service/QuestionTailorService.java
- 스냅샷: 분석 서버 필수 구성(TALENT·BUSINESS 1개 이상, 빈 이름·요약·title·content 없음)을 면접 생성 전에 확인한다.
  고정된 스냅샷이라 어긴 채 만든 면접은 영영 시작하지 못한다. 기업 행과 fact는 REPEATABLE READ로 같은 시점을 읽는다
- 가져오기: 웹 서버가 요청을 받기 전에 끝내도록 ApplicationRunner 대신 SmartInitializingSingleton에서 돈다
- 가져오기: 숨김을 반영보다 먼저 하고, 숨긴 기업이 들고 있던 DART 고유번호·사업자등록번호를 이번 묶음이 쓰면 놓게 한다.
  반영에 실패한 기업은 나머지를 반영한 뒤 한 번 더 해, 묶음 안에서 식별자를 옮겨도 처리 순서와 상관없이 들어간다
- 모회사 파일이 묶음에 있는데 실패했으면 자회사의 기존 모회사 연결을 그대로 둔다. 묶음에서 빠졌을 때만 끊는다
- 검증: parentCode가 자기 code면 오류, review.reviewedBy 50자 상한
- 검색: NBSP·전각 공백도 무시((?U)\s), 숨긴 모회사 이름은 내보내지 않음, 모회사를 함께 읽어 N+1 제거(@EntityGraph)
- 배지 id는 스냅샷 문자열 대신 interview.company_id 컬럼에서 받는다
- 테스트: 샘플 복사본(test resources)을 지우고 main 샘플을 직접 쓴다. 실제 트랜잭션 경계로 기업별 롤백·식별자 이동을
  확인하는 CompanyDataImporterIsolationTest를 추가한다

목록 조회가 스냅샷 jsonb를 함께 읽는 문제는 저장 구조(interview.company_snapshot)를 바꿔야 해 이번에는 두었다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kimwuju
kimwuju merged commit f7c66bf into main Oct 15, 2026
1 check passed
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.

3 participants