docs: Client API Flow Tester JSON 추가 및 문서 업데이트
- Client API CRUD 플로우 테스트 JSON 추가 (11단계) - Client Group API CRUD 플로우 테스트 JSON 추가 (10단계) - client_groups 테이블 스키마 정정 (group_code, group_name, price_rate 필드) - API 응답 구조 및 필드 타입 주의사항 문서화 - Flow Tester 섹션 추가 (사용법 및 주의사항)
This commit is contained in:
@@ -1,382 +1,523 @@
|
||||
# 거래처 관리 API 분석
|
||||
# 거래처 관리 API 분석 및 구현 현황
|
||||
|
||||
> **작성일**: 2025-12-04
|
||||
> **목적**: sam-api 백엔드 Client API와 프론트엔드 거래처 관리 페이지 간 연동 분석
|
||||
> **최종 업데이트**: 2025-12-08
|
||||
> **상태**: ✅ **백엔드 + 프론트엔드 구현 완료**
|
||||
|
||||
---
|
||||
|
||||
## 1. 현재 상태 요약
|
||||
## 1. 구현 현황 요약
|
||||
|
||||
### 프론트엔드 (sam-react-prod)
|
||||
- **파일**: `src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx`
|
||||
- **상태**: ❌ **API 미연동** - 로컬 샘플 데이터(`SAMPLE_CUSTOMERS`)로만 동작
|
||||
- **모든 CRUD가 클라이언트 사이드에서만 수행됨**
|
||||
### ✅ 백엔드 API 구조 (구현 완료)
|
||||
|
||||
### 백엔드 (sam-api)
|
||||
- **컨트롤러**: `app/Http/Controllers/Api/V1/ClientController.php`
|
||||
- **서비스**: `app/Services/ClientService.php`
|
||||
- **모델**: `app/Models/Orders/Client.php`
|
||||
- **상태**: ✅ **API 구현 완료** - 모든 CRUD 기능 제공
|
||||
#### 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. 백엔드 API 명세
|
||||
## 2. 테이블 스키마 (구현 완료)
|
||||
|
||||
### 2.1 Client (거래처) API
|
||||
### 2.1 `clients` 테이블 ✅
|
||||
|
||||
| Method | Endpoint | 설명 | 인증 |
|
||||
|--------|----------|------|------|
|
||||
| `GET` | `/api/v1/clients` | 목록 조회 (페이지네이션, 검색) | ✅ Required |
|
||||
| `GET` | `/api/v1/clients/{id}` | 단건 조회 | ✅ Required |
|
||||
| `POST` | `/api/v1/clients` | 생성 | ✅ Required |
|
||||
| `PUT` | `/api/v1/clients/{id}` | 수정 | ✅ Required |
|
||||
| `DELETE` | `/api/v1/clients/{id}` | 삭제 | ✅ Required |
|
||||
| `PATCH` | `/api/v1/clients/{id}/toggle` | 활성/비활성 토글 | ✅ Required |
|
||||
```sql
|
||||
CREATE TABLE clients (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
client_group_id BIGINT NULL,
|
||||
|
||||
### 2.2 Client Group (거래처 그룹) API
|
||||
-- 기본 정보
|
||||
client_code VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
client_type ENUM('매입','매출','매입매출') DEFAULT '매입',
|
||||
|
||||
| Method | Endpoint | 설명 | 인증 |
|
||||
|--------|----------|------|------|
|
||||
| `GET` | `/api/v1/client-groups` | 그룹 목록 | ✅ Required |
|
||||
| `GET` | `/api/v1/client-groups/{id}` | 그룹 단건 | ✅ Required |
|
||||
| `POST` | `/api/v1/client-groups` | 그룹 생성 | ✅ Required |
|
||||
| `PUT` | `/api/v1/client-groups/{id}` | 그룹 수정 | ✅ Required |
|
||||
| `DELETE` | `/api/v1/client-groups/{id}` | 그룹 삭제 | ✅ Required |
|
||||
| `PATCH` | `/api/v1/client-groups/{id}/toggle` | 그룹 활성/비활성 | ✅ Required |
|
||||
-- 사업자 정보
|
||||
business_no VARCHAR(20) NULL, -- 사업자등록번호
|
||||
business_type VARCHAR(50) NULL, -- 업태
|
||||
business_item VARCHAR(100) NULL, -- 업종
|
||||
|
||||
### 2.3 목록 조회 파라미터 (`GET /api/v1/clients`)
|
||||
-- 연락처 정보
|
||||
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 CHAR(1) DEFAULT 'Y', -- 'Y' 또는 'N'
|
||||
|
||||
-- 감사 컬럼
|
||||
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` | integer | 페이지 번호 | 1 |
|
||||
| `size` | integer | 페이지당 개수 | 20 |
|
||||
| `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": "Y",
|
||||
"client_group": { "id": 1, "name": "VIP" }
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 데이터 모델 비교
|
||||
### 3.2 단건 조회 `GET /api/v1/clients/{id}` ✅
|
||||
|
||||
### 3.1 필드 매핑 분석
|
||||
단건 조회 시 모든 필드 포함 (목록과 동일 구조)
|
||||
|
||||
| 프론트엔드 필드 | 백엔드 필드 | 상태 | 비고 |
|
||||
|---------------|------------|------|------|
|
||||
| `id` | `id` | ✅ 동일 | |
|
||||
| `code` | `client_code` | ✅ 매핑 필요 | 필드명 변경 |
|
||||
| `name` | `name` | ✅ 동일 | |
|
||||
| `representative` | `contact_person` | ✅ 매핑 필요 | 필드명 변경 |
|
||||
| `phone` | `phone` | ✅ 동일 | |
|
||||
| `email` | `email` | ✅ 동일 | |
|
||||
| `address` | `address` | ✅ 동일 | |
|
||||
| `registeredDate` | `created_at` | ✅ 매핑 필요 | 필드명 변경 |
|
||||
| `status` | `is_active` | ✅ 매핑 필요 | "활성"/"비활성" ↔ "Y"/"N" |
|
||||
| `businessNo` | - | ❌ **백엔드 없음** | 추가 필요 |
|
||||
| `businessType` | - | ❌ **백엔드 없음** | 추가 필요 |
|
||||
| `businessItem` | - | ❌ **백엔드 없음** | 추가 필요 |
|
||||
| - | `tenant_id` | ✅ 백엔드 전용 | 자동 처리 |
|
||||
| - | `client_group_id` | ⚠️ 프론트 없음 | 그룹 기능 미구현 |
|
||||
### 3.3 거래처 등록 `POST /api/v1/clients` ✅
|
||||
|
||||
### 3.2 백엔드 모델 필드 (Client.php)
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"client_code": "CLI-002",
|
||||
"name": "XYZ무역",
|
||||
"client_type": "매입",
|
||||
"client_group_id": 1,
|
||||
|
||||
```php
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'client_group_id',
|
||||
'client_code', // 거래처 코드
|
||||
'name', // 거래처명
|
||||
'contact_person', // 담당자
|
||||
'phone', // 전화번호
|
||||
'email', // 이메일
|
||||
'address', // 주소
|
||||
'is_active', // 활성 상태 (Y/N)
|
||||
];
|
||||
"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}` ✅
|
||||
|
||||
Soft Delete 적용
|
||||
|
||||
### 3.6 활성/비활성 토글 `PATCH /api/v1/clients/{id}/toggle` ✅
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "message.updated",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"is_active": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 백엔드 수정 요청 사항
|
||||
## 4. 필드 구조 (7개 섹션)
|
||||
|
||||
### 4.1 1차 필드 추가 ✅ 완료 (2025-12-04)
|
||||
|
||||
| 필드명 | 타입 | 설명 | 상태 |
|
||||
### 섹션 1: 기본 정보
|
||||
| 필드명 | 타입 | 필수 | 설명 |
|
||||
|--------|------|------|------|
|
||||
| `business_no` | string(20) | 사업자등록번호 | ✅ 추가됨 |
|
||||
| `business_type` | string(50) | 업태 | ✅ 추가됨 |
|
||||
| `business_item` | string(100) | 업종 | ✅ 추가됨 |
|
||||
| `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) |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 🚨 2차 필드 추가 요청 (sam-design 기준) - 2025-12-04
|
||||
## 5. 프론트엔드 타입 정의
|
||||
|
||||
> **참고**: `sam-design/src/components/ClientRegistration.tsx` 기준으로 UI 구현 필요
|
||||
> 현재 백엔드 API에 누락된 필드들 추가 요청
|
||||
|
||||
#### 섹션 1: 기본 정보 추가 필드
|
||||
| 필드명 | 타입 | 설명 | nullable | 비고 |
|
||||
|--------|------|------|----------|------|
|
||||
| `client_type` | enum('매입','매출','매입매출') | 거래처 유형 | NO | 기본값 '매입' |
|
||||
|
||||
#### 섹션 2: 연락처 정보 추가 필드
|
||||
| 필드명 | 타입 | 설명 | nullable |
|
||||
|--------|------|------|----------|
|
||||
| `mobile` | string(20) | 모바일 번호 | YES |
|
||||
| `fax` | string(20) | 팩스 번호 | YES |
|
||||
|
||||
#### 섹션 3: 담당자 정보 추가 필드
|
||||
| 필드명 | 타입 | 설명 | nullable |
|
||||
|--------|------|------|----------|
|
||||
| `manager_name` | string(50) | 담당자명 | YES |
|
||||
| `manager_tel` | string(20) | 담당자 전화 | YES |
|
||||
| `system_manager` | string(50) | 시스템 관리자 | YES |
|
||||
|
||||
#### 섹션 4: 발주처 설정 추가 필드
|
||||
| 필드명 | 타입 | 설명 | nullable |
|
||||
|--------|------|------|----------|
|
||||
| `account_id` | string(50) | 계정 ID | YES |
|
||||
| `account_password` | string(255) | 비밀번호 (암호화) | YES |
|
||||
| `purchase_payment_day` | string(20) | 매입 결제일 | YES |
|
||||
| `sales_payment_day` | string(20) | 매출 결제일 | YES |
|
||||
|
||||
#### 섹션 5: 약정 세금 추가 필드
|
||||
| 필드명 | 타입 | 설명 | nullable |
|
||||
|--------|------|------|----------|
|
||||
| `tax_agreement` | boolean | 세금 약정 여부 | YES |
|
||||
| `tax_amount` | decimal(15,2) | 약정 금액 | YES |
|
||||
| `tax_start_date` | date | 약정 시작일 | YES |
|
||||
| `tax_end_date` | date | 약정 종료일 | YES |
|
||||
|
||||
#### 섹션 6: 악성채권 정보 추가 필드
|
||||
| 필드명 | 타입 | 설명 | nullable |
|
||||
|--------|------|------|----------|
|
||||
| `bad_debt` | boolean | 악성채권 여부 | YES |
|
||||
| `bad_debt_amount` | decimal(15,2) | 악성채권 금액 | YES |
|
||||
| `bad_debt_receive_date` | date | 채권 발생일 | YES |
|
||||
| `bad_debt_end_date` | date | 채권 만료일 | YES |
|
||||
| `bad_debt_progress` | enum('협의중','소송중','회수완료','대손처리') | 진행 상태 | YES |
|
||||
|
||||
#### 섹션 7: 기타 정보 추가 필드
|
||||
| 필드명 | 타입 | 설명 | nullable |
|
||||
|--------|------|------|----------|
|
||||
| `memo` | text | 메모 | YES |
|
||||
|
||||
---
|
||||
|
||||
### 4.3 마이그레이션 예시
|
||||
|
||||
```sql
|
||||
-- 기본 정보
|
||||
ALTER TABLE clients ADD COLUMN client_type ENUM('매입','매출','매입매출') DEFAULT '매입';
|
||||
|
||||
-- 연락처 정보
|
||||
ALTER TABLE clients ADD COLUMN mobile VARCHAR(20) NULL;
|
||||
ALTER TABLE clients ADD COLUMN fax VARCHAR(20) NULL;
|
||||
|
||||
-- 담당자 정보
|
||||
ALTER TABLE clients ADD COLUMN manager_name VARCHAR(50) NULL;
|
||||
ALTER TABLE clients ADD COLUMN manager_tel VARCHAR(20) NULL;
|
||||
ALTER TABLE clients ADD COLUMN system_manager VARCHAR(50) NULL;
|
||||
|
||||
-- 발주처 설정
|
||||
ALTER TABLE clients ADD COLUMN account_id VARCHAR(50) NULL;
|
||||
ALTER TABLE clients ADD COLUMN account_password VARCHAR(255) NULL;
|
||||
ALTER TABLE clients ADD COLUMN purchase_payment_day VARCHAR(20) NULL;
|
||||
ALTER TABLE clients ADD COLUMN sales_payment_day VARCHAR(20) NULL;
|
||||
|
||||
-- 약정 세금
|
||||
ALTER TABLE clients ADD COLUMN tax_agreement TINYINT(1) DEFAULT 0;
|
||||
ALTER TABLE clients ADD COLUMN tax_amount DECIMAL(15,2) NULL;
|
||||
ALTER TABLE clients ADD COLUMN tax_start_date DATE NULL;
|
||||
ALTER TABLE clients ADD COLUMN tax_end_date DATE NULL;
|
||||
|
||||
-- 악성채권 정보
|
||||
ALTER TABLE clients ADD COLUMN bad_debt TINYINT(1) DEFAULT 0;
|
||||
ALTER TABLE clients ADD COLUMN bad_debt_amount DECIMAL(15,2) NULL;
|
||||
ALTER TABLE clients ADD COLUMN bad_debt_receive_date DATE NULL;
|
||||
ALTER TABLE clients ADD COLUMN bad_debt_end_date DATE NULL;
|
||||
ALTER TABLE clients ADD COLUMN bad_debt_progress ENUM('협의중','소송중','회수완료','대손처리') NULL;
|
||||
|
||||
-- 기타 정보
|
||||
ALTER TABLE clients ADD COLUMN memo TEXT NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 수정 필요 파일 목록
|
||||
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| `app/Models/Orders/Client.php` | fillable에 새 필드 추가, casts 설정 |
|
||||
| `database/migrations/xxxx_add_client_extended_fields.php` | 마이그레이션 생성 |
|
||||
| `app/Services/ClientService.php` | 새 필드 처리 로직 추가 |
|
||||
| `app/Http/Requests/Client/ClientStoreRequest.php` | 유효성 검증 규칙 추가 |
|
||||
| `app/Http/Requests/Client/ClientUpdateRequest.php` | 유효성 검증 규칙 추가 |
|
||||
| `app/Swagger/v1/ClientApi.php` | API 문서 업데이트 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 프론트엔드 API 연동 구현 계획
|
||||
|
||||
### 5.1 필요한 작업
|
||||
|
||||
| # | 작업 | 우선순위 | 상태 |
|
||||
|---|------|---------|------|
|
||||
| 1 | Next.js API Proxy 생성 (`/api/proxy/clients/[...path]`) | 🔴 HIGH | ⬜ 미완료 |
|
||||
| 2 | 커스텀 훅 생성 (`useClientList`) | 🔴 HIGH | ⬜ 미완료 |
|
||||
| 3 | 타입 정의 업데이트 (`CustomerAccount` → API 응답 매핑) | 🟡 MEDIUM | ⬜ 미완료 |
|
||||
| 4 | CRUD 함수를 API 호출로 변경 | 🔴 HIGH | ⬜ 미완료 |
|
||||
| 5 | 거래처 그룹 기능 추가 (선택) | 🟢 LOW | ⬜ 미완료 |
|
||||
|
||||
### 5.2 API Proxy 구현 패턴
|
||||
### 5.1 API 응답 타입
|
||||
|
||||
```typescript
|
||||
// /src/app/api/proxy/clients/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
export type ClientType = "매입" | "매출" | "매입매출";
|
||||
export type BadDebtProgress = "협의중" | "소송중" | "회수완료" | "대손처리" | "";
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const token = request.cookies.get('access_token')?.value;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/clients?${searchParams.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return NextResponse.json(await response.json());
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 useClientList 훅 구현 패턴
|
||||
|
||||
```typescript
|
||||
// /src/hooks/useClientList.ts
|
||||
export function useClientList() {
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const fetchClients = async (params: FetchParams) => {
|
||||
setIsLoading(true);
|
||||
const searchParams = new URLSearchParams({
|
||||
page: String(params.page || 1),
|
||||
size: String(params.size || 20),
|
||||
...(params.q && { q: params.q }),
|
||||
...(params.onlyActive !== undefined && { only_active: String(params.onlyActive) }),
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/proxy/clients?${searchParams}`);
|
||||
const data = await response.json();
|
||||
|
||||
setClients(data.data.data);
|
||||
setPagination({
|
||||
currentPage: data.data.current_page,
|
||||
lastPage: data.data.last_page,
|
||||
total: data.data.total,
|
||||
});
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return { clients, pagination, isLoading, fetchClients };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 데이터 변환 유틸리티
|
||||
|
||||
### 6.1 API 응답 → 프론트엔드 타입 변환
|
||||
|
||||
```typescript
|
||||
// API 응답 타입
|
||||
interface ClientApiResponse {
|
||||
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;
|
||||
is_active: 'Y' | 'N';
|
||||
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: "Y" | "N";
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 프론트엔드 타입으로 변환
|
||||
function transformClient(api: ClientApiResponse): CustomerAccount {
|
||||
return {
|
||||
id: String(api.id),
|
||||
code: api.client_code,
|
||||
name: api.name,
|
||||
representative: api.contact_person || '',
|
||||
phone: api.phone || '',
|
||||
email: api.email || '',
|
||||
address: api.address || '',
|
||||
businessNo: '', // TODO: 백엔드 필드 추가 후 매핑
|
||||
businessType: '', // TODO: 백엔드 필드 추가 후 매핑
|
||||
businessItem: '', // TODO: 백엔드 필드 추가 후 매핑
|
||||
registeredDate: api.created_at.split(' ')[0],
|
||||
status: api.is_active === 'Y' ? '활성' : '비활성',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 프론트엔드 → API 요청 변환
|
||||
### 5.2 프론트엔드 변환 타입
|
||||
|
||||
```typescript
|
||||
function transformToApiRequest(form: FormData): ClientCreateRequest {
|
||||
return {
|
||||
client_code: form.code,
|
||||
name: form.name,
|
||||
contact_person: form.representative || null,
|
||||
phone: form.phone || null,
|
||||
email: form.email || null,
|
||||
address: form.address || null,
|
||||
is_active: 'Y',
|
||||
};
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 결론 및 권장 사항
|
||||
## 6. 백엔드 참고 파일
|
||||
|
||||
### 7.1 즉시 진행 가능 (백엔드 변경 없이)
|
||||
### 컨트롤러/서비스
|
||||
- `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`
|
||||
|
||||
1. ✅ API Proxy 생성
|
||||
2. ✅ useClientList 훅 구현
|
||||
3. ✅ 기본 CRUD 연동 (현재 백엔드 필드만 사용)
|
||||
### 모델
|
||||
- `api/app/Models/Orders/Client.php`
|
||||
- `api/app/Models/Orders/ClientGroup.php`
|
||||
|
||||
### 7.2 백엔드 변경 필요
|
||||
### 요청 클래스
|
||||
- `api/app/Http/Requests/Client/ClientStoreRequest.php`
|
||||
- `api/app/Http/Requests/Client/ClientUpdateRequest.php`
|
||||
|
||||
1. ⚠️ `business_no`, `business_type`, `business_item` 필드 추가
|
||||
2. ⚠️ ClientService, ClientStoreRequest, ClientUpdateRequest 업데이트
|
||||
3. ⚠️ Swagger 문서 업데이트
|
||||
### 마이그레이션
|
||||
- `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`
|
||||
|
||||
### 7.3 선택적 개선
|
||||
### Swagger
|
||||
- `api/app/Swagger/v1/ClientApi.php`
|
||||
|
||||
1. 거래처 그룹 기능 프론트엔드 구현
|
||||
2. 거래처 상세 페이지 구현
|
||||
3. 엑셀 내보내기/가져오기 기능
|
||||
### 라우트
|
||||
- `api/routes/api.php` (Line 316-333)
|
||||
|
||||
---
|
||||
|
||||
## 참고 파일
|
||||
## 7. 프론트엔드 참고 파일
|
||||
|
||||
### 백엔드 (sam-api)
|
||||
- `app/Http/Controllers/Api/V1/ClientController.php`
|
||||
- `app/Http/Controllers/Api/V1/ClientGroupController.php`
|
||||
- `app/Services/ClientService.php`
|
||||
- `app/Services/ClientGroupService.php`
|
||||
- `app/Models/Orders/Client.php`
|
||||
- `app/Models/Orders/ClientGroup.php`
|
||||
- `app/Swagger/v1/ClientApi.php`
|
||||
- `routes/api.php` (Line 316-333)
|
||||
### 훅
|
||||
- `react/src/hooks/useClientList.ts` - CRUD 훅 (530줄)
|
||||
- `react/src/hooks/useClientGroupList.ts` - 그룹 CRUD 훅
|
||||
|
||||
### 프론트엔드 (sam-react-prod)
|
||||
- `src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx`
|
||||
### 컴포넌트
|
||||
- `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
|
||||
533
front/[API-2025-12-06] item-crud-backend-requests.md
Normal file
533
front/[API-2025-12-06] item-crud-backend-requests.md
Normal file
@@ -0,0 +1,533 @@
|
||||
# 품목 등록/수정 백엔드 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 모델
|
||||
358
front/[API-2025-12-08] pricing-api-enhancement-request.md
Normal file
358
front/[API-2025-12-08] pricing-api-enhancement-request.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# 단가관리 API 개선 요청서
|
||||
|
||||
> **작성일**: 2025-12-08
|
||||
> **요청자**: 프론트엔드 개발팀
|
||||
> **대상**: sam-api 백엔드 팀
|
||||
|
||||
---
|
||||
|
||||
## 1. 현황 요약
|
||||
|
||||
### 현재 API 구조
|
||||
| Endpoint | Method | 상태 |
|
||||
|----------|--------|------|
|
||||
| `/api/v1/pricing` | GET | 목록 조회 |
|
||||
| `/api/v1/pricing/show` | GET | 단일 가격 조회 |
|
||||
| `/api/v1/pricing/bulk` | POST | 일괄 가격 조회 |
|
||||
| `/api/v1/pricing/upsert` | POST | 등록/수정 |
|
||||
| `/api/v1/pricing/{id}` | DELETE | 삭제 |
|
||||
|
||||
### ✅ 이미 지원됨 (품목 정보)
|
||||
- `item_type_code` (품목유형) - PriceHistory 테이블
|
||||
- `item_code`, `item_name`, `specification`, `unit` - item 관계 JOIN으로 조회 가능
|
||||
|
||||
### ❌ 문제점 (단가 상세 정보)
|
||||
- 프론트엔드 단가관리 화면에서 요구하는 **단가 계산 필드** 대부분 누락
|
||||
- 현재 `price_histories` 테이블은 **단순 가격 이력**만 저장 (`price` 단일 필드)
|
||||
- 프론트엔드는 **원가 계산, 마진 관리, 리비전 관리** 기능 필요
|
||||
|
||||
---
|
||||
|
||||
## 2. 테이블 스키마 변경 요청
|
||||
|
||||
### 2.1 `price_histories` 테이블 필드 추가
|
||||
|
||||
```sql
|
||||
ALTER TABLE price_histories ADD COLUMN purchase_price DECIMAL(15,4) NULL COMMENT '매입단가(입고가)';
|
||||
ALTER TABLE price_histories ADD COLUMN processing_cost DECIMAL(15,4) NULL COMMENT '가공비';
|
||||
ALTER TABLE price_histories ADD COLUMN loss_rate DECIMAL(5,2) NULL COMMENT 'LOSS율(%)';
|
||||
ALTER TABLE price_histories ADD COLUMN rounding_rule ENUM('round','ceil','floor') DEFAULT 'round' COMMENT '반올림 규칙';
|
||||
ALTER TABLE price_histories ADD COLUMN rounding_unit INT DEFAULT 1 COMMENT '반올림 단위(1,10,100,1000)';
|
||||
ALTER TABLE price_histories ADD COLUMN margin_rate DECIMAL(5,2) NULL COMMENT '마진율(%)';
|
||||
ALTER TABLE price_histories ADD COLUMN sales_price DECIMAL(15,4) NULL COMMENT '판매단가';
|
||||
ALTER TABLE price_histories ADD COLUMN supplier VARCHAR(255) NULL COMMENT '공급업체';
|
||||
ALTER TABLE price_histories ADD COLUMN author VARCHAR(100) NULL COMMENT '작성자';
|
||||
ALTER TABLE price_histories ADD COLUMN receive_date DATE NULL COMMENT '입고일';
|
||||
ALTER TABLE price_histories ADD COLUMN note TEXT NULL COMMENT '비고';
|
||||
ALTER TABLE price_histories ADD COLUMN revision_number INT DEFAULT 0 COMMENT '리비전 번호';
|
||||
ALTER TABLE price_histories ADD COLUMN is_final BOOLEAN DEFAULT FALSE COMMENT '최종 확정 여부';
|
||||
ALTER TABLE price_histories ADD COLUMN finalized_at DATETIME NULL COMMENT '확정일시';
|
||||
ALTER TABLE price_histories ADD COLUMN finalized_by INT NULL COMMENT '확정자 ID';
|
||||
ALTER TABLE price_histories ADD COLUMN status ENUM('draft','active','inactive','finalized') DEFAULT 'draft' COMMENT '상태';
|
||||
```
|
||||
|
||||
### 2.2 기존 `price` 필드 처리 방안
|
||||
|
||||
**옵션 A (권장)**: `price` 필드를 `sales_price`로 마이그레이션
|
||||
```sql
|
||||
UPDATE price_histories SET sales_price = price WHERE price_type_code = 'SALE';
|
||||
UPDATE price_histories SET purchase_price = price WHERE price_type_code = 'PURCHASE';
|
||||
-- 이후 price 필드 deprecated 또는 삭제
|
||||
```
|
||||
|
||||
**옵션 B**: `price` 필드 유지, 새 필드와 병행 사용
|
||||
- 기존 로직 호환성 유지
|
||||
- 점진적 마이그레이션
|
||||
|
||||
---
|
||||
|
||||
## 3. API 엔드포인트 수정 요청
|
||||
|
||||
### 3.1 `POST /api/v1/pricing/upsert` 수정
|
||||
|
||||
**현재 Request Body:**
|
||||
```json
|
||||
{
|
||||
"item_type_code": "PRODUCT",
|
||||
"item_id": 10,
|
||||
"price_type_code": "SALE",
|
||||
"client_group_id": 1,
|
||||
"price": 50000.00,
|
||||
"started_at": "2025-01-01",
|
||||
"ended_at": "2025-12-31"
|
||||
}
|
||||
```
|
||||
|
||||
**요청 Request Body:**
|
||||
```json
|
||||
{
|
||||
"item_type_code": "PRODUCT",
|
||||
"item_id": 10,
|
||||
"client_group_id": 1,
|
||||
|
||||
"purchase_price": 45000,
|
||||
"processing_cost": 5000,
|
||||
"loss_rate": 3.5,
|
||||
"rounding_rule": "round",
|
||||
"rounding_unit": 100,
|
||||
"margin_rate": 20.0,
|
||||
"sales_price": 60000,
|
||||
|
||||
"supplier": "ABC공급",
|
||||
"author": "홍길동",
|
||||
"receive_date": "2025-01-01",
|
||||
"started_at": "2025-01-01",
|
||||
"ended_at": null,
|
||||
"note": "2025년 1분기 단가",
|
||||
|
||||
"is_revision": false,
|
||||
"revision_reason": "가격 인상"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 `GET /api/v1/pricing` 수정 (목록 조회)
|
||||
|
||||
**현재 Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"item_type_code": "PRODUCT",
|
||||
"item_id": 10,
|
||||
"price_type_code": "SALE",
|
||||
"price": 50000,
|
||||
"started_at": "2025-01-01"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**요청 Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"item_type_code": "PRODUCT",
|
||||
"item_id": 10,
|
||||
"item_code": "SCREEN-001",
|
||||
"item_name": "스크린 셔터 기본형",
|
||||
"specification": "표준형",
|
||||
"unit": "SET",
|
||||
|
||||
"purchase_price": 45000,
|
||||
"processing_cost": 5000,
|
||||
"loss_rate": 3.5,
|
||||
"margin_rate": 20.0,
|
||||
"sales_price": 60000,
|
||||
|
||||
"started_at": "2025-01-01",
|
||||
"ended_at": null,
|
||||
"status": "active",
|
||||
"revision_number": 1,
|
||||
"is_final": false,
|
||||
|
||||
"supplier": "ABC공급",
|
||||
"created_at": "2025-01-01 10:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**핵심 변경**: 품목 정보 JOIN 필요 (`item_masters` 또는 `products`/`materials` 테이블)
|
||||
|
||||
---
|
||||
|
||||
## 4. 신규 API 엔드포인트 요청
|
||||
|
||||
### 4.1 품목 기반 단가 현황 조회 (신규)
|
||||
|
||||
**용도**: 품목 마스터 기준으로 단가 등록/미등록 현황 조회
|
||||
|
||||
**Endpoint**: `GET /api/v1/pricing/by-items`
|
||||
|
||||
**Query Parameters:**
|
||||
| 파라미터 | 타입 | 설명 |
|
||||
|---------|------|------|
|
||||
| `item_type_code` | string | 품목 유형 (FG, PT, SM, RM, CS) |
|
||||
| `search` | string | 품목코드/품목명 검색 |
|
||||
| `status` | string | `all`, `registered`, `not_registered` |
|
||||
| `size` | int | 페이지당 항목 수 |
|
||||
| `page` | int | 페이지 번호 |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"item_id": 1,
|
||||
"item_code": "SCREEN-001",
|
||||
"item_name": "스크린 셔터 기본형",
|
||||
"item_type": "FG",
|
||||
"specification": "표준형",
|
||||
"unit": "SET",
|
||||
|
||||
"pricing_id": null,
|
||||
"has_pricing": false,
|
||||
"purchase_price": null,
|
||||
"sales_price": null,
|
||||
"margin_rate": null,
|
||||
"status": "not_registered"
|
||||
},
|
||||
{
|
||||
"item_id": 2,
|
||||
"item_code": "GR-001",
|
||||
"item_name": "가이드레일 130×80",
|
||||
"item_type": "PT",
|
||||
"specification": "130×80×2438",
|
||||
"unit": "EA",
|
||||
|
||||
"pricing_id": 5,
|
||||
"has_pricing": true,
|
||||
"purchase_price": 45000,
|
||||
"sales_price": 60000,
|
||||
"margin_rate": 20.0,
|
||||
"effective_date": "2025-01-01",
|
||||
"status": "active",
|
||||
"revision_number": 1,
|
||||
"is_final": false
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"total_items": 100,
|
||||
"registered": 45,
|
||||
"not_registered": 55,
|
||||
"finalized": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 단가 이력 조회 (신규)
|
||||
|
||||
**Endpoint**: `GET /api/v1/pricing/{id}/revisions`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"revision_number": 2,
|
||||
"revision_date": "2025-06-01",
|
||||
"revision_by": "김철수",
|
||||
"revision_reason": "원자재 가격 인상",
|
||||
"previous_purchase_price": 40000,
|
||||
"previous_sales_price": 55000,
|
||||
"new_purchase_price": 45000,
|
||||
"new_sales_price": 60000
|
||||
},
|
||||
{
|
||||
"revision_number": 1,
|
||||
"revision_date": "2025-01-01",
|
||||
"revision_by": "홍길동",
|
||||
"revision_reason": "최초 등록",
|
||||
"previous_purchase_price": null,
|
||||
"previous_sales_price": null,
|
||||
"new_purchase_price": 40000,
|
||||
"new_sales_price": 55000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 단가 확정 (신규)
|
||||
|
||||
**Endpoint**: `POST /api/v1/pricing/{id}/finalize`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"finalize_reason": "2025년 1분기 단가 확정"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 5,
|
||||
"is_final": true,
|
||||
"finalized_at": "2025-12-08 14:30:00",
|
||||
"finalized_by": 1,
|
||||
"status": "finalized"
|
||||
},
|
||||
"message": "단가가 최종 확정되었습니다."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 프론트엔드 타입 참조
|
||||
|
||||
프론트엔드에서 사용하는 타입 정의 (`src/components/pricing/types.ts`):
|
||||
|
||||
```typescript
|
||||
interface PricingData {
|
||||
id: string;
|
||||
itemId: string;
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
itemType: string;
|
||||
specification?: string;
|
||||
unit: string;
|
||||
|
||||
// 단가 정보
|
||||
effectiveDate: string; // started_at
|
||||
receiveDate?: string; // receive_date
|
||||
author?: string; // author
|
||||
purchasePrice?: number; // purchase_price
|
||||
processingCost?: number; // processing_cost
|
||||
loss?: number; // loss_rate
|
||||
roundingRule?: RoundingRule; // rounding_rule
|
||||
roundingUnit?: number; // rounding_unit
|
||||
marginRate?: number; // margin_rate
|
||||
salesPrice?: number; // sales_price
|
||||
supplier?: string; // supplier
|
||||
note?: string; // note
|
||||
|
||||
// 리비전 관리
|
||||
currentRevision: number; // revision_number
|
||||
isFinal: boolean; // is_final
|
||||
revisions?: PricingRevision[];
|
||||
finalizedDate?: string; // finalized_at
|
||||
finalizedBy?: string; // finalized_by
|
||||
status: PricingStatus; // status
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 우선순위
|
||||
|
||||
| 순위 | 항목 | 중요도 |
|
||||
|------|------|--------|
|
||||
| 1 | 테이블 스키마 변경 (필드 추가) | 🔴 필수 |
|
||||
| 2 | `POST /pricing/upsert` 수정 | 🔴 필수 |
|
||||
| 3 | `GET /pricing/by-items` 신규 | 🔴 필수 |
|
||||
| 4 | `GET /pricing` 응답 확장 | 🟡 중요 |
|
||||
| 5 | `GET /pricing/{id}/revisions` 신규 | 🟡 중요 |
|
||||
| 6 | `POST /pricing/{id}/finalize` 신규 | 🟢 권장 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 질문/협의 사항
|
||||
|
||||
1. **기존 price 필드 처리**: 마이그레이션 vs 병행 사용?
|
||||
2. **리비전 테이블 분리**: `price_history_revisions` 별도 테이블 vs 현재 테이블 확장?
|
||||
3. **품목 연결**: `item_masters` 테이블 사용 vs `products`/`materials` 각각 JOIN?
|
||||
|
||||
---
|
||||
|
||||
**연락처**: 프론트엔드 개발팀
|
||||
**관련 파일**: `src/components/pricing/types.ts`
|
||||
234
front/flow-tests/client-api-flow.json
Normal file
234
front/flow-tests/client-api-flow.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"name": "Client API CRUD Flow Test",
|
||||
"description": "거래처(Client) 관리 API 전체 CRUD 플로우 테스트 - 로그인, 목록조회, 생성, 단건조회, 수정, 토글, 삭제",
|
||||
"version": "1.0",
|
||||
"config": {
|
||||
"baseUrl": "",
|
||||
"timeout": 30000,
|
||||
"stopOnFailure": true
|
||||
},
|
||||
"variables": {
|
||||
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
|
||||
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
|
||||
"test_client_code": "TEST-{{$timestamp}}",
|
||||
"test_client_name": "테스트거래처_{{$timestamp}}"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "login",
|
||||
"name": "1. 로그인",
|
||||
"method": "POST",
|
||||
"endpoint": "/login",
|
||||
"body": {
|
||||
"user_id": "{{user_id}}",
|
||||
"user_pwd": "{{user_pwd}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.access_token": "@isString"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"token": "$.access_token"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "list_clients",
|
||||
"name": "2. 거래처 목록 조회",
|
||||
"method": "GET",
|
||||
"endpoint": "/clients",
|
||||
"params": {
|
||||
"page": 1,
|
||||
"size": 10
|
||||
},
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.current_page": 1
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"total_before": "$.data.total"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_client",
|
||||
"name": "3. 거래처 생성",
|
||||
"method": "POST",
|
||||
"endpoint": "/clients",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"client_code": "{{test_client_code}}",
|
||||
"name": "{{test_client_name}}",
|
||||
"contact_person": "테스트담당자",
|
||||
"phone": "02-1234-5678",
|
||||
"email": "test@example.com",
|
||||
"address": "서울시 강남구 테스트로 123",
|
||||
"business_no": "123-45-67890",
|
||||
"business_type": "서비스업",
|
||||
"business_item": "소프트웨어개발",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"client_id": "$.data.id",
|
||||
"client_code": "$.data.client_code"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "show_client",
|
||||
"name": "4. 거래처 단건 조회",
|
||||
"method": "GET",
|
||||
"endpoint": "/clients/{{create_client.client_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "{{create_client.client_id}}",
|
||||
"$.data.client_code": "{{create_client.client_code}}",
|
||||
"$.data.name": "{{test_client_name}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "update_client",
|
||||
"name": "5. 거래처 수정",
|
||||
"method": "PUT",
|
||||
"endpoint": "/clients/{{create_client.client_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"client_code": "{{create_client.client_code}}",
|
||||
"name": "{{test_client_name}}_수정됨",
|
||||
"contact_person": "수정담당자",
|
||||
"phone": "02-9999-8888",
|
||||
"email": "updated@example.com",
|
||||
"address": "서울시 서초구 수정로 456"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.contact_person": "수정담당자"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify_update",
|
||||
"name": "6. 수정 확인 조회",
|
||||
"method": "GET",
|
||||
"endpoint": "/clients/{{create_client.client_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.contact_person": "수정담당자",
|
||||
"$.data.phone": "02-9999-8888"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toggle_inactive",
|
||||
"name": "7. 거래처 비활성화 토글",
|
||||
"method": "PATCH",
|
||||
"endpoint": "/clients/{{create_client.client_id}}/toggle",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.is_active": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toggle_active",
|
||||
"name": "8. 거래처 활성화 토글",
|
||||
"method": "PATCH",
|
||||
"endpoint": "/clients/{{create_client.client_id}}/toggle",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.is_active": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "search_client",
|
||||
"name": "9. 거래처 검색",
|
||||
"method": "GET",
|
||||
"endpoint": "/clients",
|
||||
"params": {
|
||||
"q": "{{test_client_name}}",
|
||||
"page": 1,
|
||||
"size": 10
|
||||
},
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.total": "@isNumber"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "delete_client",
|
||||
"name": "10. 거래처 삭제",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/clients/{{create_client.client_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify_delete",
|
||||
"name": "11. 삭제 확인 (404 예상)",
|
||||
"method": "GET",
|
||||
"endpoint": "/clients/{{create_client.client_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [404],
|
||||
"jsonPath": {
|
||||
"$.success": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
193
front/flow-tests/client-group-api-flow.json
Normal file
193
front/flow-tests/client-group-api-flow.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"name": "Client Group API CRUD Flow Test",
|
||||
"description": "거래처 그룹(Client Group) API 전체 CRUD 플로우 테스트 - 로그인, 목록조회, 생성, 단건조회, 수정, 토글, 삭제",
|
||||
"version": "1.0",
|
||||
"config": {
|
||||
"baseUrl": "",
|
||||
"timeout": 30000,
|
||||
"stopOnFailure": true
|
||||
},
|
||||
"variables": {
|
||||
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
|
||||
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
|
||||
"test_group_name": "테스트그룹_{{$timestamp}}",
|
||||
"test_group_code": "TG-{{$timestamp}}"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "login",
|
||||
"name": "1. 로그인",
|
||||
"method": "POST",
|
||||
"endpoint": "/login",
|
||||
"body": {
|
||||
"user_id": "{{user_id}}",
|
||||
"user_pwd": "{{user_pwd}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.access_token": "@isString"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"token": "$.access_token"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "list_groups",
|
||||
"name": "2. 거래처 그룹 목록 조회",
|
||||
"method": "GET",
|
||||
"endpoint": "/client-groups",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_group",
|
||||
"name": "3. 거래처 그룹 생성",
|
||||
"method": "POST",
|
||||
"endpoint": "/client-groups",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"group_code": "{{test_group_code}}",
|
||||
"group_name": "{{test_group_name}}",
|
||||
"price_rate": 1.0,
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"group_id": "$.data.id",
|
||||
"group_name": "$.data.group_name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "show_group",
|
||||
"name": "4. 거래처 그룹 단건 조회",
|
||||
"method": "GET",
|
||||
"endpoint": "/client-groups/{{create_group.group_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "{{create_group.group_id}}",
|
||||
"$.data.group_name": "{{test_group_name}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "update_group",
|
||||
"name": "5. 거래처 그룹 수정",
|
||||
"method": "PUT",
|
||||
"endpoint": "/client-groups/{{create_group.group_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"group_code": "{{test_group_code}}",
|
||||
"group_name": "{{test_group_name}}_수정됨",
|
||||
"price_rate": 1.5
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify_update",
|
||||
"name": "6. 수정 확인 조회",
|
||||
"method": "GET",
|
||||
"endpoint": "/client-groups/{{create_group.group_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.price_rate": 1.5
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toggle_inactive",
|
||||
"name": "7. 그룹 비활성화 토글",
|
||||
"method": "PATCH",
|
||||
"endpoint": "/client-groups/{{create_group.group_id}}/toggle",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.is_active": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toggle_active",
|
||||
"name": "8. 그룹 활성화 토글",
|
||||
"method": "PATCH",
|
||||
"endpoint": "/client-groups/{{create_group.group_id}}/toggle",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.is_active": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "delete_group",
|
||||
"name": "9. 거래처 그룹 삭제",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/client-groups/{{create_group.group_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify_delete",
|
||||
"name": "10. 삭제 확인 (404 예상)",
|
||||
"method": "GET",
|
||||
"endpoint": "/client-groups/{{create_group.group_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [404],
|
||||
"jsonPath": {
|
||||
"$.success": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user