[BE-23] 점핏 애그리게이터 어댑터

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “애그리게이터 어댑터 설계”, “방안 8” 근거 조사: 20260722-채용소스-조사-브리프.md “전체 플랫폼 조사 결과 / 채택 가능 — 규약 청정” (실측: jumpit-api.saramin.co.kr/api/positions?page=N&size=200&jobCategory={code}, JSON 무인증, id 안정 ID, 변경 감지 필드 없음, closedAt ISO / alwaysOpen 불리언, 백엔드 118건, /positions 미차단·GPTBot만 차단)

변경 사항

점핏 JSON API를 검색 조건으로 수집하는 애그리게이터 어댑터입니다. 조사에서 확인된 두 특성 — 변경 감지 필드 부재회사가 목록에 없고 상세에만 존재 — 가 이 어댑터의 설계 난점입니다(FR-68).

능력 선언(SourceCapabilities):

항목근거
sourceTypeAGGREGATOR
changeSignalKindFIELD_HASH변경 감지 필드 없음 → 주요 필드 해시 (FR-68)
providesDeadlinetrue (조건부)closedAt, 단 alwaysOpen 우선
companyIdentifierInDetailOnlytrue목록에 회사 없음 → 상세 조회로 식별 (FR-68)
규약 정책UA·요청 지연·size 상한 200·허용 경로 /api/positionsFR-69

핵심 설계 의도:

  • **변경 감지가 없으므로 ChangeSignature.FieldHash**를 씁니다 — 제목·마감일·회사 등 주요 필드를 정규화해 SHA-256 해시하고, 이전 회차 해시와 비교해 변경을 판단합니다(FR-68). 전체 본문 해시(BodyHash, 인크루트)와 구분되는 변이입니다 — 목록 필드만으로 계산해 상세 조회 없이도 변경 판단이 가능합니다.
  • 마감일 정규화(FR-67): alwaysOpen=trueclosedAt을 무시하고 deadlineAt=null(상시채용)입니다. alwaysOpen을 보지 않고 closedAt을 그대로 쓰면 상시채용 공고가 마감일 있는 공고로 잘못 분류됩니다(브리프 경고). alwaysOpen=falseclosedAt ISO를 파싱합니다.
  • 회사 식별(FR-68): 목록 응답에 회사가 없으므로 상세 조회로 sourceCompanyName을 채웁니다. companyIdentifierInDetailOnly=true가 이 동작을 선언하고, 상세 조회는 SourceRequestExecutor의 지연 정책을 적용받습니다. 상세 조회 실패 건은 회사 미상으로 남기되 회차를 실패시키지 않습니다(부분 실패 격리).
  • idsourceJobId, structuredTagsemptyList()(②만).
  • 모든 외부 호출은 SourceRequestExecutor(BE-02) 경유(FR-69).
  • robots·약관 확인 결과를 KDoc에 기록(NFR-9·NFR-11).
  • supportedCategories()로 지원 직무 카테고리를 선언합니다(FE 요청 #12) — 브리프 실측 코드(jobCategory=1 서버/백엔드 등) 기반.

의존

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

다이어그램

처리 흐름

sequenceDiagram
    participant R as JobSourceGatewayImpl
    participant A as JumpitAggregatorAdapter
    participant E as SourceRequestExecutor
    participant J as jumpit-api
    R->>A: fetchList(Aggregator descriptor)
    loop 페이지 (size≤200)
        A->>E: get(/api/positions, page)
        E->>J: GET positions?jobCategory&page&size
        J-->>E: JSON (회사 없음)
    end
    loop 공고별 (회사 식별)
        A->>E: get(상세)
        E->>J: GET position detail
        J-->>A: 회사명 → sourceCompanyName
    end
    A->>A: alwaysOpen→null / closedAt 파싱 · 필드 해시
    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/jumpit"]
        Ad[JumpitAggregatorAdapter]
        Dto[JumpitPositionResponse]
        Hash[FieldHashCalculator]
        Deadline[JumpitDeadlineNormalizer]
    end
    subgraph Common["collector/common"]
        Exec[SourceRequestExecutor]
    end
    Ad --> Exec
    Ad --> Dto
    Ad --> Hash
    Ad --> Deadline
    Hash --> Sig
    Deadline --> Raw
    Ad --> Cap
    Ad --> Outcome

테스트 케이스

  • 실제 응답 fixture를 파싱하면 idsourceJobId로 매핑된다
  • alwaysOpen=true이고 closedAt이 존재해도 deadlineAt=null(상시채용)로 정규화된다
  • alwaysOpen=false이면 closedAt ISO가 마감일로 파싱된다
  • 주요 필드가 동일하면 같은 FieldHash가 생성되어 변경 없음으로 판정된다
  • 제목이 바뀌면 FieldHash가 달라져 변경으로 판정된다
  • 목록에 회사가 없으므로 상세 조회로 sourceCompanyName이 채워진다
  • 상세 조회가 실패한 공고는 회사 미상으로 남되 회차는 실패하지 않는다
  • size가 200을 넘지 않고 요청 지연·UA가 적용된다
  • API가 5xx를 반환하면 Failed를 반환한다
  • supports(JUMPIT)가 true, sourceTypeAGGREGATOR, changeSignalKindFIELD_HASH를 반환한다
  • supportedCategories()가 코드↔라벨 목록(예: 1↔서버/백엔드)을 반환한다