[FE-10] 공고 목록 화면 (회사 상세) — 상태 세그먼트 · 정렬 · 매칭 강등 섹션

작업 내용 (설계 의도)

근거 설계: 20260722-공고알림앱-design-fe-web.md — “S-03 회사 상세”, “방안 2 매칭/비매칭” 근거 요구: FR-50 · FR-25(매칭 실패 공고도 저장) · FR-30·33(근무형태는 라벨·정렬 전용, 필터 아님) · FR-34 · FR-18(CLOSED 소프트 삭제)

변경 사항

정보 밀도와 절제의 균형이 이 화면의 과제입니다. “아무것도 제외하면 안 되는데(FR-25·33) 다 똑같이 보여주면 밀도가 무너진다”를 세 가지 장치로 해결합니다.

  1. 상태는 세그먼트 탭(진행 중 / 마감) — 서버 postingStatus 파라미터를 사용합니다. CLOSED 공고는 삭제되지 않고(FR-18, NFR-5) 마감 탭에서 마감 사유와 함께 조회됩니다. 진행 중 탭에 섞지 않는 근거: 이 화면의 목적은 “지금 지원 가능한 것”의 판단이며, 섞으면 그 목적이 흐려집니다.
  2. 매칭 여부는 필터가 아니라 강등 — 기본 목록은 매칭된 공고, 하단에 “매칭되지 않은 공고 N건” 접힘 섹션(기본 닫힘, 건수는 항상 노출)을 둡니다. 제외가 아니라 위계 분리라 FR-25를 지키면서 밀도를 얻습니다. 건수를 상시 노출하는 이유: 사용자가 키워드 조건을 바꿔야 할 시점을 인지할 수 있어야 합니다.
  3. 근무형태는 정렬 셀렉트의 한 옵션일 뿐입니다(FR-33). 어떤 확신도에서도 목록에서 제외하지 않습니다. 정렬 라벨은 “근무형태 확실한 순”으로 표기해 확신도 기준임을 드러냅니다(FR-30).

그 외:

  • 필터·정렬 상태는 URL search params가 SSOT입니다(?postingStatus=&sort=). 전역 스토어에 두지 않는 근거: 뒤로가기·새로고침·링크 공유가 자연히 동작해야 합니다.
  • 지원 기록이 있는 공고에 [지원함] 배지를 표시하며, 마감 탭의 CLOSED 공고에서도 배지를 유지합니다(지원 이력은 공고 마감과 무관하게 보존).
  • accessRestricted 공고에 접근 제한 배지를 표시합니다(시나리오 6의 P0 표현).
  • 하단에 [공고 직접 추가] 보조 CTA(outline) — 주 CTA와 경쟁하지 않게 secondary variant를 씁니다.
  • empty가 3종으로 갈립니다: 진행 중 0건(마감 탭 유도) / 마감 0건 / 회사 전체 0건(시딩 전 — “오늘 자정에 첫 수집이 진행돼요” + 직접 추가 유도).

계약 리스크 (blocking): 위 화면은 목록 응답의 matched·applied·workArrangement·closedReason·accessRestricted 필드에 의존하는데 TDD 계약이 {jobPostings:[...]}로만 명시돼 있습니다 → 계약 요청 #4. 미수용 시 폴백은 “접힘 섹션 제거(전건 단일 목록) + 지원함 배지 제거”이며 FR-25는 지키되 밀도가 떨어집니다.

범위: src/pages/posting/JobPostingListPage.tsx(스텁 대체), 전용 컴포넌트(PostingStatusSegment·PostingSortSelect·UnmatchedSection), src/api/posting/list.ts, src/hooks/posting/useJobPostings.ts.

의존

  • FE-05 (JobPostingCard·WorkArrangementLabel·DeadlineText)
  • BE 의존: BE-18 (공고 조회 API)

다이어그램

처리 흐름

sequenceDiagram
    participant U as 사용자
    participant P as JobPostingListPage
    participant R as useSearchParams
    participant H as useJobPostings
    participant S as 서버
    U->>P: /companies/1 진입
    P->>R: postingStatus 기본 OPEN · sort 기본 DISCOVERED
    P->>H: (companyId, postingStatus, sort)
    H->>S: GET /api/companies/1/job-postings
    S-->>P: 공고 전건 (매칭·비매칭 모두)
    P-->>U: 매칭 공고 목록 + "매칭되지 않은 공고 N건" 접힘
    U->>P: 마감 탭 클릭
    P->>R: postingStatus=CLOSED (URL 갱신)
    P->>H: 재조회
    S-->>P: 마감 공고 + closedReason

클래스 의존

flowchart LR
    subgraph Page["pages/posting"]
        List[JobPostingListPage]
        Seg[PostingStatusSegment]
        Sort[PostingSortSelect]
        Un[UnmatchedSection]
    end
    subgraph Domain["components/domain"]
        JC[JobPostingCard]
    end
    subgraph Hooks["hooks/posting"]
        H[useJobPostings]
    end
    subgraph Api["api/posting"]
        L[list.ts]
    end
    List --> Seg
    List --> Sort
    List --> Un
    List --> JC
    Un --> JC
    List --> H
    H --> L

테스트 케이스

  • 매칭된 공고가 기본 목록에 렌더되고 비매칭 공고는 접힘 섹션에 들어간다
  • 접힘 섹션 헤더에 비매칭 공고 건수가 항상 노출된다 (닫힌 상태에서도)
  • 접힘 섹션을 펼치면 비매칭 공고가 렌더된다 (목록에서 제외되지 않음)
  • 마감 탭으로 전환하면 URL의 postingStatus가 갱신되고 CLOSED 공고가 마감 사유와 함께 렌더된다
  • 진행 중 탭에 CLOSED 공고가 렌더되지 않는다
  • 정렬을 “근무형태 확실한 순”으로 바꾸면 URL의 sort가 갱신되고 재조회된다
  • 근무형태 확신도가 UNKNOWN인 공고도 목록에서 제외되지 않는다 (필터 아님)
  • 확신도 UNKNOWN 공고에 “근무형태 정보 없음”이 표기된다
  • 지원 기록이 있는 공고에 [지원함] 배지가 렌더된다
  • 마감 탭의 CLOSED 공고에도 지원함 배지가 유지된다
  • accessRestricted 공고에 접근 제한 배지가 렌더된다
  • 마감일이 null인 공고가 “상시채용”으로 표기된다
  • 공고 카드 클릭 시 /job-postings/{id}로 이동한다
  • 진행 중 0건이면 마감 탭으로 유도하는 empty가 렌더된다
  • 회사 전체 공고가 0건이면 “오늘 자정에 첫 수집” 안내 empty가 렌더된다
  • 조회 실패 시 목록 영역만 에러 대체되고 세그먼트·CTA는 유지된다
  • [공고 직접 추가] 클릭 시 /job-postings/new?companyId=1로 이동한다
  • URL에 postingStatus=CLOSED를 직접 넣고 진입하면 마감 탭이 선택된 상태로 렌더된다

2차 갱신 반영 (2026-07-22)

지원 여부 판정 변경 (계약 응답 #3)

  • 목록 아이템의 applied: boolean이 **applicationId: number | null**로 확정됐습니다. [지원함] 배지는 applicationId != null 조건으로 렌더하고, 배지 클릭 시 applicationId로 지원 상세로 바로 이동합니다.

크로스 소스 표기 (FR-64)

  • 목록은 대표 공고만 나옵니다(서버가 representative_id IS NULL로 거름). alternateSourceCount > 0인 공고에 🔗 N곳 게재 칩(FE-05 CrossSourceChip)을 렌더합니다.
  • sourceType(COMPANY_BOUND/AGGREGATOR)은 목록에서 별도 라벨로 강조하지 않습니다 — 대표 공고 기준이라 사용자는 출처 유형을 신경 쓸 필요가 없고, 구체 출처는 상세의 alternateSources[]에 있습니다.

추가 테스트 케이스

  • applicationId != null인 공고에 지원함 배지가 렌더되고 null이면 없다
  • applicationId가 있는 공고의 지원함 배지 클릭 시 지원 상세로 이동한다
  • alternateSourceCount가 2 이상인 공고에 ”🔗 N곳 게재” 칩이 렌더된다
  • alternateSourceCount가 0인 공고에 크로스 소스 칩이 렌더되지 않는다