[BE-58] 이력서 프로필 도메인 모델 · 축 가중치 · 평가 기준 버전 (FR-95·97 기반)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “방안 5 (recommendation 신규 컨텍스트)”, “방안 9 지원 추천도”, “recommendation 인터페이스 시그니처”

변경 사항

  1. 신규 recommendation 컨텍스트를 만듭니다. matching에 합류시키지 않는 이유: matching은 “알림을 보낼 공고인가”(키워드·근무형태)를 판정하고, 추천도는 “지원할 만한가”(내 역량 대비 적합도)를 판정합니다 — 입력·출력·기준 버전 축이 전부 다릅니다. FR-97이 “기존 match_criteria_revisions와는 별개 축”을 명시적으로 요구했습니다.
  2. 프로필은 document가 아니라 recommendation이 소유합니다 — 프로필은 추천도 계산의 입력 기준이며 독립적 의미가 약합니다. document에 두면 확정·기준 버전 증가가 두 컨텍스트에 걸쳐 조율돼야 합니다. sourceDocumentVersionId: Long만 참조합니다.
  3. 초안 vs 확정을 타입으로 드러냅니다ResumeProfile.isConfirmed. 확정 전 초안은 추천도 계산에 쓰이지 않습니다(FR-95). confirm()은 이미 확정이면 false를 반환해 기준 버전을 올리지 않습니다(멱등).
  4. 평가 기준 버전(recommendation_criteria_revisions)은 단일 축입니다 — 프로필 확정(PROFILE_CONFIRMED)과 축 가중치 변경(AXIS_WEIGHT) 둘 다 이 revision을 증가시킵니다. change_target으로 구분합니다.
  5. 축 가중치(recommendation_axis_weights) — REQUIRED_SKILL 45 / PREFERRED_SKILL 20 / CAREER_ROLE 20 / WORK_CONDITION 15가 기본값입니다. 합계 100 검증은 RecommendationAxisWeights 값 객체가 캡슐화합니다.
  6. ProfileSkill이 충족률을 스스로 판정합니다 — fulfillmentAgainst(요구 기술, 요구 기간)FULL/PARTIAL/NONE을 반환합니다. “일부 충족 60%“는 기술명 일치 && (최근 사용 3년 초과 || 사용 기간 < 요구 기간의 1/2)입니다(FR-96 확정 규칙). 판정 로직을 외부 함수에 두지 않습니다(no-expose-value-for-external-logic).
  7. 시간 타입은 ZonedDateTime, 연월은 YearMonth를 씁니다.

이 티켓은 계산·API를 만들지 않습니다 — 모델·저장·기준 버전만 만듭니다. 계산은 BE-63, API는 BE-62·65입니다.

의존

  • BE-56 (예외·플래그 시드)

다이어그램

처리 흐름

sequenceDiagram
    participant U as ConfirmResumeProfileUseCase
    participant D as ResumeProfileDomainService
    participant P as ResumeProfile
    participant R as ResumeProfileRepository
    participant C as RecommendationCriteriaRepository
    U->>D: confirm(profileId)
    D->>R: findBy(profileId)
    D->>P: confirm()
    alt 이미 확정
        P-->>D: false → 기준 버전 미증가 (멱등)
    else 신규 확정
        P-->>D: true
        D->>R: save(profile) — 항목 cascade
        D->>C: bumpRevision(PROFILE_CONFIRMED, reason)
        C-->>D: 새 criteriaRevision
    end
    D-->>U: (profile, criteriaRevision)

클래스 의존

flowchart LR
    subgraph Domain["domain/recommendation"]
        PDS[ResumeProfileDomainService]
        Profile[ResumeProfile]
        Skill[ProfileSkill]
        Exp[ProfileExperience]
        Pref[ProfileWorkPreference]
        Axis[RecommendationAxis]
        Weights[RecommendationAxisWeights]
        PRepo[ResumeProfileRepository]
        WRepo[RecommendationAxisWeightRepository]
        CRepo[RecommendationCriteriaRepository]
    end
    PDS --> Profile
    PDS --> PRepo
    PDS --> CRepo
    Profile --> Skill
    Profile --> Exp
    Profile --> Pref
    Weights --> Axis
    Weights --> WRepo

테스트 케이스

  • 초안 프로필을 만들면 isConfirmed=false
  • 프로필을 확정하면 isConfirmed=true가 되고 새 기준 버전이 생성된다
  • 이미 확정된 프로필을 다시 확정하면 기준 버전이 증가하지 않는다 (멱등)
  • 확정된 프로필의 항목을 수정하려 하면 예외가 발생한다
  • 항목(기술·경력·선호)이 cascade로 한 단위 저장된다
  • 축 가중치 4종 합계가 100이면 저장된다
  • 합계가 99면 InvalidRecommendationWeightException이다 (경계값)
  • 축이 3종만 전달되면 예외다 (4축 필수)
  • 가중치를 변경하면 기준 버전이 AXIS_WEIGHT로 증가한다
  • 기본 가중치가 45/20/20/15로 시드된다
  • 기술명이 프로필에 없으면 충족률이 NONE이다
  • 기술명이 일치하고 최근 사용 4년 전이면 PARTIAL이다
  • 기술명이 일치하고 최근 사용 정확히 3년 전이면 FULL이다 (경계값)
  • 기술명이 일치하고 사용 기간이 요구의 40%면 PARTIAL이다
  • 기술명이 일치하고 사용 기간이 요구의 정확히 50%면 FULL이다 (경계값)
  • 기술명 정규화(대소문자·공백·하이픈)가 적용되어 Spring Bootspring-boot가 같은 기술로 판정된다