feat: 수주관리(Order Management) API Phase 1.1 구현
- OrderService 구현 (index, stats, show, store, update, destroy, updateStatus)
- OrderController 구현 (7개 API 엔드포인트)
- FormRequest 클래스 3개 생성 (Store, Update, UpdateStatus)
- 상태 전환 규칙 검증 (DRAFT → CONFIRMED → IN_PROGRESS → COMPLETED/CANCELLED)
- 수주번호 자동 생성 (ORD{YYYYMMDD}{0001} 형식)
- Swagger API 문서 작성 (OrderApi.php)
- i18n 메시지 키 추가 (ko/en)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,49 @@
|
||||
# SAM API 작업 현황
|
||||
|
||||
## 2026-01-08 (수) - Order Management API Phase 1.1 구현
|
||||
|
||||
### 작업 목표
|
||||
- 수주관리(Order Management) API 기본 CRUD 및 상태 관리 기능 구현
|
||||
- WorkOrderService/Controller 패턴을 참고하여 SAM API 규칙 준수
|
||||
|
||||
### 생성된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `app/Services/OrderService.php` | 수주 비즈니스 로직 서비스 |
|
||||
| `app/Http/Controllers/Api/V1/OrderController.php` | 수주 API 컨트롤러 |
|
||||
| `app/Http/Requests/Order/StoreOrderRequest.php` | 생성 요청 검증 |
|
||||
| `app/Http/Requests/Order/UpdateOrderRequest.php` | 수정 요청 검증 |
|
||||
| `app/Http/Requests/Order/UpdateOrderStatusRequest.php` | 상태 변경 요청 검증 |
|
||||
| `app/Swagger/v1/OrderApi.php` | Swagger API 문서 |
|
||||
|
||||
### 수정된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `routes/api.php` | OrderController import 및 7개 라우트 추가 |
|
||||
| `lang/ko/message.php` | 수주 관련 메시지 키 추가 |
|
||||
| `lang/en/message.php` | 수주 관련 메시지 키 추가 |
|
||||
| `lang/ko/error.php` | 수주 에러 메시지 키 추가 |
|
||||
| `lang/en/error.php` | 수주 에러 메시지 키 추가 |
|
||||
|
||||
### 주요 구현 내용
|
||||
1. **OrderService 메서드**: index, stats, show, store, update, destroy, updateStatus
|
||||
2. **상태 전환 규칙**: DRAFT → CONFIRMED → IN_PROGRESS → COMPLETED/CANCELLED
|
||||
3. **수주번호 자동생성**: ORD{YYYYMMDD}{0001} 형식
|
||||
4. **품목 금액 계산**: 공급가, 세액, 합계 자동 계산
|
||||
5. **Swagger 스키마**: Order, OrderItem, OrderPagination, OrderStats 등
|
||||
|
||||
### 검증 완료
|
||||
- [x] Pint 코드 스타일 (6개 파일 자동 수정)
|
||||
- [x] Swagger 문서 생성
|
||||
- [x] Service-First 아키텍처 준수
|
||||
- [x] i18n 메시지 키 사용
|
||||
|
||||
### 관련 문서
|
||||
- 계획: `docs/plans/order-management-plan.md`
|
||||
- 변경 요약: `docs/changes/20250108_order_management_phase1.md`
|
||||
|
||||
---
|
||||
|
||||
## 2026-01-02 (목) - 채권현황 동적월 지원 및 버그 수정
|
||||
|
||||
### 작업 목표
|
||||
|
||||
88
app/Http/Controllers/Api/V1/OrderController.php
Normal file
88
app/Http/Controllers/Api/V1/OrderController.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Order\StoreOrderRequest;
|
||||
use App\Http\Requests\Order\UpdateOrderRequest;
|
||||
use App\Http\Requests\Order\UpdateOrderStatusRequest;
|
||||
use App\Services\OrderService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function __construct(private OrderService $service) {}
|
||||
|
||||
/**
|
||||
* 목록 조회
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.order.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->stats();
|
||||
}, __('message.order.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 단건 조회
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.order.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성
|
||||
*/
|
||||
public function store(StoreOrderRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->store($request->validated());
|
||||
}, __('message.order.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정
|
||||
*/
|
||||
public function update(UpdateOrderRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.order.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->service->destroy($id);
|
||||
|
||||
return 'success';
|
||||
}, __('message.order.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경
|
||||
*/
|
||||
public function updateStatus(UpdateOrderStatusRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->updateStatus($id, $request->validated()['status']);
|
||||
}, __('message.order.status_updated'));
|
||||
}
|
||||
}
|
||||
71
app/Http/Requests/Order/StoreOrderRequest.php
Normal file
71
app/Http/Requests/Order/StoreOrderRequest.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Order;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreOrderRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 기본 정보
|
||||
'quote_id' => 'nullable|integer|exists:quotes,id',
|
||||
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])],
|
||||
'status_code' => ['nullable', Rule::in([
|
||||
Order::STATUS_DRAFT,
|
||||
Order::STATUS_CONFIRMED,
|
||||
])],
|
||||
'category_code' => 'nullable|string|max:50',
|
||||
|
||||
// 거래처 정보
|
||||
'client_id' => 'nullable|integer|exists:clients,id',
|
||||
'client_name' => 'nullable|string|max:200',
|
||||
'client_contact' => 'nullable|string|max:100',
|
||||
'site_name' => 'nullable|string|max:200',
|
||||
|
||||
// 금액 정보
|
||||
'supply_amount' => 'nullable|numeric|min:0',
|
||||
'tax_amount' => 'nullable|numeric|min:0',
|
||||
'total_amount' => 'nullable|numeric|min:0',
|
||||
'discount_rate' => 'nullable|numeric|min:0|max:100',
|
||||
'discount_amount' => 'nullable|numeric|min:0',
|
||||
|
||||
// 배송/기타
|
||||
'delivery_date' => 'nullable|date',
|
||||
'delivery_method_code' => 'nullable|string|max:50',
|
||||
'received_at' => 'nullable|date',
|
||||
'memo' => 'nullable|string',
|
||||
'remarks' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
|
||||
// 품목 배열
|
||||
'items' => 'nullable|array',
|
||||
'items.*.item_id' => 'nullable|integer|exists:items,id',
|
||||
'items.*.item_name' => 'required|string|max:200',
|
||||
'items.*.specification' => 'nullable|string|max:500',
|
||||
'items.*.quantity' => 'required|numeric|min:0',
|
||||
'items.*.unit' => 'nullable|string|max:20',
|
||||
'items.*.unit_price' => 'required|numeric|min:0',
|
||||
'items.*.supply_amount' => 'nullable|numeric|min:0',
|
||||
'items.*.tax_amount' => 'nullable|numeric|min:0',
|
||||
'items.*.total_amount' => 'nullable|numeric|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'items.*.item_name.required' => __('validation.required', ['attribute' => '품목명']),
|
||||
'items.*.quantity.required' => __('validation.required', ['attribute' => '수량']),
|
||||
'items.*.unit_price.required' => __('validation.required', ['attribute' => '단가']),
|
||||
];
|
||||
}
|
||||
}
|
||||
66
app/Http/Requests/Order/UpdateOrderRequest.php
Normal file
66
app/Http/Requests/Order/UpdateOrderRequest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Order;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateOrderRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 기본 정보 (order_no는 수정 불가)
|
||||
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])],
|
||||
'category_code' => 'nullable|string|max:50',
|
||||
|
||||
// 거래처 정보
|
||||
'client_id' => 'nullable|integer|exists:clients,id',
|
||||
'client_name' => 'nullable|string|max:200',
|
||||
'client_contact' => 'nullable|string|max:100',
|
||||
'site_name' => 'nullable|string|max:200',
|
||||
|
||||
// 금액 정보
|
||||
'supply_amount' => 'nullable|numeric|min:0',
|
||||
'tax_amount' => 'nullable|numeric|min:0',
|
||||
'total_amount' => 'nullable|numeric|min:0',
|
||||
'discount_rate' => 'nullable|numeric|min:0|max:100',
|
||||
'discount_amount' => 'nullable|numeric|min:0',
|
||||
|
||||
// 배송/기타
|
||||
'delivery_date' => 'nullable|date',
|
||||
'delivery_method_code' => 'nullable|string|max:50',
|
||||
'received_at' => 'nullable|date',
|
||||
'memo' => 'nullable|string',
|
||||
'remarks' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
|
||||
// 품목 배열 (전체 교체)
|
||||
'items' => 'nullable|array',
|
||||
'items.*.item_id' => 'nullable|integer|exists:items,id',
|
||||
'items.*.item_name' => 'required|string|max:200',
|
||||
'items.*.specification' => 'nullable|string|max:500',
|
||||
'items.*.quantity' => 'required|numeric|min:0',
|
||||
'items.*.unit' => 'nullable|string|max:20',
|
||||
'items.*.unit_price' => 'required|numeric|min:0',
|
||||
'items.*.supply_amount' => 'nullable|numeric|min:0',
|
||||
'items.*.tax_amount' => 'nullable|numeric|min:0',
|
||||
'items.*.total_amount' => 'nullable|numeric|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'items.*.item_name.required' => __('validation.required', ['attribute' => '품목명']),
|
||||
'items.*.quantity.required' => __('validation.required', ['attribute' => '수량']),
|
||||
'items.*.unit_price.required' => __('validation.required', ['attribute' => '단가']),
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/Order/UpdateOrderStatusRequest.php
Normal file
36
app/Http/Requests/Order/UpdateOrderStatusRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Order;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateOrderStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', Rule::in([
|
||||
Order::STATUS_DRAFT,
|
||||
Order::STATUS_CONFIRMED,
|
||||
Order::STATUS_IN_PROGRESS,
|
||||
Order::STATUS_COMPLETED,
|
||||
Order::STATUS_CANCELLED,
|
||||
])],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'status.required' => __('validation.required', ['attribute' => '상태']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
];
|
||||
}
|
||||
}
|
||||
322
app/Services/OrderService.php
Normal file
322
app/Services/OrderService.php
Normal file
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class OrderService extends Service
|
||||
{
|
||||
/**
|
||||
* 목록 조회 (검색/필터링/페이징)
|
||||
*/
|
||||
public function index(array $params)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$page = (int) ($params['page'] ?? 1);
|
||||
$size = (int) ($params['size'] ?? 20);
|
||||
$q = trim((string) ($params['q'] ?? ''));
|
||||
$status = $params['status'] ?? null;
|
||||
$orderType = $params['order_type'] ?? null;
|
||||
$clientId = $params['client_id'] ?? null;
|
||||
$dateFrom = $params['date_from'] ?? null;
|
||||
$dateTo = $params['date_to'] ?? null;
|
||||
|
||||
$query = Order::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['client:id,name', 'items']);
|
||||
|
||||
// 검색어 (수주번호, 현장명, 거래처명)
|
||||
if ($q !== '') {
|
||||
$query->where(function ($qq) use ($q) {
|
||||
$qq->where('order_no', 'like', "%{$q}%")
|
||||
->orWhere('site_name', 'like', "%{$q}%")
|
||||
->orWhere('client_name', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if ($status !== null) {
|
||||
$query->where('status_code', $status);
|
||||
}
|
||||
|
||||
// 주문유형 필터 (ORDER/PURCHASE)
|
||||
if ($orderType !== null) {
|
||||
$query->where('order_type_code', $orderType);
|
||||
}
|
||||
|
||||
// 거래처 필터
|
||||
if ($clientId !== null) {
|
||||
$query->where('client_id', $clientId);
|
||||
}
|
||||
|
||||
// 날짜 범위 (수주일 기준)
|
||||
if ($dateFrom !== null) {
|
||||
$query->where('received_at', '>=', $dateFrom);
|
||||
}
|
||||
if ($dateTo !== null) {
|
||||
$query->where('received_at', '<=', $dateTo);
|
||||
}
|
||||
|
||||
$query->orderByDesc('created_at');
|
||||
|
||||
return $query->paginate($size, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$counts = Order::where('tenant_id', $tenantId)
|
||||
->select('status_code', DB::raw('count(*) as count'))
|
||||
->groupBy('status_code')
|
||||
->pluck('count', 'status_code')
|
||||
->toArray();
|
||||
|
||||
$amounts = Order::where('tenant_id', $tenantId)
|
||||
->select('status_code', DB::raw('sum(total_amount) as total'))
|
||||
->groupBy('status_code')
|
||||
->pluck('total', 'status_code')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total' => array_sum($counts),
|
||||
'draft' => $counts[Order::STATUS_DRAFT] ?? 0,
|
||||
'confirmed' => $counts[Order::STATUS_CONFIRMED] ?? 0,
|
||||
'in_progress' => $counts[Order::STATUS_IN_PROGRESS] ?? 0,
|
||||
'completed' => $counts[Order::STATUS_COMPLETED] ?? 0,
|
||||
'cancelled' => $counts[Order::STATUS_CANCELLED] ?? 0,
|
||||
'total_amount' => array_sum($amounts),
|
||||
'confirmed_amount' => ($amounts[Order::STATUS_CONFIRMED] ?? 0) + ($amounts[Order::STATUS_IN_PROGRESS] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 단건 조회
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$order = Order::where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'client:id,name,business_no,representative,phone,email',
|
||||
'items' => fn ($q) => $q->orderBy('sort_order'),
|
||||
'quote:id,quote_no,site_name',
|
||||
])
|
||||
->find($id);
|
||||
|
||||
if (! $order) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성
|
||||
*/
|
||||
public function store(array $data)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
// 수주번호 자동 생성
|
||||
$data['order_no'] = $this->generateOrderNo($tenantId);
|
||||
$data['tenant_id'] = $tenantId;
|
||||
$data['created_by'] = $userId;
|
||||
$data['updated_by'] = $userId;
|
||||
|
||||
// 기본 상태 설정
|
||||
$data['status_code'] = $data['status_code'] ?? Order::STATUS_DRAFT;
|
||||
$data['order_type_code'] = $data['order_type_code'] ?? Order::TYPE_ORDER;
|
||||
|
||||
$items = $data['items'] ?? [];
|
||||
unset($data['items']);
|
||||
|
||||
$order = Order::create($data);
|
||||
|
||||
// 품목 저장
|
||||
foreach ($items as $index => $item) {
|
||||
$item['sort_order'] = $index;
|
||||
$this->calculateItemAmounts($item);
|
||||
$order->items()->create($item);
|
||||
}
|
||||
|
||||
// 합계 재계산
|
||||
$order->refresh();
|
||||
$order->recalculateTotals()->save();
|
||||
|
||||
return $order->load(['client:id,name', 'items']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정
|
||||
*/
|
||||
public function update(int $id, array $data)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$order = Order::where('tenant_id', $tenantId)->find($id);
|
||||
if (! $order) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 완료/취소 상태에서는 수정 불가
|
||||
if (in_array($order->status_code, [Order::STATUS_COMPLETED, Order::STATUS_CANCELLED])) {
|
||||
throw new BadRequestHttpException(__('error.order.cannot_update_completed'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($order, $data, $userId) {
|
||||
$data['updated_by'] = $userId;
|
||||
|
||||
$items = $data['items'] ?? null;
|
||||
unset($data['items'], $data['order_no']); // 번호 변경 불가
|
||||
|
||||
$order->update($data);
|
||||
|
||||
// 품목 교체 (있는 경우)
|
||||
if ($items !== null) {
|
||||
$order->items()->delete();
|
||||
foreach ($items as $index => $item) {
|
||||
$item['sort_order'] = $index;
|
||||
$this->calculateItemAmounts($item);
|
||||
$order->items()->create($item);
|
||||
}
|
||||
|
||||
// 합계 재계산
|
||||
$order->refresh();
|
||||
$order->recalculateTotals()->save();
|
||||
}
|
||||
|
||||
return $order->load(['client:id,name', 'items']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$order = Order::where('tenant_id', $tenantId)->find($id);
|
||||
if (! $order) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 진행 중이거나 완료된 수주는 삭제 불가
|
||||
if (in_array($order->status_code, [
|
||||
Order::STATUS_IN_PROGRESS,
|
||||
Order::STATUS_COMPLETED,
|
||||
])) {
|
||||
throw new BadRequestHttpException(__('error.order.cannot_delete_in_progress'));
|
||||
}
|
||||
|
||||
$order->deleted_by = $this->apiUserId();
|
||||
$order->save();
|
||||
$order->delete();
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경
|
||||
*/
|
||||
public function updateStatus(int $id, string $status)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$order = Order::where('tenant_id', $tenantId)->find($id);
|
||||
if (! $order) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 상태 유효성 검증
|
||||
$validStatuses = [
|
||||
Order::STATUS_DRAFT,
|
||||
Order::STATUS_CONFIRMED,
|
||||
Order::STATUS_IN_PROGRESS,
|
||||
Order::STATUS_COMPLETED,
|
||||
Order::STATUS_CANCELLED,
|
||||
];
|
||||
|
||||
if (! in_array($status, $validStatuses)) {
|
||||
throw new BadRequestHttpException(__('error.invalid_status'));
|
||||
}
|
||||
|
||||
// 상태 전환 규칙 검증
|
||||
$this->validateStatusTransition($order->status_code, $status);
|
||||
|
||||
$order->status_code = $status;
|
||||
$order->updated_by = $userId;
|
||||
$order->save();
|
||||
|
||||
return $order->load(['client:id,name', 'items']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 전환 규칙 검증
|
||||
*/
|
||||
private function validateStatusTransition(string $from, string $to): void
|
||||
{
|
||||
$allowedTransitions = [
|
||||
Order::STATUS_DRAFT => [Order::STATUS_CONFIRMED, Order::STATUS_CANCELLED],
|
||||
Order::STATUS_CONFIRMED => [Order::STATUS_IN_PROGRESS, Order::STATUS_CANCELLED],
|
||||
Order::STATUS_IN_PROGRESS => [Order::STATUS_COMPLETED, Order::STATUS_CANCELLED],
|
||||
Order::STATUS_COMPLETED => [], // 완료 상태에서는 변경 불가
|
||||
Order::STATUS_CANCELLED => [Order::STATUS_DRAFT], // 취소에서 임시저장으로만 복구 가능
|
||||
];
|
||||
|
||||
if (! in_array($to, $allowedTransitions[$from] ?? [])) {
|
||||
throw new BadRequestHttpException(__('error.order.invalid_status_transition'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 금액 계산
|
||||
*/
|
||||
private function calculateItemAmounts(array &$item): void
|
||||
{
|
||||
$quantity = (float) ($item['quantity'] ?? 0);
|
||||
$unitPrice = (float) ($item['unit_price'] ?? 0);
|
||||
|
||||
$item['supply_amount'] = $quantity * $unitPrice;
|
||||
$item['tax_amount'] = round($item['supply_amount'] * 0.1, 2);
|
||||
$item['total_amount'] = $item['supply_amount'] + $item['tax_amount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주번호 자동 생성
|
||||
*/
|
||||
private function generateOrderNo(int $tenantId): string
|
||||
{
|
||||
$prefix = 'ORD';
|
||||
$date = now()->format('Ymd');
|
||||
|
||||
// 오늘 날짜 기준 마지막 번호 조회
|
||||
$lastNo = Order::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('order_no', 'like', "{$prefix}{$date}%")
|
||||
->orderByDesc('order_no')
|
||||
->value('order_no');
|
||||
|
||||
if ($lastNo) {
|
||||
$seq = (int) substr($lastNo, -4) + 1;
|
||||
} else {
|
||||
$seq = 1;
|
||||
}
|
||||
|
||||
return sprintf('%s%s%04d', $prefix, $date, $seq);
|
||||
}
|
||||
}
|
||||
367
app/Swagger/v1/OrderApi.php
Normal file
367
app/Swagger/v1/OrderApi.php
Normal file
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(
|
||||
* name="Order",
|
||||
* description="수주관리 API"
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Order",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="quote_id", type="integer", nullable=true, example=1),
|
||||
* @OA\Property(property="order_no", type="string", example="ORD202501080001"),
|
||||
* @OA\Property(property="order_type_code", type="string", enum={"ORDER", "PURCHASE"}, example="ORDER"),
|
||||
* @OA\Property(property="status_code", type="string", enum={"DRAFT", "CONFIRMED", "IN_PROGRESS", "COMPLETED", "CANCELLED"}, example="DRAFT"),
|
||||
* @OA\Property(property="category_code", type="string", nullable=true, example="GENERAL"),
|
||||
* @OA\Property(property="client_id", type="integer", nullable=true, example=1),
|
||||
* @OA\Property(property="client_name", type="string", nullable=true, example="ABC 기업"),
|
||||
* @OA\Property(property="client_contact", type="string", nullable=true, example="010-1234-5678"),
|
||||
* @OA\Property(property="site_name", type="string", nullable=true, example="강남 현장"),
|
||||
* @OA\Property(property="quantity", type="number", format="float", example=100),
|
||||
* @OA\Property(property="supply_amount", type="number", format="float", example=1000000),
|
||||
* @OA\Property(property="tax_amount", type="number", format="float", example=100000),
|
||||
* @OA\Property(property="total_amount", type="number", format="float", example=1100000),
|
||||
* @OA\Property(property="discount_rate", type="number", format="float", nullable=true, example=5),
|
||||
* @OA\Property(property="discount_amount", type="number", format="float", nullable=true, example=50000),
|
||||
* @OA\Property(property="delivery_date", type="string", format="date", nullable=true, example="2025-01-15"),
|
||||
* @OA\Property(property="delivery_method_code", type="string", nullable=true, example="DELIVERY"),
|
||||
* @OA\Property(property="received_at", type="string", format="date-time", nullable=true),
|
||||
* @OA\Property(property="memo", type="string", nullable=true),
|
||||
* @OA\Property(property="remarks", type="string", nullable=true),
|
||||
* @OA\Property(property="note", type="string", nullable=true),
|
||||
* @OA\Property(property="created_by", type="integer", nullable=true),
|
||||
* @OA\Property(property="updated_by", type="integer", nullable=true),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time"),
|
||||
* @OA\Property(property="items", type="array", @OA\Items(ref="#/components/schemas/OrderItem")),
|
||||
* @OA\Property(property="client", type="object", nullable=true)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OrderItem",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="order_id", type="integer", example=1),
|
||||
* @OA\Property(property="item_id", type="integer", nullable=true, example=1),
|
||||
* @OA\Property(property="item_name", type="string", example="제품A"),
|
||||
* @OA\Property(property="specification", type="string", nullable=true, example="100x200mm"),
|
||||
* @OA\Property(property="quantity", type="number", format="float", example=10),
|
||||
* @OA\Property(property="unit", type="string", nullable=true, example="EA"),
|
||||
* @OA\Property(property="unit_price", type="number", format="float", example=10000),
|
||||
* @OA\Property(property="supply_amount", type="number", format="float", example=100000),
|
||||
* @OA\Property(property="tax_amount", type="number", format="float", example=10000),
|
||||
* @OA\Property(property="total_amount", type="number", format="float", example=110000),
|
||||
* @OA\Property(property="sort_order", type="integer", example=0)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OrderPagination",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Order")),
|
||||
* @OA\Property(property="first_page_url", type="string"),
|
||||
* @OA\Property(property="from", type="integer"),
|
||||
* @OA\Property(property="last_page", type="integer"),
|
||||
* @OA\Property(property="last_page_url", type="string"),
|
||||
* @OA\Property(property="next_page_url", type="string", nullable=true),
|
||||
* @OA\Property(property="path", type="string"),
|
||||
* @OA\Property(property="per_page", type="integer"),
|
||||
* @OA\Property(property="prev_page_url", type="string", nullable=true),
|
||||
* @OA\Property(property="to", type="integer"),
|
||||
* @OA\Property(property="total", type="integer")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OrderStats",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="total", type="integer", example=100),
|
||||
* @OA\Property(property="draft", type="integer", example=10),
|
||||
* @OA\Property(property="confirmed", type="integer", example=30),
|
||||
* @OA\Property(property="in_progress", type="integer", example=40),
|
||||
* @OA\Property(property="completed", type="integer", example=15),
|
||||
* @OA\Property(property="cancelled", type="integer", example=5),
|
||||
* @OA\Property(property="total_amount", type="number", format="float", example=50000000),
|
||||
* @OA\Property(property="confirmed_amount", type="number", format="float", example=35000000)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OrderCreateRequest",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="quote_id", type="integer", nullable=true),
|
||||
* @OA\Property(property="order_type_code", type="string", enum={"ORDER", "PURCHASE"}, example="ORDER"),
|
||||
* @OA\Property(property="status_code", type="string", enum={"DRAFT", "CONFIRMED"}, example="DRAFT"),
|
||||
* @OA\Property(property="category_code", type="string", nullable=true),
|
||||
* @OA\Property(property="client_id", type="integer", nullable=true),
|
||||
* @OA\Property(property="client_name", type="string", nullable=true),
|
||||
* @OA\Property(property="client_contact", type="string", nullable=true),
|
||||
* @OA\Property(property="site_name", type="string", nullable=true),
|
||||
* @OA\Property(property="delivery_date", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="delivery_method_code", type="string", nullable=true),
|
||||
* @OA\Property(property="received_at", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="memo", type="string", nullable=true),
|
||||
* @OA\Property(property="remarks", type="string", nullable=true),
|
||||
* @OA\Property(property="note", type="string", nullable=true),
|
||||
* @OA\Property(property="items", type="array", @OA\Items(ref="#/components/schemas/OrderItemRequest"))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OrderUpdateRequest",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="order_type_code", type="string", enum={"ORDER", "PURCHASE"}),
|
||||
* @OA\Property(property="category_code", type="string", nullable=true),
|
||||
* @OA\Property(property="client_id", type="integer", nullable=true),
|
||||
* @OA\Property(property="client_name", type="string", nullable=true),
|
||||
* @OA\Property(property="client_contact", type="string", nullable=true),
|
||||
* @OA\Property(property="site_name", type="string", nullable=true),
|
||||
* @OA\Property(property="delivery_date", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="delivery_method_code", type="string", nullable=true),
|
||||
* @OA\Property(property="received_at", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="memo", type="string", nullable=true),
|
||||
* @OA\Property(property="remarks", type="string", nullable=true),
|
||||
* @OA\Property(property="note", type="string", nullable=true),
|
||||
* @OA\Property(property="items", type="array", nullable=true, @OA\Items(ref="#/components/schemas/OrderItemRequest"))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OrderItemRequest",
|
||||
* type="object",
|
||||
* required={"item_name", "quantity", "unit_price"},
|
||||
*
|
||||
* @OA\Property(property="item_id", type="integer", nullable=true),
|
||||
* @OA\Property(property="item_name", type="string", example="제품A"),
|
||||
* @OA\Property(property="specification", type="string", nullable=true),
|
||||
* @OA\Property(property="quantity", type="number", format="float", example=10),
|
||||
* @OA\Property(property="unit", type="string", nullable=true, example="EA"),
|
||||
* @OA\Property(property="unit_price", type="number", format="float", example=10000)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OrderStatusRequest",
|
||||
* type="object",
|
||||
* required={"status"},
|
||||
*
|
||||
* @OA\Property(property="status", type="string", enum={"DRAFT", "CONFIRMED", "IN_PROGRESS", "COMPLETED", "CANCELLED"}, example="CONFIRMED")
|
||||
* )
|
||||
*/
|
||||
class OrderApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/orders",
|
||||
* tags={"Order"},
|
||||
* summary="수주 목록 조회",
|
||||
* description="수주 목록을 페이징하여 조회합니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", default=1)),
|
||||
* @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", default=20)),
|
||||
* @OA\Parameter(name="q", in="query", description="검색어 (수주번호, 현장명, 거래처명)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="status", in="query", description="상태 필터", @OA\Schema(type="string", enum={"DRAFT", "CONFIRMED", "IN_PROGRESS", "COMPLETED", "CANCELLED"})),
|
||||
* @OA\Parameter(name="order_type", in="query", description="주문유형 필터", @OA\Schema(type="string", enum={"ORDER", "PURCHASE"})),
|
||||
* @OA\Parameter(name="client_id", in="query", description="거래처 ID", @OA\Schema(type="integer")),
|
||||
* @OA\Parameter(name="date_from", in="query", description="수주일 시작", @OA\Schema(type="string", format="date")),
|
||||
* @OA\Parameter(name="date_to", in="query", description="수주일 종료", @OA\Schema(type="string", format="date")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/OrderPagination")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/orders/stats",
|
||||
* tags={"Order"},
|
||||
* summary="수주 통계 조회",
|
||||
* description="상태별 수주 건수와 금액을 조회합니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/OrderStats")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function stats() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/orders/{id}",
|
||||
* tags={"Order"},
|
||||
* summary="수주 상세 조회",
|
||||
* description="특정 수주의 상세 정보를 조회합니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Order")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="수주를 찾을 수 없음")
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/orders",
|
||||
* tags={"Order"},
|
||||
* summary="수주 생성",
|
||||
* description="새로운 수주를 생성합니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/OrderCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Order")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=422, description="유효성 검증 실패")
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/orders/{id}",
|
||||
* tags={"Order"},
|
||||
* summary="수주 수정",
|
||||
* description="기존 수주를 수정합니다. 완료/취소 상태에서는 수정할 수 없습니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/OrderUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Order")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="수정 불가 상태"),
|
||||
* @OA\Response(response=404, description="수주를 찾을 수 없음")
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/orders/{id}",
|
||||
* tags={"Order"},
|
||||
* summary="수주 삭제",
|
||||
* description="수주를 삭제합니다. 진행 중이거나 완료된 수주는 삭제할 수 없습니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", type="string", example="success")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="삭제 불가 상태"),
|
||||
* @OA\Response(response=404, description="수주를 찾을 수 없음")
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/orders/{id}/status",
|
||||
* tags={"Order"},
|
||||
* summary="수주 상태 변경",
|
||||
* description="수주의 상태를 변경합니다. 상태 전환 규칙에 따라 유효한 상태로만 변경 가능합니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/OrderStatusRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Order")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="유효하지 않은 상태 전환"),
|
||||
* @OA\Response(response=404, description="수주를 찾을 수 없음")
|
||||
* )
|
||||
*/
|
||||
public function updateStatus() {}
|
||||
}
|
||||
@@ -104,4 +104,11 @@
|
||||
'cannot_ship' => 'Not in a shippable state.',
|
||||
],
|
||||
|
||||
// Order related
|
||||
'order' => [
|
||||
'cannot_update_completed' => 'Cannot update completed or cancelled order.',
|
||||
'cannot_delete_in_progress' => 'Cannot delete in progress or completed order.',
|
||||
'invalid_status_transition' => 'Invalid status transition.',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -93,4 +93,13 @@
|
||||
'deleted' => 'File has been deleted.',
|
||||
'fetched' => 'File list retrieved successfully.',
|
||||
],
|
||||
|
||||
// Order Management
|
||||
'order' => [
|
||||
'fetched' => 'Order retrieved successfully.',
|
||||
'created' => 'Order has been created.',
|
||||
'updated' => 'Order has been updated.',
|
||||
'deleted' => 'Order has been deleted.',
|
||||
'status_updated' => 'Order status has been updated.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -356,4 +356,11 @@
|
||||
'send_failed' => 'FCM 발송 중 오류가 발생했습니다.',
|
||||
'token_not_found' => 'FCM 토큰을 찾을 수 없습니다.',
|
||||
],
|
||||
|
||||
// 수주 관련
|
||||
'order' => [
|
||||
'cannot_update_completed' => '완료 또는 취소된 수주는 수정할 수 없습니다.',
|
||||
'cannot_delete_in_progress' => '진행 중이거나 완료된 수주는 삭제할 수 없습니다.',
|
||||
'invalid_status_transition' => '유효하지 않은 상태 전환입니다.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -429,4 +429,13 @@
|
||||
'inspection_toggled' => '검사 상태가 변경되었습니다.',
|
||||
'packaging_toggled' => '포장 상태가 변경되었습니다.',
|
||||
],
|
||||
|
||||
// 수주관리
|
||||
'order' => [
|
||||
'fetched' => '수주를 조회했습니다.',
|
||||
'created' => '수주가 등록되었습니다.',
|
||||
'updated' => '수주가 수정되었습니다.',
|
||||
'deleted' => '수주가 삭제되었습니다.',
|
||||
'status_updated' => '수주 상태가 변경되었습니다.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
use App\Http\Controllers\Api\V1\UserInvitationController;
|
||||
use App\Http\Controllers\Api\V1\UserRoleController;
|
||||
use App\Http\Controllers\Api\V1\WithdrawalController;
|
||||
use App\Http\Controllers\Api\V1\OrderController;
|
||||
use App\Http\Controllers\Api\V1\WorkOrderController;
|
||||
use App\Http\Controllers\Api\V1\WorkResultController;
|
||||
use App\Http\Controllers\Api\V1\WorkSettingController;
|
||||
@@ -1070,6 +1071,20 @@
|
||||
Route::patch('/{id}/toggle', [ProcessController::class, 'toggleActive'])->whereNumber('id')->name('v1.processes.toggle');
|
||||
});
|
||||
|
||||
// 수주관리 API (Sales)
|
||||
Route::prefix('orders')->group(function () {
|
||||
// 기본 CRUD
|
||||
Route::get('', [OrderController::class, 'index'])->name('v1.orders.index'); // 목록
|
||||
Route::get('/stats', [OrderController::class, 'stats'])->name('v1.orders.stats'); // 통계
|
||||
Route::post('', [OrderController::class, 'store'])->name('v1.orders.store'); // 생성
|
||||
Route::get('/{id}', [OrderController::class, 'show'])->whereNumber('id')->name('v1.orders.show'); // 상세
|
||||
Route::put('/{id}', [OrderController::class, 'update'])->whereNumber('id')->name('v1.orders.update'); // 수정
|
||||
Route::delete('/{id}', [OrderController::class, 'destroy'])->whereNumber('id')->name('v1.orders.destroy'); // 삭제
|
||||
|
||||
// 상태 관리
|
||||
Route::patch('/{id}/status', [OrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.orders.status'); // 상태 변경
|
||||
});
|
||||
|
||||
// 작업지시 관리 API (Production)
|
||||
Route::prefix('work-orders')->group(function () {
|
||||
// 기본 CRUD
|
||||
|
||||
Reference in New Issue
Block a user