# 단가 정책 (Pricing Policy) > **작성일**: 2025-12-08 > **상태**: 설계 확정 > **관련 요청**: `docs/front/[API-2025-12-08] pricing-api-enhancement-request.md` --- ## 1. 개요 ### 1.1 목적 - 품목(제품/자재)의 **표준단가** 관리 - **실제 입고단가** 기반 원가 계산 - 단가 변경 **이력(리비전)** 추적 - 고객그룹별 **차등 판매가** 지원 ### 1.2 핵심 원칙 | 원칙 | 설명 | |------|------| | **실제원가 우선** | 수입검사 입고단가 > 표준원가 | | **시점 기반 유효성** | effective_from ~ effective_to 기간 내 유효 | | **리비전 추적** | 모든 변경 사항 이력 보관 | | **확정 후 불변** | finalized 상태는 수정 불가 | --- ## 2. 테이블 구조 ### 2.1 ERD 개요 ``` ┌─────────────────┐ ┌─────────────────────┐ │ prices │──────<│ price_revisions │ │ (단가 마스터) │ │ (변경 이력) │ └────────┬────────┘ └─────────────────────┘ │ │ item_type_code + item_id │ ┌────┴────┐ │ │ ┌───▼───┐ ┌───▼─────┐ │products│ │materials│ └───────┘ └────┬────┘ │ │ material_id ▼ ┌─────────────────┐ │material_receipts│ │ (실제 입고단가) │ └─────────────────┘ ``` ### 2.2 prices 테이블 (단가 마스터) ```sql CREATE TABLE prices ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', -- 품목 연결 -- item_type_code VARCHAR(20) NOT NULL COMMENT '품목유형 (PRODUCT/MATERIAL)', item_id BIGINT UNSIGNED NOT NULL COMMENT '품목 ID', client_group_id BIGINT UNSIGNED NULL COMMENT '고객그룹 ID (NULL=기본가)', -- 원가 정보 -- purchase_price DECIMAL(15,4) NULL COMMENT '매입단가 (표준원가)', processing_cost DECIMAL(15,4) NULL COMMENT '가공비', loss_rate DECIMAL(5,2) NULL COMMENT 'LOSS율 (%)', -- 판매가 정보 -- margin_rate DECIMAL(5,2) NULL COMMENT '마진율 (%)', sales_price DECIMAL(15,4) NULL COMMENT '판매단가', rounding_rule ENUM('round','ceil','floor') DEFAULT 'round' COMMENT '반올림 규칙', rounding_unit INT DEFAULT 1 COMMENT '반올림 단위 (1,10,100,1000)', -- 메타 정보 -- supplier VARCHAR(255) NULL COMMENT '공급업체', effective_from DATE NOT NULL COMMENT '적용 시작일', effective_to DATE NULL COMMENT '적용 종료일', note TEXT NULL COMMENT '비고', -- 상태 관리 -- status ENUM('draft','active','inactive','finalized') DEFAULT 'draft' COMMENT '상태', is_final BOOLEAN DEFAULT FALSE COMMENT '최종 확정 여부', finalized_at DATETIME NULL COMMENT '확정 일시', finalized_by BIGINT UNSIGNED NULL COMMENT '확정자 ID', -- 감사 컬럼 -- created_by BIGINT UNSIGNED NULL COMMENT '생성자 ID', updated_by BIGINT UNSIGNED NULL COMMENT '수정자 ID', deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자 ID', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL COMMENT 'Soft Delete', -- 인덱스 -- INDEX idx_prices_tenant (tenant_id), INDEX idx_prices_item (tenant_id, item_type_code, item_id), INDEX idx_prices_effective (tenant_id, effective_from, effective_to), INDEX idx_prices_status (tenant_id, status), UNIQUE idx_prices_unique (tenant_id, item_type_code, item_id, client_group_id, effective_from, deleted_at) ) COMMENT='단가 마스터'; ``` ### 2.3 price_revisions 테이블 (변경 이력) ```sql CREATE TABLE price_revisions ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', price_id BIGINT UNSIGNED NOT NULL COMMENT '단가 ID', -- 리비전 정보 -- revision_number INT NOT NULL COMMENT '리비전 번호', changed_at DATETIME NOT NULL COMMENT '변경 일시', changed_by BIGINT UNSIGNED NOT NULL COMMENT '변경자 ID', change_reason VARCHAR(500) NULL COMMENT '변경 사유', -- 변경 스냅샷 (JSON) -- before_snapshot JSON NULL COMMENT '변경 전 데이터', after_snapshot JSON NOT NULL COMMENT '변경 후 데이터', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 인덱스 -- INDEX idx_revisions_price (price_id), INDEX idx_revisions_tenant (tenant_id), UNIQUE idx_revisions_unique (price_id, revision_number), FOREIGN KEY (price_id) REFERENCES prices(id) ON DELETE CASCADE ) COMMENT='단가 변경 이력'; ``` ### 2.4 material_receipts 테이블 (기존 - 실제 입고단가) ```sql -- 기존 테이블, purchase_price_excl_vat 필드 활용 -- 수입검사 시 실제 입고 단가 입력 ``` --- ## 3. 원가 계산 정책 ### 3.1 원가 조회 우선순위 ``` ┌─────────────────────────────────────────────────────────┐ │ 원가 조회 로직 │ ├─────────────────────────────────────────────────────────┤ │ 1순위: 수입검사 입고단가 │ │ material_receipts.purchase_price_excl_vat │ │ (가장 최근 입고 기준) │ │ │ │ 2순위: 표준원가 │ │ prices.purchase_price │ │ (해당 일자에 유효한 단가) │ │ │ │ 3순위: NULL (단가 미등록) │ │ → 경고 메시지 반환 │ └─────────────────────────────────────────────────────────┘ ``` ### 3.2 원가 계산 공식 ``` 총원가 = (매입단가 + 가공비) × (1 + LOSS율/100) 예시: - 매입단가: 10,000원 - 가공비: 2,000원 - LOSS율: 5% - 총원가 = (10,000 + 2,000) × 1.05 = 12,600원 ``` ### 3.3 품목 유형별 원가 적용 | 품목 유형 | 원가 소스 | 설명 | |----------|----------|------| | **MATERIAL** (자재) | material_receipts 우선 | 실제 입고단가 사용 | | **PRODUCT** (제품) | prices 테이블 | 표준원가 사용 | --- ## 4. 판매가 계산 정책 ### 4.1 판매가 계산 공식 ``` 판매단가 = 반올림(총원가 × (1 + 마진율/100), 반올림단위, 반올림규칙) 예시: - 총원가: 12,600원 - 마진율: 25% - 반올림단위: 100 - 반올림규칙: round - 판매단가 = round(12,600 × 1.25, 100) = round(15,750, 100) = 15,800원 ``` ### 4.2 반올림 규칙 | 규칙 | 설명 | 예시 (단위: 100) | |------|------|-----------------| | `round` | 반올림 | 15,750 → 15,800 | | `ceil` | 올림 | 15,701 → 15,800 | | `floor` | 버림 | 15,799 → 15,700 | ### 4.3 고객그룹별 차등가 ``` ┌─────────────────────────────────────────────────────────┐ │ 판매가 조회 우선순위 │ ├─────────────────────────────────────────────────────────┤ │ 1순위: 고객그룹별 특별가 │ │ prices WHERE client_group_id = [고객의 그룹ID] │ │ │ │ 2순위: 기본 판매가 │ │ prices WHERE client_group_id IS NULL │ └─────────────────────────────────────────────────────────┘ ``` --- ## 5. 상태 관리 ### 5.1 단가 상태 (status) ``` ┌──────────┐ 등록 ┌──────────┐ 활성화 ┌──────────┐ │ draft │ ──────────> │ active │ ─────────── │ finalized│ │ (초안) │ │ (활성) │ 확정 │ (확정) │ └──────────┘ └────┬─────┘ └──────────┘ │ │ 비활성화 ▼ ┌──────────┐ │ inactive │ │ (비활성) │ └──────────┘ ``` ### 5.2 상태별 권한 | 상태 | 조회 | 수정 | 삭제 | 확정 | |------|------|------|------|------| | `draft` | ✅ | ✅ | ✅ | ✅ | | `active` | ✅ | ✅ | ✅ | ✅ | | `inactive` | ✅ | ✅ | ✅ | ❌ | | `finalized` | ✅ | ❌ | ❌ | ❌ | ### 5.3 확정 (Finalize) 규칙 - **확정 조건**: status가 `draft` 또는 `active`일 때만 가능 - **확정 효과**: - `is_final = true` - `status = 'finalized'` - `finalized_at = NOW()` - `finalized_by = 현재 사용자` - **확정 후**: 수정/삭제 불가, 조회만 가능 --- ## 6. 리비전 관리 ### 6.1 리비전 생성 시점 | 이벤트 | 리비전 생성 | 설명 | |--------|-----------|------| | 최초 등록 | ✅ | revision_number = 1, before_snapshot = NULL | | 수정 | ✅ | revision_number++, 변경 전/후 기록 | | 삭제 | ✅ | soft delete, 삭제 전 상태 기록 | | 확정 | ✅ | 확정 시점 스냅샷 기록 | ### 6.2 스냅샷 JSON 구조 ```json { "purchase_price": 10000, "processing_cost": 2000, "loss_rate": 5.0, "margin_rate": 25.0, "sales_price": 15800, "effective_from": "2025-01-01", "effective_to": null, "status": "active", "supplier": "ABC공급" } ``` --- ## 7. API 엔드포인트 ### 7.1 엔드포인트 목록 | Method | Endpoint | 설명 | 우선순위 | |--------|----------|------|---------| | GET | `/api/v1/pricing` | 단가 목록 조회 | 🔴 필수 | | GET | `/api/v1/pricing/{id}` | 단가 상세 조회 | 🔴 필수 | | POST | `/api/v1/pricing` | 단가 등록 | 🔴 필수 | | PUT | `/api/v1/pricing/{id}` | 단가 수정 | 🔴 필수 | | DELETE | `/api/v1/pricing/{id}` | 단가 삭제 | 🔴 필수 | | GET | `/api/v1/pricing/by-items` | 품목별 단가 현황 | 🔴 필수 | | GET | `/api/v1/pricing/{id}/revisions` | 변경 이력 조회 | 🟡 중요 | | POST | `/api/v1/pricing/{id}/finalize` | 단가 확정 | 🟢 권장 | | GET | `/api/v1/pricing/cost` | 원가 조회 (계산) | 🟡 중요 | ### 7.2 원가 조회 API ``` GET /api/v1/pricing/cost?item_type=MATERIAL&item_id=123&date=2025-01-15 ``` **Response:** ```json { "success": true, "data": { "item_type": "MATERIAL", "item_id": 123, "cost_source": "receipt", // "receipt" | "standard" | "not_found" "purchase_price": 10500, "receipt_id": 456, // cost_source가 receipt일 때 "receipt_date": "2025-01-10", "price_id": null // cost_source가 standard일 때 } } ``` --- ## 8. 비즈니스 규칙 ### 8.1 검증 규칙 | 규칙 | 설명 | |------|------| | **R1** | effective_from은 필수 | | **R2** | effective_from ≤ effective_to (종료일이 있을 경우) | | **R3** | 동일 품목+고객그룹+적용시작일 조합은 중복 불가 | | **R4** | 확정된 단가는 수정/삭제 불가 | | **R5** | 마진율은 0~100% 범위 | | **R6** | LOSS율은 0~100% 범위 | | **R7** | 반올림단위는 1, 10, 100, 1000 중 하나 | ### 8.2 기간 중복 처리 ``` 기존: 2025-01-01 ~ NULL (무기한) 신규: 2025-06-01 ~ NULL → 기존 단가의 effective_to를 2025-05-31로 자동 설정 → 신규 단가 등록 ``` ### 8.3 삭제 정책 - **Soft Delete** 적용 - 삭제 시 `deleted_at`, `deleted_by` 기록 - 삭제된 단가도 리비전 이력에서 조회 가능 --- ## 9. 기존 시스템 호환 ### 9.1 price_histories 테이블 처리 | 상태 | 설명 | |------|------| | **현재** | 기존 price_histories 데이터 유지 | | **전환 기간** | 양쪽 테이블 병행 운영 | | **전환 완료 후** | price_histories deprecated | ### 9.2 데이터 마이그레이션 ```sql -- 기존 데이터를 prices 테이블로 마이그레이션 INSERT INTO prices ( tenant_id, item_type_code, item_id, client_group_id, purchase_price, effective_from, effective_to, status, created_by, created_at ) SELECT tenant_id, item_type_code, item_id, client_group_id, price as purchase_price, started_at, ended_at, 'active', created_by, created_at FROM price_histories WHERE deleted_at IS NULL; ``` --- ## 10. 관련 문서 - [API 개발 규칙](../standards/api-rules.md) - [데이터베이스 스키마](../specs/database-schema.md) - [프론트엔드 요청서](../front/[API-2025-12-08]%20pricing-api-enhancement-request.md) --- **최종 업데이트**: 2025-12-08