refactor: 섹션 관리를 entity_relationships 참조 방식으로 전환
- SectionTemplateService: 독립 섹션 생성, page_id 있으면 링크 연결 - ItemMasterService: init API가 linkedSections 기반으로 조회 - SectionTemplateStoreRequest: page_id nullable로 변경 - Swagger: 스키마 업데이트 (sectionTemplates → sections)
This commit is contained in:
@@ -14,6 +14,7 @@ public function authorize(): bool
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'page_id' => 'nullable|integer|exists:item_pages,id',
|
||||||
'title' => 'required|string|max:255',
|
'title' => 'required|string|max:255',
|
||||||
'type' => 'required|in:fields,bom',
|
'type' => 'required|in:fields,bom',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Services\ItemMaster;
|
namespace App\Services\ItemMaster;
|
||||||
|
|
||||||
use App\Models\ItemMaster\CustomTab;
|
use App\Models\ItemMaster\CustomTab;
|
||||||
|
use App\Models\ItemMaster\EntityRelationship;
|
||||||
|
use App\Models\ItemMaster\ItemField;
|
||||||
use App\Models\ItemMaster\ItemMasterField;
|
use App\Models\ItemMaster\ItemMasterField;
|
||||||
use App\Models\ItemMaster\ItemPage;
|
use App\Models\ItemMaster\ItemPage;
|
||||||
use App\Models\ItemMaster\ItemSection;
|
use App\Models\ItemMaster\ItemSection;
|
||||||
@@ -14,8 +16,8 @@ class ItemMasterService extends Service
|
|||||||
/**
|
/**
|
||||||
* 초기화 데이터 로드
|
* 초기화 데이터 로드
|
||||||
*
|
*
|
||||||
* - pages (섹션/필드 중첩)
|
* - pages (linkedSections 기반 중첩)
|
||||||
* - sectionTemplates (is_template=true인 섹션)
|
* - sections (모든 독립 섹션)
|
||||||
* - masterFields
|
* - masterFields
|
||||||
* - customTabs (columnSetting 포함)
|
* - customTabs (columnSetting 포함)
|
||||||
* - unitOptions
|
* - unitOptions
|
||||||
@@ -24,44 +26,117 @@ public function init(): array
|
|||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
// 1. 페이지 (섹션 → 필드 중첩) - 템플릿 제외
|
// 1. 페이지 목록
|
||||||
$pages = ItemPage::with([
|
$pages = ItemPage::where('tenant_id', $tenantId)
|
||||||
'sections' => function ($query) {
|
|
||||||
$query->nonTemplates()->orderBy('order_no');
|
|
||||||
},
|
|
||||||
'sections.fields' => function ($query) {
|
|
||||||
$query->orderBy('order_no');
|
|
||||||
},
|
|
||||||
'sections.bomItems',
|
|
||||||
])
|
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('is_active', 1)
|
->where('is_active', 1)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// 2. 섹션 템플릿 (is_template=true인 섹션)
|
// 2. 페이지별 linkedSections 조회 (entity_relationships 기반)
|
||||||
$sectionTemplates = ItemSection::templates()
|
$pagesWithSections = $pages->map(function ($page) use ($tenantId) {
|
||||||
->where('tenant_id', $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'])
|
->with(['fields', 'bomItems'])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// 3. 마스터 필드
|
// 4. 마스터 필드
|
||||||
$masterFields = ItemMasterField::where('tenant_id', $tenantId)->get();
|
$masterFields = ItemMasterField::where('tenant_id', $tenantId)->get();
|
||||||
|
|
||||||
// 4. 커스텀 탭 (컬럼 설정 포함)
|
// 5. 커스텀 탭 (컬럼 설정 포함)
|
||||||
$customTabs = CustomTab::with('columnSetting')
|
$customTabs = CustomTab::with('columnSetting')
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->orderBy('order_no')
|
->orderBy('order_no')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// 5. 단위 옵션
|
// 6. 단위 옵션
|
||||||
$unitOptions = UnitOption::where('tenant_id', $tenantId)->get();
|
$unitOptions = UnitOption::where('tenant_id', $tenantId)->get();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pages' => $pages,
|
'pages' => $pagesWithSections,
|
||||||
'sectionTemplates' => $sectionTemplates,
|
'sections' => $sections,
|
||||||
'masterFields' => $masterFields,
|
'masterFields' => $masterFields,
|
||||||
'customTabs' => $customTabs,
|
'customTabs' => $customTabs,
|
||||||
'unitOptions' => $unitOptions,
|
'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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Services\ItemMaster;
|
namespace App\Services\ItemMaster;
|
||||||
|
|
||||||
|
use App\Models\ItemMaster\EntityRelationship;
|
||||||
|
use App\Models\ItemMaster\ItemPage;
|
||||||
use App\Models\ItemMaster\ItemSection;
|
use App\Models\ItemMaster\ItemSection;
|
||||||
use App\Services\Service;
|
use App\Services\Service;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
@@ -10,104 +12,145 @@
|
|||||||
/**
|
/**
|
||||||
* SectionTemplateService
|
* SectionTemplateService
|
||||||
*
|
*
|
||||||
* 섹션 템플릿 관리 서비스
|
* 섹션 관리 서비스 (참조 방식)
|
||||||
* 내부적으로 ItemSection (is_template=true) 사용
|
* - 섹션은 독립 엔티티로 생성
|
||||||
|
* - 페이지와는 entity_relationships로 링크 연결
|
||||||
*/
|
*/
|
||||||
class SectionTemplateService extends Service
|
class SectionTemplateService extends Service
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 섹션 템플릿 목록
|
* 섹션 목록 (독립 섹션 전체)
|
||||||
*/
|
*/
|
||||||
public function index(): Collection
|
public function index(): Collection
|
||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
return ItemSection::templates()
|
return ItemSection::where('tenant_id', $tenantId)
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->with(['fields', 'bomItems'])
|
->with(['fields', 'bomItems'])
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 섹션 템플릿 생성
|
* 독립 섹션 생성 (page_id가 있으면 링크도 연결)
|
||||||
|
*
|
||||||
|
* @param array $data ['page_id'(optional), 'title', 'type', 'description', 'is_default']
|
||||||
*/
|
*/
|
||||||
public function store(array $data): ItemSection
|
public function store(array $data): ItemSection
|
||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
$userId = $this->apiUserId();
|
$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,
|
'tenant_id' => $tenantId,
|
||||||
'group_id' => 1,
|
'group_id' => 1,
|
||||||
'page_id' => null,
|
'page_id' => null,
|
||||||
'title' => $data['title'],
|
'title' => $data['title'],
|
||||||
'type' => $data['type'],
|
'type' => $data['type'],
|
||||||
'order_no' => 0,
|
'order_no' => 0,
|
||||||
'is_template' => true,
|
'is_template' => false,
|
||||||
'is_default' => $data['is_default'] ?? false,
|
'is_default' => $data['is_default'] ?? false,
|
||||||
'description' => $data['description'] ?? null,
|
'description' => $data['description'] ?? null,
|
||||||
'created_by' => $userId,
|
'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
|
public function update(int $id, array $data): ItemSection
|
||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
$userId = $this->apiUserId();
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
$template = ItemSection::templates()
|
$section = ItemSection::where('tenant_id', $tenantId)
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('id', $id)
|
->where('id', $id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (! $template) {
|
if (! $section) {
|
||||||
throw new NotFoundHttpException(__('error.section_not_found'));
|
throw new NotFoundHttpException(__('error.section_not_found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$template->update([
|
$section->update([
|
||||||
'title' => $data['title'] ?? $template->title,
|
'title' => $data['title'] ?? $section->title,
|
||||||
'type' => $data['type'] ?? $template->type,
|
'type' => $data['type'] ?? $section->type,
|
||||||
'description' => $data['description'] ?? $template->description,
|
'description' => $data['description'] ?? $section->description,
|
||||||
'is_default' => $data['is_default'] ?? $template->is_default,
|
'is_default' => $data['is_default'] ?? $section->is_default,
|
||||||
'updated_by' => $userId,
|
'updated_by' => $userId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $template->fresh()->load(['fields', 'bomItems']);
|
return $section->fresh()->load(['fields', 'bomItems']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 섹션 템플릿 삭제
|
* 섹션 삭제 (Soft Delete) + 링크 해제
|
||||||
*/
|
*/
|
||||||
public function destroy(int $id): void
|
public function destroy(int $id): void
|
||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
$userId = $this->apiUserId();
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
$template = ItemSection::templates()
|
$section = ItemSection::where('tenant_id', $tenantId)
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('id', $id)
|
->where('id', $id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (! $template) {
|
if (! $section) {
|
||||||
throw new NotFoundHttpException(__('error.section_not_found'));
|
throw new NotFoundHttpException(__('error.section_not_found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$template->update(['deleted_by' => $userId]);
|
// 1. 모든 부모 링크 해제 (페이지-섹션 관계)
|
||||||
$template->delete();
|
EntityRelationship::where('tenant_id', $tenantId)
|
||||||
|
->where('child_type', EntityRelationship::TYPE_SECTION)
|
||||||
|
->where('child_id', $id)
|
||||||
|
->delete();
|
||||||
|
|
||||||
// 하위 필드/BOM도 Soft Delete
|
// 2. 모든 자식 링크 해제 (섹션-필드, 섹션-BOM 관계)
|
||||||
foreach ($template->fields as $field) {
|
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->update(['deleted_by' => $userId]);
|
||||||
$field->delete();
|
$field->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($template->bomItems as $bomItem) {
|
foreach ($section->bomItems as $bomItem) {
|
||||||
$bomItem->update(['deleted_by' => $userId]);
|
$bomItem->update(['deleted_by' => $userId]);
|
||||||
$bomItem->delete();
|
$bomItem->delete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,7 +330,8 @@
|
|||||||
* type="object",
|
* type="object",
|
||||||
* required={"title","type"},
|
* 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="type", type="string", enum={"fields","bom"}, example="fields"),
|
||||||
* @OA\Property(property="description", type="string", nullable=true, example="설명"),
|
* @OA\Property(property="description", type="string", nullable=true, example="설명"),
|
||||||
* @OA\Property(property="is_default", type="boolean", example=false)
|
* @OA\Property(property="is_default", type="boolean", example=false)
|
||||||
@@ -436,15 +437,17 @@
|
|||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="pages",
|
* property="pages",
|
||||||
* type="array",
|
* type="array",
|
||||||
|
* description="페이지 목록 (linkedSections 기반)",
|
||||||
*
|
*
|
||||||
* @OA\Items(ref="#/components/schemas/ItemPage")
|
* @OA\Items(ref="#/components/schemas/ItemPage")
|
||||||
* ),
|
* ),
|
||||||
*
|
*
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="sectionTemplates",
|
* property="sections",
|
||||||
* type="array",
|
* type="array",
|
||||||
|
* description="모든 독립 섹션 목록 (재사용 가능)",
|
||||||
*
|
*
|
||||||
* @OA\Items(ref="#/components/schemas/SectionTemplate")
|
* @OA\Items(ref="#/components/schemas/ItemSection")
|
||||||
* ),
|
* ),
|
||||||
*
|
*
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
|
|||||||
Reference in New Issue
Block a user