feat: 계좌관리 추가

This commit is contained in:
pro
2026-01-20 20:43:38 +09:00
parent 7246ac003f
commit 0ae3c5aa07
1533 changed files with 5791 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,316 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(
* name="Material",
* description="자재 관리(목록/등록/조회/수정/삭제)"
* )
*/
/**
* 자재/요청 스키마
*/
/**
* @OA\Schema(
* schema="MaterialAttribute",
* type="object",
* description="규격 정보 한 항목",
*
* @OA\Property(property="label", type="string", example="두께"),
* @OA\Property(property="value", type="string", example="10"),
* @OA\Property(property="unit", type="string", example="T")
* )
*
* @OA\Schema(
* schema="Material",
* type="object",
* description="자재 상세",
* required={"id","tenant_id","name","item_name","unit"},
*
* @OA\Property(property="id", type="integer", example=101),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
* @OA\Property(property="name", type="string", example="철판"),
* @OA\Property(property="item_name", type="string", example="철판 10T 150CM"),
* @OA\Property(property="specification", type="string", nullable=true, example="두께 10T, 길이 150CM"),
* @OA\Property(property="material_code", type="string", nullable=true, example=null),
* @OA\Property(property="unit", type="string", example="EA"),
* @OA\Property(property="is_inspection", type="string", enum={"Y","N"}, example="N"),
* @OA\Property(property="search_tag", type="string", nullable=true, example="철판, 판재, 금속"),
* @OA\Property(property="remarks", type="string", nullable=true, example="비고 메모"),
* @OA\Property(
* property="attributes",
* type="array",
* nullable=true,
*
* @OA\Items(ref="#/components/schemas/MaterialAttribute")
* ),
*
* @OA\Property(
* property="options",
* type="object",
* nullable=true,
* example={"manufacturer":"ACME","color":"SILVER"}
* ),
* @OA\Property(property="created_by", type="integer", example=12),
* @OA\Property(property="updated_by", type="integer", nullable=true, example=12),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-08-21 10:00:00"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-08-21 10:00:00")
* )
*
* @OA\Schema(
* schema="MaterialBrief",
* type="object",
* description="자재 요약",
* required={"id","name","item_name","unit"},
*
* @OA\Property(property="id", type="integer", example=101),
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
* @OA\Property(property="name", type="string", example="철판"),
* @OA\Property(property="item_name", type="string", example="철판 10T 150CM"),
* @OA\Property(property="unit", type="string", example="EA")
* )
*
* @OA\Schema(
* schema="MaterialList",
* type="array",
*
* @OA\Items(ref="#/components/schemas/MaterialBrief")
* )
*
* @OA\Schema(
* schema="MaterialIndexParams",
* type="object",
*
* @OA\Property(property="q", type="string", nullable=true, example="알루미늄"),
* @OA\Property(property="category", type="integer", nullable=true, example=3),
* @OA\Property(property="page", type="integer", nullable=true, example=1),
* @OA\Property(property="per_page", type="integer", nullable=true, example=20)
* )
*
* @OA\Schema(
* schema="MaterialCreateRequest",
* type="object",
* required={"name","unit"},
*
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
* @OA\Property(property="name", type="string", example="철판"),
* @OA\Property(property="unit", type="string", example="EA"),
* @OA\Property(property="is_inspection", type="string", enum={"Y","N"}, example="N"),
* @OA\Property(property="search_tag", type="string", nullable=true, example="철판, 판재, 금속"),
* @OA\Property(property="remarks", type="string", nullable=true, example="비고 메모"),
* @OA\Property(
* property="attributes",
* type="array",
* nullable=true,
*
* @OA\Items(ref="#/components/schemas/MaterialAttribute")
* ),
*
* @OA\Property(
* property="options",
* type="object",
* nullable=true,
* example={"manufacturer":"ACME","color":"SILVER"}
* ),
* @OA\Property(property="material_code", type="string", nullable=true, example=null),
* @OA\Property(property="specification", type="string", nullable=true, example=null)
* )
*
* @OA\Schema(
* schema="MaterialUpdateRequest",
* type="object",
*
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
* @OA\Property(property="name", type="string", nullable=true, example="철판(개선)"),
* @OA\Property(property="unit", type="string", nullable=true, example="KG"),
* @OA\Property(property="is_inspection", type="string", enum={"Y","N"}, nullable=true, example="Y"),
* @OA\Property(property="search_tag", type="string", nullable=true, example="철판, 금속"),
* @OA\Property(property="remarks", type="string", nullable=true, example="비고 변경"),
* @OA\Property(
* property="attributes",
* type="array",
* nullable=true,
*
* @OA\Items(ref="#/components/schemas/MaterialAttribute")
* ),
*
* @OA\Property(
* property="options",
* type="object",
* nullable=true,
* example={"manufacturer":"ACME","color":"BLACK"}
* ),
* @OA\Property(property="material_code", type="string", nullable=true, example="MAT-0002"),
* @OA\Property(property="specification", type="string", nullable=true, example="두께 12T, 길이 180CM")
* )
*/
class MaterialApi
{
/**
* @OA\Get(
* path="/api/v1/products/materials",
* summary="자재 목록 조회",
* description="자재 목록을 페이징으로 반환합니다. (q로 코드/이름/태그 검색, category로 분류 필터)",
* tags={"Material"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(ref="#/components/parameters/Page"),
* @OA\Parameter(ref="#/components/parameters/Size"),
* @OA\Parameter(name="q", in="query", required=false, @OA\Schema(type="string"), description="검색어(이름/코드/태그 등)"),
* @OA\Parameter(name="category", in="query", required=false, @OA\Schema(type="integer"), description="카테고리 ID"),
*
* @OA\Response(
* response=200, description="자재 목록 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(
* property="data",
* type="object",
*
* @OA\Property(property="current_page", type="integer", example=1),
* @OA\Property(property="per_page", type="integer", example=20),
* @OA\Property(property="total", type="integer", example=2),
* @OA\Property(property="data", ref="#/components/schemas/MaterialList")
* ))
* }
* )
* )
* )
*/
public function index() {}
/**
* @OA\Post(
* path="/api/v1/products/materials",
* summary="자재 등록",
* description="자재를 등록합니다. item_name/specification은 name+attributes를 기반으로 서버에서 자동 생성됩니다.",
* tags={"Material"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialCreateRequest")),
*
* @OA\Response(
* response=200, description="자재 등록 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material"))
* }
* )
* ),
*
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function store() {}
/**
* @OA\Get(
* path="/api/v1/products/materials/{id}",
* summary="자재 단건 조회",
* tags={"Material"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)),
*
* @OA\Response(
* response=200, description="자재 단건 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material"))
* }
* )
* ),
*
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function show() {}
/**
* @OA\Put(
* path="/api/v1/products/materials/{id}",
* summary="자재 수정(전체)",
* description="자재를 수정합니다. name/attributes 변경 시 item_name/specification이 서버에서 재계산됩니다.",
* tags={"Material"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)),
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialUpdateRequest")),
*
* @OA\Response(
* response=200, description="자재 수정 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material"))
* }
* )
* ),
*
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function updatePut() {}
/**
* @OA\Patch(
* path="/api/v1/products/materials/{id}",
* summary="자재 부분 수정",
* description="자재의 일부 필드를 수정합니다. name/attributes 변경 시 item_name/specification이 서버에서 재계산됩니다.",
* tags={"Material"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)),
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialUpdateRequest")),
*
* @OA\Response(
* response=200, description="자재 수정 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material"))
* }
* )
* ),
*
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function updatePatch() {}
/**
* @OA\Delete(
* path="/api/v1/products/materials/{id}",
* summary="자재 삭제",
* description="자재를 소프트 삭제합니다.",
* tags={"Material"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)),
*
* @OA\Response(response=200, description="자재 삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function destroy() {}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,780 @@
<?php
namespace App\Swagger\v1;
use OpenApi\Annotations as OA;
/**
* @OA\Tag(name="Product", description="제품 카테고리/검색(간단)")
* @OA\Tag(name="Products", description="제품/부품/서브어셈블리 CRUD")
* @OA\Tag(name="Products-BOM", description="제품 BOM (제품/자재 혼합) 관리")
*
* ========= 공용 스키마(이 파일에서 사용하는 것만 정의) =========
*
* 트리 노드 스키마 (재귀)
*
* @OA\Schema(
* schema="BomTreeNode",
* type="object",
* description="BOM 트리 한 노드(제품/자재 공통)",
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="type", type="string", example="PRODUCT", description="PRODUCT / MATERIAL 등"),
* @OA\Property(property="code", type="string", example="PRD-001"),
* @OA\Property(property="name", type="string", example="스크린 모듈 KS001"),
* @OA\Property(property="unit", type="string", nullable=true, example="SET"),
* @OA\Property(property="quantity", type="number", format="float", example=1),
* @OA\Property(property="sort_order", type="integer", example=0),
* @OA\Property(
* property="category",
* type="object",
* nullable=true,
* @OA\Property(property="id", type="integer", nullable=true, example=2),
* @OA\Property(property="name", type="string", nullable=true, example="본체")
* ),
* @OA\Property(
* property="children",
* type="array",
* description="자식 노드 목록(없으면 빈 배열)",
*
* @OA\Items(ref="#/components/schemas/BomTreeNode")
* )
* )
*
* BOM 카테고리 사용/추천 항목
*
* @OA\Schema(
* schema="BomCategoryStat",
* type="object",
*
* @OA\Property(property="category_id", type="integer", nullable=true, example=1),
* @OA\Property(property="category_name", type="string", example="기본"),
* @OA\Property(property="count", type="integer", example=5, description="해당 카테고리를 사용하는 BOM 항목 수(빈도)")
* )
*
* BOM 전체 교체 저장용 스키마
*
* @OA\Schema(
* schema="BomReplaceItem",
* type="object",
* required={"ref_type","ref_id","quantity"},
*
* @OA\Property(property="ref_type", type="string", enum={"MATERIAL","PRODUCT"}, example="MATERIAL", description="참조 타입"),
* @OA\Property(property="ref_id", type="integer", example=201, description="참조 ID (materials.id 또는 products.id)"),
* @OA\Property(property="quantity", type="number", format="float", example=2, description="수량(0 이상, 소수 허용)"),
* @OA\Property(property="sort_order", type="integer", nullable=true, example=0, description="정렬 순서(선택)")
* )
*
* @OA\Schema(
* schema="BomReplaceCategory",
* type="object",
* required={"items"},
*
* @OA\Property(property="id", type="integer", nullable=true, example=1, description="프론트 임시 카테고리 ID(선택)"),
* @OA\Property(property="name", type="string", nullable=true, example="기본", description="프론트 카테고리명(선택)"),
* @OA\Property(
* property="items",
* type="array",
*
* @OA\Items(ref="#/components/schemas/BomReplaceItem")
* )
* )
*
* @OA\Schema(
* schema="BomReplaceRequest",
* type="object",
* required={"categories"},
*
* @OA\Property(
* property="categories",
* type="array",
*
* @OA\Items(ref="#/components/schemas/BomReplaceCategory")
* ),
* example={
* "categories": {
* {
* "id": 1,
* "name": "기본",
* "items": {
* { "ref_type": "MATERIAL", "ref_id": 201, "quantity": 2, "sort_order": 0 },
* { "ref_type": "PRODUCT", "ref_id": 301, "quantity": 1 }
* }
* },
* {
* "id": 2,
* "name": "옵션",
* "items": {
* { "ref_type": "MATERIAL", "ref_id": 202, "quantity": 5 }
* }
* }
* }
* }
* )
*
* @OA\Schema(
* schema="BomReplaceResult",
* type="object",
*
* @OA\Property(property="deleted_count", type="integer", example=5, description="삭제된 기존 항목 수"),
* @OA\Property(property="inserted_count", type="integer", example=7, description="신규로 삽입된 항목 수")
* )
*/
class ProductApi
{
/**
* 카테고리 목록 조회 (기존)
*
* @OA\Get(
* path="/api/v1/product/category",
* summary="제품 카테고리 목록 조회",
* description="제품 카테고리(최상위: parent_id = null) 리스트를 반환합니다.",
* tags={"Products"},
* 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/ProductCategory")
* )
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function productCategoryIndex() {}
/**
* 제품 목록/검색
*
* @OA\Get(
* path="/api/v1/products",
* tags={"Products"},
* summary="제품 목록/검색",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(ref="#/components/parameters/Page"),
* @OA\Parameter(ref="#/components/parameters/Size"),
* @OA\Parameter(name="q", in="query", @OA\Schema(type="string"), description="코드/이름/설명 검색"),
* @OA\Parameter(name="category_id", in="query", @OA\Schema(type="integer")),
* @OA\Parameter(name="product_type", in="query", @OA\Schema(type="string", example="PRODUCT")),
* @OA\Parameter(name="active", in="query", @OA\Schema(type="integer", enum={0,1})),
*
* @OA\Response(
* response=200,
* description="조회 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ProductPagination"))
* })
* )
* )
*/
public function productsIndex() {}
/**
* 제품 생성
*
* @OA\Post(
* path="/api/v1/products",
* tags={"Products"},
* summary="제품 생성",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProductCreateRequest")),
*
* @OA\Response(
* response=200,
* description="생성 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Product"))
* })
* )
* )
*/
public function productsStore() {}
/**
* 제품 단건
*
* @OA\Get(
* path="/api/v1/products/{id}",
* tags={"Products"},
* summary="제품 단건 조회",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @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/Product"))
* })
* ),
*
* @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function productsShow() {}
/**
* 제품 수정
*
* @OA\Patch(
* path="/api/v1/products/{id}",
* tags={"Products"},
* summary="제품 수정",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProductUpdateRequest")),
*
* @OA\Response(
* response=200,
* description="수정 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Product"))
* })
* )
* )
*/
public function productsUpdate() {}
/**
* 제품 삭제(soft)
*
* @OA\Delete(
* path="/api/v1/products/{id}",
* tags={"Products"},
* summary="제품 삭제(soft)",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse"))
* )
*/
public function productsDestroy() {}
/**
* 제품 간편 검색(모달/드롭다운)
*
* @OA\Get(
* path="/api/v1/products/search",
* tags={"Products"},
* summary="제품 간편 검색",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="q", in="query", @OA\Schema(type="string")),
* @OA\Parameter(name="limit", in="query", @OA\Schema(type="integer"), description="기본 20"),
*
* @OA\Response(
* response=200,
* description="검색 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(type="object",
*
* @OA\Property(property="id", type="integer", example=101),
* @OA\Property(property="code", type="string", example="PRD-001"),
* @OA\Property(property="name", type="string", example="스크린 모듈 KS001"),
* @OA\Property(property="product_type", type="string", example="PRODUCT"),
* @OA\Property(property="category_id", type="integer", example=7),
* @OA\Property(property="is_active", type="integer", example=1)
* )
* )
* )
* })
* )
* )
*/
public function productsSearch() {}
/**
* 제품 활성/비활성 토글
*
* @OA\Post(
* path="/api/v1/products/{id}/toggle",
* tags={"Products"},
* summary="제품 활성/비활성 토글",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="토글 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="object",
* @OA\Property(property="id", type="integer", example=101),
* @OA\Property(property="is_active", type="integer", example=0)
* )
* )
* })
* )
* )
* )
*/
public function productsToggle() {}
/**
* BOM 항목 목록
*
* @OA\Get(
* path="/api/v1/products/{id}/bom/items",
* tags={"Products-BOM"},
* summary="BOM 항목 목록(제품+자재 병합)",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @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/BomItem"))
* )
* })
* )
* )
*/
public function bomItemsIndex() {}
/**
* BOM 대량 업서트
*
* @OA\Post(
* path="/api/v1/products/{id}/bom/items/bulk",
* tags={"Products-BOM"},
* summary="BOM 항목 대량 업서트",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomItemBulkUpsertRequest")),
*
* @OA\Response(
* response=200,
* description="업서트 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="object",
* @OA\Property(property="created", type="integer", example=2),
* @OA\Property(property="updated", type="integer", example=3)
* )
* )
* })
* )
* )
*/
public function bomItemsBulk() {}
/**
* BOM 단건 수정
*
* @OA\Patch(
* path="/api/v1/products/{id}/bom/items/{item}",
* tags={"Products-BOM"},
* summary="BOM 항목 단건 수정",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Parameter(name="item", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomItemUpdateRequest")),
*
* @OA\Response(
* response=200,
* description="수정 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/BomItem"))
* })
* )
* )
*/
public function bomItemsUpdate() {}
/**
* BOM 단건 삭제
*
* @OA\Delete(
* path="/api/v1/products/{id}/bom/items/{item}",
* tags={"Products-BOM"},
* summary="BOM 항목 단건 삭제",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Parameter(name="item", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse"))
* )
*/
public function bomItemsDestroy() {}
/**
* BOM 정렬 변경
*
* @OA\Post(
* path="/api/v1/products/{id}/bom/items/reorder",
* tags={"Products-BOM"},
* summary="BOM 정렬 변경",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomReorderRequest")),
*
* @OA\Response(response=200, description="저장 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse"))
* )
*/
public function bomItemsReorder() {}
/**
* BOM 요약(건수/합계 등)
*
* @OA\Get(
* path="/api/v1/products/{id}/bom/summary",
* tags={"Products-BOM"},
* summary="BOM 요약",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="요약 성공",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="object",
* @OA\Property(property="count", type="integer", example=5),
* @OA\Property(property="count_product", type="integer", example=2),
* @OA\Property(property="count_material", type="integer", example=3),
* @OA\Property(property="quantity_sum", type="string", example="7.0000")
* )
* )
* })
* )
* )
*/
public function bomSummary() {}
/**
* BOM 유효성 검사
*
* @OA\Get(
* path="/api/v1/products/{id}/bom/validate",
* tags={"Products-BOM"},
* summary="BOM 유효성 검사",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="검증 결과",
*
* @OA\JsonContent(allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="object",
* @OA\Property(property="valid", type="boolean", example=false),
* @OA\Property(property="errors", type="array",
*
* @OA\Items(type="object",
*
* @OA\Property(property="id", type="integer", example=11),
* @OA\Property(property="error", type="string", example="DUPLICATE_ITEM")
* )
* )
* )
* )
* })
* )
* )
*/
public function bomValidate() {}
/**
* BOM 전체 교체 저장
* 프론트에서 보낸 현재 BOM 상태로 기존 구성을 모두 삭제 후 재등록합니다.
*
* @OA\Post(
* path="/api/v1/products/{id}/bom",
* tags={"Products-BOM"},
* summary="BOM 구성 저장(구성 전체 교체)",
* description="기존 BOM을 모두 삭제하고, 전달된 categories/items 기준으로 다시 저장합니다.",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="상위(모델) 제품 ID",
*
* @OA\Schema(type="integer", example=123)
* ),
*
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomReplaceRequest")),
*
* @OA\Response(
* response=200,
* description="저장 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="message", type="string", example="BOM 항목이 저장되었습니다."),
* @OA\Property(property="data", ref="#/components/schemas/BomReplaceResult")
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function bomReplace() {}
/**
* 제품별 사용 중인 BOM 카테고리 목록
*
* @OA\Get(
* path="/api/v1/products/{id}/bom/categories",
* tags={"Products-BOM"},
* summary="해당 제품에서 사용 중인 BOM 카테고리 목록",
* description="product_components 테이블에서 해당 제품(parent_product_id)의 카테고리(id/name)를 집계하여 반환합니다.",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="상위(모델) 제품 ID",
*
* @OA\Schema(type="integer", example=123)
* ),
*
* @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/BomCategoryStat")
* )
* )
* },
* example={
* "success": true,
* "message": "조회 성공",
* "data": {
* {"category_id":1, "category_name":"기본", "count":5},
* {"category_id":2, "category_name":"옵션", "count":3}
* }
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function bomCategoriesForProduct() {}
/**
* 테넌트 전역 카테고리 추천(히스토리 기반)
*
* @OA\Get(
* path="/api/v1/products/bom/categories",
* tags={"Products-BOM"},
* summary="자주 사용된 BOM 카테고리 추천",
* description="테넌트 전체 product_components 데이터를 집계해 카테고리 사용 빈도가 높은 순으로 반환합니다. q로 부분 검색 가능합니다.",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="q",
* in="query",
* required=false,
* description="카테고리명 부분 검색",
*
* @OA\Schema(type="string", example="기")
* ),
*
* @OA\Parameter(
* name="limit",
* in="query",
* required=false,
* description="최대 항목 수(기본 20)",
*
* @OA\Schema(type="integer", example=20)
* ),
*
* @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/BomCategoryStat")
* )
* )
* },
* example={
* "success": true,
* "message": "조회 성공",
* "data": {
* {"category_id":1, "category_name":"기본", "count":127},
* {"category_id":2, "category_name":"옵션", "count":88},
* {"category_id":null, "category_name":"패키지", "count":12}
* }
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function bomCategoriesSuggest() {}
/**
* 제품 BOM 트리 조회
*
* @OA\Get(
* path="/api/v1/products/{id}/bom/tree",
* tags={"Products-BOM"},
* summary="제품 BOM 트리 조회(재귀)",
* description="특정 제품의 하위 구성(제품/자재)을 재귀적으로 트리 형태로 반환합니다. depth로 최대 깊이를 제한합니다(기본 10).",
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", minimum=1), description="제품 ID"),
* @OA\Parameter(
* name="depth",
* in="query",
* required=false,
* description="재귀 깊이(루트=0). 기본 10",
*
* @OA\Schema(type="integer", minimum=0, maximum=50, default=10, example=5)
* ),
*
* @OA\Response(
* response=200,
* description="조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(
* property="product",
* type="object",
* @OA\Property(property="id", type="integer"),
* @OA\Property(property="code", type="string"),
* @OA\Property(property="name", type="string"),
* @OA\Property(property="unit", type="string", nullable=true),
* @OA\Property(property="category_id", type="integer", nullable=true),
* @OA\Property(property="product_type", type="string")
* ),
* @OA\Property(property="tree", ref="#/components/schemas/BomTreeNode")
* )
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=404, description="대상 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function bomTree() {}
}

Binary file not shown.

View File

@@ -0,0 +1,178 @@
<?php
namespace App\Swagger\v1;
/**
* 스키마 컨테이너(어노테이션 스캔 용)
*/
class ProductExtraSchemas
{
/**
* 제품 카테고리 스키마
*
* @OA\Schema(
* schema="ProductCategory",
* type="object",
* required={"id","code_group","code","name","is_active","sort_order"},
*
* @OA\Property(property="id", type="integer", example=4),
* @OA\Property(property="code_group", type="string", example="category"),
* @OA\Property(property="code", type="string", example="BP"),
* @OA\Property(property="name", type="string", example="절곡판"),
* @OA\Property(property="parent_id", type="integer", nullable=true, example=null),
* @OA\Property(
* property="attributes", type="object", nullable=true,
* example={"color":"blue","flags":{"screen":true}}
* ),
* @OA\Property(property="description", type="string", nullable=true, example="절곡판"),
* @OA\Property(property="is_active", type="integer", example=1),
* @OA\Property(property="sort_order", type="integer", example=10),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-07-23T09:00:00+09:00"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-07-23T09:00:00+09:00")
* )
*/
public function _category() {}
/**
* 제품 관련 스키마들
*
* @OA\Schema(
* schema="Product",
* type="object",
* required={"id","tenant_id","code","name","category_id","product_type","is_active"},
*
* @OA\Property(property="id", type="integer", example=101),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="code", type="string", example="PRD-001"),
* @OA\Property(property="name", type="string", example="스크린 모듈 KS001"),
* @OA\Property(property="category_id", type="integer", example=7),
* @OA\Property(property="product_type", type="string", example="PRODUCT"),
* @OA\Property(property="attributes", type="object", nullable=true, example={"color":"black","size":"L"}),
* @OA\Property(property="description", type="string", nullable=true, example="고객사 스펙"),
* @OA\Property(property="is_sellable", type="integer", example=1),
* @OA\Property(property="is_purchasable", type="integer", example=0),
* @OA\Property(property="is_producible", type="integer", example=1),
* @OA\Property(property="is_active", type="integer", example=1),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-08-25 10:00:00"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-08-25 11:00:00")
* )
*
* @OA\Schema(
* schema="ProductPagination",
* type="object",
* description="라라벨 LengthAwarePaginator 구조",
*
* @OA\Property(property="current_page", type="integer", example=1),
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(ref="#/components/schemas/Product")
* ),
*
* @OA\Property(property="per_page", type="integer", example=20),
* @OA\Property(property="total", type="integer", example=123)
* )
*
* @OA\Schema(
* schema="ProductCreateRequest",
* type="object",
* required={"code","name","category_id","product_type"},
*
* @OA\Property(property="code", type="string", example="PRD-001"),
* @OA\Property(property="name", type="string", example="스크린 모듈 KS001"),
* @OA\Property(property="category_id", type="integer", example=7),
* @OA\Property(property="product_type", type="string", example="PRODUCT"),
* @OA\Property(property="attributes", type="object", nullable=true, example={"color":"black"}),
* @OA\Property(property="description", type="string", nullable=true, example=null),
* @OA\Property(property="is_sellable", type="integer", example=1),
* @OA\Property(property="is_purchasable", type="integer", example=0),
* @OA\Property(property="is_producible", type="integer", example=1),
* @OA\Property(property="is_active", type="integer", example=1)
* )
*
* @OA\Schema(
* schema="ProductUpdateRequest",
* type="object",
*
* @OA\Property(property="code", type="string", example="PRD-001"),
* @OA\Property(property="name", type="string", example="스크린 모듈 KS001"),
* @OA\Property(property="category_id", type="integer", example=7),
* @OA\Property(property="product_type", type="string", example="PART"),
* @OA\Property(property="attributes", type="object", nullable=true, example={"size":"XL"}),
* @OA\Property(property="description", type="string", nullable=true, example="사양 변경"),
* @OA\Property(property="is_sellable", type="integer", example=1),
* @OA\Property(property="is_purchasable", type="integer", example=0),
* @OA\Property(property="is_producible", type="integer", example=1),
* @OA\Property(property="is_active", type="integer", example=1)
* )
*/
public function _product() {}
/**
* BOM 스키마들
*
* @OA\Schema(
* schema="BomItem",
* type="object",
* required={"id","ref_type","ref_id","quantity","sort_order"},
*
* @OA\Property(property="id", type="integer", example=11),
* @OA\Property(property="ref_type", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"),
* @OA\Property(property="ref_id", type="integer", example=3),
* @OA\Property(property="code", type="string", nullable=true, example="PRD-003"),
* @OA\Property(property="name", type="string", nullable=true, example="모듈A"),
* @OA\Property(property="unit", type="string", nullable=true, example="EA"),
* @OA\Property(property="quantity", type="number", format="float", example=2.0000),
* @OA\Property(property="sort_order", type="integer", example=1),
* @OA\Property(property="is_default", type="integer", example=1)
* )
*
* @OA\Schema(
* schema="BomItemBulkUpsertRequest",
* type="object",
* required={"items"},
*
* @OA\Property(
* property="items",
* type="array",
*
* @OA\Items(
* type="object",
*
* @OA\Property(property="id", type="integer", nullable=true, example=null),
* @OA\Property(property="ref_type", type="string", enum={"PRODUCT","MATERIAL"}, example="MATERIAL"),
* @OA\Property(property="ref_id", type="integer", example=5),
* @OA\Property(property="quantity", type="number", format="float", example=4.0000),
* @OA\Property(property="sort_order", type="integer", example=2),
* @OA\Property(property="is_default", type="integer", example=0)
* )
* )
* )
*
* @OA\Schema(
* schema="BomItemUpdateRequest",
* type="object",
*
* @OA\Property(property="ref_type", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"),
* @OA\Property(property="ref_id", type="integer", example=9),
* @OA\Property(property="quantity", type="number", format="float", example=1.5000),
* @OA\Property(property="sort_order", type="integer", example=3),
* @OA\Property(property="is_default", type="integer", example=1)
* )
*
* @OA\Schema(
* schema="BomReorderRequest",
* type="array",
*
* @OA\Items(
* type="object",
* required={"id","sort_order"},
*
* @OA\Property(property="id", type="integer", example=11),
* @OA\Property(property="sort_order", type="integer", example=1)
* )
* )
*/
public function _bom() {}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.