[FE-03] 도메인 순수 로직 — 상태 라벨 · 전이 맵 · 확신도 규칙 · 포매터 · 검증
작업 내용 (설계 의도)
근거 설계: 20260722-공고알림앱-design-fe-web.md — “확신도 위계”, “방안 3 상태 전이”, “마감일 표기”
변경 사항
화면 여러 곳이 공유하는 도메인 판단·표기 규칙을 순수 함수로 분리합니다. UI 의존이 0이므로 FE-02(프리미티브)·FE-04(목 핸들러)와 같은 wave에서 병렬 진행됩니다. 컴포넌트 안에 비즈니스 로직·데이터 가공을 두지 않는다는 규칙(no-logic-in-component)의 실행 장치입니다.
포함 범위 (src/constants/**, src/utils/**, src/types/domain.ts):
- 한글 표기 매핑 — PRD Goals의 “한글 단계명 ↔ 상태값” 표를 그대로 상수화합니다:
APPLIED=지원 완료,DOCUMENT_SCREENING=서류전형,INTERVIEWING=면접,OFFERED=처우협의,ACCEPTED=최종 합격,REJECTED=불합격,WITHDRAWN=지원 사퇴,OFFER_DECLINED=오퍼 거절. 임의 번역 금지 — PRD가 SSOT입니다. - 허용 전이 맵 — BE TDD “상태 전이 표 / Application”을 상수화합니다.
OFFERED는ACCEPTED·OFFER_DECLINED2개만,WITHDRAWN은 앞 3단계에서만, 종료 4종은 빈 배열. 파일 상단에 “BEApplicationStatus.canTransitTo()가 SSOT — 변경 시 동기화 필수” 주석과 근거 문서 경로를 명시합니다. 계약 요청 #7(allowedNextStatuses응답 포함)이 수용되면 이 상수는 폴백으로 격하됩니다. - 종료 상태 판정 —
isTerminalStatus(status). 지원 상세가 CTA를 렌더할지 결정하는 근거입니다. - 상태 톤 매핑 — 진행 중=accent /
ACCEPTED=positive /REJECTED·OFFER_DECLINED=danger /WITHDRAWN=neutral (설계 “도메인 표시 규칙 → 토큰 매핑”). - 근무형태 확신도 표기 규칙 —
describeWorkArrangement(label)이{ fill, text, note }를 반환합니다:CONFIRMED→{fill:'filled', text:'재택 가능'},LIKELY→{fill:'outlined', text:'재택 가능', note:'본문 언급'},UNKNOWN→{fill:'none', text:'근무형태 정보 없음'}.UNKNOWN에 칩 형태를 주지 않는 것이 이 규칙의 핵심입니다(FR-34) — 정보가 없는데 정보처럼 보이면 안 됩니다.INFERRED는 P2 확장 지점으로 타입에만 존재합니다. - 정렬 가능 확신도 —
isSortableConfidence(c)가CONFIRMED·LIKELY만 true (FR-30). 필터로 쓰지 않습니다(FR-33) — 이 함수는 정렬 키 계산에만 쓰이며, 목록에서 제외하는 데 쓰면 안 된다는 주석을 남깁니다. - 마감일 포매터 —
describeDeadline(deadlineAt, postingStatus, closedReason):null→ “상시채용”, D-3 이내 →{text:'D-1', tone:'danger'}, 그 외 → “~7.30 마감”,CLOSED→ “마감됨 · {사유}”. D-day 계산은 KST 자정 기준 일 단위로 하고, 시각 차이로 D-day가 흔들리지 않게 날짜만 비교합니다. - 날짜 포매터 —
formatDate(7.20),formatDateTime(7.18 (금) 14:00),formatDateInput(2026-07-22). - 입력 검증 —
isBlank,isHttpUrl,isPositiveInteger. 폼 라이브러리를 도입하지 않는 대신 검증을 순수 함수로 분리해 테스트합니다(설계 “방안 5”).
파일 경계: src/types/api.ts(API DTO)는 FE-01이 소유합니다. 이 티켓은 src/types/domain.ts(파생 타입)만 새로 만듭니다.
의존
- FE-01
다이어그램
처리 흐름
sequenceDiagram participant C as 화면 컴포넌트 participant D as describeWorkArrangement participant T as ALLOWED_TRANSITIONS participant F as describeDeadline C->>D: label(confidence, keyword) D-->>C: fill · text · note (확신도 위계) C->>T: allowedNextStatuses(current) T-->>C: 갈 수 있는 상태만 C->>F: deadlineAt · postingStatus F-->>C: text · tone (상시채용 · D-day · 마감됨)
클래스 의존
flowchart LR subgraph Constants["constants"] Label[applicationStatusLabel] Trans[applicationTransition] Tone[statusTone] end subgraph Utils["utils"] Work[workArrangement] Deadline[deadline] DateFmt[date] Valid[validation] end subgraph Types["types"] Domain[domain.ts] end Trans --> Domain Label --> Domain Tone --> Domain Work --> Domain Deadline --> DateFmt
테스트 케이스
- 8개 상태의 한글 표기가 PRD 매핑표와 정확히 일치한다
OFFERED의 허용 전이가ACCEPTED·OFFER_DECLINED2개이고WITHDRAWN을 포함하지 않는다APPLIED·DOCUMENT_SCREENING·INTERVIEWING의 허용 전이에WITHDRAWN이 포함된다- 종료 상태 4종(
ACCEPTED·REJECTED·WITHDRAWN·OFFER_DECLINED)의 허용 전이가 빈 배열이다 isTerminalStatus가 종료 4종에만 true를 반환한다- 어떤 상태도 자기 자신으로의 전이를 허용하지 않는다
CONFIRMED확신도가fill: 'filled'로,LIKELY가fill: 'outlined'로 매핑된다UNKNOWN확신도가fill: 'none'과 “근무형태 정보 없음” 텍스트로 매핑된다 (칩 형태 아님)isSortableConfidence가CONFIRMED·LIKELY에만 true를 반환한다- 마감일이
null이면 “상시채용”으로 표기되고 D-day를 계산하지 않는다 - 마감일이 내일이면
D-1과 danger 톤이 반환된다 - 마감일이 4일 뒤면 danger가 아닌 일반 톤의 “~날짜 마감”이 반환된다
- 마감일이 오늘 23:59여도 현재 시각과 무관하게
D-0으로 계산된다 (날짜 단위 비교) CLOSED공고는 마감일과 무관하게 “마감됨 · 사유”로 표기된다isHttpUrl이http/https만 통과시키고javascript:·빈 문자열을 거부한다isBlank가 공백만 있는 문자열을 true로 판정한다
2차 갱신 반영 (2026-07-22)
전이 맵 폴백 격하 (계약 응답 #7)
- 서버가 상세·전이 응답에
allowedNextStatuses[]를 포함하므로,ALLOWED_TRANSITIONS상수는 서버 값이 없을 때만 쓰는 폴백으로 격하합니다.allowedNextStatuses(current, serverValue?)가 서버 값이 있으면 그대로 반환하고 없으면 상수를 씁니다. - 동기화 주석·전이 맵 전수 검증 테스트는 삭제 가능 — SSOT가 서버로 이동했습니다. 단 폴백 상수의 기본 동작 테스트(종료 상태 빈 배열 등)는 유지합니다.
애그리게이터 카테고리 상수 추가 → 3차 갱신에서 폐기 (아래 “3차 갱신 반영” 참조)
이 항목은 무효입니다. 카테고리는 API(
GET /api/aggregator-sources/categories)로 조회하는 것으로 최종 확정됐습니다.aggregatorCategories상수·categoriesOf를 만들지 않습니다.
추가 테스트 케이스
- 서버
allowedNextStatuses가 주어지면 폴백 상수보다 우선 반환된다 - 서버 값이 없으면 폴백 상수가 사용된다
3차 갱신 반영 (2026-07-22, BE 최종 확정)
aggregatorCategories 상수 폐기 (계약 최종 확정)
- 카테고리는
GET /api/aggregator-sources/categories?platform=API로 채우는 것으로 확정됐습니다. FE 상수화는 어댑터 지원 코드와 드리프트가 나므로 금지 → 2차 갱신에서 추가했던src/constants/aggregatorCategories.ts와categoriesOf순수 함수를 만들지 않습니다. 해당 2차 테스트 케이스(categoriesOf 관련)도 제외합니다.
platformLabel 유틸 추가 (src/utils/platformLabel.ts)
- 애그리게이터
platform코드 → 한글 라벨 매핑(SARAMIN→ “사람인”,JUMPIT→ “점핏”).alternateSources[]·소스 관리 목록·소스 등록 세그먼트가 공유하는 표시 규칙. 플랫폼은 P0 2종으로 고정이라 이건 상수화가 안전합니다(카테고리와 달리 어댑터 추가와 무관한 표시 라벨).
추가·정정 테스트 케이스
platformLabel('SARAMIN')이 “사람인”을,platformLabel('JUMPIT')이 “점핏”을 반환한다- 알 수 없는 플랫폼 코드는 코드 자체를 폴백으로 반환한다
- (정정) 카테고리 상수·
categoriesOf테스트는 존재하지 않는다 — 카테고리는 API로 조회
4차 갱신 반영 (2026-07-22, 회색지대 애그리게이터 P0 편입)
platformLabel 4종 추가 + isGrayZonePlatform 헬퍼
platformLabel에 4종 라벨 추가:WANTED→원티드,REMEMBER→리멤버,JOBKOREA→잡코리아,SURFIT→서핏 (기존 SARAMIN·JUMPIT 유지 → 총 6종).isGrayZonePlatform(platform): boolean순수 함수 — 회색지대(원티드·리멤버·잡코리아·서핏) true, 청정(사람인·점핏) false. S-12가 안내 배너 노출 여부를 이 함수로 판정합니다.- 두 함수 모두 플랫폼이 P0에서 6종으로 고정이라 상수화 안전(카테고리와 달리 어댑터 지원 코드가 아니라 고정 표시·분류 규칙).
추가 테스트 케이스
platformLabel이 6종(사람인·점핏·원티드·리멤버·잡코리아·서핏)을 정확히 반환한다isGrayZonePlatform이 원티드·리멤버·잡코리아·서핏에 true, 사람인·점핏에 false를 반환한다