diff --git a/app/Http/Requests/ItemMaster/SectionTemplateStoreRequest.php b/app/Http/Requests/ItemMaster/SectionTemplateStoreRequest.php index 83a071c..727d697 100644 --- a/app/Http/Requests/ItemMaster/SectionTemplateStoreRequest.php +++ b/app/Http/Requests/ItemMaster/SectionTemplateStoreRequest.php @@ -14,6 +14,7 @@ public function authorize(): bool public function rules(): array { return [ + 'page_id' => 'nullable|integer|exists:item_pages,id', 'title' => 'required|string|max:255', 'type' => 'required|in:fields,bom', 'description' => 'nullable|string', diff --git a/app/Services/ItemMaster/ItemMasterService.php b/app/Services/ItemMaster/ItemMasterService.php index 10c2aac..a9d78cd 100644 --- a/app/Services/ItemMaster/ItemMasterService.php +++ b/app/Services/ItemMaster/ItemMasterService.php @@ -3,6 +3,8 @@ namespace App\Services\ItemMaster; use App\Models\ItemMaster\CustomTab; +use App\Models\ItemMaster\EntityRelationship; +use App\Models\ItemMaster\ItemField; use App\Models\ItemMaster\ItemMasterField; use App\Models\ItemMaster\ItemPage; use App\Models\ItemMaster\ItemSection; @@ -14,8 +16,8 @@ class ItemMasterService extends Service /** * 초기화 데이터 로드 * - * - pages (섹션/필드 중첩) - * - sectionTemplates (is_template=true인 섹션) + * - pages (linkedSections 기반 중첩) + * - sections (모든 독립 섹션) * - masterFields * - customTabs (columnSetting 포함) * - unitOptions @@ -24,44 +26,117 @@ public function init(): array { $tenantId = $this->tenantId(); - // 1. 페이지 (섹션 → 필드 중첩) - 템플릿 제외 - $pages = ItemPage::with([ - 'sections' => function ($query) { - $query->nonTemplates()->orderBy('order_no'); - }, - 'sections.fields' => function ($query) { - $query->orderBy('order_no'); - }, - 'sections.bomItems', - ]) - ->where('tenant_id', $tenantId) + // 1. 페이지 목록 + $pages = ItemPage::where('tenant_id', $tenantId) ->where('is_active', 1) ->get(); - // 2. 섹션 템플릿 (is_template=true인 섹션) - $sectionTemplates = ItemSection::templates() - ->where('tenant_id', $tenantId) + // 2. 페이지별 linkedSections 조회 (entity_relationships 기반) + $pagesWithSections = $pages->map(function ($page) use ($tenantId) { + $linkedSections = $this->getLinkedSections($tenantId, $page->id); + + return [ + 'id' => $page->id, + 'tenant_id' => $page->tenant_id, + 'group_id' => $page->group_id, + 'page_name' => $page->page_name, + 'item_type' => $page->item_type, + 'absolute_path' => $page->absolute_path, + 'is_active' => $page->is_active, + 'created_by' => $page->created_by, + 'updated_by' => $page->updated_by, + 'created_at' => $page->created_at, + 'updated_at' => $page->updated_at, + 'sections' => $linkedSections, + ]; + }); + + // 3. 모든 독립 섹션 (재사용 가능 목록) + $sections = ItemSection::where('tenant_id', $tenantId) ->with(['fields', 'bomItems']) + ->orderBy('created_at', 'desc') ->get(); - // 3. 마스터 필드 + // 4. 마스터 필드 $masterFields = ItemMasterField::where('tenant_id', $tenantId)->get(); - // 4. 커스텀 탭 (컬럼 설정 포함) + // 5. 커스텀 탭 (컬럼 설정 포함) $customTabs = CustomTab::with('columnSetting') ->where('tenant_id', $tenantId) ->orderBy('order_no') ->get(); - // 5. 단위 옵션 + // 6. 단위 옵션 $unitOptions = UnitOption::where('tenant_id', $tenantId)->get(); return [ - 'pages' => $pages, - 'sectionTemplates' => $sectionTemplates, + 'pages' => $pagesWithSections, + 'sections' => $sections, 'masterFields' => $masterFields, 'customTabs' => $customTabs, 'unitOptions' => $unitOptions, ]; } + + /** + * 페이지에 연결된 섹션 조회 (entity_relationships 기반) + */ + private function getLinkedSections(int $tenantId, int $pageId): array + { + // 페이지-섹션 관계 조회 + $relationships = EntityRelationship::where('tenant_id', $tenantId) + ->where('parent_type', EntityRelationship::TYPE_PAGE) + ->where('parent_id', $pageId) + ->where('child_type', EntityRelationship::TYPE_SECTION) + ->orderBy('order_no') + ->get(); + + $sections = []; + foreach ($relationships as $rel) { + $section = ItemSection::with(['fields', 'bomItems']) + ->find($rel->child_id); + + if ($section) { + // 섹션에 연결된 필드 (entity_relationships 기반) + $linkedFields = $this->getLinkedFields($tenantId, $section->id); + + $sectionData = $section->toArray(); + $sectionData['order_no'] = $rel->order_no; + + // FK 기반 필드 + 링크 기반 필드 병합 + if (! empty($linkedFields)) { + $sectionData['fields'] = $linkedFields; + } + + $sections[] = $sectionData; + } + } + + return $sections; + } + + /** + * 섹션에 연결된 필드 조회 (entity_relationships 기반) + */ + private function getLinkedFields(int $tenantId, int $sectionId): array + { + $relationships = EntityRelationship::where('tenant_id', $tenantId) + ->where('parent_type', EntityRelationship::TYPE_SECTION) + ->where('parent_id', $sectionId) + ->where('child_type', EntityRelationship::TYPE_FIELD) + ->orderBy('order_no') + ->get(); + + $fields = []; + foreach ($relationships as $rel) { + $field = ItemField::find($rel->child_id); + if ($field) { + $fieldData = $field->toArray(); + $fieldData['order_no'] = $rel->order_no; + $fields[] = $fieldData; + } + } + + return $fields; + } } diff --git a/app/Services/ItemMaster/SectionTemplateService.php b/app/Services/ItemMaster/SectionTemplateService.php index f879282..4712ab5 100644 --- a/app/Services/ItemMaster/SectionTemplateService.php +++ b/app/Services/ItemMaster/SectionTemplateService.php @@ -2,6 +2,8 @@ namespace App\Services\ItemMaster; +use App\Models\ItemMaster\EntityRelationship; +use App\Models\ItemMaster\ItemPage; use App\Models\ItemMaster\ItemSection; use App\Services\Service; use Illuminate\Database\Eloquent\Collection; @@ -10,104 +12,145 @@ /** * SectionTemplateService * - * 섹션 템플릿 관리 서비스 - * 내부적으로 ItemSection (is_template=true) 사용 + * 섹션 관리 서비스 (참조 방식) + * - 섹션은 독립 엔티티로 생성 + * - 페이지와는 entity_relationships로 링크 연결 */ class SectionTemplateService extends Service { /** - * 섹션 템플릿 목록 + * 섹션 목록 (독립 섹션 전체) */ public function index(): Collection { $tenantId = $this->tenantId(); - return ItemSection::templates() - ->where('tenant_id', $tenantId) + return ItemSection::where('tenant_id', $tenantId) ->with(['fields', 'bomItems']) ->orderBy('created_at', 'desc') ->get(); } /** - * 섹션 템플릿 생성 + * 독립 섹션 생성 (page_id가 있으면 링크도 연결) + * + * @param array $data ['page_id'(optional), 'title', 'type', 'description', 'is_default'] */ public function store(array $data): ItemSection { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); + $pageId = $data['page_id'] ?? null; - $template = ItemSection::create([ + // page_id가 있으면 페이지 존재 확인 + if ($pageId) { + $page = ItemPage::where('tenant_id', $tenantId)->find($pageId); + if (! $page) { + throw new NotFoundHttpException(__('error.page_not_found')); + } + } + + // 1. 독립 섹션 생성 (page_id=null) + $section = ItemSection::create([ 'tenant_id' => $tenantId, 'group_id' => 1, 'page_id' => null, 'title' => $data['title'], 'type' => $data['type'], 'order_no' => 0, - 'is_template' => true, + 'is_template' => false, 'is_default' => $data['is_default'] ?? false, 'description' => $data['description'] ?? null, 'created_by' => $userId, ]); - return $template->load(['fields', 'bomItems']); + // 2. page_id가 있으면 링크 연결 + if ($pageId) { + $maxOrderNo = EntityRelationship::where('tenant_id', $tenantId) + ->where('parent_type', EntityRelationship::TYPE_PAGE) + ->where('parent_id', $pageId) + ->where('child_type', EntityRelationship::TYPE_SECTION) + ->max('order_no') ?? -1; + + EntityRelationship::link( + $tenantId, + EntityRelationship::TYPE_PAGE, + $pageId, + EntityRelationship::TYPE_SECTION, + $section->id, + $maxOrderNo + 1 + ); + } + + return $section->load(['fields', 'bomItems']); } /** - * 섹션 템플릿 수정 + * 섹션 수정 */ public function update(int $id, array $data): ItemSection { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); - $template = ItemSection::templates() - ->where('tenant_id', $tenantId) + $section = ItemSection::where('tenant_id', $tenantId) ->where('id', $id) ->first(); - if (! $template) { + if (! $section) { throw new NotFoundHttpException(__('error.section_not_found')); } - $template->update([ - 'title' => $data['title'] ?? $template->title, - 'type' => $data['type'] ?? $template->type, - 'description' => $data['description'] ?? $template->description, - 'is_default' => $data['is_default'] ?? $template->is_default, + $section->update([ + 'title' => $data['title'] ?? $section->title, + 'type' => $data['type'] ?? $section->type, + 'description' => $data['description'] ?? $section->description, + 'is_default' => $data['is_default'] ?? $section->is_default, 'updated_by' => $userId, ]); - return $template->fresh()->load(['fields', 'bomItems']); + return $section->fresh()->load(['fields', 'bomItems']); } /** - * 섹션 템플릿 삭제 + * 섹션 삭제 (Soft Delete) + 링크 해제 */ public function destroy(int $id): void { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); - $template = ItemSection::templates() - ->where('tenant_id', $tenantId) + $section = ItemSection::where('tenant_id', $tenantId) ->where('id', $id) ->first(); - if (! $template) { + if (! $section) { throw new NotFoundHttpException(__('error.section_not_found')); } - $template->update(['deleted_by' => $userId]); - $template->delete(); + // 1. 모든 부모 링크 해제 (페이지-섹션 관계) + EntityRelationship::where('tenant_id', $tenantId) + ->where('child_type', EntityRelationship::TYPE_SECTION) + ->where('child_id', $id) + ->delete(); - // 하위 필드/BOM도 Soft Delete - foreach ($template->fields as $field) { + // 2. 모든 자식 링크 해제 (섹션-필드, 섹션-BOM 관계) + EntityRelationship::where('tenant_id', $tenantId) + ->where('parent_type', EntityRelationship::TYPE_SECTION) + ->where('parent_id', $id) + ->delete(); + + // 3. 섹션 Soft Delete + $section->update(['deleted_by' => $userId]); + $section->delete(); + + // 4. 하위 필드/BOM도 Soft Delete + foreach ($section->fields as $field) { $field->update(['deleted_by' => $userId]); $field->delete(); } - foreach ($template->bomItems as $bomItem) { + foreach ($section->bomItems as $bomItem) { $bomItem->update(['deleted_by' => $userId]); $bomItem->delete(); } diff --git a/app/Swagger/v1/ItemMasterApi.php b/app/Swagger/v1/ItemMasterApi.php index 9147319..7567196 100644 --- a/app/Swagger/v1/ItemMasterApi.php +++ b/app/Swagger/v1/ItemMasterApi.php @@ -330,7 +330,8 @@ * type="object", * required={"title","type"}, * - * @OA\Property(property="title", type="string", maxLength=255, example="기본 템플릿"), + * @OA\Property(property="page_id", type="integer", nullable=true, example=1, description="연결할 페이지 ID (선택, 있으면 즉시 링크 연결)"), + * @OA\Property(property="title", type="string", maxLength=255, example="기본 섹션"), * @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields"), * @OA\Property(property="description", type="string", nullable=true, example="설명"), * @OA\Property(property="is_default", type="boolean", example=false) @@ -436,15 +437,17 @@ * @OA\Property( * property="pages", * type="array", + * description="페이지 목록 (linkedSections 기반)", * * @OA\Items(ref="#/components/schemas/ItemPage") * ), * * @OA\Property( - * property="sectionTemplates", + * property="sections", * type="array", + * description="모든 독립 섹션 목록 (재사용 가능)", * - * @OA\Items(ref="#/components/schemas/SectionTemplate") + * @OA\Items(ref="#/components/schemas/ItemSection") * ), * * @OA\Property(