[BE-28] 잡코리아 애그리게이터 어댑터 (규약 회색지대 · 경로 화이트리스트)

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “애그리게이터 어댑터 설계 — 규약 회색지대 4종”, “규약 강제 3중 방어” 근거 조사: 20260722-채용소스-조사-브리프.md “잡코리아” (실측: 목록 jobkorea.co.kr/recruit/joblist?menucode=duty&dutyCtgr=·상세 /Recruit/GI_Read/{gno} JSON-LD JobPosting, identifier·hiringOrganization·validThrough(ISO), 백엔드 1,958건. robots.txt가 ClaudeBot·anthropic-ai 등 AI 크롤러를 Disallow: /로 명시 차단, 키워드 검색 경로는 일반 크롤러에게도 차단, 허용 경로는 목록 1페이지 수준)

변경 사항

이 프로젝트에서 규약 준수가 가장 까다로운 어댑터입니다. robots가 AI 크롤러를 명시 차단하고 키워드 검색을 막으므로, 허용 경로 화이트리스트로 금지 경로 호출을 구조적으로 불가능하게 하는 것이 최우선입니다.

능력 선언(SourceCapabilities):

항목근거
sourceTypeAGGREGATOR직무 카테고리(dutyCtgr)
changeSignalKindBodyHash등록일만 제공 → 상세 본문 해시
providesDeadlinetrueJSON-LD validThrough
requiresDetailFetchtrue상세 /Recruit/GI_Read/{gno}의 JSON-LD
규약 정책허용 경로 화이트리스트 ["/recruit/joblist", "/Recruit/GI_Read"], UA 일반 식별자, 요청 지연, 목록 1페이지 + 개별 상세로 제한robots

핵심 설계 의도 — 규약 강제가 본체:

  • 경로 화이트리스트(FR-69-1)SourceCompliancePolicy.allowedPathPrefixes=["/recruit/joblist", "/Recruit/GI_Read"]. SourceRequestExecutor가 이 prefix에 없는 경로 호출을 예외로 차단합니다. 키워드 검색 경로(예: /Search/)를 어댑터가 실수로 때리는 것이 코드 레벨에서 불가능합니다. 이 테스트가 이 티켓의 최우선 검증입니다.
  • 수집 범위 제한 — 허용 경로에서 페이지네이션이 실질 1페이지만 동작한다는 브리프 사실을 반영해, 직무 카테고리 목록 1페이지 + 그 안의 개별 상세로 제한합니다. 전체 1,958건을 긁지 않습니다 — 목록 1페이지에 노출된 공고만 대상.
  • UA는 일반 식별자 — AI 크롤러 UA(ClaudeBot 등)가 아닌 일반 식별 UA를 씁니다(robots가 AI 크롤러만 차단하므로). 봇 차단은 없습니다.
  • 마감일 정규화(FR-67) — JSON-LD validThrough ISO 파싱 / 상시채용 문자열 또는 validThrough가 등록일 기준 약 +1년이면 상시채용(null)으로 정규화.
  • 변경 감지 — 등록일만 있어 BodyHash(상세 JSON-LD 정규화 본문 해시). 상세 조회는 지연 정책 적용.
  • data-gno(목록) / JSON-LD identifier(상세) → sourceJobId. hiringOrganizationsourceCompanyName.
  • robots·약관 확인 결과와 AI 크롤러 차단·경로 제한 근거를 KDoc에 명시(NFR-9·NFR-11). supportedCategories()dutyCtgr↔라벨 선언.

롤백: disabled_at으로 소프트 비활성화(robots 정책 변경 시 즉시).

의존

  • BE-02 (SPI·SourceRequestExecutor 경로 화이트리스트·ChangeSignature.BodyHash)

다이어그램

처리 흐름

sequenceDiagram
    participant R as JobSourceGatewayImpl
    participant A as JobkoreaAggregatorAdapter
    participant E as SourceRequestExecutor
    participant J as jobkorea.co.kr
    R->>A: fetchList(Aggregator descriptor)
    A->>E: get(/recruit/joblist?dutyCtgr) — 화이트리스트 통과
    E->>J: GET joblist (1페이지)
    J-->>A: 목록 (data-gno)
    loop 공고별 (지연)
        A->>E: get(/Recruit/GI_Read/{gno}) — 화이트리스트 통과
        E->>J: GET 상세 (JSON-LD)
        J-->>A: validThrough·hiringOrganization
    end
    Note over A,E: 키워드 검색 경로 호출 시 Executor가 예외로 차단
    A->>A: validThrough 정규화 · 본문 해시
    A-->>R: Fetched(RawJobPosting[])

클래스 의존

flowchart LR
    subgraph Domain["domain/posting"]
        Raw[RawJobPosting]
        Sig[ChangeSignature.BodyHash]
        Cap[SourceCapabilities]
        Policy[SourceCompliancePolicy]
    end
    subgraph Adapter["infrastructure/posting/collector/aggregator/jobkorea"]
        Ad[JobkoreaAggregatorAdapter]
        ListP[JobkoreaListParser]
        JsonLd[JobkoreaJsonLdParser]
        Hash[BodyHashCalculator]
    end
    subgraph Common["collector/common"]
        Exec[SourceRequestExecutor]
    end
    Ad --> Exec
    Ad --> ListP
    Ad --> JsonLd
    JsonLd --> Hash
    Hash --> Sig
    Cap --> Policy
    Ad --> Cap

테스트 케이스

  • 키워드 검색 경로(예: /Search/) 호출을 시도하면 SourceRequestExecutor가 예외로 차단한다 (최우선)
  • 허용 경로 /recruit/joblist·/Recruit/GI_Read 호출은 통과한다
  • 목록 1페이지 + 개별 상세로만 수집하고 전체 페이지를 순회하지 않는다
  • JSON-LD identifiersourceJobId, hiringOrganizationsourceCompanyName으로 매핑된다
  • validThrough ISO가 마감일로 파싱된다
  • 상시채용 문자열 또는 validThrough가 등록일 +1년 근사면 deadlineAt=null로 정규화된다
  • 상세 본문이 동일하면 같은 BodyHash로 변경 없음으로 판정된다
  • UA가 AI 크롤러가 아닌 일반 식별자로 부착된다
  • 상세 요청 사이에 지연이 적용된다
  • 목록 조회 자체가 실패하면 Failed를 반환한다
  • supports(JOBKOREA)가 true, changeSignalKindBodyHash를 반환한다
  • supportedCategories()dutyCtgr↔라벨 목록을 반환한다