[FE-40] 3단계 API 계약 타입 · queryKey · MSW 목 · 유틸
작업 내용 (설계 의도)
근거: 지원 관리 확장 FE 웹 설계 신규 TypeScript 모델 (types/api.ts 확장) · Query 규약 (기존 규약 승계 + 신규분) · API 연동 > 3단계 · 신규 순수 유틸 (utils/) · 상태 표기 규칙 · Single Writer per File 검증 > 단계 3 wave 2, 지원 관리 확장 TDD 3단계 — 연락 검토 화면 (FR-90) · 3단계 — 가중치 수정·상한 해제 (FR-97) · 3단계 — 소스 상태 레지스트리 (FR-88) · 3단계 — 소급 재평가 백필 (FR-87).
변경 사항
-
단계 3의 공통 계약 병목을 한 티켓으로 묶습니다. 후행 6개 티켓(FE-42~FE-47)이 전부 이 타입·queryKey·MSW 목을 import하므로, 쪼개면 wave 1이 직렬 사슬이 되고
types/api.ts·mocks/handlers.ts에서 머지 충돌이 납니다. 이 티켓이 닫히는 순간 wave 2가 6갈래로 열립니다. -
[분해 원칙 2026-08-09] 3단계에서 새로 필요해지는 공용 테스트 헬퍼·fixture·가드는 이 티켓이 선점 소유하고, 후행 wave(FE-42~FE-49)는 소비만 합니다.
- 1단계에서 같은 헬퍼를 두 티켓이 각자 만들어 머지 충돌이 난 사고가 있었습니다(
web/src/test/hardcodedColor.ts— FE-20·FE-22). 리뷰 지적이 양쪽에 독립적으로 갔는데 소유자가 티켓에 없었던 것이 원인입니다. web/src/test/hardcodedColor.ts는 1단계 FE-20 소유입니다 — 3단계 티켓은 다시 만들지 말고main에서 import합니다. 특히 FE-49(회귀·다크 모드 전수)가 이 헬퍼를 쓰지만 만들지 않습니다.- 새 공용 헬퍼가 필요해지면 이 티켓으로 되돌려 보고하고 화면 티켓이 직접 만들지 않습니다.
- 1단계에서 같은 헬퍼를 두 티켓이 각자 만들어 머지 충돌이 난 사고가 있었습니다(
-
types/api.ts에 3단계 신규 타입을 전량 추가합니다 —ContactEventListItem,ContactEventDetailResponse(중첩된attachments·parse·candidates·decision포함),ContactDecisionRequest,ContactConfidenceLevel,SourceRegistryResponse,JobSourceRegistryStatus(6종),FieldScopeBackfillResponse, 그리고 가중치 조회·저장 타입(criteriaRevision+{ axis, weight }[]). 필드·타입은 TDD 계약과 1:1이며 FE가 임의로 좁히거나 합치지 않습니다. -
타입 이름을 분리합니다. 기존
ConfidenceLevel은 근무형태 확신도 4종(CONFIRMED·LIKELY·INFERRED·UNKNOWN)이고, 연락 후보 신뢰도는 3종(HIGH·MEDIUM·LOW)으로 의미가 다릅니다. 후자는 **ContactConfidenceLevel**로 선언합니다(설계 “타입 이름 충돌 주의”가 지정한 이름). 기존ConfidenceLevel을 확장하거나 유니온으로 합치지 않습니다 — 합치면 근무형태 칩과 연락 신뢰도 칩이 같은 타입을 받아 잘못된 조합이 컴파일을 통과합니다. -
nullable을 계약 그대로 유지합니다.
topCandidateConfidence(후보 0건)·topCandidateCompanyName·parse(파싱 실패)·autoSelectedJobApplicationId(자동 선택 불가)·decision(미결정)·registryStatus(레지스트리 백필 전)·registryStatusChangedAt·lastNormalAt·lastCollectedAt이 전부null을 가질 수 있습니다. FE가0·빈 문자열·기본 enum으로 대체하면 사용자에게 거짓 정보를 보여 주게 되므로 금지합니다. -
api/queryKeys.ts에 3단계 키를 추가합니다 —['contact-events', { reviewStatus, days }](무한 쿼리),['contact-events', contactEventId],['recommendations','weights'],['operations','source-registry']. 배열 접두사 계층을 유지해 연락 결정 후['contact-events']접두사 단위 무효화가 목록·상세·대시보드 배너에 동시에 걸리게 합니다. -
mocks/에 3단계 엔드포인트를 전량 목 구현합니다 — 연락 목록·상세·결정, 가중치 조회·저장, 상한 해제·재적용, 소스 상태 레지스트리, 매칭 범위 재평가 백필. 후행 티켓이 이 목만으로 화면 테스트를 완결해야 하므로 loading/empty/error/success 4상태와 계약상 에러 코드(CONTACT_EVENT_NOT_FOUND·CONTACT_EVENT_ALREADY_DECIDED·CONTACT_EVENT_TARGET_TERMINAL·TRANSITION_NOT_ALLOWED·APPLICATION_NOT_FOUND·RECOMMENDATION_WEIGHT_INVALID)를 시나리오로 제공합니다. -
연락 목록 목은
reviewStatus·days·page쿼리를 실제로 반영하고PageResponse.totalCount를 세그먼트별로 다르게 반환합니다. 목이 쿼리를 무시하면 FE-42의 세그먼트 건수 표기와 세그먼트별 빈 상태 3종 구분, FE-48의 검토 대기 0건 배너 미렌더를 테스트할 수 없습니다. -
연락 상세 fixture는 후행 티켓이 요구하는 변형을 전부 포함합니다 — 후보 0건, 후보 전원
terminal: true,autoSelectedJobApplicationId: null,parse: null, 본문 20000자, 후보별로 서로 다른allowedNextStatuses,decision !== null(결정 완료). 백필 목은dryRun값을 반영해 dryRun에서는criteriaRevision을 올리지 않은 결과를 돌려줍니다. -
소스 레지스트리 fixture는 6종 상태를 전부 포함하고,
registryStatus: null(백필 전) 행을 최소 1건 넣습니다. FE-44가 정렬과 “상태 미확인” 중립 칩을 검증할 근거입니다. -
utils/contactEvent.ts를 신규로 추가합니다 — 신뢰도 3종 + 후보 없음 라벨, 후보 일치 축 4종(회사·공고·담당자·최근 지원) 라벨, 기본 회차 라벨 조립(suggestedRoundLabel ?? "{nextInterviewRoundNumber}차 면접"). 컴포넌트 안에서 문자열을 분기하지 않게 하는no-logic-in-component대응입니다. -
utils/sourceRegistry.ts를 신규로 추가합니다 — 상태 6종 한국어 라벨, 상태별 집계(전체 목록 30건 미만 전량 수신이라 클라이언트 합산 가능), 문제 우선 정렬(접근 제한 → 일시 실패 → 미지원 → 발견됨 → 활성 → 비활성). 운영 화면의 목적이 이상 감지이므로 정렬 기준을 유틸에 고정합니다. -
매칭 근거 타입
MatchFieldEvidenceResponse를 확정 계약대로 추가합니다 (C-2 수용, BE-80 신설). 필드는matchField: 'TITLE' | 'SOURCE_TAG' | 'DESCRIPTION_BODY'·jobKeywordGroupId: number·jobKeywordGroupName: string·weightPoints: number·occurrenceCount: number·snippet: string입니다. 초안에서 역제안했던 필드명(field·keywordGroupName·weight)이 아니라 확정 계약 필드명을 그대로 씁니다 — 역제안 이름을 남겨 두면 FE-47이 컴파일되는 잘못된 계약 위에 화면을 만듭니다. -
공고 상세 타입에
matchFieldEvidences: MatchFieldEvidenceResponse[]·matchScore: number | null·matchThreshold: number | null·matched: boolean·excludedByKeyword: { keywordId: number, keyword: string } | null을 둡니다. 교차 목록 항목에는matchScore·matched만 둡니다 — 항목 수 × 근거 수만큼 응답이 커지므로 근거 배열은 상세에서만 내려오고,matchThreshold·excludedByKeyword도 상세 전용입니다. -
3단계 배포 전에는
matchFieldEvidences: []·matchScore: null·matchThreshold: null·excludedByKeyword: null입니다.matchScore의 3-state를 타입 주석에 명시합니다 —null은 미배포(계산 전),0은 근거 없음(계산했는데 어느 필드에도 안 걸림),> 0은 근거 있음입니다.matchFieldEvidences === []만으로 판단하면 앞의 두 상태가 뭉개지므로, 후행 티켓이 배열 길이로 분기하지 않도록 주석으로 못을 박습니다. -
utils/matchEvidence.ts를 이 티켓이 소유합니다. 핵심은 순수 함수resolveMatchOutcome—matched·matchScore·matchThreshold·excludedByKeyword를 받아 5가지 상태 중 하나를 반환합니다:NOT_DEPLOYED(matchScore === null) ·NO_EVIDENCE(matchScore === 0) ·MATCHED·UNMATCHED_BELOW_THRESHOLD·UNMATCHED_EXCLUDED. 필드 3종 라벨(TITLE제목 /SOURCE_TAG태그 /DESCRIPTION_BODY본문)과 부족 점수(matchThreshold - matchScore) 계산도 이 파일이 담당합니다. -
목적은 점수 비교 판정을 이 파일 하나에 가두는 것입니다. 컴포넌트가
matchScore >= matchThreshold를 직접 쓸 수 있는 경로를 없앱니다 — 제외 키워드가 매칭을 이기므로(FR-23) 점수 60인데matched: false인 공고가 실제로 존재하고, 점수 비교로 판정하면 그 공고가 “매칭됨”으로 표시됩니다. 판정 SSOT는matched하나뿐이고 점수·임계치는 “얼마나 모자랐나”를 설명하는 수치일 뿐입니다. -
연락 첨부 타입을
{ fileName: string, mimeType: string, sizeBytes: number }[]로 확정합니다 (B-8).recruitment_contact_event_attachments테이블은 메타데이터만 저장하고 바이너리를 보관하지 않으므로 다운로드 URL 필드가 없습니다. 이벤트당 최대 20건이며, fixture도 20건 케이스를 포함해 FE-43이 전량 렌더를 검증할 수 있게 합니다. -
MSW 공고 상세 목을 5종 상태 fixture 전부로 확장합니다 — ① 매칭(근거 3종
TITLE60 /SOURCE_TAG35 /DESCRIPTION_BODY20,matchScore: 75·matchThreshold: 50·matched: true) ② 미매칭-점수 미달(matchScore: 35·matched: false·excludedByKeyword: null) ③ 미매칭-제외 키워드(matched: false·excludedByKeyword: { keywordId, keyword: '인턴' }) ④matchScore: 0(근거 없음·빈 배열) ⑤matchScore: null(미배포). FE-47이 5분기를 이 목만으로 전부 검증합니다. -
matchScore: 60+matched: falsefixture를 반드시 포함합니다 — 제외 키워드가 점수를 이긴 경우입니다. 이 fixture가 없으면 “점수로 판정해도 테스트가 통과하는” 상태가 되어 금지 규칙이 검증되지 않습니다. -
types/api.ts·api/queryKeys.ts·mocks/**는 이 티켓만 수정합니다(각 단계 wave 1 단독 규약). 후행 티켓은 읽기만 합니다. -
연락 후보 신뢰도 칩(
ConfidenceChip)을 이 티켓이 소유합니다. 연락 목록(FE-42)과 검토 상세(FE-43)가 같은 wave에서 둘 다 쓰므로, 어느 한쪽이 소유하면 같은 wave 의존이 생깁니다.HIGH는 filled positive,MEDIUM은 outlined neutral,LOW는 filled warning으로 표현하고, 후보 0건(null)은[후보 없음]중립 칩으로LOW와 구분합니다(신규 토큰 0건).
의존
- BE-68 (3단계 공통 계약 확정) — 계약 문서가 SSOT이므로 BE 구현 병합 전에도 착수할 수 있고, 실제 통합 검증은 BE-72·BE-74·BE-77·BE-78 완료 후 수행합니다.
- BE-80 (매칭 근거 API 노출) —
MatchFieldEvidenceResponse계약의 근거입니다. 타입·목은 확정 계약으로 선행 작성하고, 통합 검증은 BE-80 완료 후 수행합니다. - 선행 FE 티켓 없음 (단계 3 wave 1).
다이어그램
처리 흐름
sequenceDiagram participant Test as 후행 티켓 테스트 participant Hook as 3단계 훅 participant MSW as MSW handler participant Fixture as fixtures/errorScenarios Test->>Hook: queryKeys 로 조회·변경 Hook->>MSW: 3단계 엔드포인트 요청 MSW->>Fixture: reviewStatus·dryRun·시나리오로 선택 Fixture-->>MSW: 계약 형태 payload MSW-->>Hook: 200 또는 409/400 에러 코드 Hook-->>Test: types/api.ts 타입으로 소비
컴포넌트 의존
flowchart LR Types[types/api.ts] --> Keys[api/queryKeys.ts] Types --> Handlers[mocks/handlers.ts] Handlers --> Fixtures[mocks/fixtures] Handlers --> Errors[mocks/errorScenarios] Types --> ContactUtil[utils/contactEvent.ts] Types --> SourceUtil[utils/sourceRegistry.ts] Types --> MatchUtil[utils/matchEvidence.ts · resolveMatchOutcome] Keys --> Next[FE-42~FE-47] Handlers --> Next ContactUtil --> Next SourceUtil --> Next MatchUtil --> Next
테스트 케이스
- 연락 목록 목에
reviewStatus=PENDING을 넘기면 검토 대기 건만 담긴PageResponse와 그 세그먼트의totalCount가 반환된다. - 연락 목록 목의 마지막 페이지는
hasNext=false를, 그 이전 페이지는hasNext=true를 반환해 무한 스크롤을 검증할 수 있다. - 연락 목록 fixture에
topCandidateConfidence=null인 항목이 최소 1건 있어 후보 없음과LOW를 구분해 검증할 수 있다. - 연락 상세 fixture에
parse=null·autoSelectedJobApplicationId=null·후보 전원terminal=true·본문 20000자 케이스가 각각 최소 1건씩 존재한다. - 이미 결정된 연락에 결정을 다시 보내면
409 CONTACT_EVENT_ALREADY_DECIDED가 반환된다. - 종료된 후보를 지정해 결정을 보내면
409 CONTACT_EVENT_TARGET_TERMINAL이, 후보의allowedNextStatuses밖 상태를 보내면409 TRANSITION_NOT_ALLOWED가 각각 다른 코드로 반환된다. - 가중치 PUT 목에 합계 100이 아닌 4축을 보내면
400 RECOMMENDATION_WEIGHT_INVALID가 반환된다. - 백필 목을
dryRun=true로 호출하면 통계 4수치는 채워지고criteriaRevision은 이전 값 그대로 반환된다. - 소스 레지스트리 fixture가 6종 상태를 전부 포함하고
registryStatus=null행을 최소 1건 포함한다. utils/contactEvent가HIGH·MEDIUM·LOW와null(후보 없음)을 각각 다른 한국어 라벨로 변환하고 예외를 던지지 않는다.utils/contactEvent가suggestedRoundLabel이 없으면nextInterviewRoundNumber로 “{N}차 면접” 라벨을 조립한다.utils/sourceRegistry가 소스 배열을 접근 제한 → 일시 실패 → 미지원 → 발견됨 → 활성 → 비활성 순으로 정렬한다.utils/sourceRegistry가registryStatus=null항목을 집계에서 별도 “상태 미확인”으로 세고 다른 상태 카운트에 섞지 않는다.ContactConfidenceLevel과 기존ConfidenceLevel이 서로 다른 타입이어서 근무형태 확신도 값을 연락 신뢰도 자리에 넣으면 타입 검사에 실패한다.- 공고 상세 목이
TITLE60 ·SOURCE_TAG35 ·DESCRIPTION_BODY20 근거 3건과matchScore=75·matchThreshold=50·matched=true를 반환한다. - 공고 상세 목이
matchFieldEvidences=[]·matchScore=null·matchThreshold=null·excludedByKeyword=null인 3단계 배포 전 픽스처를 반환한다. - 공고 상세 목이
matchScore=0·빈 근거 배열 픽스처를matchScore=null픽스처와 별개로 반환한다. - 공고 상세 목이
matched=false·excludedByKeyword={ keywordId, keyword:'인턴' }픽스처와matched=false·excludedByKeyword=null·matchScore=35픽스처를 각각 반환한다. - 공고 상세 목에
matchScore=60·matched=false픽스처(제외 키워드가 점수를 이긴 경우)가 존재한다. MatchFieldEvidenceResponse가jobKeywordGroupName을 포함해, 화면이jobKeywordGroupId를 라벨로 매핑하지 않아도 렌더할 수 있다.- 교차 목록 항목 타입에는
matchScore·matched만 있고matchFieldEvidences·matchThreshold·excludedByKeyword가 없어, 목록에서 이들을 참조하면 타입 검사에 실패한다. resolveMatchOutcome이matchScore=null을NOT_DEPLOYED로,matchScore=0을NO_EVIDENCE로 판정해 둘을 다른 값으로 구분한다.resolveMatchOutcome이matched=true를MATCHED로 판정한다.resolveMatchOutcome이matched=false·excludedByKeyword=null을UNMATCHED_BELOW_THRESHOLD로,excludedByKeyword != null을UNMATCHED_EXCLUDED로 판정한다.resolveMatchOutcome이matchScore=60·matchThreshold=50·matched=false입력에 대해MATCHED가 아닌 미매칭 상태를 반환한다(점수가 임계치를 넘어도matched를 따른다).utils/matchEvidence가matchThreshold - matchScore로 부족 점수를 계산하고,matchScore가null이면 부족 점수를 만들지 않는다.utils/matchEvidence가TITLE·SOURCE_TAG·DESCRIPTION_BODY3종을 각각 다른 한국어 라벨로 변환하고 알 수 없는 값에 예외를 던지지 않는다.- 연락 상세 fixture에 첨부 20건 케이스가 있고, 각 첨부가
fileName·mimeType·sizeBytes만 가지며 다운로드 URL 필드를 포함하지 않는다.