# 기간별 단가 설정 시스템 구현 가이드 ## 개요 **선택항목 상세입력(selected-items-detail-input)** 컴포넌트를 활용하여 기간별 단가를 설정하는 범용 시스템입니다. ## 데이터베이스 설계 ### 1. 마이그레이션 실행 ```bash # 마이그레이션 파일 위치 db/migrations/999_add_period_price_columns_to_customer_item_mapping.sql # 실행 (로컬) npm run migrate:local # 또는 수동 실행 psql -U your_user -d erp_db -f db/migrations/999_add_period_price_columns_to_customer_item_mapping.sql ``` ### 2. 추가된 컬럼들 | 컬럼명 | 타입 | 설명 | 사진 항목 | |--------|------|------|-----------| | `start_date` | DATE | 기간 시작일 | ✅ 시작일 DatePicker | | `end_date` | DATE | 기간 종료일 | ✅ 종료일 DatePicker | | `discount_type` | VARCHAR(50) | 할인 방식 | ✅ 할인율/할인금액 Select | | `discount_value` | NUMERIC(15,2) | 할인율 또는 할인금액 | ✅ 숫자 입력 | | `rounding_type` | VARCHAR(50) | 반올림 방식 | ✅ 반올림/절삭/올림 Select | | `rounding_unit_value` | VARCHAR(50) | 반올림 단위 | ✅ 1원/10원/100원/1,000원 Select | | `calculated_price` | NUMERIC(15,2) | 계산된 최종 단가 | ✅ 계산 결과 표시 | | `is_base_price` | BOOLEAN | 기준단가 여부 | ✅ 기준단가 Checkbox | ## 화면 편집기 설정 방법 ### Step 1: 선택항목 상세입력 컴포넌트 추가 1. 화면 편집기에서 "선택항목 상세입력" 컴포넌트를 캔버스에 드래그앤드롭 2. 컴포넌트 ID: `customer-item-price-periods` ### Step 2: 데이터 소스 설정 - **원본 데이터 테이블**: `item_info` (품목 정보) - **저장 대상 테이블**: `customer_item_mapping` - **데이터 소스 ID**: URL 파라미터에서 자동 설정 (Button 컴포넌트가 전달) ### Step 3: 표시할 원본 데이터 컬럼 설정 이전 화면(품목 선택 모달)에서 전달받은 품목 정보를 표시: ``` 컬럼1: item_code (품목코드) 컬럼2: item_name (품목명) 컬럼3: spec (규격) ``` ### Step 4: 필드 그룹 2개 생성 #### 그룹 1: 거래처 품목/품명 관리 (group_customer) | 필드명 | 라벨 | 타입 | 설명 | |--------|------|------|------| | `customer_item_code` | 거래처 품번 | text | 거래처에서 사용하는 품번 | | `customer_item_name` | 거래처 품명 | text | 거래처에서 사용하는 품명 | #### 그룹 2: 기간별 단가 설정 (group_period_price) | 필드명 | 라벨 | 타입 | 자동 채우기 | 설명 | |--------|------|------|-------------|------| | `start_date` | 시작일 | date | - | 단가 적용 시작일 | | `end_date` | 종료일 | date | - | 단가 적용 종료일 (NULL이면 무기한) | | `current_unit_price` | 단가 | number | `item_info.standard_price` | 기본 단가 (품목에서 자동 채우기) | | `currency_code` | 통화 | code/category | - | 통화 코드 (KRW, USD 등) | | `discount_type` | 할인 방식 | code/category | - | 할인율없음/할인율(%)/할인금액 | | `discount_value` | 할인값 | number | - | 할인율(5) 또는 할인금액 | | `rounding_type` | 반올림 방식 | code/category | - | 반올림없음/반올림/절삭/올림 | | `rounding_unit_value` | 반올림 단위 | code/category | - | 1원/10원/100원/1,000원 | | `calculated_price` | 최종 단가 | number | - | 계산된 최종 단가 (읽기 전용) | | `is_base_price` | 기준단가 | checkbox | - | 기준단가 여부 | ### Step 5: 그룹별 표시 항목 설정 (DisplayItems) **그룹 2 (기간별 단가 설정)의 표시 설정:** ``` 1. [필드] start_date | 라벨: "" | 형식: date | 빈 값: 기본값 (미설정) 2. [텍스트] " ~ " 3. [필드] end_date | 라벨: "" | 형식: date | 빈 값: 기본값 (무기한) 4. [텍스트] " | " 5. [필드] calculated_price | 라벨: "" | 형식: currency | 빈 값: 기본값 (계산 중) 6. [텍스트] " " 7. [필드] currency_code | 라벨: "" | 형식: text | 빈 값: 기본값 (KRW) 8. [조건] is_base_price가 true이면 → [배지] "기준단가" (variant: default) ``` **렌더링 예시:** ``` 2024-01-01 ~ 2024-06-30 | 50,000 KRW [기준단가] 2024-07-01 ~ 무기한 | 55,000 KRW ``` ## 데이터 흐름 ### 1. 품목 선택 모달 (이전 화면) ```tsx // TableList 컴포넌트에서 품목 선택 ``` ### 2. 기간별 단가 설정 화면 ```tsx // 선택항목 상세입력 컴포넌트가 자동으로 처리 // 1. URL 파라미터에서 dataSourceId 읽기 // 2. modalDataStore에서 item_info 데이터 가져오기 // 3. 사용자가 그룹별로 여러 개의 기간별 단가 입력 // 4. 저장 버튼 클릭 시 customer_item_mapping 테이블에 저장 ``` ### 3. 저장 데이터 구조 **하나의 품목(item_id = "ITEM001")에 대해 3개의 기간별 단가를 입력한 경우:** ```sql -- customer_item_mapping 테이블에 3개의 행으로 저장 INSERT INTO customer_item_mapping ( customer_id, item_id, customer_item_code, customer_item_name, start_date, end_date, current_unit_price, currency_code, discount_type, discount_value, rounding_type, rounding_unit_value, calculated_price, is_base_price ) VALUES -- 첫 번째 기간 (기준단가) ('CUST001', 'ITEM001', 'CUST-A-001', '실리콘 고무 시트', '2024-01-01', '2024-06-30', 50000, 'KRW', '할인율없음', 0, '반올림', '100원', 50000, true), -- 두 번째 기간 ('CUST001', 'ITEM001', 'CUST-A-001', '실리콘 고무 시트', '2024-07-01', '2024-12-31', 50000, 'KRW', '할인율(%)', 5, '절삭', '1원', 47500, false), -- 세 번째 기간 (무기한) ('CUST001', 'ITEM001', 'CUST-A-001', '실리콘 고무 시트', '2025-01-01', NULL, 50000, 'KRW', '할인금액', 3000, '올림', '1000원', 47000, false); ``` ## 계산 로직 (선택사항) 단가 계산을 자동화하려면 프론트엔드에서 `calculated_price`를 자동 계산: ```typescript const calculatePrice = ( basePrice: number, discountType: string, discountValue: number, roundingType: string, roundingUnit: string ): number => { let price = basePrice; // 1단계: 할인 적용 if (discountType === "할인율(%)") { price = price * (1 - discountValue / 100); } else if (discountType === "할인금액") { price = price - discountValue; } // 2단계: 반올림 적용 const unitMap: Record = { "1원": 1, "10원": 10, "100원": 100, "1,000원": 1000, }; const unit = unitMap[roundingUnit] || 1; if (roundingType === "반올림") { price = Math.round(price / unit) * unit; } else if (roundingType === "절삭") { price = Math.floor(price / unit) * unit; } else if (roundingType === "올림") { price = Math.ceil(price / unit) * unit; } return price; }; // 필드 변경 시 자동 계산 useEffect(() => { const calculatedPrice = calculatePrice( basePrice, discountType, discountValue, roundingType, roundingUnit ); // calculated_price 필드 업데이트 handleFieldChange(itemId, groupId, entryId, "calculated_price", calculatedPrice); }, [basePrice, discountType, discountValue, roundingType, roundingUnit]); ``` ## 백엔드 API 구현 (필요시) ### 기간별 단가 조회 ```typescript // GET /api/customer-item/price-periods?customer_id=CUST001&item_id=ITEM001 router.get("/price-periods", async (req, res) => { const { customer_id, item_id } = req.query; const companyCode = req.user!.companyCode; const query = ` SELECT * FROM customer_item_mapping WHERE customer_id = $1 AND item_id = $2 AND company_code = $3 ORDER BY start_date ASC `; const result = await pool.query(query, [customer_id, item_id, companyCode]); return res.json({ success: true, data: result.rows }); }); ``` ### 기간별 단가 저장 ```typescript // POST /api/customer-item/price-periods router.post("/price-periods", async (req, res) => { const { items } = req.body; // 선택항목 상세입력 컴포넌트에서 전달 const companyCode = req.user!.companyCode; const client = await pool.connect(); try { await client.query("BEGIN"); for (const item of items) { // item.fieldGroups.group_period_price 배열의 각 항목을 INSERT const periodPrices = item.fieldGroups.group_period_price || []; for (const periodPrice of periodPrices) { const query = ` INSERT INTO customer_item_mapping ( company_code, customer_id, item_id, customer_item_code, customer_item_name, start_date, end_date, current_unit_price, currency_code, discount_type, discount_value, rounding_type, rounding_unit_value, calculated_price, is_base_price ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) `; await client.query(query, [ companyCode, item.originalData.customer_id, item.originalData.item_id, periodPrice.customer_item_code, periodPrice.customer_item_name, periodPrice.start_date, periodPrice.end_date || null, periodPrice.current_unit_price, periodPrice.currency_code, periodPrice.discount_type, periodPrice.discount_value, periodPrice.rounding_type, periodPrice.rounding_unit_value, periodPrice.calculated_price, periodPrice.is_base_price ]); } } await client.query("COMMIT"); return res.json({ success: true, message: "기간별 단가가 저장되었습니다." }); } catch (error) { await client.query("ROLLBACK"); console.error("기간별 단가 저장 실패:", error); return res.status(500).json({ success: false, error: "저장 실패" }); } finally { client.release(); } }); ``` ## 사용 시나리오 예시 ### 시나리오 1: 거래처별 단가 관리 1. 거래처 선택 모달 → 거래처 선택 → 다음 2. 품목 선택 모달 → 품목 여러 개 선택 → 다음 3. **기간별 단가 설정 화면** - 품목1 (실리콘 고무 시트) - **그룹1 추가**: 거래처 품번: CUST-A-001, 품명: 실리콘 시트 - **그룹2 추가**: 2024-01-01 ~ 2024-06-30, 50,000원 (기준단가) - **그룹2 추가**: 2024-07-01 ~ 무기한, 할인율 5% → 47,500원 - 품목2 (스테인리스 판) - **그룹1 추가**: 거래처 품번: CUST-A-002, 품명: SUS304 판 - **그룹2 추가**: 2024-01-01 ~ 무기한, 150,000원 (기준단가) 4. 저장 버튼 클릭 → customer_item_mapping 테이블에 4개 행 저장 ### 시나리오 2: 단순 단가 입력 필드 그룹을 사용하지 않고 단일 입력도 가능: ``` 그룹 없이 필드 정의: - customer_item_code - customer_item_name - current_unit_price - currency_code → 각 품목당 1개의 행만 저장 ``` ## 장점 ### 1. 범용성 - 기간별 단가뿐만 아니라 **모든 숫자 계산 시나리오**에 적용 가능 - 견적서, 발주서, 판매 단가, 구매 단가 등 ### 2. 유연성 - 필드 그룹으로 자유롭게 섹션 구성 - 표시 항목 설정으로 UI 커스터마이징 ### 3. 데이터 무결성 - 1:N 관계로 여러 기간별 데이터 관리 - 기간 중복 체크는 백엔드에서 처리 ### 4. 사용자 경험 - 품목별로 여러 개의 기간별 단가를 손쉽게 입력 - 입력 완료 후 작은 카드로 요약 표시 ## 다음 단계 1. **마이그레이션 실행** (999_add_period_price_columns_to_customer_item_mapping.sql) 2. **화면 편집기에서 설정** (위 Step 1~5 참고) 3. **백엔드 API 구현** (저장/조회 엔드포인트) 4. **계산 로직 추가** (선택사항: 자동 계산) 5. **테스트** (품목 선택 → 기간별 단가 입력 → 저장 → 조회) ## 참고 자료 - 선택항목 상세입력 컴포넌트: `frontend/lib/registry/components/selected-items-detail-input/` - 타입 정의: `frontend/lib/registry/components/selected-items-detail-input/types.ts` - 설정 패널: `SelectedItemsDetailInputConfigPanel.tsx`