- Price, PriceRevision 모델 추가 (PriceHistory 대체) - PricingService: CRUD, 원가 조회, 확정 기능 - PricingController: statusCode 파라미터로 201 반환 지원 - NotFoundHttpException(404) 적용 (존재하지 않는 리소스) - FormRequest 분리 (Store, Update, Index, Cost, ByItems) - Swagger 문서 업데이트 - ApiResponse::handle()에 statusCode 옵션 추가 - prices/price_revisions 마이그레이션 및 데이터 이관
400 lines
19 KiB
PHP
400 lines
19 KiB
PHP
<?php
|
|
|
|
namespace App\Swagger\v1;
|
|
|
|
/**
|
|
* @OA\Tag(name="Pricing", description="단가 관리")
|
|
*
|
|
* ========= 스키마 정의 =========
|
|
*
|
|
* @OA\Schema(
|
|
* schema="Price",
|
|
* type="object",
|
|
* description="단가 마스터",
|
|
*
|
|
* @OA\Property(property="id", type="integer", example=1),
|
|
* @OA\Property(property="tenant_id", type="integer", example=1),
|
|
* @OA\Property(property="item_type_code", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT", description="품목 유형"),
|
|
* @OA\Property(property="item_id", type="integer", example=10, description="품목 ID"),
|
|
* @OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객그룹 ID (NULL=기본가)"),
|
|
* @OA\Property(property="purchase_price", type="number", format="decimal", nullable=true, example=10000.00, description="매입단가 (표준원가)"),
|
|
* @OA\Property(property="processing_cost", type="number", format="decimal", nullable=true, example=2000.00, description="가공비"),
|
|
* @OA\Property(property="loss_rate", type="number", format="decimal", nullable=true, example=5.00, description="LOSS율 (%)"),
|
|
* @OA\Property(property="margin_rate", type="number", format="decimal", nullable=true, example=25.00, description="마진율 (%)"),
|
|
* @OA\Property(property="sales_price", type="number", format="decimal", nullable=true, example=15800.00, description="판매단가"),
|
|
* @OA\Property(property="rounding_rule", type="string", enum={"round","ceil","floor"}, example="round", description="반올림 규칙"),
|
|
* @OA\Property(property="rounding_unit", type="integer", example=100, description="반올림 단위 (1,10,100,1000)"),
|
|
* @OA\Property(property="supplier", type="string", nullable=true, example="ABC공급", description="공급업체"),
|
|
* @OA\Property(property="effective_from", type="string", format="date", example="2025-01-01", description="적용 시작일"),
|
|
* @OA\Property(property="effective_to", type="string", format="date", nullable=true, example="2025-12-31", description="적용 종료일"),
|
|
* @OA\Property(property="note", type="string", nullable=true, example="2025년 상반기 가격", description="비고"),
|
|
* @OA\Property(property="status", type="string", enum={"draft","active","inactive","finalized"}, example="active", description="상태"),
|
|
* @OA\Property(property="is_final", type="boolean", example=false, description="최종 확정 여부"),
|
|
* @OA\Property(property="finalized_at", type="string", format="datetime", nullable=true, example="2025-01-15 10:30:00", description="확정 일시"),
|
|
* @OA\Property(property="finalized_by", type="integer", nullable=true, example=1, description="확정자 ID"),
|
|
* @OA\Property(property="created_at", type="string", example="2025-01-01 12:00:00"),
|
|
* @OA\Property(property="updated_at", type="string", example="2025-01-01 12:00:00"),
|
|
* @OA\Property(
|
|
* property="client_group",
|
|
* type="object",
|
|
* nullable=true,
|
|
* @OA\Property(property="id", type="integer", example=1),
|
|
* @OA\Property(property="name", type="string", example="VIP 고객")
|
|
* )
|
|
* )
|
|
*
|
|
* @OA\Schema(
|
|
* schema="PricePagination",
|
|
* type="object",
|
|
*
|
|
* @OA\Property(property="current_page", type="integer", example=1),
|
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Price")),
|
|
* @OA\Property(property="first_page_url", type="string", example="/api/v1/pricing?page=1"),
|
|
* @OA\Property(property="from", type="integer", example=1),
|
|
* @OA\Property(property="last_page", type="integer", example=3),
|
|
* @OA\Property(property="last_page_url", type="string", example="/api/v1/pricing?page=3"),
|
|
* @OA\Property(property="links", type="array", @OA\Items(type="object",
|
|
* @OA\Property(property="url", type="string", nullable=true),
|
|
* @OA\Property(property="label", type="string"),
|
|
* @OA\Property(property="active", type="boolean")
|
|
* )),
|
|
* @OA\Property(property="next_page_url", type="string", nullable=true),
|
|
* @OA\Property(property="path", type="string", example="/api/v1/pricing"),
|
|
* @OA\Property(property="per_page", type="integer", example=20),
|
|
* @OA\Property(property="prev_page_url", type="string", nullable=true),
|
|
* @OA\Property(property="to", type="integer", example=20),
|
|
* @OA\Property(property="total", type="integer", example=50)
|
|
* )
|
|
*
|
|
* @OA\Schema(
|
|
* schema="PriceRevision",
|
|
* type="object",
|
|
* description="단가 변경 이력",
|
|
*
|
|
* @OA\Property(property="id", type="integer", example=1),
|
|
* @OA\Property(property="price_id", type="integer", example=1),
|
|
* @OA\Property(property="revision_number", type="integer", example=1, description="리비전 번호"),
|
|
* @OA\Property(property="changed_at", type="string", format="datetime", example="2025-01-01 12:00:00", description="변경 일시"),
|
|
* @OA\Property(property="changed_by", type="integer", example=1, description="변경자 ID"),
|
|
* @OA\Property(property="change_reason", type="string", nullable=true, example="2025년 단가 인상", description="변경 사유"),
|
|
* @OA\Property(property="before_snapshot", type="object", nullable=true, description="변경 전 데이터"),
|
|
* @OA\Property(property="after_snapshot", type="object", description="변경 후 데이터"),
|
|
* @OA\Property(
|
|
* property="changed_by_user",
|
|
* type="object",
|
|
* nullable=true,
|
|
* @OA\Property(property="id", type="integer", example=1),
|
|
* @OA\Property(property="name", type="string", example="홍길동")
|
|
* )
|
|
* )
|
|
*
|
|
* @OA\Schema(
|
|
* schema="PriceStoreRequest",
|
|
* type="object",
|
|
* required={"item_type_code","item_id","effective_from"},
|
|
*
|
|
* @OA\Property(property="item_type_code", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"),
|
|
* @OA\Property(property="item_id", type="integer", example=10),
|
|
* @OA\Property(property="client_group_id", type="integer", nullable=true, example=1),
|
|
* @OA\Property(property="purchase_price", type="number", nullable=true, example=10000.00),
|
|
* @OA\Property(property="processing_cost", type="number", nullable=true, example=2000.00),
|
|
* @OA\Property(property="loss_rate", type="number", nullable=true, example=5.00),
|
|
* @OA\Property(property="margin_rate", type="number", nullable=true, example=25.00),
|
|
* @OA\Property(property="sales_price", type="number", nullable=true, example=15800.00),
|
|
* @OA\Property(property="rounding_rule", type="string", enum={"round","ceil","floor"}, example="round"),
|
|
* @OA\Property(property="rounding_unit", type="integer", enum={1,10,100,1000}, example=100),
|
|
* @OA\Property(property="supplier", type="string", nullable=true, example="ABC공급"),
|
|
* @OA\Property(property="effective_from", type="string", format="date", example="2025-01-01"),
|
|
* @OA\Property(property="effective_to", type="string", format="date", nullable=true, example="2025-12-31"),
|
|
* @OA\Property(property="note", type="string", nullable=true, example="2025년 상반기 가격"),
|
|
* @OA\Property(property="status", type="string", enum={"draft","active","inactive"}, example="draft")
|
|
* )
|
|
*
|
|
* @OA\Schema(
|
|
* schema="PriceUpdateRequest",
|
|
* type="object",
|
|
*
|
|
* @OA\Property(property="item_type_code", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"),
|
|
* @OA\Property(property="item_id", type="integer", example=10),
|
|
* @OA\Property(property="client_group_id", type="integer", nullable=true, example=1),
|
|
* @OA\Property(property="purchase_price", type="number", nullable=true, example=10000.00),
|
|
* @OA\Property(property="processing_cost", type="number", nullable=true, example=2000.00),
|
|
* @OA\Property(property="loss_rate", type="number", nullable=true, example=5.00),
|
|
* @OA\Property(property="margin_rate", type="number", nullable=true, example=25.00),
|
|
* @OA\Property(property="sales_price", type="number", nullable=true, example=15800.00),
|
|
* @OA\Property(property="rounding_rule", type="string", enum={"round","ceil","floor"}, example="round"),
|
|
* @OA\Property(property="rounding_unit", type="integer", enum={1,10,100,1000}, example=100),
|
|
* @OA\Property(property="supplier", type="string", nullable=true, example="ABC공급"),
|
|
* @OA\Property(property="effective_from", type="string", format="date", example="2025-01-01"),
|
|
* @OA\Property(property="effective_to", type="string", format="date", nullable=true, example="2025-12-31"),
|
|
* @OA\Property(property="note", type="string", nullable=true, example="2025년 상반기 가격"),
|
|
* @OA\Property(property="status", type="string", enum={"draft","active","inactive"}, example="active"),
|
|
* @OA\Property(property="change_reason", type="string", nullable=true, example="단가 인상", description="변경 사유 (리비전 기록용)")
|
|
* )
|
|
*
|
|
* @OA\Schema(
|
|
* schema="PriceByItemsRequest",
|
|
* type="object",
|
|
* required={"items"},
|
|
*
|
|
* @OA\Property(
|
|
* property="items",
|
|
* type="array",
|
|
*
|
|
* @OA\Items(type="object",
|
|
*
|
|
* @OA\Property(property="item_type_code", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"),
|
|
* @OA\Property(property="item_id", type="integer", example=10)
|
|
* )
|
|
* ),
|
|
* @OA\Property(property="client_group_id", type="integer", nullable=true, example=1),
|
|
* @OA\Property(property="date", type="string", format="date", nullable=true, example="2025-01-15")
|
|
* )
|
|
*
|
|
* @OA\Schema(
|
|
* schema="PriceByItemsResult",
|
|
* type="array",
|
|
*
|
|
* @OA\Items(type="object",
|
|
*
|
|
* @OA\Property(property="item_type_code", type="string", example="PRODUCT"),
|
|
* @OA\Property(property="item_id", type="integer", example=10),
|
|
* @OA\Property(property="price", ref="#/components/schemas/Price", nullable=true),
|
|
* @OA\Property(property="has_price", type="boolean", example=true)
|
|
* )
|
|
* )
|
|
*
|
|
* @OA\Schema(
|
|
* schema="PriceCostResult",
|
|
* type="object",
|
|
*
|
|
* @OA\Property(property="item_type_code", type="string", example="MATERIAL"),
|
|
* @OA\Property(property="item_id", type="integer", example=123),
|
|
* @OA\Property(property="date", type="string", format="date", example="2025-01-15"),
|
|
* @OA\Property(property="cost_source", type="string", enum={"receipt","standard","not_found"}, example="receipt", description="원가 출처"),
|
|
* @OA\Property(property="purchase_price", type="number", nullable=true, example=10500.00),
|
|
* @OA\Property(property="receipt_id", type="integer", nullable=true, example=456, description="수입검사 ID (cost_source=receipt일 때)"),
|
|
* @OA\Property(property="receipt_date", type="string", format="date", nullable=true, example="2025-01-10"),
|
|
* @OA\Property(property="price_id", type="integer", nullable=true, example=null, description="단가 ID (cost_source=standard일 때)")
|
|
* )
|
|
*/
|
|
class PricingApi
|
|
{
|
|
/**
|
|
* @OA\Get(
|
|
* path="/api/v1/pricing",
|
|
* tags={"Pricing"},
|
|
* summary="단가 목록 조회",
|
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
|
*
|
|
* @OA\Parameter(name="q", in="query", description="검색어 (supplier, note)", @OA\Schema(type="string")),
|
|
* @OA\Parameter(name="item_type_code", in="query", @OA\Schema(type="string", enum={"PRODUCT","MATERIAL"})),
|
|
* @OA\Parameter(name="item_id", in="query", @OA\Schema(type="integer")),
|
|
* @OA\Parameter(name="client_group_id", in="query", description="고객그룹 ID (빈값/null=기본가만)", @OA\Schema(type="string")),
|
|
* @OA\Parameter(name="status", in="query", @OA\Schema(type="string", enum={"draft","active","inactive","finalized"})),
|
|
* @OA\Parameter(name="valid_at", in="query", description="특정 날짜에 유효한 단가만", @OA\Schema(type="string", format="date")),
|
|
* @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", example=20)),
|
|
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", example=1)),
|
|
*
|
|
* @OA\Response(response=200, description="조회 성공",
|
|
*
|
|
* @OA\JsonContent(allOf={
|
|
*
|
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
|
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PricePagination"))
|
|
* })
|
|
* ),
|
|
*
|
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
|
* )
|
|
*/
|
|
public function index() {}
|
|
|
|
/**
|
|
* @OA\Get(
|
|
* path="/api/v1/pricing/{id}",
|
|
* tags={"Pricing"},
|
|
* 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/Price"))
|
|
* })
|
|
* ),
|
|
*
|
|
* @OA\Response(response=404, description="미존재", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
|
* )
|
|
*/
|
|
public function show() {}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/api/v1/pricing",
|
|
* tags={"Pricing"},
|
|
* summary="단가 등록",
|
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
|
*
|
|
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PriceStoreRequest")),
|
|
*
|
|
* @OA\Response(response=200, description="등록 성공",
|
|
*
|
|
* @OA\JsonContent(allOf={
|
|
*
|
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
|
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Price"))
|
|
* })
|
|
* ),
|
|
*
|
|
* @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
|
* )
|
|
*/
|
|
public function store() {}
|
|
|
|
/**
|
|
* @OA\Put(
|
|
* path="/api/v1/pricing/{id}",
|
|
* tags={"Pricing"},
|
|
* summary="단가 수정",
|
|
* description="확정(finalized) 상태의 단가는 수정 불가",
|
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
|
*
|
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
|
*
|
|
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PriceUpdateRequest")),
|
|
*
|
|
* @OA\Response(response=200, description="수정 성공",
|
|
*
|
|
* @OA\JsonContent(allOf={
|
|
*
|
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
|
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Price"))
|
|
* })
|
|
* ),
|
|
*
|
|
* @OA\Response(response=400, description="수정 불가 (확정된 단가)", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
|
* )
|
|
*/
|
|
public function update() {}
|
|
|
|
/**
|
|
* @OA\Delete(
|
|
* path="/api/v1/pricing/{id}",
|
|
* tags={"Pricing"},
|
|
* summary="단가 삭제 (soft)",
|
|
* description="확정(finalized) 상태의 단가는 삭제 불가",
|
|
* 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")),
|
|
* @OA\Response(response=400, description="삭제 불가", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
|
* )
|
|
*/
|
|
public function destroy() {}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/api/v1/pricing/{id}/finalize",
|
|
* tags={"Pricing"},
|
|
* summary="단가 확정",
|
|
* description="단가를 확정 상태로 변경 (확정 후 수정/삭제 불가)",
|
|
* 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/Price"))
|
|
* })
|
|
* ),
|
|
*
|
|
* @OA\Response(response=400, description="확정 불가", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
|
* )
|
|
*/
|
|
public function finalize() {}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/api/v1/pricing/by-items",
|
|
* tags={"Pricing"},
|
|
* summary="품목별 단가 현황 조회",
|
|
* description="여러 품목의 현재 유효한 단가를 한번에 조회",
|
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
|
*
|
|
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PriceByItemsRequest")),
|
|
*
|
|
* @OA\Response(response=200, description="조회 성공",
|
|
*
|
|
* @OA\JsonContent(allOf={
|
|
*
|
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
|
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PriceByItemsResult"))
|
|
* })
|
|
* )
|
|
* )
|
|
*/
|
|
public function byItems() {}
|
|
|
|
/**
|
|
* @OA\Get(
|
|
* path="/api/v1/pricing/{id}/revisions",
|
|
* tags={"Pricing"},
|
|
* summary="변경 이력 조회",
|
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
|
*
|
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
|
* @OA\Parameter(name="size", in="query", @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="object",
|
|
*
|
|
* @OA\Property(property="current_page", type="integer"),
|
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/PriceRevision")),
|
|
* @OA\Property(property="total", type="integer")
|
|
* ))
|
|
* })
|
|
* ),
|
|
*
|
|
* @OA\Response(response=404, description="단가 미존재", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
|
* )
|
|
*/
|
|
public function revisions() {}
|
|
|
|
/**
|
|
* @OA\Get(
|
|
* path="/api/v1/pricing/cost",
|
|
* tags={"Pricing"},
|
|
* summary="원가 조회",
|
|
* description="품목의 원가를 조회. 자재는 수입검사 입고단가 우선, 없으면 표준원가 사용",
|
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
|
*
|
|
* @OA\Parameter(name="item_type_code", in="query", required=true, @OA\Schema(type="string", enum={"PRODUCT","MATERIAL"})),
|
|
* @OA\Parameter(name="item_id", in="query", required=true, @OA\Schema(type="integer")),
|
|
* @OA\Parameter(name="date", in="query", description="기준일 (미지정시 오늘)", @OA\Schema(type="string", format="date")),
|
|
*
|
|
* @OA\Response(response=200, description="조회 성공",
|
|
*
|
|
* @OA\JsonContent(allOf={
|
|
*
|
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
|
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PriceCostResult"))
|
|
* })
|
|
* )
|
|
* )
|
|
*/
|
|
public function cost() {}
|
|
}
|