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:
@@ -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
|
||||
@@ -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. **알림**: 견적 발송 시 이메일/카카오톡 연동 계획?
|
||||
@@ -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 모델
|
||||
@@ -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
@@ -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": [...] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
나머지는 요청서대로 진행합니다.
|
||||
Reference in New Issue
Block a user