feat: Item Master 하이브리드 구조 전환 및 독립 API 추가
- CASCADE FK → 독립 엔티티 + entity_relationships 링크 테이블 - 독립 API 10개 추가 (섹션/필드/BOM CRUD, clone, usage) - SectionTemplate 모델 제거 → ItemSection.is_template 통합 - 페이지-섹션, 섹션-필드, 섹션-BOM 링크/언링크 API 14개 추가 - Swagger 문서 업데이트
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\ItemMaster;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ItemMaster\LinkEntityRequest;
|
||||
use App\Http\Requests\ItemMaster\ReorderRelationshipsRequest;
|
||||
use App\Services\ItemMaster\EntityRelationshipService;
|
||||
|
||||
/**
|
||||
* EntityRelationshipController - 엔티티 간 관계(링크) 관리 API
|
||||
*/
|
||||
class EntityRelationshipController extends Controller
|
||||
{
|
||||
public function __construct(private EntityRelationshipService $service) {}
|
||||
|
||||
/**
|
||||
* 페이지에 섹션 연결
|
||||
*
|
||||
* POST /api/v1/item-master/pages/{pageId}/link-section
|
||||
*/
|
||||
public function linkSectionToPage(int $pageId, LinkEntityRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($pageId, $request) {
|
||||
$data = $request->validated();
|
||||
|
||||
return $this->service->linkSectionToPage(
|
||||
$pageId,
|
||||
$data['child_id'],
|
||||
$data['order_no'] ?? 0
|
||||
);
|
||||
}, __('message.linked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지에서 섹션 연결 해제
|
||||
*
|
||||
* DELETE /api/v1/item-master/pages/{pageId}/unlink-section/{sectionId}
|
||||
*/
|
||||
public function unlinkSectionFromPage(int $pageId, int $sectionId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($pageId, $sectionId) {
|
||||
$this->service->unlinkSectionFromPage($pageId, $sectionId);
|
||||
|
||||
return 'success';
|
||||
}, __('message.unlinked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지에 필드 직접 연결
|
||||
*
|
||||
* POST /api/v1/item-master/pages/{pageId}/link-field
|
||||
*/
|
||||
public function linkFieldToPage(int $pageId, LinkEntityRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($pageId, $request) {
|
||||
$data = $request->validated();
|
||||
|
||||
return $this->service->linkFieldToPage(
|
||||
$pageId,
|
||||
$data['child_id'],
|
||||
$data['order_no'] ?? 0
|
||||
);
|
||||
}, __('message.linked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지에서 필드 연결 해제
|
||||
*
|
||||
* DELETE /api/v1/item-master/pages/{pageId}/unlink-field/{fieldId}
|
||||
*/
|
||||
public function unlinkFieldFromPage(int $pageId, int $fieldId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($pageId, $fieldId) {
|
||||
$this->service->unlinkFieldFromPage($pageId, $fieldId);
|
||||
|
||||
return 'success';
|
||||
}, __('message.unlinked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션에 필드 연결
|
||||
*
|
||||
* POST /api/v1/item-master/sections/{sectionId}/link-field
|
||||
*/
|
||||
public function linkFieldToSection(int $sectionId, LinkEntityRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($sectionId, $request) {
|
||||
$data = $request->validated();
|
||||
|
||||
return $this->service->linkFieldToSection(
|
||||
$sectionId,
|
||||
$data['child_id'],
|
||||
$data['order_no'] ?? 0
|
||||
);
|
||||
}, __('message.linked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션에서 필드 연결 해제
|
||||
*
|
||||
* DELETE /api/v1/item-master/sections/{sectionId}/unlink-field/{fieldId}
|
||||
*/
|
||||
public function unlinkFieldFromSection(int $sectionId, int $fieldId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($sectionId, $fieldId) {
|
||||
$this->service->unlinkFieldFromSection($sectionId, $fieldId);
|
||||
|
||||
return 'success';
|
||||
}, __('message.unlinked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션에 BOM 항목 연결
|
||||
*
|
||||
* POST /api/v1/item-master/sections/{sectionId}/link-bom
|
||||
*/
|
||||
public function linkBomToSection(int $sectionId, LinkEntityRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($sectionId, $request) {
|
||||
$data = $request->validated();
|
||||
|
||||
return $this->service->linkBomToSection(
|
||||
$sectionId,
|
||||
$data['child_id'],
|
||||
$data['order_no'] ?? 0
|
||||
);
|
||||
}, __('message.linked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션에서 BOM 항목 연결 해제
|
||||
*
|
||||
* DELETE /api/v1/item-master/sections/{sectionId}/unlink-bom/{bomId}
|
||||
*/
|
||||
public function unlinkBomFromSection(int $sectionId, int $bomId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($sectionId, $bomId) {
|
||||
$this->service->unlinkBomFromSection($sectionId, $bomId);
|
||||
|
||||
return 'success';
|
||||
}, __('message.unlinked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지의 모든 관계 조회
|
||||
*
|
||||
* GET /api/v1/item-master/pages/{pageId}/relationships
|
||||
*/
|
||||
public function getPageRelationships(int $pageId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($pageId) {
|
||||
return $this->service->getPageRelationships($pageId);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 구조 조회 (섹션 + 직접 연결된 필드 + 중첩 구조)
|
||||
*
|
||||
* GET /api/v1/item-master/pages/{pageId}/structure
|
||||
*/
|
||||
public function getPageStructure(int $pageId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($pageId) {
|
||||
return $this->service->getPageStructure($pageId);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션의 자식 관계 조회
|
||||
*
|
||||
* GET /api/v1/item-master/sections/{sectionId}/relationships
|
||||
*/
|
||||
public function getSectionRelationships(int $sectionId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($sectionId) {
|
||||
return $this->service->getSectionChildRelationships($sectionId);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계 순서 변경
|
||||
*
|
||||
* POST /api/v1/item-master/relationships/reorder
|
||||
*/
|
||||
public function reorderRelationships(ReorderRelationshipsRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$data = $request->validated();
|
||||
$this->service->reorderRelationships(
|
||||
$data['parent_type'],
|
||||
$data['parent_id'],
|
||||
$data['ordered_items']
|
||||
);
|
||||
|
||||
return 'success';
|
||||
}, __('message.reordered'));
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\ItemMaster;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ItemMaster\IndependentBomItemStoreRequest;
|
||||
use App\Http\Requests\ItemMaster\ItemBomItemStoreRequest;
|
||||
use App\Http\Requests\ItemMaster\ItemBomItemUpdateRequest;
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Services\ItemMaster\ItemBomItemService;
|
||||
|
||||
class ItemBomItemController extends Controller
|
||||
@@ -15,7 +16,33 @@ public function __construct(
|
||||
) {}
|
||||
|
||||
/**
|
||||
* BOM 항목 생성
|
||||
* 독립 BOM 목록 조회
|
||||
*
|
||||
* GET /api/v1/item-master/bom-items
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->index();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 독립 BOM 생성 (섹션 연결 없음)
|
||||
*
|
||||
* POST /api/v1/item-master/bom-items
|
||||
*/
|
||||
public function storeIndependent(IndependentBomItemStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->storeIndependent($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 항목 생성 (섹션에 연결)
|
||||
*
|
||||
* POST /api/v1/item-master/sections/{sectionId}/bom-items
|
||||
*/
|
||||
public function store(int $sectionId, ItemBomItemStoreRequest $request)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ItemMaster\IndependentFieldStoreRequest;
|
||||
use App\Http\Requests\ItemMaster\ItemFieldStoreRequest;
|
||||
use App\Http\Requests\ItemMaster\ItemFieldUpdateRequest;
|
||||
use App\Http\Requests\ItemMaster\ReorderRequest;
|
||||
@@ -14,7 +15,55 @@ class ItemFieldController extends Controller
|
||||
public function __construct(private ItemFieldService $service) {}
|
||||
|
||||
/**
|
||||
* 필드 생성
|
||||
* 독립 필드 목록 조회
|
||||
*
|
||||
* GET /api/v1/item-master/fields
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->index();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 독립 필드 생성 (섹션 연결 없음)
|
||||
*
|
||||
* POST /api/v1/item-master/fields
|
||||
*/
|
||||
public function storeIndependent(IndependentFieldStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->storeIndependent($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 필드 복제
|
||||
*
|
||||
* POST /api/v1/item-master/fields/{id}/clone
|
||||
*/
|
||||
public function clone(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->clone($id);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 필드 사용처 조회
|
||||
*
|
||||
* GET /api/v1/item-master/fields/{id}/usage
|
||||
*/
|
||||
public function getUsage(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->getUsage($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 필드 생성 (섹션에 연결)
|
||||
*
|
||||
* POST /api/v1/item-master/sections/{sectionId}/fields
|
||||
*/
|
||||
|
||||
@@ -4,17 +4,71 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ItemMaster\IndependentSectionStoreRequest;
|
||||
use App\Http\Requests\ItemMaster\ItemSectionStoreRequest;
|
||||
use App\Http\Requests\ItemMaster\ItemSectionUpdateRequest;
|
||||
use App\Http\Requests\ItemMaster\ReorderRequest;
|
||||
use App\Services\ItemMaster\ItemSectionService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ItemSectionController extends Controller
|
||||
{
|
||||
public function __construct(private ItemSectionService $service) {}
|
||||
|
||||
/**
|
||||
* 섹션 생성
|
||||
* 독립 섹션 목록 조회
|
||||
*
|
||||
* GET /api/v1/item-master/sections
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$isTemplate = $request->has('is_template')
|
||||
? filter_var($request->query('is_template'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
|
||||
: null;
|
||||
|
||||
return $this->service->index($isTemplate);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 독립 섹션 생성 (페이지 연결 없음)
|
||||
*
|
||||
* POST /api/v1/item-master/sections
|
||||
*/
|
||||
public function storeIndependent(IndependentSectionStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->storeIndependent($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 복제
|
||||
*
|
||||
* POST /api/v1/item-master/sections/{id}/clone
|
||||
*/
|
||||
public function clone(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->clone($id);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 사용처 조회
|
||||
*
|
||||
* GET /api/v1/item-master/sections/{id}/usage
|
||||
*/
|
||||
public function getUsage(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->getUsage($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 생성 (페이지에 연결)
|
||||
*
|
||||
* POST /api/v1/item-master/pages/{pageId}/sections
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ItemMaster;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndependentBomItemStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'group_id' => 'nullable|integer',
|
||||
'item_code' => 'nullable|string|max:100',
|
||||
'item_name' => 'required|string|max:255',
|
||||
'quantity' => 'nullable|numeric|min:0',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'unit_price' => 'nullable|numeric|min:0',
|
||||
'total_price' => 'nullable|numeric|min:0',
|
||||
'spec' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ItemMaster;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndependentFieldStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'group_id' => 'nullable|integer',
|
||||
'field_name' => 'required|string|max:255',
|
||||
'field_type' => 'required|in:textbox,number,dropdown,checkbox,date,textarea',
|
||||
'is_required' => 'nullable|boolean',
|
||||
'default_value' => 'nullable|string',
|
||||
'placeholder' => 'nullable|string|max:255',
|
||||
'display_condition' => 'nullable|array',
|
||||
'validation_rules' => 'nullable|array',
|
||||
'options' => 'nullable|array',
|
||||
'properties' => 'nullable|array',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ItemMaster;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndependentSectionStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'group_id' => 'nullable|integer',
|
||||
'title' => 'required|string|max:255',
|
||||
'type' => 'required|in:fields,bom',
|
||||
'is_template' => 'nullable|boolean',
|
||||
'is_default' => 'nullable|boolean',
|
||||
'description' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/ItemMaster/LinkEntityRequest.php
Normal file
30
app/Http/Requests/ItemMaster/LinkEntityRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ItemMaster;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LinkEntityRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'child_id' => 'required|integer|min:1',
|
||||
'order_no' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'child_id.required' => __('validation.required', ['attribute' => '연결 대상 ID']),
|
||||
'child_id.integer' => __('validation.integer', ['attribute' => '연결 대상 ID']),
|
||||
'child_id.min' => __('validation.min.numeric', ['attribute' => '연결 대상 ID', 'min' => 1]),
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Http/Requests/ItemMaster/ReorderRelationshipsRequest.php
Normal file
35
app/Http/Requests/ItemMaster/ReorderRelationshipsRequest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ItemMaster;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReorderRelationshipsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parent_type' => 'required|string|in:page,section',
|
||||
'parent_id' => 'required|integer|min:1',
|
||||
'ordered_items' => 'required|array|min:1',
|
||||
'ordered_items.*.child_type' => 'required|string|in:section,field,bom',
|
||||
'ordered_items.*.child_id' => 'required|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'parent_type.required' => __('validation.required', ['attribute' => '부모 타입']),
|
||||
'parent_type.in' => __('validation.in', ['attribute' => '부모 타입']),
|
||||
'parent_id.required' => __('validation.required', ['attribute' => '부모 ID']),
|
||||
'ordered_items.required' => __('validation.required', ['attribute' => '정렬 항목']),
|
||||
'ordered_items.array' => __('validation.array', ['attribute' => '정렬 항목']),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user