[BE-29] 서핏 애그리게이터 어댑터 (규약 회색지대)

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “애그리게이터 어댑터 설계 — 규약 회색지대 4종” 근거 조사: 20260722-채용소스-조사-브리프.md “서핏 채용” (실측: POST api.surfit.io/v1/jobs/all body {"page":N} JSON 무인증, Origin/Referer 헤더 필요, id 안정 ID, is_closed 불리언 + close 문자열(“상시채용”) + ads_end_at. robots /jobs 미차단, 기업약관 크롤링 금지 취지. speciality 필터 값 포맷 미확정)

변경 사항

서핏 JSON API를 수집하는 애그리게이터 어댑터입니다. 테크·스타트업 큐레이션이라 신호 대 잡음비가 높습니다(브리프). speciality 필터 포맷 미검증Origin/Referer 헤더 요구가 구현 난점입니다.

능력 선언(SourceCapabilities):

항목근거
sourceTypeAGGREGATORspeciality 필터(포맷 실호출 확정)
changeSignalKindFIELD_HASH변경 감지 필드 없음
providesDeadlinetrue (조건부)is_closed·close·ads_end_at 조합
규약 정책UA 명시·Origin/Referer 헤더·요청 지연·페이지 상한·1일 1회·재배포 금지robots·약관

핵심 설계 의도:

  • speciality 필터 포맷을 첫 단계 실호출로 확정합니다(브리프 방침). 확정 전까지 searchCategoryCode를 그대로 전달하고, supportedCategories()는 실호출로 확인된 값으로 채웁니다. 어댑터 계약은 이미 이를 수용합니다.
  • Origin/Referer 헤더 요구SourceRequestExecutor가 소스별 고정 헤더(UA 포함)를 부착하도록 정책에 담습니다. 어댑터가 헤더를 빠뜨려 403을 맞는 것을 방지합니다.
  • 마감일 정규화(FR-67)is_closed=true면 마감 신호(마감 판정 보조), close=“상시채용” 문자열이면 deadlineAt=null(상시채용), 그 외 ads_end_at 파싱. 세 필드 조합 우선순위를 실호출로 확정합니다.
  • 변경 감지 — 필드 없음 → FieldHash(제목·마감·회사).
  • idsourceJobId, 회사명은 목록에서 확보 → sourceCompanyName.
  • 회색지대 규약 — 기업약관 크롤링 금지 취지라 재배포·상업 이용 금지(개인 열람 전용), 1일 1회(DB 유니크), 요청 지연. robots /jobs 미차단 확인 결과를 KDoc에 기록(NFR-9·NFR-11).
  • 모든 호출은 SourceRequestExecutor 경유. supportedCategories() 선언.

롤백: disabled_at으로 소프트 비활성화.

의존

  • BE-02 (SPI·SourceRequestExecutor·ChangeSignature.FieldHash)

다이어그램

처리 흐름

sequenceDiagram
    participant R as JobSourceGatewayImpl
    participant A as SurfitAggregatorAdapter
    participant E as SourceRequestExecutor
    participant S as api.surfit.io
    R->>A: fetchList(Aggregator descriptor)
    loop 페이지 (Origin/Referer/UA · 지연)
        A->>E: post(/v1/jobs/all, {page, speciality})
        E->>S: POST (고정 헤더 부착)
        S-->>E: JSON (id·is_closed·close·ads_end_at)
    end
    A->>A: 마감일 정규화(is_closed·close·ads_end_at) · 필드 해시 · 회사명 추출
    A-->>R: Fetched(RawJobPosting[])

클래스 의존

flowchart LR
    subgraph Domain["domain/posting"]
        Raw[RawJobPosting]
        Sig[ChangeSignature.FieldHash]
        Cap[SourceCapabilities]
        Outcome[SourceCollectionOutcome]
    end
    subgraph Adapter["infrastructure/posting/collector/aggregator/surfit"]
        Ad[SurfitAggregatorAdapter]
        Dto[SurfitJobResponse]
        Hash[FieldHashCalculator]
        Deadline[SurfitDeadlineNormalizer]
    end
    subgraph Common["collector/common"]
        Exec[SourceRequestExecutor]
    end
    Ad --> Exec
    Ad --> Dto
    Ad --> Hash
    Ad --> Deadline
    Hash --> Sig
    Ad --> Cap
    Ad --> Outcome

테스트 케이스

  • 실호출 확정 fixture를 파싱하면 idsourceJobId, 회사명이 sourceCompanyName으로 매핑된다
  • close=“상시채용”이면 deadlineAt=null(상시채용)로 정규화된다
  • is_closed=true이면 마감 신호로 처리된다
  • ads_end_at이 있고 상시채용이 아니면 마감일로 파싱된다
  • Origin/Referer/UA 헤더가 항상 부착된다
  • 주요 필드가 동일하면 같은 FieldHash로 변경 없음으로 판정된다
  • 요청 사이에 지연이 적용되고 페이지 상한을 넘지 않는다
  • API가 5xx/헤더 누락 403을 반환하면 Failed를 반환한다
  • supports(SURFIT)가 true를 반환한다
  • 첫 단계 실호출로 speciality 포맷과 마감 필드 우선순위를 확정한다(구현 절차)
  • supportedCategories()가 실호출로 확인된 코드↔라벨 목록을 반환한다