diff --git a/plans/dummy-data-seeding-plan.md b/plans/dummy-data-seeding-plan.md new file mode 100644 index 0000000..6f4ce23 --- /dev/null +++ b/plans/dummy-data-seeding-plan.md @@ -0,0 +1,1574 @@ +# 더미 데이터 시딩 계획 + +> **작성일**: 2025-12-23 +> **목적**: React API 연동 테스트를 위한 더미 데이터 생성 +> **참고 문서**: `react-mock-to-api-migration-plan.md` + +--- + +## 1. 현황 분석 + +### 1.1 기존 데이터 현황 + +| 테이블 | 현재 개수 | 추가 목표 | 최종 목표 | +|--------|----------|----------|----------| +| tenants | 5 | - | 기존 활용 | +| users | 13 | - | 기존 활용 | +| clients | 4 (tenant 287) | +20 | 24개 | +| client_groups | 0 | +5 | 5개 | +| bank_accounts | 0 | +5 | 5개 | +| sales | 0 (tenant 287) | +80 | 80개 | +| purchases | 0 | +70 | 70개 | +| deposits | 0 | +60 | 60개 | +| withdrawals | 0 | +60 | 60개 | +| bills | 0 | +30 | 30개 | +| bill_installments | 0 | +15 | 15개 | +| **총계** | - | **~345개** | - | + +### 1.2 대상 테넌트 + +**Target: ID 287 (프론트_테스트회사)** +- Code: `JTKKPNNG6D` +- 기존 거래처 4개 보유 (ID: 9, 10, 11, 12) +- 테스트 목적으로 적합 + +### 1.3 데이터 기간 + +**2025년 1월 ~ 12월 (1년간)** +- 월별 균등 분포 +- 최근월(11~12월)은 draft 상태 포함 + +--- + +## 2. 테이블 의존성 분석 + +### 2.1 의존성 다이어그램 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Level 0: 기반 데이터 (이미 존재) │ +├─────────────────────────────────────────────────────────────────┤ +│ tenants (ID: 287) ──┬── users (ID: 1) │ +│ │ │ +└──────────────────────┼──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Level 1: 마스터 데이터 (5 + 5 = 10개) │ +├─────────────────────────────────────────────────────────────────┤ +│ ├── client_groups (5개) - 거래처 그룹 │ +│ └── bank_accounts (5개) - 은행 계좌 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Level 2: 거래처 데이터 (20개) │ +├─────────────────────────────────────────────────────────────────┤ +│ clients (20개) ← client_group_id (optional) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Level 3: 입출금/어음 데이터 (60 + 60 + 30 = 150개) │ +├─────────────────────────────────────────────────────────────────┤ +│ ├── deposits (60개) ← client_id, bank_account_id │ +│ ├── withdrawals (60개) ← client_id, bank_account_id │ +│ └── bills (30개) ← client_id, bank_account_id │ +│ └── bill_installments (15개) ← bill_id │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Level 4: 매출/매입 데이터 (80 + 70 = 150개) │ +├─────────────────────────────────────────────────────────────────┤ +│ ├── sales (80개) ← client_id, deposit_id (optional) │ +│ └── purchases (70개) ← client_id, withdrawal_id (optional) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 삽입 순서 및 수량 + +``` +1. client_groups (5개) - 거래처 그룹 +2. bank_accounts (5개) - 은행 계좌 +3. clients (20개) - 거래처 (매출처/매입처) +4. deposits (60개) - 입금 (월 5건) +5. withdrawals (60개) - 출금 (월 5건) +6. bills (30개) - 어음 (수취 15건 + 발행 15건) +7. sales (80개) - 매출 (월 6~7건) +8. purchases (70개) - 매입 (월 5~6건) +──────────────────────────── +총계 ~345개 +``` + +--- + +## 3. 더미 데이터 스키마 + +### 3.1 client_groups (거래처 그룹) - 5개 + +```php +[ + ['group_code' => 'VIP', 'group_name' => 'VIP 고객', 'price_rate' => 0.95], + ['group_code' => 'GOLD', 'group_name' => '골드 고객', 'price_rate' => 0.97], + ['group_code' => 'SILVER', 'group_name' => '실버 고객', 'price_rate' => 0.98], + ['group_code' => 'NORMAL', 'group_name' => '일반 고객', 'price_rate' => 1.00], + ['group_code' => 'NEW', 'group_name' => '신규 고객', 'price_rate' => 1.00], +] +``` + +### 3.2 bank_accounts (은행 계좌) - 5개 + +```php +[ + ['bank_code' => '004', 'bank_name' => 'KB국민은행', 'account_number' => '123-45-6789012', 'account_holder' => '프론트테스트', 'account_name' => '운영계좌', 'is_primary' => true], + ['bank_code' => '088', 'bank_name' => '신한은행', 'account_number' => '110-123-456789', 'account_holder' => '프론트테스트', 'account_name' => '급여계좌', 'is_primary' => false], + ['bank_code' => '020', 'bank_name' => '우리은행', 'account_number' => '1002-123-456789', 'account_holder' => '프론트테스트', 'account_name' => '예비계좌', 'is_primary' => false], + ['bank_code' => '081', 'bank_name' => '하나은행', 'account_number' => '123-456789-12345','account_holder' => '프론트테스트', 'account_name' => '법인카드', 'is_primary' => false], + ['bank_code' => '011', 'bank_name' => 'NH농협은행', 'account_number' => '351-1234-5678-12','account_holder' => '프론트테스트', 'account_name' => '비상금', 'is_primary' => false], +] +``` + +### 3.3 clients (거래처) - 20개 + +#### 매출처 (SALES) - 10개 +| Code | 회사명 | 사업자번호 | 그룹 | 담당자 | +|------|--------|-----------|------|--------| +| S001 | 삼성전자 | 124-81-00998 | VIP | 김철수 | +| S002 | LG전자 | 107-86-14075 | VIP | 이영희 | +| S003 | SK하이닉스 | 204-81-17169 | GOLD | 박민수 | +| S004 | 현대자동차 | 101-81-05765 | GOLD | 정은지 | +| S005 | 네이버 | 220-81-62517 | GOLD | 최준호 | +| S006 | 카카오 | 120-87-65763 | SILVER | 강미래 | +| S007 | 쿠팡 | 120-88-00767 | SILVER | 임도현 | +| S008 | 토스 | 120-87-83139 | NORMAL | 윤서연 | +| S009 | 배달의민족 | 220-87-93847 | NORMAL | 한지민 | +| S010 | 당근마켓 | 815-87-01234 | NEW | 오태양 | + +#### 매입처 (PURCHASE) - 7개 +| Code | 회사명 | 사업자번호 | 그룹 | 담당자 | +|------|--------|-----------|------|--------| +| P001 | 한화솔루션 | 138-81-00610 | - | 김재원 | +| P002 | 포스코 | 506-81-08754 | - | 이현석 | +| P003 | 롯데케미칼 | 301-81-07123 | - | 박서준 | +| P004 | GS칼텍스 | 104-81-23858 | - | 정해인 | +| P005 | 대한항공 | 110-81-14794 | - | 송민호 | +| P006 | 현대제철 | 130-81-12345 | - | 강동원 | +| P007 | SK이노베이션 | 110-81-67890 | - | 유재석 | + +#### 매출매입처 (BOTH) - 3개 +| Code | 회사명 | 사업자번호 | 그룹 | 담당자 | +|------|--------|-----------|------|--------| +| B001 | 두산에너빌리티 | 124-81-08628 | GOLD | 조인성 | +| B002 | CJ대한통운 | 104-81-39849 | SILVER | 공유 | +| B003 | 삼성SDS | 124-81-34567 | VIP | 이정재 | + +### 3.4 월별 데이터 분포 + +| 월 | 입금 | 출금 | 매출 | 매입 | 상태 | +|----|------|------|------|------|------| +| 1월 | 5건 | 5건 | 6건 | 5건 | confirmed/invoiced | +| 2월 | 5건 | 5건 | 6건 | 5건 | confirmed/invoiced | +| 3월 | 5건 | 5건 | 7건 | 6건 | confirmed/invoiced | +| 4월 | 5건 | 5건 | 6건 | 5건 | confirmed/invoiced | +| 5월 | 5건 | 5건 | 7건 | 6건 | confirmed/invoiced | +| 6월 | 5건 | 5건 | 6건 | 6건 | confirmed/invoiced | +| 7월 | 5건 | 5건 | 7건 | 6건 | confirmed/invoiced | +| 8월 | 5건 | 5건 | 6건 | 6건 | confirmed/invoiced | +| 9월 | 5건 | 5건 | 7건 | 6건 | confirmed/invoiced | +| 10월 | 5건 | 5건 | 7건 | 6건 | confirmed/invoiced | +| 11월 | 5건 | 5건 | 8건 | 7건 | confirmed + draft | +| 12월 | 5건 | 5건 | 7건 | 6건 | draft 위주 | +| **합계** | **60건** | **60건** | **80건** | **70건** | | + +### 3.5 금액 범위 및 분포 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 금액 분포 (공급가액 기준) │ +├─────────────────────────────────────────────────────────────────┤ +│ 소액 (30%): 1,000,000 ~ 5,000,000원 │ +│ 중액 (50%): 5,000,000 ~ 30,000,000원 │ +│ 대액 (20%): 30,000,000 ~ 100,000,000원 │ +├─────────────────────────────────────────────────────────────────┤ +│ 세액: 공급가액 × 10% │ +│ 합계: 공급가액 × 110% │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 3.6 deposits (입금) - 60개 예시 + +```php +// 월별 5건씩, 총 60건 +// 결제수단: transfer(70%), card(15%), cash(10%), check(5%) + +$deposits = [ + // 1월 + ['date' => '2025-01-05', 'client' => '삼성전자', 'amount' => 55000000, 'method' => 'transfer'], + ['date' => '2025-01-10', 'client' => 'LG전자', 'amount' => 28000000, 'method' => 'transfer'], + ['date' => '2025-01-15', 'client' => '현대자동차', 'amount' => 42000000, 'method' => 'transfer'], + ['date' => '2025-01-20', 'client' => '네이버', 'amount' => 15000000, 'method' => 'card'], + ['date' => '2025-01-25', 'client' => '카카오', 'amount' => 8500000, 'method' => 'transfer'], + + // 2월 + ['date' => '2025-02-05', 'client' => 'SK하이닉스', 'amount' => 68000000, 'method' => 'transfer'], + ['date' => '2025-02-10', 'client' => '쿠팡', 'amount' => 22000000, 'method' => 'transfer'], + ['date' => '2025-02-15', 'client' => '토스', 'amount' => 9800000, 'method' => 'card'], + ['date' => '2025-02-20', 'client' => '삼성SDS', 'amount' => 35000000, 'method' => 'transfer'], + ['date' => '2025-02-25', 'client' => '두산에너빌리티','amount' => 48000000, 'method' => 'check'], + + // ... 3월 ~ 12월 (패턴 반복, Seeder에서 자동 생성) +]; +``` + +### 3.7 withdrawals (출금) - 60개 예시 + +```php +// 월별 5건씩, 총 60건 +$withdrawals = [ + // 1월 + ['date' => '2025-01-03', 'client' => '한화솔루션', 'amount' => 32000000, 'method' => 'transfer'], + ['date' => '2025-01-08', 'client' => '포스코', 'amount' => 45000000, 'method' => 'transfer'], + ['date' => '2025-01-15', 'client' => '롯데케미칼', 'amount' => 18000000, 'method' => 'transfer'], + ['date' => '2025-01-22', 'client' => 'GS칼텍스', 'amount' => 12500000, 'method' => 'card'], + ['date' => '2025-01-28', 'client' => '대한항공', 'amount' => 5800000, 'method' => 'transfer'], + + // 2월 + ['date' => '2025-02-05', 'client' => '현대제철', 'amount' => 52000000, 'method' => 'transfer'], + ['date' => '2025-02-12', 'client' => 'SK이노베이션', 'amount' => 28000000, 'method' => 'transfer'], + ['date' => '2025-02-18', 'client' => 'CJ대한통운', 'amount' => 8500000, 'method' => 'transfer'], + ['date' => '2025-02-22', 'client' => '한화솔루션', 'amount' => 25000000, 'method' => 'check'], + ['date' => '2025-02-28', 'client' => '포스코', 'amount' => 38000000, 'method' => 'transfer'], + + // ... 3월 ~ 12월 (패턴 반복) +]; +``` + +### 3.8 sales (매출) - 80개 예시 + +```php +// 월별 6~7건, 총 80건 +// 상태: 1~10월 invoiced/confirmed, 11~12월 draft 포함 +$sales = [ + // 1월 (6건) + ['number' => 'SAL-202501-0001', 'date' => '2025-01-05', 'client' => '삼성전자', 'supply' => 50000000, 'status' => 'invoiced'], + ['number' => 'SAL-202501-0002', 'date' => '2025-01-08', 'client' => 'LG전자', 'supply' => 25454545, 'status' => 'invoiced'], + ['number' => 'SAL-202501-0003', 'date' => '2025-01-12', 'client' => '현대자동차', 'supply' => 38181818, 'status' => 'invoiced'], + ['number' => 'SAL-202501-0004', 'date' => '2025-01-18', 'client' => '네이버', 'supply' => 13636364, 'status' => 'confirmed'], + ['number' => 'SAL-202501-0005', 'date' => '2025-01-22', 'client' => '카카오', 'supply' => 7727273, 'status' => 'confirmed'], + ['number' => 'SAL-202501-0006', 'date' => '2025-01-28', 'client' => '삼성SDS', 'supply' => 31818182, 'status' => 'invoiced'], + + // ... 2월 ~ 10월 (invoiced/confirmed) + + // 11월 (8건 - draft 포함) + ['number' => 'SAL-202511-0001', 'date' => '2025-11-03', 'client' => '삼성전자', 'supply' => 45000000, 'status' => 'invoiced'], + ['number' => 'SAL-202511-0002', 'date' => '2025-11-06', 'client' => 'SK하이닉스', 'supply' => 62000000, 'status' => 'invoiced'], + ['number' => 'SAL-202511-0003', 'date' => '2025-11-10', 'client' => '쿠팡', 'supply' => 18181818, 'status' => 'confirmed'], + ['number' => 'SAL-202511-0004', 'date' => '2025-11-14', 'client' => '두산에너빌리티','supply' => 55000000, 'status' => 'confirmed'], + ['number' => 'SAL-202511-0005', 'date' => '2025-11-18', 'client' => '당근마켓', 'supply' => 8909091, 'status' => 'confirmed'], + ['number' => 'SAL-202511-0006', 'date' => '2025-11-22', 'client' => '토스', 'supply' => 12727273, 'status' => 'draft'], + ['number' => 'SAL-202511-0007', 'date' => '2025-11-26', 'client' => '배달의민족', 'supply' => 15454545, 'status' => 'draft'], + ['number' => 'SAL-202511-0008', 'date' => '2025-11-29', 'client' => 'LG전자', 'supply' => 28000000, 'status' => 'draft'], + + // 12월 (7건 - draft 위주) + ['number' => 'SAL-202512-0001', 'date' => '2025-12-02', 'client' => '삼성전자', 'supply' => 48000000, 'status' => 'confirmed'], + ['number' => 'SAL-202512-0002', 'date' => '2025-12-05', 'client' => '현대자동차', 'supply' => 35000000, 'status' => 'draft'], + ['number' => 'SAL-202512-0003', 'date' => '2025-12-10', 'client' => '네이버', 'supply' => 22727273, 'status' => 'draft'], + ['number' => 'SAL-202512-0004', 'date' => '2025-12-13', 'client' => '카카오', 'supply' => 16363636, 'status' => 'draft'], + ['number' => 'SAL-202512-0005', 'date' => '2025-12-17', 'client' => '삼성SDS', 'supply' => 42000000, 'status' => 'draft'], + ['number' => 'SAL-202512-0006', 'date' => '2025-12-20', 'client' => 'SK하이닉스', 'supply' => 58000000, 'status' => 'draft'], + ['number' => 'SAL-202512-0007', 'date' => '2025-12-23', 'client' => '쿠팡', 'supply' => 20000000, 'status' => 'draft'], +]; +``` + +### 3.9 purchases (매입) - 70개 예시 + +```php +// 월별 5~6건, 총 70건 +$purchases = [ + // 1월 (5건) + ['number' => 'PUR-202501-0001', 'date' => '2025-01-03', 'client' => '한화솔루션', 'supply' => 29090909, 'status' => 'confirmed'], + ['number' => 'PUR-202501-0002', 'date' => '2025-01-10', 'client' => '포스코', 'supply' => 40909091, 'status' => 'confirmed'], + ['number' => 'PUR-202501-0003', 'date' => '2025-01-15', 'client' => '롯데케미칼', 'supply' => 16363636, 'status' => 'confirmed'], + ['number' => 'PUR-202501-0004', 'date' => '2025-01-22', 'client' => 'GS칼텍스', 'supply' => 11363636, 'status' => 'confirmed'], + ['number' => 'PUR-202501-0005', 'date' => '2025-01-28', 'client' => '대한항공', 'supply' => 5272727, 'status' => 'confirmed'], + + // ... 2월 ~ 10월 (confirmed) + + // 11월 (7건 - draft 포함) + ['number' => 'PUR-202511-0001', 'date' => '2025-11-03', 'client' => '현대제철', 'supply' => 48000000, 'status' => 'confirmed'], + ['number' => 'PUR-202511-0002', 'date' => '2025-11-08', 'client' => 'SK이노베이션', 'supply' => 32000000, 'status' => 'confirmed'], + ['number' => 'PUR-202511-0003', 'date' => '2025-11-12', 'client' => 'CJ대한통운', 'supply' => 9090909, 'status' => 'confirmed'], + ['number' => 'PUR-202511-0004', 'date' => '2025-11-18', 'client' => '한화솔루션', 'supply' => 35000000, 'status' => 'confirmed'], + ['number' => 'PUR-202511-0005', 'date' => '2025-11-22', 'client' => '포스코', 'supply' => 42000000, 'status' => 'draft'], + ['number' => 'PUR-202511-0006', 'date' => '2025-11-26', 'client' => '롯데케미칼', 'supply' => 18181818, 'status' => 'draft'], + ['number' => 'PUR-202511-0007', 'date' => '2025-11-29', 'client' => '두산에너빌리티','supply' => 55000000, 'status' => 'draft'], + + // 12월 (6건 - draft 위주) + ['number' => 'PUR-202512-0001', 'date' => '2025-12-02', 'client' => 'GS칼텍스', 'supply' => 15454545, 'status' => 'confirmed'], + ['number' => 'PUR-202512-0002', 'date' => '2025-12-06', 'client' => '대한항공', 'supply' => 7272727, 'status' => 'draft'], + ['number' => 'PUR-202512-0003', 'date' => '2025-12-11', 'client' => '현대제철', 'supply' => 52000000, 'status' => 'draft'], + ['number' => 'PUR-202512-0004', 'date' => '2025-12-15', 'client' => 'SK이노베이션', 'supply' => 28000000, 'status' => 'draft'], + ['number' => 'PUR-202512-0005', 'date' => '2025-12-19', 'client' => '한화솔루션', 'supply' => 38000000, 'status' => 'draft'], + ['number' => 'PUR-202512-0006', 'date' => '2025-12-23', 'client' => '포스코', 'supply' => 45000000, 'status' => 'draft'], +]; +``` + +### 3.10 bills (어음) - 30개 예시 + +```php +// 수취 어음 15건 + 발행 어음 15건, 총 30건 +// bill_type: received(수취), issued(발행) +// status (수취): stored, maturityAlert, maturityResult, paymentComplete, dishonored +// status (발행): stored, maturityAlert, collectionRequest, collectionComplete, suing, dishonored + +$bills = [ + // 수취 어음 (received) - 15건 + ['bill_number' => '202501000001', 'type' => 'received', 'client' => '삼성전자', 'amount' => 50000000, 'issue_date' => '2025-01-15', 'maturity_date' => '2025-04-15', 'status' => 'paymentComplete'], + ['bill_number' => '202501000002', 'type' => 'received', 'client' => 'LG전자', 'amount' => 35000000, 'issue_date' => '2025-02-10', 'maturity_date' => '2025-05-10', 'status' => 'paymentComplete'], + ['bill_number' => '202502000001', 'type' => 'received', 'client' => 'SK하이닉스', 'amount' => 80000000, 'issue_date' => '2025-02-20', 'maturity_date' => '2025-05-20', 'status' => 'paymentComplete'], + ['bill_number' => '202503000001', 'type' => 'received', 'client' => '현대자동차', 'amount' => 45000000, 'issue_date' => '2025-03-05', 'maturity_date' => '2025-06-05', 'status' => 'maturityResult'], + ['bill_number' => '202504000001', 'type' => 'received', 'client' => '네이버', 'amount' => 25000000, 'issue_date' => '2025-04-12', 'maturity_date' => '2025-07-12', 'status' => 'maturityResult'], + ['bill_number' => '202505000001', 'type' => 'received', 'client' => '카카오', 'amount' => 18000000, 'issue_date' => '2025-05-08', 'maturity_date' => '2025-08-08', 'status' => 'stored'], + ['bill_number' => '202506000001', 'type' => 'received', 'client' => '쿠팡', 'amount' => 32000000, 'issue_date' => '2025-06-15', 'maturity_date' => '2025-09-15', 'status' => 'stored'], + ['bill_number' => '202507000001', 'type' => 'received', 'client' => '삼성SDS', 'amount' => 65000000, 'issue_date' => '2025-07-20', 'maturity_date' => '2025-10-20', 'status' => 'stored'], + ['bill_number' => '202508000001', 'type' => 'received', 'client' => '토스', 'amount' => 15000000, 'issue_date' => '2025-08-10', 'maturity_date' => '2025-11-10', 'status' => 'stored'], + ['bill_number' => '202509000001', 'type' => 'received', 'client' => '두산에너빌리티','amount' => 55000000, 'issue_date' => '2025-09-05', 'maturity_date' => '2025-12-05', 'status' => 'maturityAlert'], + ['bill_number' => '202510000001', 'type' => 'received', 'client' => '삼성전자', 'amount' => 42000000, 'issue_date' => '2025-10-15', 'maturity_date' => '2026-01-15', 'status' => 'stored'], + ['bill_number' => '202511000001', 'type' => 'received', 'client' => 'LG전자', 'amount' => 28000000, 'issue_date' => '2025-11-08', 'maturity_date' => '2026-02-08', 'status' => 'stored'], + ['bill_number' => '202511000002', 'type' => 'received', 'client' => '네이버', 'amount' => 38000000, 'issue_date' => '2025-11-20', 'maturity_date' => '2026-02-20', 'status' => 'stored'], + ['bill_number' => '202512000001', 'type' => 'received', 'client' => '현대자동차', 'amount' => 52000000, 'issue_date' => '2025-12-10', 'maturity_date' => '2026-03-10', 'status' => 'stored'], + ['bill_number' => '202512000002', 'type' => 'received', 'client' => 'SK하이닉스', 'amount' => 70000000, 'issue_date' => '2025-12-18', 'maturity_date' => '2026-03-18', 'status' => 'stored'], + + // 발행 어음 (issued) - 15건 + ['bill_number' => '202501100001', 'type' => 'issued', 'client' => '한화솔루션', 'amount' => 40000000, 'issue_date' => '2025-01-20', 'maturity_date' => '2025-04-20', 'status' => 'collectionComplete'], + ['bill_number' => '202502100001', 'type' => 'issued', 'client' => '포스코', 'amount' => 55000000, 'issue_date' => '2025-02-15', 'maturity_date' => '2025-05-15', 'status' => 'collectionComplete'], + ['bill_number' => '202503100001', 'type' => 'issued', 'client' => '롯데케미칼', 'amount' => 30000000, 'issue_date' => '2025-03-10', 'maturity_date' => '2025-06-10', 'status' => 'collectionComplete'], + ['bill_number' => '202504100001', 'type' => 'issued', 'client' => 'GS칼텍스', 'amount' => 22000000, 'issue_date' => '2025-04-18', 'maturity_date' => '2025-07-18', 'status' => 'collectionComplete'], + ['bill_number' => '202505100001', 'type' => 'issued', 'client' => '대한항공', 'amount' => 18000000, 'issue_date' => '2025-05-12', 'maturity_date' => '2025-08-12', 'status' => 'collectionRequest'], + ['bill_number' => '202506100001', 'type' => 'issued', 'client' => '현대제철', 'amount' => 48000000, 'issue_date' => '2025-06-20', 'maturity_date' => '2025-09-20', 'status' => 'collectionRequest'], + ['bill_number' => '202507100001', 'type' => 'issued', 'client' => 'SK이노베이션', 'amount' => 35000000, 'issue_date' => '2025-07-15', 'maturity_date' => '2025-10-15', 'status' => 'stored'], + ['bill_number' => '202508100001', 'type' => 'issued', 'client' => 'CJ대한통운', 'amount' => 25000000, 'issue_date' => '2025-08-22', 'maturity_date' => '2025-11-22', 'status' => 'stored'], + ['bill_number' => '202509100001', 'type' => 'issued', 'client' => '두산에너빌리티','amount' => 60000000, 'issue_date' => '2025-09-10', 'maturity_date' => '2025-12-10', 'status' => 'maturityAlert'], + ['bill_number' => '202510100001', 'type' => 'issued', 'client' => '한화솔루션', 'amount' => 45000000, 'issue_date' => '2025-10-08', 'maturity_date' => '2026-01-08', 'status' => 'stored'], + ['bill_number' => '202511100001', 'type' => 'issued', 'client' => '포스코', 'amount' => 58000000, 'issue_date' => '2025-11-05', 'maturity_date' => '2026-02-05', 'status' => 'stored'], + ['bill_number' => '202511100002', 'type' => 'issued', 'client' => '롯데케미칼', 'amount' => 32000000, 'issue_date' => '2025-11-18', 'maturity_date' => '2026-02-18', 'status' => 'stored'], + ['bill_number' => '202512100001', 'type' => 'issued', 'client' => 'GS칼텍스', 'amount' => 28000000, 'issue_date' => '2025-12-05', 'maturity_date' => '2026-03-05', 'status' => 'stored'], + ['bill_number' => '202512100002', 'type' => 'issued', 'client' => '현대제철', 'amount' => 42000000, 'issue_date' => '2025-12-15', 'maturity_date' => '2026-03-15', 'status' => 'stored'], + ['bill_number' => '202512100003', 'type' => 'issued', 'client' => 'SK이노베이션', 'amount' => 38000000, 'issue_date' => '2025-12-22', 'maturity_date' => '2026-03-22', 'status' => 'stored'], +]; + +// 일부 어음에 차수 관리 내역 추가 (15건) +$billInstallments = [ + // 수취 어음 차수 + ['bill_number' => '202501000001', 'installments' => [ + ['date' => '2025-02-15', 'amount' => 25000000, 'note' => '1차 분할 입금'], + ['date' => '2025-03-15', 'amount' => 25000000, 'note' => '2차 분할 입금'], + ]], + ['bill_number' => '202502000001', 'installments' => [ + ['date' => '2025-03-20', 'amount' => 40000000, 'note' => '1차 분할 입금'], + ['date' => '2025-04-20', 'amount' => 40000000, 'note' => '2차 분할 입금'], + ]], + ['bill_number' => '202507000001', 'installments' => [ + ['date' => '2025-08-20', 'amount' => 30000000, 'note' => '1차 분할 입금'], + ['date' => '2025-09-20', 'amount' => 35000000, 'note' => '2차 분할 입금'], + ]], + + // 발행 어음 차수 + ['bill_number' => '202501100001', 'installments' => [ + ['date' => '2025-02-20', 'amount' => 20000000, 'note' => '1차 분할 지급'], + ['date' => '2025-03-20', 'amount' => 20000000, 'note' => '2차 분할 지급'], + ]], + ['bill_number' => '202502100001', 'installments' => [ + ['date' => '2025-03-15', 'amount' => 27500000, 'note' => '1차 분할 지급'], + ['date' => '2025-04-15', 'amount' => 27500000, 'note' => '2차 분할 지급'], + ]], + ['bill_number' => '202506100001', 'installments' => [ + ['date' => '2025-07-20', 'amount' => 24000000, 'note' => '1차 분할 지급'], + ['date' => '2025-08-20', 'amount' => 24000000, 'note' => '2차 분할 지급'], + ]], +]; +``` + +--- + +## 4. Laravel Seeder 구현 전략 + +### 4.1 Seeder 파일 구조 + +``` +database/seeders/ +├── DummyDataSeeder.php ← 메인 Seeder (순서 제어) +└── Dummy/ + ├── DummyClientGroupSeeder.php ← 거래처 그룹 (5개) + ├── DummyBankAccountSeeder.php ← 은행 계좌 (5개) + ├── DummyClientSeeder.php ← 거래처 (20개) + ├── DummyDepositSeeder.php ← 입금 (60개) + ├── DummyWithdrawalSeeder.php ← 출금 (60개) + ├── DummyBillSeeder.php ← 어음 (30개) + 차수 (15개) + ├── DummySaleSeeder.php ← 매출 (80개) + └── DummyPurchaseSeeder.php ← 매입 (70개) +``` + +### 4.2 메인 Seeder + +```php +command->info('🌱 더미 데이터 시딩 시작...'); + $this->command->info(' 대상 테넌트: ID ' . self::TENANT_ID); + + $this->call([ + Dummy\DummyClientGroupSeeder::class, + Dummy\DummyBankAccountSeeder::class, + Dummy\DummyClientSeeder::class, + Dummy\DummyDepositSeeder::class, + Dummy\DummyWithdrawalSeeder::class, + Dummy\DummyBillSeeder::class, + Dummy\DummySaleSeeder::class, + Dummy\DummyPurchaseSeeder::class, + ]); + + $this->command->info(''); + $this->command->info('✅ 더미 데이터 시딩 완료!'); + $this->command->table( + ['테이블', '생성 수량'], + [ + ['client_groups', '5'], + ['bank_accounts', '5'], + ['clients', '20'], + ['deposits', '60'], + ['withdrawals', '60'], + ['bills', '30'], + ['bill_installments', '15'], + ['sales', '80'], + ['purchases', '70'], + ['총계', '~345'], + ] + ); + } +} +``` + +### 4.3 실행 방법 + +```bash +# 더미 데이터 시딩 +php artisan db:seed --class=DummyDataSeeder + +# 특정 Seeder만 실행 +php artisan db:seed --class=Database\\Seeders\\Dummy\\DummyClientSeeder +``` + +### 4.4 DB 스키마 상세 정의 + +#### 4.4.1 client_groups 테이블 +```sql +CREATE TABLE client_groups ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + group_code VARCHAR(30) NOT NULL COMMENT '그룹 코드', + group_name VARCHAR(100) NOT NULL COMMENT '그룹명', + price_rate DECIMAL(5,4) DEFAULT 1.0000 COMMENT '가격 배율', + is_active TINYINT DEFAULT 1 COMMENT '활성 여부', + created_by BIGINT UNSIGNED NULL COMMENT '생성자 ID', + updated_by BIGINT UNSIGNED NULL COMMENT '수정자 ID', + deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자 ID', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + UNIQUE INDEX uq_client_groups_tenant_code (tenant_id, group_code), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +); +``` + +#### 4.4.2 bank_accounts 테이블 +```sql +CREATE TABLE bank_accounts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + bank_code VARCHAR(10) NOT NULL COMMENT '은행 코드', + bank_name VARCHAR(50) NOT NULL COMMENT '은행명', + account_number VARCHAR(30) NOT NULL COMMENT '계좌번호', + account_holder VARCHAR(50) NOT NULL COMMENT '예금주', + account_name VARCHAR(100) NOT NULL COMMENT '계좌 별칭', + status VARCHAR(20) DEFAULT 'active' COMMENT '상태: active/inactive', + assigned_user_id BIGINT UNSIGNED NULL COMMENT '담당자 ID', + is_primary BOOLEAN DEFAULT FALSE COMMENT '대표계좌 여부', + created_by BIGINT UNSIGNED NULL COMMENT '생성자 ID', + updated_by BIGINT UNSIGNED NULL COMMENT '수정자 ID', + deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자 ID', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL +); +``` + +#### 4.4.3 clients 테이블 (주요 컬럼) +```sql +-- 핵심 필드만 기재 (전체 스키마는 마이그레이션 참조) +CREATE TABLE clients ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + client_group_id BIGINT UNSIGNED NULL COMMENT '그룹 ID (FK)', + client_code VARCHAR(20) NOT NULL COMMENT '거래처 코드', + name VARCHAR(100) NOT NULL COMMENT '거래처명', + client_type VARCHAR(20) NULL COMMENT 'SALES/PURCHASE/BOTH', + contact_person VARCHAR(50) NULL COMMENT '담당자명', + phone VARCHAR(20) NULL, + mobile VARCHAR(20) NULL, + email VARCHAR(100) NULL, + address TEXT NULL, + business_no VARCHAR(20) NULL COMMENT '사업자번호', + business_type VARCHAR(50) NULL COMMENT '업종', + business_item VARCHAR(100) NULL COMMENT '업태', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL +); +``` + +#### 4.4.4 deposits 테이블 +```sql +CREATE TABLE deposits ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + deposit_date DATE NOT NULL COMMENT '입금일', + client_id BIGINT UNSIGNED NULL COMMENT '거래처 ID', + client_name VARCHAR(100) NULL COMMENT '비회원 거래처명', + bank_account_id BIGINT UNSIGNED NULL COMMENT '입금 계좌 ID', + amount DECIMAL(15,2) NOT NULL COMMENT '금액', + payment_method VARCHAR(20) NOT NULL COMMENT 'cash/transfer/card/check', + account_code VARCHAR(20) NULL COMMENT '계정과목', + description TEXT NULL COMMENT '적요', + reference_type VARCHAR(50) NULL COMMENT '참조 유형', + reference_id BIGINT UNSIGNED NULL COMMENT '참조 ID', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL +); +``` + +#### 4.4.5 withdrawals 테이블 +```sql +CREATE TABLE withdrawals ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + withdrawal_date DATE NOT NULL COMMENT '출금일', + client_id BIGINT UNSIGNED NULL COMMENT '거래처 ID', + client_name VARCHAR(100) NULL COMMENT '비회원 거래처명', + bank_account_id BIGINT UNSIGNED NULL COMMENT '출금 계좌 ID', + amount DECIMAL(15,2) NOT NULL COMMENT '금액', + payment_method VARCHAR(20) NOT NULL COMMENT 'cash/transfer/card/check', + account_code VARCHAR(20) NULL COMMENT '계정과목', + description TEXT NULL COMMENT '적요', + reference_type VARCHAR(50) NULL COMMENT '참조 유형', + reference_id BIGINT UNSIGNED NULL COMMENT '참조 ID', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL +); +``` + +#### 4.4.6 sales 테이블 +```sql +CREATE TABLE sales ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + sale_number VARCHAR(30) NOT NULL COMMENT '매출번호', + sale_date DATE NOT NULL COMMENT '매출일자', + client_id BIGINT UNSIGNED NOT NULL COMMENT '거래처 ID', + supply_amount DECIMAL(15,2) NOT NULL COMMENT '공급가액', + tax_amount DECIMAL(15,2) NOT NULL COMMENT '세액', + total_amount DECIMAL(15,2) NOT NULL COMMENT '합계', + description TEXT NULL COMMENT '적요', + status VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/confirmed/invoiced', + tax_invoice_id BIGINT UNSIGNED NULL COMMENT '세금계산서 ID', + deposit_id BIGINT UNSIGNED NULL COMMENT '입금 연결 ID', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + UNIQUE INDEX uk_tenant_sale_number (tenant_id, sale_number) +); +``` + +#### 4.4.7 purchases 테이블 +```sql +CREATE TABLE purchases ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + purchase_number VARCHAR(30) NOT NULL COMMENT '매입번호', + purchase_date DATE NOT NULL COMMENT '매입일자', + client_id BIGINT UNSIGNED NOT NULL COMMENT '거래처 ID', + supply_amount DECIMAL(15,2) NOT NULL COMMENT '공급가액', + tax_amount DECIMAL(15,2) NOT NULL COMMENT '세액', + total_amount DECIMAL(15,2) NOT NULL COMMENT '합계', + description TEXT NULL COMMENT '적요', + status VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/confirmed', + withdrawal_id BIGINT UNSIGNED NULL COMMENT '출금 연결 ID', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + UNIQUE INDEX uk_tenant_purchase_number (tenant_id, purchase_number) +); +``` + +### 4.5 모델 경로 및 네임스페이스 + +| 테이블 | 모델 클래스 | 네임스페이스 | +|--------|------------|-------------| +| client_groups | ClientGroup | `App\Models\Orders\ClientGroup` | +| bank_accounts | BankAccount | `App\Models\Tenants\BankAccount` | +| clients | Client | `App\Models\Orders\Client` | +| deposits | Deposit | `App\Models\Tenants\Deposit` | +| withdrawals | Withdrawal | `App\Models\Tenants\Withdrawal` | +| sales | Sale | `App\Models\Tenants\Sale` | +| purchases | Purchase | `App\Models\Tenants\Purchase` | + +### 4.6 필수 상수값 + +#### 결제수단 (payment_method) +```php +// Deposit::PAYMENT_METHODS, Withdrawal::PAYMENT_METHODS +[ + 'cash' => '현금', + 'transfer' => '계좌이체', + 'card' => '카드', + 'check' => '수표', +] +``` + +#### 매출 상태 (Sale::STATUSES) +```php +[ + 'draft' => '임시저장', + 'confirmed' => '확정', + 'invoiced' => '세금계산서발행', +] +``` + +#### 매입 상태 (Purchase::STATUSES) +```php +[ + 'draft' => '임시저장', + 'confirmed' => '확정', +] +``` + +#### 거래처 유형 (client_type) +```php +// common_codes 테이블에서 관리 +// group: 'client_type' +[ + 'SALES' => '매출처', + 'PURCHASE' => '매입처', + 'BOTH' => '매출매입처', +] +``` + +### 4.7 완전한 Seeder 코드 구현 + +#### 4.7.1 DummyClientGroupSeeder.php +```php + 'VIP', 'group_name' => 'VIP 고객', 'price_rate' => 0.95], + ['group_code' => 'GOLD', 'group_name' => '골드 고객', 'price_rate' => 0.97], + ['group_code' => 'SILVER', 'group_name' => '실버 고객', 'price_rate' => 0.98], + ['group_code' => 'NORMAL', 'group_name' => '일반 고객', 'price_rate' => 1.00], + ['group_code' => 'NEW', 'group_name' => '신규 고객', 'price_rate' => 1.00], + ]; + + foreach ($groups as $group) { + ClientGroup::create([ + 'tenant_id' => $tenantId, + 'group_code' => $group['group_code'], + 'group_name' => $group['group_name'], + 'price_rate' => $group['price_rate'], + 'is_active' => true, + 'created_by' => $userId, + ]); + } + + $this->command->info(' ✓ client_groups: ' . count($groups) . '건 생성'); + } +} +``` + +#### 4.7.2 DummyBankAccountSeeder.php +```php + '004', 'bank_name' => 'KB국민은행', 'account_number' => '123-45-6789012', 'account_holder' => '프론트테스트', 'account_name' => '운영계좌', 'is_primary' => true], + ['bank_code' => '088', 'bank_name' => '신한은행', 'account_number' => '110-123-456789', 'account_holder' => '프론트테스트', 'account_name' => '급여계좌', 'is_primary' => false], + ['bank_code' => '020', 'bank_name' => '우리은행', 'account_number' => '1002-123-456789', 'account_holder' => '프론트테스트', 'account_name' => '예비계좌', 'is_primary' => false], + ['bank_code' => '081', 'bank_name' => '하나은행', 'account_number' => '123-456789-12345','account_holder' => '프론트테스트', 'account_name' => '법인카드', 'is_primary' => false], + ['bank_code' => '011', 'bank_name' => 'NH농협은행', 'account_number' => '351-1234-5678-12','account_holder' => '프론트테스트', 'account_name' => '비상금', 'is_primary' => false], + ]; + + foreach ($accounts as $account) { + BankAccount::create([ + 'tenant_id' => $tenantId, + 'bank_code' => $account['bank_code'], + 'bank_name' => $account['bank_name'], + 'account_number' => $account['account_number'], + 'account_holder' => $account['account_holder'], + 'account_name' => $account['account_name'], + 'status' => 'active', + 'is_primary' => $account['is_primary'], + 'created_by' => $userId, + ]); + } + + $this->command->info(' ✓ bank_accounts: ' . count($accounts) . '건 생성'); + } +} +``` + +#### 4.7.3 DummyClientSeeder.php +```php +pluck('id', 'group_code') + ->toArray(); + + // 매출처 (SALES) - 10개 + $salesClients = [ + ['code' => 'S001', 'name' => '삼성전자', 'business_no' => '124-81-00998', 'group' => 'VIP', 'contact' => '김철수', 'phone' => '02-1234-5678', 'email' => 'kim@samsung.com'], + ['code' => 'S002', 'name' => 'LG전자', 'business_no' => '107-86-14075', 'group' => 'VIP', 'contact' => '이영희', 'phone' => '02-2345-6789', 'email' => 'lee@lg.com'], + ['code' => 'S003', 'name' => 'SK하이닉스', 'business_no' => '204-81-17169', 'group' => 'GOLD', 'contact' => '박민수', 'phone' => '031-123-4567', 'email' => 'park@skhynix.com'], + ['code' => 'S004', 'name' => '현대자동차', 'business_no' => '101-81-05765', 'group' => 'GOLD', 'contact' => '정은지', 'phone' => '02-3456-7890', 'email' => 'jung@hyundai.com'], + ['code' => 'S005', 'name' => '네이버', 'business_no' => '220-81-62517', 'group' => 'GOLD', 'contact' => '최준호', 'phone' => '031-234-5678', 'email' => 'choi@naver.com'], + ['code' => 'S006', 'name' => '카카오', 'business_no' => '120-87-65763', 'group' => 'SILVER', 'contact' => '강미래', 'phone' => '02-4567-8901', 'email' => 'kang@kakao.com'], + ['code' => 'S007', 'name' => '쿠팡', 'business_no' => '120-88-00767', 'group' => 'SILVER', 'contact' => '임도현', 'phone' => '02-5678-9012', 'email' => 'lim@coupang.com'], + ['code' => 'S008', 'name' => '토스', 'business_no' => '120-87-83139', 'group' => 'NORMAL', 'contact' => '윤서연', 'phone' => '02-6789-0123', 'email' => 'yoon@toss.im'], + ['code' => 'S009', 'name' => '배달의민족', 'business_no' => '220-87-93847', 'group' => 'NORMAL', 'contact' => '한지민', 'phone' => '02-7890-1234', 'email' => 'han@woowahan.com'], + ['code' => 'S010', 'name' => '당근마켓', 'business_no' => '815-87-01234', 'group' => 'NEW', 'contact' => '오태양', 'phone' => '02-8901-2345', 'email' => 'oh@daangn.com'], + ]; + + // 매입처 (PURCHASE) - 7개 + $purchaseClients = [ + ['code' => 'P001', 'name' => '한화솔루션', 'business_no' => '138-81-00610', 'group' => null, 'contact' => '김재원', 'phone' => '02-1111-2222', 'email' => 'kim@hanwha.com'], + ['code' => 'P002', 'name' => '포스코', 'business_no' => '506-81-08754', 'group' => null, 'contact' => '이현석', 'phone' => '054-111-2222', 'email' => 'lee@posco.com'], + ['code' => 'P003', 'name' => '롯데케미칼', 'business_no' => '301-81-07123', 'group' => null, 'contact' => '박서준', 'phone' => '02-2222-3333', 'email' => 'park@lottechem.com'], + ['code' => 'P004', 'name' => 'GS칼텍스', 'business_no' => '104-81-23858', 'group' => null, 'contact' => '정해인', 'phone' => '02-3333-4444', 'email' => 'jung@gscaltex.com'], + ['code' => 'P005', 'name' => '대한항공', 'business_no' => '110-81-14794', 'group' => null, 'contact' => '송민호', 'phone' => '02-4444-5555', 'email' => 'song@koreanair.com'], + ['code' => 'P006', 'name' => '현대제철', 'business_no' => '130-81-12345', 'group' => null, 'contact' => '강동원', 'phone' => '032-555-6666', 'email' => 'kang@hyundaisteel.com'], + ['code' => 'P007', 'name' => 'SK이노베이션', 'business_no' => '110-81-67890', 'group' => null, 'contact' => '유재석', 'phone' => '02-6666-7777', 'email' => 'yoo@skinnovation.com'], + ]; + + // 매출매입처 (BOTH) - 3개 + $bothClients = [ + ['code' => 'B001', 'name' => '두산에너빌리티', 'business_no' => '124-81-08628', 'group' => 'GOLD', 'contact' => '조인성', 'phone' => '02-7777-8888', 'email' => 'cho@doosan.com'], + ['code' => 'B002', 'name' => 'CJ대한통운', 'business_no' => '104-81-39849', 'group' => 'SILVER', 'contact' => '공유', 'phone' => '02-8888-9999', 'email' => 'gong@cjlogistics.com'], + ['code' => 'B003', 'name' => '삼성SDS', 'business_no' => '124-81-34567', 'group' => 'VIP', 'contact' => '이정재', 'phone' => '02-9999-0000', 'email' => 'lee@samsungsds.com'], + ]; + + $count = 0; + + // 매출처 생성 + foreach ($salesClients as $client) { + $this->createClient($client, 'SALES', $tenantId, $userId, $groups); + $count++; + } + + // 매입처 생성 + foreach ($purchaseClients as $client) { + $this->createClient($client, 'PURCHASE', $tenantId, $userId, $groups); + $count++; + } + + // 매출매입처 생성 + foreach ($bothClients as $client) { + $this->createClient($client, 'BOTH', $tenantId, $userId, $groups); + $count++; + } + + $this->command->info(' ✓ clients: ' . $count . '건 생성'); + } + + private function createClient(array $data, string $type, int $tenantId, int $userId, array $groups): void + { + Client::create([ + 'tenant_id' => $tenantId, + 'client_group_id' => $data['group'] ? ($groups[$data['group']] ?? null) : null, + 'client_code' => $data['code'], + 'name' => $data['name'], + 'client_type' => $type, + 'contact_person' => $data['contact'], + 'phone' => $data['phone'], + 'email' => $data['email'], + 'business_no' => $data['business_no'], + 'business_type' => '제조업', + 'business_item' => '전자제품', + 'is_active' => true, + ]); + } +} +``` + +#### 4.7.4 DummyDepositSeeder.php +```php +whereIn('client_type', ['SALES', 'BOTH']) + ->get() + ->keyBy('name'); + + // 은행계좌 (대표계좌 우선) + $bankAccounts = BankAccount::where('tenant_id', $tenantId) + ->where('status', 'active') + ->orderByDesc('is_primary') + ->get(); + + $primaryBankId = $bankAccounts->first()?->id; + + // 결제수단 분포: transfer(70%), card(15%), cash(10%), check(5%) + $methods = array_merge( + array_fill(0, 14, 'transfer'), + array_fill(0, 3, 'card'), + array_fill(0, 2, 'cash'), + array_fill(0, 1, 'check') + ); + + // 거래처 순환 목록 + $clientNames = [ + '삼성전자', 'LG전자', 'SK하이닉스', '현대자동차', '네이버', + '카카오', '쿠팡', '토스', '배달의민족', '당근마켓', + '두산에너빌리티', 'CJ대한통운', '삼성SDS', + ]; + + // 금액 범위 + $amounts = [ + 'small' => [1000000, 5000000], // 30% + 'medium' => [5000000, 30000000], // 50% + 'large' => [30000000, 100000000], // 20% + ]; + + $count = 0; + $year = 2025; + + // 월별 5건씩, 총 60건 + for ($month = 1; $month <= 12; $month++) { + $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + + for ($i = 0; $i < 5; $i++) { + $day = rand(1, $daysInMonth); + $clientName = $clientNames[($month * 5 + $i) % count($clientNames)]; + $client = $clients->get($clientName); + + // 금액 결정 (분포에 따라) + $rand = rand(1, 100); + if ($rand <= 30) { + $amount = rand($amounts['small'][0], $amounts['small'][1]); + } elseif ($rand <= 80) { + $amount = rand($amounts['medium'][0], $amounts['medium'][1]); + } else { + $amount = rand($amounts['large'][0], $amounts['large'][1]); + } + + Deposit::create([ + 'tenant_id' => $tenantId, + 'deposit_date' => sprintf('%04d-%02d-%02d', $year, $month, $day), + 'client_id' => $client?->id, + 'client_name' => $client ? null : $clientName, + 'bank_account_id' => $primaryBankId, + 'amount' => $amount, + 'payment_method' => $methods[array_rand($methods)], + 'description' => $clientName . ' 입금', + 'created_by' => $userId, + ]); + + $count++; + } + } + + $this->command->info(' ✓ deposits: ' . $count . '건 생성'); + } +} +``` + +#### 4.7.5 DummyWithdrawalSeeder.php +```php +whereIn('client_type', ['PURCHASE', 'BOTH']) + ->get() + ->keyBy('name'); + + // 은행계좌 + $primaryBankId = BankAccount::where('tenant_id', $tenantId) + ->where('is_primary', true) + ->value('id'); + + // 결제수단 분포 + $methods = array_merge( + array_fill(0, 14, 'transfer'), + array_fill(0, 3, 'card'), + array_fill(0, 2, 'cash'), + array_fill(0, 1, 'check') + ); + + // 매입처 순환 목록 + $clientNames = [ + '한화솔루션', '포스코', '롯데케미칼', 'GS칼텍스', '대한항공', + '현대제철', 'SK이노베이션', 'CJ대한통운', '두산에너빌리티', + ]; + + $amounts = [ + 'small' => [1000000, 5000000], + 'medium' => [5000000, 30000000], + 'large' => [30000000, 80000000], + ]; + + $count = 0; + $year = 2025; + + for ($month = 1; $month <= 12; $month++) { + $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + + for ($i = 0; $i < 5; $i++) { + $day = rand(1, $daysInMonth); + $clientName = $clientNames[($month * 5 + $i) % count($clientNames)]; + $client = $clients->get($clientName); + + $rand = rand(1, 100); + if ($rand <= 30) { + $amount = rand($amounts['small'][0], $amounts['small'][1]); + } elseif ($rand <= 80) { + $amount = rand($amounts['medium'][0], $amounts['medium'][1]); + } else { + $amount = rand($amounts['large'][0], $amounts['large'][1]); + } + + Withdrawal::create([ + 'tenant_id' => $tenantId, + 'withdrawal_date' => sprintf('%04d-%02d-%02d', $year, $month, $day), + 'client_id' => $client?->id, + 'client_name' => $client ? null : $clientName, + 'bank_account_id' => $primaryBankId, + 'amount' => $amount, + 'payment_method' => $methods[array_rand($methods)], + 'description' => $clientName . ' 지급', + 'created_by' => $userId, + ]); + + $count++; + } + } + + $this->command->info(' ✓ withdrawals: ' . $count . '건 생성'); + } +} +``` + +#### 4.7.6 DummySaleSeeder.php +```php +whereIn('client_type', ['SALES', 'BOTH']) + ->get() + ->keyBy('name'); + + $clientNames = [ + '삼성전자', 'LG전자', 'SK하이닉스', '현대자동차', '네이버', + '카카오', '쿠팡', '토스', '배달의민족', '당근마켓', + '두산에너빌리티', 'CJ대한통운', '삼성SDS', + ]; + + $amounts = [ + 'small' => [1000000, 5000000], + 'medium' => [5000000, 30000000], + 'large' => [30000000, 100000000], + ]; + + // 월별 건수: 1~10월 6~7건, 11월 8건, 12월 7건 = 80건 + $monthlyCount = [6, 6, 7, 6, 7, 6, 7, 6, 7, 7, 8, 7]; + + $count = 0; + $year = 2025; + + for ($month = 1; $month <= 12; $month++) { + $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + $salesCount = $monthlyCount[$month - 1]; + + for ($i = 0; $i < $salesCount; $i++) { + $day = (int) (($i + 1) * $daysInMonth / ($salesCount + 1)); + $day = max(1, min($day, $daysInMonth)); + + $clientName = $clientNames[($count) % count($clientNames)]; + $client = $clients->get($clientName); + + // 금액 결정 + $rand = rand(1, 100); + if ($rand <= 30) { + $supply = rand($amounts['small'][0], $amounts['small'][1]); + } elseif ($rand <= 80) { + $supply = rand($amounts['medium'][0], $amounts['medium'][1]); + } else { + $supply = rand($amounts['large'][0], $amounts['large'][1]); + } + + // 상태 결정: 1~10월 invoiced/confirmed, 11~12월 draft 포함 + if ($month <= 10) { + $status = rand(0, 1) ? 'invoiced' : 'confirmed'; + } elseif ($month == 11) { + $status = $i < 5 ? (rand(0, 1) ? 'invoiced' : 'confirmed') : 'draft'; + } else { + $status = $i < 1 ? 'confirmed' : 'draft'; + } + + $tax = round($supply * 0.1); + $total = $supply + $tax; + + Sale::create([ + 'tenant_id' => $tenantId, + 'sale_number' => sprintf('SAL-%04d%02d-%04d', $year, $month, $i + 1), + 'sale_date' => sprintf('%04d-%02d-%02d', $year, $month, $day), + 'client_id' => $client->id, + 'supply_amount' => $supply, + 'tax_amount' => $tax, + 'total_amount' => $total, + 'description' => $clientName . ' 매출', + 'status' => $status, + 'created_by' => $userId, + ]); + + $count++; + } + } + + $this->command->info(' ✓ sales: ' . $count . '건 생성'); + } +} +``` + +#### 4.7.7 DummyPurchaseSeeder.php +```php +whereIn('client_type', ['PURCHASE', 'BOTH']) + ->get() + ->keyBy('name'); + + $clientNames = [ + '한화솔루션', '포스코', '롯데케미칼', 'GS칼텍스', '대한항공', + '현대제철', 'SK이노베이션', 'CJ대한통운', '두산에너빌리티', + ]; + + $amounts = [ + 'small' => [1000000, 5000000], + 'medium' => [5000000, 30000000], + 'large' => [30000000, 80000000], + ]; + + // 월별 건수: 1~10월 5~6건, 11월 7건, 12월 6건 = 70건 + $monthlyCount = [5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 7, 6]; + + $count = 0; + $year = 2025; + + for ($month = 1; $month <= 12; $month++) { + $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + $purchaseCount = $monthlyCount[$month - 1]; + + for ($i = 0; $i < $purchaseCount; $i++) { + $day = (int) (($i + 1) * $daysInMonth / ($purchaseCount + 1)); + $day = max(1, min($day, $daysInMonth)); + + $clientName = $clientNames[($count) % count($clientNames)]; + $client = $clients->get($clientName); + + $rand = rand(1, 100); + if ($rand <= 30) { + $supply = rand($amounts['small'][0], $amounts['small'][1]); + } elseif ($rand <= 80) { + $supply = rand($amounts['medium'][0], $amounts['medium'][1]); + } else { + $supply = rand($amounts['large'][0], $amounts['large'][1]); + } + + // 상태 결정 + if ($month <= 10) { + $status = 'confirmed'; + } elseif ($month == 11) { + $status = $i < 4 ? 'confirmed' : 'draft'; + } else { + $status = $i < 1 ? 'confirmed' : 'draft'; + } + + $tax = round($supply * 0.1); + $total = $supply + $tax; + + Purchase::create([ + 'tenant_id' => $tenantId, + 'purchase_number' => sprintf('PUR-%04d%02d-%04d', $year, $month, $i + 1), + 'purchase_date' => sprintf('%04d-%02d-%02d', $year, $month, $day), + 'client_id' => $client->id, + 'supply_amount' => $supply, + 'tax_amount' => $tax, + 'total_amount' => $total, + 'description' => $clientName . ' 매입', + 'status' => $status, + 'created_by' => $userId, + ]); + + $count++; + } + } + + $this->command->info(' ✓ purchases: ' . $count . '건 생성'); + } +} +``` + +#### 4.7.8 DummyBillSeeder.php +```php +get()->keyBy('name'); + + // 은행계좌 (대표계좌) + $primaryBankId = BankAccount::where('tenant_id', $tenantId) + ->where('is_primary', true) + ->value('id'); + + // 수취 어음 데이터 (received) - 15건 + $receivedBills = [ + ['bill_number' => '202501000001', 'client' => '삼성전자', 'amount' => 50000000, 'issue_date' => '2025-01-15', 'maturity_date' => '2025-04-15', 'status' => 'paymentComplete'], + ['bill_number' => '202501000002', 'client' => 'LG전자', 'amount' => 35000000, 'issue_date' => '2025-02-10', 'maturity_date' => '2025-05-10', 'status' => 'paymentComplete'], + ['bill_number' => '202502000001', 'client' => 'SK하이닉스', 'amount' => 80000000, 'issue_date' => '2025-02-20', 'maturity_date' => '2025-05-20', 'status' => 'paymentComplete'], + ['bill_number' => '202503000001', 'client' => '현대자동차', 'amount' => 45000000, 'issue_date' => '2025-03-05', 'maturity_date' => '2025-06-05', 'status' => 'maturityResult'], + ['bill_number' => '202504000001', 'client' => '네이버', 'amount' => 25000000, 'issue_date' => '2025-04-12', 'maturity_date' => '2025-07-12', 'status' => 'maturityResult'], + ['bill_number' => '202505000001', 'client' => '카카오', 'amount' => 18000000, 'issue_date' => '2025-05-08', 'maturity_date' => '2025-08-08', 'status' => 'stored'], + ['bill_number' => '202506000001', 'client' => '쿠팡', 'amount' => 32000000, 'issue_date' => '2025-06-15', 'maturity_date' => '2025-09-15', 'status' => 'stored'], + ['bill_number' => '202507000001', 'client' => '삼성SDS', 'amount' => 65000000, 'issue_date' => '2025-07-20', 'maturity_date' => '2025-10-20', 'status' => 'stored'], + ['bill_number' => '202508000001', 'client' => '토스', 'amount' => 15000000, 'issue_date' => '2025-08-10', 'maturity_date' => '2025-11-10', 'status' => 'stored'], + ['bill_number' => '202509000001', 'client' => '두산에너빌리티', 'amount' => 55000000, 'issue_date' => '2025-09-05', 'maturity_date' => '2025-12-05', 'status' => 'maturityAlert'], + ['bill_number' => '202510000001', 'client' => '삼성전자', 'amount' => 42000000, 'issue_date' => '2025-10-15', 'maturity_date' => '2026-01-15', 'status' => 'stored'], + ['bill_number' => '202511000001', 'client' => 'LG전자', 'amount' => 28000000, 'issue_date' => '2025-11-08', 'maturity_date' => '2026-02-08', 'status' => 'stored'], + ['bill_number' => '202511000002', 'client' => '네이버', 'amount' => 38000000, 'issue_date' => '2025-11-20', 'maturity_date' => '2026-02-20', 'status' => 'stored'], + ['bill_number' => '202512000001', 'client' => '현대자동차', 'amount' => 52000000, 'issue_date' => '2025-12-10', 'maturity_date' => '2026-03-10', 'status' => 'stored'], + ['bill_number' => '202512000002', 'client' => 'SK하이닉스', 'amount' => 70000000, 'issue_date' => '2025-12-18', 'maturity_date' => '2026-03-18', 'status' => 'stored'], + ]; + + // 발행 어음 데이터 (issued) - 15건 + $issuedBills = [ + ['bill_number' => '202501100001', 'client' => '한화솔루션', 'amount' => 40000000, 'issue_date' => '2025-01-20', 'maturity_date' => '2025-04-20', 'status' => 'collectionComplete'], + ['bill_number' => '202502100001', 'client' => '포스코', 'amount' => 55000000, 'issue_date' => '2025-02-15', 'maturity_date' => '2025-05-15', 'status' => 'collectionComplete'], + ['bill_number' => '202503100001', 'client' => '롯데케미칼', 'amount' => 30000000, 'issue_date' => '2025-03-10', 'maturity_date' => '2025-06-10', 'status' => 'collectionComplete'], + ['bill_number' => '202504100001', 'client' => 'GS칼텍스', 'amount' => 22000000, 'issue_date' => '2025-04-18', 'maturity_date' => '2025-07-18', 'status' => 'collectionComplete'], + ['bill_number' => '202505100001', 'client' => '대한항공', 'amount' => 18000000, 'issue_date' => '2025-05-12', 'maturity_date' => '2025-08-12', 'status' => 'collectionRequest'], + ['bill_number' => '202506100001', 'client' => '현대제철', 'amount' => 48000000, 'issue_date' => '2025-06-20', 'maturity_date' => '2025-09-20', 'status' => 'collectionRequest'], + ['bill_number' => '202507100001', 'client' => 'SK이노베이션', 'amount' => 35000000, 'issue_date' => '2025-07-15', 'maturity_date' => '2025-10-15', 'status' => 'stored'], + ['bill_number' => '202508100001', 'client' => 'CJ대한통운', 'amount' => 25000000, 'issue_date' => '2025-08-22', 'maturity_date' => '2025-11-22', 'status' => 'stored'], + ['bill_number' => '202509100001', 'client' => '두산에너빌리티', 'amount' => 60000000, 'issue_date' => '2025-09-10', 'maturity_date' => '2025-12-10', 'status' => 'maturityAlert'], + ['bill_number' => '202510100001', 'client' => '한화솔루션', 'amount' => 45000000, 'issue_date' => '2025-10-08', 'maturity_date' => '2026-01-08', 'status' => 'stored'], + ['bill_number' => '202511100001', 'client' => '포스코', 'amount' => 58000000, 'issue_date' => '2025-11-05', 'maturity_date' => '2026-02-05', 'status' => 'stored'], + ['bill_number' => '202511100002', 'client' => '롯데케미칼', 'amount' => 32000000, 'issue_date' => '2025-11-18', 'maturity_date' => '2026-02-18', 'status' => 'stored'], + ['bill_number' => '202512100001', 'client' => 'GS칼텍스', 'amount' => 28000000, 'issue_date' => '2025-12-05', 'maturity_date' => '2026-03-05', 'status' => 'stored'], + ['bill_number' => '202512100002', 'client' => '현대제철', 'amount' => 42000000, 'issue_date' => '2025-12-15', 'maturity_date' => '2026-03-15', 'status' => 'stored'], + ['bill_number' => '202512100003', 'client' => 'SK이노베이션', 'amount' => 38000000, 'issue_date' => '2025-12-22', 'maturity_date' => '2026-03-22', 'status' => 'stored'], + ]; + + // 차수 관리 데이터 + $installmentsData = [ + '202501000001' => [ + ['date' => '2025-02-15', 'amount' => 25000000, 'note' => '1차 분할 입금'], + ['date' => '2025-03-15', 'amount' => 25000000, 'note' => '2차 분할 입금'], + ], + '202502000001' => [ + ['date' => '2025-03-20', 'amount' => 40000000, 'note' => '1차 분할 입금'], + ['date' => '2025-04-20', 'amount' => 40000000, 'note' => '2차 분할 입금'], + ], + '202507000001' => [ + ['date' => '2025-08-20', 'amount' => 30000000, 'note' => '1차 분할 입금'], + ['date' => '2025-09-20', 'amount' => 35000000, 'note' => '2차 분할 입금'], + ], + '202501100001' => [ + ['date' => '2025-02-20', 'amount' => 20000000, 'note' => '1차 분할 지급'], + ['date' => '2025-03-20', 'amount' => 20000000, 'note' => '2차 분할 지급'], + ], + '202502100001' => [ + ['date' => '2025-03-15', 'amount' => 27500000, 'note' => '1차 분할 지급'], + ['date' => '2025-04-15', 'amount' => 27500000, 'note' => '2차 분할 지급'], + ], + '202506100001' => [ + ['date' => '2025-07-20', 'amount' => 24000000, 'note' => '1차 분할 지급'], + ['date' => '2025-08-20', 'amount' => 24000000, 'note' => '2차 분할 지급'], + ], + ]; + + $billCount = 0; + $installmentCount = 0; + + // 수취 어음 생성 + foreach ($receivedBills as $data) { + $client = $clients->get($data['client']); + $bill = Bill::create([ + 'tenant_id' => $tenantId, + 'bill_number' => $data['bill_number'], + 'bill_type' => 'received', + 'client_id' => $client?->id, + 'client_name' => $client ? null : $data['client'], + 'amount' => $data['amount'], + 'issue_date' => $data['issue_date'], + 'maturity_date' => $data['maturity_date'], + 'status' => $data['status'], + 'is_electronic' => rand(0, 1) === 1, + 'bank_account_id' => $primaryBankId, + 'installment_count' => isset($installmentsData[$data['bill_number']]) ? count($installmentsData[$data['bill_number']]) : 0, + 'created_by' => $userId, + ]); + $billCount++; + + // 차수 관리 데이터 추가 + if (isset($installmentsData[$data['bill_number']])) { + foreach ($installmentsData[$data['bill_number']] as $instData) { + BillInstallment::create([ + 'bill_id' => $bill->id, + 'installment_date' => $instData['date'], + 'amount' => $instData['amount'], + 'note' => $instData['note'], + ]); + $installmentCount++; + } + } + } + + // 발행 어음 생성 + foreach ($issuedBills as $data) { + $client = $clients->get($data['client']); + $bill = Bill::create([ + 'tenant_id' => $tenantId, + 'bill_number' => $data['bill_number'], + 'bill_type' => 'issued', + 'client_id' => $client?->id, + 'client_name' => $client ? null : $data['client'], + 'amount' => $data['amount'], + 'issue_date' => $data['issue_date'], + 'maturity_date' => $data['maturity_date'], + 'status' => $data['status'], + 'is_electronic' => rand(0, 1) === 1, + 'bank_account_id' => $primaryBankId, + 'installment_count' => isset($installmentsData[$data['bill_number']]) ? count($installmentsData[$data['bill_number']]) : 0, + 'created_by' => $userId, + ]); + $billCount++; + + // 차수 관리 데이터 추가 + if (isset($installmentsData[$data['bill_number']])) { + foreach ($installmentsData[$data['bill_number']] as $instData) { + BillInstallment::create([ + 'bill_id' => $bill->id, + 'installment_date' => $instData['date'], + 'amount' => $instData['amount'], + 'note' => $instData['note'], + ]); + $installmentCount++; + } + } + } + + $this->command->info(' ✓ bills: ' . $billCount . '건 생성'); + $this->command->info(' ✓ bill_installments: ' . $installmentCount . '건 생성'); + } +} +``` + +--- + +## 5. 데이터 검증 + +### 5.1 시딩 후 검증 쿼리 + +```sql +-- 테이블별 데이터 수 확인 +SELECT 'client_groups' as tbl, COUNT(*) as cnt FROM client_groups WHERE tenant_id = 287 +UNION ALL +SELECT 'bank_accounts', COUNT(*) FROM bank_accounts WHERE tenant_id = 287 +UNION ALL +SELECT 'clients', COUNT(*) FROM clients WHERE tenant_id = 287 +UNION ALL +SELECT 'deposits', COUNT(*) FROM deposits WHERE tenant_id = 287 +UNION ALL +SELECT 'withdrawals', COUNT(*) FROM withdrawals WHERE tenant_id = 287 +UNION ALL +SELECT 'bills', COUNT(*) FROM bills WHERE tenant_id = 287 +UNION ALL +SELECT 'bill_installments', COUNT(*) FROM bill_installments WHERE bill_id IN (SELECT id FROM bills WHERE tenant_id = 287) +UNION ALL +SELECT 'sales', COUNT(*) FROM sales WHERE tenant_id = 287 +UNION ALL +SELECT 'purchases', COUNT(*) FROM purchases WHERE tenant_id = 287; + +-- 어음 현황 (수취/발행별) +SELECT bill_type, status, COUNT(*) as count, SUM(amount) as total_amount +FROM bills +WHERE tenant_id = 287 +GROUP BY bill_type, status +ORDER BY bill_type, status; + +-- 월별 매출 현황 +SELECT DATE_FORMAT(sale_date, '%Y-%m') as month, + COUNT(*) as count, + SUM(total_amount) as total +FROM sales +WHERE tenant_id = 287 +GROUP BY month +ORDER BY month; + +-- 거래처별 매출/매입 합계 +SELECT c.name, + COALESCE(SUM(s.total_amount), 0) as total_sales, + COALESCE(SUM(p.total_amount), 0) as total_purchases +FROM clients c +LEFT JOIN sales s ON c.id = s.client_id AND s.deleted_at IS NULL +LEFT JOIN purchases p ON c.id = p.client_id AND p.deleted_at IS NULL +WHERE c.tenant_id = 287 +GROUP BY c.id, c.name +ORDER BY total_sales DESC; +``` + +### 5.2 예상 결과 요약 + +| 항목 | 예상 값 | +|------|---------| +| 총 거래처 | 24개 (기존 4 + 신규 20) | +| 총 매출건수 | 80건 | +| 총 매입건수 | 70건 | +| 총 입금건수 | 60건 | +| 총 출금건수 | 60건 | +| 총 어음건수 | 30건 (수취 15건 + 발행 15건) | +| 총 차수건수 | 12건 (6개 어음 × 2차) | +| 연간 매출액 | 약 25~30억원 | +| 연간 매입액 | 약 18~22억원 | +| 수취어음 총액 | 약 6.5억원 | +| 발행어음 총액 | 약 5.8억원 | + +--- + +## 6. 변경 이력 + +| 날짜 | 내용 | 작성자 | +|------|------|--------| +| 2025-12-23 | 문서 초안 작성 | Claude | +| 2025-12-23 | 1년치 300개 데이터로 확장 | Claude | +| 2025-12-23 | DB 스키마 상세 정의 추가 (4.4) | Claude | +| 2025-12-23 | 모델 경로 및 네임스페이스 추가 (4.5) | Claude | +| 2025-12-23 | 필수 상수값 정의 추가 (4.6) | Claude | +| 2025-12-23 | 완전한 Seeder 코드 구현 추가 (4.7) | Claude | +| 2025-12-23 | 어음(bills) 더미 데이터 추가 (30건 + 차수 12건) | Claude | + +--- + +## 7. 다음 단계 + +1. [x] Seeder 파일 구현 → 4.7 섹션에 완전한 코드 포함 +2. [ ] Seeder 파일 생성 (코드 복사 후 파일 생성) +3. [ ] 시딩 실행 (`php artisan db:seed --class=DummyDataSeeder`) +4. [ ] 데이터 검증 (5.1 검증 쿼리 실행) +5. [ ] React 연동 테스트 +6. [ ] 추가 데이터 필요시 확장 + +--- + +## 8. 빠른 시작 가이드 + +### 8.1 Seeder 파일 생성 순서 + +```bash +# 1. Dummy 디렉토리 생성 +mkdir -p api/database/seeders/Dummy + +# 2. 파일 생성 (4.2 ~ 4.7 코드 복사) +# - api/database/seeders/DummyDataSeeder.php +# - api/database/seeders/Dummy/DummyClientGroupSeeder.php +# - api/database/seeders/Dummy/DummyBankAccountSeeder.php +# - api/database/seeders/Dummy/DummyClientSeeder.php +# - api/database/seeders/Dummy/DummyDepositSeeder.php +# - api/database/seeders/Dummy/DummyWithdrawalSeeder.php +# - api/database/seeders/Dummy/DummySaleSeeder.php +# - api/database/seeders/Dummy/DummyPurchaseSeeder.php + +# 3. 시딩 실행 +cd api +php artisan db:seed --class=DummyDataSeeder + +# 4. 검증 +php artisan tinker +>>> \App\Models\Tenants\Sale::where('tenant_id', 287)->count() +# 결과: 80 +``` + +### 8.2 주요 참조 정보 + +| 항목 | 값 | +|------|-----| +| **대상 테넌트 ID** | 287 | +| **생성자 사용자 ID** | 1 | +| **데이터 기간** | 2025년 1월 ~ 12월 | +| **총 레코드 수** | ~300개 | + +### 8.3 관련 문서 + +- [React Mock to API Migration Plan](./react-mock-to-api-migration-plan.md) - API 마이그레이션 계획 +- [API Rules](../reference/api-rules.md) - API 개발 규칙 \ No newline at end of file diff --git a/plans/flow-tests/account-management-flow.json b/plans/flow-tests/account-management-flow.json new file mode 100644 index 0000000..e9fdd20 --- /dev/null +++ b/plans/flow-tests/account-management-flow.json @@ -0,0 +1,143 @@ +{ + "name": "계정 관리 플로우", + "description": "이용 약관 조회/동의 → 계정 일시 정지 → 회원 탈퇴 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "get_agreements", + "name": "이용 약관 목록 조회", + "method": "GET", + "endpoint": "/api/v1/account/agreements", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "agreements": "$.data" + } + }, + { + "id": "update_agreements", + "name": "이용 약관 동의 상태 수정", + "method": "PUT", + "endpoint": "/api/v1/account/agreements", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "agreements": [ + { + "type": "terms", + "agreed": true + }, + { + "type": "privacy", + "agreed": true + }, + { + "type": "marketing", + "agreed": false + } + ] + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_agreements", + "name": "약관 동의 상태 확인", + "method": "GET", + "endpoint": "/api/v1/account/agreements", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "suspend_account", + "name": "계정 일시 정지", + "description": "계정을 일시 정지 상태로 변경", + "method": "POST", + "endpoint": "/api/v1/account/suspend", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "reason": "테스트용 일시 정지", + "duration_days": 30 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "withdraw_account", + "name": "회원 탈퇴 (테스트 스킵)", + "description": "회원 탈퇴 API - 실제 실행 시 계정 삭제됨 (주의)", + "method": "POST", + "endpoint": "/api/v1/account/withdraw", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "reason": "서비스 불만족", + "feedback": "테스트용 피드백입니다" + }, + "expect": { + "status": [200, 400, 422], + "jsonPath": { + "$.success": "@isBoolean" + } + }, + "continueOnFailure": true, + "skip": true, + "skipReason": "실제 탈퇴 실행 방지 - 수동 테스트 필요" + } + ] +} diff --git a/plans/flow-tests/attendance-api-crud.json b/plans/flow-tests/attendance-api-crud.json new file mode 100644 index 0000000..4ebbc9d --- /dev/null +++ b/plans/flow-tests/attendance-api-crud.json @@ -0,0 +1,227 @@ +{ + "name": "Attendance API 근태관리 테스트", + "description": "근태 CRUD, 출퇴근 기록, 월간 통계 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}", + "test_date": "{{$date}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.message": "로그인 성공", + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token", + "current_user_id": "$.user.id" + } + }, + { + "id": "check_in", + "name": "출근 기록", + "method": "POST", + "endpoint": "/attendances/check-in", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "check_in": "09:00:00", + "gps_data": { + "latitude": 37.5665, + "longitude": 126.978, + "accuracy": 10 + } + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber", + "$.data.status": "@isString" + } + }, + "extract": { + "attendance_id": "$.data.id" + } + }, + { + "id": "show_attendance", + "name": "근태 상세 조회", + "method": "GET", + "endpoint": "/attendances/{{check_in.attendance_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "{{check_in.attendance_id}}", + "$.data.base_date": "@isString" + } + } + }, + { + "id": "check_out", + "name": "퇴근 기록", + "method": "POST", + "endpoint": "/attendances/check-out", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "check_out": "18:00:00", + "gps_data": { + "latitude": 37.5665, + "longitude": 126.978, + "accuracy": 15 + } + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + } + }, + { + "id": "list_attendances", + "name": "근태 목록 조회", + "method": "GET", + "endpoint": "/attendances", + "query": { + "page": 1, + "per_page": 10, + "date": "{{test_date}}" + }, + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray" + } + } + }, + { + "id": "monthly_stats", + "name": "월간 통계 조회", + "method": "GET", + "endpoint": "/attendances/monthly-stats", + "query": { + "year": 2025, + "month": 12 + }, + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "create_attendance", + "name": "근태 수동 등록 (관리자)", + "method": "POST", + "endpoint": "/attendances", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "user_id": "{{login.current_user_id}}", + "base_date": "2025-12-01", + "status": "onTime", + "json_details": { + "check_in": "09:00:00", + "check_out": "18:00:00", + "work_minutes": 480 + }, + "remarks": "Flow Tester 테스트 데이터" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "manual_attendance_id": "$.data.id" + } + }, + { + "id": "update_attendance", + "name": "근태 수정", + "method": "PATCH", + "endpoint": "/attendances/{{create_attendance.manual_attendance_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "status": "late", + "remarks": "수정된 테스트 데이터" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "late" + } + } + }, + { + "id": "delete_manual_attendance", + "name": "수동 등록 근태 삭제", + "method": "DELETE", + "endpoint": "/attendances/{{create_attendance.manual_attendance_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "delete_checkin_attendance", + "name": "출퇴근 기록 삭제 (정리)", + "method": "DELETE", + "endpoint": "/attendances/{{check_in.attendance_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] +} diff --git a/plans/flow-tests/auth-legacy-flow.json b/plans/flow-tests/auth-legacy-flow.json new file mode 100644 index 0000000..48aac9c --- /dev/null +++ b/plans/flow-tests/auth-legacy-flow.json @@ -0,0 +1,85 @@ +{ + "name": "인증 플로우 테스트", + "description": "로그인, 프로필 조회, 토큰 갱신, 로그아웃 플로우를 테스트합니다.", + "version": "1.0", + "config": { + "apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a", + "baseUrl": "https://api.sam.kr/api/v1", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "codebridgex", + "user_pwd": "code1234" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token", + "refreshToken": "$.refresh_token" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + } + }, + { + "id": "get_profile", + "name": "프로필 조회", + "method": "GET", + "endpoint": "/users/me", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "refresh_token", + "name": "토큰 갱신", + "method": "POST", + "endpoint": "/refresh", + "body": { + "refresh_token": "{{login.refreshToken}}" + }, + "dependsOn": ["get_profile"], + "extract": { + "newToken": "$.access_token" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + } + }, + { + "id": "logout", + "name": "로그아웃", + "method": "POST", + "endpoint": "/logout", + "headers": { + "Authorization": "Bearer {{refresh_token.newToken}}" + }, + "dependsOn": ["refresh_token"], + "expect": { + "status": [200, 204] + } + } + ] +} diff --git a/plans/flow-tests/bad-debt-flow.json b/plans/flow-tests/bad-debt-flow.json new file mode 100644 index 0000000..7cc2f95 --- /dev/null +++ b/plans/flow-tests/bad-debt-flow.json @@ -0,0 +1,233 @@ +{ + "name": "부실채권 관리 플로우", + "description": "부실채권 등록 → 문서 첨부 → 메모 추가 → 상태 변경 → 삭제 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "list_bad_debts", + "name": "부실채권 목록 조회", + "method": "GET", + "endpoint": "/api/v1/bad-debts", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "page": 1, + "per_page": 10 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "existing_count": "$.data.total" + } + }, + { + "id": "create_bad_debt", + "name": "부실채권 등록", + "method": "POST", + "endpoint": "/api/v1/bad-debts", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "client_id": 1, + "sale_id": 1, + "amount": 5000000, + "occurred_at": "2025-01-01", + "reason": "연체 90일 초과", + "status": "pending", + "description": "Flow 테스트용 부실채권" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "bad_debt_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "get_bad_debt_detail", + "name": "부실채권 상세 조회", + "method": "GET", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "continueOnFailure": true + }, + { + "id": "add_document", + "name": "관련 문서 첨부", + "method": "POST", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/documents", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "file_id": 1, + "document_type": "collection_notice", + "description": "독촉장 발송 증빙" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "list_documents", + "name": "첨부 문서 목록 조회", + "method": "GET", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/documents", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "add_memo", + "name": "메모 추가", + "method": "POST", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/memos", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "content": "1차 독촉장 발송 완료", + "memo_type": "collection" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "memo_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "list_memos", + "name": "메모 목록 조회", + "method": "GET", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/memos", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "toggle_status", + "name": "상태 토글 (처리중)", + "method": "POST", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/toggle", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "status": "in_progress" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "update_bad_debt", + "name": "부실채권 수정", + "method": "PUT", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "description": "Flow 테스트 - 수정됨", + "status": "resolved" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "delete_bad_debt", + "name": "부실채권 삭제", + "method": "DELETE", + "endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + } + ] +} diff --git a/plans/flow-tests/branching-example-flow.json b/plans/flow-tests/branching-example-flow.json new file mode 100644 index 0000000..ff4cfc5 --- /dev/null +++ b/plans/flow-tests/branching-example-flow.json @@ -0,0 +1,166 @@ +{ + "$schema": "flow-tester-schema.json", + "name": "분기 테스트 플로우 예시", + "description": "Flow Tester의 조건 분기 기능을 보여주는 예시 플로우입니다. 로그인 성공/실패에 따른 분기, 권한에 따른 분기, 조건부 의존성 등을 테스트합니다.", + "version": "1.0.0", + "author": "SAM Team", + "category": "examples", + "tags": ["branching", "condition", "if-else", "example"], + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": false + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "1. 로그인 시도", + "description": "사용자 인증을 시도합니다", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "expect": { + "status": [200, 401] + }, + "extract": { + "token": "$.access_token", + "role": "$.user.role", + "permissions": "$.user.permissions" + }, + "continueOnFailure": true + }, + + { + "id": "login_success_path", + "name": "2-A. 로그인 성공 처리", + "description": "로그인이 성공했을 때만 실행됩니다", + "condition": {"stepResult": "login", "is": "success"}, + "dependsOn": ["login"], + "method": "GET", + "endpoint": "/api/v1/users/me", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200] + } + }, + + { + "id": "login_failure_path", + "name": "2-B. 로그인 실패 처리", + "description": "로그인이 실패했을 때만 실행됩니다", + "condition": {"stepResult": "login", "is": "failure"}, + "dependsOn": [{"step": "login", "onlyIf": "any"}], + "method": "POST", + "endpoint": "/api/v1/auth/password-reset-request", + "body": { + "email": "{{variables.user_id}}" + }, + "expect": { + "status": [200, 404] + }, + "continueOnFailure": true + }, + + { + "id": "admin_dashboard", + "name": "3-A. 관리자 대시보드", + "description": "관리자 권한이 있을 때만 실행됩니다", + "condition": "{{login.role}} == 'admin'", + "dependsOn": ["login_success_path"], + "method": "GET", + "endpoint": "/api/v1/admin/dashboard", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200] + } + }, + + { + "id": "user_dashboard", + "name": "3-B. 일반 사용자 대시보드", + "description": "일반 사용자 권한일 때만 실행됩니다", + "condition": "{{login.role}} == 'user'", + "dependsOn": ["login_success_path"], + "method": "GET", + "endpoint": "/api/v1/user/dashboard", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200] + } + }, + + { + "id": "premium_features", + "name": "3-C. 프리미엄 기능", + "description": "프리미엄 권한이 있을 때만 실행됩니다", + "condition": { + "and": [ + {"stepResult": "login", "is": "success"}, + {"left": "{{login.permissions}}", "op": "contains", "right": "premium"} + ] + }, + "dependsOn": ["login_success_path"], + "method": "GET", + "endpoint": "/api/v1/premium/features", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200] + } + }, + + { + "id": "create_report", + "name": "4. 리포트 생성 (관리자만)", + "description": "관리자만 리포트를 생성할 수 있습니다", + "condition": {"stepResult": "admin_dashboard", "is": "success"}, + "dependsOn": [{"step": "admin_dashboard", "onlyIf": "executed"}], + "method": "POST", + "endpoint": "/api/v1/admin/reports", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "type": "daily", + "date": "{{$faker.date}}" + }, + "expect": { + "status": [201] + } + }, + + { + "id": "cleanup_always", + "name": "5. 정리 작업 (항상 실행)", + "description": "성공/실패와 무관하게 항상 실행되는 정리 작업입니다", + "dependsOn": [ + {"step": "login", "onlyIf": "any"}, + {"step": "admin_dashboard", "onlyIf": "any"}, + {"step": "user_dashboard", "onlyIf": "any"} + ], + "method": "POST", + "endpoint": "/api/v1/logout", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200, 401] + }, + "continueOnFailure": true + } + ] +} \ No newline at end of file diff --git a/plans/flow-tests/client-legacy-flow.json b/plans/flow-tests/client-legacy-flow.json new file mode 100644 index 0000000..ab3c254 --- /dev/null +++ b/plans/flow-tests/client-legacy-flow.json @@ -0,0 +1,215 @@ +{ + "name": "Client API CRUD 테스트", + "description": "거래처(Client) API 전체 CRUD 테스트 - 생성, 조회, 수정, 토글, 삭제 포함. business_no, business_type, business_item 신규 필드 검증 포함.", + "version": "1.0", + "config": { + "baseUrl": "https://api.sam.kr/api/v1", + "apiKey": "{{$env.FLOW_TESTER_API_KEY}}", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}", + "test_client_code": "TEST_CLIENT_{{$timestamp}}" + }, + "steps": [ + { + "id": "login", + "name": "1. 로그인 - 토큰 획득", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "create_client", + "name": "2. 거래처 생성 (신규 필드 포함)", + "method": "POST", + "endpoint": "/clients", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "client_code": "{{variables.test_client_code}}", + "name": "테스트 거래처", + "contact_person": "홍길동", + "phone": "02-1234-5678", + "email": "test@example.com", + "address": "서울시 강남구 테헤란로 123", + "business_no": "123-45-67890", + "business_type": "제조업", + "business_item": "전자부품", + "is_active": "Y" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber", + "$.data.client_code": "{{variables.test_client_code}}", + "$.data.name": "테스트 거래처", + "$.data.business_no": "123-45-67890", + "$.data.business_type": "제조업", + "$.data.business_item": "전자부품" + } + }, + "extract": { + "client_id": "$.data.id" + } + }, + { + "id": "list_clients", + "name": "3. 거래처 목록 조회", + "method": "GET", + "endpoint": "/clients?page=1&size=20&q=테스트", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray", + "$.data.current_page": 1 + } + } + }, + { + "id": "show_client", + "name": "4. 거래처 단건 조회", + "method": "GET", + "endpoint": "/clients/{{create_client.client_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "{{create_client.client_id}}", + "$.data.client_code": "{{variables.test_client_code}}", + "$.data.business_no": "123-45-67890", + "$.data.business_type": "제조업", + "$.data.business_item": "전자부품" + } + } + }, + { + "id": "update_client", + "name": "5. 거래처 수정 (신규 필드 변경)", + "method": "PUT", + "endpoint": "/clients/{{create_client.client_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "name": "테스트 거래처 (수정됨)", + "contact_person": "김철수", + "business_no": "987-65-43210", + "business_type": "도소매업", + "business_item": "IT솔루션" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.name": "테스트 거래처 (수정됨)", + "$.data.contact_person": "김철수", + "$.data.business_no": "987-65-43210", + "$.data.business_type": "도소매업", + "$.data.business_item": "IT솔루션" + } + } + }, + { + "id": "toggle_client", + "name": "6. 거래처 활성/비활성 토글 (N으로)", + "method": "PATCH", + "endpoint": "/clients/{{create_client.client_id}}/toggle", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.is_active": false + } + } + }, + { + "id": "toggle_client_back", + "name": "7. 거래처 토글 복원 (Y로)", + "method": "PATCH", + "endpoint": "/clients/{{create_client.client_id}}/toggle", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.is_active": true + } + } + }, + { + "id": "list_active_only", + "name": "8. 활성 거래처만 조회", + "method": "GET", + "endpoint": "/clients?only_active=1", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray" + } + } + }, + { + "id": "delete_client", + "name": "9. 거래처 삭제", + "method": "DELETE", + "endpoint": "/clients/{{create_client.client_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_deleted", + "name": "10. 삭제 확인 (404 예상)", + "method": "GET", + "endpoint": "/clients/{{create_client.client_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [404], + "jsonPath": { + "$.success": false + } + } + } + ] +} \ No newline at end of file diff --git a/plans/flow-tests/company-request-flow.json b/plans/flow-tests/company-request-flow.json new file mode 100644 index 0000000..3551bb0 --- /dev/null +++ b/plans/flow-tests/company-request-flow.json @@ -0,0 +1,200 @@ +{ + "name": "회사 가입 신청 플로우", + "description": "사업자번호 확인 → 가입 신청 → 내 신청 목록 → 승인/거절 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}", + "test_business_number": "123-45-67890" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "check_business_number", + "name": "사업자번호 확인", + "description": "가입 가능한 회사인지 사업자번호로 확인", + "method": "POST", + "endpoint": "/api/v1/companies/check", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "business_number": "{{test_business_number}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "company_exists": "$.data.exists", + "company_id": "$.data.company_id" + } + }, + { + "id": "submit_request", + "name": "회사 가입 신청", + "method": "POST", + "endpoint": "/api/v1/companies/request", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "company_id": "{{check_business_number.company_id}}", + "department": "개발팀", + "position": "개발자", + "message": "Flow 테스트용 가입 신청입니다." + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "request_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "get_my_requests", + "name": "내 신청 목록 조회", + "method": "GET", + "endpoint": "/api/v1/companies/my-requests", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "get_request_detail", + "name": "신청 상세 조회", + "method": "GET", + "endpoint": "/api/v1/companies/requests/{{submit_request.request_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "continueOnFailure": true + }, + { + "id": "approve_request", + "name": "신청 승인 (관리자)", + "description": "관리자 권한으로 가입 신청 승인", + "method": "POST", + "endpoint": "/api/v1/companies/requests/{{submit_request.request_id}}/approve", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "role_id": 2, + "department_id": 1, + "approved_message": "승인되었습니다." + }, + "expect": { + "status": [200, 403], + "jsonPath": { + "$.success": "@isBoolean" + } + }, + "continueOnFailure": true + }, + { + "id": "submit_another_request", + "name": "거절 테스트용 신청", + "method": "POST", + "endpoint": "/api/v1/companies/request", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "company_id": "{{check_business_number.company_id}}", + "department": "영업팀", + "position": "매니저", + "message": "거절 테스트용 신청입니다." + }, + "expect": { + "status": [200, 201, 400, 422], + "jsonPath": { + "$.success": "@isBoolean" + } + }, + "extract": { + "reject_request_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "reject_request", + "name": "신청 거절 (관리자)", + "description": "관리자 권한으로 가입 신청 거절", + "method": "POST", + "endpoint": "/api/v1/companies/requests/{{submit_another_request.reject_request_id}}/reject", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "reason": "테스트 거절 사유" + }, + "expect": { + "status": [200, 403], + "jsonPath": { + "$.success": "@isBoolean" + } + }, + "continueOnFailure": true + }, + { + "id": "verify_rejection", + "name": "거절 상태 확인", + "method": "GET", + "endpoint": "/api/v1/companies/requests/{{submit_another_request.reject_request_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "rejected" + } + }, + "continueOnFailure": true + } + ] +} diff --git a/plans/flow-tests/department-tree-api.json b/plans/flow-tests/department-tree-api.json new file mode 100644 index 0000000..503e11c --- /dev/null +++ b/plans/flow-tests/department-tree-api.json @@ -0,0 +1,87 @@ +{ + "name": "Department Tree API 테스트", + "description": "부서 트리 조회 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.message": "로그인 성공", + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "get_tree", + "name": "부서 트리 조회", + "method": "GET", + "endpoint": "/departments/tree", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data": "@isArray" + } + } + }, + { + "id": "get_tree_with_users", + "name": "부서 트리 조회 (사용자 포함)", + "method": "GET", + "endpoint": "/departments/tree", + "query": { + "with_users": true + }, + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data": "@isArray" + } + } + }, + { + "id": "list_departments", + "name": "부서 목록 조회", + "method": "GET", + "endpoint": "/departments", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data": "@isArray" + } + } + } + ] +} diff --git a/plans/flow-tests/employee-api-crud.json b/plans/flow-tests/employee-api-crud.json new file mode 100644 index 0000000..9eaf608 --- /dev/null +++ b/plans/flow-tests/employee-api-crud.json @@ -0,0 +1,188 @@ +{ + "name": "Employee API CRUD 테스트", + "description": "사원 관리 API 전체 CRUD 및 계정 생성 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}", + "test_email": "test.employee.{{$timestamp}}@example.com", + "test_name": "테스트사원{{$random:4}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.message": "로그인 성공", + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token", + "current_user_id": "$.user.id" + } + }, + { + "id": "get_stats", + "name": "사원 통계 조회", + "method": "GET", + "endpoint": "/employees/stats", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.total": "@isNumber", + "$.data.active": "@isNumber", + "$.data.leave": "@isNumber", + "$.data.resigned": "@isNumber" + } + } + }, + { + "id": "list_employees", + "name": "사원 목록 조회", + "method": "GET", + "endpoint": "/employees", + "query": { + "page": 1, + "per_page": 10 + }, + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray" + } + } + }, + { + "id": "create_employee", + "name": "사원 등록", + "method": "POST", + "endpoint": "/employees", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "name": "{{test_name}}", + "email": "{{test_email}}", + "phone": "010-1234-5678", + "employee_number": "EMP{{$random:6}}", + "employee_status": "active", + "position": "사원", + "hire_date": "{{$date}}", + "json_extra": { + "emergency_contact": "010-9999-8888", + "address": "서울시 강남구" + } + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber", + "$.data.user.name": "{{test_name}}" + } + }, + "extract": { + "employee_id": "$.data.id", + "user_id": "$.data.user_id" + } + }, + { + "id": "show_employee", + "name": "사원 상세 조회", + "method": "GET", + "endpoint": "/employees/{{create_employee.employee_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "{{create_employee.employee_id}}", + "$.data.employee_status": "active" + } + } + }, + { + "id": "update_employee", + "name": "사원 정보 수정", + "method": "PATCH", + "endpoint": "/employees/{{create_employee.employee_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "position": "대리", + "employee_status": "active", + "json_extra": { + "emergency_contact": "010-1111-2222", + "address": "서울시 서초구", + "skills": ["Laravel", "React"] + } + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + } + }, + { + "id": "list_filtered", + "name": "사원 필터 조회 (재직자)", + "method": "GET", + "endpoint": "/employees", + "query": { + "status": "active", + "q": "{{test_name}}" + }, + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "delete_employee", + "name": "사원 삭제", + "method": "DELETE", + "endpoint": "/employees/{{create_employee.employee_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] +} diff --git a/plans/flow-tests/item-delete-legacy-flow.json b/plans/flow-tests/item-delete-legacy-flow.json new file mode 100644 index 0000000..e03e817 --- /dev/null +++ b/plans/flow-tests/item-delete-legacy-flow.json @@ -0,0 +1,287 @@ +{ + "name": "품목 삭제 API 테스트", + "description": "품목 삭제 시 참조 무결성 체크 및 soft delete 동작을 테스트합니다.", + "version": "1.0", + "config": { + "apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a", + "baseUrl": "https://api.sam.kr/api/v1", + "timeout": 30000, + "stopOnFailure": false + }, + "variables": { + "user_id": "codebridgex", + "user_pwd": "code1234", + "testProductCode": "TEST-DEL-001", + "testProductName": "삭제 테스트용 품목", + "testBomParentCode": "TEST-BOM-PARENT-001", + "testBomParentName": "BOM 부모 품목" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "description": "API 테스트를 위한 로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token", + "refreshToken": "$.refresh_token" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + } + }, + { + "id": "create_product_for_delete", + "name": "삭제 테스트용 품목 생성", + "description": "단순 삭제 테스트용 품목 생성 (BOM에 미사용)", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "code": "{{variables.testProductCode}}", + "name": "{{variables.testProductName}}", + "unit": "EA", + "product_type": "FG", + "is_active": true + }, + "dependsOn": ["login"], + "extract": { + "createdProductId": "$.data.id", + "createdProductCode": "$.data.code" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + } + }, + { + "id": "get_items_list", + "name": "품목 목록 조회", + "description": "생성된 품목이 목록에 있는지 확인", + "method": "GET", + "endpoint": "/items?type=FG&search={{create_product_for_delete.createdProductCode}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["create_product_for_delete"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "delete_product_success", + "name": "품목 삭제 (정상)", + "description": "BOM에서 사용되지 않는 품목 삭제 - 성공해야 함", + "method": "DELETE", + "endpoint": "/items/{{create_product_for_delete.createdProductId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["get_items_list"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_deleted_not_in_list", + "name": "삭제된 품목 목록 미포함 확인", + "description": "삭제된 품목이 기본 목록에서 제외되는지 확인", + "method": "GET", + "endpoint": "/items?type=FG&search={{create_product_for_delete.createdProductCode}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["delete_product_success"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "note": "data.total이 0이어야 함 (삭제된 품목 미포함)" + }, + { + "id": "delete_already_deleted_item", + "name": "이미 삭제된 품목 재삭제 시도", + "description": "soft delete된 품목을 다시 삭제하면 404 반환해야 함", + "method": "DELETE", + "endpoint": "/items/{{create_product_for_delete.createdProductId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["verify_deleted_not_in_list"], + "expect": { + "status": [404], + "jsonPath": { + "$.success": false + } + } + }, + { + "id": "create_bom_parent", + "name": "BOM 부모 품목 생성", + "description": "BOM 테스트를 위한 부모 품목 생성", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "code": "{{variables.testBomParentCode}}", + "name": "{{variables.testBomParentName}}", + "unit": "EA", + "product_type": "FG", + "is_active": true + }, + "dependsOn": ["login"], + "extract": { + "bomParentId": "$.data.id" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "create_bom_child", + "name": "BOM 자식 품목 생성", + "description": "BOM 구성품으로 사용될 품목 생성", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "code": "TEST-BOM-CHILD-001", + "name": "BOM 자식 품목", + "unit": "EA", + "product_type": "PT", + "is_active": true + }, + "dependsOn": ["create_bom_parent"], + "extract": { + "bomChildId": "$.data.id" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "add_bom_component", + "name": "BOM 구성품 추가", + "description": "부모 품목에 자식 품목을 BOM으로 등록", + "method": "POST", + "endpoint": "/items/{{create_bom_parent.bomParentId}}/bom", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "items": [ + { + "ref_type": "PRODUCT", + "ref_id": "{{create_bom_child.bomChildId}}", + "quantity": 2, + "sort_order": 1 + } + ] + }, + "dependsOn": ["create_bom_child"], + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "delete_bom_used_item_fail", + "name": "BOM 사용 중인 품목 삭제 시도", + "description": "다른 BOM에서 구성품으로 사용 중인 품목 삭제 - 400 에러 반환해야 함", + "method": "DELETE", + "endpoint": "/items/{{create_bom_child.bomChildId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["add_bom_component"], + "expect": { + "status": [400], + "jsonPath": { + "$.success": false, + "$.message": "@contains:BOM" + } + } + }, + { + "id": "cleanup_bom", + "name": "BOM 구성품 제거", + "description": "테스트 정리 - BOM 구성품 제거", + "method": "DELETE", + "endpoint": "/items/{{create_bom_parent.bomParentId}}/bom", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["delete_bom_used_item_fail"], + "expect": { + "status": [200, 204] + } + }, + { + "id": "delete_bom_child_after_cleanup", + "name": "BOM 해제 후 자식 품목 삭제", + "description": "BOM에서 제거된 품목은 삭제 가능해야 함", + "method": "DELETE", + "endpoint": "/items/{{create_bom_child.bomChildId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["cleanup_bom"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "cleanup_bom_parent", + "name": "BOM 부모 품목 삭제", + "description": "테스트 정리 - 부모 품목 삭제", + "method": "DELETE", + "endpoint": "/items/{{create_bom_parent.bomParentId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["delete_bom_child_after_cleanup"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] +} \ No newline at end of file diff --git a/plans/flow-tests/item-fields-is-active-test.json b/plans/flow-tests/item-fields-is-active-test.json new file mode 100644 index 0000000..450ed15 --- /dev/null +++ b/plans/flow-tests/item-fields-is-active-test.json @@ -0,0 +1,172 @@ +{ + "name": "ItemField is_active 컬럼 검증 테스트", + "description": "item_fields 테이블에 추가된 is_active 컬럼의 기능을 검증합니다. 필드 생성 시 기본값(true), 수정, 조회 시 is_active 필드 포함 여부를 테스트합니다.", + "version": "1.0", + "config": { + "baseUrl": "https://api.sam.kr/api/v1", + "apiKey": "{{$env.FLOW_TESTER_API_KEY}}", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "1. 로그인 - 인증 토큰 획득", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.message": "로그인 성공", + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "get_fields_list", + "name": "2. 필드 목록 조회 - is_active 필드 포함 확인", + "method": "GET", + "endpoint": "/item-master/fields", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data": "@isArray" + } + }, + "extract": { + "existingFieldId": "$.data[0].id", + "fieldCount": "$.data.length" + } + }, + { + "id": "create_field", + "name": "3. 독립 필드 생성 - is_active 기본값 true 확인", + "method": "POST", + "endpoint": "/item-master/fields", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "field_name": "[테스트] is_active 검증 필드", + "field_type": "textbox", + "field_key": "test_is_active", + "is_required": false, + "placeholder": "is_active 기본값 테스트", + "description": "API Flow Tester에서 생성한 테스트 필드" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber", + "$.data.field_name": "[테스트] is_active 검증 필드", + "$.data.is_active": true + } + }, + "extract": { + "newFieldId": "$.data.id" + } + }, + { + "id": "verify_field_created", + "name": "4. 생성된 필드 상세 확인 - is_active=true", + "method": "GET", + "endpoint": "/item-master/fields", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "allFields": "$.data" + } + }, + { + "id": "update_field_inactive", + "name": "5. 필드 비활성화 - is_active=false로 수정", + "method": "PUT", + "endpoint": "/item-master/fields/{{create_field.newFieldId}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "is_active": false + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.is_active": false + } + } + }, + { + "id": "verify_field_inactive", + "name": "6. 비활성화 상태 확인", + "method": "GET", + "endpoint": "/item-master/fields", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "update_field_active", + "name": "7. 필드 재활성화 - is_active=true로 수정", + "method": "PUT", + "endpoint": "/item-master/fields/{{create_field.newFieldId}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "is_active": true + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.is_active": true + } + } + }, + { + "id": "delete_test_field", + "name": "8. 테스트 필드 삭제 (정리)", + "method": "DELETE", + "endpoint": "/item-master/fields/{{create_field.newFieldId}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] +} diff --git a/plans/flow-tests/item-master-legacy-flow.json b/plans/flow-tests/item-master-legacy-flow.json new file mode 100644 index 0000000..77663a7 --- /dev/null +++ b/plans/flow-tests/item-master-legacy-flow.json @@ -0,0 +1,157 @@ +{ + "name": "품목기준관리 통합 테스트", + "description": "품목기준관리 API의 전체 CRUD 플로우를 테스트합니다.", + "version": "1.0", + "config": { + "apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a", + "baseUrl": "https://api.sam.kr/api/v1", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "codebridgex", + "user_pwd": "code1234", + "testItemCode": "TEST-ITEM-{{$timestamp}}", + "testItemName": "테스트 품목", + "testItemSpec": "100x100x10", + "updatedItemName": "수정된 테스트 품목", + "updatedItemSpec": "200x200x20" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + } + }, + { + "id": "create_item", + "name": "품목 생성", + "method": "POST", + "endpoint": "/item-master-data", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "item_code": "{{variables.testItemCode}}", + "item_name": "{{variables.testItemName}}", + "item_spec": "{{variables.testItemSpec}}", + "item_type": "PRODUCT", + "unit": "EA", + "is_active": true + }, + "dependsOn": ["login"], + "extract": { + "createdItemId": "$.data.id", + "createdItemCode": "$.data.item_code" + }, + "expect": { + "status": [201], + "jsonPath": { + "$.data.id": "@exists" + } + } + }, + { + "id": "get_item", + "name": "품목 단건 조회", + "method": "GET", + "endpoint": "/item-master-data/{{create_item.createdItemId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["create_item"], + "expect": { + "status": [200], + "jsonPath": { + "$.data.id": "@exists" + } + } + }, + { + "id": "list_items", + "name": "품목 목록 조회", + "method": "GET", + "endpoint": "/item-master-data", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["create_item"], + "expect": { + "status": [200], + "jsonPath": { + "$.data": "@isArray" + } + } + }, + { + "id": "update_item", + "name": "품목 수정", + "method": "PUT", + "endpoint": "/item-master-data/{{create_item.createdItemId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "item_name": "{{variables.updatedItemName}}", + "item_spec": "{{variables.updatedItemSpec}}" + }, + "dependsOn": ["get_item"], + "expect": { + "status": [200] + } + }, + { + "id": "verify_update", + "name": "수정 확인", + "method": "GET", + "endpoint": "/item-master-data/{{create_item.createdItemId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["update_item"], + "expect": { + "status": [200] + } + }, + { + "id": "delete_item", + "name": "품목 삭제", + "method": "DELETE", + "endpoint": "/item-master-data/{{create_item.createdItemId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["verify_update"], + "expect": { + "status": [200] + } + }, + { + "id": "verify_delete", + "name": "삭제 확인", + "method": "GET", + "endpoint": "/item-master-data/{{create_item.createdItemId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["delete_item"], + "expect": { + "status": [404] + } + } + ] +} \ No newline at end of file diff --git a/plans/flow-tests/notification-settings-flow.json b/plans/flow-tests/notification-settings-flow.json new file mode 100644 index 0000000..b9c13d1 --- /dev/null +++ b/plans/flow-tests/notification-settings-flow.json @@ -0,0 +1,254 @@ +{ + "name": "알림 설정 조회/수정 플로우", + "description": "알림 설정 조회 → 그룹별 설정 변경 → 저장 → 확인 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "get_settings", + "name": "알림 설정 조회 (그룹 기반)", + "method": "GET", + "endpoint": "/api/v1/settings/notifications", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "original_notice_enabled": "$.data.notice.enabled", + "original_approval_enabled": "$.data.approval.enabled" + } + }, + { + "id": "update_notice_group", + "name": "공지사항 그룹 설정 수정", + "method": "PUT", + "endpoint": "/api/v1/settings/notifications", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "notice": { + "enabled": true, + "notice": { + "enabled": true, + "email": true + }, + "event": { + "enabled": true, + "email": false + } + } + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_notice_update", + "name": "공지사항 설정 변경 확인", + "method": "GET", + "endpoint": "/api/v1/settings/notifications", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.notice.enabled": true + } + } + }, + { + "id": "update_approval_group", + "name": "결재 그룹 설정 수정", + "method": "PUT", + "endpoint": "/api/v1/settings/notifications", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "approval": { + "enabled": true, + "approvalRequest": { + "enabled": true, + "email": true + }, + "draftApproved": { + "enabled": true, + "email": false + }, + "draftRejected": { + "enabled": true, + "email": true + }, + "draftCompleted": { + "enabled": false, + "email": false + } + } + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "update_multiple_groups", + "name": "여러 그룹 동시 수정", + "method": "PUT", + "endpoint": "/api/v1/settings/notifications", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "order": { + "enabled": true, + "salesOrder": { + "enabled": true, + "email": false + }, + "purchaseOrder": { + "enabled": true, + "email": false + } + }, + "production": { + "enabled": false, + "safetyStock": { + "enabled": false, + "email": false + }, + "productionComplete": { + "enabled": false, + "email": false + } + } + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "get_flat_settings", + "name": "플랫 구조 알림 설정 조회", + "method": "GET", + "endpoint": "/api/v1/users/me/notification-settings", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "update_single_type", + "name": "단일 알림 유형 수정 (플랫)", + "method": "PUT", + "endpoint": "/api/v1/users/me/notification-settings", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "notification_type": "approval", + "push_enabled": true, + "email_enabled": false, + "sms_enabled": false, + "in_app_enabled": true + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "bulk_update", + "name": "일괄 업데이트 (플랫)", + "method": "PUT", + "endpoint": "/api/v1/users/me/notification-settings/bulk", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "settings": [ + { + "notification_type": "order", + "push_enabled": true, + "email_enabled": false + }, + { + "notification_type": "deposit", + "push_enabled": true, + "email_enabled": true + } + ] + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "final_verify", + "name": "최종 설정 확인", + "method": "GET", + "endpoint": "/api/v1/settings/notifications", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] +} diff --git a/plans/flow-tests/payment-flow.json b/plans/flow-tests/payment-flow.json new file mode 100644 index 0000000..b605e99 --- /dev/null +++ b/plans/flow-tests/payment-flow.json @@ -0,0 +1,260 @@ +{ + "name": "결제 관리 플로우", + "description": "결제 등록 → 완료 처리 → 명세서 조회 → 취소 → 환불 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "list_payments", + "name": "결제 목록 조회", + "method": "GET", + "endpoint": "/api/v1/payments", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "page": 1, + "per_page": 10 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "create_payment", + "name": "결제 등록", + "method": "POST", + "endpoint": "/api/v1/payments", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "subscription_id": 1, + "amount": 99000, + "payment_method": "card", + "billing_name": "테스트 결제", + "billing_email": "test@example.com" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "payment_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "get_payment_detail", + "name": "결제 상세 조회", + "method": "GET", + "endpoint": "/api/v1/payments/{{create_payment.payment_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "continueOnFailure": true + }, + { + "id": "complete_payment", + "name": "결제 완료 처리", + "method": "POST", + "endpoint": "/api/v1/payments/{{create_payment.payment_id}}/complete", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "transaction_id": "TXN_{{$timestamp}}", + "pg_response": { + "code": "0000", + "message": "성공" + } + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "verify_completed", + "name": "완료 상태 확인", + "method": "GET", + "endpoint": "/api/v1/payments/{{create_payment.payment_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "completed" + } + }, + "continueOnFailure": true + }, + { + "id": "get_statement", + "name": "결제 명세서 조회", + "method": "GET", + "endpoint": "/api/v1/payments/{{create_payment.payment_id}}/statement", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "cancel_payment", + "name": "결제 취소 요청", + "method": "POST", + "endpoint": "/api/v1/payments/{{create_payment.payment_id}}/cancel", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "reason": "테스트 취소", + "cancel_type": "full" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "create_payment_for_refund", + "name": "환불 테스트용 결제 등록", + "method": "POST", + "endpoint": "/api/v1/payments", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "subscription_id": 1, + "amount": 50000, + "payment_method": "card", + "billing_name": "환불 테스트", + "billing_email": "refund@example.com" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "refund_payment_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "complete_refund_payment", + "name": "환불 테스트용 결제 완료", + "method": "POST", + "endpoint": "/api/v1/payments/{{create_payment_for_refund.refund_payment_id}}/complete", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "transaction_id": "TXN_REFUND_{{$timestamp}}", + "pg_response": { + "code": "0000", + "message": "성공" + } + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "request_refund", + "name": "환불 요청", + "method": "POST", + "endpoint": "/api/v1/payments/{{create_payment_for_refund.refund_payment_id}}/refund", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "refund_amount": 50000, + "reason": "테스트 환불", + "refund_method": "original" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "verify_refunded", + "name": "환불 상태 확인", + "method": "GET", + "endpoint": "/api/v1/payments/{{create_payment_for_refund.refund_payment_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "refunded" + } + }, + "continueOnFailure": true + } + ] +} diff --git a/plans/flow-tests/popup-flow.json b/plans/flow-tests/popup-flow.json new file mode 100644 index 0000000..733d476 --- /dev/null +++ b/plans/flow-tests/popup-flow.json @@ -0,0 +1,188 @@ +{ + "name": "팝업 관리 플로우", + "description": "팝업 등록 → 목록 조회 → 활성 팝업 조회 → 수정 → 삭제 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "list_popups", + "name": "팝업 목록 조회", + "method": "GET", + "endpoint": "/api/v1/popups", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "page": 1, + "per_page": 10 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "create_popup", + "name": "팝업 등록", + "method": "POST", + "endpoint": "/api/v1/popups", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "title": "Flow 테스트 팝업", + "content": "

테스트용 팝업 콘텐츠입니다.

", + "popup_type": "notice", + "position": "center", + "width": 500, + "height": 400, + "start_at": "2025-01-01T00:00:00Z", + "end_at": "2025-12-31T23:59:59Z", + "is_active": true, + "show_today_close": true, + "target_pages": ["dashboard", "main"] + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "popup_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "get_popup_detail", + "name": "팝업 상세 조회", + "method": "GET", + "endpoint": "/api/v1/popups/{{create_popup.popup_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "continueOnFailure": true + }, + { + "id": "get_active_popups", + "name": "활성 팝업 목록 조회", + "description": "현재 표시 중인 팝업 목록", + "method": "GET", + "endpoint": "/api/v1/popups/active", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "page": "dashboard" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "update_popup", + "name": "팝업 수정", + "method": "PUT", + "endpoint": "/api/v1/popups/{{create_popup.popup_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "title": "Flow 테스트 팝업 (수정됨)", + "is_active": false + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "verify_inactive", + "name": "비활성 상태 확인", + "method": "GET", + "endpoint": "/api/v1/popups/{{create_popup.popup_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.is_active": false + } + }, + "continueOnFailure": true + }, + { + "id": "delete_popup", + "name": "팝업 삭제", + "method": "DELETE", + "endpoint": "/api/v1/popups/{{create_popup.popup_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "verify_deleted", + "name": "삭제 확인", + "method": "GET", + "endpoint": "/api/v1/popups/{{create_popup.popup_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [404] + }, + "continueOnFailure": true + } + ] +} diff --git a/plans/flow-tests/pricing-crud-flow.json b/plans/flow-tests/pricing-crud-flow.json new file mode 100644 index 0000000..074543e --- /dev/null +++ b/plans/flow-tests/pricing-crud-flow.json @@ -0,0 +1,277 @@ +{ + "name": "단가 관리 CRUD 테스트", + "description": "단가(Pricing) API의 생성, 조회, 수정, 확정, 삭제 전체 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.message": "로그인 성공", + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "list_prices", + "name": "단가 목록 조회", + "method": "GET", + "endpoint": "/pricing", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "query": { + "per_page": 10, + "page": 1 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray" + } + } + }, + { + "id": "create_price", + "name": "단가 생성 (MATERIAL)", + "method": "POST", + "endpoint": "/pricing", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "item_type_code": "MATERIAL", + "item_id": 1, + "client_group_id": null, + "purchase_price": 10000, + "processing_cost": 500, + "loss_rate": 5, + "margin_rate": 20, + "sales_price": 12600, + "rounding_rule": "round", + "rounding_unit": 100, + "supplier": "테스트 공급업체", + "effective_from": "2025-01-01", + "effective_to": "2025-12-31", + "note": "API Flow 테스트용 단가", + "status": "draft" + }, + "expect": { + "status": [201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber", + "$.data.item_type_code": "MATERIAL", + "$.data.purchase_price": 10000, + "$.data.status": "draft" + } + }, + "extract": { + "price_id": "$.data.id" + } + }, + { + "id": "show_price", + "name": "생성된 단가 상세 조회", + "method": "GET", + "endpoint": "/pricing/{{create_price.price_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "{{create_price.price_id}}", + "$.data.item_type_code": "MATERIAL", + "$.data.supplier": "테스트 공급업체" + } + } + }, + { + "id": "update_price", + "name": "단가 수정 (가격 변경)", + "method": "PUT", + "endpoint": "/pricing/{{create_price.price_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "purchase_price": 11000, + "processing_cost": 600, + "margin_rate": 25, + "sales_price": 14500, + "note": "단가 수정 테스트", + "change_reason": "원가 인상으로 인한 가격 조정", + "status": "active" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.purchase_price": 11000, + "$.data.processing_cost": 600, + "$.data.status": "active" + } + } + }, + { + "id": "get_revisions", + "name": "변경 이력 조회", + "method": "GET", + "endpoint": "/pricing/{{create_price.price_id}}/revisions", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data": "@isArray" + } + } + }, + { + "id": "get_cost", + "name": "원가 조회 (receipt > standard 폴백)", + "method": "GET", + "endpoint": "/pricing/cost", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "query": { + "item_type_code": "MATERIAL", + "item_id": 1, + "date": "2025-06-15" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.item_type_code": "MATERIAL", + "$.data.item_id": 1 + } + } + }, + { + "id": "by_items", + "name": "다중 품목 단가 조회", + "method": "POST", + "endpoint": "/pricing/by-items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "items": [ + { + "item_type_code": "MATERIAL", + "item_id": 1 + } + ], + "date": "2025-06-15" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data": "@isArray" + } + } + }, + { + "id": "create_price_for_finalize", + "name": "확정 테스트용 단가 생성", + "method": "POST", + "endpoint": "/pricing", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "item_type_code": "PRODUCT", + "item_id": 1, + "purchase_price": 50000, + "sales_price": 70000, + "effective_from": "2025-01-01", + "status": "active" + }, + "expect": { + "status": [201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "finalize_price_id": "$.data.id" + } + }, + { + "id": "finalize_price", + "name": "가격 확정 (불변 처리)", + "method": "POST", + "endpoint": "/pricing/{{create_price_for_finalize.finalize_price_id}}/finalize", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "finalized", + "$.data.is_final": true + } + } + }, + { + "id": "delete_price", + "name": "단가 삭제 (soft delete)", + "method": "DELETE", + "endpoint": "/pricing/{{create_price.price_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_deleted", + "name": "삭제된 단가 조회 시 404 확인", + "method": "GET", + "endpoint": "/pricing/{{create_price.price_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [404], + "jsonPath": { + "$.success": false + } + } + } + ] +} diff --git a/plans/flow-tests/sales-statement-flow.json b/plans/flow-tests/sales-statement-flow.json new file mode 100644 index 0000000..fff29ea --- /dev/null +++ b/plans/flow-tests/sales-statement-flow.json @@ -0,0 +1,201 @@ +{ + "name": "매출 명세서 플로우", + "description": "매출 목록 조회 → 매출 상세 → 명세서 생성 → 확정 → 발송 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "list_sales", + "name": "매출 목록 조회", + "method": "GET", + "endpoint": "/api/v1/sales", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "page": 1, + "per_page": 10 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "first_sale_id": "$.data.data[0].id" + } + }, + { + "id": "get_sale_detail", + "name": "매출 상세 조회", + "method": "GET", + "endpoint": "/api/v1/sales/{{list_sales.first_sale_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "sale_data": "$.data" + }, + "continueOnFailure": true + }, + { + "id": "create_sale", + "name": "매출 등록", + "method": "POST", + "endpoint": "/api/v1/sales", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "client_id": 1, + "sale_date": "2025-01-15", + "due_date": "2025-02-15", + "items": [ + { + "product_id": 1, + "quantity": 10, + "unit_price": 50000, + "description": "테스트 상품" + } + ], + "memo": "Flow 테스트용 매출" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "new_sale_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "get_statement", + "name": "매출 명세서 조회", + "method": "GET", + "endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}/statement", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "confirm_sale", + "name": "매출 확정", + "method": "POST", + "endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}/confirm", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "confirmed_at": "2025-01-15T10:00:00Z" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "send_statement", + "name": "명세서 발송", + "method": "POST", + "endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}/send", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "send_type": "email", + "recipient_email": "test@example.com", + "message": "매출 명세서를 발송합니다." + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "update_sale", + "name": "매출 수정", + "method": "PUT", + "endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "memo": "Flow 테스트 - 수정됨" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "delete_sale", + "name": "매출 삭제", + "method": "DELETE", + "endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + } + ] +} diff --git a/plans/flow-tests/subscription-flow.json b/plans/flow-tests/subscription-flow.json new file mode 100644 index 0000000..e994025 --- /dev/null +++ b/plans/flow-tests/subscription-flow.json @@ -0,0 +1,260 @@ +{ + "name": "구독 관리 플로우", + "description": "구독 등록 → 사용량 조회 → 일시 정지 → 재개 → 갱신 → 해지 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "list_subscriptions", + "name": "구독 목록 조회", + "method": "GET", + "endpoint": "/api/v1/subscriptions", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "page": 1, + "per_page": 10 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "create_subscription", + "name": "구독 등록", + "method": "POST", + "endpoint": "/api/v1/subscriptions", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "plan_id": 1, + "billing_cycle": "monthly", + "start_date": "2025-01-01", + "payment_method": "card", + "auto_renew": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + }, + "extract": { + "subscription_id": "$.data.id" + }, + "continueOnFailure": true + }, + { + "id": "get_subscription_detail", + "name": "구독 상세 조회", + "method": "GET", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "continueOnFailure": true + }, + { + "id": "get_usage", + "name": "사용량 조회", + "method": "GET", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/usage", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "period": "current" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "suspend_subscription", + "name": "구독 일시 정지", + "method": "POST", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/suspend", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "reason": "테스트용 일시 정지" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "verify_suspended", + "name": "정지 상태 확인", + "method": "GET", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "suspended" + } + }, + "continueOnFailure": true + }, + { + "id": "resume_subscription", + "name": "구독 재개", + "method": "POST", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/resume", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "renew_subscription", + "name": "구독 갱신", + "method": "POST", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/renew", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "extend_months": 1 + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "update_subscription", + "name": "구독 수정", + "method": "PUT", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "auto_renew": false + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "export_usage", + "name": "사용량 내역 내보내기", + "method": "GET", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/export", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "queryParams": { + "format": "xlsx", + "period": "last_month" + }, + "expect": { + "status": [200] + }, + "continueOnFailure": true + }, + { + "id": "cancel_subscription", + "name": "구독 해지", + "method": "POST", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/cancel", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "reason": "테스트 완료", + "cancel_immediately": true + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + }, + "continueOnFailure": true + }, + { + "id": "verify_cancelled", + "name": "해지 상태 확인", + "method": "GET", + "endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "cancelled" + } + }, + "continueOnFailure": true + } + ] +} diff --git a/plans/flow-tests/user-invitation-flow.json b/plans/flow-tests/user-invitation-flow.json new file mode 100644 index 0000000..1076b57 --- /dev/null +++ b/plans/flow-tests/user-invitation-flow.json @@ -0,0 +1,125 @@ +{ + "name": "사용자 초대 CRUD 플로우", + "description": "사용자 초대 발송 → 목록 조회 → 재발송 → 취소 전체 플로우 테스트", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}", + "test_email": "flowtest_invite_{{$timestamp}}@example.com" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "invite_user", + "name": "사용자 초대 발송 (role=user)", + "method": "POST", + "endpoint": "/api/v1/users/invite", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "email": "{{test_email}}", + "role": "user", + "message": "SAM 시스템에 합류해 주세요!" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "pending", + "$.data.email": "@isString" + } + }, + "extract": { + "invitation_id": "$.data.id", + "invitation_token": "$.data.token", + "invited_email": "$.data.email" + } + }, + { + "id": "list_pending", + "name": "대기 중 초대 목록 조회", + "method": "GET", + "endpoint": "/api/v1/users/invitations?status=pending&per_page=10", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray" + } + } + }, + { + "id": "resend_invitation", + "name": "초대 재발송", + "method": "POST", + "endpoint": "/api/v1/users/invitations/{{invite_user.invitation_id}}/resend", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "pending" + } + } + }, + { + "id": "cancel_invitation", + "name": "초대 취소", + "method": "DELETE", + "endpoint": "/api/v1/users/invitations/{{invite_user.invitation_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_cancelled", + "name": "취소된 초대 목록 확인", + "method": "GET", + "endpoint": "/api/v1/users/invitations?status=cancelled&per_page=10", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] +} diff --git a/plans/mng-quote-formula-development-plan.md b/plans/mng-quote-formula-development-plan.md index 6288780..a632902 100644 --- a/plans/mng-quote-formula-development-plan.md +++ b/plans/mng-quote-formula-development-plan.md @@ -1,7 +1,7 @@ # MNG 견적수식 관리 개발 계획 > **작성일**: 2025-12-22 -> **상태**: 계획 수립 +> **상태**: ✅ 완료 > **대상**: mng.sam.kr/quote-formulas --- @@ -24,15 +24,9 @@ | 시뮬레이터 | ✅ 완료 | 입력값 → 계산 결과 미리보기 | | 변수 참조 | ✅ 완료 | 사용 가능한 변수 목록 표시 | | 수식 검증 | ✅ 완료 | 문법 검증 API | - -#### 미구현/미완성 기능 (mng) - -| 기능 | 상태 | 설명 | -|-----|------|-----| -| 범위(Range) 관리 UI | ❌ 미구현 | 범위별 결과 설정 화면 없음 | -| 매핑(Mapping) 관리 UI | ❌ 미구현 | 매핑 규칙 설정 화면 없음 | -| 품목(Item) 관리 UI | ❌ 미구현 | 출력 품목 설정 화면 없음 | -| 5130 데이터 연동 | ❌ 미구현 | 레거시 가격 데이터 동기화 | +| 범위(Range) 관리 UI | ✅ 완료 | 범위별 결과 설정 화면 (Phase 1) | +| 매핑(Mapping) 관리 UI | ✅ 완료 | 매핑 규칙 설정 화면 (Phase 2) | +| 품목(Item) 관리 UI | ✅ 완료 | 출력 품목 설정 화면 (Phase 3) | ### 1.2 API 프로젝트 현재 상태 @@ -52,7 +46,7 @@ QuoteFormulaCategory (카테고리) |-----|---------|-----| | QuoteFormulaCategorySeeder | 11개 | 카테고리 (오픈사이즈~단가수식) | | QuoteFormulaSeeder | 30개 수식, 18개 범위 | 스크린 계산 수식 | -| QuoteFormulaItemSeeder | 25개 | 품목 마스터 (5130 가격 적용) | +| QuoteFormulaItemSeeder | 25개 | 품목 마스터 | #### 서비스 (api) @@ -86,41 +80,26 @@ QuoteFormulaCategory (카테고리) |-----|-----|-----|-----| | 수식 CRUD | ✅ | ✅ | 동일 | | 카테고리 CRUD | ✅ | ✅ | 동일 | -| **범위 관리 UI** | ❌ | ✅ (시더) | MNG에 UI 필요 | -| **매핑 관리 UI** | ❌ | ✅ (시더) | MNG에 UI 필요 | -| **품목 관리 UI** | ❌ | ✅ (시더) | MNG에 UI 필요 | +| 범위 관리 UI | ✅ | ✅ (시더) | Phase 1 완료 | +| 매핑 관리 UI | ✅ | ✅ (시더) | Phase 2 완료 | +| 품목 관리 UI | ✅ | ✅ (시더) | Phase 3 완료 | | 시뮬레이터 | ✅ | ✅ | 동일 | | 자동산출 API | - | ✅ | API 전용 | -### 2.3 핵심 차이점 - -``` -MNG (관리 UI) -├── 수식 기본 정보 관리 ✅ -├── 카테고리 관리 ✅ -├── 시뮬레이터 ✅ -└── 범위/매핑/품목 관리 ❌ ← 개발 필요 - -API (자동산출 엔진) -├── 시더로 데이터 주입 ✅ -├── 자동산출 서비스 ✅ -└── 견적 생성 API ✅ -``` - --- -## 3. 개발 계획 +## 3. 개발 계획 (완료) ### 3.1 목표 MNG에서 **범위(Range), 매핑(Mapping), 품목(Item)** 관리 UI를 추가하여: 1. 시더 없이도 관리자가 직접 수식 규칙 설정 가능 -2. 5130 레거시 데이터를 참조하여 가격 설정 가능 +2. SAM 자체 품목 마스터로 가격 설정 3. 실시간 시뮬레이션으로 설정 검증 가능 -### 3.2 개발 범위 +### 3.2 개발 범위 (완료) -#### Phase 1: 범위(Range) 관리 UI +#### Phase 1: 범위(Range) 관리 UI ✅ **우선순위**: 높음 **이유**: 모터, 가이드레일, 케이스 자동 선택에 필수 @@ -160,7 +139,7 @@ DELETE /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId} POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder ``` -#### Phase 2: 매핑(Mapping) 관리 UI +#### Phase 2: 매핑(Mapping) 관리 UI ✅ **우선순위**: 중간 **이유**: 제어기 유형 등 코드 매핑에 사용 @@ -185,7 +164,7 @@ POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder └── [+ 매핑 추가] ``` -#### Phase 3: 품목(Item) 관리 UI +#### Phase 3: 품목(Item) 관리 UI ✅ **우선순위**: 중간 **이유**: 수식 결과로 생성되는 품목 정의 @@ -195,7 +174,7 @@ POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder 2. 품목 목록 표시 3. 품목 추가/수정/삭제 4. 수량/단가 수식 입력 -5. 5130 품목 검색 및 가격 참조 +5. SAM 품목 마스터에서 가격 참조 **화면 설계**: ``` @@ -207,120 +186,45 @@ POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder │ │ PT-MOTOR-150 │ 개폐전동기 150kg│ 150K(S) │ 1 │ 285000│ │ │ PT-GR-3000 │ 가이드레일 3000 │ 3000mm │ 2 │ 42000 │ │ └───────────────────────────────────────────────────────────┘ -├── [+ 품목 추가] -└── [5130에서 가져오기] ← 레거시 연동 +└── [+ 품목 추가] ``` -#### Phase 4: 5130 데이터 연동 - -**우선순위**: 높음 -**이유**: 실제 가격 데이터 필요 - -**기능 목록**: -1. 5130 DB 가격 테이블 조회 API -2. 품목 추가 시 5130 검색 모달 -3. 가격 동기화 기능 -4. 가격 변경 이력 관리 - -**5130 테이블 참조**: -```sql --- chandj.price_motor: 모터 가격 --- chandj.price_*: 기타 품목 가격 -``` - -### 3.3 파일 수정/추가 목록 - -#### Routes (mng/routes/web.php) -```php -// 기존 라우트에 추가 -Route::prefix('quote-formulas')->group(function () { - // ... 기존 라우트 - Route::get('/{id}/ranges', [QuoteFormulaController::class, 'ranges']); - Route::get('/{id}/mappings', [QuoteFormulaController::class, 'mappings']); - Route::get('/{id}/items', [QuoteFormulaController::class, 'items']); -}); -``` - -#### API Routes (mng/routes/api.php) -```php -// 범위/매핑/품목 CRUD API -Route::prefix('quote-formulas/formulas/{formulaId}')->group(function () { - Route::apiResource('ranges', QuoteFormulaRangeController::class); - Route::post('ranges/reorder', [QuoteFormulaRangeController::class, 'reorder']); - - Route::apiResource('mappings', QuoteFormulaMappingController::class); - - Route::apiResource('items', QuoteFormulaItemController::class); -}); - -// 5130 연동 -Route::prefix('legacy')->group(function () { - Route::get('items/search', [LegacyController::class, 'searchItems']); - Route::get('prices/{itemCode}', [LegacyController::class, 'getPrice']); -}); -``` +### 3.3 파일 구조 (구현 완료) #### Controllers ``` app/Http/Controllers/ ├── QuoteFormulaController.php (수정: 탭 추가) └── Api/Admin/Quote/ - ├── QuoteFormulaRangeController.php (신규) - ├── QuoteFormulaMappingController.php (신규) - ├── QuoteFormulaItemController.php (신규) - └── LegacyController.php (신규: 5130 연동) + ├── QuoteFormulaController.php + ├── QuoteFormulaRangeController.php ✅ + ├── QuoteFormulaMappingController.php ✅ + ├── QuoteFormulaItemController.php ✅ + └── QuoteFormulaCategoryController.php ``` #### Services ``` -app/Services/ -├── Quote/ -│ ├── QuoteFormulaRangeService.php (신규) -│ ├── QuoteFormulaMappingService.php (신규) -│ └── QuoteFormulaItemService.php (신규) -└── Legacy/ - └── LegacyPriceService.php (신규: 5130 가격 조회) +app/Services/Quote/ +├── QuoteFormulaService.php +├── QuoteFormulaRangeService.php ✅ +├── QuoteFormulaMappingService.php ✅ +├── QuoteFormulaItemService.php ✅ +└── QuoteFormulaCategoryService.php ``` #### Views ``` resources/views/quote-formulas/ -├── edit.blade.php (수정: 탭 구조로 변경) -├── partials/ -│ ├── ranges-tab.blade.php (신규) -│ ├── mappings-tab.blade.php (신규) -│ └── items-tab.blade.php (신규) -└── modals/ - ├── range-form.blade.php (신규) - ├── mapping-form.blade.php (신규) - ├── item-form.blade.php (신규) - └── legacy-item-search.blade.php (신규) -``` - -### 3.4 개발 순서 - -``` -Phase 1: 범위 관리 (1주) -├── Day 1-2: API 엔드포인트 구현 -├── Day 3-4: UI 컴포넌트 구현 -└── Day 5: 테스트 및 검증 - -Phase 2: 매핑 관리 (0.5주) -├── Day 1: API 구현 -└── Day 2-3: UI 구현 - -Phase 3: 품목 관리 (0.5주) -├── Day 1: API 구현 -└── Day 2-3: UI 구현 - -Phase 4: 5130 연동 (1주) -├── Day 1-2: 레거시 DB 조회 서비스 -├── Day 3-4: 검색 모달 UI -└── Day 5: 가격 동기화 기능 - -통합 테스트 (0.5주) -├── 시뮬레이터 연동 테스트 -└── 전체 플로우 검증 +├── index.blade.php +├── create.blade.php +├── edit.blade.php (수정: 탭 구조) +├── simulator.blade.php +└── partials/ + ├── basic-info-tab.blade.php ✅ + ├── ranges-tab.blade.php ✅ + ├── mappings-tab.blade.php ✅ + └── items-tab.blade.php ✅ ``` --- @@ -336,30 +240,17 @@ Phase 4: 5130 연동 (1주) ### 4.2 Backend (MNG) - **Framework**: Laravel 12 - **ORM**: Eloquent -- **DB**: MySQL (samdb + chandj) +- **DB**: MySQL (samdb) - **Auth**: Session 기반 ### 4.3 API 연동 - MNG 내부 API (`/api/admin/quote-formulas/*`) -- 5130 DB 직접 조회 (chandj 데이터베이스) --- -## 5. 데이터 마이그레이션 +## 5. 검증 계획 -### 5.1 현재 상태 -- API 시더로 30개 수식, 18개 범위, 25개 품목 등록됨 -- 5130 실제 가격 데이터 반영 완료 - -### 5.2 마이그레이션 계획 -1. **Phase 1 완료 후**: 시더 데이터를 MNG UI로 확인 가능 -2. **Phase 4 완료 후**: 5130 데이터 자동 동기화 - ---- - -## 6. 검증 계획 - -### 6.1 시뮬레이터 테스트 +### 5.1 시뮬레이터 테스트 ``` 입력: W0=3000, H0=2500 예상 결과: @@ -368,22 +259,31 @@ Phase 4: 5130 연동 (1주) - MOTOR: PT-MOTOR-150 (K=41.21kg) ``` -### 6.2 CRUD 테스트 +### 5.2 CRUD 테스트 - 범위 추가/수정/삭제 후 시뮬레이터 결과 확인 - 품목 가격 변경 후 합계 확인 --- -## 7. 참고 자료 +## 6. 참고 자료 -### 7.1 기존 파일 위치 (MNG) +### 6.1 파일 위치 (MNG) ``` mng/ ├── app/Http/Controllers/ │ ├── QuoteFormulaController.php -│ └── Api/Admin/Quote/QuoteFormulaController.php +│ └── Api/Admin/Quote/ +│ ├── QuoteFormulaController.php +│ ├── QuoteFormulaRangeController.php +│ ├── QuoteFormulaMappingController.php +│ ├── QuoteFormulaItemController.php +│ └── QuoteFormulaCategoryController.php ├── app/Services/Quote/ -│ └── QuoteFormulaService.php +│ ├── QuoteFormulaService.php +│ ├── QuoteFormulaRangeService.php +│ ├── QuoteFormulaMappingService.php +│ ├── QuoteFormulaItemService.php +│ └── QuoteFormulaCategoryService.php ├── app/Models/Quote/ │ ├── QuoteFormula.php │ ├── QuoteFormulaCategory.php @@ -394,10 +294,15 @@ mng/ ├── index.blade.php ├── create.blade.php ├── edit.blade.php - └── simulator.blade.php + ├── simulator.blade.php + └── partials/ + ├── basic-info-tab.blade.php + ├── ranges-tab.blade.php + ├── mappings-tab.blade.php + └── items-tab.blade.php ``` -### 7.2 API 시더 위치 +### 6.2 API 시더 위치 ``` api/database/seeders/ ├── QuoteFormulaCategorySeeder.php @@ -405,35 +310,11 @@ api/database/seeders/ └── QuoteFormulaItemSeeder.php ``` -### 7.3 5130 가격 테이블 -```sql -chandj.price_motor -- 모터 가격 -chandj.price_* -- 기타 품목 가격 (확인 필요) -``` - --- -## 8. 리스크 및 대응 +## 7. 코딩 컨벤션 및 예시 코드 -| 리스크 | 영향 | 대응 | -|-------|-----|-----| -| 5130 DB 스키마 변경 | 중 | JSON 파싱 로직 유연하게 구현 | -| MNG-API 데이터 불일치 | 높 | 동일 DB 사용으로 해결됨 | -| 시뮬레이터 성능 저하 | 낮 | 수식 캐싱 적용 | - ---- - -## 9. 다음 단계 - -1. **승인 요청**: 이 계획에 대한 검토 및 승인 -2. **Phase 1 착수**: 범위 관리 UI 개발 시작 -3. **주간 리뷰**: 진행 상황 점검 - ---- - -## 10. 코딩 컨벤션 및 예시 코드 - -### 10.1 API Controller 패턴 (MNG) +### 7.1 API Controller 패턴 (MNG) ```php 'decimal:4', - 'max_value' => 'decimal:4', - 'sort_order' => 'integer', - ]; - - public function formula(): BelongsTo - { - return $this->belongsTo(QuoteFormula::class, 'formula_id'); - } - - public function isInRange($value): bool - { - // min < value <= max 체크 - } -} -``` - -### 10.4 Blade View 패턴 (HTMX + Alpine.js) - -```blade -{{-- 파일: resources/views/quote-formulas/partials/ranges-tab.blade.php --}} - -
- {{-- 헤더 --}} -
-

범위 설정

- -
- - {{-- 조건 변수 표시 --}} -
- 조건 변수: - -
- - {{-- 범위 목록 테이블 --}} -
- - - - - - - - - - - - - - -
#최소값최대값결과값품목코드액션
-
- - {{-- 빈 상태 --}} -
- 설정된 범위가 없습니다. -
- - {{-- 범위 추가/수정 모달 --}} - - - -
- - -``` - -### 10.5 DB 스키마 (참조용) - -```sql --- quote_formula_ranges 테이블 (기존) -CREATE TABLE `quote_formula_ranges` ( - `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - `formula_id` BIGINT UNSIGNED NOT NULL, - `min_value` DECIMAL(15,4) NULL COMMENT '최소값 (NULL=제한없음)', - `max_value` DECIMAL(15,4) NULL COMMENT '최대값 (NULL=제한없음)', - `condition_variable` VARCHAR(50) NOT NULL COMMENT '조건 변수 (K, H1, S 등)', - `result_value` TEXT NOT NULL COMMENT '결과값 (JSON 또는 문자열)', - `result_type` ENUM('fixed','formula') DEFAULT 'fixed' COMMENT '결과 유형', - `sort_order` INT DEFAULT 0, - `created_at` TIMESTAMP NULL, - `updated_at` TIMESTAMP NULL, - - INDEX `idx_formula_id` (`formula_id`), - CONSTRAINT `fk_ranges_formula` FOREIGN KEY (`formula_id`) - REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE -); - --- quote_formula_items 테이블 (기존) -CREATE TABLE `quote_formula_items` ( - `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - `formula_id` BIGINT UNSIGNED NOT NULL, - `item_code` VARCHAR(50) NOT NULL COMMENT '품목 코드', - `item_name` VARCHAR(200) NOT NULL COMMENT '품목명', - `specification` VARCHAR(200) NULL COMMENT '규격', - `unit` VARCHAR(20) NOT NULL COMMENT '단위', - `quantity_formula` VARCHAR(500) NOT NULL COMMENT '수량 계산식', - `unit_price_formula` VARCHAR(500) NULL COMMENT '단가 계산식 (NULL=마스터 참조)', - `sort_order` INT DEFAULT 0, - `created_at` TIMESTAMP NULL, - `updated_at` TIMESTAMP NULL, - - INDEX `idx_formula_id` (`formula_id`), - INDEX `idx_item_code` (`item_code`), - CONSTRAINT `fk_items_formula` FOREIGN KEY (`formula_id`) - REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE -); -``` - -### 10.6 API 응답 형식 +### 7.3 API 응답 형식 ```json // 성공 응답 @@ -924,29 +528,26 @@ CREATE TABLE `quote_formula_items` ( --- -## 11. 체크리스트 (새 세션용) +## 8. 체크리스트 (완료) -### 개발 시작 전 확인 +### 개발 완료 확인 -- [ ] mng 프로젝트 디렉토리 확인: `/Users/hskwon/Works/@KD_SAM/SAM/mng` -- [ ] 기존 Controller 패턴 확인: `app/Http/Controllers/Api/Admin/Quote/QuoteFormulaController.php` -- [ ] 기존 Model 확인: `app/Models/Quote/QuoteFormulaRange.php` -- [ ] 기존 View 확인: `resources/views/quote-formulas/edit.blade.php` -- [ ] DB 테이블 확인: `quote_formula_ranges`, `quote_formula_items` - -### Phase 1 (범위 관리) 작업 순서 - -1. [ ] `QuoteFormulaRangeController.php` 생성 -2. [ ] `QuoteFormulaRangeService.php` 생성 -3. [ ] `routes/api.php`에 라우트 추가 -4. [ ] `edit.blade.php` 탭 구조로 수정 -5. [ ] `partials/ranges-tab.blade.php` 생성 -6. [ ] HTMX/Alpine.js 연동 테스트 -7. [ ] 시뮬레이터와 연동 확인 +- [x] mng 프로젝트 디렉토리: `/Users/hskwon/Works/@KD_SAM/SAM/mng` +- [x] `QuoteFormulaRangeController.php` 생성 +- [x] `QuoteFormulaRangeService.php` 생성 +- [x] `QuoteFormulaMappingController.php` 생성 +- [x] `QuoteFormulaMappingService.php` 생성 +- [x] `QuoteFormulaItemController.php` 생성 +- [x] `QuoteFormulaItemService.php` 생성 +- [x] `routes/api.php`에 라우트 추가 +- [x] `edit.blade.php` 탭 구조로 수정 +- [x] `partials/ranges-tab.blade.php` 생성 +- [x] `partials/mappings-tab.blade.php` 생성 +- [x] `partials/items-tab.blade.php` 생성 --- -*문서 버전*: 1.1 +*문서 버전*: 2.0 *작성자*: Claude Code *검토자*: - -*업데이트*: 코딩 컨벤션 및 예시 코드 추가 (2025-12-22) \ No newline at end of file +*최종 업데이트*: 2025-12-22 (Phase 1-3 완료, 5130 연동 제거) \ No newline at end of file diff --git a/plans/quote-auto-calculation-development-plan.md b/plans/quote-auto-calculation-development-plan.md new file mode 100644 index 0000000..2034c20 --- /dev/null +++ b/plans/quote-auto-calculation-development-plan.md @@ -0,0 +1,743 @@ +# 견적 자동산출 개발 계획 + +> **작성일**: 2025-12-22 +> **상태**: ✅ 구현 완료 +> **목표**: MNG 견적수식 데이터 셋팅 + React 견적관리 자동산출 기능 구현 +> **완료일**: 2025-12-22 +> **실제 소요 시간**: 약 2시간 + +--- + +## 0. 빠른 시작 가이드 + +### 폴더 구조 이해 (중요!) + +| 폴더 | 포트 | 역할 | 비고 | +|------|------|------|------| +| `design/` | localhost:3002 | 디자인 프로토타입 | UI 참고용 | +| `react/` | localhost:3000 | **실제 프론트엔드** | 구현 대상 ✅ | +| `mng/` | mng.sam.kr | 관리자 패널 | 수식 데이터 관리 | +| `api/` | api.sam.kr | REST API | 견적 산출 엔진 | + +### 이 문서만으로 작업을 시작하려면: + +```bash +# 1. Docker 서비스 시작 +cd /Users/hskwon/Works/@KD_SAM/SAM +docker-compose up -d + +# 2. MNG 시더 실행 (Phase 1 완료 후) +cd mng +php artisan quote:seed-formulas --tenant=1 + +# 3. React 개발 서버 (실제 구현 대상) +cd react +npm run dev +# http://localhost:3000 접속 +``` + +### 핵심 파일 위치 + +| 구분 | 파일 경로 | 역할 | +|------|----------|------| +| **MNG 시더** | `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | 🆕 생성 필요 | +| **React 자동산출** | `react/src/components/quotes/QuoteRegistration.tsx` | ⚡ 수정 필요 (line 332) | +| **API 클라이언트** | `react/src/lib/api/client.ts` | 참조 | +| **API 엔드포인트** | `api/app/Http/Controllers/Api/V1/QuoteController.php` | ✅ 구현됨 | +| **수식 엔진** | `api/app/Services/Quote/QuoteCalculationService.php` | ✅ 구현됨 | + +--- + +## 1. 현황 분석 + +### 1.1 시스템 구조 + +``` +┌───────────────────────────────────────────────────────────────────────────────┐ +│ SAM 시스템 │ +├───────────────────────────────────────────────────────────────────────────────┤ +│ MNG (mng.sam.kr) │ React (react/ 폴더) │ Design │ +│ ├── 기준정보관리 │ ├── 판매관리 │ (참고용) │ +│ │ └── 견적수식관리 ✅ │ │ └── 견적관리 │ │ +│ │ - 카테고리 CRUD │ │ └── 자동견적산출 │ design/ │ +│ │ - 수식 CRUD │ │ UI 있음 ✅ │ :3002 │ +│ │ - 범위/매핑/품목 탭 │ │ API 연동 ❌ │ │ +│ │ │ │ │ │ +│ └── DB: quote_formulas 테이블 │ └── API 호출: │ │ +│ (데이터 없음! ❌) │ POST /v1/quotes/calculate │ │ +└───────────────────────────────────────────────────────────────────────────────┘ + +※ design/ 폴더 (localhost:3002)는 UI 프로토타입용이며, 실제 구현은 react/ 폴더에서 진행 +``` + +### 1.2 React 견적등록 컴포넌트 현황 + +**파일**: `react/src/components/quotes/QuoteRegistration.tsx` + +```typescript +// 현재 상태 (line 332-335) +const handleAutoCalculate = () => { + toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`); +}; + +// 입력 필드 (이미 구현됨): +interface QuoteItem { + openWidth: string; // W0 (오픈사이즈 가로) + openHeight: string; // H0 (오픈사이즈 세로) + productCategory: string; // screen | steel + quantity: number; + // ... 기타 필드 +} +``` + +### 1.3 API 엔드포인트 현황 + +**파일**: `api/app/Http/Controllers/Api/V1/QuoteController.php` + +```php +// 이미 구현됨 (line 135-145) +public function calculate(QuoteCalculateRequest $request) +{ + return ApiResponse::handle(function () use ($request) { + $validated = $request->validated(); + return $this->calculationService->calculate( + $validated['inputs'] ?? $validated, + $validated['product_category'] ?? null + ); + }, __('message.quote.calculated')); +} +``` + +### 1.4 수식 시더 데이터 (API) + +**파일**: `api/database/seeders/QuoteFormulaSeeder.php` + +| 카테고리 | 수식 수 | 설명 | +|---------|--------|------| +| OPEN_SIZE | 2 | W0, H0 입력값 | +| MAKE_SIZE | 4 | 제작사이즈 계산 | +| AREA | 1 | 면적 = W1 * H1 / 1000000 | +| WEIGHT | 2 | 중량 계산 (스크린/철재) | +| GUIDE_RAIL | 5 | 가이드레일 자동 선택 | +| CASE | 3 | 케이스 자동 선택 | +| MOTOR | 1 | 모터 자동 선택 (범위 9개) | +| CONTROLLER | 2 | 제어기 매핑 | +| EDGE_WING | 1 | 마구리 수량 | +| INSPECTION | 1 | 검사비 | +| PRICE_FORMULA | 8 | 단가 수식 | +| **합계** | **30개** | + 범위 18개 | + +--- + +## 2. 개발 상세 계획 + +### Phase 1: MNG 시더 데이터 생성 (1일) + +#### 2.1 Artisan 명령어 생성 + +**생성할 파일**: `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` + +```php +option('tenant'); + $only = $this->option('only'); + $fresh = $this->option('fresh'); + + if ($fresh) { + $this->warn('기존 데이터를 삭제합니다...'); + $this->truncateTables($tenantId); + } + + if (!$only || $only === 'categories') { + $this->seedCategories($tenantId); + } + + if (!$only || $only === 'formulas') { + $this->seedFormulas($tenantId); + } + + if (!$only || $only === 'ranges') { + $this->seedRanges($tenantId); + } + + $this->info('✅ 견적수식 시드 완료!'); + return Command::SUCCESS; + } + + private function seedCategories(int $tenantId): void + { + $categories = [ + ['code' => 'OPEN_SIZE', 'name' => '오픈사이즈', 'sort_order' => 1], + ['code' => 'MAKE_SIZE', 'name' => '제작사이즈', 'sort_order' => 2], + ['code' => 'AREA', 'name' => '면적', 'sort_order' => 3], + ['code' => 'WEIGHT', 'name' => '중량', 'sort_order' => 4], + ['code' => 'GUIDE_RAIL', 'name' => '가이드레일', 'sort_order' => 5], + ['code' => 'CASE', 'name' => '케이스', 'sort_order' => 6], + ['code' => 'MOTOR', 'name' => '모터', 'sort_order' => 7], + ['code' => 'CONTROLLER', 'name' => '제어기', 'sort_order' => 8], + ['code' => 'EDGE_WING', 'name' => '마구리', 'sort_order' => 9], + ['code' => 'INSPECTION', 'name' => '검사', 'sort_order' => 10], + ['code' => 'PRICE_FORMULA', 'name' => '단가수식', 'sort_order' => 11], + ]; + + foreach ($categories as $cat) { + DB::table('quote_formula_categories')->updateOrInsert( + ['tenant_id' => $tenantId, 'code' => $cat['code']], + array_merge($cat, [ + 'tenant_id' => $tenantId, + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]) + ); + } + + $this->info("카테고리 " . count($categories) . "개 생성됨"); + } + + private function seedFormulas(int $tenantId): void + { + // API 시더와 동일한 데이터 (api/database/seeders/QuoteFormulaSeeder.php 참조) + $formulas = $this->getFormulaData(); + + $categoryMap = DB::table('quote_formula_categories') + ->where('tenant_id', $tenantId) + ->pluck('id', 'code') + ->toArray(); + + $count = 0; + foreach ($formulas as $formula) { + $categoryId = $categoryMap[$formula['category_code']] ?? null; + if (!$categoryId) continue; + + DB::table('quote_formulas')->updateOrInsert( + ['tenant_id' => $tenantId, 'variable' => $formula['variable']], + [ + 'tenant_id' => $tenantId, + 'category_id' => $categoryId, + 'variable' => $formula['variable'], + 'name' => $formula['name'], + 'type' => $formula['type'], + 'formula' => $formula['formula'] ?? null, + 'output_type' => 'variable', + 'description' => $formula['description'] ?? null, + 'sort_order' => $formula['sort_order'] ?? 0, + 'is_active' => $formula['is_active'] ?? true, + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + $count++; + } + + $this->info("수식 {$count}개 생성됨"); + } + + private function getFormulaData(): array + { + return [ + // 오픈사이즈 + ['category_code' => 'OPEN_SIZE', 'variable' => 'W0', 'name' => '오픈사이즈 W0 (가로)', 'type' => 'input', 'formula' => null, 'sort_order' => 1], + ['category_code' => 'OPEN_SIZE', 'variable' => 'H0', 'name' => '오픈사이즈 H0 (세로)', 'type' => 'input', 'formula' => null, 'sort_order' => 2], + + // 제작사이즈 + ['category_code' => 'MAKE_SIZE', 'variable' => 'W1_SCREEN', 'name' => '제작사이즈 W1 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 140', 'sort_order' => 1], + ['category_code' => 'MAKE_SIZE', 'variable' => 'H1_SCREEN', 'name' => '제작사이즈 H1 (스크린)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 2], + ['category_code' => 'MAKE_SIZE', 'variable' => 'W1_STEEL', 'name' => '제작사이즈 W1 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 110', 'sort_order' => 3], + ['category_code' => 'MAKE_SIZE', 'variable' => 'H1_STEEL', 'name' => '제작사이즈 H1 (철재)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 4], + + // 면적 + ['category_code' => 'AREA', 'variable' => 'M', 'name' => '면적 계산', 'type' => 'calculation', 'formula' => 'W1 * H1 / 1000000', 'sort_order' => 1], + + // 중량 + ['category_code' => 'WEIGHT', 'variable' => 'K_SCREEN', 'name' => '중량 계산 (스크린)', 'type' => 'calculation', 'formula' => 'M * 2 + W0 / 1000 * 14.17', 'sort_order' => 1], + ['category_code' => 'WEIGHT', 'variable' => 'K_STEEL', 'name' => '중량 계산 (철재)', 'type' => 'calculation', 'formula' => 'M * 25', 'sort_order' => 2], + + // 가이드레일 + ['category_code' => 'GUIDE_RAIL', 'variable' => 'G', 'name' => '가이드레일 제작길이', 'type' => 'calculation', 'formula' => 'H0 + 250', 'sort_order' => 1], + ['category_code' => 'GUIDE_RAIL', 'variable' => 'GR_AUTO_SELECT', 'name' => '가이드레일 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 2], + + // 케이스 + ['category_code' => 'CASE', 'variable' => 'S_SCREEN', 'name' => '케이스 사이즈 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 220', 'sort_order' => 1], + ['category_code' => 'CASE', 'variable' => 'S_STEEL', 'name' => '케이스 사이즈 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 240', 'sort_order' => 2], + ['category_code' => 'CASE', 'variable' => 'CASE_AUTO_SELECT', 'name' => '케이스 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 3], + + // 모터 + ['category_code' => 'MOTOR', 'variable' => 'MOTOR_AUTO_SELECT', 'name' => '모터 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 1], + + // 제어기 + ['category_code' => 'CONTROLLER', 'variable' => 'CONTROLLER_TYPE', 'name' => '제어기 유형', 'type' => 'input', 'formula' => null, 'sort_order' => 0], + ['category_code' => 'CONTROLLER', 'variable' => 'CTRL_AUTO_SELECT', 'name' => '제어기 자동 선택', 'type' => 'mapping', 'formula' => null, 'sort_order' => 1], + + // 검사 + ['category_code' => 'INSPECTION', 'variable' => 'INSP_FEE', 'name' => '검사비', 'type' => 'calculation', 'formula' => '1', 'sort_order' => 1], + ]; + } + + // ... 나머지 메서드 (seedRanges, truncateTables 등) +} +``` + +#### 2.2 작업 순서 + +```bash +# 1. 명령어 파일 생성 +# mng/app/Console/Commands/SeedQuoteFormulasCommand.php + +# 2. 실행 +cd mng +php artisan quote:seed-formulas --tenant=1 + +# 3. 확인 +php artisan tinker +>>> \App\Models\Quote\QuoteFormula::count() +# 예상: 30 + +# 4. 시뮬레이터 테스트 +# mng.sam.kr/quote-formulas/simulator +# 입력: W0=3000, H0=2500 +``` + +--- + +### Phase 2: React 자동산출 기능 구현 (2-3일) + +#### 2.1 API 클라이언트 추가 + +**수정할 파일**: `react/src/lib/api/quote.ts` (신규) + +```typescript +// react/src/lib/api/quote.ts +import { ApiClient } from './client'; +import { AUTH_CONFIG } from './auth/auth-config'; + +// API 응답 타입 +interface CalculationResult { + inputs: Record; + outputs: Record; + items: Array<{ + item_code: string; + item_name: string; + specification?: string; + unit?: string; + quantity: number; + unit_price: number; + total_price: number; + formula_variable: string; + }>; + costs: { + material_cost: number; + labor_cost: number; + install_cost: number; + subtotal: number; + }; + errors: string[]; +} + +interface CalculateRequest { + inputs: { + W0: number; + H0: number; + QTY?: number; + INSTALL_TYPE?: string; + CONTROL_TYPE?: string; + }; + product_category: 'screen' | 'steel'; +} + +// Quote API 클라이언트 +class QuoteApiClient extends ApiClient { + constructor() { + super({ + mode: 'bearer', + apiKey: AUTH_CONFIG.apiKey, + getToken: () => { + if (typeof window !== 'undefined') { + return localStorage.getItem('auth_token'); + } + return null; + }, + }); + } + + /** + * 자동 견적 산출 + */ + async calculate(request: CalculateRequest): Promise<{ success: boolean; data: CalculationResult; message: string }> { + return this.post('/api/v1/quotes/calculate', request); + } + + /** + * 입력 스키마 조회 + */ + async getCalculationSchema(productCategory?: string): Promise<{ success: boolean; data: Record }> { + const query = productCategory ? `?product_category=${productCategory}` : ''; + return this.get(`/api/v1/quotes/calculation-schema${query}`); + } +} + +export const quoteApi = new QuoteApiClient(); +``` + +#### 2.2 QuoteRegistration.tsx 수정 + +**수정할 파일**: `react/src/components/quotes/QuoteRegistration.tsx` + +```typescript +// 추가할 import +import { quoteApi } from '@/lib/api/quote'; +import { useState } from 'react'; + +// 상태 추가 (컴포넌트 내부) +const [calculationResult, setCalculationResult] = useState(null); +const [isCalculating, setIsCalculating] = useState(false); + +// handleAutoCalculate 수정 (line 332-335) +const handleAutoCalculate = async () => { + const item = formData.items[activeItemIndex]; + + if (!item.openWidth || !item.openHeight) { + toast.error('오픈사이즈(W0, H0)를 입력해주세요.'); + return; + } + + setIsCalculating(true); + try { + const response = await quoteApi.calculate({ + inputs: { + W0: parseFloat(item.openWidth), + H0: parseFloat(item.openHeight), + QTY: item.quantity, + INSTALL_TYPE: item.guideRailType, + CONTROL_TYPE: item.controller, + }, + product_category: item.productCategory as 'screen' | 'steel' || 'screen', + }); + + if (response.success) { + setCalculationResult(response.data); + toast.success('자동 산출이 완료되었습니다.'); + } else { + toast.error(response.message || '산출 중 오류가 발생했습니다.'); + } + } catch (error) { + console.error('자동 산출 오류:', error); + toast.error('서버 연결에 실패했습니다.'); + } finally { + setIsCalculating(false); + } +}; + +// 산출 결과 반영 함수 추가 +const handleApplyCalculation = () => { + if (!calculationResult) return; + + // 산출된 품목을 견적 항목에 반영 + const newItems = calculationResult.items.map((item, index) => ({ + id: `calc-${Date.now()}-${index}`, + floor: formData.items[activeItemIndex].floor, + code: item.item_code, + productCategory: formData.items[activeItemIndex].productCategory, + productName: item.item_name, + openWidth: formData.items[activeItemIndex].openWidth, + openHeight: formData.items[activeItemIndex].openHeight, + guideRailType: formData.items[activeItemIndex].guideRailType, + motorPower: formData.items[activeItemIndex].motorPower, + controller: formData.items[activeItemIndex].controller, + quantity: item.quantity, + wingSize: formData.items[activeItemIndex].wingSize, + inspectionFee: item.unit_price, + unitPrice: item.unit_price, + totalAmount: item.total_price, + })); + + setFormData({ + ...formData, + items: [...formData.items.slice(0, activeItemIndex), ...newItems, ...formData.items.slice(activeItemIndex + 1)], + }); + + setCalculationResult(null); + toast.success(`${newItems.length}개 품목이 반영되었습니다.`); +}; +``` + +#### 2.3 산출 결과 표시 UI 추가 + +```tsx +{/* 자동 견적 산출 버튼 아래에 추가 */} +{calculationResult && ( + + + + + 산출 결과 + + + + {/* 계산 변수 */} +
+ {Object.entries(calculationResult.outputs).map(([key, val]) => ( +
+
{val.name}
+
{typeof val.value === 'number' ? val.value.toFixed(2) : val.value}
+
+ ))} +
+ + {/* 산출 품목 */} + + + + + + + + + + + + {calculationResult.items.map((item, i) => ( + + + + + + + + ))} + + + + + + + +
품목코드품목명수량단가금액
{item.item_code}{item.item_name}{item.quantity}{item.unit_price.toLocaleString()}{item.total_price.toLocaleString()}
합계{calculationResult.costs.subtotal.toLocaleString()}원
+ + {/* 반영 버튼 */} + +
+
+)} +``` + +--- + +### Phase 3: 통합 테스트 (1일) + +#### 3.1 테스트 시나리오 + +| 번호 | 테스트 케이스 | 입력값 | 예상 결과 | +|-----|-------------|-------|----------| +| 1 | 기본 스크린 산출 | W0=3000, H0=2500 | 가이드레일 PT-GR-3000, 모터 PT-MOTOR-150 | +| 2 | 대형 스크린 산출 | W0=5000, H0=4000 | 모터 규격 상향 (300K 이상) | +| 3 | 철재 산출 | W0=2000, H0=2000, steel | 중량 M*25 적용 | +| 4 | 품목 반영 | 산출 후 반영 클릭 | 견적 항목에 추가됨 | +| 5 | 에러 처리 | W0/H0 미입력 | "오픈사이즈를 입력해주세요" | + +#### 3.2 검증 체크리스트 + +``` +□ MNG 시뮬레이터에서 수식 계산 정확도 확인 +□ React 자동산출 버튼 클릭 → API 호출 확인 +□ 산출 결과 테이블 정상 표시 +□ "품목에 반영하기" 클릭 → 견적 항목 추가 확인 +□ 견적 저장 시 calculation_inputs 필드 저장 확인 +□ 에러 시 적절한 메시지 표시 +``` + +--- + +## 3. SAM 개발 규칙 요약 + +### 3.1 API 개발 규칙 (CLAUDE.md 참조) + +```php +// Controller: FormRequest + ApiResponse 패턴 +public function calculate(QuoteCalculateRequest $request) +{ + return ApiResponse::handle(function () use ($request) { + return $this->calculationService->calculate($request->validated()); + }, __('message.quote.calculated')); +} + +// Service: 비즈니스 로직 분리 +class QuoteCalculationService extends Service +{ + public function calculate(array $inputs, ?string $productCategory = null): array + { + $tenantId = $this->tenantId(); // 필수 + // ... + } +} + +// 응답 형식 +{ + "success": true, + "message": "견적이 산출되었습니다.", + "data": { ... } +} +``` + +### 3.2 React 개발 패턴 + +```typescript +// API 클라이언트 패턴 (react/src/lib/api/client.ts) +class ApiClient { + async post(endpoint: string, data?: unknown): Promise + async get(endpoint: string): Promise +} + +// 컴포넌트 패턴 +// - shadcn/ui 컴포넌트 사용 +// - toast (sonner) 알림 +// - FormField, Card, Button 등 +``` + +### 3.3 MNG 개발 패턴 + +```php +// Artisan 명령어 패턴 +protected $signature = 'quote:seed-formulas {--tenant=1}'; + +// 모델 사용 +use App\Models\Quote\QuoteFormula; +use App\Models\Quote\QuoteFormulaCategory; + +// 서비스 패턴 +class QuoteFormulaService { + public function __construct( + private FormulaEvaluatorService $evaluator + ) {} +} +``` + +--- + +## 4. 파일 구조 + +``` +SAM/ +├── mng/ +│ ├── app/Console/Commands/ +│ │ └── SeedQuoteFormulasCommand.php # 🆕 Phase 1 +│ ├── app/Models/Quote/ +│ │ ├── QuoteFormula.php # ✅ 있음 +│ │ ├── QuoteFormulaCategory.php # ✅ 있음 +│ │ └── QuoteFormulaRange.php # ✅ 있음 +│ └── app/Services/Quote/ +│ └── FormulaEvaluatorService.php # ✅ 있음 +│ +├── api/ +│ ├── app/Http/Controllers/Api/V1/ +│ │ └── QuoteController.php # ✅ calculate() 있음 +│ ├── app/Services/Quote/ +│ │ ├── QuoteCalculationService.php # ✅ 있음 +│ │ └── FormulaEvaluatorService.php # ✅ 있음 +│ └── database/seeders/ +│ └── QuoteFormulaSeeder.php # 참조용 데이터 +│ +├── react/ +│ ├── src/lib/api/ +│ │ ├── client.ts # ✅ ApiClient 클래스 +│ │ └── quote.ts # 🆕 Phase 2 +│ └── src/components/quotes/ +│ └── QuoteRegistration.tsx # ⚡ Phase 2 수정 +│ +└── docs/plans/ + └── quote-auto-calculation-development-plan.md # 이 문서 +``` + +--- + +## 5. 수식 계산 예시 + +``` +입력: W0=3000mm, H0=2500mm, product_category=screen + +계산 순서: +1. W1 = W0 + 140 = 3140mm (스크린 제작 가로) +2. H1 = H0 + 350 = 2850mm (스크린 제작 세로) +3. M = W1 * H1 / 1000000 = 8.949㎡ (면적) +4. K = M * 2 + W0 / 1000 * 14.17 = 60.41kg (중량) +5. G = H0 + 250 = 2750mm (가이드레일 길이) +6. S = W0 + 220 = 3220mm (케이스 사이즈) + +범위 자동 선택: +- 가이드레일: G=2750 → 2438 < G ≤ 3000 → PT-GR-3000 × 2개 +- 케이스: S=3220 → 3000 < S ≤ 3600 → PT-CASE-3600 × 1개 +- 모터: K=60.41 → 0 < K ≤ 150 → PT-MOTOR-150 × 1개 +``` + +--- + +## 6. 일정 요약 + +| Phase | 작업 | 예상 기간 | 상태 | +|-------|------|----------|------| +| 1 | MNG 시더 명령어 생성 | 1일 | ✅ 완료 | +| 2 | React Quote API 클라이언트 생성 | 0.5일 | ✅ 완료 | +| 3 | React handleAutoCalculate API 연동 | 0.5일 | ✅ 완료 | +| 4 | 산출 결과 UI 추가 | 0.5일 | ✅ 완료 | +| 5 | 문서 업데이트 | 0.5시간 | ✅ 완료 | +| **합계** | | **약 2시간** | ✅ | + +--- + +## 7. 완료된 구현 내역 + +### 생성된 파일 +| 파일 경로 | 역할 | +|----------|------| +| `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | MNG 견적수식 시더 명령어 | +| `react/src/lib/api/quote.ts` | React Quote API 클라이언트 | + +### 수정된 파일 +| 파일 경로 | 변경 내용 | +|----------|----------| +| `react/src/components/quotes/QuoteRegistration.tsx` | handleAutoCalculate API 연동, 산출 결과 UI 추가 | + +### MNG 시더 실행 결과 +``` +✅ 견적수식 시드 완료! +카테고리: 11개 +수식: 18개 +범위: 18개 +``` + +### React 기능 구현 +- `handleAutoCalculate`: API 호출 및 로딩 상태 관리 +- `handleApplyCalculation`: 산출 결과를 견적 항목에 반영 +- 산출 결과 UI: 변수, 품목 테이블, 비용 합계 표시 +- 에러 처리: 입력값 검증, API 에러 토스트 + +--- + +*문서 버전*: 3.0 (구현 완료) +*작성자*: Claude Code +*최종 업데이트*: 2025-12-22 \ No newline at end of file diff --git a/plans/react-api-integration-plan.md b/plans/react-api-integration-plan.md index 91058b6..94a5edf 100644 --- a/plans/react-api-integration-plan.md +++ b/plans/react-api-integration-plan.md @@ -47,19 +47,19 @@ ├─────────────────────────────────────────────────────────────────┤ │ - 도구: https://mng.sam.kr/dev-tools/flow-tester │ │ - 목적: 각 기능의 모든 경우의 수(TC)를 자동화 테스트 │ -│ - 저장: api/docs/flow-tests/{feature}-flow.json │ +│ - 저장: docs/plans/flow-tests/{feature}-flow.json │ └─────────────────────────────────────────────────────────────────┘ ``` **Flow Test JSON 형식:** + ```json { - "name": "기능명 플로우 테스트", - "description": "테스트 설명", + "name": "플로우 이름 (50자 이내)", + "description": "플로우 상세 설명", "version": "1.0", "config": { - "baseUrl": "https://api.sam.kr/api/v1", - "apiKey": "{{$env.FLOW_TESTER_API_KEY}}", + "baseUrl": "", "timeout": 30000, "stopOnFailure": true }, @@ -69,22 +69,69 @@ }, "steps": [ { - "id": "step_id", - "name": "단계명", - "method": "GET|POST|PUT|PATCH|DELETE", - "endpoint": "/endpoint", - "headers": { "Authorization": "Bearer {{login.token}}" }, - "body": { }, + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/api/v1/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { "$.access_token": "@isString" } + }, + "extract": { "token": "$.access_token" } + }, + { + "id": "step2", + "name": "다음 단계", + "method": "GET", + "endpoint": "/api/v1/path", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, "expect": { "status": [200], "jsonPath": { "$.success": true } - }, - "extract": { "key": "$.data.value" } + } } ] } ``` +#### 🔴 Flow Test JSON 작성 규칙 (필수 준수) + +| # | 규칙 | 설명 | +|---|------|------| +| 1 | **config.baseUrl** | 빈 문자열 `""` 로 설정 (서버에서 .env 기본값 사용) | +| 2 | **config.apiKey** | ❌ JSON에 포함하지 않음 (서버에서 자동 주입) | +| 3 | **variables** | `{{$env.XXX}}` 형식으로 환경변수 참조 | +| 4 | **steps[].body** | `{{변수명}}` 형식으로 variables 값 참조 | +| 5 | **headers.Authorization** | `Bearer {{login.token}}` 형식으로 추출한 토큰 사용 | +| 6 | **extract** | 다음 스텝에서 `{{stepId.변수명}}`으로 참조됨 | +| 7 | **endpoint** | `/api/v1`을 포함한 전체 경로 필수 | +| 8 | **flows 배열** | ❌ 사용 금지 - 최상위에 바로 `steps` 배열 사용 | +| 9 | **dependsOn** | ❌ 사용 금지 - steps 순서대로 실행됨 | + +#### 변수 참조 규칙 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ✅ 올바른 참조 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ variables 참조: "user_id": "{{user_id}}" │ +│ 환경변수 참조: "user_id": "{{$env.FLOW_TESTER_USER_ID}}" │ +│ 추출값 참조: "Authorization": "Bearer {{login.token}}" │ +│ 동적 값: "email": "test_{{$timestamp}}@example.com" │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ❌ 잘못된 참조 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ❌ "user_id": "{{variables.user_id}}" (variables. 접두사 사용 금지) │ +│ ❌ "Authorization": "Bearer {{login.accessToken}}" (추출 키 불일치) │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + --- ## 2. 대상 페이지 목록 @@ -93,13 +140,13 @@ | # | React URL (예상) | 기능 | 관련 API | 연동 | Flow Test | |---|-----------------|------|----------|:----:|:---------:| -| 5.1-1 | `/hr/employees/invite` | 사용자 초대 | `POST /v1/users/invite` | ✅ | ⏳ | +| 5.1-1 | `/hr/employees/invite` | 사용자 초대 | `POST /v1/users/invite` | ✅ | ✅ | | 5.1-2 | `/hr/employees/invitations` | 초대 목록 | `GET /v1/users/invitations` | ⏭️ | ⏭️ | -| 5.2-1 | `/settings/notifications` | 알림 설정 | `GET /v1/settings/notifications` | ✅ | ⏳ | -| 5.2-2 | `/settings/notifications` | 알림 설정 수정 | `PUT /v1/settings/notifications` | ✅ | ⏳ | -| 5.3-1 | `/account` | 회원 탈퇴 | `POST /v1/account/withdraw` | ✅ | ⏳ | -| 5.3-2 | `/account` | 사용 중지 | `POST /v1/account/suspend` | ✅ | ⏳ | -| 5.4-1 | `/accounting/sales/{id}` | 거래명세서 | `GET /v1/sales/{id}/statement` | ✅ | ⏳ | +| 5.2-1 | `/settings/notifications` | 알림 설정 | `GET /v1/settings/notifications` | ✅ | ✅ | +| 5.2-2 | `/settings/notifications` | 알림 설정 수정 | `PUT /v1/settings/notifications` | ✅ | ✅ | +| 5.3-1 | `/account` | 회원 탈퇴 | `POST /v1/account/withdraw` | ✅ | ✅ | +| 5.3-2 | `/account` | 사용 중지 | `POST /v1/account/suspend` | ✅ | ✅ | +| 5.4-1 | `/accounting/sales/{id}` | 거래명세서 | `GET /v1/sales/{id}/statement` | ✅ | ✅ | **Phase 5 Flow Test 파일:** - `user-invitation-flow.json` - 사용자 초대 전체 플로우 @@ -111,13 +158,13 @@ | # | React URL (예상) | 기능 | 관련 API | 연동 | Flow Test | |---|-----------------|------|----------|:----:|:---------:| -| 6.1-1 | `/accounting/bad-debts` | 악성채권 목록 | `GET /v1/bad-debts` | ✅ | ⏳ | -| 6.1-2 | `/accounting/bad-debts` | 악성채권 요약 | `GET /v1/bad-debts/summary` | ✅ | ⏳ | -| 6.1-3 | `/accounting/bad-debts/{id}` | 악성채권 상세 | `GET /v1/bad-debts/{id}` | ✅ | ⏳ | -| 6.1-4 | `/accounting/bad-debts/new` | 악성채권 등록 | `POST /v1/bad-debts` | ✅ | ⏳ | -| 6.2-1 | `/settings/popups` | 팝업 목록 | `GET /v1/popups` | ✅ | ⏳ | -| 6.2-2 | `/settings/popups` | 활성 팝업 | `GET /v1/popups/active` | ✅ | ⏳ | -| 6.2-3 | `/settings/popups/{id}` | 팝업 상세 | `GET /v1/popups/{id}` | ✅ | ⏳ | +| 6.1-1 | `/accounting/bad-debts` | 악성채권 목록 | `GET /v1/bad-debts` | ✅ | ✅ | +| 6.1-2 | `/accounting/bad-debts` | 악성채권 요약 | `GET /v1/bad-debts/summary` | ✅ | ✅ | +| 6.1-3 | `/accounting/bad-debts/{id}` | 악성채권 상세 | `GET /v1/bad-debts/{id}` | ✅ | ✅ | +| 6.1-4 | `/accounting/bad-debts/new` | 악성채권 등록 | `POST /v1/bad-debts` | ✅ | ✅ | +| 6.2-1 | `/settings/popups` | 팝업 목록 | `GET /v1/popups` | ✅ | ✅ | +| 6.2-2 | `/settings/popups` | 활성 팝업 | `GET /v1/popups/active` | ✅ | ✅ | +| 6.2-3 | `/settings/popups/{id}` | 팝업 상세 | `GET /v1/popups/{id}` | ✅ | ✅ | **Phase 6 Flow Test 파일:** - `bad-debt-flow.json` - 악성채권 추심관리 CRUD 플로우 @@ -143,14 +190,14 @@ | # | React URL (예상) | 기능 | 관련 API | 연동 | Flow Test | |---|-----------------|------|----------|:----:|:---------:| -| 8.1-1 | `/subscription` | 현재 구독 정보 | `GET /v1/subscriptions/current` | ✅ | ⏳ | -| 8.1-2 | `/subscription` | 사용량 조회 | `GET /v1/subscriptions/usage` | ✅ | ⏳ | -| 8.1-3 | `/subscription/export` | 자료 내보내기 | `POST /v1/subscriptions/export` | ✅ | ⏳ | -| 8.1-4 | `/subscription` | 서비스 해지 | `POST /v1/subscriptions/{id}/cancel` | ✅ | ⏳ | -| 8.2-1 | `/payments` | 결제 내역 | `GET /v1/payments` | ✅ | ⏳ | -| 8.2-2 | `/payments/{id}` | 결제 상세 | `GET /v1/payments/{id}` | ✅ | ⏳ | -| 8.2-3 | `/payments/{id}/statement` | 거래명세서 | `GET /v1/payments/{id}/statement` | ✅ | ⏳ | -| 8.3-1 | `/companies/add` | 회사 추가 | `POST /v1/companies/request` | ✅ | ⏳ | +| 8.1-1 | `/subscription` | 현재 구독 정보 | `GET /v1/subscriptions/current` | ✅ | ✅ | +| 8.1-2 | `/subscription` | 사용량 조회 | `GET /v1/subscriptions/usage` | ✅ | ✅ | +| 8.1-3 | `/subscription/export` | 자료 내보내기 | `POST /v1/subscriptions/export` | ✅ | ✅ | +| 8.1-4 | `/subscription` | 서비스 해지 | `POST /v1/subscriptions/{id}/cancel` | ✅ | ✅ | +| 8.2-1 | `/payments` | 결제 내역 | `GET /v1/payments` | ✅ | ✅ | +| 8.2-2 | `/payments/{id}` | 결제 상세 | `GET /v1/payments/{id}` | ✅ | ✅ | +| 8.2-3 | `/payments/{id}/statement` | 거래명세서 | `GET /v1/payments/{id}/statement` | ✅ | ✅ | +| 8.3-1 | `/companies/add` | 회사 추가 | `POST /v1/companies/request` | ✅ | ✅ | **Phase 8 Flow Test 파일:** - `subscription-flow.json` - 구독관리 플로우 @@ -193,7 +240,7 @@ Step 4: 수정 사항 정리 Step 5: Flow Test 작성 ├── 해당 기능의 모든 TC(Test Case) 도출 ├── Flow Test JSON 작성 -├── api/docs/flow-tests/{feature}-flow.json 저장 +├── docs/plans/flow-tests/{feature}-flow.json 저장 └── https://mng.sam.kr/dev-tools/flow-tester 에서 실행 ``` @@ -326,16 +373,16 @@ Step 5: Flow Test 작성 **Flow Test 저장 위치:** ``` -api/docs/flow-tests/ -├── user-invitation-flow.json -├── notification-settings-flow.json -├── bad-debt-flow.json -├── popup-flow.json -├── board-management-flow.json -├── post-crud-flow.json -├── subscription-flow.json -├── payment-flow.json -└── company-request-flow.json +docs/plans/flow-tests/ +├── user-invitation-flow.json ✅ 완료 (6 steps) +├── notification-settings-flow.json ✅ 완료 (10 steps) +├── account-management-flow.json ✅ 완료 (6 steps) +├── sales-statement-flow.json ✅ 완료 (9 steps) +├── bad-debt-flow.json ✅ 완료 (11 steps) +├── popup-flow.json ✅ 완료 (9 steps) +├── subscription-flow.json ✅ 완료 (13 steps) +├── payment-flow.json ✅ 완료 (12 steps) +└── company-request-flow.json ✅ 완료 (9 steps) ``` ### 3.3 분석 템플릿 @@ -726,6 +773,18 @@ if (checkResult.valid) { | 날짜 | 페이지 | 변경 내용 | 파일 | 승인 | |------|--------|----------|------|------| +| 2025-12-22 | 8.3 | 회사 가입 신청 Flow Test 작성 (9 steps) | `company-request-flow.json` | ✅ | +| 2025-12-22 | 8.2 | 결제 관리 Flow Test 작성 (12 steps) | `payment-flow.json` | ✅ | +| 2025-12-22 | 8.1 | 구독 관리 Flow Test 작성 (13 steps) | `subscription-flow.json` | ✅ | +| 2025-12-22 | 6.2 | 팝업 관리 Flow Test 작성 (9 steps) | `popup-flow.json` | ✅ | +| 2025-12-22 | 6.1 | 부실채권 관리 Flow Test 작성 (11 steps) | `bad-debt-flow.json` | ✅ | +| 2025-12-22 | 5.4 | 매출 명세서 Flow Test 작성 (9 steps) | `sales-statement-flow.json` | ✅ | +| 2025-12-22 | 5.3 | 계정 관리 Flow Test 작성 (6 steps) | `account-management-flow.json` | ✅ | +| 2025-12-22 | 5.2 | 알림 설정 Flow Test 작성 (10 steps) | `notification-settings-flow.json` | ✅ | +| 2025-12-22 | - | Flow Test JSON 작성 규칙 정리 (9개 필수 규칙 + 변수 참조 규칙) | 문서 | ✅ | +| 2025-12-22 | - | endpoint 정책 추가 (/api/v1 전체 경로 필수) | 문서 + user-invitation-flow.json | ✅ | +| 2025-12-22 | - | Flow Test 파일 통합 (docs/plans/flow-tests/로 이동) | 9개 파일 이동 | ✅ | +| 2025-12-22 | 5.1-1 | 사용자 초대 Flow Test 작성 (14 flows, P1~P3) | `user-invitation-flow.json` | ✅ | | 2025-12-22 | 8.1 | 구독관리 API 분석 - 완전 구현됨, 필드명 매핑 필요 | - | ✅ | | 2025-12-22 | 8.2 | 결제내역 API 분석 - 완전 구현됨, 필드명 매핑 필요 | - | ✅ | | 2025-12-22 | 8.3 | 회사 추가 API 분석 - 완전 구현됨, React 연동만 필요 | - | ✅ | @@ -750,4 +809,4 @@ if (checkResult.valid) { - **API Swagger UI**: http://sam.kr/api-docs/index.html - **React 개발 서버**: http://dev.sam.kr - **Flow Tester**: https://mng.sam.kr/dev-tools/flow-tester -- **Flow Test 예제**: `api/claudedocs/flow-tester-*.json` +- **Flow Test 파일**: `docs/plans/flow-tests/*.json` diff --git a/plans/react-mock-to-api-migration-plan.md b/plans/react-mock-to-api-migration-plan.md index d9d12df..51eaa15 100644 --- a/plans/react-mock-to-api-migration-plan.md +++ b/plans/react-mock-to-api-migration-plan.md @@ -72,22 +72,22 @@ ListClient.tsx (클라이언트) | # | 페이지 | React 경로 | API 엔드포인트 | 상태 | |---|--------|-----------|---------------|------| | A-1 | 악성채권 관리 | `/accounting/bad-debt-collection` | `GET/POST /v1/bad-debts` | ✅ 완료 | -| A-2 | 팝업 관리 | `/settings/popup-management` | `GET/POST /v1/popups` | ⏳ 대기 | -| A-3 | 결제 내역 | `/settings/payment-history` | `GET /v1/payments` | ⏳ 대기 | -| A-4 | 구독 관리 | `/settings/subscription` | `GET /v1/subscriptions` | ⏳ 대기 | -| A-5 | 알림 설정 | `/settings/notifications` | `GET/PUT /v1/settings/notifications` | ⏳ 대기 | -| A-6 | 거래처 원장 | `/accounting/vendor-ledger` | `GET /v1/vendor-ledger` | ⏳ 대기 | +| A-2 | 팝업 관리 | `/settings/popup-management` | `GET/POST /v1/popups` | ✅ 완료 | +| A-3 | 결제 내역 | `/settings/payment-history` | `GET /v1/payments` | ✅ 완료 | +| A-4 | 구독 관리 | `/settings/subscription` | `GET /v1/subscriptions` | ✅ 완료 | +| A-5 | 알림 설정 | `/settings/notifications` | `GET/PUT /v1/settings/notifications` | ✅ 완료 | +| A-6 | 거래처 원장 | `/accounting/vendor-ledger` | `GET /v1/vendor-ledger` | ⏭️ API 미존재 | ### 2.2 Phase B: 핵심 업무 기능 | # | 페이지 | React 경로 | API 엔드포인트 | 상태 | |---|--------|-----------|---------------|------| -| B-1 | 매출 관리 | `/accounting/sales` | `GET/POST /v1/sales` | ⏳ 대기 | -| B-2 | 매입 관리 | `/accounting/purchase` | `GET/POST /v1/purchases` | ⏳ 대기 | -| B-3 | 입금 관리 | `/accounting/deposit` | `GET/POST /v1/deposits` | ⏳ 대기 | -| B-4 | 출금 관리 | `/accounting/withdrawal` | `GET/POST /v1/withdrawals` | ⏳ 대기 | -| B-5 | 거래처 관리 | `/accounting/vendor` | `GET/POST /v1/clients` | ⏳ 대기 | -| B-6 | 어음 관리 | `/accounting/bill` | `GET/POST /v1/bills` | ⏳ 대기 | +| B-1 | 매출 관리 | `/accounting/sales` | `GET/POST /v1/sales` | ✅ 완료 (기존 연동) | +| B-2 | 매입 관리 | `/accounting/purchase` | `GET/POST /v1/purchases` | ✅ 완료 (기존 연동) | +| B-3 | 입금 관리 | `/accounting/deposit` | `GET/POST /v1/deposits` | ✅ 완료 | +| B-4 | 출금 관리 | `/accounting/withdrawal` | `GET/POST /v1/withdrawals` | ✅ 완료 | +| B-5 | 거래처 관리 | `/accounting/vendor` | `GET/POST /v1/clients` | ✅ 완료 | +| B-6 | 어음 관리 | `/accounting/bills` | `GET/POST /v1/bills` | ✅ 완료 | ### 2.3 Phase C: 인사/근태 @@ -776,10 +776,11 @@ export function transformFrontendToApi(data: Record): Record): Record SAM 시스템 개발 프로젝트별 문서 모음 +> **최종 업데이트**: 2025-12-23 + +--- + +## 프로젝트 현황 요약 + +| 프로젝트 | 상태 | 설명 | +|---------|------|------| +| [mes](#mes---meserp-프로젝트) | 🟡 진행중 | 차세대 MES/ERP 기능 개발 | +| [quotation](#quotation---견적-기능) | 🟢 Phase 3 완료 | 5130 견적 → SAM 이관 | +| [api-integration](#api-integration---react--api-연동) | 🟡 진행중 | React ↔ API 연동 | +| [5130-migration](#5130-migration---품목-마이그레이션) | 🟡 Phase 1 진행중 | 5130 품목 데이터 마이그레이션 | +| [legacy-5130](#legacy-5130---레거시-분석) | 📚 참조용 | 5130 레거시 모듈 분석 | +| [mng-mobile-responsive](#mng-mobile-responsive---모바일-반응형) | 🟡 진행중 | mng 모바일 반응형 개선 | +| [auto-login](#auto-login---자동-로그인) | ⚪ 대기 | 자동 로그인 기능 | +| [migration-5130-mng](#migration-5130-mng---5130--mng-마이그레이션) | 🟡 진행중 | 5130 → mng 통합 마이그레이션 | + +--- + +## 프로젝트 상세 + +### mes - MES/ERP 프로젝트 + +**경로**: `docs/projects/mes/` +**상태**: 🟡 Phase 0 (베이스라인 분석) 30% 완료 +**목표**: SAM 시스템의 차세대 MES/ERP 기능 개발 + +**핵심 문서**: +- [README.md](./mes/README.md) - 프로젝트 개요 및 문서 안내 +- [MES_PROGRESS_TRACKER.md](./mes/MES_PROGRESS_TRACKER.md) - 진행 상황 추적 +- [MES_PROJECT_ROADMAP.md](./mes/MES_PROJECT_ROADMAP.md) - 전체 로드맵 + +**분석 결과**: +- `00_baseline/` - Phase 0 분석 결과 + - [PHASE_0_FINAL_REPORT.md](./mes/00_baseline/PHASE_0_FINAL_REPORT.md) + - [BACKEND_DEVELOPMENT_ROADMAP_V2.md](./mes/00_baseline/BACKEND_DEVELOPMENT_ROADMAP_V2.md) + - `docs_breakdown/` - 문서 분석 (7개) + +**v2 분석**: +- `v2-analysis/` - MES v2 화면 분석 + - `quote-analysis/` - 견적 분석 + - `order-analysis/` - 주문 분석 + - `production-analysis/` - 생산 분석 + - `customer-analysis/` - 거래처 분석 + - `site-analysis/` - 현장 분석 + - `price-analysis/` - 단가 분석 + - `master-data-analysis/` - 기준정보 분석 + - `production-userflow/` - 생산 유저플로우 + +--- + +### quotation - 견적 기능 + +**경로**: `docs/projects/quotation/` +**상태**: 🟢 Phase 3 완료 (2025-12-19) +**목표**: 5130 레거시 견적 기능을 SAM 시스템으로 이관 + +**핵심 문서**: +- [MASTER_PLAN.md](./quotation/MASTER_PLAN.md) - 마스터 플랜 +- [PROGRESS.md](./quotation/PROGRESS.md) - 진행 현황 + +**Phase 문서**: +| Phase | 상태 | 경로 | +|-------|------|------| +| 1. 5130 분석 | ✅ 완료 | `phase-1-5130-analysis/` | +| 2. mng 분석 | ✅ 완료 | `phase-2-mng-analysis/` | +| 3. 구현 | ✅ 완료 | `phase-3-implementation/` | +| 4. API 개발 | ⚪ 대기 | `phase-4-api/` | + +**참조 자료**: +- `screenshots/` - MES 프로토타입 화면 캡쳐 (7개) + +--- + +### api-integration - React ↔ API 연동 + +**경로**: `docs/projects/api-integration/` +**상태**: 🟡 Phase 4 진행중 +**목표**: React(dev.sam.kr)와 API(api.sam.kr) 완벽 연동 + +**핵심 문서**: +- [MASTER_PLAN.md](./api-integration/MASTER_PLAN.md) - 마스터 플랜 +- [PROGRESS.md](./api-integration/PROGRESS.md) - 진행 현황 +- [WORKFLOW.md](./api-integration/WORKFLOW.md) - 작업 프로세스 + +**Phase 문서**: +| Phase | 상태 | 경로 | +|-------|------|------| +| 1. 테이블 통합 | 🟢 완료(스킵) | `phase-1-table-migration/` | +| 2. 메뉴 추출 | 🟡 진행중 | `phase-2-menu-extraction/` | +| 3. API 매핑 | 🟡 진행중 | `phase-3-api-mapping/` | +| 4. 연동+검증 | 🟡 진행중 | `phase-4-integration/` | + +**TC 파일**: `phase-4-integration/tc/` - 기능별 테스트 케이스 JSON (17개) + +--- + +### 5130-migration - 품목 마이그레이션 + +**경로**: `docs/projects/5130-migration/` +**상태**: 🟡 Phase 1 진행중 +**목표**: 5130 품목(부품, 자재, BOM) 데이터를 SAM DB로 이전 + +**핵심 문서**: +- [MASTER_PLAN.md](./5130-migration/MASTER_PLAN.md) - 마스터 플랜 +- [PROGRESS.md](./5130-migration/PROGRESS.md) - 진행 현황 + +**Phase 문서**: +| Phase | 상태 | 경로 | +|-------|------|------| +| 1. 소스 분석 | 🟡 진행중 | `phase-1-source-analysis/` | +| 2. 타겟 분석 | ⚪ 대기 | `phase-2-target-analysis/` | +| 3. 매핑 설계 | ⚪ 대기 | `phase-3-mapping/` | + +--- + +### legacy-5130 - 레거시 분석 + +**경로**: `docs/projects/legacy-5130/` +**상태**: 📚 참조용 문서 +**용도**: 5130 레거시 시스템 모듈별 분석 + +**모듈별 분석 문서**: +| 문서 | 내용 | +|------|------| +| [00_OVERVIEW.md](./legacy-5130/00_OVERVIEW.md) | 시스템 개요 | +| [01_MATERIAL.md](./legacy-5130/01_MATERIAL.md) | 자재 관리 | +| [02_PRODUCT.md](./legacy-5130/02_PRODUCT.md) | 제품 관리 | +| [03_ESTIMATE.md](./legacy-5130/03_ESTIMATE.md) | 견적 관리 | +| [04_PRODUCTION.md](./legacy-5130/04_PRODUCTION.md) | 생산 관리 | +| [05_SHIPPING.md](./legacy-5130/05_SHIPPING.md) | 출하 관리 | +| [06_QUALITY.md](./legacy-5130/06_QUALITY.md) | 품질 관리 | +| [07_ACCOUNTING.md](./legacy-5130/07_ACCOUNTING.md) | 회계 관리 | +| [08_SAM_COMPARISON.md](./legacy-5130/08_SAM_COMPARISON.md) | SAM 비교 분석 | +| [draw-module.md](./legacy-5130/draw-module.md) | 도면 모듈 | + +--- + +### mng-mobile-responsive - 모바일 반응형 + +**경로**: `docs/projects/mng-mobile-responsive/` +**상태**: 🟡 진행중 +**목표**: mng 관리자 패널 모바일 반응형 개선 + +**문서**: +- [01-analysis.md](./mng-mobile-responsive/01-analysis.md) - 분석 +- [02-implementation-plan.md](./mng-mobile-responsive/02-implementation-plan.md) - 구현 계획 +- [06-excluded-menus.md](./mng-mobile-responsive/06-excluded-menus.md) - 제외 메뉴 +- [PROGRESS.md](./mng-mobile-responsive/PROGRESS.md) - 진행 현황 + +--- + +### auto-login - 자동 로그인 + +**경로**: `docs/projects/auto-login/` +**상태**: ⚪ 대기 +**목표**: 자동 로그인 기능 구현 + +**문서**: +- [PROGRESS.md](./auto-login/PROGRESS.md) - 진행 현황 + +--- + +### migration-5130-mng - 5130 → mng 마이그레이션 + +**경로**: `docs/projects/migration-5130-mng/` +**상태**: 🟡 진행중 +**목표**: 5130 기능을 mng로 통합 마이그레이션 + +**문서**: +- [MIGRATION_TRACKER.md](./migration-5130-mng/MIGRATION_TRACKER.md) - 마이그레이션 추적 + +--- + +## 디렉토리 구조 + +``` +docs/projects/ +├── INDEX.md # 이 파일 +├── mes/ # MES/ERP 프로젝트 (핵심) +│ ├── 00_baseline/ # Phase 0 분석 +│ ├── v1-analysis/ # v1 분석 +│ ├── v2-analysis/ # v2 화면 분석 +│ └── phases/ # Phase별 진행 +├── quotation/ # 견적 기능 +│ ├── phase-1-5130-analysis/ +│ ├── phase-2-mng-analysis/ +│ ├── phase-3-implementation/ +│ └── screenshots/ +├── api-integration/ # React ↔ API 연동 +│ ├── phase-1-table-migration/ +│ ├── phase-2-menu-extraction/ +│ ├── phase-3-api-mapping/ +│ └── phase-4-integration/tc/ +├── 5130-migration/ # 품목 마이그레이션 +│ ├── phase-1-source-analysis/ +│ ├── phase-2-target-analysis/ +│ └── phase-3-mapping/ +├── legacy-5130/ # 레거시 분석 (참조용) +├── mng-mobile-responsive/ # 모바일 반응형 +├── auto-login/ # 자동 로그인 +└── migration-5130-mng/ # 5130→mng 마이그레이션 +``` + +--- + +## 관련 문서 + +- [docs/INDEX.md](../INDEX.md) - 전체 문서 인덱스 +- [docs/plans/index_plans.md](../plans/index_plans.md) - 기획 문서 인덱스 +- [docs/guides/PROJECT_DEVELOPMENT_POLICY.md](../guides/PROJECT_DEVELOPMENT_POLICY.md) - 공통 개발 정책 +- [CURRENT_WORKS.md](../../CURRENT_WORKS.md) - 현재 작업 + +--- + +**범례**: +- 🟢 완료 +- 🟡 진행중 +- ⚪ 대기 +- 📚 참조용 \ No newline at end of file diff --git a/requests/prompt-workflow-sharing.md b/requests/prompt-workflow-sharing.md new file mode 100644 index 0000000..e9d7f08 --- /dev/null +++ b/requests/prompt-workflow-sharing.md @@ -0,0 +1,287 @@ +# Claude Code 프롬프트 워크플로우 공유 + +> **목적:** 팀 내 Claude Code 사용 방식 공유 및 개선 논의 +> **작성일:** 2025-12-23 +> **작성자:** hskwon + +--- + +## 📋 현재 사용 중인 워크플로우 + +### 1. Code-Workflow 스킬 (5단계 강제 프로세스) + +코드 수정 시 체계적인 프로세스를 **강제**하는 커스텀 스킬입니다. + +``` +분석 → 수정 → 검증 → 정리 → 커밋 +``` + +#### 왜 사용하는가? + +| 문제 | 해결책 | +|------|--------| +| Claude가 분석 없이 바로 수정 시작 | 1단계에서 **분석 후 승인** 강제 | +| 여러 파일 동시 수정 → 롤백 어려움 | 한 번에 하나씩 순차 수정 | +| 수정 후 검증 생략 | 3단계 검증 필수 | +| 커밋 메시지 대충 작성 | 4단계에서 변경 내용 정리 문서화 | +| 자동 커밋으로 실수 | 5단계에서 **커밋 전 승인** 강제 | + +#### 핵심 규칙 + +```markdown +🔴 필수 규칙 (반드시 준수) + +1. 분석 후 승인 필수 + - 분석 결과 + 일정 산정 제시 + - AskUserQuestion으로 승인 요청 + - 승인 없이 수정 진행 금지 + +2. 커밋 전 승인 필수 + - 변경 내용 요약 제시 + - 승인 없이 커밋 금지 + - 승인 후 → 요약 MD 작성 → 커밋 +``` + +#### 스킬 호출 방법 + +```bash +/code-workflow # 또는 Claude가 코드 수정 요청 시 자동 활성화 +``` + +--- + +### 2. 프로젝트 문서 기반 진행 관리 + +#### 문서 구조 + +``` +docs/projects/{프로젝트명}/ +├── MASTER_PLAN.md # 마스터 플랜 (전체 로드맵) +├── PROGRESS.md # 진행 현황 (실시간 업데이트) +├── screenshots/ # 참조 이미지 +├── phase-1-xxx/ # Phase별 상세 문서 +│ ├── README.md # Phase 체크리스트 및 요약 +│ └── *.md # 분석/구현 문서 +├── phase-2-xxx/ +└── phase-3-xxx/ +``` + +#### 핵심 문서 역할 + +| 문서 | 역할 | 업데이트 주기 | +|------|------|---------------| +| `MASTER_PLAN.md` | 전체 방향, Phase 정의, 핵심 원칙 | 큰 변경 시 | +| `PROGRESS.md` | 현재 진행률, 완료 작업, 다음 작업 | **매 작업 완료 시** | +| `phase-*/README.md` | 각 Phase 체크리스트 | Phase 진행 중 | + +#### 실제 사용 예시 (견적 프로젝트) + +```markdown +# PROGRESS.md 예시 + +## 전체 진행률 + +| Phase | 상태 | 진행률 | 시작일 | 완료일 | +|-------|------|--------|--------|--------| +| Phase 1: 5130 분석 | ✅ 완료 | 100% | 2025-12-19 | 2025-12-19 | +| Phase 2: mng 분석 | ✅ 완료 | 100% | 2025-12-19 | 2025-12-19 | +| Phase 3: 구현 | ✅ 완료 | 100% | 2025-12-19 | 2025-12-19 | +| Phase 4: API 개발 | 🔄 진행 | 60% | 2025-12-19 | - | + +## 🔄 현재 작업 + +**현재 Phase:** Phase 4 진행 중 +**완료된 작업:** mng 패턴 적용 - DB 기반 견적 산출 서비스 재작성 +**다음 작업:** 견적 API 통합 테스트 및 Swagger 문서화 +``` + +--- + +### 3. WORKFLOW_STATE.md (작업 중 상태 추적) + +코드 수정 중 **중단 후 재개**를 위한 임시 상태 파일입니다. + +#### 생성 시점 +- code-workflow 2단계(순차 수정) 시작 시 생성 +- 5단계(커밋) 완료 후 삭제 + +#### 내용 + +```markdown +# Workflow 진행 상태 + +## 현재 단계: 2단계 (순차 수정) + +## 전체 작업 목록 +- [x] 1단계: 분석 완료 +- [ ] 2단계: 순차 수정 (3/7 완료) +- [ ] 3단계: 검증 대기 +- [ ] 4단계: 문서 정리 대기 +- [ ] 5단계: 커밋 대기 + +## 순차 수정 상세 (2단계) +- [x] 수정 1/7: tenants 마이그레이션 생성 +- [x] 수정 2/7: folders 마이그레이션 생성 +- [x] 수정 3/7: files 마이그레이션 확장 +- [ ] 수정 4/7: FileStorageService 구현 ← 현재 진행 중 +- [ ] 수정 5/7: Tenant 모델 확장 +- [ ] 수정 6/7: File 모델 확장 +- [ ] 수정 7/7: Folder 모델 생성 +``` + +#### 재개 시 + +``` +사용자: "어디까지 진행했지?" + +Claude: [WORKFLOW_STATE.md 확인] + +📋 작업 진행 현황 +- 현재 단계: 2단계 (순차 수정) +- 전체 진행률: 43% (3/7 완료) +- 마지막 완료: files 마이그레이션 확장 + +계속 진행할까요? (y/n) +``` + +--- + +### 4. 도구 사용 기록 + +모든 단계에서 **사용된 도구를 명시적으로 기록**합니다. + +#### Footer 형식 + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔧 사용된 도구 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📦 MCP 서버 +- Sequential Thinking: 복잡한 의존성 분석 +- Context7: Laravel Storage 공식 문서 조회 + +🧠 SuperClaude 페르소나 +- backend-architect: 파일 저장 시스템 아키텍처 설계 +- security-engineer: 파일 업로드 보안 검토 + +🛠️ 네이티브 도구 +- Read: 5회 (File.php, Tenant.php 등) +- Glob: 3회 +- Grep: 2회 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## 💡 이 방식의 장점 + +### 1. 세션 중단에 강함 +- `PROGRESS.md` + `WORKFLOW_STATE.md`로 언제든 이어서 작업 가능 +- Claude 컨텍스트가 끊겨도 문서만 읽으면 상태 파악 + +### 2. 실수 방지 +- 분석 → 승인 → 수정 순서 강제 +- 커밋 전 승인 강제 +- 여러 파일 동시 수정 금지 + +### 3. 문서화 자동화 +- 변경 내용이 자동으로 `docs/changes/` 에 기록 +- 나중에 "왜 이렇게 바꿨지?" 추적 가능 + +### 4. 복잡한 프로젝트 관리 +- Phase별 분리로 큰 프로젝트도 관리 가능 +- 체크리스트 기반으로 누락 방지 + +--- + +## ⚠️ 이 방식의 단점 + +### 1. 초기 설정 비용 +- 스킬 파일, CLAUDE.md 설정 필요 +- 프로젝트별 문서 구조 생성 필요 + +### 2. 간단한 작업에는 과함 +- 오타 수정 같은 건 5단계가 불필요 +- 예외 처리 규칙 필요 (hotfix 모드 등) + +### 3. Claude 의존 +- Claude가 규칙을 "잊어버리면" 무용지물 +- 가끔 리마인드 필요 + +--- + +## 🤔 논의 포인트 + +### 1. 다른 분들은 어떻게 사용하고 계신가요? + +- 프로젝트 문서 구조는? +- 코드 수정 시 어떤 프로세스? +- 세션 중단 후 재개 방법? +- 커밋 관리 방식? + +### 2. 개선 아이디어 + +- 현재 방식에서 불편한 점? +- 더 좋은 문서 구조? +- 더 나은 진행 상태 추적 방법? + +### 3. 공통화 가능한 부분 + +- 팀 공통 CLAUDE.md? +- 공통 스킬 라이브러리? +- 프로젝트 템플릿? + +--- + +## 📁 관련 파일 + +| 파일 | 설명 | +|------|------| +| `.claude/skills/code-workflow/SKILL.md` | code-workflow 스킬 정의 | +| `CLAUDE.md` | 프로젝트 지시사항 | +| `docs/projects/*/PROGRESS.md` | 프로젝트별 진행 현황 | +| `docs/projects/*/MASTER_PLAN.md` | 프로젝트별 마스터 플랜 | + +--- + +## 📝 스킬 전문 (참고용) + +
+code-workflow 스킬 전체 내용 (클릭해서 펼치기) + +```markdown +# 5단계 워크플로우 요약 + +## 1단계: 분석 (ANALYZE) +- SAM 프로젝트 컨텍스트 로드 +- MCP로 관련 파일 검색 및 의존성 분석 +- 영향받는 파일 식별 +- 예상 부작용/리스크 파악 +- **사용자 확인 후 다음 단계로** + +## 2단계: 순차 수정 (MODIFY) +- WORKFLOW_STATE.md 생성 +- 한 번에 하나씩 수정 +- 각 수정 완료 후 상태 업데이트 +- 이전 코드 주석 처리 (삭제 금지) + +## 3단계: 검증 (VERIFY) +- 문법 오류 확인 (Pint) +- 로직 검증 +- 기존 기능 유지 확인 +- 보안/성능 체크 + +## 4단계: 변경 내용 정리 (SUMMARY) +- docs/changes/YYYYMMDD_HHMM_feature_name.md 생성 +- CURRENT_WORKS.md 업데이트 + +## 5단계: Git 커밋 (COMMIT) +- 변경 파일 확인 +- 커밋 메시지 제안 +- **사용자 확인 후 커밋** +- WORKFLOW_STATE.md 삭제 +``` + +
\ No newline at end of file