[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) —DISCOVERED→WATCHED. 승격 후 그 회사 공고는 개별 알림 대상이 됩니다.
범위: 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를 반환한다
- 회사 목록 조회 시
sourceCount와brokenSourceCount가 함께 반환된다 - 소스 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로 필터하면 발견 회사만 반환된다 - 회사 목록이 페이지네이션되어
totalCount와page가 함께 반환된다 - 발견 회사를 승격하면
companyOrigin이WATCHED로 바뀐다 - 이미 관심 회사인 회사를 승격하면 멱등하게 처리된다
- 잘못된 형식의 URL을 입력하면 400을 반환한다
- 등록된 소스는 다음 자정 수집 대상 조회에 포함된다