[BE-09] 인크루트 HTML 소스 어댑터 (EUC-KR · 상세 전건 조회)
작업 내용 (설계 의도)
근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “소스별 어댑터 설계”, “수집 실패 경로”
근거 조사: 20260722-채용소스-조사-브리프.md §3·§4 (실제 확인: recruit.incruit.com/{slug}/ 서버 렌더링 HTML, 마감일 2026.07.16 00:00 ~ 2026.07.30 17:00 형식 노출, EUC-KR 계열 인코딩, recruit.incruit.com/wooribank/job/2507310011이 비공개 공고 로그인 페이지로 동작)
변경 사항
인크루트 채용대행을 사용하는 회사(초기: 수협은행 신입, 우리은행 신입)를 커버합니다. 세 어댑터 중 난이도와 리스크가 가장 높습니다.
능력 선언(SourceCapabilities):
| 항목 | 값 | 근거 |
|---|---|---|
changeSignalKind | DETAIL_BODY_HASH | 목록에 변경 감지용 필드가 없어 상세 본문 해시 비교 (FR-12) |
providesDeadline | true | 목록에 마감일 노출 |
providesStructuredWorkArrangement | false | 구조화 필드 없음 → ②만 |
requiresDetailFetch | true | 상세 페이지 전건 조회 |
detailRequestDelayMillis | 1000 | 대상 사이트 부하 관리 (Operations) |
detailRequestLimitPerRun | 200 | 회차당 상한 |
핵심 설계 의도:
- EUC-KR 처리(NFR-4):
Content-Type의 charset을 신뢰하지 않고EUC-KR(MS949 상위호환)로 명시 디코딩합니다 —Jsoup.parse(inputStream, "EUC-KR", url). 문자 깨짐 0건이 완료 기준이며, 고정 바이트 fixture로 테스트를 강제합니다. - 상세 전건 조회 + 요청량 관리: 목록 파싱 후 각 공고 상세를 조회하되 요청 간 1,000ms 지연, 회차당 상한 200건, connect 5s / read 10s를 적용합니다. 상한 초과분은 목록 정보만 갱신하고
detailSkippedCount로 기록합니다. - 부분 실패 격리: 목록은 성공했는데 상세 일부가 실패하면 회차 전체를 실패로 만들지 않습니다.
Fetched(postings, detailFailureCount)를 반환하고, 실패 건은changeSignature를 갱신하지 않아 다음 회차에 재시도합니다. 목록에 존재했으므로 미발견 판정에는 영향이 없습니다 — 상세 조회 실패가 마감 오판정으로 번지지 않게 하는 것이 이 설계의 목적입니다. - 로그인 필요 공고 감지(시나리오 6): 상세 응답이 로그인 페이지로 리다이렉트되거나 로그인 폼 마커를 포함하면
accessRestricted=true로 표시합니다. 이 공고는 자동 매칭·마감 판정 대상에서 제외되고 목록에 “접근 제한” 배지로 표시됩니다(P0에서 알림은 발송하지 않습니다 — TDD Open Questions #1). - 마감일 파싱:
2026.07.16 00:00 ~ 2026.07.30 17:00에서 종료 시각을 추출합니다. 파싱 실패 시 예외 대신null(상시채용 취급)로 처리하고 경고 로그를 남깁니다 — 파싱 실패로 회차 전체를 실패시키면 그 소스의 모든 공고가 판정 불가가 됩니다. - 본문 해시: 광고·타임스탬프 같은 변동 요소를 제거한 정규화 본문의 SHA-256을 시그니처로 사용합니다. 정규화 규칙이 불안정하면 매 회차 “변경됨”으로 오탐되므로, 텍스트 추출 후 공백 정규화 + 스크립트/스타일 제거를 적용합니다.
- robots.txt·이용약관 확인 결과를 어댑터 KDoc에 기록합니다(NFR-9).
SPI 정합(BE-02): SourcePlatformAdapter를 구현하며 fetchList(CollectionDescriptor.CompanyBound)를 받습니다(sourceType=COMPANY_BOUND). 외부 호출은 SourceRequestExecutor(EUC-KR 디코딩·상세 지연 1000ms·상한 200건 정책)를 경유합니다.
의존
- BE-02
다이어그램
처리 흐름
sequenceDiagram participant R as JobSourceGatewayImpl participant A as IncruitJobSourceAdapter participant C as IncruitHtmlClient participant I as recruit.incruit.com R->>A: fetchList(descriptor) A->>C: fetchListPage(slug) C->>I: GET /{slug}/ I-->>C: HTML (EUC-KR) C->>C: EUC-KR 명시 디코딩 C-->>A: 공고 목록 (id, title, url, 마감일) loop 공고별 (상한 200건, 1000ms 지연) A->>C: fetchDetail(url) alt 로그인 리다이렉트 C-->>A: accessRestricted=true else 정상 C-->>A: 본문 → 정규화 → SHA-256 else 상세 실패 C-->>A: 실패 (detailFailureCount++) end end A-->>R: Fetched(postings, detailFailureCount)
클래스 의존
flowchart LR subgraph Domain["domain/posting"] Raw[RawJobPosting] Sig[ChangeSignature.BodyHash] Outcome[SourceCollectionOutcome] Cap[SourceCapabilities] end subgraph Adapter["infrastructure/posting/collector/incruit"] Ad[IncruitJobSourceAdapter] Cl[IncruitHtmlClient] ListP[IncruitListParser] DetailP[IncruitDetailParser] Hash[BodyHashCalculator] end Ad --> Cl Ad --> ListP Ad --> DetailP DetailP --> Hash Hash --> Sig ListP --> Raw Ad --> Outcome Ad --> Cap
테스트 케이스
- EUC-KR 바이트 fixture를 파싱하면 한글 제목이 깨짐 없이 정확히 일치한다 (NFR-4)
- 응답 헤더의 charset이 잘못 표기돼 있어도 EUC-KR로 강제 디코딩해 정상 파싱한다
- 목록의
2026.07.16 00:00 ~ 2026.07.30 17:00에서 종료 시각이deadlineAt으로 파싱된다 - 마감일 형식이 예상과 다르면
deadlineAt = null+ 경고 로그로 처리되고 회차는 실패하지 않는다 - 상세 본문이 동일하면 같은 해시가 생성되어 변경 없음으로 판정된다
- 본문의 공백·스크립트만 다른 경우 정규화로 같은 해시가 생성된다(오탐 방지)
- 상세 응답이 로그인 페이지면
accessRestricted=true로 표시된다 - 상세 조회 일부가 실패해도
Fetched를 반환하며detailFailureCount가 증가한다 - 상세 조회 실패 건은
changeSignature가 갱신되지 않아 다음 회차에 재시도된다 - 공고가 250건이면 상세 조회는 200건만 수행하고 나머지는 목록 정보만 갱신한다
- 상세 요청 사이에 설정된 지연이 적용된다
- 목록 페이지 조회 자체가 실패하면
Failed를 반환한다 supports(INCRUIT)가 true를 반환한다