[BE-08] 배민형 내부 API 소스 어댑터

[폐기 — 2026-07-22] 구현 중 실측 결과 career.woowahan.com/robots.txt가 수집 대상 경로 /w1/**Disallow로 명시함을 확인했습니다. robots.txt 존중 결정에 따라 배민 직접 어댑터를 폐기합니다. 배민 공고는 애그리게이터(원티드·점핏·사람인 등, BE-22~29) 수집으로 발견됩니다(DISCOVERED, 관심 승격 가능). 후행 티켓 영향: BE-10(수집 오케스트레이션)은 배민 어댑터에 의존하지 않습니다(SPI 레지스트리에 WOOWAHAN 어댑터 미등록). JobPlatform.WOOWAHAN enum 값은 유지하되 어댑터 구현체는 없습니다.

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “소스별 어댑터 설계” 근거 조사: 20260722-채용소스-조사-브리프.md §2 (실제 호출 검증: GET https://career.woowahan.com/w1/recruits?page=1&size=5 → 200, 총 61건, 인증 불필요)

변경 사항

SPA 뒤의 내부 JSON API를 직접 호출하는 어댑터입니다. 헤드리스 브라우저 없이 수집이 성립함이 조사에서 검증됐습니다(PRD Non-Goals와 정합).

능력 선언(SourceCapabilities):

항목근거
changeSignalKindSOURCE_VERSIONrecruitVersion 증가 = 수정 (해시 불필요)
providesDeadlinetruerecruitEndDate
providesStructuredWorkArrangementfalse근무형태 구조화 필드 없음 → ②(JD 본문)만 가능
requiresDetailFetch조사 결과에 따라 결정상세 엔드포인트 존재 여부 미검증 (TDD Open Questions #4)

핵심 설계 의도:

  • 마감일 정규화(FR-13)가 이 어댑터의 핵심입니다. recruitEndDate9999-*(상시채용 센티널) 또는 recruitCloseDate2999-*이면 deadlineAt = null로 통일합니다. 센티널 판정은 연도 임계값(>= 2999)으로 처리해 유사 센티널도 함께 걸러냅니다. 이 값이 도메인에 그대로 흘러가면 “9999년에 마감되는 공고”가 되어 마감일 경과 판정이 영원히 동작하지 않습니다.
  • 비공식 API 리스크를 전제로 설계합니다 — 사전 통보 없이 스키마가 바뀌므로, 필수 필드(recruitSeq·recruitName) 누락이 감지되면 그 회차를 Failed로 처리해 소스 가드(FR-15)가 발동하게 합니다. 예외를 삼키고 빈 목록을 반환하면 silent failure가 되어 마감 오판정으로 이어집니다.
  • 페이지네이션 base 미검증(TDD Open Questions #3): 첫 응답의 pageNumber로 0-based/1-based를 판별해 전량 수집하고, 응답의 총 건수와 실제 수집 건수가 불일치하면 회차를 Failed로 처리합니다. 부분 수집이 “공고가 사라졌다”로 오인되는 사고를 막습니다.
  • recruitSeqsourceJobId, recruitName → 직무 매칭 대상, structuredTagsemptyList().
  • 상세 엔드포인트 존재 여부는 이 티켓에서 확인합니다. 존재하면 requiresDetailFetch=true로 능력 선언만 바꾸면 되고, 없으면 목록 필드만으로 시작해 근무형태는 ②근거 부재로 UNKNOWN이 됩니다.
  • robots.txt·이용약관 확인 결과를 어댑터 KDoc에 기록합니다(NFR-9).

SPI 정합(BE-02): SourcePlatformAdapter를 구현하며 fetchList(CollectionDescriptor.CompanyBound)를 받습니다(sourceType=COMPANY_BOUND). 외부 호출은 SourceRequestExecutor를 경유합니다.

의존

  • BE-02

다이어그램

처리 흐름

sequenceDiagram
    participant R as JobSourceGatewayImpl
    participant A as WoowahanJobSourceAdapter
    participant C as WoowahanCareerClient
    participant W as career.woowahan.com
    R->>A: fetchList(descriptor)
    A->>C: fetchPage(첫 페이지)
    C->>W: GET /w1/recruits?page=N&size=100
    W-->>C: JSON (totalCount, pageNumber, recruits[])
    A->>A: pageNumber 로 base 판별
    loop 남은 페이지
        A->>C: fetchPage(다음)
    end
    alt 총 건수 == 수집 건수
        A->>A: 9999/2999 센티널 → deadlineAt=null 정규화
        A-->>R: Fetched(RawJobPosting[])
    else 불일치 또는 필수 필드 누락
        A-->>R: Failed(reason)
    end

클래스 의존

flowchart LR
    subgraph Domain["domain/posting"]
        Raw[RawJobPosting]
        Sig[ChangeSignature.SourceVersion]
        Outcome[SourceCollectionOutcome]
        Cap[SourceCapabilities]
    end
    subgraph Adapter["infrastructure/posting/collector/woowahan"]
        Ad[WoowahanJobSourceAdapter]
        Cl[WoowahanCareerClient]
        Dto[WoowahanRecruitResponse]
        Deadline[WoowahanDeadlineNormalizer]
    end
    Ad --> Cl
    Ad --> Deadline
    Cl --> Dto
    Deadline --> Raw
    Ad --> Sig
    Ad --> Outcome
    Ad --> Cap

테스트 케이스

  • 실제 응답 fixture를 파싱하면 recruitSeqsourceJobId로 매핑된다
  • recruitEndDate = 9999-12-31 00:00:00인 공고의 deadlineAtnull로 정규화된다
  • recruitEndDate = 2026-08-31 18:00:00인 공고는 실제 마감일로 파싱된다
  • recruitCloseDate = 2999-12-31도 센티널로 판정되어 null이 된다
  • recruitVersion 값이 ChangeSignature.SourceVersion으로 매핑되고, 버전이 증가하면 변경으로 판정된다
  • 버전이 동일하면 변경 없음으로 판정된다
  • 여러 페이지 응답을 전량 수집해 총 건수와 일치시킨다
  • 응답 총 건수와 수집 건수가 불일치하면 Failed를 반환한다
  • 필수 필드(recruitSeq)가 누락된 응답은 Failed를 반환하고 빈 목록을 반환하지 않는다
  • structuredTags가 빈 목록으로 반환되어 근무형태 ①근거가 없음을 표현한다
  • API가 5xx를 반환하면 Failed(reason)을 반환한다
  • supports(WOOWAHAN)가 true를 반환한다