# 기간별 단가 설정 시스템 구현 가이드
## 개요
**선택항목 상세입력(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`