feat: H-1 입고 관리 API 구현
- ReceivingController: CRUD 및 목록 조회 API - ReceivingService: 입고 비즈니스 로직 - Receiving 모델: 다중 테넌트 지원 - FormRequest 검증 클래스 - Swagger 문서화 - receivings 테이블 마이그레이션 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
99
app/Http/Controllers/Api/V1/ReceivingController.php
Normal file
99
app/Http/Controllers/Api/V1/ReceivingController.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Receiving\ProcessReceivingRequest;
|
||||
use App\Http\Requests\V1\Receiving\StoreReceivingRequest;
|
||||
use App\Http\Requests\V1\Receiving\UpdateReceivingRequest;
|
||||
use App\Services\ReceivingService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReceivingController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ReceivingService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 입고 목록
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'search',
|
||||
'status',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
'per_page',
|
||||
'page',
|
||||
]);
|
||||
|
||||
$receivings = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($receivings, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 통계
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
$stats = $this->service->stats();
|
||||
|
||||
return ApiResponse::success($stats, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 등록
|
||||
*/
|
||||
public function store(StoreReceivingRequest $request)
|
||||
{
|
||||
$receiving = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($receiving, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 상세
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$receiving = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($receiving, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 수정
|
||||
*/
|
||||
public function update(int $id, UpdateReceivingRequest $request)
|
||||
{
|
||||
$receiving = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($receiving, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->service->destroy($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고처리 (상태 변경 + 입고 정보 입력)
|
||||
*/
|
||||
public function process(int $id, ProcessReceivingRequest $request)
|
||||
{
|
||||
$receiving = $this->service->process($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($receiving, __('message.receiving.processed'));
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/V1/Receiving/ProcessReceivingRequest.php
Normal file
36
app/Http/Requests/V1/Receiving/ProcessReceivingRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Receiving;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ProcessReceivingRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'receiving_qty' => ['required', 'numeric', 'min:0'],
|
||||
'receiving_date' => ['nullable', 'date'],
|
||||
'lot_no' => ['nullable', 'string', 'max:50'],
|
||||
'supplier_lot' => ['nullable', 'string', 'max:50'],
|
||||
'receiving_location' => ['required', 'string', 'max:100'],
|
||||
'receiving_manager' => ['nullable', 'string', 'max:50'],
|
||||
'remark' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'receiving_qty.required' => __('validation.required', ['attribute' => '입고수량']),
|
||||
'receiving_qty.numeric' => __('validation.numeric', ['attribute' => '입고수량']),
|
||||
'receiving_qty.min' => __('validation.min.numeric', ['attribute' => '입고수량', 'min' => 0]),
|
||||
'receiving_location.required' => __('validation.required', ['attribute' => '입고위치']),
|
||||
];
|
||||
}
|
||||
}
|
||||
43
app/Http/Requests/V1/Receiving/StoreReceivingRequest.php
Normal file
43
app/Http/Requests/V1/Receiving/StoreReceivingRequest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Receiving;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreReceivingRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'order_no' => ['nullable', 'string', 'max:30'],
|
||||
'order_date' => ['nullable', 'date'],
|
||||
'item_id' => ['nullable', 'integer'],
|
||||
'item_code' => ['required', 'string', 'max:50'],
|
||||
'item_name' => ['required', 'string', 'max:200'],
|
||||
'specification' => ['nullable', 'string', 'max:200'],
|
||||
'supplier' => ['required', 'string', 'max:100'],
|
||||
'order_qty' => ['required', 'numeric', 'min:0'],
|
||||
'order_unit' => ['nullable', 'string', 'max:20'],
|
||||
'due_date' => ['nullable', 'date'],
|
||||
'status' => ['nullable', 'string', 'in:order_completed,shipping,inspection_pending,receiving_pending'],
|
||||
'remark' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'item_code.required' => __('validation.required', ['attribute' => '품목코드']),
|
||||
'item_name.required' => __('validation.required', ['attribute' => '품목명']),
|
||||
'supplier.required' => __('validation.required', ['attribute' => '공급업체']),
|
||||
'order_qty.required' => __('validation.required', ['attribute' => '발주수량']),
|
||||
'order_qty.numeric' => __('validation.numeric', ['attribute' => '발주수량']),
|
||||
'order_qty.min' => __('validation.min.numeric', ['attribute' => '발주수량', 'min' => 0]),
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Http/Requests/V1/Receiving/UpdateReceivingRequest.php
Normal file
38
app/Http/Requests/V1/Receiving/UpdateReceivingRequest.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Receiving;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateReceivingRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'order_no' => ['nullable', 'string', 'max:30'],
|
||||
'order_date' => ['nullable', 'date'],
|
||||
'item_code' => ['sometimes', 'string', 'max:50'],
|
||||
'item_name' => ['sometimes', 'string', 'max:200'],
|
||||
'specification' => ['nullable', 'string', 'max:200'],
|
||||
'supplier' => ['sometimes', 'string', 'max:100'],
|
||||
'order_qty' => ['sometimes', 'numeric', 'min:0'],
|
||||
'order_unit' => ['nullable', 'string', 'max:20'],
|
||||
'due_date' => ['nullable', 'date'],
|
||||
'status' => ['sometimes', 'string', 'in:order_completed,shipping,inspection_pending,receiving_pending'],
|
||||
'remark' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'order_qty.numeric' => __('validation.numeric', ['attribute' => '발주수량']),
|
||||
'order_qty.min' => __('validation.min.numeric', ['attribute' => '발주수량', 'min' => 0]),
|
||||
];
|
||||
}
|
||||
}
|
||||
107
app/Models/Tenants/Receiving.php
Normal file
107
app/Models/Tenants/Receiving.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Receiving extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'receiving_number',
|
||||
'order_no',
|
||||
'order_date',
|
||||
'item_id',
|
||||
'item_code',
|
||||
'item_name',
|
||||
'specification',
|
||||
'supplier',
|
||||
'order_qty',
|
||||
'order_unit',
|
||||
'due_date',
|
||||
'receiving_qty',
|
||||
'receiving_date',
|
||||
'lot_no',
|
||||
'supplier_lot',
|
||||
'receiving_location',
|
||||
'receiving_manager',
|
||||
'status',
|
||||
'remark',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'order_date' => 'date',
|
||||
'due_date' => 'date',
|
||||
'receiving_date' => 'date',
|
||||
'order_qty' => 'decimal:2',
|
||||
'receiving_qty' => 'decimal:2',
|
||||
'item_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 상태 목록
|
||||
*/
|
||||
public const STATUSES = [
|
||||
'order_completed' => '발주완료',
|
||||
'shipping' => '배송중',
|
||||
'inspection_pending' => '검사대기',
|
||||
'receiving_pending' => '입고대기',
|
||||
'completed' => '입고완료',
|
||||
];
|
||||
|
||||
/**
|
||||
* 품목 관계
|
||||
*/
|
||||
public function item(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Items\Item::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자 관계
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Members\User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return self::STATUSES[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 가능 여부
|
||||
*/
|
||||
public function canEdit(): bool
|
||||
{
|
||||
return $this->status !== 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 가능 여부
|
||||
*/
|
||||
public function canDelete(): bool
|
||||
{
|
||||
return $this->status !== 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고처리 가능 여부
|
||||
*/
|
||||
public function canProcess(): bool
|
||||
{
|
||||
return in_array($this->status, ['order_completed', 'shipping', 'inspection_pending', 'receiving_pending']);
|
||||
}
|
||||
}
|
||||
298
app/Services/ReceivingService.php
Normal file
298
app/Services/ReceivingService.php
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\Receiving;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ReceivingService extends Service
|
||||
{
|
||||
/**
|
||||
* 입고 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Receiving::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('order_no', 'like', "%{$search}%")
|
||||
->orWhere('item_code', 'like', "%{$search}%")
|
||||
->orWhere('item_name', 'like', "%{$search}%")
|
||||
->orWhere('supplier', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
if ($params['status'] === 'receiving_pending') {
|
||||
// 입고대기: receiving_pending + inspection_pending
|
||||
$query->whereIn('status', ['receiving_pending', 'inspection_pending']);
|
||||
} elseif ($params['status'] === 'completed') {
|
||||
$query->where('status', 'completed');
|
||||
} else {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
}
|
||||
|
||||
// 날짜 범위 필터
|
||||
if (! empty($params['start_date'])) {
|
||||
$query->where('receiving_date', '>=', $params['start_date']);
|
||||
}
|
||||
if (! empty($params['end_date'])) {
|
||||
$query->where('receiving_date', '<=', $params['end_date']);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 통계 조회
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$today = now()->toDateString();
|
||||
|
||||
$receivingPendingCount = Receiving::where('tenant_id', $tenantId)
|
||||
->where('status', 'receiving_pending')
|
||||
->count();
|
||||
|
||||
$shippingCount = Receiving::where('tenant_id', $tenantId)
|
||||
->where('status', 'shipping')
|
||||
->count();
|
||||
|
||||
$inspectionPendingCount = Receiving::where('tenant_id', $tenantId)
|
||||
->where('status', 'inspection_pending')
|
||||
->count();
|
||||
|
||||
$todayReceivingCount = Receiving::where('tenant_id', $tenantId)
|
||||
->where('status', 'completed')
|
||||
->whereDate('receiving_date', $today)
|
||||
->count();
|
||||
|
||||
return [
|
||||
'receiving_pending_count' => $receivingPendingCount,
|
||||
'shipping_count' => $shippingCount,
|
||||
'inspection_pending_count' => $inspectionPendingCount,
|
||||
'today_receiving_count' => $todayReceivingCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 상세 조회
|
||||
*/
|
||||
public function show(int $id): Receiving
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Receiving::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['creator:id,name'])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 등록
|
||||
*/
|
||||
public function store(array $data): Receiving
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
// 입고번호 자동 생성
|
||||
$receivingNumber = $this->generateReceivingNumber($tenantId);
|
||||
|
||||
$receiving = new Receiving;
|
||||
$receiving->tenant_id = $tenantId;
|
||||
$receiving->receiving_number = $receivingNumber;
|
||||
$receiving->order_no = $data['order_no'] ?? null;
|
||||
$receiving->order_date = $data['order_date'] ?? null;
|
||||
$receiving->item_id = $data['item_id'] ?? null;
|
||||
$receiving->item_code = $data['item_code'];
|
||||
$receiving->item_name = $data['item_name'];
|
||||
$receiving->specification = $data['specification'] ?? null;
|
||||
$receiving->supplier = $data['supplier'];
|
||||
$receiving->order_qty = $data['order_qty'];
|
||||
$receiving->order_unit = $data['order_unit'] ?? 'EA';
|
||||
$receiving->due_date = $data['due_date'] ?? null;
|
||||
$receiving->status = $data['status'] ?? 'order_completed';
|
||||
$receiving->remark = $data['remark'] ?? null;
|
||||
$receiving->created_by = $userId;
|
||||
$receiving->updated_by = $userId;
|
||||
$receiving->save();
|
||||
|
||||
return $receiving;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 수정
|
||||
*/
|
||||
public function update(int $id, array $data): Receiving
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$receiving = Receiving::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $receiving->canEdit()) {
|
||||
throw new \Exception(__('error.receiving.cannot_edit'));
|
||||
}
|
||||
|
||||
if (isset($data['order_no'])) {
|
||||
$receiving->order_no = $data['order_no'];
|
||||
}
|
||||
if (isset($data['order_date'])) {
|
||||
$receiving->order_date = $data['order_date'];
|
||||
}
|
||||
if (isset($data['item_code'])) {
|
||||
$receiving->item_code = $data['item_code'];
|
||||
}
|
||||
if (isset($data['item_name'])) {
|
||||
$receiving->item_name = $data['item_name'];
|
||||
}
|
||||
if (array_key_exists('specification', $data)) {
|
||||
$receiving->specification = $data['specification'];
|
||||
}
|
||||
if (isset($data['supplier'])) {
|
||||
$receiving->supplier = $data['supplier'];
|
||||
}
|
||||
if (isset($data['order_qty'])) {
|
||||
$receiving->order_qty = $data['order_qty'];
|
||||
}
|
||||
if (isset($data['order_unit'])) {
|
||||
$receiving->order_unit = $data['order_unit'];
|
||||
}
|
||||
if (array_key_exists('due_date', $data)) {
|
||||
$receiving->due_date = $data['due_date'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$receiving->status = $data['status'];
|
||||
}
|
||||
if (array_key_exists('remark', $data)) {
|
||||
$receiving->remark = $data['remark'];
|
||||
}
|
||||
|
||||
$receiving->updated_by = $userId;
|
||||
$receiving->save();
|
||||
|
||||
return $receiving->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고 삭제
|
||||
*/
|
||||
public function destroy(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$receiving = Receiving::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $receiving->canDelete()) {
|
||||
throw new \Exception(__('error.receiving.cannot_delete'));
|
||||
}
|
||||
|
||||
$receiving->deleted_by = $userId;
|
||||
$receiving->save();
|
||||
$receiving->delete();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고처리 (상태 변경 + 입고 정보 입력)
|
||||
*/
|
||||
public function process(int $id, array $data): Receiving
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$receiving = Receiving::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $receiving->canProcess()) {
|
||||
throw new \Exception(__('error.receiving.cannot_process'));
|
||||
}
|
||||
|
||||
// LOT번호 생성 (없으면 자동 생성)
|
||||
$lotNo = $data['lot_no'] ?? $this->generateLotNo();
|
||||
|
||||
$receiving->receiving_qty = $data['receiving_qty'];
|
||||
$receiving->receiving_date = $data['receiving_date'] ?? now()->toDateString();
|
||||
$receiving->lot_no = $lotNo;
|
||||
$receiving->supplier_lot = $data['supplier_lot'] ?? null;
|
||||
$receiving->receiving_location = $data['receiving_location'];
|
||||
$receiving->receiving_manager = $data['receiving_manager'] ?? null;
|
||||
$receiving->status = 'completed';
|
||||
$receiving->remark = $data['remark'] ?? $receiving->remark;
|
||||
$receiving->updated_by = $userId;
|
||||
$receiving->save();
|
||||
|
||||
return $receiving->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 입고번호 자동 생성
|
||||
*/
|
||||
private function generateReceivingNumber(int $tenantId): string
|
||||
{
|
||||
$prefix = 'RV'.date('Ymd');
|
||||
|
||||
$lastReceiving = Receiving::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('receiving_number', 'like', $prefix.'%')
|
||||
->orderBy('receiving_number', 'desc')
|
||||
->first();
|
||||
|
||||
if ($lastReceiving) {
|
||||
$lastSeq = (int) substr($lastReceiving->receiving_number, -4);
|
||||
$newSeq = $lastSeq + 1;
|
||||
} else {
|
||||
$newSeq = 1;
|
||||
}
|
||||
|
||||
return $prefix.str_pad($newSeq, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* LOT번호 자동 생성
|
||||
*/
|
||||
private function generateLotNo(): string
|
||||
{
|
||||
$now = now();
|
||||
$year = $now->format('y');
|
||||
$month = $now->format('m');
|
||||
$day = $now->format('d');
|
||||
$seq = str_pad(rand(1, 99), 2, '0', STR_PAD_LEFT);
|
||||
|
||||
return "{$year}{$month}{$day}-{$seq}";
|
||||
}
|
||||
}
|
||||
360
app/Swagger/v1/ReceivingApi.php
Normal file
360
app/Swagger/v1/ReceivingApi.php
Normal file
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Receivings", description="입고 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Receiving",
|
||||
* type="object",
|
||||
* description="입고 정보",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1, description="입고 ID"),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||
* @OA\Property(property="receiving_number", type="string", example="RV202512260001", description="입고번호"),
|
||||
* @OA\Property(property="order_no", type="string", example="PO-2025-001", nullable=true, description="발주번호"),
|
||||
* @OA\Property(property="order_date", type="string", format="date", example="2025-12-20", nullable=true, description="발주일자"),
|
||||
* @OA\Property(property="item_id", type="integer", example=1, nullable=true, description="품목 ID"),
|
||||
* @OA\Property(property="item_code", type="string", example="ITEM-001", description="품목코드"),
|
||||
* @OA\Property(property="item_name", type="string", example="원재료 A", description="품목명"),
|
||||
* @OA\Property(property="specification", type="string", example="100x100", nullable=true, description="규격"),
|
||||
* @OA\Property(property="supplier", type="string", example="(주)공급사", description="공급업체"),
|
||||
* @OA\Property(property="order_qty", type="number", format="float", example=100.00, description="발주수량"),
|
||||
* @OA\Property(property="order_unit", type="string", example="EA", description="발주단위"),
|
||||
* @OA\Property(property="due_date", type="string", format="date", example="2025-12-26", nullable=true, description="납기일"),
|
||||
* @OA\Property(property="receiving_qty", type="number", format="float", example=100.00, nullable=true, description="입고수량"),
|
||||
* @OA\Property(property="receiving_date", type="string", format="date", example="2025-12-26", nullable=true, description="입고일자"),
|
||||
* @OA\Property(property="lot_no", type="string", example="251226-01", nullable=true, description="입고 LOT번호"),
|
||||
* @OA\Property(property="supplier_lot", type="string", example="SUP-LOT-001", nullable=true, description="공급업체 LOT"),
|
||||
* @OA\Property(property="receiving_location", type="string", example="A-01-01", nullable=true, description="입고위치"),
|
||||
* @OA\Property(property="receiving_manager", type="string", example="홍길동", nullable=true, description="입고담당"),
|
||||
* @OA\Property(property="status", type="string", enum={"order_completed","shipping","inspection_pending","receiving_pending","completed"}, example="receiving_pending", description="상태"),
|
||||
* @OA\Property(property="status_label", type="string", example="입고대기", description="상태 라벨"),
|
||||
* @OA\Property(property="remark", type="string", example="비고 내용", nullable=true, description="비고"),
|
||||
* @OA\Property(property="creator", type="object", nullable=true,
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="관리자"),
|
||||
* description="생성자 정보"
|
||||
* ),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ReceivingStats",
|
||||
* type="object",
|
||||
* description="입고 통계",
|
||||
*
|
||||
* @OA\Property(property="receiving_pending_count", type="integer", example=5, description="입고대기 건수"),
|
||||
* @OA\Property(property="shipping_count", type="integer", example=3, description="배송중 건수"),
|
||||
* @OA\Property(property="inspection_pending_count", type="integer", example=2, description="검사대기 건수"),
|
||||
* @OA\Property(property="today_receiving_count", type="integer", example=10, description="금일 입고 건수")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ReceivingCreateRequest",
|
||||
* type="object",
|
||||
* required={"item_code","item_name","supplier","order_qty"},
|
||||
* description="입고 등록 요청",
|
||||
*
|
||||
* @OA\Property(property="order_no", type="string", example="PO-2025-001", maxLength=30, nullable=true, description="발주번호"),
|
||||
* @OA\Property(property="order_date", type="string", format="date", example="2025-12-20", nullable=true, description="발주일자"),
|
||||
* @OA\Property(property="item_id", type="integer", example=1, nullable=true, description="품목 ID"),
|
||||
* @OA\Property(property="item_code", type="string", example="ITEM-001", maxLength=50, description="품목코드"),
|
||||
* @OA\Property(property="item_name", type="string", example="원재료 A", maxLength=200, description="품목명"),
|
||||
* @OA\Property(property="specification", type="string", example="100x100", maxLength=200, nullable=true, description="규격"),
|
||||
* @OA\Property(property="supplier", type="string", example="(주)공급사", maxLength=100, description="공급업체"),
|
||||
* @OA\Property(property="order_qty", type="number", format="float", example=100.00, description="발주수량"),
|
||||
* @OA\Property(property="order_unit", type="string", example="EA", maxLength=20, description="발주단위"),
|
||||
* @OA\Property(property="due_date", type="string", format="date", example="2025-12-26", nullable=true, description="납기일"),
|
||||
* @OA\Property(property="status", type="string", enum={"order_completed","shipping","inspection_pending","receiving_pending"}, example="order_completed", description="상태"),
|
||||
* @OA\Property(property="remark", type="string", example="비고 내용", maxLength=1000, nullable=true, description="비고")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ReceivingUpdateRequest",
|
||||
* type="object",
|
||||
* description="입고 수정 요청",
|
||||
*
|
||||
* @OA\Property(property="order_no", type="string", example="PO-2025-001", maxLength=30, nullable=true, description="발주번호"),
|
||||
* @OA\Property(property="order_date", type="string", format="date", example="2025-12-20", nullable=true, description="발주일자"),
|
||||
* @OA\Property(property="item_code", type="string", example="ITEM-001", maxLength=50, description="품목코드"),
|
||||
* @OA\Property(property="item_name", type="string", example="원재료 A", maxLength=200, description="품목명"),
|
||||
* @OA\Property(property="specification", type="string", example="100x100", maxLength=200, nullable=true, description="규격"),
|
||||
* @OA\Property(property="supplier", type="string", example="(주)공급사", maxLength=100, description="공급업체"),
|
||||
* @OA\Property(property="order_qty", type="number", format="float", example=100.00, description="발주수량"),
|
||||
* @OA\Property(property="order_unit", type="string", example="EA", maxLength=20, description="발주단위"),
|
||||
* @OA\Property(property="due_date", type="string", format="date", example="2025-12-26", nullable=true, description="납기일"),
|
||||
* @OA\Property(property="status", type="string", enum={"order_completed","shipping","inspection_pending","receiving_pending"}, example="shipping", description="상태"),
|
||||
* @OA\Property(property="remark", type="string", example="비고 내용", maxLength=1000, nullable=true, description="비고")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ReceivingProcessRequest",
|
||||
* type="object",
|
||||
* required={"receiving_qty","receiving_location"},
|
||||
* description="입고처리 요청",
|
||||
*
|
||||
* @OA\Property(property="receiving_qty", type="number", format="float", example=100.00, description="입고수량"),
|
||||
* @OA\Property(property="receiving_date", type="string", format="date", example="2025-12-26", nullable=true, description="입고일자 (미입력시 오늘)"),
|
||||
* @OA\Property(property="lot_no", type="string", example="251226-01", maxLength=50, nullable=true, description="LOT번호 (미입력시 자동생성)"),
|
||||
* @OA\Property(property="supplier_lot", type="string", example="SUP-LOT-001", maxLength=50, nullable=true, description="공급업체 LOT"),
|
||||
* @OA\Property(property="receiving_location", type="string", example="A-01-01", maxLength=100, description="입고위치"),
|
||||
* @OA\Property(property="receiving_manager", type="string", example="홍길동", maxLength=50, nullable=true, description="입고담당"),
|
||||
* @OA\Property(property="remark", type="string", example="비고 내용", maxLength=1000, nullable=true, description="비고")
|
||||
* )
|
||||
*/
|
||||
class ReceivingApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/receivings",
|
||||
* tags={"Receivings"},
|
||||
* summary="입고 목록 조회",
|
||||
* description="입고 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="search", in="query", description="검색어 (발주번호, 품목코드, 품목명, 공급업체)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="status", in="query", description="상태 (receiving_pending 선택시 inspection_pending도 포함)", @OA\Schema(type="string", enum={"order_completed","shipping","inspection_pending","receiving_pending","completed"})),
|
||||
* @OA\Parameter(name="start_date", in="query", description="입고일 시작일", @OA\Schema(type="string", format="date")),
|
||||
* @OA\Parameter(name="end_date", in="query", description="입고일 종료일", @OA\Schema(type="string", format="date")),
|
||||
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"receiving_date","due_date","created_at"}, default="created_at")),
|
||||
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
*
|
||||
* @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="data", type="array", @OA\Items(ref="#/components/schemas/Receiving")),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=50)
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/receivings/stats",
|
||||
* tags={"Receivings"},
|
||||
* summary="입고 통계 조회",
|
||||
* description="입고 현황 통계를 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ReceivingStats")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function stats() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/receivings",
|
||||
* tags={"Receivings"},
|
||||
* summary="입고 등록",
|
||||
* description="새로운 입고를 등록합니다. 입고번호는 자동 생성됩니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ReceivingCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="등록 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Receiving")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/receivings/{id}",
|
||||
* tags={"Receivings"},
|
||||
* summary="입고 상세 조회",
|
||||
* description="입고 상세 정보를 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="입고 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Receiving")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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 show() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/receivings/{id}",
|
||||
* tags={"Receivings"},
|
||||
* summary="입고 수정",
|
||||
* description="입고 정보를 수정합니다. 입고완료(completed) 상태에서는 수정 불가합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="입고 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ReceivingUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Receiving")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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 update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/receivings/{id}",
|
||||
* tags={"Receivings"},
|
||||
* summary="입고 삭제",
|
||||
* description="입고를 삭제합니다. 입고완료(completed) 상태에서는 삭제 불가합니다. (Soft Delete)",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="입고 ID", @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")),
|
||||
* @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 destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/receivings/{id}/process",
|
||||
* tags={"Receivings"},
|
||||
* summary="입고처리",
|
||||
* description="입고를 처리합니다. 입고수량, 입고위치, LOT번호 등을 입력하고 상태를 '입고완료'로 변경합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, description="입고 ID", @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ReceivingProcessRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="입고처리 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Receiving")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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 process() {}
|
||||
}
|
||||
Reference in New Issue
Block a user