docs: Item Master 스펙 문서 추가

- ITEM-MASTER-INDEX.md: 문서 인덱스 및 핵심 개념 정의
- item-master-field-integration.md: 범용 메타 필드 시스템 구현 계획 (v1.3)
- item-master-field-key-validation.md: field_key 검증 정책
This commit is contained in:
2025-12-09 20:28:38 +09:00
parent 12ce8b84b4
commit 182c2d1b57
9 changed files with 1663 additions and 3246 deletions

View File

@@ -1,523 +0,0 @@
# 거래처 관리 API 분석 및 구현 현황
> **작성일**: 2025-12-04
> **최종 업데이트**: 2025-12-08
> **상태**: ✅ **백엔드 + 프론트엔드 구현 완료**
---
## 1. 구현 현황 요약
### ✅ 백엔드 API 구조 (구현 완료)
#### Client (거래처) API
| Method | Endpoint | 설명 | 상태 |
|--------|----------|------|------|
| `GET` | `/api/v1/clients` | 목록 조회 (페이지네이션, 검색) | ✅ 완료 |
| `GET` | `/api/v1/clients/{id}` | 단건 조회 | ✅ 완료 |
| `POST` | `/api/v1/clients` | 생성 | ✅ 완료 |
| `PUT` | `/api/v1/clients/{id}` | 수정 | ✅ 완료 |
| `DELETE` | `/api/v1/clients/{id}` | 삭제 | ✅ 완료 |
| `PATCH` | `/api/v1/clients/{id}/toggle` | 활성/비활성 토글 | ✅ 완료 |
#### Client Group (거래처 그룹) API
| Method | Endpoint | 설명 | 상태 |
|--------|----------|------|------|
| `GET` | `/api/v1/client-groups` | 그룹 목록 | ✅ 완료 |
| `GET` | `/api/v1/client-groups/{id}` | 그룹 단건 | ✅ 완료 |
| `POST` | `/api/v1/client-groups` | 그룹 생성 | ✅ 완료 |
| `PUT` | `/api/v1/client-groups/{id}` | 그룹 수정 | ✅ 완료 |
| `DELETE` | `/api/v1/client-groups/{id}` | 그룹 삭제 | ✅ 완료 |
| `PATCH` | `/api/v1/client-groups/{id}/toggle` | 그룹 활성/비활성 | ✅ 완료 |
### ✅ 프론트엔드 구현 현황 (구현 완료)
| 작업 | 상태 | 파일 |
|------|------|------|
| API Proxy | ✅ 완료 | `/api/proxy/[...path]/route.ts` (catch-all) |
| `useClientList` 훅 | ✅ 완료 | `src/hooks/useClientList.ts` |
| 타입 정의 (확장 필드 포함) | ✅ 완료 | `src/hooks/useClientList.ts` |
| 거래처 등록 페이지 | ✅ 완료 | `.../client-management-sales-admin/new/page.tsx` |
| 거래처 상세 페이지 | ✅ 완료 | `.../client-management-sales-admin/[id]/page.tsx` |
| 거래처 수정 페이지 | ✅ 완료 | `.../client-management-sales-admin/[id]/edit/page.tsx` |
| 목록 페이지 개선 | ✅ 완료 | 모달 삭제, 페이지 네비게이션 적용 |
---
## 2. 테이블 스키마 (구현 완료)
### 2.1 `clients` 테이블 ✅
```sql
CREATE TABLE clients (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
client_group_id BIGINT NULL,
-- 기본 정보
client_code VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
client_type ENUM('매입','매출','매입매출') DEFAULT '매입',
-- 사업자 정보
business_no VARCHAR(20) NULL, -- 사업자등록번호
business_type VARCHAR(50) NULL, -- 업태
business_item VARCHAR(100) NULL, -- 업종
-- 연락처 정보
contact_person VARCHAR(100) NULL, -- 대표 담당자
phone VARCHAR(20) NULL,
mobile VARCHAR(20) NULL,
fax VARCHAR(20) NULL,
email VARCHAR(255) NULL,
address TEXT NULL,
-- 담당자 정보
manager_name VARCHAR(50) NULL, -- 담당자명
manager_tel VARCHAR(20) NULL, -- 담당자 전화
system_manager VARCHAR(50) NULL, -- 시스템 관리자
-- 발주처 설정
account_id VARCHAR(50) NULL, -- 계정 ID
account_password VARCHAR(255) NULL, -- 비밀번호 (암호화, hidden)
purchase_payment_day VARCHAR(20) NULL, -- 매입 결제일
sales_payment_day VARCHAR(20) NULL, -- 매출 결제일
-- 약정 세금
tax_agreement BOOLEAN DEFAULT FALSE,
tax_amount DECIMAL(15,2) NULL,
tax_start_date DATE NULL,
tax_end_date DATE NULL,
-- 악성채권 정보
bad_debt BOOLEAN DEFAULT FALSE,
bad_debt_amount DECIMAL(15,2) NULL,
bad_debt_receive_date DATE NULL,
bad_debt_end_date DATE NULL,
bad_debt_progress ENUM('협의중','소송중','회수완료','대손처리') NULL,
-- 기타
memo TEXT NULL,
is_active TINYINT(1) DEFAULT 1, -- 1=활성, 0=비활성
-- 감사 컬럼
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 2.2 `client_groups` 테이블 ✅
```sql
CREATE TABLE client_groups (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
group_code VARCHAR(30) NOT NULL, -- 그룹 코드 (필수)
group_name VARCHAR(100) NOT NULL, -- 그룹명 (필수)
price_rate DECIMAL(6,4) NOT NULL, -- 단가율 (필수, 0~99.9999)
is_active BOOLEAN DEFAULT TRUE,
created_by BIGINT NULL,
updated_by BIGINT NULL,
deleted_by BIGINT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP NULL -- Soft Delete
);
```
**⚠️ 주의**: `client_groups` API 필드명
- `group_code` (필수) - 그룹 코드
- `group_name` (필수) - 그룹명 (`name`이 아님!)
- `price_rate` (필수) - 단가율
- `is_active` (선택) - boolean
---
## 3. API 엔드포인트 상세 (구현 완료)
### 3.1 목록 조회 `GET /api/v1/clients` ✅
**Query Parameters:**
| 파라미터 | 타입 | 설명 | 기본값 |
|---------|------|------|--------|
| `page` | int | 페이지 번호 | 1 |
| `size` | int | 페이지당 개수 | 20 |
| `q` | string | 검색어 (이름, 코드, 담당자) | - |
| `only_active` | boolean | 활성 거래처만 조회 | - |
**Response:**
```json
{
"success": true,
"message": "message.fetched",
"data": {
"current_page": 1,
"data": [
{
"id": 1,
"client_code": "CLI-001",
"name": "ABC상사",
"client_type": "매입매출",
"business_no": "123-45-67890",
"business_type": "제조업",
"business_item": "기계부품",
"contact_person": "홍길동",
"phone": "02-1234-5678",
"mobile": "010-1234-5678",
"fax": "02-1234-5679",
"email": "hong@abc.com",
"address": "서울시 강남구...",
"manager_name": "김철수",
"manager_tel": "02-1234-5680",
"purchase_payment_day": "말일",
"sales_payment_day": "말일",
"tax_agreement": false,
"bad_debt": false,
"memo": null,
"is_active": true,
"client_group": { "id": 1, "name": "VIP" }
}
],
"total": 1
}
}
```
### 3.2 단건 조회 `GET /api/v1/clients/{id}` ✅
단건 조회 시 모든 필드 포함 (목록과 동일 구조)
### 3.3 거래처 등록 `POST /api/v1/clients` ✅
**Request Body:**
```json
{
"client_code": "CLI-002",
"name": "XYZ무역",
"client_type": "매입",
"client_group_id": 1,
"business_no": "234-56-78901",
"business_type": "도소매업",
"business_item": "전자부품",
"contact_person": "이영희",
"phone": "02-2345-6789",
"mobile": "010-2345-6789",
"fax": "02-2345-6790",
"email": "lee@xyz.com",
"address": "서울시 서초구...",
"manager_name": "박민수",
"manager_tel": "02-2345-6791",
"system_manager": "관리자A",
"account_id": "xyz_user",
"account_password": "secret123",
"purchase_payment_day": "15일",
"sales_payment_day": "말일",
"tax_agreement": true,
"tax_amount": 1000000,
"tax_start_date": "2025-01-01",
"tax_end_date": "2025-12-31",
"bad_debt": false,
"memo": "신규 거래처"
}
```
### 3.4 거래처 수정 `PUT /api/v1/clients/{id}` ✅
등록과 동일한 필드 구조 (부분 수정 가능)
**주의:** `account_password`는 입력한 경우에만 업데이트됨
### 3.5 거래처 삭제 `DELETE /api/v1/clients/{id}` ✅
Hard Delete 적용 (연관 주문이 없는 경우만 삭제 가능)
### 3.6 활성/비활성 토글 `PATCH /api/v1/clients/{id}/toggle` ✅
**Response:**
```json
{
"success": true,
"message": "message.updated",
"data": {
"id": 1,
"is_active": false
}
}
```
---
## 4. 필드 구조 (7개 섹션)
### 섹션 1: 기본 정보
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `client_code` | string | ✅ | 거래처 코드 |
| `name` | string | ✅ | 거래처명 |
| `client_type` | enum | ❌ | 매입/매출/매입매출 (기본: 매입) |
| `client_group_id` | int | ❌ | 거래처 그룹 ID |
### 섹션 2: 사업자 정보
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `business_no` | string | ❌ | 사업자등록번호 |
| `business_type` | string | ❌ | 업태 |
| `business_item` | string | ❌ | 업종 |
### 섹션 3: 연락처 정보
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `contact_person` | string | ❌ | 대표 담당자 |
| `phone` | string | ❌ | 전화번호 |
| `mobile` | string | ❌ | 휴대폰 |
| `fax` | string | ❌ | 팩스 |
| `email` | string | ❌ | 이메일 |
| `address` | text | ❌ | 주소 |
### 섹션 4: 담당자 정보
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `manager_name` | string | ❌ | 담당자명 |
| `manager_tel` | string | ❌ | 담당자 전화 |
| `system_manager` | string | ❌ | 시스템 관리자 |
### 섹션 5: 발주처 설정
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `account_id` | string | ❌ | 계정 ID |
| `account_password` | string | ❌ | 비밀번호 (hidden) |
| `purchase_payment_day` | string | ❌ | 매입 결제일 |
| `sales_payment_day` | string | ❌ | 매출 결제일 |
### 섹션 6: 약정 세금
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `tax_agreement` | boolean | ❌ | 세금 약정 여부 |
| `tax_amount` | decimal | ❌ | 약정 금액 |
| `tax_start_date` | date | ❌ | 약정 시작일 |
| `tax_end_date` | date | ❌ | 약정 종료일 |
### 섹션 7: 악성채권 정보
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `bad_debt` | boolean | ❌ | 악성채권 여부 |
| `bad_debt_amount` | decimal | ❌ | 악성채권 금액 |
| `bad_debt_receive_date` | date | ❌ | 채권 발생일 |
| `bad_debt_end_date` | date | ❌ | 채권 만료일 |
| `bad_debt_progress` | enum | ❌ | 진행 상태 |
### 섹션 8: 기타
| 필드명 | 타입 | 필수 | 설명 |
|--------|------|------|------|
| `memo` | text | ❌ | 메모 |
| `is_active` | boolean | ❌ | 활성 상태 (기본: true) |
---
## 5. 프론트엔드 타입 정의
### 5.1 API 응답 타입
```typescript
export type ClientType = "매입" | "매출" | "매입매출";
export type BadDebtProgress = "협의중" | "소송중" | "회수완료" | "대손처리" | "";
export interface ClientApiResponse {
id: number;
tenant_id: number;
client_group_id: number | null;
client_code: string;
name: string;
client_type?: ClientType;
business_no: string | null;
business_type: string | null;
business_item: string | null;
contact_person: string | null;
phone: string | null;
mobile?: string | null;
fax?: string | null;
email: string | null;
address: string | null;
manager_name?: string | null;
manager_tel?: string | null;
system_manager?: string | null;
account_id?: string | null;
purchase_payment_day?: string | null;
sales_payment_day?: string | null;
tax_agreement?: boolean;
tax_amount?: number | null;
tax_start_date?: string | null;
tax_end_date?: string | null;
bad_debt?: boolean;
bad_debt_amount?: number | null;
bad_debt_receive_date?: string | null;
bad_debt_end_date?: string | null;
bad_debt_progress?: BadDebtProgress;
memo?: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
```
### 5.2 프론트엔드 변환 타입
```typescript
export interface Client {
id: string;
code: string;
name: string;
businessNo: string;
representative: string;
phone: string;
address: string;
email: string;
businessType: string;
businessItem: string;
registeredDate: string;
status: "활성" | "비활성";
groupId: string | null;
clientType: ClientType;
mobile: string;
fax: string;
managerName: string;
managerTel: string;
systemManager: string;
accountId: string;
purchasePaymentDay: string;
salesPaymentDay: string;
taxAgreement: boolean;
taxAmount: string;
taxStartDate: string;
taxEndDate: string;
badDebt: boolean;
badDebtAmount: string;
badDebtReceiveDate: string;
badDebtEndDate: string;
badDebtProgress: BadDebtProgress;
memo: string;
}
```
---
## 6. 백엔드 참고 파일
### 컨트롤러/서비스
- `api/app/Http/Controllers/Api/V1/ClientController.php`
- `api/app/Http/Controllers/Api/V1/ClientGroupController.php`
- `api/app/Services/ClientService.php`
- `api/app/Services/ClientGroupService.php`
### 모델
- `api/app/Models/Orders/Client.php`
- `api/app/Models/Orders/ClientGroup.php`
### 요청 클래스
- `api/app/Http/Requests/Client/ClientStoreRequest.php`
- `api/app/Http/Requests/Client/ClientUpdateRequest.php`
### 마이그레이션
- `api/database/migrations/2025_12_04_145912_add_business_fields_to_clients_table.php`
- `api/database/migrations/2025_12_04_205603_add_extended_fields_to_clients_table.php`
### Swagger
- `api/app/Swagger/v1/ClientApi.php`
### 라우트
- `api/routes/api.php` (Line 316-333)
---
## 7. 프론트엔드 참고 파일
### 훅
- `react/src/hooks/useClientList.ts` - CRUD 훅 (530줄)
- `react/src/hooks/useClientGroupList.ts` - 그룹 CRUD 훅
### 컴포넌트
- `react/src/components/clients/ClientRegistration.tsx` - 등록/수정 폼 (sam-design 기반)
- `react/src/components/clients/ClientDetail.tsx` - 상세 보기
### 페이지
- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx` - 목록
- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx` - 등록
- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx` - 상세
- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/edit/page.tsx` - 수정
---
## 8. 추가 개선 사항 (선택)
| 항목 | 우선순위 | 상태 |
|------|---------|------|
| 거래처 그룹 관리 UI | 🟢 LOW | ⬜ 미구현 |
| 엑셀 내보내기/가져오기 | 🟢 LOW | ⬜ 미구현 |
| 거래처 검색 고급 필터 | 🟢 LOW | ⬜ 미구현 |
---
## 9. API Flow Tester 테스트 파일
### 9.1 Flow Tester 설명
API Flow Tester를 사용하여 Client 및 Client Group API의 전체 CRUD 플로우를 자동으로 테스트할 수 있습니다.
### 9.2 테스트 파일 위치
| 파일 | 설명 |
|------|------|
| `docs/front/flow-tests/client-api-flow.json` | 거래처 API CRUD 플로우 테스트 (11단계) |
| `docs/front/flow-tests/client-group-api-flow.json` | 거래처 그룹 API CRUD 플로우 테스트 (10단계) |
### 9.3 테스트 플로우 요약
#### Client API Flow (11단계)
1. 로그인 → 토큰 추출
2. 거래처 목록 조회
3. 거래처 생성
4. 거래처 단건 조회
5. 거래처 수정
6. 수정 확인 조회
7. 비활성화 토글
8. 활성화 토글
9. 거래처 검색
10. 거래처 삭제
11. 삭제 확인 (404 예상)
#### Client Group API Flow (10단계)
1. 로그인 → 토큰 추출
2. 그룹 목록 조회
3. 그룹 생성 (`group_code`, `group_name`, `price_rate` 필수)
4. 그룹 단건 조회
5. 그룹 수정
6. 수정 확인 조회
7. 비활성화 토글
8. 활성화 토글
9. 그룹 삭제
10. 삭제 확인 (404 예상)
### 9.4 Flow Tester 사용 시 주의사항
**환경변수 설정 필요:**
```bash
FLOW_TESTER_USER_ID=테스트계정ID
FLOW_TESTER_USER_PWD=테스트계정비밀번호
```
**API 응답 구조 차이점:**
- `/login` 응답: `{ "access_token": "...", "token_type": "..." }` (루트 레벨)
- 기타 API 응답: `{ "success": true, "data": {...} }` (data 래핑)
**필드 타입 주의:**
- `is_active`: boolean (`true`/`false`), 문자열 `"Y"`/`"N"` 아님
- `price_rate`: numeric (0~99.9999)
---
**최종 업데이트**: 2025-12-08

View File

@@ -1,675 +0,0 @@
# 견적관리 API 요청서
> **작성일**: 2025-12-04
> **목적**: 견적관리 기능을 위한 백엔드 API 요청
> **참조**: sam-design/QuoteManagement3Write.tsx, QuoteManagement3Detail.tsx
---
## 1. 개요
### 1.1 기능 요약
견적관리 시스템은 다음 기능을 지원해야 합니다:
- 견적 CRUD (등록, 조회, 수정, 삭제)
- 견적 상태 관리 (임시저장 → 확정 → 수주전환)
- 견적 수정 이력 관리 (버전 관리)
- 견적 품목(BOM) 관리
- 자동 견적 산출 (수식 기반 계산) ← **백엔드 구현**
### 1.2 특이사항
- **자동 견적 산출 로직**은 백엔드에서 구현 예정 (수식 계산 엔진)
- 프론트엔드는 입력값을 전달하고 계산 결과를 받아서 표시
---
## 2. 데이터 모델
### 2.1 Quote (견적) - 메인 엔티티
```typescript
interface Quote {
// === 기본 정보 ===
id: number;
tenant_id: number;
quote_number: string; // 견적번호 (예: KD-SC-251204-01)
registration_date: string; // 등록일 (YYYY-MM-DD)
receipt_date: string; // 접수일
author: string; // 작성자
// === 발주처 정보 ===
client_id: number | null; // 거래처 ID (FK)
client_name: string; // 거래처명 (직접입력 대응)
manager: string | null; // 담당자
contact: string | null; // 연락처
// === 현장 정보 ===
site_id: number | null; // 현장 ID (FK, 별도 테이블 필요시)
site_name: string | null; // 현장명
site_code: string | null; // 현장코드
// === 제품 정보 ===
product_category: 'SCREEN' | 'STEEL'; // 제품 카테고리
product_id: number | null; // 선택된 제품 ID (품목마스터 FK)
product_code: string | null; // 제품코드
product_name: string | null; // 제품명
// === 규격 정보 ===
open_size_width: number | null; // 오픈사이즈 폭 (mm)
open_size_height: number | null; // 오픈사이즈 높이 (mm)
quantity: number; // 수량 (기본값: 1)
unit_symbol: string | null; // 부호
floors: string | null; // 층수
// === 금액 정보 ===
material_cost: number; // 재료비 합계
labor_cost: number; // 노무비
install_cost: number; // 설치비
subtotal: number; // 소계
discount_rate: number; // 할인율 (%)
discount_amount: number; // 할인금액
total_amount: number; // 최종 금액
// === 상태 관리 ===
status: 'draft' | 'sent' | 'approved' | 'rejected' | 'finalized' | 'converted';
current_revision: number; // 현재 수정 차수 (0부터 시작)
is_final: boolean; // 최종확정 여부
finalized_at: string | null; // 확정일시
finalized_by: number | null; // 확정자 ID
// === 기타 정보 ===
completion_date: string | null; // 납기일
remarks: string | null; // 비고
memo: string | null; // 메모
notes: string | null; // 특이사항
// === 시스템 필드 ===
created_at: string;
updated_at: string;
created_by: number | null;
updated_by: number | null;
deleted_at: string | null; // Soft Delete
}
```
### 2.2 QuoteItem (견적 품목) - BOM 계산 결과
```typescript
interface QuoteItem {
id: number;
quote_id: number; // 견적 ID (FK)
tenant_id: number;
// === 품목 정보 ===
item_id: number | null; // 품목마스터 ID (FK)
item_code: string; // 품목코드
item_name: string; // 품명
specification: string | null; // 규격
unit: string; // 단위
// === 수량/금액 ===
base_quantity: number; // 기본수량
calculated_quantity: number; // 계산된 수량
unit_price: number; // 단가
total_price: number; // 금액 (수량 × 단가)
// === 수식 정보 ===
formula: string | null; // 수식 (예: "W/1000 + 0.1")
formula_source: string | null; // 수식 출처 (BOM템플릿, 제품BOM 등)
formula_category: string | null; // 수식 카테고리
data_source: string | null; // 데이터 출처
// === 기타 ===
delivery_date: string | null; // 품목별 납기일
note: string | null; // 비고
sort_order: number; // 정렬순서
created_at: string;
updated_at: string;
}
```
### 2.3 QuoteRevision (견적 수정 이력)
```typescript
interface QuoteRevision {
id: number;
quote_id: number; // 견적 ID (FK)
tenant_id: number;
revision_number: number; // 수정 차수 (1, 2, 3...)
revision_date: string; // 수정일
revision_by: number; // 수정자 ID
revision_by_name: string; // 수정자 이름
revision_reason: string | null; // 수정 사유
// 이전 버전 데이터 (JSON)
previous_data: object; // 수정 전 견적 전체 데이터 (스냅샷)
created_at: string;
}
```
---
## 3. API 엔드포인트
### 3.1 견적 CRUD
| Method | Endpoint | 설명 | 비고 |
|--------|----------|------|------|
| `GET` | `/api/v1/quotes` | 목록 조회 | 페이지네이션, 필터, 검색 |
| `GET` | `/api/v1/quotes/{id}` | 단건 조회 | 품목(items), 이력(revisions) 포함 |
| `POST` | `/api/v1/quotes` | 생성 | 품목 배열 포함 |
| `PUT` | `/api/v1/quotes/{id}` | 수정 | 수정이력 자동 생성 |
| `DELETE` | `/api/v1/quotes/{id}` | 삭제 | Soft Delete |
| `DELETE` | `/api/v1/quotes` | 일괄 삭제 | `ids[]` 파라미터 |
### 3.2 견적 상태 관리
| Method | Endpoint | 설명 | 비고 |
|--------|----------|------|------|
| `PATCH` | `/api/v1/quotes/{id}/finalize` | 최종확정 | status → 'finalized', is_final → true |
| `PATCH` | `/api/v1/quotes/{id}/convert-to-order` | 수주전환 | status → 'converted', 수주 데이터 생성 |
| `PATCH` | `/api/v1/quotes/{id}/cancel-finalize` | 확정취소 | is_final → false (조건부) |
### 3.3 자동 견적 산출 (핵심 기능)
| Method | Endpoint | 설명 | 비고 |
|--------|----------|------|------|
| `POST` | `/api/v1/quotes/calculate` | 자동 산출 | **수식 계산 엔진** |
| `POST` | `/api/v1/quotes/{id}/recalculate` | 재계산 | 기존 견적 재산출 |
### 3.4 견적 문서 출력
| Method | Endpoint | 설명 | 비고 |
|--------|----------|------|------|
| `GET` | `/api/v1/quotes/{id}/document/quote` | 견적서 PDF | |
| `GET` | `/api/v1/quotes/{id}/document/calculation` | 산출내역서 PDF | |
| `GET` | `/api/v1/quotes/{id}/document/purchase-order` | 발주서 PDF | |
### 3.5 문서 발송 API ⭐ 신규 요청
| Method | Endpoint | 설명 | 비고 |
|--------|----------|------|------|
| `POST` | `/api/v1/quotes/{id}/send/email` | 이메일 발송 | 첨부파일 포함 |
| `POST` | `/api/v1/quotes/{id}/send/fax` | 팩스 발송 | 팩스 서비스 연동 |
| `POST` | `/api/v1/quotes/{id}/send/kakao` | 카카오톡 발송 | 알림톡/친구톡 |
### 3.6 견적번호 생성
| Method | Endpoint | 설명 | 비고 |
|--------|----------|------|------|
| `GET` | `/api/v1/quotes/generate-number` | 견적번호 생성 | `?category=SCREEN` |
---
## 4. 상세 API 명세
### 4.1 목록 조회 `GET /api/v1/quotes`
**Query Parameters:**
```
page: number (default: 1)
size: number (default: 20)
q: string (검색어 - 견적번호, 발주처, 담당자, 현장명)
status: string (상태 필터)
product_category: string (제품 카테고리)
client_id: number (발주처 ID)
date_from: string (등록일 시작)
date_to: string (등록일 종료)
sort_by: string (정렬 컬럼)
sort_order: 'asc' | 'desc'
```
**Response:**
```json
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"id": 1,
"quote_number": "KD-SC-251204-01",
"registration_date": "2025-12-04",
"client_name": "ABC건설",
"site_name": "강남 오피스텔 현장",
"product_category": "SCREEN",
"product_name": "전동스크린 A형",
"quantity": 10,
"total_amount": 15000000,
"status": "draft",
"current_revision": 0,
"is_final": false,
"created_at": "2025-12-04T10:00:00Z"
}
],
"last_page": 5,
"per_page": 20,
"total": 100
}
}
```
### 4.2 단건 조회 `GET /api/v1/quotes/{id}`
**Response:**
```json
{
"success": true,
"data": {
"id": 1,
"quote_number": "KD-SC-251204-01",
"registration_date": "2025-12-04",
"receipt_date": "2025-12-04",
"author": "김철수",
"client_id": 10,
"client_name": "ABC건설",
"manager": "이영희",
"contact": "010-1234-5678",
"site_id": 5,
"site_name": "강남 오피스텔 현장",
"site_code": "PJ-20251204-01",
"product_category": "SCREEN",
"product_id": 100,
"product_code": "SCR-001",
"product_name": "전동스크린 A형",
"open_size_width": 2000,
"open_size_height": 3000,
"quantity": 10,
"unit_symbol": "A",
"floors": "3층",
"material_cost": 12000000,
"labor_cost": 1500000,
"install_cost": 1500000,
"subtotal": 15000000,
"discount_rate": 0,
"discount_amount": 0,
"total_amount": 15000000,
"status": "draft",
"current_revision": 2,
"is_final": false,
"completion_date": "2025-12-31",
"remarks": "급하게 진행 필요",
"memo": "",
"notes": "",
"items": [
{
"id": 1,
"item_code": "SCR-MOTOR-001",
"item_name": "스크린 모터",
"specification": "220V, 1/4HP",
"unit": "EA",
"base_quantity": 1,
"calculated_quantity": 10,
"unit_price": 150000,
"total_price": 1500000,
"formula": "Q",
"formula_source": "제품BOM",
"sort_order": 1
}
],
"revisions": [
{
"revision_number": 2,
"revision_date": "2025-12-04",
"revision_by_name": "김철수",
"revision_reason": "고객 요청으로 수량 변경"
},
{
"revision_number": 1,
"revision_date": "2025-12-03",
"revision_by_name": "김철수",
"revision_reason": "단가 조정"
}
],
"created_at": "2025-12-04T10:00:00Z",
"updated_at": "2025-12-04T15:30:00Z"
}
}
```
### 4.3 생성 `POST /api/v1/quotes`
**Request Body:**
```json
{
"registration_date": "2025-12-04",
"receipt_date": "2025-12-04",
"client_id": 10,
"client_name": "ABC건설",
"manager": "이영희",
"contact": "010-1234-5678",
"site_id": 5,
"site_name": "강남 오피스텔 현장",
"site_code": "PJ-20251204-01",
"product_category": "SCREEN",
"product_id": 100,
"open_size_width": 2000,
"open_size_height": 3000,
"quantity": 10,
"unit_symbol": "A",
"floors": "3층",
"completion_date": "2025-12-31",
"remarks": "급하게 진행 필요",
"items": [
{
"item_id": 50,
"item_code": "SCR-MOTOR-001",
"item_name": "스크린 모터",
"unit": "EA",
"base_quantity": 1,
"calculated_quantity": 10,
"unit_price": 150000,
"total_price": 1500000,
"formula": "Q",
"sort_order": 1
}
]
}
```
### 4.4 자동 산출 `POST /api/v1/quotes/calculate` ⭐ 핵심
**Request Body:**
```json
{
"product_id": 100,
"product_category": "SCREEN",
"open_size_width": 2000,
"open_size_height": 3000,
"quantity": 10,
"floors": "3층",
"unit_symbol": "A",
"options": {
"guide_rail_install_type": "벽부형",
"motor_power": "1/4HP",
"controller": "표준형",
"edge_wing_size": 50,
"inspection_fee": 0
}
}
```
**Response:**
```json
{
"success": true,
"data": {
"product_id": 100,
"product_name": "전동스크린 A형",
"product_category": "SCREEN",
"open_size": {
"width": 2000,
"height": 3000
},
"quantity": 10,
"items": [
{
"item_id": 50,
"item_code": "SCR-MOTOR-001",
"item_name": "스크린 모터",
"specification": "220V, 1/4HP",
"unit": "EA",
"base_quantity": 1,
"calculated_quantity": 10,
"unit_price": 150000,
"total_price": 1500000,
"formula": "Q",
"formula_result": "10 × 1 = 10",
"formula_source": "제품BOM: 전동스크린 A형",
"data_source": "품목마스터 [SCR-MOTOR-001]"
},
{
"item_id": 51,
"item_code": "SCR-RAIL-001",
"item_name": "가이드레일",
"specification": "알루미늄",
"unit": "M",
"base_quantity": 1,
"calculated_quantity": 60,
"unit_price": 15000,
"total_price": 900000,
"formula": "H/1000 × 2 × Q",
"formula_result": "(3000/1000) × 2 × 10 = 60",
"formula_source": "BOM템플릿: 스크린_표준",
"data_source": "품목마스터 [SCR-RAIL-001]"
}
],
"summary": {
"material_cost": 12000000,
"labor_cost": 1500000,
"install_cost": 1500000,
"subtotal": 15000000,
"total_amount": 15000000
},
"calculation_info": {
"bom_template_used": "스크린_표준",
"formula_variables": {
"W": 2000,
"H": 3000,
"Q": 10
},
"calculated_at": "2025-12-04T10:00:00Z"
}
}
}
```
---
## 5. 수식 계산 엔진 (백엔드 구현 요청)
### 5.1 수식 변수
| 변수 | 설명 | 예시 |
|------|------|------|
| `W` | 오픈사이즈 폭 (mm) | 2000 |
| `H` | 오픈사이즈 높이 (mm) | 3000 |
| `Q` | 수량 | 10 |
### 5.2 수식 예시
```
수량 그대로: Q
높이 기반: H/1000
폭+높이: (W + H) / 1000
가이드레일: H/1000 × 2 × Q
스크린원단: (W/1000 + 0.1) × (H/1000 + 0.3) × Q
```
### 5.3 반올림 규칙
| 규칙 | 설명 |
|------|------|
| `ceil` | 올림 |
| `floor` | 내림 |
| `round` | 반올림 |
### 5.4 BOM 템플릿 연동
- 제품별 BOM 템플릿에서 수식 조회
- 템플릿이 없으면 품목마스터 BOM 사용
- 수식 + 단가로 자동 금액 계산
---
## 6. 상태 흐름도
```
[신규등록]
[draft] 임시저장
↓ (최종확정)
[finalized] 확정
↓ (수주전환)
[converted] 수주전환
```
### 6.1 상태별 제약
| 상태 | 수정 가능 | 삭제 가능 | 비고 |
|------|----------|----------|------|
| `draft` | O | O | 자유롭게 수정 |
| `sent` | O | O | 발송 후 수정 가능 (이력 기록) |
| `finalized` | X | X | 확정 후 수정 불가 |
| `converted` | X | X | 수주전환 후 불변 |
---
## 7. 프론트엔드 구현 현황 (2025-12-04 업데이트)
### 7.1 구현 완료된 파일
| 파일 | 설명 | 상태 |
|------|------|------|
| `quote-management/page.tsx` | 견적 목록 페이지 | ✅ 완료 (샘플 데이터) |
| `quote-management/new/page.tsx` | 견적 등록 페이지 | ✅ 완료 |
| `quote-management/[id]/page.tsx` | 견적 상세 페이지 | ✅ 완료 |
| `quote-management/[id]/edit/page.tsx` | 견적 수정 페이지 | ✅ 완료 |
| `components/quotes/QuoteRegistration.tsx` | 견적 등록/수정 컴포넌트 | ✅ 완료 |
| `components/quotes/QuoteDocument.tsx` | 견적서 문서 컴포넌트 | ✅ 완료 |
| `components/quotes/QuoteCalculationReport.tsx` | 산출내역서 문서 컴포넌트 | ✅ 완료 |
| `components/quotes/PurchaseOrderDocument.tsx` | 발주서 문서 컴포넌트 | ✅ 완료 |
### 7.2 UI 기능 구현 현황
| 기능 | 상태 | 비고 |
|------|------|------|
| 견적 목록 조회 | ✅ UI 완료 | 샘플 데이터, API 연동 필요 |
| 견적 검색/필터 | ✅ UI 완료 | 로컬 필터링, API 연동 필요 |
| 견적 등록 폼 | ✅ UI 완료 | API 연동 필요 |
| 견적 상세 페이지 | ✅ UI 완료 | API 연동 필요 |
| 견적 수정 폼 | ✅ UI 완료 | API 연동 필요 |
| 견적 삭제 | ✅ UI 완료 | 로컬 상태, API 연동 필요 |
| 견적 일괄 삭제 | ✅ UI 완료 | 로컬 상태, API 연동 필요 |
| 자동 견적 산출 | ⏳ 버튼만 | 백엔드 수식 엔진 필요 |
| 발주처 선택 | ⏳ 샘플 데이터 | `/api/v1/clients` 연동 필요 |
| 현장 선택 | ⏳ 샘플 데이터 | 발주처 연동 후 현장 API 필요 |
| 제품 선택 | ⏳ 샘플 데이터 | `/api/v1/item-masters` 연동 필요 |
| **견적서 모달** | ✅ UI 완료 | PDF/이메일/팩스/카톡 버튼, **발송 API 필요** |
| **산출내역서 모달** | ✅ UI 완료 | PDF/이메일/팩스/카톡 버튼, **발송 API 필요** |
| **발주서 모달** | ✅ UI 완료 | PDF/이메일/팩스/카톡 버튼, **발송 API 필요** |
| 최종확정 버튼 | ✅ UI 완료 | API 연동 필요 |
### 7.3 견적 등록/수정 폼 필드 (구현 완료)
**기본 정보 섹션:**
- 등록일 (readonly, 오늘 날짜)
- 작성자 (readonly, 로그인 사용자)
- 발주처 선택 * (필수)
- 현장명 (발주처 선택 시 연동)
- 발주 담당자
- 연락처
- 납기일
- 비고
**자동 견적 산출 섹션 (동적 항목):**
- 층수
- 부호
- 제품 카테고리 (PC) *
- 제품명 *
- 오픈사이즈 (W0) *
- 오픈사이즈 (H0) *
- 가이드레일 설치 유형 (GT) *
- 모터 전원 (MP) *
- 연동제어기 (CT) *
- 수량 (QTY) *
- 마구리 날개치수 (WS)
- 검사비 (INSP)
**기능:**
- 견적 항목 추가/복사/삭제
- 자동 견적 산출 버튼
- 샘플 데이터 생성 버튼
### 7.4 다음 단계 (API 연동)
```typescript
// useQuoteList 훅 (목록)
const {
quotes,
pagination,
isLoading,
fetchQuotes,
deleteQuote,
bulkDelete
} = useQuoteList();
// useQuote 훅 (단건 CRUD)
const {
quote,
isLoading,
fetchQuote,
createQuote,
updateQuote,
finalizeQuote,
convertToOrder
} = useQuote();
// useQuoteCalculation 훅 (자동 산출)
const {
calculationResult,
isCalculating,
calculate,
recalculate
} = useQuoteCalculation();
```
---
## 8. 관련 참조
### 8.1 거래처 API 연동
- 발주처 선택 시 `/api/v1/clients` 연동
- 직접입력 시 자동 등록 가능
### 8.2 현장 API (추후)
- 현장 선택 시 `/api/v1/sites` 연동 (별도 API 필요시)
### 8.3 품목마스터 연동
- 제품 선택 시 `/api/v1/item-masters` 연동
- BOM 조회 시 품목마스터 BOM 활용
---
## 9. 요청 우선순위
| 순위 | API | 설명 |
|------|-----|------|
| P1 | 견적 CRUD | 기본 목록/등록/수정/삭제 |
| P1 | 자동 산출 | 수식 계산 엔진 (핵심) |
| P1 | 견적번호 생성 | 자동 채번 |
| P2 | 상태 관리 | 확정/수주전환 |
| P2 | 수정 이력 | 버전 관리 |
| P3 | 문서 출력 | PDF 생성 |
---
## 10. 질문사항
1. **현장(Site) 테이블**: 별도 테이블로 관리할지? (거래처 하위 개념)
2. **수식 계산**: BOM 템플릿 테이블 구조는?
3. **문서 출력**: PDF 라이브러리 선정 (TCPDF, Dompdf 등)
4. **알림**: 견적 발송 시 이메일/카카오톡 연동 계획?

View File

@@ -1,533 +0,0 @@
# 품목 등록/수정 백엔드 API 수정 요청
> 프론트엔드 품목관리 기능 개발 중 발견된 백엔드 수정 필요 사항 정리
---
## 🔴 [핵심] field_key 통일 필요 - 근본적 구조 개선
### 상태: 🔴 구조 개선 필요
### 발견일: 2025-12-06
### 요약
| 항목 | 내용 |
|------|------|
| **근본 원인** | 품목기준관리 / 품목관리 API 요청서 분리로 키값 불일치 |
| **해결 방향** | 품목기준관리 field_key = Single Source of Truth |
| **백엔드 요청** | 동적 필드 → attributes JSON 저장, field_key 통일 |
| **프론트 작업** | 백엔드 완료 후 하드코딩 매핑 제거 |
### 현재 문제
```
품목기준관리 field_key 프론트엔드 변환 백엔드 API 응답
───────────────────── ───────────────── ─────────────────
Part_type → part_type → part_type (불일치!)
Installation_type_1 → installation_type → 저장 안됨
side_dimensions_horizontal → side_spec_width → 저장 안됨
```
**두 개의 요청서가 따로 놀면서** 백엔드에서 각각 다르게 구현 → 키 불일치 발생
### 이상적인 구조
```
품목기준관리 (field_key 정의)
┌────────────┐
│ field_key │ ← Single Source of Truth
└────────────┘
┌────┴────┬────────┬────────┐
▼ ▼ ▼ ▼
등록 수정 상세 리스트
(전부 동일한 field_key로 저장/조회/렌더링)
```
### 기대 효과
1. **단위 필드 혼란 해결**: field_key가 "unit"이면 그게 단위 (명확!)
2. **필드 타입 자동 인식**: 품목기준관리 field_type 보고 자동 렌더링
3. **누락 데이터 분석 용이**: field_key 하나만 확인하면 끝
4. **디버깅 속도 향상**: API 응답 = 폼 데이터 (변환 없음)
### 수정 요청
1. **품목기준관리 field_key를 기준**으로 API 요청/응답 키 통일
2. 동적 필드는 `attributes` JSON에 field_key 그대로 저장
3. 조회 시에도 field_key 그대로 응답
### 영향 범위
- 품목관리 전체 (등록/수정/상세/리스트)
- 모든 품목 유형 (FG, PT, SM, RM, CS)
### 우선순위
🔴 **최우선** - 이 구조 개선 후 아래 개별 이슈들 대부분 자동 해결
---
## 1. 소모품(CS) 등록 시 규격(specification) 저장 안됨
### 상태: 🔴 수정 필요
### 발견일: 2025-12-06
### 파일 위치
`/app/Http/Requests/Item/ItemStoreRequest.php` - rules() 메서드 (Line 14-42)
### 현재 문제
- `specification` 필드가 validation rules에 없음
- Laravel FormRequest는 rules에 정의되지 않은 필드를 `$request->validated()` 결과에서 제외
- 프론트엔드에서 `specification: "테스트"` 값을 보내도 백엔드에서 무시됨
- DB에 규격 값이 null로 저장됨
### 프론트엔드 확인 사항
- `DynamicItemForm`에서 `97_specification``spec``specification`으로 정상 변환됨
- API 요청 payload에 `specification` 필드 포함 확인됨
- 백엔드 `ItemsService.createMaterial()`에서 `$data['specification']` 참조하나 값이 없음
### 수정 요청
```php
// /app/Http/Requests/Item/ItemStoreRequest.php
public function rules(): array
{
return [
// 필수 필드
'code' => 'required|string|max:50',
'name' => 'required|string|max:255',
'product_type' => 'required|string|in:FG,PT,SM,RM,CS',
'unit' => 'required|string|max:20',
// 선택 필드
'category_id' => 'nullable|integer|exists:categories,id',
'description' => 'nullable|string',
'specification' => 'nullable|string|max:255', // ✅ 추가 필요
// ... 나머지 기존 필드들 ...
];
}
```
### 영향 범위
- 소모품(CS) 등록
- 원자재(RM) 등록 (해당 시)
- 부자재(SM) 등록 (해당 시)
---
## 2. Material(SM, RM, CS) 수정 시 material_code 중복 에러
### 상태: 🔴 수정 필요
### 발견일: 2025-12-06
### 현재 문제
- Material 수정 시 `material_code` 중복 체크 에러 발생
- 케이스 1: 값을 변경하지 않아도 자기 자신과 중복 체크됨
- 케이스 2: 소프트 삭제된 품목의 코드와도 중복 체크됨
- 에러 메시지: `Duplicate entry '알루미늄-옵션2-2' for key 'materials.materials_material_code_unique'`
### 원인
1. UPDATE 시 자기 자신의 ID를 제외하지 않음
2. 소프트 삭제(`deleted_at`)된 레코드도 unique 체크에 포함됨
### 수정 요청
```php
// Material 수정 Request 파일 (MaterialUpdateRequest.php 또는 해당 파일)
use Illuminate\Validation\Rule;
public function rules(): array
{
return [
// ... 기존 필드들 ...
// ✅ 수정 시 자기 자신 제외 + 소프트삭제 제외하고 unique 체크
'material_code' => [
'required',
'string',
'max:255',
Rule::unique('materials', 'material_code')
->whereNull('deleted_at') // 소프트삭제된 건 제외
->ignore($this->route('id')), // 자기 자신 제외
],
// ... 나머지 필드들 ...
];
}
```
### 영향 범위
- 원자재(RM) 수정
- 부자재(SM) 수정
- 소모품(CS) 수정
### 비고
- 현재 수정 자체가 불가능하여 수정 후 데이터 검증이 어려움
- 이 이슈 해결 후 수정 기능 재검증 필요
---
## 3. Material(SM, RM) 수정 시 규격(specification) 로딩 안됨
### 상태: 🔴 확인 필요
### 발견일: 2025-12-06
### 현재 문제
- SM(부자재), RM(원자재) 수정 페이지 진입 시 규격 값이 표시 안됨
- 1회 수정 후 다시 수정 페이지 진입 시 규격 값 없음
- CS(소모품)과 동일한 문제로 추정
### 원인 추정
- 백엔드에서 `options` 배열이 제대로 저장되지 않거나 반환되지 않음
- SM/RM은 `standard_*` 필드 조합으로 `specification`을 생성하고, 이 값을 `options` 배열에도 저장
- 수정 페이지에서는 `options` 배열을 읽어서 폼에 표시
- `options`가 null이면 규격 필드들이 빈 값으로 표시됨
### 확인 요청
```php
// Material 조회 API 응답에서 options 필드 확인 필요
// GET /api/v1/items/{id}?item_type=MATERIAL
// 응답 예시 (정상)
{
"id": 396,
"name": "썬더볼트",
"specification": "부자재2-2",
"options": [
{"label": "standard_1", "value": "부자재2"},
{"label": "standard_2", "value": "2"}
] // ✅ options 배열이 있어야 함
}
// 응답 예시 (문제)
{
"id": 396,
"name": "썬더볼트",
"specification": "부자재2-2",
"options": null // ❌ options가 null이면 규격 로딩 불가
}
```
### 수정 요청
1. Material 저장 시 `options` 배열 정상 저장 확인
2. Material 조회 시 `options` 필드 반환 확인
3. `options`가 JSON 컬럼이라면 정상적인 JSON 형식으로 저장되는지 확인
### 영향 범위
- 원자재(RM) 수정
- 부자재(SM) 수정
---
## 4. Material(SM, RM, CS) 비고(remarks) 저장 안됨
### 상태: 🔴 수정 필요
### 발견일: 2025-12-06
### 현재 문제
- 소모품(CS), 원자재(RM), 부자재(SM) 등록/수정 시 비고(remarks)가 저장되지 않음
- 프론트에서 `note``remarks`로 정상 변환하여 전송
- 백엔드 Service에서 `$data['remarks']` 참조하지만 값이 없음
### 원인 분석
- **프론트엔드**: `note``remarks` 변환 ✅ (`materialTransform.ts:115`)
- **백엔드 Service**: `'remarks' => $data['remarks'] ?? null` ✅ (`ItemsService.php:301`)
- **백엔드 Model**: `remarks` 컬럼 존재 ✅ (`Material.php:31`)
- **백엔드 Request**: `remarks` validation rule 없음 ❌ **누락**
### 수정 요청
```php
// /app/Http/Requests/Item/ItemStoreRequest.php
// /app/Http/Requests/Item/ItemUpdateRequest.php
public function rules(): array
{
return [
// 기존 필드들...
'description' => 'nullable|string',
// ✅ 추가 필요
'remarks' => 'nullable|string', // 비고 필드
// ... 나머지 필드들 ...
];
}
```
### 영향 범위
- 소모품(CS) 등록/수정
- 원자재(RM) 등록/수정
- 부자재(SM) 등록/수정
### 비고
- 1번 이슈(specification)와 동일한 원인: Request validation 누락
- 함께 처리하면 효율적
---
## 5. 품목기준관리 옵션 필드 식별자 필요 (장기 개선)
### 상태: 🟡 개선 권장
### 발견일: 2025-12-06
### 현재 문제
- 품목기준관리에서 옵션 필드의 `field_key`를 자유롭게 입력 가능
- 프론트엔드는 `standard_*`, `option_*` 패턴으로 옵션 필드를 식별
- 패턴에 맞지 않는 field_key (예: `st_2`)는 규격(specification) 조합에서 누락됨
- 결과: 부자재(SM)의 규격값이 저장되지 않음
### 임시 해결 (프론트엔드)
- 품목기준관리에서 field_key를 `standard_*` 패턴으로 통일
- 예: `st_2``standard_2`
### 근본 해결 요청 (백엔드)
```php
// 품목기준관리 API 응답에 옵션 필드 여부 표시
// 현재 응답
{
"field_key": "st_2",
"field_type": "select",
"field_name": "규격옵션"
}
// 개선 요청
{
"field_key": "st_2",
"field_type": "select",
"field_name": "규격옵션",
"is_spec_option": true // ✅ 규격 조합용 옵션 필드인지 표시
}
```
### 기대 효과
- 프론트엔드가 field_key 패턴에 의존하지 않음
- `is_spec_option: true`인 필드만 규격 조합에 사용
- 새로운 field_key 패턴이 추가되어도 프론트 수정 불필요
### 영향 범위
- 원자재(RM) 등록/수정
- 부자재(SM) 등록/수정
- 향후 추가되는 Material 유형
---
## 6. 조립부품(PT-ASSEMBLY) 필드 저장 안됨 - fillable 누락
### 상태: 🔴 수정 필요
### 발견일: 2025-12-06
### 현재 문제
- 조립부품 등록 후 상세보기/수정 페이지에서 데이터가 제대로 표시되지 않음
- **프론트엔드는 데이터를 정상 전송하고 있음** ✅
- **백엔드에서 조립부품 필드들이 저장되지 않음** ❌
### 원인 분석
#### 프론트엔드 전송 데이터 (정상)
```javascript
// DynamicItemForm/index.tsx - handleFormSubmit()
const submitData = {
...convertedData, // 폼 데이터 (installation_type, assembly_type 등 포함)
product_type: 'PT',
part_type: 'ASSEMBLY', // ✅ 명시적 추가
bending_diagram: base64, // ✅ 캔버스 이미지
bom: [...], // ✅ BOM 데이터
};
```
#### 백엔드 저장 로직 (문제)
```php
// ItemsService.php - createProduct()
private function createProduct(array $data, int $tenantId, int $userId): Product
{
$payload = $data;
// ... 기본 필드만 설정
return Product::create($payload); // Mass Assignment
}
```
#### Product 모델 fillable (누락 필드 있음)
```php
// Product.php
protected $fillable = [
'tenant_id', 'code', 'name', 'unit', 'category_id',
'product_type',
'attributes', 'description', // ✅ attributes JSON 있음
'part_type', // ✅ 있음
'bending_diagram', 'bending_details', // ✅ 있음
// ❌ installation_type 없음
// ❌ assembly_type 없음
// ❌ side_spec_width 없음
// ❌ side_spec_height 없음
// ❌ length 없음
];
```
### 필드별 저장 상태
| 필드 | 프론트 전송 | fillable | 저장 여부 |
|------|------------|----------|----------|
| `part_type` | ✅ | ✅ 컬럼 | ✅ 저장됨 |
| `bending_diagram` | ✅ | ✅ 컬럼 | ⚠️ 파일 업로드 별도 처리 |
| `installation_type` | ✅ | ❌ 없음 | ❌ **저장 안됨** |
| `assembly_type` | ✅ | ❌ 없음 | ❌ **저장 안됨** |
| `side_spec_width` | ✅ | ❌ 없음 | ❌ **저장 안됨** |
| `side_spec_height` | ✅ | ❌ 없음 | ❌ **저장 안됨** |
| `length` | ✅ | ❌ 없음 | ❌ **저장 안됨** |
| `bom` | ✅ | ⚠️ 별도 처리 | ❓ 확인 필요 |
### 수정 요청
#### 방법 1: attributes JSON에 저장 (권장)
```php
// ItemsService.php - createProduct() 수정
private function createProduct(array $data, int $tenantId, int $userId): Product
{
// 조립부품 전용 필드들을 attributes JSON으로 묶기
$assemblyFields = ['installation_type', 'assembly_type', 'side_spec_width', 'side_spec_height', 'length'];
$attributes = $data['attributes'] ?? [];
foreach ($assemblyFields as $field) {
if (isset($data[$field])) {
$attributes[$field] = $data[$field];
unset($data[$field]); // payload에서 제거
}
}
$payload = $data;
$payload['tenant_id'] = $tenantId;
$payload['created_by'] = $userId;
$payload['attributes'] = !empty($attributes) ? $attributes : null;
// ... 나머지 동일
return Product::create($payload);
}
```
#### 방법 2: 컬럼 추가 + fillable 등록
```php
// Product.php - fillable에 추가
protected $fillable = [
// 기존 필드들...
'installation_type', // ✅ 추가
'assembly_type', // ✅ 추가
'side_spec_width', // ✅ 추가
'side_spec_height', // ✅ 추가
'length', // ✅ 추가 (또는 assembly_length)
];
// + migration으로 컬럼 추가 필요
```
### 프론트엔드 대응 (완료)
- 프론트에서 `data.xxx` 또는 `data.attributes.xxx` 둘 다에서 값을 찾도록 수정 완료
- 상세보기: `items/[id]/page.tsx` - `mapApiResponseToItemMaster` 함수
- 수정페이지: `items/[id]/edit/page.tsx` - `mapApiResponseToFormData` 함수
### 영향 범위
- 조립부품(PT-ASSEMBLY) 등록/조회/수정
- 설치유형, 마감, 측면규격, 길이 모든 필드
### 우선순위
🔴 **높음** - 조립부품 등록 기능이 완전히 동작하지 않음
---
## 7. 파일 업로드 API 500 에러 - ApiResponse 클래스 네임스페이스 불일치
### 상태: 🔴 수정 필요
### 발견일: 2025-12-06
### 현재 문제
- 품목 파일 업로드 (전개도, 시방서, 인정서) 시 500 에러 발생
- 절곡부품, 조립부품 모두 동일한 에러
- 파일이 업로드되지 않음
### 에러 로그
```
[2025-12-06 17:28:22] DEV.ERROR: Class "App\Http\Responses\ApiResponse" not found
{"exception":"[object] (Error(code: 0): Class \"App\\Http\\Responses\\ApiResponse\" not found
at /home/webservice/api/app/Http/Controllers/Api/V1/ItemsFileController.php:31)
```
### 원인 분석
**네임스페이스 불일치**
| 위치 | 네임스페이스 |
|------|-------------|
| `ItemsFileController.php` (Line 7) | `use App\Http\Responses\ApiResponse` ❌ |
| 실제 파일 위치 | `App\Helpers\ApiResponse` ✅ |
### 파일 위치
`/app/Http/Controllers/Api/V1/ItemsFileController.php` (Line 7)
### 현재 코드
```php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemsFileUploadRequest;
use App\Http\Responses\ApiResponse; // ❌ 잘못된 경로
// ...
```
### 수정 요청
```php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemsFileUploadRequest;
use App\Helpers\ApiResponse; // ✅ 올바른 경로
// ...
```
### 영향 범위
- 절곡부품(PT-BENDING) 전개도 업로드
- 조립부품(PT-ASSEMBLY) 전개도 업로드
- 제품(FG) 시방서 업로드
- 제품(FG) 인정서 업로드
### 우선순위
🔴 **긴급** - 모든 파일 업로드 기능이 동작하지 않음 (한 줄 수정으로 해결 가능)
---
## 수정 완료 내역
> 수정 완료된 항목은 아래로 이동
(아직 없음)
---
## 참고 사항
### 관련 파일 (프론트엔드)
- `src/app/[locale]/(protected)/items/create/page.tsx` - 품목 등록 페이지
- `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` - 품목 수정 페이지
- `src/components/items/DynamicItemForm/index.tsx` - 동적 폼 컴포넌트
### 관련 파일 (백엔드)
- `/app/Http/Controllers/Api/V1/ItemsController.php` - 품목 API 컨트롤러
- `/app/Services/ItemsService.php` - 품목 서비스 레이어
- `/app/Http/Requests/Item/ItemStoreRequest.php` - 등록 요청 검증
- `/app/Http/Requests/Item/ItemUpdateRequest.php` - 수정 요청 검증
- `/app/Models/Materials/Material.php` - Material 모델

View File

@@ -1,379 +0,0 @@
# 단가관리 API 분석 및 구현 현황
> **작성일**: 2025-12-08
> **최종 업데이트**: 2025-12-08
> **상태**: ✅ **백엔드 API 구현 완료**
---
## 1. 구현 현황 요약
### ✅ 현재 API 구조 (구현 완료)
| Method | Endpoint | 설명 | 상태 |
|--------|----------|------|------|
| `GET` | `/api/v1/pricing` | 목록 조회 (페이지네이션, 필터) | ✅ 완료 |
| `GET` | `/api/v1/pricing/{id}` | 단건 조회 | ✅ 완료 |
| `POST` | `/api/v1/pricing` | 등록 | ✅ 완료 |
| `PUT` | `/api/v1/pricing/{id}` | 수정 | ✅ 완료 |
| `DELETE` | `/api/v1/pricing/{id}` | 삭제 (soft delete) | ✅ 완료 |
| `POST` | `/api/v1/pricing/{id}/finalize` | 확정 | ✅ 완료 |
| `GET` | `/api/v1/pricing/{id}/revisions` | 변경이력 조회 | ✅ 완료 |
| `POST` | `/api/v1/pricing/by-items` | 품목별 단가 현황 (다건) | ✅ 완료 |
| `GET` | `/api/v1/pricing/cost` | 원가 조회 (수입검사 > 표준원가) | ✅ 완료 |
### ✅ 완료된 사항
- **새 테이블 구조**: `prices`, `price_revisions` 테이블 생성
- **기존 테이블 마이그레이션**: `price_histories``prices` 데이터 이관
- **모든 요청 필드**: 원가 계산, 마진 관리, 리비전 관리 기능 구현
---
## 2. 테이블 스키마 (구현 완료)
### 2.1 `prices` 테이블 (신규 생성) ✅
```sql
CREATE TABLE prices (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
-- 품목 연결
item_type_code VARCHAR(20) NOT NULL, -- 'PRODUCT' | 'MATERIAL'
item_id BIGINT NOT NULL,
client_group_id BIGINT NULL, -- NULL=기본가
-- 원가 정보
purchase_price DECIMAL(15,4) NULL, -- 매입단가
processing_cost DECIMAL(15,4) NULL, -- 가공비
loss_rate DECIMAL(5,2) NULL, -- LOSS율 (%)
-- 판매가 정보
margin_rate DECIMAL(5,2) NULL, -- 마진율 (%)
sales_price DECIMAL(15,4) NULL, -- 판매단가
rounding_rule ENUM('round','ceil','floor') DEFAULT 'round',
rounding_unit INT DEFAULT 1, -- 1, 10, 100, 1000
-- 메타 정보
supplier VARCHAR(255) NULL,
effective_from DATE NOT NULL,
effective_to DATE NULL,
note TEXT NULL,
-- 상태 관리
status ENUM('draft','active','inactive','finalized') DEFAULT 'draft',
is_final BOOLEAN DEFAULT FALSE,
finalized_at DATETIME NULL,
finalized_by BIGINT NULL,
-- 감사 컬럼
created_by, updated_by, deleted_by, timestamps, soft_deletes
);
```
### 2.2 `price_revisions` 테이블 (변경 이력) ✅
```sql
CREATE TABLE price_revisions (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
price_id BIGINT NOT NULL,
revision_number INT NOT NULL,
changed_at DATETIME NOT NULL,
changed_by BIGINT NOT NULL,
change_reason VARCHAR(500) NULL,
before_snapshot JSON NULL,
after_snapshot JSON NOT NULL
);
```
### 2.3 기존 `price_histories` 테이블 처리 ✅
-`prices` 테이블로 데이터 마이그레이션 완료
-`price_histories` 테이블 삭제됨
---
## 3. API 엔드포인트 상세 (구현 완료)
### 3.1 단가 등록 `POST /api/v1/pricing` ✅
**Request Body:**
```json
{
"item_type_code": "MATERIAL",
"item_id": 123,
"client_group_id": null,
"purchase_price": 1000,
"processing_cost": 100,
"loss_rate": 5,
"margin_rate": 20,
"rounding_rule": "round",
"rounding_unit": 10,
"supplier": "ABC상사",
"effective_from": "2025-01-01",
"effective_to": null,
"note": "2025년 1분기 단가",
"status": "active"
}
```
**자동 처리:**
- `sales_price` 자동 계산 (입력 안해도 됨)
- 기존 무기한 단가의 `effective_to` 자동 설정
- 최초 리비전 자동 생성
### 3.2 목록 조회 `GET /api/v1/pricing` ✅
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `page` | int | 페이지 번호 |
| `size` | int | 페이지당 개수 (max 100) |
| `q` | string | 검색어 (공급업체, 비고) |
| `item_type_code` | string | `PRODUCT` / `MATERIAL` |
| `item_id` | int | 품목 ID |
| `client_group_id` | int/null | 고객그룹 ID |
| `status` | string | `draft`, `active`, `inactive`, `finalized` |
| `valid_at` | date | 특정 일자에 유효한 단가 |
**Response:**
```json
{
"success": true,
"message": "message.fetched",
"data": {
"current_page": 1,
"data": [
{
"id": 1,
"item_type_code": "MATERIAL",
"item_id": 123,
"client_group_id": null,
"purchase_price": "1000.0000",
"processing_cost": "100.0000",
"loss_rate": "5.00",
"margin_rate": "20.00",
"sales_price": "1386.0000",
"rounding_rule": "round",
"rounding_unit": 10,
"supplier": "ABC상사",
"effective_from": "2025-01-01",
"effective_to": null,
"status": "active",
"is_final": false,
"client_group": null
}
],
"total": 1
}
}
```
---
## 4. 추가 API 엔드포인트 (구현 완료)
### 4.1 품목별 단가 현황 `POST /api/v1/pricing/by-items` ✅
**용도**: 여러 품목의 현재 유효한 단가를 한번에 조회
**Request Body:**
```json
{
"items": [
{ "item_type_code": "MATERIAL", "item_id": 123 },
{ "item_type_code": "MATERIAL", "item_id": 124 },
{ "item_type_code": "PRODUCT", "item_id": 10 }
],
"client_group_id": 1,
"date": "2025-12-08"
}
```
**조회 우선순위:**
1. 고객그룹별 단가 (`client_group_id` 지정 시)
2. 기본 단가 (`client_group_id = NULL`)
**Response:**
```json
{
"success": true,
"data": [
{
"item_type_code": "MATERIAL",
"item_id": 123,
"price": { ... },
"has_price": true
},
{
"item_type_code": "MATERIAL",
"item_id": 124,
"price": null,
"has_price": false
}
]
}
```
### 4.2 변경 이력 조회 `GET /api/v1/pricing/{id}/revisions` ✅
**Response:**
```json
{
"success": true,
"data": {
"data": [
{
"id": 2,
"revision_number": 2,
"changed_at": "2025-12-08T11:00:00.000000Z",
"changed_by": 1,
"change_reason": "마진율 조정",
"before_snapshot": {
"purchase_price": "1000.0000",
"margin_rate": "15.00",
"sales_price": "1265.0000"
},
"after_snapshot": {
"purchase_price": "1000.0000",
"margin_rate": "20.00",
"sales_price": "1386.0000"
},
"changed_by_user": { "id": 1, "name": "홍길동" }
}
]
}
}
```
### 4.3 단가 확정 `POST /api/v1/pricing/{id}/finalize` ✅
**동작:**
- `is_final``true`
- `status``finalized`
- 확정 후 **수정/삭제 불가**
**Response:**
```json
{
"success": true,
"message": "message.pricing.finalized",
"data": {
"id": 5,
"is_final": true,
"finalized_at": "2025-12-08T14:30:00.000000Z",
"finalized_by": 1,
"status": "finalized"
}
}
```
### 4.4 원가 조회 `GET /api/v1/pricing/cost` ✅
**용도**: 수입검사 입고단가 > 표준원가 우선순위로 원가 조회
**Query Parameters:**
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| `item_type_code` | string | ✅ | `PRODUCT` / `MATERIAL` |
| `item_id` | int | ✅ | 품목 ID |
| `date` | date | ❌ | 조회 기준일 (기본: 오늘) |
**Response:**
```json
{
"success": true,
"data": {
"item_type_code": "MATERIAL",
"item_id": 123,
"date": "2025-12-08",
"cost_source": "receipt",
"purchase_price": 1050.00,
"receipt_id": 45,
"receipt_date": "2025-12-01",
"price_id": null
}
}
```
| cost_source | 설명 |
|-------------|------|
| `receipt` | 수입검사 입고단가 |
| `standard` | 표준원가 (prices 테이블) |
| `not_found` | 단가 미등록 |
---
## 5. 비즈니스 로직
### 5.1 판매단가 자동 계산
```
총원가 = (매입단가 + 가공비) × (1 + LOSS율/100)
판매단가 = 반올림(총원가 × (1 + 마진율/100), 반올림단위, 반올림규칙)
```
### 5.2 상태 흐름
```
draft → active → finalized
inactive
```
### 5.3 주요 검증 규칙
- 동일 품목+고객그룹+시작일 조합 중복 불가
- 확정된 단가는 수정/삭제 불가
- 마진율/LOSS율은 0~100% 범위
- 반올림단위는 1, 10, 100, 1000 중 하나
---
## 6. 프론트엔드 작업 현황
| 작업 | 상태 | 비고 |
|------|------|------|
| `usePricingList` 훅 생성 | ⬜ 미완료 | |
| 타입 정의 | ⬜ 미완료 | |
| 단가 목록 페이지 | ⬜ 미완료 | |
| 단가 등록/수정 페이지 | ⬜ 미완료 | |
| 단가 상세 페이지 (이력 포함) | ⬜ 미완료 | |
| 품목별 단가 현황 컴포넌트 | ⬜ 미완료 | |
---
## 7. 백엔드 참고 파일
### 컨트롤러/서비스
- `api/app/Http/Controllers/Api/V1/PricingController.php`
- `api/app/Services/PricingService.php`
### 모델
- `api/app/Models/Products/Price.php`
- `api/app/Models/Products/PriceRevision.php`
### 요청 클래스
- `api/app/Http/Requests/Pricing/PriceIndexRequest.php`
- `api/app/Http/Requests/Pricing/PriceStoreRequest.php`
- `api/app/Http/Requests/Pricing/PriceUpdateRequest.php`
- `api/app/Http/Requests/Pricing/PriceByItemsRequest.php`
- `api/app/Http/Requests/Pricing/PriceCostRequest.php`
### 마이그레이션
- `api/database/migrations/2025_12_08_154633_create_prices_table.php`
- `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php`
- `api/database/migrations/2025_12_08_154635_migrate_price_histories_to_prices.php`
- `api/database/migrations/2025_12_08_154636_drop_price_histories_table.php`
### 라우트
- `api/routes/api.php` (Line 368-379)
---
## 8. 관련 문서
- [단가 정책](../rules/pricing-policy.md) - 상세 정책 및 계산 공식
---
**최종 업데이트**: 2025-12-08

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
# API 스펙 회신 (2025-11-28)
요청서 검토 완료. 아래 2가지만 변경됩니다.
---
## 변경 1: 식별자 코드 → ID
```
PUT /items/{code} → PUT /items/{id}
DELETE /items/{code} → DELETE /items/{id}
/items/{code}/bom/* → /items/{id}/bom/*
/items/{code}/files/* → /items/{id}/files/*
```
## 변경 2: 일괄 삭제 파라미터
```
{ "codes": [...] } → { "ids": [...] }
```
---
나머지는 요청서대로 진행합니다.