[BE-06] company 도메인 모델 — 회사 · 소스 매핑
작업 내용 (설계 의도)
근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “방안 7 컨텍스트 경계”, “시스템 역할 경계”
변경 사항
회사와 채용 데이터 소스의 매핑을 소유합니다. 소스 탐색(discovery)은 회사 등록 시 1회, 공고 수집(collection)은 매일 자정으로 실행 주기가 명시적으로 분리되므로(FR-6) posting 컨텍스트와 라이프사이클이 다릅니다.
핵심 설계 의도:
- 관심/발견 회사 축 (
companyOrigin, FR-61) — 사용자 직접 등록은WATCHED, 애그리게이터 자동 등록은DISCOVERED입니다. 이 축은registrationType(AUTO/MANUAL_ONLY)과 직교합니다 — 전자는 “출처가 사용자냐 발견이냐”, 후자는 “자동 수집 가능한가”.Company.createWatched()/createDiscovered(name)두 팩토리로 생성하고,isWatched()가 알림 분기(개별 vs 일일 요약)의 근거입니다.promoteToWatched()로 발견 회사를 관심 회사로 승격합니다(시나리오 9.5). - 발견 회사 자동 등록 (
DiscoveredCompanyDomainService, FR-61) — 애그리게이터 공고의 회사가 미등록이면DISCOVERED로 생성합니다.resolveOrCreate(companyNames): Map<name, companyId>— 회사명 유니크로 기존 회사는 그대로 반환(멱등), 미존재만 생성합니다. 이 서비스는 애그리게이터 수집(BE-25)이 상위 오케스트레이션으로 호출하며, posting 도메인이 company를 import하지 않고 companyId만 받게 하는 경계입니다. - 애그리게이터 소스 등록 — 애그리게이터
JobSource는 회사에 종속되지 않으므로companyId=null,sourceType=AGGREGATOR,searchCategoryCode(+선택searchKeyword)를 보유합니다. 회사 종속형은companyId있음·sourceSlug있음·검색조건 null입니다.JobSource팩토리를 유형별로 분리합니다(forCompany(...)/forAggregator(...)). Company : JobSource = 1:N— 우리은행처럼 한 회사가 인크루트(신입)와 잡코리아(경력) 두 소스를 갖는 사례가 조사에서 실제로 확인됐습니다(FR-4). 애그리게이터 소스는 회사에 속하지 않습니다.- 후보 0건이면
MANUAL_ONLY로 등록하고 자동 수집 대상에서 제외합니다(FR-5).Company.isAutoCollectable()이 이 판단을 캡슐화해 호출부가registrationType == MANUAL_ONLY비교를 하지 않게 합니다.MANUAL_ONLY회사도 수동 공고 등록(FR-20)과 지원 관리(FR-21)의 완전한 대상입니다. - 시딩 상태는
JobSource가 소유합니다 —markSeeded()/isSeeded(). 최초 수집은 저장만 하고 알림을 보내지 않으며(FR-10), 이 판정을 소스 단위로 관리해 회사에 소스를 나중에 추가해도 그 소스만 시딩됩니다. - 소스 비활성화(
disable())를 제공합니다 — 파서가 깨졌을 때 회사 삭제 없이 수집만 멈추는 롤백 수단입니다(Release Scenario 단계 2 롤백). 불리언enabled가 아니라disabledAt시각을 기록하고isActive()가 NULL 여부로 판정합니다(DB 컬럼 명명 규칙 — “언제 비활성화했는가”가 함께 남습니다). - 소스는
(platform, sourceSlug, baseUrl)3요소를 보유합니다. 플랫폼별 base URL 패턴은 어댑터가 알고, 여기서는 사용자가 확정한 값을 그대로 보관합니다.
범위: 도메인 POJO(Company·CompanyOrigin·JobSource·SourceType) + DiscoveredCompanyDomainService + Repository interface + JPA 엔티티/RepositoryImpl(Testcontainers 통합 테스트 포함). 탐색·회사 등록·애그리게이터 소스 등록·승격 UseCase와 API는 BE-17에서 처리합니다. 컬럼은 20260722-공고알림앱-design-db.md가 SSOT입니다(company_origin·source_type·search_category_code 등 애그리게이터 편입 컬럼 포함).
의존
- BE-01
다이어그램
처리 흐름
sequenceDiagram participant C as 호출부(등록 서비스) participant Co as Company participant S as JobSource participant R as CompanyRepository C->>Co: create(name, candidates) alt 후보 0건 Co->>Co: registrationType = MANUAL_ONLY else 후보 1건 이상 Co->>S: create(platform, slug, baseUrl) Co->>Co: registrationType = AUTO end C->>R: save(company) C->>Co: isAutoCollectable() Co-->>C: 수집 대상 여부
클래스 의존
flowchart LR subgraph Domain["domain/company"] Company[Company] RegType[CompanyRegistrationType] Source[JobSource] CRepo[CompanyRepository] SRepo[JobSourceRepository] end subgraph Infra["infrastructure/company"] CImpl[CompanyRepositoryImpl] SImpl[JobSourceRepositoryImpl] end Company --> RegType Company --> Source CImpl -.->|implements| CRepo SImpl -.->|implements| SRepo
테스트 케이스
- 소스 후보 1건으로 회사를 등록하면
registrationType=AUTO이고isAutoCollectable()이 true를 반환한다 - 소스 후보 0건으로 회사를 등록하면
registrationType=MANUAL_ONLY이고isAutoCollectable()이 false를 반환한다 - 한 회사에 인크루트·잡코리아 두 소스를 등록하면 소스가 2건 조회된다 (1:N)
- 신규 소스는
isSeeded()가 false이고,markSeeded()후 true를 반환한다 - 같은 회사에 소스를 나중에 추가하면 기존 소스의 시딩 상태는 영향받지 않는다
disable()호출 시disabledAt이 기록되고isActive()가 false를 반환하며 활성 소스 조회 결과에서 제외된다- 활성 소스는
disabledAt이 NULL이고isActive()가 true를 반환한다 - 같은 이름의 회사를 두 번 등록하면 중복으로 거부된다
createWatched()는companyOrigin=WATCHED,createDiscovered()는DISCOVERED로 생성된다promoteToWatched()후isWatched()가 true를 반환한다resolveOrCreate가 기존 회사명은 그대로 반환하고 미존재만DISCOVERED로 생성한다(멱등)resolveOrCreate가 같은 회사명을 동시에 두 번 받아도 한 회사만 생성한다- 애그리게이터
JobSource는companyId=null·sourceType=AGGREGATOR·searchCategoryCode보유로 생성된다 - 회사 종속형
JobSource는sourceSlug보유·검색조건 null로 생성된다 - Testcontainers: 회사 저장 후 소스와 함께 조회하면 매핑이 정확히 복원된다