From 182c2d1b57e1ffa10c2185a7d03926e11e209506 Mon Sep 17 00:00:00 2001 From: hskwon Date: Tue, 9 Dec 2025 20:28:38 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20Item=20Master=20=EC=8A=A4=ED=8E=99=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ITEM-MASTER-INDEX.md: 문서 인덱스 및 핵심 개념 정의 - item-master-field-integration.md: 범용 메타 필드 시스템 구현 계획 (v1.3) - item-master-field-key-validation.md: field_key 검증 정책 --- front/[API-2025-12-04] client-api-analysis.md | 523 ------- front/[API-2025-12-04] quote-api-request.md | 675 --------- ...-2025-12-06] item-crud-backend-requests.md | 533 ------- ...-12-08] pricing-api-enhancement-request.md | 379 ----- ...-2025-11-28] dynamic-page-rendering-api.md | 1112 --------------- ...ONSE-2025-11-28] items-api-spec-changes.md | 24 - specs/ITEM-MASTER-INDEX.md | 225 +++ specs/item-master-field-integration.md | 1238 +++++++++++++++++ specs/item-master-field-key-validation.md | 200 +++ 9 files changed, 1663 insertions(+), 3246 deletions(-) delete mode 100644 front/[API-2025-12-04] client-api-analysis.md delete mode 100644 front/[API-2025-12-04] quote-api-request.md delete mode 100644 front/[API-2025-12-06] item-crud-backend-requests.md delete mode 100644 front/[API-2025-12-08] pricing-api-enhancement-request.md delete mode 100644 front/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md delete mode 100644 front/[API-RESPONSE-2025-11-28] items-api-spec-changes.md create mode 100644 specs/ITEM-MASTER-INDEX.md create mode 100644 specs/item-master-field-integration.md create mode 100644 specs/item-master-field-key-validation.md diff --git a/front/[API-2025-12-04] client-api-analysis.md b/front/[API-2025-12-04] client-api-analysis.md deleted file mode 100644 index 91fad47..0000000 --- a/front/[API-2025-12-04] client-api-analysis.md +++ /dev/null @@ -1,523 +0,0 @@ -# 거래처 관리 API 분석 및 구현 현황 - -> **작성일**: 2025-12-04 -> **최종 업데이트**: 2025-12-08 -> **상태**: ✅ **백엔드 + 프론트엔드 구현 완료** - ---- - -## 1. 구현 현황 요약 - -### ✅ 백엔드 API 구조 (구현 완료) - -#### Client (거래처) API -| Method | Endpoint | 설명 | 상태 | -|--------|----------|------|------| -| `GET` | `/api/v1/clients` | 목록 조회 (페이지네이션, 검색) | ✅ 완료 | -| `GET` | `/api/v1/clients/{id}` | 단건 조회 | ✅ 완료 | -| `POST` | `/api/v1/clients` | 생성 | ✅ 완료 | -| `PUT` | `/api/v1/clients/{id}` | 수정 | ✅ 완료 | -| `DELETE` | `/api/v1/clients/{id}` | 삭제 | ✅ 완료 | -| `PATCH` | `/api/v1/clients/{id}/toggle` | 활성/비활성 토글 | ✅ 완료 | - -#### Client Group (거래처 그룹) API -| Method | Endpoint | 설명 | 상태 | -|--------|----------|------|------| -| `GET` | `/api/v1/client-groups` | 그룹 목록 | ✅ 완료 | -| `GET` | `/api/v1/client-groups/{id}` | 그룹 단건 | ✅ 완료 | -| `POST` | `/api/v1/client-groups` | 그룹 생성 | ✅ 완료 | -| `PUT` | `/api/v1/client-groups/{id}` | 그룹 수정 | ✅ 완료 | -| `DELETE` | `/api/v1/client-groups/{id}` | 그룹 삭제 | ✅ 완료 | -| `PATCH` | `/api/v1/client-groups/{id}/toggle` | 그룹 활성/비활성 | ✅ 완료 | - -### ✅ 프론트엔드 구현 현황 (구현 완료) -| 작업 | 상태 | 파일 | -|------|------|------| -| API Proxy | ✅ 완료 | `/api/proxy/[...path]/route.ts` (catch-all) | -| `useClientList` 훅 | ✅ 완료 | `src/hooks/useClientList.ts` | -| 타입 정의 (확장 필드 포함) | ✅ 완료 | `src/hooks/useClientList.ts` | -| 거래처 등록 페이지 | ✅ 완료 | `.../client-management-sales-admin/new/page.tsx` | -| 거래처 상세 페이지 | ✅ 완료 | `.../client-management-sales-admin/[id]/page.tsx` | -| 거래처 수정 페이지 | ✅ 완료 | `.../client-management-sales-admin/[id]/edit/page.tsx` | -| 목록 페이지 개선 | ✅ 완료 | 모달 삭제, 페이지 네비게이션 적용 | - ---- - -## 2. 테이블 스키마 (구현 완료) - -### 2.1 `clients` 테이블 ✅ - -```sql -CREATE TABLE clients ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT NOT NULL, - client_group_id BIGINT NULL, - - -- 기본 정보 - client_code VARCHAR(50) NOT NULL, - name VARCHAR(255) NOT NULL, - client_type ENUM('매입','매출','매입매출') DEFAULT '매입', - - -- 사업자 정보 - business_no VARCHAR(20) NULL, -- 사업자등록번호 - business_type VARCHAR(50) NULL, -- 업태 - business_item VARCHAR(100) NULL, -- 업종 - - -- 연락처 정보 - contact_person VARCHAR(100) NULL, -- 대표 담당자 - phone VARCHAR(20) NULL, - mobile VARCHAR(20) NULL, - fax VARCHAR(20) NULL, - email VARCHAR(255) NULL, - address TEXT NULL, - - -- 담당자 정보 - manager_name VARCHAR(50) NULL, -- 담당자명 - manager_tel VARCHAR(20) NULL, -- 담당자 전화 - system_manager VARCHAR(50) NULL, -- 시스템 관리자 - - -- 발주처 설정 - account_id VARCHAR(50) NULL, -- 계정 ID - account_password VARCHAR(255) NULL, -- 비밀번호 (암호화, hidden) - purchase_payment_day VARCHAR(20) NULL, -- 매입 결제일 - sales_payment_day VARCHAR(20) NULL, -- 매출 결제일 - - -- 약정 세금 - tax_agreement BOOLEAN DEFAULT FALSE, - tax_amount DECIMAL(15,2) NULL, - tax_start_date DATE NULL, - tax_end_date DATE NULL, - - -- 악성채권 정보 - bad_debt BOOLEAN DEFAULT FALSE, - bad_debt_amount DECIMAL(15,2) NULL, - bad_debt_receive_date DATE NULL, - bad_debt_end_date DATE NULL, - bad_debt_progress ENUM('협의중','소송중','회수완료','대손처리') NULL, - - -- 기타 - memo TEXT NULL, - is_active TINYINT(1) DEFAULT 1, -- 1=활성, 0=비활성 - - -- 감사 컬럼 - created_at TIMESTAMP, - updated_at TIMESTAMP -); -``` - -### 2.2 `client_groups` 테이블 ✅ - -```sql -CREATE TABLE client_groups ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT NOT NULL, - group_code VARCHAR(30) NOT NULL, -- 그룹 코드 (필수) - group_name VARCHAR(100) NOT NULL, -- 그룹명 (필수) - price_rate DECIMAL(6,4) NOT NULL, -- 단가율 (필수, 0~99.9999) - is_active BOOLEAN DEFAULT TRUE, - created_by BIGINT NULL, - updated_by BIGINT NULL, - deleted_by BIGINT NULL, - created_at TIMESTAMP, - updated_at TIMESTAMP, - deleted_at TIMESTAMP NULL -- Soft Delete -); -``` - -**⚠️ 주의**: `client_groups` API 필드명 -- `group_code` (필수) - 그룹 코드 -- `group_name` (필수) - 그룹명 (`name`이 아님!) -- `price_rate` (필수) - 단가율 -- `is_active` (선택) - boolean - ---- - -## 3. API 엔드포인트 상세 (구현 완료) - -### 3.1 목록 조회 `GET /api/v1/clients` ✅ - -**Query Parameters:** -| 파라미터 | 타입 | 설명 | 기본값 | -|---------|------|------|--------| -| `page` | int | 페이지 번호 | 1 | -| `size` | int | 페이지당 개수 | 20 | -| `q` | string | 검색어 (이름, 코드, 담당자) | - | -| `only_active` | boolean | 활성 거래처만 조회 | - | - -**Response:** -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "current_page": 1, - "data": [ - { - "id": 1, - "client_code": "CLI-001", - "name": "ABC상사", - "client_type": "매입매출", - "business_no": "123-45-67890", - "business_type": "제조업", - "business_item": "기계부품", - "contact_person": "홍길동", - "phone": "02-1234-5678", - "mobile": "010-1234-5678", - "fax": "02-1234-5679", - "email": "hong@abc.com", - "address": "서울시 강남구...", - "manager_name": "김철수", - "manager_tel": "02-1234-5680", - "purchase_payment_day": "말일", - "sales_payment_day": "말일", - "tax_agreement": false, - "bad_debt": false, - "memo": null, - "is_active": true, - "client_group": { "id": 1, "name": "VIP" } - } - ], - "total": 1 - } -} -``` - -### 3.2 단건 조회 `GET /api/v1/clients/{id}` ✅ - -단건 조회 시 모든 필드 포함 (목록과 동일 구조) - -### 3.3 거래처 등록 `POST /api/v1/clients` ✅ - -**Request Body:** -```json -{ - "client_code": "CLI-002", - "name": "XYZ무역", - "client_type": "매입", - "client_group_id": 1, - - "business_no": "234-56-78901", - "business_type": "도소매업", - "business_item": "전자부품", - - "contact_person": "이영희", - "phone": "02-2345-6789", - "mobile": "010-2345-6789", - "fax": "02-2345-6790", - "email": "lee@xyz.com", - "address": "서울시 서초구...", - - "manager_name": "박민수", - "manager_tel": "02-2345-6791", - "system_manager": "관리자A", - - "account_id": "xyz_user", - "account_password": "secret123", - "purchase_payment_day": "15일", - "sales_payment_day": "말일", - - "tax_agreement": true, - "tax_amount": 1000000, - "tax_start_date": "2025-01-01", - "tax_end_date": "2025-12-31", - - "bad_debt": false, - "memo": "신규 거래처" -} -``` - -### 3.4 거래처 수정 `PUT /api/v1/clients/{id}` ✅ - -등록과 동일한 필드 구조 (부분 수정 가능) - -**주의:** `account_password`는 입력한 경우에만 업데이트됨 - -### 3.5 거래처 삭제 `DELETE /api/v1/clients/{id}` ✅ - -Hard Delete 적용 (연관 주문이 없는 경우만 삭제 가능) - -### 3.6 활성/비활성 토글 `PATCH /api/v1/clients/{id}/toggle` ✅ - -**Response:** -```json -{ - "success": true, - "message": "message.updated", - "data": { - "id": 1, - "is_active": false - } -} -``` - ---- - -## 4. 필드 구조 (7개 섹션) - -### 섹션 1: 기본 정보 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `client_code` | string | ✅ | 거래처 코드 | -| `name` | string | ✅ | 거래처명 | -| `client_type` | enum | ❌ | 매입/매출/매입매출 (기본: 매입) | -| `client_group_id` | int | ❌ | 거래처 그룹 ID | - -### 섹션 2: 사업자 정보 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `business_no` | string | ❌ | 사업자등록번호 | -| `business_type` | string | ❌ | 업태 | -| `business_item` | string | ❌ | 업종 | - -### 섹션 3: 연락처 정보 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `contact_person` | string | ❌ | 대표 담당자 | -| `phone` | string | ❌ | 전화번호 | -| `mobile` | string | ❌ | 휴대폰 | -| `fax` | string | ❌ | 팩스 | -| `email` | string | ❌ | 이메일 | -| `address` | text | ❌ | 주소 | - -### 섹션 4: 담당자 정보 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `manager_name` | string | ❌ | 담당자명 | -| `manager_tel` | string | ❌ | 담당자 전화 | -| `system_manager` | string | ❌ | 시스템 관리자 | - -### 섹션 5: 발주처 설정 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `account_id` | string | ❌ | 계정 ID | -| `account_password` | string | ❌ | 비밀번호 (hidden) | -| `purchase_payment_day` | string | ❌ | 매입 결제일 | -| `sales_payment_day` | string | ❌ | 매출 결제일 | - -### 섹션 6: 약정 세금 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `tax_agreement` | boolean | ❌ | 세금 약정 여부 | -| `tax_amount` | decimal | ❌ | 약정 금액 | -| `tax_start_date` | date | ❌ | 약정 시작일 | -| `tax_end_date` | date | ❌ | 약정 종료일 | - -### 섹션 7: 악성채권 정보 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `bad_debt` | boolean | ❌ | 악성채권 여부 | -| `bad_debt_amount` | decimal | ❌ | 악성채권 금액 | -| `bad_debt_receive_date` | date | ❌ | 채권 발생일 | -| `bad_debt_end_date` | date | ❌ | 채권 만료일 | -| `bad_debt_progress` | enum | ❌ | 진행 상태 | - -### 섹션 8: 기타 -| 필드명 | 타입 | 필수 | 설명 | -|--------|------|------|------| -| `memo` | text | ❌ | 메모 | -| `is_active` | boolean | ❌ | 활성 상태 (기본: true) | - ---- - -## 5. 프론트엔드 타입 정의 - -### 5.1 API 응답 타입 - -```typescript -export type ClientType = "매입" | "매출" | "매입매출"; -export type BadDebtProgress = "협의중" | "소송중" | "회수완료" | "대손처리" | ""; - -export interface ClientApiResponse { - id: number; - tenant_id: number; - client_group_id: number | null; - client_code: string; - name: string; - client_type?: ClientType; - business_no: string | null; - business_type: string | null; - business_item: string | null; - contact_person: string | null; - phone: string | null; - mobile?: string | null; - fax?: string | null; - email: string | null; - address: string | null; - manager_name?: string | null; - manager_tel?: string | null; - system_manager?: string | null; - account_id?: string | null; - purchase_payment_day?: string | null; - sales_payment_day?: string | null; - tax_agreement?: boolean; - tax_amount?: number | null; - tax_start_date?: string | null; - tax_end_date?: string | null; - bad_debt?: boolean; - bad_debt_amount?: number | null; - bad_debt_receive_date?: string | null; - bad_debt_end_date?: string | null; - bad_debt_progress?: BadDebtProgress; - memo?: string | null; - is_active: boolean; - created_at: string; - updated_at: string; -} -``` - -### 5.2 프론트엔드 변환 타입 - -```typescript -export interface Client { - id: string; - code: string; - name: string; - businessNo: string; - representative: string; - phone: string; - address: string; - email: string; - businessType: string; - businessItem: string; - registeredDate: string; - status: "활성" | "비활성"; - groupId: string | null; - clientType: ClientType; - mobile: string; - fax: string; - managerName: string; - managerTel: string; - systemManager: string; - accountId: string; - purchasePaymentDay: string; - salesPaymentDay: string; - taxAgreement: boolean; - taxAmount: string; - taxStartDate: string; - taxEndDate: string; - badDebt: boolean; - badDebtAmount: string; - badDebtReceiveDate: string; - badDebtEndDate: string; - badDebtProgress: BadDebtProgress; - memo: string; -} -``` - ---- - -## 6. 백엔드 참고 파일 - -### 컨트롤러/서비스 -- `api/app/Http/Controllers/Api/V1/ClientController.php` -- `api/app/Http/Controllers/Api/V1/ClientGroupController.php` -- `api/app/Services/ClientService.php` -- `api/app/Services/ClientGroupService.php` - -### 모델 -- `api/app/Models/Orders/Client.php` -- `api/app/Models/Orders/ClientGroup.php` - -### 요청 클래스 -- `api/app/Http/Requests/Client/ClientStoreRequest.php` -- `api/app/Http/Requests/Client/ClientUpdateRequest.php` - -### 마이그레이션 -- `api/database/migrations/2025_12_04_145912_add_business_fields_to_clients_table.php` -- `api/database/migrations/2025_12_04_205603_add_extended_fields_to_clients_table.php` - -### Swagger -- `api/app/Swagger/v1/ClientApi.php` - -### 라우트 -- `api/routes/api.php` (Line 316-333) - ---- - -## 7. 프론트엔드 참고 파일 - -### 훅 -- `react/src/hooks/useClientList.ts` - CRUD 훅 (530줄) -- `react/src/hooks/useClientGroupList.ts` - 그룹 CRUD 훅 - -### 컴포넌트 -- `react/src/components/clients/ClientRegistration.tsx` - 등록/수정 폼 (sam-design 기반) -- `react/src/components/clients/ClientDetail.tsx` - 상세 보기 - -### 페이지 -- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx` - 목록 -- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx` - 등록 -- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx` - 상세 -- `react/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/edit/page.tsx` - 수정 - ---- - -## 8. 추가 개선 사항 (선택) - -| 항목 | 우선순위 | 상태 | -|------|---------|------| -| 거래처 그룹 관리 UI | 🟢 LOW | ⬜ 미구현 | -| 엑셀 내보내기/가져오기 | 🟢 LOW | ⬜ 미구현 | -| 거래처 검색 고급 필터 | 🟢 LOW | ⬜ 미구현 | - ---- - -## 9. API Flow Tester 테스트 파일 - -### 9.1 Flow Tester 설명 - -API Flow Tester를 사용하여 Client 및 Client Group API의 전체 CRUD 플로우를 자동으로 테스트할 수 있습니다. - -### 9.2 테스트 파일 위치 - -| 파일 | 설명 | -|------|------| -| `docs/front/flow-tests/client-api-flow.json` | 거래처 API CRUD 플로우 테스트 (11단계) | -| `docs/front/flow-tests/client-group-api-flow.json` | 거래처 그룹 API CRUD 플로우 테스트 (10단계) | - -### 9.3 테스트 플로우 요약 - -#### Client API Flow (11단계) -1. 로그인 → 토큰 추출 -2. 거래처 목록 조회 -3. 거래처 생성 -4. 거래처 단건 조회 -5. 거래처 수정 -6. 수정 확인 조회 -7. 비활성화 토글 -8. 활성화 토글 -9. 거래처 검색 -10. 거래처 삭제 -11. 삭제 확인 (404 예상) - -#### Client Group API Flow (10단계) -1. 로그인 → 토큰 추출 -2. 그룹 목록 조회 -3. 그룹 생성 (`group_code`, `group_name`, `price_rate` 필수) -4. 그룹 단건 조회 -5. 그룹 수정 -6. 수정 확인 조회 -7. 비활성화 토글 -8. 활성화 토글 -9. 그룹 삭제 -10. 삭제 확인 (404 예상) - -### 9.4 Flow Tester 사용 시 주의사항 - -**환경변수 설정 필요:** -```bash -FLOW_TESTER_USER_ID=테스트계정ID -FLOW_TESTER_USER_PWD=테스트계정비밀번호 -``` - -**API 응답 구조 차이점:** -- `/login` 응답: `{ "access_token": "...", "token_type": "..." }` (루트 레벨) -- 기타 API 응답: `{ "success": true, "data": {...} }` (data 래핑) - -**필드 타입 주의:** -- `is_active`: boolean (`true`/`false`), 문자열 `"Y"`/`"N"` 아님 -- `price_rate`: numeric (0~99.9999) - ---- - -**최종 업데이트**: 2025-12-08 \ No newline at end of file diff --git a/front/[API-2025-12-04] quote-api-request.md b/front/[API-2025-12-04] quote-api-request.md deleted file mode 100644 index ebe9174..0000000 --- a/front/[API-2025-12-04] quote-api-request.md +++ /dev/null @@ -1,675 +0,0 @@ -# 견적관리 API 요청서 - -> **작성일**: 2025-12-04 -> **목적**: 견적관리 기능을 위한 백엔드 API 요청 -> **참조**: sam-design/QuoteManagement3Write.tsx, QuoteManagement3Detail.tsx - ---- - -## 1. 개요 - -### 1.1 기능 요약 -견적관리 시스템은 다음 기능을 지원해야 합니다: -- 견적 CRUD (등록, 조회, 수정, 삭제) -- 견적 상태 관리 (임시저장 → 확정 → 수주전환) -- 견적 수정 이력 관리 (버전 관리) -- 견적 품목(BOM) 관리 -- 자동 견적 산출 (수식 기반 계산) ← **백엔드 구현** - -### 1.2 특이사항 -- **자동 견적 산출 로직**은 백엔드에서 구현 예정 (수식 계산 엔진) -- 프론트엔드는 입력값을 전달하고 계산 결과를 받아서 표시 - ---- - -## 2. 데이터 모델 - -### 2.1 Quote (견적) - 메인 엔티티 - -```typescript -interface Quote { - // === 기본 정보 === - id: number; - tenant_id: number; - quote_number: string; // 견적번호 (예: KD-SC-251204-01) - registration_date: string; // 등록일 (YYYY-MM-DD) - receipt_date: string; // 접수일 - author: string; // 작성자 - - // === 발주처 정보 === - client_id: number | null; // 거래처 ID (FK) - client_name: string; // 거래처명 (직접입력 대응) - manager: string | null; // 담당자 - contact: string | null; // 연락처 - - // === 현장 정보 === - site_id: number | null; // 현장 ID (FK, 별도 테이블 필요시) - site_name: string | null; // 현장명 - site_code: string | null; // 현장코드 - - // === 제품 정보 === - product_category: 'SCREEN' | 'STEEL'; // 제품 카테고리 - product_id: number | null; // 선택된 제품 ID (품목마스터 FK) - product_code: string | null; // 제품코드 - product_name: string | null; // 제품명 - - // === 규격 정보 === - open_size_width: number | null; // 오픈사이즈 폭 (mm) - open_size_height: number | null; // 오픈사이즈 높이 (mm) - quantity: number; // 수량 (기본값: 1) - unit_symbol: string | null; // 부호 - floors: string | null; // 층수 - - // === 금액 정보 === - material_cost: number; // 재료비 합계 - labor_cost: number; // 노무비 - install_cost: number; // 설치비 - subtotal: number; // 소계 - discount_rate: number; // 할인율 (%) - discount_amount: number; // 할인금액 - total_amount: number; // 최종 금액 - - // === 상태 관리 === - status: 'draft' | 'sent' | 'approved' | 'rejected' | 'finalized' | 'converted'; - current_revision: number; // 현재 수정 차수 (0부터 시작) - is_final: boolean; // 최종확정 여부 - finalized_at: string | null; // 확정일시 - finalized_by: number | null; // 확정자 ID - - // === 기타 정보 === - completion_date: string | null; // 납기일 - remarks: string | null; // 비고 - memo: string | null; // 메모 - notes: string | null; // 특이사항 - - // === 시스템 필드 === - created_at: string; - updated_at: string; - created_by: number | null; - updated_by: number | null; - deleted_at: string | null; // Soft Delete -} -``` - -### 2.2 QuoteItem (견적 품목) - BOM 계산 결과 - -```typescript -interface QuoteItem { - id: number; - quote_id: number; // 견적 ID (FK) - tenant_id: number; - - // === 품목 정보 === - item_id: number | null; // 품목마스터 ID (FK) - item_code: string; // 품목코드 - item_name: string; // 품명 - specification: string | null; // 규격 - unit: string; // 단위 - - // === 수량/금액 === - base_quantity: number; // 기본수량 - calculated_quantity: number; // 계산된 수량 - unit_price: number; // 단가 - total_price: number; // 금액 (수량 × 단가) - - // === 수식 정보 === - formula: string | null; // 수식 (예: "W/1000 + 0.1") - formula_source: string | null; // 수식 출처 (BOM템플릿, 제품BOM 등) - formula_category: string | null; // 수식 카테고리 - data_source: string | null; // 데이터 출처 - - // === 기타 === - delivery_date: string | null; // 품목별 납기일 - note: string | null; // 비고 - sort_order: number; // 정렬순서 - - created_at: string; - updated_at: string; -} -``` - -### 2.3 QuoteRevision (견적 수정 이력) - -```typescript -interface QuoteRevision { - id: number; - quote_id: number; // 견적 ID (FK) - tenant_id: number; - - revision_number: number; // 수정 차수 (1, 2, 3...) - revision_date: string; // 수정일 - revision_by: number; // 수정자 ID - revision_by_name: string; // 수정자 이름 - revision_reason: string | null; // 수정 사유 - - // 이전 버전 데이터 (JSON) - previous_data: object; // 수정 전 견적 전체 데이터 (스냅샷) - - created_at: string; -} -``` - ---- - -## 3. API 엔드포인트 - -### 3.1 견적 CRUD - -| Method | Endpoint | 설명 | 비고 | -|--------|----------|------|------| -| `GET` | `/api/v1/quotes` | 목록 조회 | 페이지네이션, 필터, 검색 | -| `GET` | `/api/v1/quotes/{id}` | 단건 조회 | 품목(items), 이력(revisions) 포함 | -| `POST` | `/api/v1/quotes` | 생성 | 품목 배열 포함 | -| `PUT` | `/api/v1/quotes/{id}` | 수정 | 수정이력 자동 생성 | -| `DELETE` | `/api/v1/quotes/{id}` | 삭제 | Soft Delete | -| `DELETE` | `/api/v1/quotes` | 일괄 삭제 | `ids[]` 파라미터 | - -### 3.2 견적 상태 관리 - -| Method | Endpoint | 설명 | 비고 | -|--------|----------|------|------| -| `PATCH` | `/api/v1/quotes/{id}/finalize` | 최종확정 | status → 'finalized', is_final → true | -| `PATCH` | `/api/v1/quotes/{id}/convert-to-order` | 수주전환 | status → 'converted', 수주 데이터 생성 | -| `PATCH` | `/api/v1/quotes/{id}/cancel-finalize` | 확정취소 | is_final → false (조건부) | - -### 3.3 자동 견적 산출 (핵심 기능) - -| Method | Endpoint | 설명 | 비고 | -|--------|----------|------|------| -| `POST` | `/api/v1/quotes/calculate` | 자동 산출 | **수식 계산 엔진** | -| `POST` | `/api/v1/quotes/{id}/recalculate` | 재계산 | 기존 견적 재산출 | - -### 3.4 견적 문서 출력 - -| Method | Endpoint | 설명 | 비고 | -|--------|----------|------|------| -| `GET` | `/api/v1/quotes/{id}/document/quote` | 견적서 PDF | | -| `GET` | `/api/v1/quotes/{id}/document/calculation` | 산출내역서 PDF | | -| `GET` | `/api/v1/quotes/{id}/document/purchase-order` | 발주서 PDF | | - -### 3.5 문서 발송 API ⭐ 신규 요청 - -| Method | Endpoint | 설명 | 비고 | -|--------|----------|------|------| -| `POST` | `/api/v1/quotes/{id}/send/email` | 이메일 발송 | 첨부파일 포함 | -| `POST` | `/api/v1/quotes/{id}/send/fax` | 팩스 발송 | 팩스 서비스 연동 | -| `POST` | `/api/v1/quotes/{id}/send/kakao` | 카카오톡 발송 | 알림톡/친구톡 | - -### 3.6 견적번호 생성 - -| Method | Endpoint | 설명 | 비고 | -|--------|----------|------|------| -| `GET` | `/api/v1/quotes/generate-number` | 견적번호 생성 | `?category=SCREEN` | - ---- - -## 4. 상세 API 명세 - -### 4.1 목록 조회 `GET /api/v1/quotes` - -**Query Parameters:** -``` -page: number (default: 1) -size: number (default: 20) -q: string (검색어 - 견적번호, 발주처, 담당자, 현장명) -status: string (상태 필터) -product_category: string (제품 카테고리) -client_id: number (발주처 ID) -date_from: string (등록일 시작) -date_to: string (등록일 종료) -sort_by: string (정렬 컬럼) -sort_order: 'asc' | 'desc' -``` - -**Response:** -```json -{ - "success": true, - "data": { - "current_page": 1, - "data": [ - { - "id": 1, - "quote_number": "KD-SC-251204-01", - "registration_date": "2025-12-04", - "client_name": "ABC건설", - "site_name": "강남 오피스텔 현장", - "product_category": "SCREEN", - "product_name": "전동스크린 A형", - "quantity": 10, - "total_amount": 15000000, - "status": "draft", - "current_revision": 0, - "is_final": false, - "created_at": "2025-12-04T10:00:00Z" - } - ], - "last_page": 5, - "per_page": 20, - "total": 100 - } -} -``` - -### 4.2 단건 조회 `GET /api/v1/quotes/{id}` - -**Response:** -```json -{ - "success": true, - "data": { - "id": 1, - "quote_number": "KD-SC-251204-01", - "registration_date": "2025-12-04", - "receipt_date": "2025-12-04", - "author": "김철수", - - "client_id": 10, - "client_name": "ABC건설", - "manager": "이영희", - "contact": "010-1234-5678", - - "site_id": 5, - "site_name": "강남 오피스텔 현장", - "site_code": "PJ-20251204-01", - - "product_category": "SCREEN", - "product_id": 100, - "product_code": "SCR-001", - "product_name": "전동스크린 A형", - - "open_size_width": 2000, - "open_size_height": 3000, - "quantity": 10, - "unit_symbol": "A", - "floors": "3층", - - "material_cost": 12000000, - "labor_cost": 1500000, - "install_cost": 1500000, - "subtotal": 15000000, - "discount_rate": 0, - "discount_amount": 0, - "total_amount": 15000000, - - "status": "draft", - "current_revision": 2, - "is_final": false, - - "completion_date": "2025-12-31", - "remarks": "급하게 진행 필요", - "memo": "", - "notes": "", - - "items": [ - { - "id": 1, - "item_code": "SCR-MOTOR-001", - "item_name": "스크린 모터", - "specification": "220V, 1/4HP", - "unit": "EA", - "base_quantity": 1, - "calculated_quantity": 10, - "unit_price": 150000, - "total_price": 1500000, - "formula": "Q", - "formula_source": "제품BOM", - "sort_order": 1 - } - ], - - "revisions": [ - { - "revision_number": 2, - "revision_date": "2025-12-04", - "revision_by_name": "김철수", - "revision_reason": "고객 요청으로 수량 변경" - }, - { - "revision_number": 1, - "revision_date": "2025-12-03", - "revision_by_name": "김철수", - "revision_reason": "단가 조정" - } - ], - - "created_at": "2025-12-04T10:00:00Z", - "updated_at": "2025-12-04T15:30:00Z" - } -} -``` - -### 4.3 생성 `POST /api/v1/quotes` - -**Request Body:** -```json -{ - "registration_date": "2025-12-04", - "receipt_date": "2025-12-04", - - "client_id": 10, - "client_name": "ABC건설", - "manager": "이영희", - "contact": "010-1234-5678", - - "site_id": 5, - "site_name": "강남 오피스텔 현장", - "site_code": "PJ-20251204-01", - - "product_category": "SCREEN", - "product_id": 100, - - "open_size_width": 2000, - "open_size_height": 3000, - "quantity": 10, - "unit_symbol": "A", - "floors": "3층", - - "completion_date": "2025-12-31", - "remarks": "급하게 진행 필요", - - "items": [ - { - "item_id": 50, - "item_code": "SCR-MOTOR-001", - "item_name": "스크린 모터", - "unit": "EA", - "base_quantity": 1, - "calculated_quantity": 10, - "unit_price": 150000, - "total_price": 1500000, - "formula": "Q", - "sort_order": 1 - } - ] -} -``` - -### 4.4 자동 산출 `POST /api/v1/quotes/calculate` ⭐ 핵심 - -**Request Body:** -```json -{ - "product_id": 100, - "product_category": "SCREEN", - "open_size_width": 2000, - "open_size_height": 3000, - "quantity": 10, - "floors": "3층", - "unit_symbol": "A", - - "options": { - "guide_rail_install_type": "벽부형", - "motor_power": "1/4HP", - "controller": "표준형", - "edge_wing_size": 50, - "inspection_fee": 0 - } -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "product_id": 100, - "product_name": "전동스크린 A형", - "product_category": "SCREEN", - - "open_size": { - "width": 2000, - "height": 3000 - }, - "quantity": 10, - - "items": [ - { - "item_id": 50, - "item_code": "SCR-MOTOR-001", - "item_name": "스크린 모터", - "specification": "220V, 1/4HP", - "unit": "EA", - "base_quantity": 1, - "calculated_quantity": 10, - "unit_price": 150000, - "total_price": 1500000, - "formula": "Q", - "formula_result": "10 × 1 = 10", - "formula_source": "제품BOM: 전동스크린 A형", - "data_source": "품목마스터 [SCR-MOTOR-001]" - }, - { - "item_id": 51, - "item_code": "SCR-RAIL-001", - "item_name": "가이드레일", - "specification": "알루미늄", - "unit": "M", - "base_quantity": 1, - "calculated_quantity": 60, - "unit_price": 15000, - "total_price": 900000, - "formula": "H/1000 × 2 × Q", - "formula_result": "(3000/1000) × 2 × 10 = 60", - "formula_source": "BOM템플릿: 스크린_표준", - "data_source": "품목마스터 [SCR-RAIL-001]" - } - ], - - "summary": { - "material_cost": 12000000, - "labor_cost": 1500000, - "install_cost": 1500000, - "subtotal": 15000000, - "total_amount": 15000000 - }, - - "calculation_info": { - "bom_template_used": "스크린_표준", - "formula_variables": { - "W": 2000, - "H": 3000, - "Q": 10 - }, - "calculated_at": "2025-12-04T10:00:00Z" - } - } -} -``` - ---- - -## 5. 수식 계산 엔진 (백엔드 구현 요청) - -### 5.1 수식 변수 - -| 변수 | 설명 | 예시 | -|------|------|------| -| `W` | 오픈사이즈 폭 (mm) | 2000 | -| `H` | 오픈사이즈 높이 (mm) | 3000 | -| `Q` | 수량 | 10 | - -### 5.2 수식 예시 - -``` -수량 그대로: Q -높이 기반: H/1000 -폭+높이: (W + H) / 1000 -가이드레일: H/1000 × 2 × Q -스크린원단: (W/1000 + 0.1) × (H/1000 + 0.3) × Q -``` - -### 5.3 반올림 규칙 - -| 규칙 | 설명 | -|------|------| -| `ceil` | 올림 | -| `floor` | 내림 | -| `round` | 반올림 | - -### 5.4 BOM 템플릿 연동 - -- 제품별 BOM 템플릿에서 수식 조회 -- 템플릿이 없으면 품목마스터 BOM 사용 -- 수식 + 단가로 자동 금액 계산 - ---- - -## 6. 상태 흐름도 - -``` -[신규등록] - ↓ -[draft] 임시저장 - ↓ (최종확정) -[finalized] 확정 - ↓ (수주전환) -[converted] 수주전환 -``` - -### 6.1 상태별 제약 - -| 상태 | 수정 가능 | 삭제 가능 | 비고 | -|------|----------|----------|------| -| `draft` | O | O | 자유롭게 수정 | -| `sent` | O | O | 발송 후 수정 가능 (이력 기록) | -| `finalized` | X | X | 확정 후 수정 불가 | -| `converted` | X | X | 수주전환 후 불변 | - ---- - -## 7. 프론트엔드 구현 현황 (2025-12-04 업데이트) - -### 7.1 구현 완료된 파일 - -| 파일 | 설명 | 상태 | -|------|------|------| -| `quote-management/page.tsx` | 견적 목록 페이지 | ✅ 완료 (샘플 데이터) | -| `quote-management/new/page.tsx` | 견적 등록 페이지 | ✅ 완료 | -| `quote-management/[id]/page.tsx` | 견적 상세 페이지 | ✅ 완료 | -| `quote-management/[id]/edit/page.tsx` | 견적 수정 페이지 | ✅ 완료 | -| `components/quotes/QuoteRegistration.tsx` | 견적 등록/수정 컴포넌트 | ✅ 완료 | -| `components/quotes/QuoteDocument.tsx` | 견적서 문서 컴포넌트 | ✅ 완료 | -| `components/quotes/QuoteCalculationReport.tsx` | 산출내역서 문서 컴포넌트 | ✅ 완료 | -| `components/quotes/PurchaseOrderDocument.tsx` | 발주서 문서 컴포넌트 | ✅ 완료 | - -### 7.2 UI 기능 구현 현황 - -| 기능 | 상태 | 비고 | -|------|------|------| -| 견적 목록 조회 | ✅ UI 완료 | 샘플 데이터, API 연동 필요 | -| 견적 검색/필터 | ✅ UI 완료 | 로컬 필터링, API 연동 필요 | -| 견적 등록 폼 | ✅ UI 완료 | API 연동 필요 | -| 견적 상세 페이지 | ✅ UI 완료 | API 연동 필요 | -| 견적 수정 폼 | ✅ UI 완료 | API 연동 필요 | -| 견적 삭제 | ✅ UI 완료 | 로컬 상태, API 연동 필요 | -| 견적 일괄 삭제 | ✅ UI 완료 | 로컬 상태, API 연동 필요 | -| 자동 견적 산출 | ⏳ 버튼만 | 백엔드 수식 엔진 필요 | -| 발주처 선택 | ⏳ 샘플 데이터 | `/api/v1/clients` 연동 필요 | -| 현장 선택 | ⏳ 샘플 데이터 | 발주처 연동 후 현장 API 필요 | -| 제품 선택 | ⏳ 샘플 데이터 | `/api/v1/item-masters` 연동 필요 | -| **견적서 모달** | ✅ UI 완료 | PDF/이메일/팩스/카톡 버튼, **발송 API 필요** | -| **산출내역서 모달** | ✅ UI 완료 | PDF/이메일/팩스/카톡 버튼, **발송 API 필요** | -| **발주서 모달** | ✅ UI 완료 | PDF/이메일/팩스/카톡 버튼, **발송 API 필요** | -| 최종확정 버튼 | ✅ UI 완료 | API 연동 필요 | - -### 7.3 견적 등록/수정 폼 필드 (구현 완료) - -**기본 정보 섹션:** -- 등록일 (readonly, 오늘 날짜) -- 작성자 (readonly, 로그인 사용자) -- 발주처 선택 * (필수) -- 현장명 (발주처 선택 시 연동) -- 발주 담당자 -- 연락처 -- 납기일 -- 비고 - -**자동 견적 산출 섹션 (동적 항목):** -- 층수 -- 부호 -- 제품 카테고리 (PC) * -- 제품명 * -- 오픈사이즈 (W0) * -- 오픈사이즈 (H0) * -- 가이드레일 설치 유형 (GT) * -- 모터 전원 (MP) * -- 연동제어기 (CT) * -- 수량 (QTY) * -- 마구리 날개치수 (WS) -- 검사비 (INSP) - -**기능:** -- 견적 항목 추가/복사/삭제 -- 자동 견적 산출 버튼 -- 샘플 데이터 생성 버튼 - -### 7.4 다음 단계 (API 연동) - -```typescript -// useQuoteList 훅 (목록) -const { - quotes, - pagination, - isLoading, - fetchQuotes, - deleteQuote, - bulkDelete -} = useQuoteList(); - -// useQuote 훅 (단건 CRUD) -const { - quote, - isLoading, - fetchQuote, - createQuote, - updateQuote, - finalizeQuote, - convertToOrder -} = useQuote(); - -// useQuoteCalculation 훅 (자동 산출) -const { - calculationResult, - isCalculating, - calculate, - recalculate -} = useQuoteCalculation(); -``` - ---- - -## 8. 관련 참조 - -### 8.1 거래처 API 연동 -- 발주처 선택 시 `/api/v1/clients` 연동 -- 직접입력 시 자동 등록 가능 - -### 8.2 현장 API (추후) -- 현장 선택 시 `/api/v1/sites` 연동 (별도 API 필요시) - -### 8.3 품목마스터 연동 -- 제품 선택 시 `/api/v1/item-masters` 연동 -- BOM 조회 시 품목마스터 BOM 활용 - ---- - -## 9. 요청 우선순위 - -| 순위 | API | 설명 | -|------|-----|------| -| P1 | 견적 CRUD | 기본 목록/등록/수정/삭제 | -| P1 | 자동 산출 | 수식 계산 엔진 (핵심) | -| P1 | 견적번호 생성 | 자동 채번 | -| P2 | 상태 관리 | 확정/수주전환 | -| P2 | 수정 이력 | 버전 관리 | -| P3 | 문서 출력 | PDF 생성 | - ---- - -## 10. 질문사항 - -1. **현장(Site) 테이블**: 별도 테이블로 관리할지? (거래처 하위 개념) -2. **수식 계산**: BOM 템플릿 테이블 구조는? -3. **문서 출력**: PDF 라이브러리 선정 (TCPDF, Dompdf 등) -4. **알림**: 견적 발송 시 이메일/카카오톡 연동 계획? diff --git a/front/[API-2025-12-06] item-crud-backend-requests.md b/front/[API-2025-12-06] item-crud-backend-requests.md deleted file mode 100644 index 30c3243..0000000 --- a/front/[API-2025-12-06] item-crud-backend-requests.md +++ /dev/null @@ -1,533 +0,0 @@ -# 품목 등록/수정 백엔드 API 수정 요청 - -> 프론트엔드 품목관리 기능 개발 중 발견된 백엔드 수정 필요 사항 정리 - ---- - -## 🔴 [핵심] field_key 통일 필요 - 근본적 구조 개선 - -### 상태: 🔴 구조 개선 필요 - -### 발견일: 2025-12-06 - -### 요약 - -| 항목 | 내용 | -|------|------| -| **근본 원인** | 품목기준관리 / 품목관리 API 요청서 분리로 키값 불일치 | -| **해결 방향** | 품목기준관리 field_key = Single Source of Truth | -| **백엔드 요청** | 동적 필드 → attributes JSON 저장, field_key 통일 | -| **프론트 작업** | 백엔드 완료 후 하드코딩 매핑 제거 | - -### 현재 문제 - -``` -품목기준관리 field_key 프론트엔드 변환 백엔드 API 응답 -───────────────────── ───────────────── ───────────────── -Part_type → part_type → part_type (불일치!) -Installation_type_1 → installation_type → 저장 안됨 -side_dimensions_horizontal → side_spec_width → 저장 안됨 -``` - -**두 개의 요청서가 따로 놀면서** 백엔드에서 각각 다르게 구현 → 키 불일치 발생 - -### 이상적인 구조 - -``` -품목기준관리 (field_key 정의) - │ - ▼ - ┌────────────┐ - │ field_key │ ← Single Source of Truth - └────────────┘ - │ - ┌────┴────┬────────┬────────┐ - ▼ ▼ ▼ ▼ - 등록 수정 상세 리스트 - - (전부 동일한 field_key로 저장/조회/렌더링) -``` - -### 기대 효과 - -1. **단위 필드 혼란 해결**: field_key가 "unit"이면 그게 단위 (명확!) -2. **필드 타입 자동 인식**: 품목기준관리 field_type 보고 자동 렌더링 -3. **누락 데이터 분석 용이**: field_key 하나만 확인하면 끝 -4. **디버깅 속도 향상**: API 응답 = 폼 데이터 (변환 없음) - -### 수정 요청 - -1. **품목기준관리 field_key를 기준**으로 API 요청/응답 키 통일 -2. 동적 필드는 `attributes` JSON에 field_key 그대로 저장 -3. 조회 시에도 field_key 그대로 응답 - -### 영향 범위 - -- 품목관리 전체 (등록/수정/상세/리스트) -- 모든 품목 유형 (FG, PT, SM, RM, CS) - -### 우선순위 - -🔴 **최우선** - 이 구조 개선 후 아래 개별 이슈들 대부분 자동 해결 - ---- - -## 1. 소모품(CS) 등록 시 규격(specification) 저장 안됨 - -### 상태: 🔴 수정 필요 - -### 발견일: 2025-12-06 - -### 파일 위치 -`/app/Http/Requests/Item/ItemStoreRequest.php` - rules() 메서드 (Line 14-42) - -### 현재 문제 -- `specification` 필드가 validation rules에 없음 -- Laravel FormRequest는 rules에 정의되지 않은 필드를 `$request->validated()` 결과에서 제외 -- 프론트엔드에서 `specification: "테스트"` 값을 보내도 백엔드에서 무시됨 -- DB에 규격 값이 null로 저장됨 - -### 프론트엔드 확인 사항 -- `DynamicItemForm`에서 `97_specification` → `spec` → `specification`으로 정상 변환됨 -- API 요청 payload에 `specification` 필드 포함 확인됨 -- 백엔드 `ItemsService.createMaterial()`에서 `$data['specification']` 참조하나 값이 없음 - -### 수정 요청 -```php -// /app/Http/Requests/Item/ItemStoreRequest.php - -public function rules(): array -{ - return [ - // 필수 필드 - 'code' => 'required|string|max:50', - 'name' => 'required|string|max:255', - 'product_type' => 'required|string|in:FG,PT,SM,RM,CS', - 'unit' => 'required|string|max:20', - - // 선택 필드 - 'category_id' => 'nullable|integer|exists:categories,id', - 'description' => 'nullable|string', - 'specification' => 'nullable|string|max:255', // ✅ 추가 필요 - - // ... 나머지 기존 필드들 ... - ]; -} -``` - -### 영향 범위 -- 소모품(CS) 등록 -- 원자재(RM) 등록 (해당 시) -- 부자재(SM) 등록 (해당 시) - ---- - -## 2. Material(SM, RM, CS) 수정 시 material_code 중복 에러 - -### 상태: 🔴 수정 필요 - -### 발견일: 2025-12-06 - -### 현재 문제 -- Material 수정 시 `material_code` 중복 체크 에러 발생 -- 케이스 1: 값을 변경하지 않아도 자기 자신과 중복 체크됨 -- 케이스 2: 소프트 삭제된 품목의 코드와도 중복 체크됨 -- 에러 메시지: `Duplicate entry '알루미늄-옵션2-2' for key 'materials.materials_material_code_unique'` - -### 원인 -1. UPDATE 시 자기 자신의 ID를 제외하지 않음 -2. 소프트 삭제(`deleted_at`)된 레코드도 unique 체크에 포함됨 - -### 수정 요청 -```php -// Material 수정 Request 파일 (MaterialUpdateRequest.php 또는 해당 파일) -use Illuminate\Validation\Rule; - -public function rules(): array -{ - return [ - // ... 기존 필드들 ... - - // ✅ 수정 시 자기 자신 제외 + 소프트삭제 제외하고 unique 체크 - 'material_code' => [ - 'required', - 'string', - 'max:255', - Rule::unique('materials', 'material_code') - ->whereNull('deleted_at') // 소프트삭제된 건 제외 - ->ignore($this->route('id')), // 자기 자신 제외 - ], - - // ... 나머지 필드들 ... - ]; -} -``` - -### 영향 범위 -- 원자재(RM) 수정 -- 부자재(SM) 수정 -- 소모품(CS) 수정 - -### 비고 -- 현재 수정 자체가 불가능하여 수정 후 데이터 검증이 어려움 -- 이 이슈 해결 후 수정 기능 재검증 필요 - ---- - -## 3. Material(SM, RM) 수정 시 규격(specification) 로딩 안됨 - -### 상태: 🔴 확인 필요 - -### 발견일: 2025-12-06 - -### 현재 문제 -- SM(부자재), RM(원자재) 수정 페이지 진입 시 규격 값이 표시 안됨 -- 1회 수정 후 다시 수정 페이지 진입 시 규격 값 없음 -- CS(소모품)과 동일한 문제로 추정 - -### 원인 추정 -- 백엔드에서 `options` 배열이 제대로 저장되지 않거나 반환되지 않음 -- SM/RM은 `standard_*` 필드 조합으로 `specification`을 생성하고, 이 값을 `options` 배열에도 저장 -- 수정 페이지에서는 `options` 배열을 읽어서 폼에 표시 -- `options`가 null이면 규격 필드들이 빈 값으로 표시됨 - -### 확인 요청 -```php -// Material 조회 API 응답에서 options 필드 확인 필요 -// GET /api/v1/items/{id}?item_type=MATERIAL - -// 응답 예시 (정상) -{ - "id": 396, - "name": "썬더볼트", - "specification": "부자재2-2", - "options": [ - {"label": "standard_1", "value": "부자재2"}, - {"label": "standard_2", "value": "2"} - ] // ✅ options 배열이 있어야 함 -} - -// 응답 예시 (문제) -{ - "id": 396, - "name": "썬더볼트", - "specification": "부자재2-2", - "options": null // ❌ options가 null이면 규격 로딩 불가 -} -``` - -### 수정 요청 -1. Material 저장 시 `options` 배열 정상 저장 확인 -2. Material 조회 시 `options` 필드 반환 확인 -3. `options`가 JSON 컬럼이라면 정상적인 JSON 형식으로 저장되는지 확인 - -### 영향 범위 -- 원자재(RM) 수정 -- 부자재(SM) 수정 - ---- - -## 4. Material(SM, RM, CS) 비고(remarks) 저장 안됨 - -### 상태: 🔴 수정 필요 - -### 발견일: 2025-12-06 - -### 현재 문제 -- 소모품(CS), 원자재(RM), 부자재(SM) 등록/수정 시 비고(remarks)가 저장되지 않음 -- 프론트에서 `note` → `remarks`로 정상 변환하여 전송 -- 백엔드 Service에서 `$data['remarks']` 참조하지만 값이 없음 - -### 원인 분석 -- **프론트엔드**: `note` → `remarks` 변환 ✅ (`materialTransform.ts:115`) -- **백엔드 Service**: `'remarks' => $data['remarks'] ?? null` ✅ (`ItemsService.php:301`) -- **백엔드 Model**: `remarks` 컬럼 존재 ✅ (`Material.php:31`) -- **백엔드 Request**: `remarks` validation rule 없음 ❌ **누락** - -### 수정 요청 -```php -// /app/Http/Requests/Item/ItemStoreRequest.php -// /app/Http/Requests/Item/ItemUpdateRequest.php - -public function rules(): array -{ - return [ - // 기존 필드들... - 'description' => 'nullable|string', - - // ✅ 추가 필요 - 'remarks' => 'nullable|string', // 비고 필드 - - // ... 나머지 필드들 ... - ]; -} -``` - -### 영향 범위 -- 소모품(CS) 등록/수정 -- 원자재(RM) 등록/수정 -- 부자재(SM) 등록/수정 - -### 비고 -- 1번 이슈(specification)와 동일한 원인: Request validation 누락 -- 함께 처리하면 효율적 - ---- - -## 5. 품목기준관리 옵션 필드 식별자 필요 (장기 개선) - -### 상태: 🟡 개선 권장 - -### 발견일: 2025-12-06 - -### 현재 문제 -- 품목기준관리에서 옵션 필드의 `field_key`를 자유롭게 입력 가능 -- 프론트엔드는 `standard_*`, `option_*` 패턴으로 옵션 필드를 식별 -- 패턴에 맞지 않는 field_key (예: `st_2`)는 규격(specification) 조합에서 누락됨 -- 결과: 부자재(SM)의 규격값이 저장되지 않음 - -### 임시 해결 (프론트엔드) -- 품목기준관리에서 field_key를 `standard_*` 패턴으로 통일 -- 예: `st_2` → `standard_2` - -### 근본 해결 요청 (백엔드) -```php -// 품목기준관리 API 응답에 옵션 필드 여부 표시 - -// 현재 응답 -{ - "field_key": "st_2", - "field_type": "select", - "field_name": "규격옵션" -} - -// 개선 요청 -{ - "field_key": "st_2", - "field_type": "select", - "field_name": "규격옵션", - "is_spec_option": true // ✅ 규격 조합용 옵션 필드인지 표시 -} -``` - -### 기대 효과 -- 프론트엔드가 field_key 패턴에 의존하지 않음 -- `is_spec_option: true`인 필드만 규격 조합에 사용 -- 새로운 field_key 패턴이 추가되어도 프론트 수정 불필요 - -### 영향 범위 -- 원자재(RM) 등록/수정 -- 부자재(SM) 등록/수정 -- 향후 추가되는 Material 유형 - ---- - -## 6. 조립부품(PT-ASSEMBLY) 필드 저장 안됨 - fillable 누락 - -### 상태: 🔴 수정 필요 - -### 발견일: 2025-12-06 - -### 현재 문제 -- 조립부품 등록 후 상세보기/수정 페이지에서 데이터가 제대로 표시되지 않음 -- **프론트엔드는 데이터를 정상 전송하고 있음** ✅ -- **백엔드에서 조립부품 필드들이 저장되지 않음** ❌ - -### 원인 분석 - -#### 프론트엔드 전송 데이터 (정상) -```javascript -// DynamicItemForm/index.tsx - handleFormSubmit() -const submitData = { - ...convertedData, // 폼 데이터 (installation_type, assembly_type 등 포함) - product_type: 'PT', - part_type: 'ASSEMBLY', // ✅ 명시적 추가 - bending_diagram: base64, // ✅ 캔버스 이미지 - bom: [...], // ✅ BOM 데이터 -}; -``` - -#### 백엔드 저장 로직 (문제) -```php -// ItemsService.php - createProduct() -private function createProduct(array $data, int $tenantId, int $userId): Product -{ - $payload = $data; - // ... 기본 필드만 설정 - return Product::create($payload); // Mass Assignment -} -``` - -#### Product 모델 fillable (누락 필드 있음) -```php -// Product.php -protected $fillable = [ - 'tenant_id', 'code', 'name', 'unit', 'category_id', - 'product_type', - 'attributes', 'description', // ✅ attributes JSON 있음 - 'part_type', // ✅ 있음 - 'bending_diagram', 'bending_details', // ✅ 있음 - // ❌ installation_type 없음 - // ❌ assembly_type 없음 - // ❌ side_spec_width 없음 - // ❌ side_spec_height 없음 - // ❌ length 없음 -]; -``` - -### 필드별 저장 상태 - -| 필드 | 프론트 전송 | fillable | 저장 여부 | -|------|------------|----------|----------| -| `part_type` | ✅ | ✅ 컬럼 | ✅ 저장됨 | -| `bending_diagram` | ✅ | ✅ 컬럼 | ⚠️ 파일 업로드 별도 처리 | -| `installation_type` | ✅ | ❌ 없음 | ❌ **저장 안됨** | -| `assembly_type` | ✅ | ❌ 없음 | ❌ **저장 안됨** | -| `side_spec_width` | ✅ | ❌ 없음 | ❌ **저장 안됨** | -| `side_spec_height` | ✅ | ❌ 없음 | ❌ **저장 안됨** | -| `length` | ✅ | ❌ 없음 | ❌ **저장 안됨** | -| `bom` | ✅ | ⚠️ 별도 처리 | ❓ 확인 필요 | - -### 수정 요청 - -#### 방법 1: attributes JSON에 저장 (권장) -```php -// ItemsService.php - createProduct() 수정 - -private function createProduct(array $data, int $tenantId, int $userId): Product -{ - // 조립부품 전용 필드들을 attributes JSON으로 묶기 - $assemblyFields = ['installation_type', 'assembly_type', 'side_spec_width', 'side_spec_height', 'length']; - $attributes = $data['attributes'] ?? []; - - foreach ($assemblyFields as $field) { - if (isset($data[$field])) { - $attributes[$field] = $data[$field]; - unset($data[$field]); // payload에서 제거 - } - } - - $payload = $data; - $payload['tenant_id'] = $tenantId; - $payload['created_by'] = $userId; - $payload['attributes'] = !empty($attributes) ? $attributes : null; - // ... 나머지 동일 - - return Product::create($payload); -} -``` - -#### 방법 2: 컬럼 추가 + fillable 등록 -```php -// Product.php - fillable에 추가 -protected $fillable = [ - // 기존 필드들... - 'installation_type', // ✅ 추가 - 'assembly_type', // ✅ 추가 - 'side_spec_width', // ✅ 추가 - 'side_spec_height', // ✅ 추가 - 'length', // ✅ 추가 (또는 assembly_length) -]; - -// + migration으로 컬럼 추가 필요 -``` - -### 프론트엔드 대응 (완료) -- 프론트에서 `data.xxx` 또는 `data.attributes.xxx` 둘 다에서 값을 찾도록 수정 완료 -- 상세보기: `items/[id]/page.tsx` - `mapApiResponseToItemMaster` 함수 -- 수정페이지: `items/[id]/edit/page.tsx` - `mapApiResponseToFormData` 함수 - -### 영향 범위 -- 조립부품(PT-ASSEMBLY) 등록/조회/수정 -- 설치유형, 마감, 측면규격, 길이 모든 필드 - -### 우선순위 -🔴 **높음** - 조립부품 등록 기능이 완전히 동작하지 않음 - ---- - -## 7. 파일 업로드 API 500 에러 - ApiResponse 클래스 네임스페이스 불일치 - -### 상태: 🔴 수정 필요 - -### 발견일: 2025-12-06 - -### 현재 문제 -- 품목 파일 업로드 (전개도, 시방서, 인정서) 시 500 에러 발생 -- 절곡부품, 조립부품 모두 동일한 에러 -- 파일이 업로드되지 않음 - -### 에러 로그 -``` -[2025-12-06 17:28:22] DEV.ERROR: Class "App\Http\Responses\ApiResponse" not found -{"exception":"[object] (Error(code: 0): Class \"App\\Http\\Responses\\ApiResponse\" not found -at /home/webservice/api/app/Http/Controllers/Api/V1/ItemsFileController.php:31) -``` - -### 원인 분석 -**네임스페이스 불일치** - -| 위치 | 네임스페이스 | -|------|-------------| -| `ItemsFileController.php` (Line 7) | `use App\Http\Responses\ApiResponse` ❌ | -| 실제 파일 위치 | `App\Helpers\ApiResponse` ✅ | - -### 파일 위치 -`/app/Http/Controllers/Api/V1/ItemsFileController.php` (Line 7) - -### 현재 코드 -```php - 수정 완료된 항목은 아래로 이동 - -(아직 없음) - ---- - -## 참고 사항 - -### 관련 파일 (프론트엔드) -- `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 deleted file mode 100644 index a962ef1..0000000 --- a/front/[API-2025-12-08] pricing-api-enhancement-request.md +++ /dev/null @@ -1,379 +0,0 @@ -# 단가관리 API 분석 및 구현 현황 - -> **작성일**: 2025-12-08 -> **최종 업데이트**: 2025-12-08 -> **상태**: ✅ **백엔드 API 구현 완료** - ---- - -## 1. 구현 현황 요약 - -### ✅ 현재 API 구조 (구현 완료) -| Method | Endpoint | 설명 | 상태 | -|--------|----------|------|------| -| `GET` | `/api/v1/pricing` | 목록 조회 (페이지네이션, 필터) | ✅ 완료 | -| `GET` | `/api/v1/pricing/{id}` | 단건 조회 | ✅ 완료 | -| `POST` | `/api/v1/pricing` | 등록 | ✅ 완료 | -| `PUT` | `/api/v1/pricing/{id}` | 수정 | ✅ 완료 | -| `DELETE` | `/api/v1/pricing/{id}` | 삭제 (soft delete) | ✅ 완료 | -| `POST` | `/api/v1/pricing/{id}/finalize` | 확정 | ✅ 완료 | -| `GET` | `/api/v1/pricing/{id}/revisions` | 변경이력 조회 | ✅ 완료 | -| `POST` | `/api/v1/pricing/by-items` | 품목별 단가 현황 (다건) | ✅ 완료 | -| `GET` | `/api/v1/pricing/cost` | 원가 조회 (수입검사 > 표준원가) | ✅ 완료 | - -### ✅ 완료된 사항 -- **새 테이블 구조**: `prices`, `price_revisions` 테이블 생성 -- **기존 테이블 마이그레이션**: `price_histories` → `prices` 데이터 이관 -- **모든 요청 필드**: 원가 계산, 마진 관리, 리비전 관리 기능 구현 - ---- - -## 2. 테이블 스키마 (구현 완료) - -### 2.1 `prices` 테이블 (신규 생성) ✅ - -```sql -CREATE TABLE prices ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT NOT NULL, - - -- 품목 연결 - item_type_code VARCHAR(20) NOT NULL, -- 'PRODUCT' | 'MATERIAL' - item_id BIGINT NOT NULL, - client_group_id BIGINT NULL, -- NULL=기본가 - - -- 원가 정보 - purchase_price DECIMAL(15,4) NULL, -- 매입단가 - processing_cost DECIMAL(15,4) NULL, -- 가공비 - loss_rate DECIMAL(5,2) NULL, -- LOSS율 (%) - - -- 판매가 정보 - margin_rate DECIMAL(5,2) NULL, -- 마진율 (%) - sales_price DECIMAL(15,4) NULL, -- 판매단가 - rounding_rule ENUM('round','ceil','floor') DEFAULT 'round', - rounding_unit INT DEFAULT 1, -- 1, 10, 100, 1000 - - -- 메타 정보 - supplier VARCHAR(255) NULL, - effective_from DATE NOT NULL, - effective_to DATE NULL, - note TEXT NULL, - - -- 상태 관리 - status ENUM('draft','active','inactive','finalized') DEFAULT 'draft', - is_final BOOLEAN DEFAULT FALSE, - finalized_at DATETIME NULL, - finalized_by BIGINT NULL, - - -- 감사 컬럼 - created_by, updated_by, deleted_by, timestamps, soft_deletes -); -``` - -### 2.2 `price_revisions` 테이블 (변경 이력) ✅ - -```sql -CREATE TABLE price_revisions ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, - price_id BIGINT NOT NULL, - revision_number INT NOT NULL, - changed_at DATETIME NOT NULL, - changed_by BIGINT NOT NULL, - change_reason VARCHAR(500) NULL, - before_snapshot JSON NULL, - after_snapshot JSON NOT NULL -); -``` - -### 2.3 기존 `price_histories` 테이블 처리 ✅ - -- ✅ `prices` 테이블로 데이터 마이그레이션 완료 -- ✅ `price_histories` 테이블 삭제됨 - ---- - -## 3. API 엔드포인트 상세 (구현 완료) - -### 3.1 단가 등록 `POST /api/v1/pricing` ✅ - -**Request Body:** -```json -{ - "item_type_code": "MATERIAL", - "item_id": 123, - "client_group_id": null, - - "purchase_price": 1000, - "processing_cost": 100, - "loss_rate": 5, - "margin_rate": 20, - "rounding_rule": "round", - "rounding_unit": 10, - - "supplier": "ABC상사", - "effective_from": "2025-01-01", - "effective_to": null, - "note": "2025년 1분기 단가", - "status": "active" -} -``` - -**자동 처리:** -- `sales_price` 자동 계산 (입력 안해도 됨) -- 기존 무기한 단가의 `effective_to` 자동 설정 -- 최초 리비전 자동 생성 - -### 3.2 목록 조회 `GET /api/v1/pricing` ✅ - -**Query Parameters:** -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `page` | int | 페이지 번호 | -| `size` | int | 페이지당 개수 (max 100) | -| `q` | string | 검색어 (공급업체, 비고) | -| `item_type_code` | string | `PRODUCT` / `MATERIAL` | -| `item_id` | int | 품목 ID | -| `client_group_id` | int/null | 고객그룹 ID | -| `status` | string | `draft`, `active`, `inactive`, `finalized` | -| `valid_at` | date | 특정 일자에 유효한 단가 | - -**Response:** -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "current_page": 1, - "data": [ - { - "id": 1, - "item_type_code": "MATERIAL", - "item_id": 123, - "client_group_id": null, - "purchase_price": "1000.0000", - "processing_cost": "100.0000", - "loss_rate": "5.00", - "margin_rate": "20.00", - "sales_price": "1386.0000", - "rounding_rule": "round", - "rounding_unit": 10, - "supplier": "ABC상사", - "effective_from": "2025-01-01", - "effective_to": null, - "status": "active", - "is_final": false, - "client_group": null - } - ], - "total": 1 - } -} -``` - ---- - -## 4. 추가 API 엔드포인트 (구현 완료) - -### 4.1 품목별 단가 현황 `POST /api/v1/pricing/by-items` ✅ - -**용도**: 여러 품목의 현재 유효한 단가를 한번에 조회 - -**Request Body:** -```json -{ - "items": [ - { "item_type_code": "MATERIAL", "item_id": 123 }, - { "item_type_code": "MATERIAL", "item_id": 124 }, - { "item_type_code": "PRODUCT", "item_id": 10 } - ], - "client_group_id": 1, - "date": "2025-12-08" -} -``` - -**조회 우선순위:** -1. 고객그룹별 단가 (`client_group_id` 지정 시) -2. 기본 단가 (`client_group_id = NULL`) - -**Response:** -```json -{ - "success": true, - "data": [ - { - "item_type_code": "MATERIAL", - "item_id": 123, - "price": { ... }, - "has_price": true - }, - { - "item_type_code": "MATERIAL", - "item_id": 124, - "price": null, - "has_price": false - } - ] -} -``` - -### 4.2 변경 이력 조회 `GET /api/v1/pricing/{id}/revisions` ✅ - -**Response:** -```json -{ - "success": true, - "data": { - "data": [ - { - "id": 2, - "revision_number": 2, - "changed_at": "2025-12-08T11:00:00.000000Z", - "changed_by": 1, - "change_reason": "마진율 조정", - "before_snapshot": { - "purchase_price": "1000.0000", - "margin_rate": "15.00", - "sales_price": "1265.0000" - }, - "after_snapshot": { - "purchase_price": "1000.0000", - "margin_rate": "20.00", - "sales_price": "1386.0000" - }, - "changed_by_user": { "id": 1, "name": "홍길동" } - } - ] - } -} -``` - -### 4.3 단가 확정 `POST /api/v1/pricing/{id}/finalize` ✅ - -**동작:** -- `is_final` → `true` -- `status` → `finalized` -- 확정 후 **수정/삭제 불가** - -**Response:** -```json -{ - "success": true, - "message": "message.pricing.finalized", - "data": { - "id": 5, - "is_final": true, - "finalized_at": "2025-12-08T14:30:00.000000Z", - "finalized_by": 1, - "status": "finalized" - } -} -``` - -### 4.4 원가 조회 `GET /api/v1/pricing/cost` ✅ - -**용도**: 수입검사 입고단가 > 표준원가 우선순위로 원가 조회 - -**Query Parameters:** -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| `item_type_code` | string | ✅ | `PRODUCT` / `MATERIAL` | -| `item_id` | int | ✅ | 품목 ID | -| `date` | date | ❌ | 조회 기준일 (기본: 오늘) | - -**Response:** -```json -{ - "success": true, - "data": { - "item_type_code": "MATERIAL", - "item_id": 123, - "date": "2025-12-08", - "cost_source": "receipt", - "purchase_price": 1050.00, - "receipt_id": 45, - "receipt_date": "2025-12-01", - "price_id": null - } -} -``` - -| cost_source | 설명 | -|-------------|------| -| `receipt` | 수입검사 입고단가 | -| `standard` | 표준원가 (prices 테이블) | -| `not_found` | 단가 미등록 | - ---- - -## 5. 비즈니스 로직 - -### 5.1 판매단가 자동 계산 -``` -총원가 = (매입단가 + 가공비) × (1 + LOSS율/100) -판매단가 = 반올림(총원가 × (1 + 마진율/100), 반올림단위, 반올림규칙) -``` - -### 5.2 상태 흐름 -``` -draft → active → finalized - ↓ - inactive -``` - -### 5.3 주요 검증 규칙 -- 동일 품목+고객그룹+시작일 조합 중복 불가 -- 확정된 단가는 수정/삭제 불가 -- 마진율/LOSS율은 0~100% 범위 -- 반올림단위는 1, 10, 100, 1000 중 하나 - ---- - -## 6. 프론트엔드 작업 현황 - -| 작업 | 상태 | 비고 | -|------|------|------| -| `usePricingList` 훅 생성 | ⬜ 미완료 | | -| 타입 정의 | ⬜ 미완료 | | -| 단가 목록 페이지 | ⬜ 미완료 | | -| 단가 등록/수정 페이지 | ⬜ 미완료 | | -| 단가 상세 페이지 (이력 포함) | ⬜ 미완료 | | -| 품목별 단가 현황 컴포넌트 | ⬜ 미완료 | | - ---- - -## 7. 백엔드 참고 파일 - -### 컨트롤러/서비스 -- `api/app/Http/Controllers/Api/V1/PricingController.php` -- `api/app/Services/PricingService.php` - -### 모델 -- `api/app/Models/Products/Price.php` -- `api/app/Models/Products/PriceRevision.php` - -### 요청 클래스 -- `api/app/Http/Requests/Pricing/PriceIndexRequest.php` -- `api/app/Http/Requests/Pricing/PriceStoreRequest.php` -- `api/app/Http/Requests/Pricing/PriceUpdateRequest.php` -- `api/app/Http/Requests/Pricing/PriceByItemsRequest.php` -- `api/app/Http/Requests/Pricing/PriceCostRequest.php` - -### 마이그레이션 -- `api/database/migrations/2025_12_08_154633_create_prices_table.php` -- `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php` -- `api/database/migrations/2025_12_08_154635_migrate_price_histories_to_prices.php` -- `api/database/migrations/2025_12_08_154636_drop_price_histories_table.php` - -### 라우트 -- `api/routes/api.php` (Line 368-379) - ---- - -## 8. 관련 문서 - -- [단가 정책](../rules/pricing-policy.md) - 상세 정책 및 계산 공식 - ---- - -**최종 업데이트**: 2025-12-08 \ No newline at end of file diff --git a/front/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md b/front/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md deleted file mode 100644 index 768ba64..0000000 --- a/front/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md +++ /dev/null @@ -1,1112 +0,0 @@ -# 품목관리 페이지 전체 API 요청서 - -## 작업 일자: 2025-11-28 - -## 문서 버전 -| 버전 | 날짜 | 작성자 | 내용 | -|------|------|--------|------| -| 1.0 | 2025-11-28 | Claude | 초안 작성 (동적 렌더링만) | -| 2.0 | 2025-11-28 | Claude | 전체 CRUD + 리스트 + 페이징 추가 | -| 3.0 | 2025-11-28 | Claude | **백엔드 스키마/기존 API 기반 재검토** | - ---- - -## 🔴 백엔드 검토 결과 요약 - -### 기존 API와의 정합성 분석 - -| 항목 | 기존 백엔드 | 요청서 v2.0 | 조정 필요 | -|------|------------|-------------|----------| -| 품목 수정/삭제 | `/{code}` (코드 기반) | `/{id}` (ID 기반) | ✅ **코드 기반으로 통일** | -| 품목 조회 | `item_type` 파라미터 필수 | 자동 감지 | ✅ 파라미터 추가 | -| 페이징 파라미터 | `size` | `per_page` | ✅ `size`로 변경 | -| item-master init | 이미 구현됨 | 별도 요청 | ✅ 기존 활용 | -| BOM API | `/items/{code}/bom` | `/items/{id}/bom` | ✅ **코드 기반으로 통일** | -| 파일 API | `/items/{code}/files` | `/files/upload` | ✅ 기존 구조 활용 | -| 테넌트 격리 | `tenant_id` 자동 | 미언급 | ✅ 서버 자동 처리 | - -### 주요 백엔드 아키텍처 특성 (반영 필요) - -```yaml -multi_tenancy: - - 모든 테이블에 tenant_id 필수 - - BelongsToTenant 글로벌 스코프 적용 - - API 레벨에서 자동 격리 (클라이언트 처리 불필요) - -soft_delete: - - deleted_at, deleted_by 컬럼 사용 - - 삭제 시 실제 삭제가 아닌 soft delete - -audit_columns: - - created_by, updated_by, deleted_by - - 사용자 추적 자동 처리 - -api_response_format: - success: true/false - data: {} | [] - message: string (i18n key) -``` - ---- - -## 작업 체크리스트 - -### Phase 1: 분석 단계 -- [x] 품목기준관리 API 저장/수정/삭제 검토 -- [x] 현재 품목관리 페이지 구조 분석 -- [x] 품목기준관리 저장 데이터 구조 확인 -- [x] 품목관리 리스트/상세/등록/수정 페이지 분석 - -### Phase 2: 설계 단계 -- [x] 품목 리스트 API (페이징, 필터, 검색) -- [x] 품목 CRUD API (생성, 조회, 수정, 삭제) -- [x] 동적 페이지 렌더링 API -- [x] BOM 관리 API -- [x] 파일 업로드 API -- [x] 통계/대시보드 API - -### Phase 3: 검토 단계 ✅ 완료 -- [x] 백엔드 DB 스키마 검토 (`database-schema.md`) -- [x] 기존 item-master-spec 검토 (`item-master-spec.md`) -- [x] sam-api 기존 API 구조 확인 (`api.php`, Controllers) -- [x] API 스펙 정합성 조정 - -### Phase 4: 확정 단계 -- [ ] 백엔드 팀 최종 리뷰 -- [ ] API 스펙 확정 -- [ ] 프론트엔드 연동 테스트 - ---- - -## 1. 개요 - -### 1.1 목적 -품목관리 페이지(`/items/*`)에서 필요한 **모든 API**를 정의합니다: -- 품목 목록 조회 (페이징, 필터, 검색) -- 품목 CRUD (생성, 조회, 수정, 삭제) -- 동적 폼 렌더링 (품목기준관리 연동) -- BOM 관리 -- 파일 업로드/다운로드 -- 통계 정보 - -### 1.2 관련 페이지 -| 경로 | 페이지 | 필요 API | -|------|--------|----------| -| `/items` | 품목 목록 | 리스트, 검색, 필터, 페이징, 통계, 삭제 | -| `/items/create` | 품목 등록 | 동적 폼 구조, 생성, BOM 검색, 파일 업로드 | -| `/items/[id]` | 품목 상세 | 단건 조회, BOM 조회 | -| `/items/[id]/edit` | 품목 수정 | 단건 조회, 수정, BOM 관리, 파일 업로드 | - ---- - -## 2. 품목 목록 API (List) - -### API 2.1: 품목 목록 조회 (페이징) - -> ⚠️ **기존 API 존재**: `ItemsController@index` - 파라미터명 조정 필요 - -``` -GET /api/v1/items -``` - -**Query Parameters** (기존 백엔드 기준): -| 파라미터 | 타입 | 필수 | 기본값 | 설명 | -|----------|------|------|--------|------| -| page | number | N | 1 | 페이지 번호 | -| size | number | N | 20 | 페이지당 항목 수 (기존: `size`, 변경 금지) | -| type | string | N | - | 품목유형 필터 (FG, PT, SM, RM, CS) | -| search | string | N | - | 검색어 (품목코드, 품목명) | -| q | string | N | - | 검색어 (search와 동일, 호환용) | -| category_id | number | N | - | 카테고리 ID 필터 | -| is_active | boolean | N | - | 활성 상태 필터 **(신규 요청)** | -| sort_by | string | N | created_at | 정렬 기준 **(신규 요청)** | -| sort_order | string | N | desc | 정렬 순서 **(신규 요청)** | - -**Request Example**: -``` -GET /api/v1/items?page=1&size=20&type=FG&search=스크린 -``` - -**Response**: -```json -{ - "success": true, - "data": { - "items": [ - { - "id": 1, - "item_code": "KD-FG-001", - "item_name": "스크린 제품 A", - "item_type": "FG", - "unit": "EA", - "specification": "2000x2000", - "is_active": true, - "category1": "본체부품", - "category2": "가이드시스템", - "sales_price": 150000, - "purchase_price": 100000, - "product_category": "SCREEN", - "current_revision": 0, - "is_final": false, - "created_at": "2025-01-10T00:00:00Z", - "updated_at": "2025-01-10T00:00:00Z" - } - ], - "pagination": { - "current_page": 1, - "per_page": 20, - "total_items": 150, - "total_pages": 8, - "has_next": true, - "has_prev": false - } - } -} -``` - ---- - -### API 2.2: 품목 통계 조회 - -> 🆕 **신규 API 요청** - -``` -GET /api/v1/items/stats -``` - -**Response**: -```json -{ - "success": true, - "data": { - "total_count": 500, - "by_item_type": { - "FG": 50, - "PT": 200, - "SM": 100, - "RM": 100, - "CS": 50 - }, - "active_count": 450, - "inactive_count": 50 - }, - "message": "message.fetched" -} -``` - ---- - -### API 2.3: 품목 검색 (BOM용 자동완성) -``` -GET /api/v1/items/search -``` - -**Query Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| q | string | Y | 검색어 (최소 2자) | -| item_type | string | N | 품목유형 필터 (복수 가능: PT,SM) | -| limit | number | N | 결과 개수 제한 (기본 10, 최대 50) | - -**Request Example**: -``` -GET /api/v1/items/search?q=가이드&item_type=PT&limit=10 -``` - -**Response**: -```json -{ - "success": true, - "data": [ - { - "id": 2, - "item_code": "KD-PT-001", - "item_name": "가이드레일(벽면형)", - "item_type": "PT", - "unit": "EA", - "specification": "2438mm", - "sales_price": 50000 - } - ] -} -``` - ---- - -## 3. 품목 CRUD API - -### API 3.1: 품목 단건 조회 (ID 기반) - -> ⚠️ **기존 API 존재**: `ItemsController@show` - `item_type` 파라미터 필수 - -``` -GET /api/v1/items/{id} -``` - -**Path Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| id | number | Y | 품목 ID | - -**Query Parameters** (기존 백엔드 기준): -| 파라미터 | 타입 | 필수 | 기본값 | 설명 | -|----------|------|------|--------|------| -| item_type | string | Y | PRODUCT | 품목 유형 (PRODUCT, MATERIAL) | -| include_price | boolean | N | false | 가격 정보 포함 여부 | -| client_id | number | N | - | 고객별 가격 조회 시 | -| price_date | string | N | - | 가격 기준일 (YYYY-MM-DD) | - -### API 3.1.1: 품목 단건 조회 (Code 기반) - -> ✅ **기존 API 존재**: `ItemsController@showByCode` - -``` -GET /api/v1/items/code/{code} -``` - -**Path Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| code | string | Y | 품목 코드 (예: KD-FG-001) | - -**Query Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| include_bom | boolean | N | BOM 정보 포함 여부 | - -**Response**: -```json -{ - "success": true, - "data": { - "id": 1, - "item_code": "KD-FG-001", - "item_name": "스크린 제품 A", - "item_type": "FG", - "unit": "EA", - "specification": "2000x2000", - "is_active": true, - "product_category": "SCREEN", - "lot_abbreviation": "KD", - - "category1": "본체부품", - "category2": "가이드시스템", - "category3": null, - - "purchase_price": 100000, - "sales_price": 150000, - "margin_rate": 50, - "processing_cost": 10000, - "labor_cost": 5000, - "install_cost": 3000, - - "certification_number": "인정번호-001", - "certification_start_date": "2025-01-01", - "certification_end_date": "2028-01-01", - "specification_file": "/files/spec-001.pdf", - "specification_file_name": "시방서.pdf", - "certification_file": "/files/cert-001.pdf", - "certification_file_name": "인정서.pdf", - "note": "비고 내용", - - "current_revision": 0, - "is_final": false, - "finalized_date": null, - "finalized_by": null, - - "bom": [ - { - "id": 1, - "child_item_code": "KD-PT-001", - "child_item_name": "가이드레일(벽면형)", - "quantity": 2, - "unit": "EA", - "unit_price": 50000, - "quantity_formula": "H / 1000", - "note": "높이에 따라 수량 변동" - } - ], - - "revisions": [], - "created_at": "2025-01-10T00:00:00Z", - "updated_at": "2025-01-10T00:00:00Z", - "created_by": 1, - "updated_by": 1 - } -} -``` - ---- - -### API 3.2: 품목 생성 -``` -POST /api/v1/items -``` - -**Request Body**: -```json -{ - "item_type": "FG", - "item_name": "스크린 제품 신규", - "unit": "EA", - "specification": "2500x2500", - "is_active": true, - "product_category": "SCREEN", - "lot_abbreviation": "KD", - - "category1": "본체부품", - "category2": "가이드시스템", - - "purchase_price": 120000, - "sales_price": 180000, - - "certification_number": "인정번호-002", - "certification_start_date": "2025-01-01", - "certification_end_date": "2028-01-01", - "note": "신규 제품", - - "bom": [ - { - "child_item_id": 2, - "quantity": 2, - "unit": "EA", - "quantity_formula": "H / 1000", - "note": "높이에 따라 수량 변동" - } - ] -} -``` - -**Response**: -```json -{ - "success": true, - "data": { - "id": 10, - "item_code": "KD-FG-010", - "item_name": "스크린 제품 신규", - "message": "품목이 성공적으로 생성되었습니다." - } -} -``` - ---- - -### API 3.3: 품목 수정 - -> ⚠️ **기존 API 존재**: `ItemsController@update` - **코드 기반 경로** - -``` -PUT /api/v1/items/{code} -``` - -**Path Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| code | string | Y | 품목 코드 (예: KD-FG-001) | - -**Request Body**: (생성과 동일, 변경할 필드만 전송) -```json -{ - "item_name": "스크린 제품 A (수정)", - "sales_price": 160000, - "note": "가격 조정됨", - "bom": [ - { - "id": 1, - "quantity": 3 - }, - { - "child_item_id": 5, - "quantity": 10, - "unit": "EA" - } - ] -} -``` - -**Response**: -```json -{ - "success": true, - "data": { - "id": 1, - "item_code": "KD-FG-001", - "message": "품목이 성공적으로 수정되었습니다." - } -} -``` - ---- - -### API 3.4: 품목 삭제 - -> ⚠️ **기존 API 존재**: `ItemsController@destroy` - **코드 기반 경로** - -``` -DELETE /api/v1/items/{code} -``` - -**Path Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| code | string | Y | 품목 코드 (예: KD-FG-001) | - -**Response**: -```json -{ - "success": true, - "data": { - "message": "품목이 성공적으로 삭제되었습니다." - } -} -``` - -**Error Response** (사용 중인 품목): -```json -{ - "success": false, - "error": { - "code": "ITEM_IN_USE", - "message": "해당 품목은 다른 BOM에서 사용 중이므로 삭제할 수 없습니다.", - "details": { - "used_in": [ - {"item_code": "KD-FG-001", "item_name": "스크린 제품 A"} - ] - } - } -} -``` - ---- - -### API 3.5: 품목 일괄 삭제 - -> 🆕 **신규 API 요청** - 코드 기반 일괄 삭제 - -``` -DELETE /api/v1/items/batch -``` - -**Request Body**: -```json -{ - "codes": ["KD-FG-001", "KD-PT-002", "KD-SM-003"] -} -``` - -**Response**: -```json -{ - "success": true, - "data": { - "deleted_count": 2, - "failed_count": 1, - "failed_items": [ - { - "id": 3, - "item_code": "KD-PT-003", - "reason": "BOM에서 사용 중" - } - ], - "message": "2개 품목이 삭제되었습니다. 1개 품목은 삭제할 수 없습니다." - } -} -``` - ---- - -## 4. 동적 폼 렌더링 API - -### API 4.1: 품목 유형별 폼 구조 조회 -``` -GET /api/v1/item-master/form-structure/{item_type} -``` - -**Path Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| item_type | string | Y | 품목 유형 (FG, PT, SM, RM, CS) | - -**Query Parameters**: -| 파라미터 | 타입 | 필수 | 기본값 | 설명 | -|----------|------|------|--------|------| -| include_conditions | boolean | N | true | 조건부 필드 포함 여부 | - -**Response**: -```json -{ - "success": true, - "data": { - "page": { - "id": 1, - "page_name": "제품 등록", - "item_type": "FG", - "description": "제품(완제품) 등록 페이지" - }, - "sections": [ - { - "id": 101, - "title": "기본 정보", - "section_type": "BASIC", - "order_no": 1, - "is_collapsible": false, - "is_default_open": true, - "fields": [ - { - "id": 1001, - "field_name": "품목명", - "field_key": "item_name", - "field_type": "textbox", - "order_no": 1, - "is_required": true, - "placeholder": "품목명을 입력하세요", - "validation_rules": { - "maxLength": 100 - }, - "grid_row": 1, - "grid_col": 1, - "grid_span": 2 - }, - { - "id": 1002, - "field_name": "품목 상태", - "field_key": "status", - "field_type": "dropdown", - "order_no": 2, - "is_required": true, - "default_value": "DEV", - "options": [ - {"label": "개발", "value": "DEV"}, - {"label": "양산", "value": "PROD"}, - {"label": "단종", "value": "EOL"} - ], - "grid_row": 2, - "grid_col": 1, - "grid_span": 1 - } - ] - }, - { - "id": 102, - "title": "인정 정보", - "section_type": "BASIC", - "order_no": 2, - "is_collapsible": true, - "is_default_open": false, - "fields": [ - { - "id": 1010, - "field_name": "인정번호", - "field_key": "certification_number", - "field_type": "textbox", - "order_no": 1, - "is_required": false - }, - { - "id": 1011, - "field_name": "시방서", - "field_key": "specification_file", - "field_type": "file", - "order_no": 2, - "is_required": false, - "component_type": "file-upload", - "properties": { - "accept": ".pdf,.doc,.docx", - "max_size_mb": 10 - } - } - ] - }, - { - "id": 103, - "title": "부품 구성 (BOM)", - "section_type": "BOM", - "order_no": 3, - "is_collapsible": true, - "is_default_open": false, - "bom_config": { - "columns": [ - {"key": "child_item_code", "label": "품목코드", "width": 120, "editable": false}, - {"key": "child_item_name", "label": "품목명", "width": 200, "editable": false}, - {"key": "quantity", "label": "수량", "width": 80, "type": "number", "editable": true}, - {"key": "unit", "label": "단위", "width": 60, "editable": false}, - {"key": "quantity_formula", "label": "수량식", "width": 120, "editable": true}, - {"key": "note", "label": "비고", "width": 150, "editable": true} - ], - "allow_search": true, - "search_endpoint": "/api/v1/items/search", - "searchable_item_types": ["PT", "SM", "RM"] - } - } - ], - "conditional_sections": [ - { - "condition": { - "field_key": "needs_bom", - "operator": "equals", - "value": true - }, - "show_sections": [103] - } - ] - } -} -``` - ---- - -### API 4.2: 부품(PT) 조건부 필드 조회 -``` -GET /api/v1/item-master/form-structure/PT/conditional -``` - -**Query Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| part_type | string | Y | 부품유형 (ASSEMBLY, BENDING, PURCHASED) | -| category1 | string | N | 대분류 값 | - -**Request Example**: -``` -GET /api/v1/item-master/form-structure/PT/conditional?part_type=BENDING&category1=가이드레일 -``` - -**Response**: -```json -{ - "success": true, - "data": { - "part_type": "BENDING", - "category1": "가이드레일", - "additional_sections": [ - { - "id": 201, - "title": "절곡품 정보", - "section_type": "CUSTOM", - "order_no": 2, - "fields": [ - { - "id": 2001, - "field_name": "재질", - "field_key": "material", - "field_type": "dropdown", - "is_required": true, - "options": [ - {"label": "EGI 1.55T", "value": "EGI_155"}, - {"label": "SUS 1.2T", "value": "SUS_12"}, - {"label": "SPCC 1.6T", "value": "SPCC_16"} - ] - }, - { - "id": 2002, - "field_name": "길이", - "field_key": "length", - "field_type": "dropdown", - "is_required": true, - "options": [ - {"label": "2438mm", "value": "2438"}, - {"label": "3000mm", "value": "3000"}, - {"label": "4000mm", "value": "4000"} - ] - } - ] - }, - { - "id": 202, - "title": "전개도", - "section_type": "CUSTOM", - "order_no": 3, - "fields": [ - { - "id": 2010, - "field_name": "전개도 이미지", - "field_key": "bending_diagram", - "field_type": "custom", - "component_type": "drawing-canvas", - "is_required": false, - "properties": { - "width": 800, - "height": 400, - "tools": ["pen", "line", "eraser", "text"] - } - }, - { - "id": 2011, - "field_name": "전개도 상세", - "field_key": "bending_details", - "field_type": "custom", - "component_type": "bending-detail-table", - "is_required": false - } - ] - } - ] - } -} -``` - ---- - -## 5. BOM 관리 API - -### API 5.1: 품목 BOM 조회 -``` -GET /api/v1/items/{id}/bom -``` - -**Response**: -```json -{ - "success": true, - "data": { - "item_id": 1, - "item_code": "KD-FG-001", - "item_name": "스크린 제품 A", - "bom_lines": [ - { - "id": 1, - "child_item_id": 2, - "child_item_code": "KD-PT-001", - "child_item_name": "가이드레일(벽면형)", - "quantity": 2, - "unit": "EA", - "unit_price": 50000, - "total_price": 100000, - "quantity_formula": "H / 1000", - "note": "높이에 따라 수량 변동", - "order_no": 1 - } - ], - "total_cost": 250000 - } -} -``` - ---- - -### API 5.2: BOM 항목 추가 -``` -POST /api/v1/items/{id}/bom -``` - -**Request Body**: -```json -{ - "child_item_id": 5, - "quantity": 10, - "unit": "EA", - "quantity_formula": null, - "note": "추가 부품" -} -``` - ---- - -### API 5.3: BOM 항목 수정 -``` -PUT /api/v1/items/{item_id}/bom/{bom_id} -``` - -**Request Body**: -```json -{ - "quantity": 15, - "note": "수량 증가" -} -``` - ---- - -### API 5.4: BOM 항목 삭제 -``` -DELETE /api/v1/items/{item_id}/bom/{bom_id} -``` - ---- - -## 6. 파일 관리 API - -### API 6.1: 파일 업로드 -``` -POST /api/v1/files/upload -``` - -**Content-Type**: `multipart/form-data` - -**Form Data**: -| 필드 | 타입 | 필수 | 설명 | -|------|------|------|------| -| file | File | Y | 업로드할 파일 | -| type | string | Y | 파일 유형 (specification, certification, bending_diagram) | -| item_id | number | N | 연결할 품목 ID (수정 시) | - -**Response**: -```json -{ - "success": true, - "data": { - "file_id": "uuid-1234-5678", - "file_name": "시방서.pdf", - "file_url": "/files/uuid-1234-5678/시방서.pdf", - "file_size": 1024000, - "mime_type": "application/pdf" - } -} -``` - ---- - -### API 6.2: 파일 다운로드 -``` -GET /api/v1/files/{file_id}/download -``` - ---- - -### API 6.3: 파일 삭제 -``` -DELETE /api/v1/files/{file_id} -``` - ---- - -## 7. 마스터 데이터 조회 API - -### API 7.1: 단위 목록 조회 -``` -GET /api/v1/master/units -``` - -**Response**: -```json -{ - "success": true, - "data": [ - {"value": "EA", "label": "EA (개)"}, - {"value": "SET", "label": "SET (세트)"}, - {"value": "M", "label": "M (미터)"}, - {"value": "KG", "label": "KG (킬로그램)"}, - {"value": "L", "label": "L (리터)"} - ] -} -``` - ---- - -### API 7.2: 카테고리 목록 조회 -``` -GET /api/v1/master/categories -``` - -**Query Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| item_type | string | N | 품목유형별 필터 | -| parent_id | number | N | 상위 카테고리 ID | - ---- - -### API 7.3: 재질 목록 조회 -``` -GET /api/v1/master/materials -``` - -**Response**: -```json -{ - "success": true, - "data": [ - {"value": "EGI_155", "label": "EGI 1.55T", "thickness": "1.55"}, - {"value": "SUS_12", "label": "SUS 1.2T", "thickness": "1.2"}, - {"value": "SPCC_16", "label": "SPCC 1.6T", "thickness": "1.6"} - ] -} -``` - ---- - -### API 7.4: 규격 목록 조회 (원자재/부자재) -``` -GET /api/v1/master/specifications -``` - -**Query Parameters**: -| 파라미터 | 타입 | 필수 | 설명 | -|----------|------|------|------| -| item_type | string | Y | RM 또는 SM | -| item_name | string | N | 품목명 필터 (예: SPHC-SD) | - ---- - -## 8. API 요약 테이블 - -### 8.1 필수 API (MVP) -| 우선순위 | 메서드 | 엔드포인트 | 설명 | 상태 | -|----------|--------|------------|------|------| -| 🔴 P0 | GET | `/api/v1/items` | 품목 목록 조회 (페이징) | ⚠️ 기존 | -| 🔴 P0 | GET | `/api/v1/items/{id}` | 품목 단건 조회 (ID) | ⚠️ 기존 | -| 🔴 P0 | GET | `/api/v1/items/code/{code}` | 품목 단건 조회 (코드) | ⚠️ 기존 | -| 🔴 P0 | POST | `/api/v1/items` | 품목 생성 | ⚠️ 기존 | -| 🔴 P0 | PUT | `/api/v1/items/{code}` | 품목 수정 (코드 기반) | ⚠️ 기존 | -| 🔴 P0 | DELETE | `/api/v1/items/{code}` | 품목 삭제 (코드 기반) | ⚠️ 기존 | -| 🔴 P0 | GET | `/api/v1/items/search` | 품목 검색 (BOM용) | 🆕 신규 | - -### 8.2 중요 API -| 우선순위 | 메서드 | 엔드포인트 | 설명 | -|----------|--------|------------|------| -| 🟡 P1 | GET | `/api/v1/item-master/form-structure/{type}` | 동적 폼 구조 조회 | -| 🟡 P1 | GET | `/api/v1/items/stats` | 품목 통계 | -| 🟡 P1 | DELETE | `/api/v1/items/batch` | 품목 일괄 삭제 | -| 🟡 P1 | POST | `/api/v1/files/upload` | 파일 업로드 | - -### 8.3 추가 API -| 우선순위 | 메서드 | 엔드포인트 | 설명 | -|----------|--------|------------|------| -| 🟢 P2 | GET | `/api/v1/item-master/form-structure/PT/conditional` | PT 조건부 필드 | -| 🟢 P2 | GET | `/api/v1/items/{id}/bom` | BOM 조회 | -| 🟢 P2 | POST | `/api/v1/items/{id}/bom` | BOM 항목 추가 | -| 🟢 P2 | PUT | `/api/v1/items/{id}/bom/{bom_id}` | BOM 항목 수정 | -| 🟢 P2 | DELETE | `/api/v1/items/{id}/bom/{bom_id}` | BOM 항목 삭제 | -| 🟢 P2 | GET | `/api/v1/master/units` | 단위 목록 | -| 🟢 P2 | GET | `/api/v1/master/categories` | 카테고리 목록 | -| 🟢 P2 | GET | `/api/v1/master/materials` | 재질 목록 | -| 🟢 P2 | GET | `/api/v1/master/specifications` | 규격 목록 | -| 🟢 P2 | GET | `/api/v1/files/{id}/download` | 파일 다운로드 | -| 🟢 P2 | DELETE | `/api/v1/files/{id}` | 파일 삭제 | - ---- - -## 9. 에러 응답 형식 - -### 공통 에러 응답 -```json -{ - "success": false, - "error": { - "code": "ERROR_CODE", - "message": "사용자 친화적 에러 메시지", - "details": {} - } -} -``` - -### 에러 코드 목록 -| 코드 | HTTP Status | 설명 | -|------|-------------|------| -| VALIDATION_ERROR | 400 | 입력값 검증 실패 | -| ITEM_NOT_FOUND | 404 | 품목을 찾을 수 없음 | -| ITEM_IN_USE | 409 | 품목이 다른 곳에서 사용 중 | -| DUPLICATE_ITEM_CODE | 409 | 중복된 품목코드 | -| FILE_TOO_LARGE | 413 | 파일 크기 초과 | -| UNAUTHORIZED | 401 | 인증 필요 | -| FORBIDDEN | 403 | 권한 없음 | -| INTERNAL_ERROR | 500 | 서버 내부 오류 | - ---- - -## 10. 데이터 모델 참조 - -### 10.1 품목(Item) 테이블 필드 -```sql -CREATE TABLE items ( - id SERIAL PRIMARY KEY, - tenant_id INTEGER NOT NULL, - item_code VARCHAR(50) UNIQUE NOT NULL, - item_name VARCHAR(200) NOT NULL, - item_type VARCHAR(10) NOT NULL, -- FG, PT, SM, RM, CS - unit VARCHAR(20) NOT NULL, - specification VARCHAR(200), - is_active BOOLEAN DEFAULT true, - - -- 제품(FG) 전용 - product_category VARCHAR(20), -- SCREEN, STEEL - lot_abbreviation VARCHAR(10), - certification_number VARCHAR(100), - certification_start_date DATE, - certification_end_date DATE, - specification_file VARCHAR(500), - specification_file_name VARCHAR(200), - certification_file VARCHAR(500), - certification_file_name VARCHAR(200), - - -- 부품(PT) 전용 - part_type VARCHAR(20), -- ASSEMBLY, BENDING, PURCHASED - part_usage VARCHAR(50), - installation_type VARCHAR(50), - assembly_type VARCHAR(10), - assembly_length VARCHAR(20), - side_spec_width VARCHAR(20), - side_spec_height VARCHAR(20), - material VARCHAR(50), - length VARCHAR(20), - bending_diagram TEXT, -- Base64 이미지 - bending_details JSONB, -- 전개도 상세 데이터 - - -- 분류 - category1 VARCHAR(100), - category2 VARCHAR(100), - category3 VARCHAR(100), - - -- 가격 - purchase_price DECIMAL(15,2), - sales_price DECIMAL(15,2), - margin_rate DECIMAL(5,2), - processing_cost DECIMAL(15,2), - labor_cost DECIMAL(15,2), - install_cost DECIMAL(15,2), - - -- 재고 - safety_stock INTEGER, - lead_time INTEGER, - - -- 버전 관리 - current_revision INTEGER DEFAULT 0, - is_final BOOLEAN DEFAULT false, - finalized_date TIMESTAMP, - finalized_by INTEGER, - - note TEXT, - created_by INTEGER, - updated_by INTEGER, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); -``` - -### 10.2 BOM 테이블 -```sql -CREATE TABLE item_bom ( - id SERIAL PRIMARY KEY, - tenant_id INTEGER NOT NULL, - parent_item_id INTEGER REFERENCES items(id), - child_item_id INTEGER REFERENCES items(id), - quantity DECIMAL(10,3) NOT NULL, - unit VARCHAR(20), - unit_price DECIMAL(15,2), - quantity_formula VARCHAR(100), -- 예: "H / 1000" - note TEXT, - order_no INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); -``` - ---- - -## 11. 참고 문서 - -- `src/components/items/ItemListClient.tsx` - 품목 목록 UI -- `src/components/items/ItemForm/index.tsx` - 품목 등록/수정 폼 -- `src/contexts/ItemMasterContext.tsx` - 품목기준관리 데이터 구조 -- `[API-2025-11-25] item-master-data-management-api-request.md` - 품목기준관리 API - ---- - -## 변경 이력 - -| 날짜 | 버전 | 변경 내용 | -|------|------|----------| -| 2025-11-28 | 1.0 | 초안 작성 (동적 렌더링만) | -| 2025-11-28 | 2.0 | **전체 CRUD + 리스트 + 페이징 + BOM + 파일 + 마스터 데이터 추가** | -| 2025-11-28 | 3.0 | **백엔드 스키마/기존 API 기반 재검토**: `/{id}` → `/{code}` 경로 변경, `per_page` → `size` 파라미터 변경, 기존 API 주석 추가, 일괄삭제 코드 기반 변경 | \ No newline at end of file diff --git a/front/[API-RESPONSE-2025-11-28] items-api-spec-changes.md b/front/[API-RESPONSE-2025-11-28] items-api-spec-changes.md deleted file mode 100644 index b05b462..0000000 --- a/front/[API-RESPONSE-2025-11-28] items-api-spec-changes.md +++ /dev/null @@ -1,24 +0,0 @@ -# API 스펙 회신 (2025-11-28) - -요청서 검토 완료. 아래 2가지만 변경됩니다. - ---- - -## 변경 1: 식별자 코드 → ID - -``` -PUT /items/{code} → PUT /items/{id} -DELETE /items/{code} → DELETE /items/{id} -/items/{code}/bom/* → /items/{id}/bom/* -/items/{code}/files/* → /items/{id}/files/* -``` - -## 변경 2: 일괄 삭제 파라미터 - -``` -{ "codes": [...] } → { "ids": [...] } -``` - ---- - -나머지는 요청서대로 진행합니다. \ No newline at end of file diff --git a/specs/ITEM-MASTER-INDEX.md b/specs/ITEM-MASTER-INDEX.md new file mode 100644 index 0000000..e817a34 --- /dev/null +++ b/specs/ITEM-MASTER-INDEX.md @@ -0,0 +1,225 @@ +# Item Master 문서 인덱스 + +> 품목기준관리(ItemMaster) 관련 문서 현황 및 개발 상태 +> +> **최종 업데이트**: 2025-12-09 + +--- + +## 🔑 핵심 개념 정의 + +### item_type (품목 유형 코드) + +| 코드 | 한글명 | 영문명 | source_table | +|------|--------|--------|--------------| +| `FG` | 제품 | Finished Goods | products | +| `PT` | 부품 | Parts | products | +| `SM` | 부자재 | Sub-Materials | materials | +| `RM` | 원자재 | Raw Materials | materials | +| `CS` | 소모품 | Consumables | materials | + +> **저장 위치**: `common_codes` 테이블 (`code_group = 'item_type'`) +> **소스 테이블 매핑**: `attributes.source_table` JSON 필드 +> **시딩 상태**: ✅ 구현 완료 (`ItemTypeSeeder.php`) + +### 관련 용어 구분 + +| 용어 | 역할 | 값 | 사용처 | +|------|------|-----|--------| +| `item_type` | 품목 유형 코드 | FG/PT/SM/RM/CS | API 파라미터, 필터링, UI 표시 | +| `source_table` | 물리적 저장 테이블 | products/materials | DB 조회, 서비스 로직 분기 | +| `ref_type` | 폴리모픽 참조 타입 | PRODUCT/MATERIAL | 기존 폴리모픽 관계 유지 | + +### 매핑 규칙 + +``` +item_type → source_table 자동 매핑: +├─ FG, PT → products 테이블 +└─ SM, RM, CS → materials 테이블 + +API 흐름: +1. 클라이언트: item_type=FG 전송 +2. 서버: common_codes에서 source_table 조회 +3. 서버: products 또는 materials 테이블에 저장/조회 +``` + +--- + +## 📐 문서 관계도 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Item Master 문서 체계 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ 참조 ┌─────────────────────────────┐│ +│ │ API 가이드 │◄──────────│ items-api-unified-plan.md ││ +│ │ (front/) │ │ (plans/) ││ +│ │ │ │ - API 구현 계획 ││ +│ │ 프론트엔드용 │ │ - item_type 값 변경 스펙 ││ +│ │ API 명세 │ │ - Swagger 스키마 정의 ││ +│ └────────┬────────┘ └──────────────┬──────────────┘│ +│ │ │ │ +│ │ 연동 │ 구현 기반 │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────────────────┐│ +│ │ field-integration│ │ field-key-validation.md ││ +│ │ (specs/) │◄───────────│ (specs/) ││ +│ │ │ 검증 정책 │ ││ +│ │ - 필드 통합 설계 │ │ - SystemFields 상수 ││ +│ │ - source_table │ │ - 예약어 검증 로직 ││ +│ │ 기반 분기 │ │ - 에러 메시지 정의 ││ +│ └─────────────────┘ └─────────────────────────────┘│ +│ │ +│ ════════════════════════════════════════════════════════════ │ +│ 공통 기반: common_codes 테이블 (code_group='item_type') │ +│ ════════════════════════════════════════════════════════════ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📋 문서 구조 + +``` +docs/ +├── front/ +│ └── item-master-guide.md ✅ 최신 API 가이드 +├── specs/ +│ ├── ITEM-MASTER-INDEX.md 📋 이 파일 +│ ├── item-master-integration.md 🔧 연동 설계서 (개발 중) +│ ├── item-master-field-integration.md 📄 필드 통합 스펙 +│ └── item-master-field-key-validation.md ✅ 검증 정책 (구현 완료) +├── plans/ +│ ├── items-api-unified-plan.md 📄 Items API 통합 계획 (최신) +│ ├── items-api-modification-plan.md 📦 API 수정 계획 (통합됨) +│ ├── items-naming-convention.md 📄 명명 규칙 +│ └── flow-tests/ +│ └── item-master-*.json 🧪 API 테스트 플로우 +├── guides/ +│ └── item-management-migration.md 📄 마이그레이션 가이드 +├── data/analysis/ +│ └── item-db-analysis.md 📊 DB 분석 +└── history/2025-11/ + ├── item-master-spec.md 📦 구버전 스펙 + ├── item-master-gap-analysis.md 📦 갭 분석 + ├── front-requests/ 📦 프론트 요청서 아카이브 + └── item-master-archived/ 📦 기타 아카이브 +``` + +--- + +## 🎯 핵심 문서 (현재 유효) + +| 문서 | 경로 | 역할 | 상태 | +|------|------|------|------| +| **API 가이드** | `front/item-master-guide.md` | 프론트엔드용 API 명세 | ✅ 최신 | +| **API 통합 계획** | `plans/items-api-unified-plan.md` | Items API 통합 구현 계획 | 📄 최신 | +| **필드 통합 스펙** | `specs/item-master-field-integration.md` | source_table 기반 필드 설계 | 🔧 v1.3 구현 중 | +| **검증 정책** | `specs/item-master-field-key-validation.md` | field_key 검증 로직 | ✅ 구현 완료 | +| **연동 설계서** | `specs/item-master-integration.md` | BE-FE 연동 아키텍처 | 🔧 개발 중 | + +--- + +## 🔴 개발 필요 항목 + +### 1. Items API Material 지원 (items-api-modification-plan.md) + +| API | Product | Material | 작업 내용 | +|-----|:-------:|:--------:|----------| +| `PUT /items/{id}` | ✅ | ❌ | Material 수정 지원 추가 | +| `DELETE /items/{id}` | ✅ | ❌ | Material 삭제 지원 추가 | +| `DELETE /items/batch` | ✅ | ❌ | Material 일괄삭제 지원 | +| `GET /items/code/{code}` | ✅ | ❌ | Material 코드 조회 지원 | + +**관련 파일**: +- `api/app/Services/ItemsService.php` +- `api/app/Http/Controllers/Api/V1/ItemsController.php` + +### 2. 연동 설계 구현 (item-master-integration.md) + +| 항목 | 상태 | 설명 | +|------|------|------| +| ItemFieldValidationService | 🔧 개발 중 | attributes 값 검증 서비스 | +| Products/Materials 수정 | ⏳ 대기 | 검증 연동 적용 | +| field_meta 응답 옵션 | ⏳ 대기 | `?include_field_meta=true` | + +--- + +## ✅ 구현 완료 항목 + +### 1. field_key 검증 정책 (item-master-field-key-validation.md) + +- ✅ `SystemFields` 상수 클래스 생성 +- ✅ 시스템 예약어 검증 로직 +- ✅ source_table 기반 분기 처리 +- ✅ 에러 메시지 (`error.field_key_reserved`) + +### 2. ItemMaster CRUD API + +- ✅ Pages, Sections, Fields, BomItems CRUD +- ✅ 독립 엔티티 아키텍처 +- ✅ entity_relationships 링크 테이블 +- ✅ Lock 기능 (연결 잠금) + +### 3. item_type 코드 시딩 (ItemTypeSeeder) + +- ✅ common_codes 테이블에 item_type 그룹 데이터 시딩 +- ✅ attributes.source_table JSON 매핑 (products/materials) +- ✅ 5개 코드: FG(완제품), PT(부품), SM(부자재), RM(원자재), CS(소모품) + +### 4. DB 구조 확정 (item_pages, item_sections, item_fields) + +- ✅ item_pages 테이블 (페이지 설정) - 이미 존재, source_table 컬럼 포함 +- ✅ item_sections 테이블 (섹션 설정) - 이미 존재 +- ✅ item_fields 테이블 (필드 설정) - 이미 존재 +- ✅ entity_relationships 링크 테이블로 page→section→field 연결 + +> **중요**: item_pages는 common_codes로 대체 불가 +> - common_codes: item_type 마스터 코드 정의 (FG/PT/SM/RM/CS) +> - item_pages: UI 페이지 설정 (같은 item_type에 여러 페이지 가능) +> - 두 테이블은 역할이 다르며 공존해야 함 + +--- + +## 📦 아카이브 (History) + +구버전 문서는 `docs/history/2025-11/`에 보관됨: + +| 폴더 | 내용 | +|------|------| +| `front-requests/` | 11월 프론트엔드 API 요청서 | +| `item-master-archived/` | 분석, 구현, 디자인 문서 | +| `item-master-spec.md` | 초기 API 명세 (11-20) | +| `item-master-gap-analysis.md` | 갭 분석 문서 | + +--- + +## 📊 관련 테스트 + +| 파일 | 위치 | 용도 | +|------|------|------| +| `item-master-init-api-flow.json` | `docs/plans/flow-tests/` | 초기화 API 테스트 | +| `item-master-page-api-flow.json` | `docs/plans/flow-tests/` | 페이지 CRUD 테스트 | +| `item-master-field-api-flow.json` | `docs/plans/flow-tests/` | 필드 CRUD 테스트 | +| `item-fields-is-active-test.json` | `api/docs/api-flows/` | 활성 필터 테스트 | + +--- + +## 🔗 참고 문서 + +- **명명 규칙**: `docs/plans/items-naming-convention.md` +- **마이그레이션**: `docs/guides/item-management-migration.md` +- **DB 분석**: `docs/data/analysis/item-db-analysis.md` +- **MES 분석**: `docs/projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md` + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2025-12-09 | ItemTypeSeeder 구현 완료 (attributes.source_table 추가), DB 구조 확정 기록 추가, field-integration.md v1.3 반영 | +| 2025-12-09 | 핵심 개념 정의 (item_type/source_table/ref_type) 추가, 문서 관계도 추가 | +| 2025-12-09 | items-api-unified-plan.md 추가, 핵심 문서 테이블 갱신 | +| 2025-12-09 | 문서 인덱스 생성, 중복 문서 정리 | diff --git a/specs/item-master-field-integration.md b/specs/item-master-field-integration.md new file mode 100644 index 0000000..a784046 --- /dev/null +++ b/specs/item-master-field-integration.md @@ -0,0 +1,1238 @@ +# ItemMaster 범용 메타 필드 시스템 구현 계획 + +**작성일**: 2025-12-08 +**버전**: v1.3 +**상태**: 구현 중 (DB 구조 확정) + +--- + +## 1. 개요 + +### 1.1 목적 +ItemMaster를 **범용 메타 필드 정의 시스템**으로 확장하여, 다양한 도메인(제품, 자재, 회계, 생산 등)의 필드를 동일한 구조로 관리 + +### 1.2 핵심 원칙 +| 항목 | 방침 | +|------|------| +| **프론트엔드** | 변경 없음 | +| **API 응답** | 변경 없음 (매핑 정보 미노출) | +| **DB 스키마** | `common_codes`로 도메인 관리, `source_table`로 테이블 분기 | +| **백엔드 서비스** | `page.source_table`로 테이블 분기, 저장 시 자동 분배 | + +### 1.3 적용 대상 테이블 (1차) +- `products` - 제품 (FG, PT) +- `materials` - 자재 (SM, RM, CS) +- `product_components` - BOM +- `material_inspections` - 자재 검수 +- `material_inspection_items` - 검수 항목 +- `material_receipts` - 자재 입고 + +### 1.4 향후 확장 예정 +- `journals` - 회계 전표 +- `work_orders` - 생산 지시 +- `quality_controls` - 품질 관리 +- 기타 도메인 테이블 + +--- + +## 2. 분기 로직 플로우 + +### 2.1 현재 구조 (✅ 구현 완료) + +> **참고**: 아래 구조는 이미 DB에 구현되어 운영 중입니다. + +#### 2.1.1 common_codes (item_type 마스터) + +``` +common_codes (code_group = 'item_type') - ✅ 시딩 완료 +┌────────────┬────────┬──────────┬─────────────────────────────────────────┐ +│ code_group │ code │ name │ attributes (JSON) │ +├────────────┼────────┼──────────┼─────────────────────────────────────────┤ +│ item_type │ FG │ 완제품 │ {"source_table":"products","name_en":...}│ +│ item_type │ PT │ 부품 │ {"source_table":"products","name_en":...}│ +│ item_type │ SM │ 부자재 │ {"source_table":"materials","name_en":..}│ +│ item_type │ RM │ 원자재 │ {"source_table":"materials","name_en":..}│ +│ item_type │ CS │ 소모품 │ {"source_table":"materials","name_en":..}│ +└────────────┴────────┴──────────┴─────────────────────────────────────────┘ + +→ attributes.source_table: 물리 테이블 매핑 정보 +→ FG/PT → products, SM/RM/CS → materials +``` + +#### 2.1.2 item_pages (페이지 설정) + +``` +item_pages - ✅ 테이블 존재 (source_table 컬럼 포함) +┌────┬───────────┬──────────┬────────────┬──────────────┬───────────────────┐ +│ id │ tenant_id │ group_id │ page_name │ item_type │ source_table │ +├────┼───────────┼──────────┼────────────┼──────────────┼───────────────────┤ +│ 1 │ 287 │ 1 │ 완제품기본 │ FG │ products │ +│ 2 │ 287 │ 1 │ 완제품상세 │ FG │ products │ ← 같은 FG로 여러 페이지! +│ 3 │ 287 │ 1 │ 부품관리 │ PT │ products │ +│ 4 │ 287 │ 1 │ 부자재 │ SM │ materials │ +└────┴───────────┴──────────┴────────────┴──────────────┴───────────────────┘ + +→ page_name: 유지 (테넌트별 페이지명 커스터마이징) +→ source_table: 성능을 위해 중복 저장 (common_codes에서도 조회 가능) +→ 같은 item_type으로 여러 페이지 생성 가능 +``` + +#### 2.1.3 entity_relationships (N:M 링크) + +``` +entity_relationships - ✅ 테이블 존재 +┌────┬─────────────┬───────────┬─────────────┬──────────┐ +│ id │ parent_type │ parent_id │ child_type │ child_id │ +├────┼─────────────┼───────────┼─────────────┼──────────┤ +│ 1 │ page │ 981 │ section │ 1 │ +│ 2 │ page │ 981 │ section │ 2 │ +│ 3 │ section │ 1 │ field │ 1 │ +└────┴─────────────┴───────────┴─────────────┴──────────┘ + +→ 독립 엔티티 아키텍처 +→ page → section → field 관계를 링크 테이블로 관리 +``` + +### 2.2 아키텍처 관계도 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ItemMaster 테이블 구조 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ common_codes (마스터 코드) │ │ +│ │ code_group='item_type' │ │ +│ │ ────────────────────── │ │ +│ │ FG → attributes.source_table │ │ +│ │ PT → attributes.source_table │ │ +│ │ SM/RM/CS → attributes.source_table │ │ +│ └──────────────┬───────────────────────┘ │ +│ │ 참조 (item_type) │ +│ ▼ │ +│ ┌──────────────────────────────────────┐ │ +│ │ item_pages (페이지 설정) │ │ +│ │ ────────────────────── │ │ +│ │ - 테넌트별 페이지 구성 │ │ +│ │ - 같은 item_type으로 N개 페이지 │ │ +│ │ - source_table (성능용 중복 저장) │ │ +│ └──────────────┬───────────────────────┘ │ +│ │ parent_id │ +│ ▼ │ +│ ┌──────────────────────────────────────┐ │ +│ │ entity_relationships (N:M 링크) │ │ +│ │ ────────────────────── │ │ +│ │ page → section → field │ │ +│ └──────────────┬───────────────────────┘ │ +│ │ │ +│ ┌───────┴───────┐ │ +│ ▼ ▼ │ +│ ┌────────────┐ ┌────────────┐ │ +│ │item_sections│ │item_fields │ │ +│ │(독립 엔티티)│ │(독립 엔티티)│ │ +│ └────────────┘ └────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.3 source_table 조회 방법 + +```php +// 방법 1: item_pages에서 직접 조회 (성능 우선) +$page = ItemPage::find($pageId); +$sourceTable = $page->source_table; // 'products' or 'materials' + +// 방법 2: common_codes에서 조회 (정규화 우선) +$code = CommonCode::where('code_group', 'item_type') + ->where('code', $itemType) + ->first(); +$sourceTable = $code->attributes['source_table']; +``` + +#### 2.2.4 향후 테이블 분리 확장 예시 + +``` +나중에 item_type별로 다른 테이블 사용이 필요할 경우: + +현재: + FG → source_table = 'products' + PT → source_table = 'products' + +확장 가능: + FG → source_table = 'finished_goods' (별도 테이블) + PT → source_table = 'semi_products' (별도 테이블) + +→ source_table만 변경하면 테이블 스위칭 가능 +→ item_type은 그대로 유지 (프론트엔드 변경 없음) +``` + +### 2.3 데이터 저장 플로우 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ [프론트엔드] │ +│ │ │ +│ ▼ │ +│ 1. 페이지 선택 (page_id = 1, 완제품) │ +│ │ │ +│ ▼ │ +│ 2. 필드 입력 후 저장 │ +│ │ │ +│ ▼ │ +│ POST /item-master/data │ +│ { │ +│ "page_id": 1, │ +│ "field_values": { │ +│ "1": "FG-001", ← 품목코드 │ +│ "2": "완제품A", ← 품목명 │ +│ "3": "EA" ← 단위 │ +│ } │ +│ } │ +│ │ │ +│ ▼ │ +│ [백엔드] │ +│ │ │ +│ ▼ │ +│ 3. page_id → source_table 조회 ('products') │ +│ │ │ +│ ▼ │ +│ 4. source_table = 'products' → products 테이블에 저장 │ +│ │ │ +│ ▼ │ +│ 5. 필드별 source_column 매핑 │ +│ field_id=1 → source_column='code' │ +│ field_id=2 → source_column='name' │ +│ field_id=3 → source_column='unit' │ +│ │ │ +│ ▼ │ +│ 6. INSERT INTO products (code, name, unit) VALUES (...) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.4 향후 확장 예시 (회계) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ [프론트엔드] - 동일한 ItemMaster UI 사용 │ +│ │ │ +│ ▼ │ +│ POST /item-master/data │ +│ { │ +│ "page_id": 6, ← 회계전표 페이지 │ +│ "field_values": { │ +│ "101": "2025-12-08", ← 전표일자 │ +│ "102": "매출", ← 전표유형 │ +│ "103": 1000000 ← 금액 │ +│ } │ +│ } │ +│ │ │ +│ ▼ │ +│ [백엔드] │ +│ │ │ +│ ▼ │ +│ page_id=6 → source_table='journals' → journals 테이블에 저장 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 현재 테이블 스키마 분석 + +### 3.1 products (31 컬럼) + +| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | +|--------|------|------|---------------------| +| code | varchar(50) | 품목코드 | textbox (필수) | +| name | varchar(255) | 품목명 | textbox (필수) | +| unit | varchar(20) | 단위 | dropdown (필수) | +| product_type | varchar(20) | 제품유형 (FG/PT) | dropdown | +| category_id | bigint | 카테고리 | dropdown | +| is_sellable | tinyint(1) | 판매가능 | checkbox | +| is_purchasable | tinyint(1) | 구매가능 | checkbox | +| is_producible | tinyint(1) | 생산가능 | checkbox | +| is_active | tinyint(1) | 활성화 | checkbox | +| certification_number | varchar(100) | 인증번호 | textbox | +| certification_date | date | 인증일자 | date | +| certification_expiry | date | 인증만료일 | date | +| bending_diagram_file_id | bigint | 밴딩도면 파일 | file | +| specification_file_id | bigint | 시방서 파일 | file | +| certification_file_id | bigint | 인증서 파일 | file | +| attributes | json | 동적 속성 | (커스텀 필드 저장용) | + +### 3.2 materials (20 컬럼) + +| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | +|--------|------|------|---------------------| +| material_code | varchar(50) | 자재코드 | textbox (필수) | +| name | varchar(255) | 자재명 | textbox (필수) | +| item_name | varchar(255) | 품목명 | textbox | +| specification | varchar(255) | 규격 | textbox | +| unit | varchar(20) | 단위 | dropdown (필수) | +| category_id | bigint | 카테고리 | dropdown | +| is_inspection | tinyint(1) | 검수필요 | checkbox | +| search_tag | text | 검색태그 | textarea | +| attributes | json | 동적 속성 | (커스텀 필드 저장용) | +| options | json | 옵션 | (커스텀 필드 저장용) | + +### 3.3 product_components (15 컬럼) - BOM + +| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | +|--------|------|------|---------------------| +| parent_product_id | bigint | 상위제품 | lookup | +| ref_type | varchar(20) | 참조유형 (product/material) | dropdown | +| ref_id | bigint | 참조ID | lookup | +| quantity | decimal(18,6) | 수량 | number (필수) | +| formula | varchar(500) | 계산공식 | textbox | +| sort_order | int | 정렬순서 | number | +| note | text | 비고 | textarea | + +### 3.4 material_inspections (14 컬럼) + +| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | +|--------|------|------|---------------------| +| material_id | bigint | 자재ID | lookup | +| inspection_date | date | 검수일 | date (필수) | +| inspector_id | bigint | 검수자 | dropdown | +| status | varchar(20) | 상태 | dropdown | +| lot_no | varchar(50) | LOT번호 | textbox | +| quantity | decimal(15,4) | 검수수량 | number | +| passed_quantity | decimal(15,4) | 합격수량 | number | +| rejected_quantity | decimal(15,4) | 불합격수량 | number | +| note | text | 비고 | textarea | + +### 3.5 material_inspection_items (9 컬럼) + +| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | +|--------|------|------|---------------------| +| inspection_id | bigint | 검수ID | lookup | +| check_item | varchar(255) | 점검항목 | textbox (필수) | +| standard | varchar(255) | 기준 | textbox | +| result | varchar(20) | 결과 | dropdown | +| measured_value | varchar(100) | 측정값 | textbox | +| note | text | 비고 | textarea | + +### 3.6 material_receipts (18 컬럼) + +| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | +|--------|------|------|---------------------| +| material_id | bigint | 자재ID | lookup | +| receipt_date | date | 입고일 | date (필수) | +| lot_no | varchar(50) | LOT번호 | textbox | +| quantity | decimal(15,4) | 입고수량 | number (필수) | +| unit_price | decimal(15,4) | 단가 | number | +| total_price | decimal(15,4) | 금액 | number | +| supplier_id | bigint | 공급업체 | dropdown | +| warehouse_id | bigint | 입고창고 | dropdown | +| po_number | varchar(50) | 발주번호 | textbox | +| invoice_number | varchar(50) | 송장번호 | textbox | +| note | text | 비고 | textarea | + +--- + +## 4. DB 스키마 변경 + +### 4.1 마이그레이션: item_fields 확장 + +```php +string('source_table', 100) + ->nullable() + ->after('properties') + ->comment('내부용: 원본 테이블명 (products, materials 등)'); + + $table->string('source_column', 100) + ->nullable() + ->after('source_table') + ->comment('내부용: 원본 컬럼명 (code, name 등)'); + + $table->enum('storage_type', ['column', 'json']) + ->default('json') + ->after('source_column') + ->comment('내부용: 저장방식 (column=DB컬럼, json=attributes/options)'); + + $table->string('json_path', 200) + ->nullable() + ->after('storage_type') + ->comment('내부용: JSON 저장 경로 (예: attributes.custom_size)'); + + // 인덱스 + $table->index(['source_table', 'source_column'], 'idx_source_mapping'); + }); + } + + public function down(): void + { + Schema::table('item_fields', function (Blueprint $table) { + $table->dropIndex('idx_source_mapping'); + $table->dropColumn(['source_table', 'source_column', 'storage_type', 'json_path']); + }); + } +}; +``` + +### 4.2 컬럼 설명 + +| 컬럼 | 타입 | 용도 | +|------|------|------| +| `source_table` | varchar(100) | 원본 테이블명 (NULL이면 커스텀 필드) | +| `source_column` | varchar(100) | 원본 컬럼명 | +| `storage_type` | enum | `column`: DB 컬럼 직접 저장, `json`: JSON 필드에 저장 | +| `json_path` | varchar(200) | JSON 저장 시 경로 (예: `attributes.custom_size`) | + +### 4.3 item_pages 테이블 (✅ 이미 구현됨) + +> **참고**: item_pages 테이블은 이미 source_table 컬럼이 추가되어 있습니다. +> page_name은 유지됩니다 (테넌트별 페이지명 커스터마이징 필요). + +```sql +-- 현재 item_pages 구조 +CREATE TABLE item_pages ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, + group_id INT DEFAULT 1, + page_name VARCHAR(100), -- 유지 (테넌트별 커스텀 가능) + item_type ENUM('FG','PT','SM','RM','CS'), + source_table VARCHAR(100), -- ✅ 이미 추가됨 + absolute_path VARCHAR(500), + is_active TINYINT(1) DEFAULT 1, + ... +); +``` + +### 4.4 common_codes 시더 (item_type) - ✅ 구현 완료 + +```php + 'item_type', + 'code' => 'FG', + 'name' => '완제품', + 'tenant_id' => $tenantId, + 'attributes' => json_encode([ + 'source_table' => 'products', + 'name_en' => 'Finished Goods', + ]), + ], + [ + 'code_group' => 'item_type', + 'code' => 'PT', + 'name' => '부품', + 'tenant_id' => $tenantId, + 'attributes' => json_encode([ + 'source_table' => 'products', + 'name_en' => 'Parts', + ]), + ], + [ + 'code_group' => 'item_type', + 'code' => 'SM', + 'name' => '부자재', + 'tenant_id' => $tenantId, + 'attributes' => json_encode([ + 'source_table' => 'materials', + 'name_en' => 'Sub-Materials', + ]), + ], + [ + 'code_group' => 'item_type', + 'code' => 'RM', + 'name' => '원자재', + 'tenant_id' => $tenantId, + 'attributes' => json_encode([ + 'source_table' => 'materials', + 'name_en' => 'Raw Materials', + ]), + ], + [ + 'code_group' => 'item_type', + 'code' => 'CS', + 'name' => '소모품', + 'tenant_id' => $tenantId, + 'attributes' => json_encode([ + 'source_table' => 'materials', + 'name_en' => 'Consumables', + ]), + ], + ]; + + foreach ($itemTypes as $index => $item) { + DB::table('common_codes')->updateOrInsert( + [ + 'code_group' => $item['code_group'], + 'code' => $item['code'], + 'tenant_id' => $item['tenant_id'], + ], + array_merge($item, [ + 'sort_order' => $index + 1, + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]) + ); + } + } +} + +// 실행: php artisan db:seed --class=ItemTypeSeeder +``` + +--- + +## 5. 모델 수정 + +### 5.1 ItemField 모델 + +```php + 'boolean', + 'display_condition' => 'array', + 'validation_rules' => 'array', + 'options' => 'array', + 'properties' => 'array', + ]; + + /** + * API 응답에서 제외할 컬럼 (내부용) + */ + protected $hidden = [ + 'source_table', + 'source_column', + 'storage_type', + 'json_path', + ]; + + /** + * 시스템 필드 여부 확인 + */ + public function isSystemField(): bool + { + return !is_null($this->source_table) && !is_null($this->source_column); + } + + /** + * 컬럼 직접 저장 여부 + */ + public function isColumnStorage(): bool + { + return $this->storage_type === 'column'; + } + + /** + * JSON 저장 여부 + */ + public function isJsonStorage(): bool + { + return $this->storage_type === 'json'; + } +} +``` + +--- + +## 6. 시딩 데이터 + +### 6.1 시더 클래스 + +```php +getProductFields($tenantId), + $this->getMaterialFields($tenantId), + $this->getBomFields($tenantId), + $this->getInspectionFields($tenantId), + $this->getReceiptFields($tenantId) + ); + + foreach ($systemFields as $field) { + DB::table('item_fields')->updateOrInsert( + [ + 'tenant_id' => $field['tenant_id'], + 'source_table' => $field['source_table'], + 'source_column' => $field['source_column'], + ], + $field + ); + } + } + + private function getProductFields(int $tenantId): array + { + $baseFields = [ + 'tenant_id' => $tenantId, + 'source_table' => 'products', + 'storage_type' => 'column', + 'created_at' => now(), + 'updated_at' => now(), + ]; + + return [ + array_merge($baseFields, [ + 'source_column' => 'code', + 'field_name' => '품목코드', + 'field_type' => 'textbox', + 'is_required' => true, + 'order_no' => 1, + ]), + array_merge($baseFields, [ + 'source_column' => 'name', + 'field_name' => '품목명', + 'field_type' => 'textbox', + 'is_required' => true, + 'order_no' => 2, + ]), + array_merge($baseFields, [ + 'source_column' => 'unit', + 'field_name' => '단위', + 'field_type' => 'dropdown', + 'is_required' => true, + 'order_no' => 3, + ]), + array_merge($baseFields, [ + 'source_column' => 'product_type', + 'field_name' => '제품유형', + 'field_type' => 'dropdown', + 'order_no' => 4, + 'options' => json_encode([ + ['label' => '완제품', 'value' => 'FG'], + ['label' => '반제품', 'value' => 'PT'], + ]), + ]), + array_merge($baseFields, [ + 'source_column' => 'category_id', + 'field_name' => '카테고리', + 'field_type' => 'dropdown', + 'order_no' => 5, + ]), + array_merge($baseFields, [ + 'source_column' => 'is_sellable', + 'field_name' => '판매가능', + 'field_type' => 'checkbox', + 'order_no' => 6, + 'default_value' => 'true', + ]), + array_merge($baseFields, [ + 'source_column' => 'is_purchasable', + 'field_name' => '구매가능', + 'field_type' => 'checkbox', + 'order_no' => 7, + 'default_value' => 'false', + ]), + array_merge($baseFields, [ + 'source_column' => 'is_producible', + 'field_name' => '생산가능', + 'field_type' => 'checkbox', + 'order_no' => 8, + 'default_value' => 'true', + ]), + array_merge($baseFields, [ + 'source_column' => 'is_active', + 'field_name' => '활성화', + 'field_type' => 'checkbox', + 'order_no' => 9, + 'default_value' => 'true', + ]), + array_merge($baseFields, [ + 'source_column' => 'certification_number', + 'field_name' => '인증번호', + 'field_type' => 'textbox', + 'order_no' => 10, + ]), + array_merge($baseFields, [ + 'source_column' => 'certification_date', + 'field_name' => '인증일자', + 'field_type' => 'date', + 'order_no' => 11, + ]), + array_merge($baseFields, [ + 'source_column' => 'certification_expiry', + 'field_name' => '인증만료일', + 'field_type' => 'date', + 'order_no' => 12, + ]), + ]; + } + + private function getMaterialFields(int $tenantId): array + { + $baseFields = [ + 'tenant_id' => $tenantId, + 'source_table' => 'materials', + 'storage_type' => 'column', + 'created_at' => now(), + 'updated_at' => now(), + ]; + + return [ + array_merge($baseFields, [ + 'source_column' => 'material_code', + 'field_name' => '자재코드', + 'field_type' => 'textbox', + 'is_required' => true, + 'order_no' => 1, + ]), + array_merge($baseFields, [ + 'source_column' => 'name', + 'field_name' => '자재명', + 'field_type' => 'textbox', + 'is_required' => true, + 'order_no' => 2, + ]), + array_merge($baseFields, [ + 'source_column' => 'item_name', + 'field_name' => '품목명', + 'field_type' => 'textbox', + 'order_no' => 3, + ]), + array_merge($baseFields, [ + 'source_column' => 'specification', + 'field_name' => '규격', + 'field_type' => 'textbox', + 'order_no' => 4, + ]), + array_merge($baseFields, [ + 'source_column' => 'unit', + 'field_name' => '단위', + 'field_type' => 'dropdown', + 'is_required' => true, + 'order_no' => 5, + ]), + array_merge($baseFields, [ + 'source_column' => 'category_id', + 'field_name' => '카테고리', + 'field_type' => 'dropdown', + 'order_no' => 6, + ]), + array_merge($baseFields, [ + 'source_column' => 'is_inspection', + 'field_name' => '검수필요', + 'field_type' => 'checkbox', + 'order_no' => 7, + 'default_value' => 'false', + ]), + array_merge($baseFields, [ + 'source_column' => 'search_tag', + 'field_name' => '검색태그', + 'field_type' => 'textarea', + 'order_no' => 8, + ]), + ]; + } + + private function getBomFields(int $tenantId): array + { + $baseFields = [ + 'tenant_id' => $tenantId, + 'source_table' => 'product_components', + 'storage_type' => 'column', + 'created_at' => now(), + 'updated_at' => now(), + ]; + + return [ + array_merge($baseFields, [ + 'source_column' => 'ref_type', + 'field_name' => '참조유형', + 'field_type' => 'dropdown', + 'order_no' => 1, + 'options' => json_encode([ + ['label' => '제품', 'value' => 'product'], + ['label' => '자재', 'value' => 'material'], + ]), + ]), + array_merge($baseFields, [ + 'source_column' => 'ref_id', + 'field_name' => '참조품목', + 'field_type' => 'dropdown', + 'order_no' => 2, + ]), + array_merge($baseFields, [ + 'source_column' => 'quantity', + 'field_name' => '수량', + 'field_type' => 'number', + 'is_required' => true, + 'order_no' => 3, + 'properties' => json_encode(['precision' => 6]), + ]), + array_merge($baseFields, [ + 'source_column' => 'formula', + 'field_name' => '계산공식', + 'field_type' => 'textbox', + 'order_no' => 4, + ]), + array_merge($baseFields, [ + 'source_column' => 'note', + 'field_name' => '비고', + 'field_type' => 'textarea', + 'order_no' => 5, + ]), + ]; + } + + private function getInspectionFields(int $tenantId): array + { + $baseFields = [ + 'tenant_id' => $tenantId, + 'source_table' => 'material_inspections', + 'storage_type' => 'column', + 'created_at' => now(), + 'updated_at' => now(), + ]; + + return [ + array_merge($baseFields, [ + 'source_column' => 'inspection_date', + 'field_name' => '검수일', + 'field_type' => 'date', + 'is_required' => true, + 'order_no' => 1, + ]), + array_merge($baseFields, [ + 'source_column' => 'inspector_id', + 'field_name' => '검수자', + 'field_type' => 'dropdown', + 'order_no' => 2, + ]), + array_merge($baseFields, [ + 'source_column' => 'status', + 'field_name' => '검수상태', + 'field_type' => 'dropdown', + 'order_no' => 3, + 'options' => json_encode([ + ['label' => '대기', 'value' => 'pending'], + ['label' => '진행중', 'value' => 'in_progress'], + ['label' => '완료', 'value' => 'completed'], + ['label' => '불합격', 'value' => 'rejected'], + ]), + ]), + array_merge($baseFields, [ + 'source_column' => 'lot_no', + 'field_name' => 'LOT번호', + 'field_type' => 'textbox', + 'order_no' => 4, + ]), + array_merge($baseFields, [ + 'source_column' => 'quantity', + 'field_name' => '검수수량', + 'field_type' => 'number', + 'order_no' => 5, + ]), + array_merge($baseFields, [ + 'source_column' => 'passed_quantity', + 'field_name' => '합격수량', + 'field_type' => 'number', + 'order_no' => 6, + ]), + array_merge($baseFields, [ + 'source_column' => 'rejected_quantity', + 'field_name' => '불합격수량', + 'field_type' => 'number', + 'order_no' => 7, + ]), + array_merge($baseFields, [ + 'source_column' => 'note', + 'field_name' => '비고', + 'field_type' => 'textarea', + 'order_no' => 8, + ]), + ]; + } + + private function getReceiptFields(int $tenantId): array + { + $baseFields = [ + 'tenant_id' => $tenantId, + 'source_table' => 'material_receipts', + 'storage_type' => 'column', + 'created_at' => now(), + 'updated_at' => now(), + ]; + + return [ + array_merge($baseFields, [ + 'source_column' => 'receipt_date', + 'field_name' => '입고일', + 'field_type' => 'date', + 'is_required' => true, + 'order_no' => 1, + ]), + array_merge($baseFields, [ + 'source_column' => 'lot_no', + 'field_name' => 'LOT번호', + 'field_type' => 'textbox', + 'order_no' => 2, + ]), + array_merge($baseFields, [ + 'source_column' => 'quantity', + 'field_name' => '입고수량', + 'field_type' => 'number', + 'is_required' => true, + 'order_no' => 3, + ]), + array_merge($baseFields, [ + 'source_column' => 'unit_price', + 'field_name' => '단가', + 'field_type' => 'number', + 'order_no' => 4, + 'properties' => json_encode(['precision' => 4]), + ]), + array_merge($baseFields, [ + 'source_column' => 'total_price', + 'field_name' => '금액', + 'field_type' => 'number', + 'order_no' => 5, + 'properties' => json_encode(['precision' => 4]), + ]), + array_merge($baseFields, [ + 'source_column' => 'supplier_id', + 'field_name' => '공급업체', + 'field_type' => 'dropdown', + 'order_no' => 6, + ]), + array_merge($baseFields, [ + 'source_column' => 'warehouse_id', + 'field_name' => '입고창고', + 'field_type' => 'dropdown', + 'order_no' => 7, + ]), + array_merge($baseFields, [ + 'source_column' => 'po_number', + 'field_name' => '발주번호', + 'field_type' => 'textbox', + 'order_no' => 8, + ]), + array_merge($baseFields, [ + 'source_column' => 'invoice_number', + 'field_name' => '송장번호', + 'field_type' => 'textbox', + 'order_no' => 9, + ]), + array_merge($baseFields, [ + 'source_column' => 'note', + 'field_name' => '비고', + 'field_type' => 'textarea', + 'order_no' => 10, + ]), + ]; + } +} +``` + +--- + +## 7. 서비스 로직 (데이터 저장) + +### 7.1 ItemDataService (신규) + +```php + value] 형태 + * @param int|null $recordId 수정 시 레코드 ID + * @return array 저장된 데이터 + */ + public function saveData(string $sourceTable, array $fieldValues, ?int $recordId = null): array + { + // 해당 테이블의 필드 매핑 정보 조회 + $fields = ItemField::where('tenant_id', $this->tenantId()) + ->where('source_table', $sourceTable) + ->get() + ->keyBy('id'); + + $columnData = []; // DB 컬럼 직접 저장 + $jsonData = []; // JSON (attributes/options) 저장 + + foreach ($fieldValues as $fieldId => $value) { + $field = $fields->get($fieldId); + + if (!$field) { + // 시스템 필드가 아닌 커스텀 필드 + $customField = ItemField::find($fieldId); + if ($customField) { + $jsonPath = $customField->json_path ?? "attributes.{$customField->field_name}"; + data_set($jsonData, $jsonPath, $value); + } + continue; + } + + if ($field->isColumnStorage()) { + // DB 컬럼에 직접 저장 + $columnData[$field->source_column] = $this->castValue($value, $field); + } else { + // JSON 필드에 저장 + $jsonPath = $field->json_path ?? "attributes.{$field->field_name}"; + data_set($jsonData, $jsonPath, $value); + } + } + + // JSON 데이터 병합 + if (!empty($jsonData['attributes'])) { + $columnData['attributes'] = json_encode($jsonData['attributes']); + } + if (!empty($jsonData['options'])) { + $columnData['options'] = json_encode($jsonData['options']); + } + + // 공통 컬럼 추가 + $columnData['tenant_id'] = $this->tenantId(); + $columnData['updated_by'] = $this->apiUserId(); + + if ($recordId) { + // 수정 + DB::table($sourceTable) + ->where('tenant_id', $this->tenantId()) + ->where('id', $recordId) + ->update($columnData); + + return array_merge(['id' => $recordId], $columnData); + } else { + // 생성 + $columnData['created_by'] = $this->apiUserId(); + $id = DB::table($sourceTable)->insertGetId($columnData); + + return array_merge(['id' => $id], $columnData); + } + } + + /** + * 필드 타입에 따른 값 변환 + */ + private function castValue($value, ItemField $field) + { + return match ($field->field_type) { + 'number' => is_numeric($value) ? (float) $value : null, + 'checkbox' => filter_var($value, FILTER_VALIDATE_BOOLEAN), + 'date' => $value ? date('Y-m-d', strtotime($value)) : null, + default => $value, + }; + } + + /** + * 레코드 조회 시 필드 매핑 적용 + */ + public function getData(string $sourceTable, int $recordId): array + { + $record = DB::table($sourceTable) + ->where('tenant_id', $this->tenantId()) + ->where('id', $recordId) + ->first(); + + if (!$record) { + return []; + } + + // 필드 매핑 정보 조회 + $fields = ItemField::where('tenant_id', $this->tenantId()) + ->where('source_table', $sourceTable) + ->get(); + + $result = []; + $attributes = json_decode($record->attributes ?? '{}', true); + $options = json_decode($record->options ?? '{}', true); + + foreach ($fields as $field) { + if ($field->isColumnStorage()) { + $result[$field->id] = $record->{$field->source_column} ?? null; + } else { + $jsonPath = $field->json_path ?? "attributes.{$field->field_name}"; + $result[$field->id] = data_get( + ['attributes' => $attributes, 'options' => $options], + $jsonPath + ); + } + } + + return $result; + } +} +``` + +--- + +## 8. API 영향 없음 확인 + +### 8.1 기존 API 응답 (변경 없음) + +```json +// GET /api/v1/item-master/init +{ + "success": true, + "message": "message.fetched", + "data": { + "pages": [{ + "id": 1, + "page_name": "기본정보", + "item_type": "FG", + "sections": [{ + "id": 1, + "title": "품목코드 정보", + "fields": [ + { + "id": 1, + "field_name": "품목코드", + "field_type": "textbox", + "is_required": true, + "order_no": 1 + // source_table, source_column 등은 $hidden으로 제외됨 + } + ] + }] + }] + } +} +``` + +### 8.2 프론트엔드 (변경 없음) + +- 기존 ItemMaster API 그대로 사용 +- 필드 정의 조회/수정 동일 +- 품목 데이터 저장 시 기존 Products/Materials API 사용 + +--- + +## 9. 구현 순서 + +| 순서 | 작업 | 예상 시간 | 담당 | +|------|------|----------|------| +| 1 | 마이그레이션 파일 생성 및 실행 | 30분 | Backend | +| 2 | ItemField 모델 수정 ($hidden 추가) | 15분 | Backend | +| 3 | 시더 클래스 생성 | 1시간 | Backend | +| 4 | 시딩 실행 및 데이터 확인 | 30분 | Backend | +| 5 | ItemDataService 구현 | 2시간 | Backend | +| 6 | 기존 ProductService/MaterialService 연동 | 2시간 | Backend | +| 7 | 테스트 | 1시간 | Backend | + +**총 예상 시간: 7~8시간 (1일)** + +--- + +## 10. 향후 확장 + +### 10.1 신규 도메인 추가 시 +1. 대상 테이블 스키마 분석 +2. 시더에 필드 매핑 추가 +3. 시딩 실행 +4. (필요시) ItemDataService에 특수 로직 추가 + +### 10.2 예정 도메인 +- [ ] 회계 (accounts, journals, ledgers) +- [ ] 생산 (work_orders, production_records) +- [ ] 재고 (inventories, stock_movements) +- [ ] 품질 (quality_controls, defect_reports) + +--- + +## 11. 체크리스트 + +### 구현 전 +- [ ] 현재 item_fields 테이블 구조 확인 +- [ ] 마이그레이션 롤백 계획 수립 +- [ ] 기존 데이터 백업 + +### 구현 중 +- [ ] 마이그레이션 실행 +- [ ] 모델 $hidden 적용 +- [ ] 시더 실행 +- [ ] API 응답 검증 (매핑 컬럼 미노출 확인) + +### 구현 후 +- [ ] 기존 ItemMaster API 정상 동작 확인 +- [ ] 프론트엔드 영향 없음 확인 +- [ ] 품목 저장 시 매핑 정상 동작 확인 + +--- + +**문서 끝** diff --git a/specs/item-master-field-key-validation.md b/specs/item-master-field-key-validation.md new file mode 100644 index 0000000..c035c59 --- /dev/null +++ b/specs/item-master-field-key-validation.md @@ -0,0 +1,200 @@ +# Item Master field_key 검증 정책 + +## 개요 + +field_key 저장 및 검증 정책을 변경하여 시스템 필드(고정 컬럼)와의 충돌을 방지합니다. + +## 변경 사항 + +### 1. field_key 저장 정책 변경 + +**변경 전:** +``` +field_key = {id}_{입력값} +예: 98_code, 99_name +``` + +**변경 후:** +``` +field_key = {입력값} +예: code, name (단, 시스템 예약어는 사용 불가) +``` + +### 2. 시스템 필드 예약어 검증 추가 + +#### 검증 흐름 +``` +field_key 입력 + ↓ +source_table 확인 (products / materials) + ↓ +해당 테이블 예약어 체크 + ↓ +기존 필드 중복 체크 + ↓ +저장 +``` + +#### source_table 기반 예약어 매핑 + +| source_table | 대상 테이블 | 예약어 목록 | +|--------------|-------------|-------------| +| `products` | products | code, name, unit, product_type, ... | +| `materials` | materials | name, material_code, material_type, ... | +| `null` | 전체 | products + materials 예약어 모두 체크 (안전 모드) | + +## 구현 상세 + +### 파일 구조 + +``` +app/ +├── Constants/ +│ └── SystemFields.php # 신규: 예약어 상수 클래스 +└── Services/ + └── ItemMaster/ + └── ItemFieldService.php # 수정: 예약어 검증 추가 +``` + +### SystemFields 상수 클래스 + +```php +// app/Constants/SystemFields.php + +class SystemFields +{ + // 소스 테이블 상수 + public const SOURCE_TABLE_PRODUCTS = 'products'; + public const SOURCE_TABLE_MATERIALS = 'materials'; + + // 그룹 ID 상수 + public const GROUP_ITEM_MASTER = 1; + + // products 테이블 고정 컬럼 + public const PRODUCTS = [ + 'code', 'name', 'unit', 'category_id', 'product_type', 'description', + 'is_sellable', 'is_purchasable', 'is_producible', 'is_variable_size', 'is_active', + 'safety_stock', 'lead_time', 'product_category', 'part_type', + 'bending_diagram', 'bending_details', + 'specification_file', 'specification_file_name', + 'certification_file', 'certification_file_name', + 'certification_number', 'certification_start_date', 'certification_end_date', + 'attributes', 'attributes_archive', + ]; + + // materials 테이블 고정 컬럼 + public const MATERIALS = [ + 'name', 'item_name', 'specification', 'material_code', 'material_type', + 'unit', 'category_id', 'is_inspection', 'is_active', + 'search_tag', 'remarks', 'attributes', 'options', + ]; + + // 공통 시스템 컬럼 + public const COMMON = [ + 'id', 'tenant_id', 'created_by', 'updated_by', 'deleted_by', + 'created_at', 'updated_at', 'deleted_at', + ]; + + // source_table 기반 예약어 조회 + public static function getReservedKeys(string $sourceTable): array; + + // 예약어 여부 확인 + public static function isReserved(string $fieldKey, string $sourceTable): bool; + + // 그룹 내 전체 예약어 조회 (안전 모드) + public static function getAllReservedKeysForGroup(int $groupId): array; + + // 그룹 내 예약어 여부 확인 + public static function isReservedInGroup(string $fieldKey, int $groupId): bool; +} +``` + +### ItemFieldService 검증 메서드 + +```php +private function validateFieldKeyUnique( + string $fieldKey, + int $tenantId, + ?string $sourceTable = null, + int $groupId = 1, + ?int $excludeId = null +): void { + // 1. 시스템 필드(예약어) 체크 + if ($sourceTable) { + if (SystemFields::isReserved($fieldKey, $sourceTable)) { + throw ValidationException::withMessages([ + 'field_key' => [__('error.field_key_reserved', ['field_key' => $fieldKey])], + ]); + } + } else { + // 안전 모드: 그룹 내 모든 테이블 예약어 체크 + if (SystemFields::isReservedInGroup($fieldKey, $groupId)) { + throw ValidationException::withMessages([ + 'field_key' => [__('error.field_key_reserved', ['field_key' => $fieldKey])], + ]); + } + } + + // 2. 기존 필드 중복 체크 + // ... +} +``` + +### 호출 예시 + +```php +// 독립 필드 생성 시 +$this->validateFieldKeyUnique( + $data['field_key'], + $tenantId, + $data['source_table'] ?? null, // 'products' 또는 'materials' + $data['group_id'] ?? 1 +); + +// 필드 수정 시 +$this->validateFieldKeyUnique( + $data['field_key'], + $tenantId, + $data['source_table'] ?? null, + $field->group_id ?? 1, + $id // excludeId +); +``` + +## 에러 메시지 + +| 상황 | 메시지 키 | 메시지 | +|------|----------|--------| +| 시스템 예약어 충돌 | `error.field_key_reserved` | `"code"은(는) 시스템 예약어로 사용할 수 없습니다.` | +| 기존 필드 중복 | `validation.unique` | `field_key은(는) 이미 사용 중입니다.` | + +```php +// lang/ko/error.php +'field_key_reserved' => '":field_key"은(는) 시스템 예약어로 사용할 수 없습니다.', + +// lang/ko/validation.php (Laravel 기본) +'unique' => ':attribute은(는) 이미 사용 중입니다.', +``` + +## clone 메서드 field_key 복제 정책 + +``` +원본 field_key: custom_field +복제본: custom_field_copy + +중복 시: custom_field_copy2, custom_field_copy3, ... +``` + +## 관련 파일 + +| 파일 | 변경 유형 | 설명 | +|------|----------|------| +| `app/Constants/SystemFields.php` | 신규 | 예약어 상수 클래스 | +| `app/Services/ItemMaster/ItemFieldService.php` | 수정 | 검증 로직 추가 | +| `lang/ko/error.php` | 수정 | 에러 메시지 추가 | + +## 참고 + +- ItemPage 테이블의 `source_table` 컬럼: 실제 저장 테이블명 (products, materials) +- ItemPage 테이블의 `item_type` 컬럼: FG, PT, SM, RM, CS (품목 유형 코드) +- `group_id`: 카테고리 격리용 (1 = 품목관리)