From 33ef01b2b406454f0790debd423ce598f01bedda Mon Sep 17 00:00:00 2001 From: hskwon Date: Fri, 12 Dec 2025 08:51:54 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20API=20=EB=AC=B8=EC=84=9C=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=9C=EB=82=98=EB=A6=AC?= =?UTF-8?q?=EC=98=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - front/item-master-items-api.md - Items API 프론트엔드 연동 문서 - guides/menu-delete-verification-queries.md - 메뉴 삭제 검증 쿼리 가이드 - plans/flow-tests/item-delete-force-delete.json - 품목 삭제 테스트 시나리오 --- front/item-master-items-api.md | 763 ++++++++++++++++++ guides/menu-delete-verification-queries.md | 198 +++++ .../flow-tests/item-delete-force-delete.json | 410 ++++++++++ 3 files changed, 1371 insertions(+) create mode 100644 front/item-master-items-api.md create mode 100644 guides/menu-delete-verification-queries.md create mode 100644 plans/flow-tests/item-delete-force-delete.json diff --git a/front/item-master-items-api.md b/front/item-master-items-api.md new file mode 100644 index 0000000..d00dbc1 --- /dev/null +++ b/front/item-master-items-api.md @@ -0,0 +1,763 @@ +# 품목기준관리(ItemMaster) & 품목관리(Items) API 문서 + +> 프론트엔드 개발자를 위한 API 스펙 문서 +> 작성일: 2025-12-10 + +## 목차 +1. [개요](#개요) +2. [품목관리 (Items) API](#품목관리-items-api) +3. [품목기준관리 (ItemMaster) API](#품목기준관리-itemmaster-api) +4. [공통 응답 형식](#공통-응답-형식) +5. [에러 처리](#에러-처리) + +--- + +## 개요 + +### 품목관리 (Items) vs 품목기준관리 (ItemMaster) + +| 구분 | 품목관리 (Items) | 품목기준관리 (ItemMaster) | +|------|------------------|---------------------------| +| **역할** | 실제 품목 데이터 CRUD | 품목 입력 화면/폼 구성 관리 | +| **대상 데이터** | products, materials 테이블 | item_pages, item_sections, item_fields 테이블 | +| **사용자** | 품목 등록/수정하는 일반 사용자 | 화면 구성을 설정하는 관리자 | +| **비유** | 엑셀 데이터 | 엑셀 양식(템플릿) | + +### 품목 유형 코드 (item_type / product_type) + +| 코드 | 설명 | 저장 테이블 | +|------|------|-------------| +| `FG` | 완제품 (Finished Goods) | products | +| `PT` | 반제품/부품 (Part) | products | +| `SM` | 반자재 (Semi-Material) | materials | +| `RM` | 원자재 (Raw Material) | materials | +| `CS` | 소모품 (Consumables) | materials | + +--- + +## 품목관리 (Items) API + +실제 품목 데이터를 조회/생성/수정/삭제하는 API입니다. + +### 1. 통합 품목 목록 조회 + +**역할**: products와 materials를 통합하여 조회 (UNION 방식) + +``` +GET /api/v1/items +``` + +#### Request Parameters (Query) + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `type` | string | N | 품목 유형 필터 (쉼표 구분). 기본값: `FG,PT,SM,RM,CS` | +| `search` 또는 `q` | string | N | 검색어 (코드, 이름, 태그) | +| `category_id` | integer | N | 카테고리 ID 필터 | +| `size` | integer | N | 페이지당 항목 수. 기본값: 20 | +| `page` | integer | N | 페이지 번호. 기본값: 1 | +| `include_deleted` | boolean | N | 삭제된 항목 포함 여부. 기본값: false | + +#### Request 예시 +```http +GET /api/v1/items?type=FG,PT&search=모터&size=10&page=1 +``` + +#### Response +```json +{ + "success": true, + "message": "조회되었습니다.", + "data": { + "data": [ + { + "id": 1, + "item_type": "FG", + "code": "P-001", + "name": "완제품A", + "specification": null, + "unit": "EA", + "category_id": 5, + "type_code": "FG", + "created_at": "2025-01-01T00:00:00.000000Z", + "deleted_at": null, + "safety_stock": 100, + "lead_time": 7 + } + ], + "current_page": 1, + "per_page": 20, + "total": 150, + "last_page": 8 + } +} +``` + +#### Response 필드 설명 + +| 필드 | 설명 | +|------|------| +| `id` | 품목 고유 ID (테이블별 독립) | +| `item_type` | 품목 유형 코드 | +| `code` | 품목 코드 | +| `name` | 품목명 | +| `specification` | 규격 (materials만 해당) | +| `unit` | 단위 (EA, KG, M 등) | +| `category_id` | 카테고리 ID | +| `type_code` | 품목 유형 코드 (item_type과 동일) | +| `created_at` | 생성일시 | +| `deleted_at` | 삭제일시 (Soft Delete) | +| `safety_stock` | 안전재고 (attributes에서 플랫 전개) | +| `lead_time` | 리드타임 (attributes에서 플랫 전개) | + +> **Note**: `attributes` JSON 필드는 자동으로 최상위로 플랫 전개되어 반환됩니다. + +--- + +### 2. 단일 품목 조회 (ID 기반) + +**역할**: 특정 ID의 품목 상세 정보 조회 (가격 정보 옵션) + +``` +GET /api/v1/items/{id} +``` + +#### Path Parameters + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `id` | integer | Y | 품목 ID | + +#### Query Parameters + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `item_type` | string | N | 품목 유형 코드. 기본값: `FG` | +| `include_price` | boolean | N | 가격 정보 포함 여부. 기본값: false | +| `client_id` | integer | N | 고객 ID (가격 조회 시) | +| `price_date` | string | N | 가격 기준일 (YYYY-MM-DD) | + +#### Request 예시 +```http +GET /api/v1/items/123?item_type=SM&include_price=true&client_id=5 +``` + +#### Response +```json +{ + "success": true, + "message": "조회되었습니다.", + "data": { + "id": 123, + "item_type": "SM", + "code": "M-001", + "name": "반자재A", + "specification": "10mm x 20mm", + "unit": "EA", + "category_id": 10, + "category": { + "id": 10, + "name": "철강류" + }, + "type_code": "SM", + "prices": { + "sale": { + "unit_price": 15000, + "currency": "KRW", + "effective_from": "2025-01-01" + }, + "purchase": { + "unit_price": 10000, + "currency": "KRW", + "effective_from": "2025-01-01" + } + } + } +} +``` + +--- + +### 3. 단일 품목 조회 (코드 기반) + +**역할**: 품목 코드로 상세 정보 조회 (Product → Material 순서로 검색) + +``` +GET /api/v1/items/code/{code} +``` + +#### Path Parameters + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `code` | string | Y | 품목 코드 | + +#### Query Parameters + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `include_bom` | boolean | N | BOM 정보 포함 여부. 기본값: false | + +#### Request 예시 +```http +GET /api/v1/items/code/P-001?include_bom=true +``` + +--- + +### 4. 품목 생성 + +**역할**: 새 품목 등록 (product_type에 따라 products 또는 materials에 저장) + +``` +POST /api/v1/items +``` + +#### Request Body + +| 필드 | 타입 | 필수 | 설명 | +|------|------|------|------| +| `code` | string | Y | 품목 코드 (최대 50자, 중복 시 자동 증가) | +| `name` | string | Y | 품목명 (최대 255자) | +| `product_type` | string | Y | 품목 유형: `FG`, `PT`, `SM`, `RM`, `CS` | +| `unit` | string | Y | 단위 (최대 20자) | +| `category_id` | integer | N | 카테고리 ID | +| `description` | string | N | 설명 | +| `is_sellable` | boolean | N | 판매 가능 여부. 기본값: true | +| `is_purchasable` | boolean | N | 구매 가능 여부. 기본값: false | +| `is_producible` | boolean | N | 생산 가능 여부. 기본값: false | +| `safety_stock` | integer | N | 안전재고 (0 이상) | +| `lead_time` | integer | N | 리드타임 (0 이상) | +| `is_variable_size` | boolean | N | 가변 사이즈 여부 | +| `product_category` | string | N | 제품 분류 (최대 20자) | +| `part_type` | string | N | 부품 유형 (최대 20자) | +| `attributes` | object | N | 동적 필드 (JSON) | +| `material_code` | string | N | 자재 코드 (Material 전용) | +| `item_name` | string | N | 품명 (Material 전용) | +| `specification` | string | N | 규격 (Material 전용) | +| `is_inspection` | string | N | 검수 여부: `Y`, `N` | +| `search_tag` | string | N | 검색 태그 | +| `remarks` | string | N | 비고 | +| `options` | object | N | 옵션 (JSON) | + +#### Request 예시 +```json +{ + "code": "P-NEW-001", + "name": "신규 완제품", + "product_type": "FG", + "unit": "EA", + "category_id": 5, + "is_sellable": true, + "safety_stock": 50, + "attributes": { + "color": "red", + "size": "L" + } +} +``` + +#### Response +```json +{ + "success": true, + "message": "품목이 생성되었습니다.", + "data": { + "id": 999, + "code": "P-NEW-001", + "name": "신규 완제품", + "product_type": "FG", + "unit": "EA", + "category_id": 5, + "is_active": true, + "is_sellable": true, + "is_purchasable": false, + "is_producible": false, + "created_by": 1, + "created_at": "2025-12-10T10:00:00.000000Z" + } +} +``` + +> **중복 코드 처리**: 코드가 이미 존재하면 자동으로 증가합니다. +> - `P-001` 중복 → `P-002` +> - `ABC` 중복 → `ABC-001` + +--- + +### 5. 품목 수정 + +**역할**: 기존 품목 정보 수정 + +``` +PUT /api/v1/items/{id} +``` + +#### Path Parameters + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `id` | integer | Y | 품목 ID | + +#### Request Body + +| 필드 | 타입 | 필수 | 설명 | +|------|------|------|------| +| `item_type` | string | Y | 품목 유형 (테이블 분기용) | +| `code` | string | N | 품목 코드 | +| `name` | string | N | 품목명 | +| ... | | | (생성과 동일한 필드, 모두 선택) | + +#### Request 예시 +```json +{ + "item_type": "FG", + "name": "수정된 완제품명", + "safety_stock": 100 +} +``` + +#### Response +```json +{ + "success": true, + "message": "품목이 수정되었습니다.", + "data": { + "id": 999, + "code": "P-NEW-001", + "name": "수정된 완제품명", + "safety_stock": 100, + "updated_by": 1, + "updated_at": "2025-12-10T11:00:00.000000Z" + } +} +``` + +--- + +### 6. 품목 삭제 (Soft Delete) + +**역할**: 품목 삭제 (BOM 구성품으로 사용 중이면 삭제 불가) + +``` +DELETE /api/v1/items/{id} +``` + +#### Path Parameters + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `id` | integer | Y | 품목 ID | + +#### Query Parameters + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `item_type` | string | N | 품목 유형. 기본값: `FG` | + +#### Response +```json +{ + "success": true, + "message": "품목이 삭제되었습니다.", + "data": "success" +} +``` + +#### 에러 Response (BOM 사용 중) +```json +{ + "success": false, + "message": "해당 품목은 3건의 BOM에서 구성품으로 사용 중입니다." +} +``` + +--- + +### 7. 품목 일괄 삭제 + +**역할**: 여러 품목 일괄 삭제 + +``` +DELETE /api/v1/items/batch +``` + +#### Request Body + +| 필드 | 타입 | 필수 | 설명 | +|------|------|------|------| +| `item_type` | string | Y | 품목 유형 | +| `ids` | array | Y | 삭제할 품목 ID 배열 | + +#### Request 예시 +```json +{ + "item_type": "FG", + "ids": [1, 2, 3, 4, 5] +} +``` + +--- + +## 품목기준관리 (ItemMaster) API + +품목 입력 화면의 구성(페이지, 섹션, 필드)을 관리하는 API입니다. + +### 구조 개요 + +``` +ItemMaster 계층 구조: + +Page (페이지) +├── Section (섹션) - fields 타입 +│ ├── Field (필드) +│ ├── Field (필드) +│ └── Field (필드) +└── Section (섹션) - bom 타입 + ├── BomItem (BOM 항목) + └── BomItem (BOM 항목) +``` + +- **Page**: 품목 유형별 입력 화면 (예: 완제품 등록 페이지) +- **Section**: 페이지 내 영역 구분 (예: 기본정보, BOM 구성) +- **Field**: 입력 필드 정의 (예: 품목코드, 품목명) +- **BomItem**: BOM 섹션의 구성품 정의 + +### 1. 초기화 데이터 로드 + +**역할**: 프론트엔드 앱 초기화 시 필요한 전체 ItemMaster 데이터 로드 + +``` +GET /api/v1/item-master/init +``` + +#### Response +```json +{ + "success": true, + "message": "조회되었습니다.", + "data": { + "pages": [ + { + "id": 1, + "tenant_id": 1, + "group_id": null, + "page_name": "완제품 등록", + "item_type": "FG", + "absolute_path": "/items/fg/create", + "is_active": true, + "sections": [ + { + "id": 10, + "title": "기본정보", + "type": "fields", + "order_no": 1, + "is_locked": false, + "fields": [ + { + "id": 100, + "field_name": "품목코드", + "field_key": "code", + "field_type": "textbox", + "is_required": true, + "order_no": 1, + "is_locked": true + } + ] + } + ] + } + ], + "sections": [...], + "fields": [...], + "customTabs": [...], + "unitOptions": [...] + } +} +``` + +#### Response 필드 설명 + +| 필드 | 설명 | +|------|------| +| `pages` | 페이지 목록 (섹션, 필드 중첩 포함) | +| `sections` | 모든 독립 섹션 목록 (재사용 가능) | +| `fields` | 모든 독립 필드 목록 (재사용 가능) | +| `customTabs` | 커스텀 탭 목록 (컬럼 설정 포함) | +| `unitOptions` | 단위 옵션 목록 | + +--- + +### 2. 페이지 API + +#### 페이지 목록 조회 + +``` +GET /api/v1/item-master/pages +``` + +| Query 파라미터 | 타입 | 필수 | 설명 | +|----------------|------|------|------| +| `item_type` | string | N | 품목 유형 필터 | + +#### 페이지 생성 + +``` +POST /api/v1/item-master/pages +``` + +| Request Body | 타입 | 필수 | 설명 | +|--------------|------|------|------| +| `page_name` | string | Y | 페이지명 (최대 255자) | +| `item_type` | string | Y | 품목 유형: `FG`, `PT`, `SM`, `RM`, `CS` | +| `absolute_path` | string | N | 절대 경로 (최대 500자) | + +#### 페이지 수정 + +``` +PUT /api/v1/item-master/pages/{id} +``` + +#### 페이지 삭제 + +``` +DELETE /api/v1/item-master/pages/{id} +``` + +--- + +### 3. 섹션 API + +#### 독립 섹션 목록 조회 + +``` +GET /api/v1/item-master/sections +``` + +| Query 파라미터 | 타입 | 필수 | 설명 | +|----------------|------|------|------| +| `is_template` | boolean | N | 템플릿 섹션 필터 | + +#### 독립 섹션 생성 (페이지 연결 없음) + +``` +POST /api/v1/item-master/sections +``` + +| Request Body | 타입 | 필수 | 설명 | +|--------------|------|------|------| +| `group_id` | integer | N | 계층 번호 | +| `title` | string | Y | 섹션 제목 (최대 255자) | +| `type` | string | Y | 섹션 타입: `fields`, `bom` | + +#### 페이지에 섹션 생성 (연결) + +``` +POST /api/v1/item-master/pages/{pageId}/sections +``` + +#### 섹션 복제 + +``` +POST /api/v1/item-master/sections/{id}/clone +``` + +#### 섹션 사용처 조회 + +``` +GET /api/v1/item-master/sections/{id}/usage +``` + +**역할**: 해당 섹션이 어떤 페이지에서 사용되고 있는지 조회 + +#### 섹션 수정 + +``` +PUT /api/v1/item-master/sections/{id} +``` + +#### 섹션 삭제 + +``` +DELETE /api/v1/item-master/sections/{id} +``` + +#### 섹션 순서 변경 + +``` +PUT /api/v1/item-master/pages/{pageId}/sections/reorder +``` + +| Request Body | 타입 | 필수 | 설명 | +|--------------|------|------|------| +| `items` | array | Y | `[{id: 1, order_no: 1}, {id: 2, order_no: 2}]` | + +--- + +### 4. 필드 API + +#### 독립 필드 목록 조회 + +``` +GET /api/v1/item-master/fields +``` + +#### 독립 필드 생성 (섹션 연결 없음) + +``` +POST /api/v1/item-master/fields +``` + +| Request Body | 타입 | 필수 | 설명 | +|--------------|------|------|------| +| `group_id` | integer | N | 계층 번호 | +| `field_name` | string | Y | 필드 표시명 (최대 255자) | +| `field_key` | string | N | 필드 키 (최대 80자, 영문 시작) | +| `field_type` | string | Y | 필드 타입 (아래 참조) | +| `is_required` | boolean | N | 필수 입력 여부 | +| `default_value` | string | N | 기본값 | +| `placeholder` | string | N | 플레이스홀더 (최대 255자) | +| `display_condition` | object | N | 표시 조건 (JSON) | +| `validation_rules` | object | N | 검증 규칙 (JSON) | +| `options` | array | N | 드롭다운 옵션 등 (JSON) | +| `properties` | object | N | 추가 속성 (JSON) | +| `is_locked` | boolean | N | 잠금 여부 | + +#### 필드 타입 (field_type) + +| 타입 | 설명 | +|------|------| +| `textbox` | 한 줄 텍스트 입력 | +| `number` | 숫자 입력 | +| `dropdown` | 드롭다운 선택 | +| `checkbox` | 체크박스 | +| `date` | 날짜 선택 | +| `textarea` | 여러 줄 텍스트 입력 | + +#### 섹션에 필드 생성 (연결) + +``` +POST /api/v1/item-master/sections/{sectionId}/fields +``` + +#### 필드 복제 + +``` +POST /api/v1/item-master/fields/{id}/clone +``` + +#### 필드 사용처 조회 + +``` +GET /api/v1/item-master/fields/{id}/usage +``` + +**역할**: 해당 필드가 어떤 섹션에서 사용되고 있는지 조회 + +#### 필드 수정 + +``` +PUT /api/v1/item-master/fields/{id} +``` + +#### 필드 삭제 + +``` +DELETE /api/v1/item-master/fields/{id} +``` + +--- + +## 공통 응답 형식 + +모든 API는 동일한 응답 구조를 따릅니다. + +### 성공 응답 + +```json +{ + "success": true, + "message": "처리되었습니다.", + "data": { ... } +} +``` + +### 페이지네이션 응답 + +```json +{ + "success": true, + "message": "조회되었습니다.", + "data": { + "data": [...], + "current_page": 1, + "per_page": 20, + "total": 100, + "last_page": 5, + "from": 1, + "to": 20 + } +} +``` + +--- + +## 에러 처리 + +### 에러 응답 형식 + +```json +{ + "success": false, + "message": "에러 메시지", + "errors": { + "field_name": ["검증 오류 메시지"] + } +} +``` + +### 주요 HTTP 상태 코드 + +| 코드 | 설명 | +|------|------| +| `200` | 성공 | +| `201` | 생성 성공 | +| `400` | 잘못된 요청 (검증 실패, 중복 코드 등) | +| `401` | 인증 필요 | +| `403` | 권한 없음 | +| `404` | 리소스 없음 | +| `422` | 검증 실패 (Validation Error) | +| `500` | 서버 오류 | + +### 주요 에러 케이스 + +| 상황 | 메시지 예시 | +|------|-------------| +| 품목 없음 | "해당 품목을 찾을 수 없습니다." | +| 코드 중복 | "이미 사용 중인 품목코드입니다." | +| BOM 사용 중 | "해당 품목은 N건의 BOM에서 구성품으로 사용 중입니다." | +| 필수 필드 누락 | "품목코드는 필수입니다." | +| 잘못된 품목 유형 | "품목 유형은 FG, PT, SM, RM, CS 중 하나여야 합니다." | + +--- + +## 인증 + +모든 API는 다음 인증이 필요합니다: + +1. **API Key**: `X-API-KEY` 헤더 +2. **Bearer Token**: `Authorization: Bearer {token}` 헤더 + +```http +GET /api/v1/items +X-API-KEY: your-api-key +Authorization: Bearer your-access-token +``` + +--- + +## 변경 이력 + +| 날짜 | 버전 | 변경 내용 | +|------|------|----------| +| 2025-12-10 | 1.0 | 최초 작성 | diff --git a/guides/menu-delete-verification-queries.md b/guides/menu-delete-verification-queries.md new file mode 100644 index 0000000..d84c1ef --- /dev/null +++ b/guides/menu-delete-verification-queries.md @@ -0,0 +1,198 @@ +# 메뉴 삭제 검증 쿼리 + +메뉴 영구 삭제 전/후 연관 데이터 확인용 SQL 쿼리 + +## 삭제 전 확인 쿼리 + +### 1. 특정 메뉴의 연관 권한 조회 +```sql +-- 메뉴 ID를 기준으로 연관 권한 조회 +-- {MENU_ID}를 실제 메뉴 ID로 변경 + +SELECT + p.id, + p.name, + p.guard_name, + p.tenant_id +FROM permissions p +WHERE p.name LIKE 'menu:{MENU_ID}.%' +ORDER BY p.name; +``` + +### 2. 역할-권한 연결 조회 +```sql +-- 해당 메뉴 권한이 어떤 역할에 할당되어 있는지 확인 + +SELECT + r.id AS role_id, + r.name AS role_name, + r.tenant_id, + p.name AS permission_name +FROM role_has_permissions rhp +JOIN roles r ON rhp.role_id = r.id +JOIN permissions p ON rhp.permission_id = p.id +WHERE p.name LIKE 'menu:{MENU_ID}.%' +ORDER BY r.name, p.name; +``` + +### 3. 사용자 직접 권한 조회 +```sql +-- 해당 메뉴 권한이 어떤 사용자에게 직접 할당되어 있는지 확인 + +SELECT + u.id AS user_id, + u.name AS user_name, + u.email, + p.name AS permission_name +FROM model_has_permissions mhp +JOIN users u ON mhp.model_id = u.id AND mhp.model_type = 'App\\Models\\User' +JOIN permissions p ON mhp.permission_id = p.id +WHERE p.name LIKE 'menu:{MENU_ID}.%' +ORDER BY u.name, p.name; +``` + +### 4. 부서 권한 조회 +```sql +-- 해당 메뉴 권한이 어떤 부서에 할당되어 있는지 확인 + +SELECT + d.id AS dept_id, + d.name AS dept_name, + d.tenant_id, + p.name AS permission_name +FROM model_has_permissions mhp +JOIN departments d ON mhp.model_id = d.id AND mhp.model_type = 'App\\Models\\Tenants\\Department' +JOIN permissions p ON mhp.permission_id = p.id +WHERE p.name LIKE 'menu:{MENU_ID}.%' +ORDER BY d.name, p.name; +``` + +### 5. 종합 영향도 조회 (삭제 전 확인용) +```sql +-- 삭제 시 영향받는 모든 데이터 요약 + +SELECT + 'permissions' AS table_name, + COUNT(*) AS record_count +FROM permissions +WHERE name LIKE 'menu:{MENU_ID}.%' + +UNION ALL + +SELECT + 'role_has_permissions' AS table_name, + COUNT(*) AS record_count +FROM role_has_permissions rhp +JOIN permissions p ON rhp.permission_id = p.id +WHERE p.name LIKE 'menu:{MENU_ID}.%' + +UNION ALL + +SELECT + 'model_has_permissions (users)' AS table_name, + COUNT(*) AS record_count +FROM model_has_permissions mhp +JOIN permissions p ON mhp.permission_id = p.id +WHERE p.name LIKE 'menu:{MENU_ID}.%' +AND mhp.model_type = 'App\\Models\\User' + +UNION ALL + +SELECT + 'model_has_permissions (departments)' AS table_name, + COUNT(*) AS record_count +FROM model_has_permissions mhp +JOIN permissions p ON mhp.permission_id = p.id +WHERE p.name LIKE 'menu:{MENU_ID}.%' +AND mhp.model_type = 'App\\Models\\Tenants\\Department'; +``` + +## 삭제 후 확인 쿼리 + +### 1. 메뉴가 삭제되었는지 확인 +```sql +-- 메뉴 테이블에서 해당 ID 확인 (soft delete 포함) +SELECT id, name, deleted_at FROM menus WHERE id = {MENU_ID}; + +-- 완전히 삭제된 경우 결과 없음 +``` + +### 2. 연관 권한이 삭제되었는지 확인 +```sql +-- 연관 권한이 모두 삭제되었는지 확인 (결과가 0이어야 함) +SELECT COUNT(*) AS remaining_permissions +FROM permissions +WHERE name LIKE 'menu:{MENU_ID}.%'; +``` + +### 3. 역할-권한 연결이 삭제되었는지 확인 +```sql +-- FK CASCADE로 자동 삭제되었는지 확인 +SELECT COUNT(*) AS remaining_role_permissions +FROM role_has_permissions rhp +WHERE NOT EXISTS ( + SELECT 1 FROM permissions p WHERE p.id = rhp.permission_id +); +``` + +### 4. 아카이브에 저장되었는지 확인 +```sql +-- archived_records에서 삭제 기록 조회 +SELECT + ar.id, + ar.batch_id, + ar.batch_description, + ar.record_type, + ar.original_id, + ar.deleted_at, + ar.notes, + JSON_EXTRACT(ar.main_data, '$.name') AS menu_name +FROM archived_records ar +WHERE ar.record_type = 'menu' +AND ar.original_id = {MENU_ID} +ORDER BY ar.deleted_at DESC; + +-- 연관 테이블 데이터도 확인 +SELECT + arr.table_name, + arr.record_count, + arr.data +FROM archived_record_relations arr +JOIN archived_records ar ON arr.archived_record_id = ar.id +WHERE ar.record_type = 'menu' +AND ar.original_id = {MENU_ID}; +``` + +## 글로벌 메뉴용 쿼리 + +글로벌 메뉴의 경우 `menu:{ID}` 대신 `global_menu:{ID}` 패턴 사용: + +```sql +-- 글로벌 메뉴 권한 조회 +SELECT * FROM permissions WHERE name LIKE 'global_menu:{MENU_ID}.%'; + +-- 글로벌 메뉴 참조하는 테넌트 메뉴 확인 +SELECT id, tenant_id, name, global_menu_id +FROM menus +WHERE global_menu_id = {MENU_ID}; + +-- 삭제 후 참조 해제 확인 (global_menu_id가 NULL이고 is_customized가 true) +SELECT id, tenant_id, name, global_menu_id, is_customized +FROM menus +WHERE is_customized = 1; +``` + +## 실행 예시 + +```bash +# MySQL 콘솔에서 실행 +mysql -u root -p samdb + +# 또는 Laravel Tinker에서 실행 +php artisan tinker +>>> DB::select("SELECT * FROM permissions WHERE name LIKE 'menu:123.%'"); +``` + +--- + +**최종 업데이트**: 2025-12-09 diff --git a/plans/flow-tests/item-delete-force-delete.json b/plans/flow-tests/item-delete-force-delete.json new file mode 100644 index 0000000..14bf097 --- /dev/null +++ b/plans/flow-tests/item-delete-force-delete.json @@ -0,0 +1,410 @@ +{ + "name": "품목 삭제 테스트 (Force Delete & 사용중 체크)", + "description": "품목 삭제 기능 테스트: 1) 사용되지 않는 품목은 Force Delete 2) 사용 중인 품목은 삭제 불가 에러 반환", + "version": "1.0", + "config": { + "baseUrl": "", + "timeout": 30000, + "stopOnFailure": true + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}", + "ts": "{{$timestamp}}" + }, + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{user_id}}", + "user_pwd": "{{user_pwd}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + }, + "extract": { + "token": "$.access_token" + } + }, + { + "id": "create_test_product", + "name": "테스트용 제품 생성 (FG)", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "product_type": "FG", + "code": "TEST-DEL-FG-{{ts}}", + "name": "삭제테스트용 완제품", + "unit": "EA", + "is_active": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "product_id": "$.data.id", + "product_code": "$.data.code" + } + }, + { + "id": "create_test_material", + "name": "테스트용 자재 생성 (RM)", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "product_type": "RM", + "code": "TEST-DEL-RM-{{ts}}", + "name": "삭제테스트용 원자재", + "unit": "EA", + "is_active": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "material_id": "$.data.id", + "material_code": "$.data.code" + } + }, + { + "id": "delete_unused_product", + "name": "사용되지 않는 제품 삭제 (Force Delete 성공)", + "method": "DELETE", + "endpoint": "/items/{{create_test_product.product_id}}?item_type=FG", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_product_deleted", + "name": "제품 영구 삭제 확인 (404 응답)", + "method": "GET", + "endpoint": "/items/{{create_test_product.product_id}}", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [404], + "jsonPath": { + "$.success": false + } + } + }, + { + "id": "delete_unused_material", + "name": "사용되지 않는 자재 삭제 (Force Delete 성공)", + "method": "DELETE", + "endpoint": "/items/{{create_test_material.material_id}}?item_type=RM", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "verify_material_deleted", + "name": "자재 영구 삭제 확인 (404 응답)", + "method": "GET", + "endpoint": "/items/{{create_test_material.material_id}}?item_type=RM", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [404], + "jsonPath": { + "$.success": false + } + } + }, + { + "id": "create_parent_product", + "name": "BOM 상위 제품 생성", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "product_type": "FG", + "code": "TEST-BOM-PARENT-{{ts}}", + "name": "BOM 상위 제품", + "unit": "EA", + "is_active": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "parent_id": "$.data.id" + } + }, + { + "id": "create_child_material", + "name": "BOM 구성품용 자재 생성", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "product_type": "RM", + "code": "TEST-BOM-CHILD-{{ts}}", + "name": "BOM 구성품 자재", + "unit": "EA", + "is_active": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "child_material_id": "$.data.id", + "child_material_code": "$.data.code" + } + }, + { + "id": "add_bom_component", + "name": "BOM 구성품 추가 (자재를 상위 제품에 연결)", + "method": "POST", + "endpoint": "/items/{{create_parent_product.parent_id}}/bom", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "items": [ + { + "ref_type": "MATERIAL", + "ref_id": "{{create_child_material.child_material_id}}", + "quantity": 2, + "unit": "EA" + } + ] + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "delete_used_material", + "name": "사용 중인 자재 삭제 시도 (400 에러 - BOM 구성품)", + "method": "DELETE", + "endpoint": "/items/{{create_child_material.child_material_id}}?item_type=RM", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [400], + "jsonPath": { + "$.success": false + } + } + }, + { + "id": "create_batch_products", + "name": "일괄 삭제용 제품 1 생성", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "product_type": "FG", + "code": "TEST-BATCH-1-{{ts}}", + "name": "일괄삭제 테스트 1", + "unit": "EA", + "is_active": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "batch_id_1": "$.data.id" + } + }, + { + "id": "create_batch_products_2", + "name": "일괄 삭제용 제품 2 생성", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "product_type": "FG", + "code": "TEST-BATCH-2-{{ts}}", + "name": "일괄삭제 테스트 2", + "unit": "EA", + "is_active": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "batch_id_2": "$.data.id" + } + }, + { + "id": "batch_delete_unused", + "name": "사용되지 않는 제품들 일괄 삭제 (성공)", + "method": "DELETE", + "endpoint": "/items/batch", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "item_type": "FG", + "ids": ["{{create_batch_products.batch_id_1}}", "{{create_batch_products_2.batch_id_2}}"] + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "create_batch_for_fail", + "name": "일괄 삭제 실패 테스트용 제품 생성", + "method": "POST", + "endpoint": "/items", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "product_type": "FG", + "code": "TEST-BATCH-FAIL-{{ts}}", + "name": "일괄삭제 실패 테스트", + "unit": "EA", + "is_active": true + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "batch_fail_id": "$.data.id" + } + }, + { + "id": "batch_delete_with_used", + "name": "사용 중인 자재 일괄 삭제 시도 (400 에러)", + "method": "DELETE", + "endpoint": "/items/batch", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "item_type": "RM", + "ids": ["{{create_child_material.child_material_id}}"] + }, + "expect": { + "status": [400], + "jsonPath": { + "$.success": false + } + } + }, + { + "id": "cleanup_bom", + "name": "정리: BOM 구성품 전체 삭제 (빈 배열로 교체)", + "method": "POST", + "endpoint": "/items/{{create_parent_product.parent_id}}/bom/replace", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "body": { + "items": [] + }, + "expect": { + "status": [200, 201, 404] + } + }, + { + "id": "cleanup_parent", + "name": "정리: 상위 제품 삭제", + "method": "DELETE", + "endpoint": "/items/{{create_parent_product.parent_id}}?item_type=FG", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200, 404] + } + }, + { + "id": "cleanup_child", + "name": "정리: 구성품 자재 삭제 (BOM 해제 후)", + "method": "DELETE", + "endpoint": "/items/{{create_child_material.child_material_id}}?item_type=RM", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200, 404] + } + }, + { + "id": "cleanup_batch_fail", + "name": "정리: 일괄삭제 테스트용 제품 삭제", + "method": "DELETE", + "endpoint": "/items/{{create_batch_for_fail.batch_fail_id}}?item_type=FG", + "headers": { + "Authorization": "Bearer {{login.token}}" + }, + "expect": { + "status": [200, 404] + } + } + ] +} \ No newline at end of file