[FE-11] 공고 상세 화면 · 지원 기록 생성 시트

작업 내용 (설계 의도)

근거 설계: 20260722-공고알림앱-design-fe-web.md — “S-04 공고 상세” 근거 요구: FR-29(근거·확신도 함께 저장) · FR-34 · FR-42(미지원 = 기록 미존재) · FR-21(수동 공고도 동일 지원 관리) · 시나리오 6

변경 사항

공고 하나의 판단 근거를 모두 보여주고, “지원 기록을 남길 것인가”라는 하나의 결론으로 이어지는 화면입니다(토스 Clear CTA).

핵심 설계 의도:

  • CTA는 언제나 하나입니다. 지원 기록이 없으면 [지원 기록 남기기], 있으면 [지원 현황 보기]바뀝니다. 버튼을 2개로 늘리지 않습니다.
  • “미지원”을 상태로 표현하지 않습니다(FR-42). 응답의 applicationnull이면 그냥 CTA가 생성 모드일 뿐이고, NOT_APPLIED 같은 배지를 만들지 않습니다.
  • 근무형태 근거를 스니펫과 함께 노출합니다(FR-29). 목록에서는 라벨만 보이지만 상세에서는 “왜 그렇게 판정했는가”(근거 단계 + 원문 스니펫)를 볼 수 있어야 사용자가 확신도를 신뢰하거나 의심할 수 있습니다.
  • 매칭 결과도 근거와 함께 표시합니다 — 매칭됨이면 매칭된 그룹, 매칭 안 됨이면 사유(제외어 등). 매칭 실패 공고도 조회 대상이므로(FR-25) “왜 알림이 안 왔는가”를 여기서 답합니다.
  • accessRestricted상단 warning 배너로 “로그인이 필요한 공고예요”를 알리고 공고 바로가기를 강조합니다(시나리오 6 — P0에서는 알림 대신 배지·배너로 표현).
  • 지원 기록 생성 시트는 Minimum Input입니다 — 지원일 기본값을 오늘로 채우고 메모는 선택입니다. “기록하면 ‘지원 완료’ 상태로 시작해요”로 결과를 사전 고지합니다.
  • 시트 제출 실패 시 입력값을 보존합니다. 409(이미 지원 기록 존재)는 시트를 유지한 채 인라인 에러 + 지원 현황 링크를 제공합니다.
  • 자동 수집 공고와 수동 등록 공고의 화면·흐름이 동일합니다(FR-21) — origin에 따른 분기를 만들지 않습니다.

범위: src/pages/posting/JobPostingDetailPage.tsx(스텁 대체), 전용 컴포넌트(WorkArrangementEvidenceBox·MatchResultBox·CreateApplicationSheet), src/api/posting/detail.ts, src/api/application/create.ts, src/hooks/posting/useJobPosting.ts, src/hooks/application/useCreateApplication.ts.

의존

  • FE-05 (WorkArrangementLabel·DeadlineText·ApplicationStatusChip)
  • BE 의존: BE-18 (공고 상세), BE-14 (지원 기록 생성)

다이어그램

처리 흐름

sequenceDiagram
    participant U as 사용자
    participant P as JobPostingDetailPage
    participant H as useJobPosting
    participant Sheet as CreateApplicationSheet
    participant M as useCreateApplication
    participant S as 서버
    P->>H: GET /api/job-postings/{id}
    alt application 이 null
        P-->>U: CTA "지원 기록 남기기"
        U->>Sheet: CTA 클릭
        Sheet->>M: mutate(jobPostingId, appliedAt, memo?)
        M->>S: POST /api/applications
        alt 409 이미 지원함
            S-->>Sheet: 409
            Sheet-->>U: 인라인 에러 + 지원 현황 링크
        else 성공
            S-->>M: {applicationId}
            M-->>P: 상세 무효화 후 지원 상세로 이동
        end
    else application 존재
        P-->>U: 상태 칩 + CTA "지원 현황 보기"
    end

클래스 의존

flowchart LR
    subgraph Page["pages/posting"]
        Detail[JobPostingDetailPage]
        Evi[WorkArrangementEvidenceBox]
        Match[MatchResultBox]
        Sheet[CreateApplicationSheet]
    end
    subgraph Domain["components/domain"]
        WA[WorkArrangementLabel]
        DT[DeadlineText]
        SC[ApplicationStatusChip]
    end
    subgraph Hooks["hooks"]
        HP[useJobPosting]
        HA[useCreateApplication]
    end
    Detail --> Evi
    Detail --> Match
    Detail --> Sheet
    Detail --> WA
    Detail --> DT
    Detail --> SC
    Detail --> HP
    Sheet --> HA

테스트 케이스

  • 공고 제목·회사·플랫폼·근무형태 라벨·마감 표기가 렌더된다
  • 근무형태 근거 박스에 확신도와 원문 스니펫이 렌더된다
  • 확신도가 UNKNOWN이면 “근무형태 정보 없음”이 표기되고 근거 박스가 렌더되지 않는다
  • 매칭된 공고에 매칭된 키워드 그룹이 표시된다
  • 매칭 안 된 공고에 제외 사유가 표시된다
  • 지원 기록이 없으면 CTA가 “지원 기록 남기기”로 렌더된다
  • 지원 기록이 있으면 CTA가 “지원 현황 보기”로 바뀌고 상태 칩이 상단에 렌더된다
  • 지원 기록이 없어도 NOT_APPLIED 같은 상태 배지가 렌더되지 않는다
  • accessRestricted 공고에 상단 warning 배너가 렌더된다
  • 수동 등록 공고도 자동 수집 공고와 동일한 CTA·흐름으로 렌더된다
  • 지원 기록 생성 시트의 지원일 기본값이 오늘 날짜다
  • 시트 제출 성공 시 지원 상세로 이동하고 토스트가 표시된다
  • 시트 제출이 409면 시트가 유지되고 인라인 에러와 지원 현황 링크가 보인다
  • 시트 제출이 500이면 입력값이 보존된 채 에러 토스트가 표시된다
  • 404 응답 시 “공고를 찾을 수 없어요”와 회사 목록 이동 수단이 렌더된다
  • 로딩 중 스켈레톤이 렌더되고 CTA는 비활성 상태로 보인다
  • 공고 바로가기 링크가 새 탭으로 열리는 안전한 속성(rel="noopener noreferrer")을 갖는다

2차 갱신 반영 (2026-07-22) — 크로스 소스 출처 (FR-64)

AlternateSourcesBox 추가

  • 상세 응답의 alternateSources[]가 비어 있지 않으면 “이 공고는 N곳에 게재됐어요” 박스를 렌더합니다(AlternateSourcesBox — 이 화면 전용, 공용화하지 않음).
  • 대표 출처를 “(대표)“로 표시하고(isRepresentative), 각 출처는 외부 링크(새 탭, rel="noopener noreferrer")로 렌더합니다.
  • “중복”·“dedup” 같은 내부 용어를 쓰지 않고 “게재됐어요”로 표현합니다(토스 Casual Concept). 지원·마감·매칭은 대표 공고 기준이므로 사용자는 대표 하나만 신경 쓰면 됩니다.
  • alternateSources[]가 비면 박스를 렌더하지 않습니다(단독 공고).
  • 지원 여부는 상세 응답의 application(없으면 null) 기준으로 기존과 동일하게 처리합니다.

계약 (요청 #11 확정 대기)

  • alternateSources[] 아이템 shape({ jobSourceId, platform, postingUrl, isRepresentative })는 요청안 기준으로 MSW 목·타입에 선언하고, BE 확정 시 FE-17이 동기화합니다.

추가 테스트 케이스

  • alternateSources[]가 3건이면 대표 1건이 “(대표)“로, 나머지가 외부 링크로 렌더된다
  • alternateSources[]가 비면 크로스 소스 박스가 렌더되지 않는다
  • 대체 출처 링크가 새 탭 안전 속성(rel="noopener noreferrer")을 갖는다

3차 갱신 반영 (2026-07-22, BE 최종 확정)

alternateSources[] shape 확정

  • shape = { jobSourceId, platform, postingUrl, isRepresentative }, 대표 자신을 포함한 그룹 전체가 반환됩니다(기존 “확정 대기” 해소).
  • isRepresentative === true인 항목을 “(대표)“로 강조하고 나머지는 외부 링크로 렌더합니다.
  • sourceLabel 필드가 없으므로 platform을 FE-03 platformLabel로 매핑해 표시합니다(예: SARAMIN → “사람인”).
  • alternateSources[]가 비면(단독 공고) 박스를 렌더하지 않습니다.

정정 테스트 케이스

  • alternateSources[]에서 isRepresentative=true인 항목이 “(대표)“로 강조된다
  • 대체 출처의 platform이 한글 라벨로 렌더된다