[FE-12] 수동 공고 등록 화면

작업 내용 (설계 의도)

근거 설계: 20260722-공고알림앱-design-fe-web.md — “S-05 수동 공고 등록” 근거 요구: FR-20(수동 등록) · FR-21(동일 지원 관리) · FR-48(로그인·자소서 필수 공고 제외) · FR-13(마감일 없음 = 상시채용) · 시나리오 5 · 시나리오 6

변경 사항

자동 수집이 불가능한 공고(로그인 필요·자소서 필수·매핑 후보 0건 회사)를 사용자가 직접 등록하는 화면입니다. 등록된 공고는 자동 수집 공고와 같은 목록·같은 지원 관리 흐름에 들어갑니다(FR-21).

핵심 설계 의도:

  • Minimum Input — 필수는 공고명·링크 2개뿐입니다. 회사는 진입 경로(?companyId=)에서 이미 결정되므로 다시 묻지 않고 상단에 고정 표시합니다. 마감일은 선택입니다.
  • 마감일을 비우면 상시채용임을 필드 하단에 명시합니다(FR-13). 이 규칙을 모르면 사용자는 마감일을 억지로 채웁니다.
  • 자동 공고와의 차이를 안내 박스로 명시합니다 — “직접 추가한 공고는 자동 수집·마감 판정 대상이 아니에요. 지원 관리는 똑같이 사용할 수 있어요.” 앞 문장은 기대 관리, 뒤 문장은 FR-21 보장입니다. 이 안내가 없으면 사용자는 수동 공고가 자동으로 마감 처리될 것이라 기대하거나, 반대로 지원 관리가 안 될 것이라 오해합니다.
  • 입력 검증은 순수 함수에 위임합니다(FE-03의 isBlank·isHttpUrl). 폼 라이브러리를 도입하지 않는 대신 검증을 테스트 가능한 형태로 분리합니다.
  • 제출 실패(5xx) 시 입력값을 보존합니다 — 사용자가 붙여넣은 링크를 다시 찾게 만들지 않습니다.
  • companyId 쿼리 파라미터가 없으면 회사 선택 셀렉트를 렌더합니다(지원 목록 등 다른 경로에서 진입할 여지). 이 경우에만 회사 목록 조회의 loading 상태가 존재합니다.

범위: src/pages/posting/ManualJobPostingPage.tsx(스텁 대체), src/api/posting/manualRegistration.ts, src/hooks/posting/useRegisterManualJobPosting.ts.

Single Writer 주의: api/posting/list.ts(FE-10)·detail.ts(FE-11)를 건드리지 않습니다. 회사 목록 조회가 필요하면 FE-09의 useCompanies소비만 합니다.

의존

  • FE-05
  • BE 의존: BE-18 (수동 공고 등록 API)

다이어그램

처리 흐름

sequenceDiagram
    participant U as 사용자
    participant P as ManualJobPostingPage
    participant V as validation
    participant M as useRegisterManualJobPosting
    participant S as 서버
    U->>P: 공고명 · 링크 입력 후 제출
    P->>V: isBlank(title) · isHttpUrl(url)
    alt 검증 실패
        V-->>P: 실패
        P-->>U: 인라인 에러 (요청 전송 없음)
    else 검증 통과
        P->>M: mutate(companyId, title, url, deadlineAt?)
        M->>S: POST /api/job-postings/manual-registrations
        alt 성공
            S-->>M: {jobPostingId}
            M-->>U: 공고 상세로 이동 + 토스트
        else 실패
            S-->>M: 404 또는 500
            M-->>U: 에러 표시 + 입력값 보존
        end
    end

클래스 의존

flowchart LR
    subgraph Page["pages/posting"]
        Manual[ManualJobPostingPage]
    end
    subgraph Hooks["hooks/posting"]
        H[useRegisterManualJobPosting]
    end
    subgraph Api["api/posting"]
        A[manualRegistration.ts]
    end
    subgraph Utils["utils"]
        V[validation]
    end
    subgraph Ui["components/ui"]
        TF[TextField]
        DF[DateField]
        Btn[Button]
    end
    Manual --> H
    Manual --> V
    Manual --> TF
    Manual --> DF
    Manual --> Btn
    H --> A

테스트 케이스

  • ?companyId=1로 진입하면 회사명이 고정 표시되고 회사 입력 필드가 없다
  • companyId 없이 진입하면 회사 선택 셀렉트가 렌더된다
  • 공고명이 비어 있으면 제출 시 인라인 에러가 보이고 요청이 전송되지 않는다
  • 잘못된 형식의 링크(javascript: 등)는 인라인 에러로 거부된다
  • 마감일 필드 하단에 “비워두면 상시채용으로 저장해요” 안내가 렌더된다
  • 마감일 없이 제출하면 deadlineAtnull로 전송된다
  • 마감일과 함께 제출하면 해당 값이 전송된다
  • 자동 공고와의 차이를 알리는 안내 박스가 렌더된다
  • 등록 성공 시 공고 상세로 이동하고 토스트가 표시된다
  • 등록이 404(존재하지 않는 회사)면 전면 에러와 홈 이동 수단이 렌더된다
  • 등록이 500이면 입력값이 보존된 채 에러 토스트가 표시된다
  • 제출 중 CTA에 로딩이 표시되고 폼이 비활성화된다
  • 다크 모드에서 입력 필드와 안내 박스가 오류 없이 렌더된다