[BE-17] 회사 등록 · 소스 탐색(discovery) API

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “API 계약”, “시스템 역할 경계” 근거 조사: 20260722-채용소스-조사-브리프.md — “어댑터 개발 표준 절차”(① 공개 API → ② 내부 API → ③ HTML → ⑤ 수동 등록)

변경 사항

회사명을 입력받아 소스 후보를 탐색하고, 사용자가 확정한 후보로 회사-소스 매핑을 저장합니다(FR-1, 2, 4, 5).

핵심 설계 의도:

  • 탐색과 수집의 실행 주기를 분리합니다(FR-6). 탐색은 회사 등록 시 1회 동기 호출이고, 수집은 매일 자정 배치입니다. 탐색이 느리거나 실패해도 수집 배치에 영향이 없습니다.
  • 후보에는 샘플 공고 3건 미리보기를 함께 제시합니다(FR-2). 사용자가 “이 slug가 맞는 회사인지”를 제목으로 확인할 수 있어야 합니다. 미리보기가 없으면 잘못된 slug를 확정해 엉뚱한 회사 공고를 수집하는 사고가 납니다.
  • 탐색 경로는 두 가지입니다 — slug 프로빙(회사명에서 slug 후보 도출 후 각 플랫폼 어댑터의 probe 호출)과 URL 직접 입력(사용자가 붙여넣은 채용 페이지 URL에서 플랫폼·slug 역추출). 웹 검색 보조 탐색은 P1(FR-3)이라 이 티켓 범위 밖입니다.
  • 후보 0건이면 MANUAL_ONLY로 등록하고 자동 수집 대상에서 제외합니다(FR-5, 시나리오 5). 이 회사도 수동 공고 등록(BE-18)과 지원 관리(BE-14)의 완전한 대상입니다 — 등록 자체를 거부하지 않는 것이 핵심입니다.
  • 한 회사에 복수 소스를 확정할 수 있습니다(FR-4). 우리은행처럼 인크루트(신입)와 잡코리아(경력)를 함께 갖는 사례가 조사에서 확인됐습니다.
  • 탐색은 여러 플랫폼 어댑터를 순회하므로 일부 어댑터 실패가 전체 탐색을 실패시키지 않습니다 — 실패한 플랫폼은 후보에서 빠지고 나머지 결과를 반환합니다.
  • 회사 목록 응답 필드 확정(FE 요청 #5 — 문서 간 불일치 해소): GET /api/companies{id, name, registrationType, sourceCount, brokenSourceCount}를 반환합니다. TDD 계약 표에 고장 필드가 누락돼 있었고 이 티켓 테스트 케이스에는 있었는데, 티켓 쪽이 맞습니다 — Operations가 “소스 가드가 활성화된 동안 해당 소스 상태를 표기”할 것을 요구하고, 회사 목록이 소스 고장을 알아채는 첫 화면이기 때문입니다. brokenSourceCount > 0이면 FE가 경고 배지를 띄웁니다. 불리언 대신 개수로 내리는 이유는 복수 소스 회사(우리은행)에서 “2개 중 1개 고장”을 표현할 수 있기 때문입니다.

애그리게이터 편입분

  • 애그리게이터 소스 등록 API (POST /api/aggregator-sources, FR-60){platform(SARAMIN|JUMPIT|WANTED|REMEMBER|JOBKOREA|SURFIT), searchCategoryCode, searchKeyword?}를 받아 회사에 종속되지 않는 애그리게이터 소스를 등록합니다. 회사 종속형 등록과 별도 컨트롤러(AggregatorSourceApiController)로 분리해 Single Writer를 지킵니다. searchKeyword 미지정 시 빈 문자열 ''로 저장합니다 — NULL이면 unique(platform, search_category_code, search_keyword)가 중복을 못 막습니다(BE-01 규칙 1-b). 등록 전 searchCategoryCode가 어댑터 지원 목록(categoriesOf)에 있는지 검증합니다.
  • 애그리게이터 소스 목록·삭제 API (FE 요청 #10)GET /api/aggregator-sources(활성·비활성 함께, disabled 플래그) / DELETE /api/aggregator-sources/{id}(소프트 삭제disabled_at 기록, 수집 이력·기존 공고 보존). 회사 종속형 소스의 disable()과 대칭입니다.
  • 카테고리 조회 API (FE 요청 #12)GET /api/aggregator-sources/categories?platform=JobSourceGateway.categoriesOf(platform)를 노출합니다. FE Select가 이 코드↔라벨을 받아 잘못된 코드 등록을 차단합니다.
  • 회사 목록 필터·페이지네이션 (FR-61, NFR-1)GET /api/companies?companyOrigin=WATCHED|DISCOVERED&page=&size=. 발견 회사가 수백 곳까지 늘 수 있어(NFR-1) 페이지네이션이 필수입니다. 응답 아이템에 companyOrigin을 포함합니다.
  • 발견 회사 승격 API (POST /api/companies/{id}/promotion, 시나리오 9.5)DISCOVEREDWATCHED. 승격 후 그 회사 공고는 개별 알림 대상이 됩니다.

범위: CompanyApiController, AggregatorSourceApiController, DiscoverJobSourcesUseCase, RegisterCompanyUseCase, RegisterAggregatorSourceUseCase, ListAggregatorSourcesUseCase, DisableAggregatorSourceUseCase, ListAggregatorCategoriesUseCase, PromoteCompanyUseCase, ListCompaniesUseCase, CompanyRegistrationDomainService, JobSourceDiscoveryDomainService.

롤백: 애그리게이터 소스 삭제는 소프트 삭제(disabled_at)이므로 disabled_at을 NULL로 되돌리면 복구됩니다.

의존

  • BE-06 (회사·소스 도메인), BE-02 (탐색 Gateway 계약)

다이어그램

처리 흐름

sequenceDiagram
    participant U as 사용자
    participant C as CompanyApiController
    participant UC as DiscoverJobSourcesUseCase
    participant D as JobSourceDiscoveryDomainService
    participant G as JobSourceGateway
    participant RC as RegisterCompanyUseCase
    U->>C: POST /api/companies/source-discoveries
    C->>UC: execute(companyName, siteUrl?)
    UC->>D: discover(companyName, siteUrl)
    D->>G: probe(각 플랫폼)
    G-->>D: 후보 + 샘플 3건 (실패 플랫폼은 제외)
    D-->>C: 후보 목록
    U->>C: POST /api/companies (확정 후보)
    C->>RC: execute(name, sources)
    alt 후보 0건
        RC->>RC: MANUAL_ONLY 로 등록
    else 후보 1건 이상
        RC->>RC: AUTO 로 등록 + 소스 저장
    end

클래스 의존

flowchart LR
    subgraph Presentation["presentation/company"]
        Api[CompanyApiController]
    end
    subgraph Application["application/company"]
        UC1[DiscoverJobSourcesUseCase]
        UC2[RegisterCompanyUseCase]
        UC3[ListCompaniesUseCase]
    end
    subgraph Domain["domain/company"]
        DS1[JobSourceDiscoveryDomainService]
        DS2[CompanyRegistrationDomainService]
        Company[Company]
        Source[JobSource]
    end
    subgraph DomainPosting["domain/posting"]
        GW[JobSourceGateway]
    end
    Api --> UC1
    Api --> UC2
    Api --> UC3
    UC1 --> DS1
    UC2 --> DS2
    UC3 --> DS2
    DS1 --> GW
    DS2 --> Company
    DS2 --> Source

테스트 케이스

  • 회사명으로 탐색하면 유효한 slug 후보가 샘플 공고 3건과 함께 반환된다
  • 사용자가 채용 페이지 URL을 직접 입력하면 플랫폼과 slug가 역추출되어 후보가 된다
  • 탐색 결과가 0건이면 빈 후보 목록과 “수동 등록 전용” 안내 코드가 반환된다
  • 후보 0건 상태로 회사를 등록하면 MANUAL_ONLY로 저장되고 자동 수집 대상에서 제외된다
  • MANUAL_ONLY 회사에도 수동 공고 등록이 가능하다(등록 자체가 거부되지 않는다)
  • 한 회사에 두 개 소스를 확정하면 소스가 2건 저장된다
  • 한 플랫폼 어댑터가 예외를 던져도 나머지 플랫폼 후보는 정상 반환된다
  • 이미 등록된 회사명으로 재등록하면 409를 반환한다
  • 회사 목록 조회 시 sourceCountbrokenSourceCount가 함께 반환된다
  • 소스 2개 중 1개가 고장이면 brokenSourceCount=1로 반환된다
  • 고장 소스가 없으면 brokenSourceCount=0이다
  • 애그리게이터 소스를 등록하면 companyId=null·sourceType=AGGREGATOR로 저장되고 수집 대상에 포함된다
  • searchKeyword 없이 등록하면 search_keyword=''(빈 문자열)로 저장된다
  • 같은 (platform, searchCategoryCode)를 키워드 없이 두 번 등록하면 유니크 제약으로 거부된다
  • 어댑터가 지원하지 않는 searchCategoryCode로 등록하면 400으로 거부된다
  • GET /api/aggregator-sources가 활성·비활성 소스를 disabled 플래그와 함께 반환한다
  • DELETE /api/aggregator-sources/{id}가 소프트 삭제해 disabled_at을 기록하고 기존 공고·수집 이력은 보존된다
  • GET /api/aggregator-sources/categories?platform=SARAMIN이 코드↔라벨 목록을 반환한다
  • 회사 목록을 companyOrigin=DISCOVERED로 필터하면 발견 회사만 반환된다
  • 회사 목록이 페이지네이션되어 totalCountpage가 함께 반환된다
  • 발견 회사를 승격하면 companyOriginWATCHED로 바뀐다
  • 이미 관심 회사인 회사를 승격하면 멱등하게 처리된다
  • 잘못된 형식의 URL을 입력하면 400을 반환한다
  • 등록된 소스는 다음 자정 수집 대상 조회에 포함된다