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:
2025-11-26 14:09:31 +09:00
parent 3fefb8ce26
commit bccfa19791
38 changed files with 5888 additions and 92 deletions

View File

@@ -35,10 +35,14 @@
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="page_id", type="integer", example=1),
* @OA\Property(property="group_id", type="integer", nullable=true, example=1),
* @OA\Property(property="page_id", type="integer", nullable=true, example=1),
* @OA\Property(property="title", type="string", example="제품 상세"),
* @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields"),
* @OA\Property(property="order_no", type="integer", example=0),
* @OA\Property(property="is_template", type="boolean", example=false, description="템플릿 여부"),
* @OA\Property(property="is_default", type="boolean", example=false, description="기본 템플릿 여부"),
* @OA\Property(property="description", type="string", nullable=true, example="섹션 설명"),
* @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"),
* @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00"),
* @OA\Property(
@@ -188,6 +192,73 @@
* )
*
* @OA\Schema(
* schema="IndependentSectionStoreRequest",
* type="object",
* required={"title","type"},
*
* @OA\Property(property="group_id", type="integer", nullable=true, example=1),
* @OA\Property(property="title", type="string", maxLength=255, example="독립 섹션"),
* @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields"),
* @OA\Property(property="is_template", type="boolean", example=false),
* @OA\Property(property="is_default", type="boolean", example=false),
* @OA\Property(property="description", type="string", nullable=true, example="섹션 설명")
* )
*
* @OA\Schema(
* schema="IndependentFieldStoreRequest",
* type="object",
* required={"field_name","field_type"},
*
* @OA\Property(property="group_id", type="integer", nullable=true, example=1),
* @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"),
* @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"),
* @OA\Property(property="is_required", type="boolean", example=false),
* @OA\Property(property="default_value", type="string", nullable=true, example=null),
* @OA\Property(property="placeholder", type="string", nullable=true, maxLength=255, example="입력하세요"),
* @OA\Property(property="display_condition", type="object", nullable=true, example=null),
* @OA\Property(property="validation_rules", type="object", nullable=true, example=null),
* @OA\Property(property="options", type="object", nullable=true, example=null),
* @OA\Property(property="properties", type="object", nullable=true, example=null)
* )
*
* @OA\Schema(
* schema="IndependentBomItemStoreRequest",
* type="object",
* required={"item_name"},
*
* @OA\Property(property="group_id", type="integer", nullable=true, example=1),
* @OA\Property(property="item_code", type="string", nullable=true, maxLength=100, example="ITEM001"),
* @OA\Property(property="item_name", type="string", maxLength=255, example="부품 A"),
* @OA\Property(property="quantity", type="number", format="float", example=1),
* @OA\Property(property="unit", type="string", nullable=true, maxLength=50, example="EA"),
* @OA\Property(property="unit_price", type="number", format="float", nullable=true, example=10000),
* @OA\Property(property="total_price", type="number", format="float", nullable=true, example=10000),
* @OA\Property(property="spec", type="string", nullable=true, example="규격"),
* @OA\Property(property="note", type="string", nullable=true, example="비고")
* )
*
* @OA\Schema(
* schema="SectionUsageResponse",
* type="object",
*
* @OA\Property(property="section_id", type="integer", example=1),
* @OA\Property(property="direct_page", type="object", nullable=true),
* @OA\Property(property="linked_pages", type="array", @OA\Items(type="object")),
* @OA\Property(property="total_usage_count", type="integer", example=2)
* )
*
* @OA\Schema(
* schema="FieldUsageResponse",
* type="object",
*
* @OA\Property(property="field_id", type="integer", example=1),
* @OA\Property(property="direct_section", type="object", nullable=true),
* @OA\Property(property="linked_sections", type="array", @OA\Items(type="object")),
* @OA\Property(property="linked_pages", type="array", @OA\Items(type="object")),
* @OA\Property(property="total_usage_count", type="integer", example=3)
* )
*
* @OA\Schema(
* schema="ItemSectionUpdateRequest",
* type="object",
*
@@ -507,11 +578,247 @@ public function updatePages() {}
*/
public function destroyPages() {}
/**
* @OA\Get(
* path="/api/v1/item-master/sections",
* tags={"ItemMaster"},
* summary="독립 섹션 목록 조회",
* description="페이지와 연결되지 않은 독립 섹션 목록을 조회합니다. is_template 파라미터로 템플릿 필터링이 가능합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="is_template", in="query", description="템플릿 여부 필터", @OA\Schema(type="boolean")),
*
* @OA\Response(response=200, description="조회 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ItemSection")))
* })
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function indexSections() {}
/**
* @OA\Post(
* path="/api/v1/item-master/sections",
* tags={"ItemMaster"},
* summary="독립 섹션 생성",
* description="페이지와 연결되지 않은 독립 섹션을 생성합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/IndependentSectionStoreRequest")),
*
* @OA\Response(response=200, description="생성 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemSection"))
* })
* ),
*
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function storeIndependentSection() {}
/**
* @OA\Post(
* path="/api/v1/item-master/sections/{id}/clone",
* tags={"ItemMaster"},
* summary="섹션 복제",
* description="기존 섹션을 복제하여 새 독립 섹션을 생성합니다. 하위 필드와 BOM 항목도 함께 복제됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="복제할 섹션 ID", @OA\Schema(type="integer")),
*
* @OA\Response(response=200, description="복제 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemSection"))
* })
* ),
*
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function cloneSection() {}
/**
* @OA\Get(
* path="/api/v1/item-master/sections/{id}/usage",
* tags={"ItemMaster"},
* summary="섹션 사용처 조회",
* description="섹션이 어떤 페이지에 연결되어 있는지 조회합니다. FK 기반 연결과 entity_relationships 기반 연결 모두 조회됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="섹션 ID", @OA\Schema(type="integer")),
*
* @OA\Response(response=200, description="조회 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/SectionUsageResponse"))
* })
* ),
*
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function getSectionUsage() {}
/**
* @OA\Get(
* path="/api/v1/item-master/fields",
* tags={"ItemMaster"},
* summary="독립 필드 목록 조회",
* description="섹션과 연결되지 않은 독립 필드 목록을 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Response(response=200, description="조회 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ItemField")))
* })
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function indexFields() {}
/**
* @OA\Post(
* path="/api/v1/item-master/fields",
* tags={"ItemMaster"},
* summary="독립 필드 생성",
* description="섹션과 연결되지 않은 독립 필드를 생성합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/IndependentFieldStoreRequest")),
*
* @OA\Response(response=200, description="생성 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemField"))
* })
* ),
*
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function storeIndependentField() {}
/**
* @OA\Post(
* path="/api/v1/item-master/fields/{id}/clone",
* tags={"ItemMaster"},
* summary="필드 복제",
* description="기존 필드를 복제하여 새 독립 필드를 생성합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="복제할 필드 ID", @OA\Schema(type="integer")),
*
* @OA\Response(response=200, description="복제 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemField"))
* })
* ),
*
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function cloneField() {}
/**
* @OA\Get(
* path="/api/v1/item-master/fields/{id}/usage",
* tags={"ItemMaster"},
* summary="필드 사용처 조회",
* description="필드가 어떤 섹션/페이지에 연결되어 있는지 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="필드 ID", @OA\Schema(type="integer")),
*
* @OA\Response(response=200, description="조회 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/FieldUsageResponse"))
* })
* ),
*
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function getFieldUsage() {}
/**
* @OA\Get(
* path="/api/v1/item-master/bom-items",
* tags={"ItemMaster"},
* summary="독립 BOM 목록 조회",
* description="섹션과 연결되지 않은 독립 BOM 항목 목록을 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Response(response=200, description="조회 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ItemBomItem")))
* })
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function indexBomItems() {}
/**
* @OA\Post(
* path="/api/v1/item-master/bom-items",
* tags={"ItemMaster"},
* summary="독립 BOM 생성",
* description="섹션과 연결되지 않은 독립 BOM 항목을 생성합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/IndependentBomItemStoreRequest")),
*
* @OA\Response(response=200, description="생성 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemBomItem"))
* })
* ),
*
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function storeIndependentBomItem() {}
/**
* @OA\Post(
* path="/api/v1/item-master/pages/{pageId}/sections",
* tags={"ItemMaster"},
* summary="섹션 생성",
* summary="섹션 생성 (페이지 연결)",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="pageId", in="path", required=true, @OA\Schema(type="integer")),