[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):
| 항목 | 값 | 근거 |
|---|---|---|
sourceType | AGGREGATOR | speciality 필터(포맷 실호출 확정) |
changeSignalKind | FIELD_HASH | 변경 감지 필드 없음 |
providesDeadline | true (조건부) | 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(제목·마감·회사). id→sourceJobId, 회사명은 목록에서 확보 →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를 파싱하면
id가sourceJobId, 회사명이sourceCompanyName으로 매핑된다 close=“상시채용”이면deadlineAt=null(상시채용)로 정규화된다is_closed=true이면 마감 신호로 처리된다ads_end_at이 있고 상시채용이 아니면 마감일로 파싱된다Origin/Referer/UA 헤더가 항상 부착된다- 주요 필드가 동일하면 같은
FieldHash로 변경 없음으로 판정된다 - 요청 사이에 지연이 적용되고 페이지 상한을 넘지 않는다
- API가 5xx/헤더 누락 403을 반환하면
Failed를 반환한다 supports(SURFIT)가 true를 반환한다- 첫 단계 실호출로
speciality포맷과 마감 필드 우선순위를 확정한다(구현 절차) supportedCategories()가 실호출로 확인된 코드↔라벨 목록을 반환한다