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:
2025-12-08 20:19:32 +09:00
parent 88644c88ab
commit e152de469a
5 changed files with 1772 additions and 313 deletions

View File

@@ -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

View 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 모델

View 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`

View 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
}
}
}
]
}

View 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
}
}
}
]
}