[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):

  1. 한글 표기 매핑 — PRD Goals의 “한글 단계명 ↔ 상태값” 표를 그대로 상수화합니다: APPLIED=지원 완료, DOCUMENT_SCREENING=서류전형, INTERVIEWING=면접, OFFERED=처우협의, ACCEPTED=최종 합격, REJECTED=불합격, WITHDRAWN=지원 사퇴, OFFER_DECLINED=오퍼 거절. 임의 번역 금지 — PRD가 SSOT입니다.
  2. 허용 전이 맵 — BE TDD “상태 전이 표 / Application”을 상수화합니다. OFFEREDACCEPTED·OFFER_DECLINED 2개만, WITHDRAWN은 앞 3단계에서만, 종료 4종은 빈 배열. 파일 상단에 “BE ApplicationStatus.canTransitTo()가 SSOT — 변경 시 동기화 필수” 주석과 근거 문서 경로를 명시합니다. 계약 요청 #7(allowedNextStatuses 응답 포함)이 수용되면 이 상수는 폴백으로 격하됩니다.
  3. 종료 상태 판정isTerminalStatus(status). 지원 상세가 CTA를 렌더할지 결정하는 근거입니다.
  4. 상태 톤 매핑 — 진행 중=accent / ACCEPTED=positive / REJECTED·OFFER_DECLINED=danger / WITHDRAWN=neutral (설계 “도메인 표시 규칙 → 토큰 매핑”).
  5. 근무형태 확신도 표기 규칙describeWorkArrangement(label){ fill, text, note }를 반환합니다: CONFIRMED{fill:'filled', text:'재택 가능'}, LIKELY{fill:'outlined', text:'재택 가능', note:'본문 언급'}, UNKNOWN{fill:'none', text:'근무형태 정보 없음'}. UNKNOWN에 칩 형태를 주지 않는 것이 이 규칙의 핵심입니다(FR-34) — 정보가 없는데 정보처럼 보이면 안 됩니다. INFERRED는 P2 확장 지점으로 타입에만 존재합니다.
  6. 정렬 가능 확신도isSortableConfidence(c)CONFIRMED·LIKELY만 true (FR-30). 필터로 쓰지 않습니다(FR-33) — 이 함수는 정렬 키 계산에만 쓰이며, 목록에서 제외하는 데 쓰면 안 된다는 주석을 남깁니다.
  7. 마감일 포매터describeDeadline(deadlineAt, postingStatus, closedReason): null → “상시채용”, D-3 이내 → {text:'D-1', tone:'danger'}, 그 외 → “~7.30 마감”, CLOSED → “마감됨 · {사유}”. D-day 계산은 KST 자정 기준 일 단위로 하고, 시각 차이로 D-day가 흔들리지 않게 날짜만 비교합니다.
  8. 날짜 포매터formatDate(7.20), formatDateTime(7.18 (금) 14:00), formatDateInput(2026-07-22).
  9. 입력 검증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_DECLINED 2개이고 WITHDRAWN을 포함하지 않는다
  • APPLIED·DOCUMENT_SCREENING·INTERVIEWING의 허용 전이에 WITHDRAWN이 포함된다
  • 종료 상태 4종(ACCEPTED·REJECTED·WITHDRAWN·OFFER_DECLINED)의 허용 전이가 빈 배열이다
  • isTerminalStatus가 종료 4종에만 true를 반환한다
  • 어떤 상태도 자기 자신으로의 전이를 허용하지 않는다
  • CONFIRMED 확신도가 fill: 'filled'로, LIKELYfill: 'outlined'로 매핑된다
  • UNKNOWN 확신도가 fill: 'none'과 “근무형태 정보 없음” 텍스트로 매핑된다 (칩 형태 아님)
  • isSortableConfidenceCONFIRMED·LIKELY에만 true를 반환한다
  • 마감일이 null이면 “상시채용”으로 표기되고 D-day를 계산하지 않는다
  • 마감일이 내일이면 D-1과 danger 톤이 반환된다
  • 마감일이 4일 뒤면 danger가 아닌 일반 톤의 “~날짜 마감”이 반환된다
  • 마감일이 오늘 23:59여도 현재 시각과 무관하게 D-0으로 계산된다 (날짜 단위 비교)
  • CLOSED 공고는 마감일과 무관하게 “마감됨 · 사유”로 표기된다
  • isHttpUrlhttp/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.tscategoriesOf 순수 함수를 만들지 않습니다. 해당 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를 반환한다