From e152de469a3840765e5742f1d0785efef1118e89 Mon Sep 17 00:00:00 2001 From: hskwon Date: Mon, 8 Dec 2025 20:19:32 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20Client=20API=20Flow=20Tester=20JSON=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=AC=B8=EC=84=9C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Client API CRUD 플로우 테스트 JSON 추가 (11단계) - Client Group API CRUD 플로우 테스트 JSON 추가 (10단계) - client_groups 테이블 스키마 정정 (group_code, group_name, price_rate 필드) - API 응답 구조 및 필드 타입 주의사항 문서화 - Flow Tester 섹션 추가 (사용법 및 주의사항) --- front/[API-2025-12-04] client-api-analysis.md | 767 +++++++++++------- ...-2025-12-06] item-crud-backend-requests.md | 533 ++++++++++++ ...-12-08] pricing-api-enhancement-request.md | 358 ++++++++ front/flow-tests/client-api-flow.json | 234 ++++++ front/flow-tests/client-group-api-flow.json | 193 +++++ 5 files changed, 1772 insertions(+), 313 deletions(-) create mode 100644 front/[API-2025-12-06] item-crud-backend-requests.md create mode 100644 front/[API-2025-12-08] pricing-api-enhancement-request.md create mode 100644 front/flow-tests/client-api-flow.json create mode 100644 front/flow-tests/client-group-api-flow.json diff --git a/front/[API-2025-12-04] client-api-analysis.md b/front/[API-2025-12-04] client-api-analysis.md index 62f5542..5a2a801 100644 --- a/front/[API-2025-12-04] client-api-analysis.md +++ b/front/[API-2025-12-04] client-api-analysis.md @@ -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([]); - const [pagination, setPagination] = useState(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` \ No newline at end of file +### 컴포넌트 +- `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 \ No newline at end of file diff --git a/front/[API-2025-12-06] item-crud-backend-requests.md b/front/[API-2025-12-06] item-crud-backend-requests.md new file mode 100644 index 0000000..30c3243 --- /dev/null +++ b/front/[API-2025-12-06] item-crud-backend-requests.md @@ -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 + 수정 완료된 항목은 아래로 이동 + +(아직 없음) + +--- + +## 참고 사항 + +### 관련 파일 (프론트엔드) +- `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 모델 diff --git a/front/[API-2025-12-08] pricing-api-enhancement-request.md b/front/[API-2025-12-08] pricing-api-enhancement-request.md new file mode 100644 index 0000000..e717957 --- /dev/null +++ b/front/[API-2025-12-08] pricing-api-enhancement-request.md @@ -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` \ No newline at end of file diff --git a/front/flow-tests/client-api-flow.json b/front/flow-tests/client-api-flow.json new file mode 100644 index 0000000..7d15db8 --- /dev/null +++ b/front/flow-tests/client-api-flow.json @@ -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 + } + } + } + ] +} diff --git a/front/flow-tests/client-group-api-flow.json b/front/flow-tests/client-group-api-flow.json new file mode 100644 index 0000000..23886ed --- /dev/null +++ b/front/flow-tests/client-group-api-flow.json @@ -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 + } + } + } + ] +}