feat: H-3 출하 관리 API 구현
- ShipmentController: 출하 CRUD 및 상태 관리 API - ShipmentService: 출하 비즈니스 로직 - Shipment, ShipmentItem 모델 - FormRequest 검증 클래스 - Swagger 문서화 - shipments, shipment_items 테이블 마이그레이션 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
164
app/Http/Controllers/Api/V1/ShipmentController.php
Normal file
164
app/Http/Controllers/Api/V1/ShipmentController.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Shipment\ShipmentStoreRequest;
|
||||
use App\Http\Requests\Shipment\ShipmentUpdateRequest;
|
||||
use App\Http\Requests\Shipment\ShipmentUpdateStatusRequest;
|
||||
use App\Services\ShipmentService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShipmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ShipmentService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 출하 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->only([
|
||||
'search',
|
||||
'status',
|
||||
'priority',
|
||||
'delivery_method',
|
||||
'scheduled_from',
|
||||
'scheduled_to',
|
||||
'can_ship',
|
||||
'deposit_confirmed',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
'per_page',
|
||||
'page',
|
||||
]);
|
||||
|
||||
$shipments = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($shipments, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 통계 조회
|
||||
*/
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$stats = $this->service->stats();
|
||||
|
||||
return ApiResponse::success($stats, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 통계 조회 (탭용)
|
||||
*/
|
||||
public function statsByStatus(): JsonResponse
|
||||
{
|
||||
$stats = $this->service->statsByStatus();
|
||||
|
||||
return ApiResponse::success($stats, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 상세 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$shipment = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($shipment, __('message.fetched'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.shipment.not_found'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 생성
|
||||
*/
|
||||
public function store(ShipmentStoreRequest $request): JsonResponse
|
||||
{
|
||||
$shipment = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($shipment, __('message.created'), 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 수정
|
||||
*/
|
||||
public function update(ShipmentUpdateRequest $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$shipment = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($shipment, __('message.updated'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.shipment.not_found'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 상태 변경
|
||||
*/
|
||||
public function updateStatus(ShipmentUpdateStatusRequest $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$shipment = $this->service->updateStatus(
|
||||
$id,
|
||||
$request->validated('status'),
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return ApiResponse::success($shipment, __('message.updated'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.shipment.not_found'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 삭제
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$this->service->delete($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.shipment.not_found'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LOT 옵션 조회
|
||||
*/
|
||||
public function lotOptions(): JsonResponse
|
||||
{
|
||||
$options = $this->service->getLotOptions();
|
||||
|
||||
return ApiResponse::success($options, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 물류사 옵션 조회
|
||||
*/
|
||||
public function logisticsOptions(): JsonResponse
|
||||
{
|
||||
$options = $this->service->getLogisticsOptions();
|
||||
|
||||
return ApiResponse::success($options, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 톤수 옵션 조회
|
||||
*/
|
||||
public function vehicleTonnageOptions(): JsonResponse
|
||||
{
|
||||
$options = $this->service->getVehicleTonnageOptions();
|
||||
|
||||
return ApiResponse::success($options, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
80
app/Http/Requests/Shipment/ShipmentStoreRequest.php
Normal file
80
app/Http/Requests/Shipment/ShipmentStoreRequest.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Shipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ShipmentStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 기본 정보
|
||||
'shipment_no' => 'nullable|string|max:50',
|
||||
'lot_no' => 'nullable|string|max:50',
|
||||
'order_id' => 'nullable|integer|exists:orders,id',
|
||||
'scheduled_date' => 'required|date',
|
||||
'status' => 'nullable|in:scheduled,ready,shipping,completed',
|
||||
'priority' => 'nullable|in:urgent,normal,low',
|
||||
'delivery_method' => 'nullable|in:pickup,direct,logistics',
|
||||
|
||||
// 발주처/배송 정보
|
||||
'client_id' => 'nullable|integer|exists:clients,id',
|
||||
'customer_name' => 'nullable|string|max:100',
|
||||
'site_name' => 'nullable|string|max:100',
|
||||
'delivery_address' => 'nullable|string|max:255',
|
||||
'receiver' => 'nullable|string|max:50',
|
||||
'receiver_contact' => 'nullable|string|max:50',
|
||||
|
||||
// 상태 플래그
|
||||
'can_ship' => 'nullable|boolean',
|
||||
'deposit_confirmed' => 'nullable|boolean',
|
||||
'invoice_issued' => 'nullable|boolean',
|
||||
'customer_grade' => 'nullable|string|max:20',
|
||||
|
||||
// 상차 정보
|
||||
'loading_manager' => 'nullable|string|max:50',
|
||||
'loading_time' => 'nullable|date',
|
||||
|
||||
// 물류/배차 정보
|
||||
'logistics_company' => 'nullable|string|max:50',
|
||||
'vehicle_tonnage' => 'nullable|string|max:20',
|
||||
'shipping_cost' => 'nullable|numeric|min:0',
|
||||
|
||||
// 차량/운전자 정보
|
||||
'vehicle_no' => 'nullable|string|max:20',
|
||||
'driver_name' => 'nullable|string|max:50',
|
||||
'driver_contact' => 'nullable|string|max:50',
|
||||
'expected_arrival' => 'nullable|date',
|
||||
|
||||
// 기타
|
||||
'remarks' => 'nullable|string',
|
||||
|
||||
// 출하 품목
|
||||
'items' => 'nullable|array',
|
||||
'items.*.seq' => 'nullable|integer|min:1',
|
||||
'items.*.item_code' => 'nullable|string|max:50',
|
||||
'items.*.item_name' => 'required|string|max:100',
|
||||
'items.*.floor_unit' => 'nullable|string|max:50',
|
||||
'items.*.specification' => 'nullable|string|max:100',
|
||||
'items.*.quantity' => 'nullable|numeric|min:0',
|
||||
'items.*.unit' => 'nullable|string|max:20',
|
||||
'items.*.lot_no' => 'nullable|string|max:50',
|
||||
'items.*.stock_lot_id' => 'nullable|integer|exists:stock_lots,id',
|
||||
'items.*.remarks' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'scheduled_date.required' => __('validation.required', ['attribute' => '출고예정일']),
|
||||
'items.*.item_name.required' => __('validation.required', ['attribute' => '품목명']),
|
||||
];
|
||||
}
|
||||
}
|
||||
70
app/Http/Requests/Shipment/ShipmentUpdateRequest.php
Normal file
70
app/Http/Requests/Shipment/ShipmentUpdateRequest.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Shipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ShipmentUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 기본 정보
|
||||
'lot_no' => 'nullable|string|max:50',
|
||||
'order_id' => 'nullable|integer|exists:orders,id',
|
||||
'scheduled_date' => 'nullable|date',
|
||||
'priority' => 'nullable|in:urgent,normal,low',
|
||||
'delivery_method' => 'nullable|in:pickup,direct,logistics',
|
||||
|
||||
// 발주처/배송 정보
|
||||
'client_id' => 'nullable|integer|exists:clients,id',
|
||||
'customer_name' => 'nullable|string|max:100',
|
||||
'site_name' => 'nullable|string|max:100',
|
||||
'delivery_address' => 'nullable|string|max:255',
|
||||
'receiver' => 'nullable|string|max:50',
|
||||
'receiver_contact' => 'nullable|string|max:50',
|
||||
|
||||
// 상태 플래그
|
||||
'can_ship' => 'nullable|boolean',
|
||||
'deposit_confirmed' => 'nullable|boolean',
|
||||
'invoice_issued' => 'nullable|boolean',
|
||||
'customer_grade' => 'nullable|string|max:20',
|
||||
|
||||
// 상차 정보
|
||||
'loading_manager' => 'nullable|string|max:50',
|
||||
'loading_time' => 'nullable|date',
|
||||
|
||||
// 물류/배차 정보
|
||||
'logistics_company' => 'nullable|string|max:50',
|
||||
'vehicle_tonnage' => 'nullable|string|max:20',
|
||||
'shipping_cost' => 'nullable|numeric|min:0',
|
||||
|
||||
// 차량/운전자 정보
|
||||
'vehicle_no' => 'nullable|string|max:20',
|
||||
'driver_name' => 'nullable|string|max:50',
|
||||
'driver_contact' => 'nullable|string|max:50',
|
||||
'expected_arrival' => 'nullable|date',
|
||||
|
||||
// 기타
|
||||
'remarks' => 'nullable|string',
|
||||
|
||||
// 출하 품목
|
||||
'items' => 'nullable|array',
|
||||
'items.*.seq' => 'nullable|integer|min:1',
|
||||
'items.*.item_code' => 'nullable|string|max:50',
|
||||
'items.*.item_name' => 'required|string|max:100',
|
||||
'items.*.floor_unit' => 'nullable|string|max:50',
|
||||
'items.*.specification' => 'nullable|string|max:100',
|
||||
'items.*.quantity' => 'nullable|numeric|min:0',
|
||||
'items.*.unit' => 'nullable|string|max:20',
|
||||
'items.*.lot_no' => 'nullable|string|max:50',
|
||||
'items.*.stock_lot_id' => 'nullable|integer|exists:stock_lots,id',
|
||||
'items.*.remarks' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/Shipment/ShipmentUpdateStatusRequest.php
Normal file
36
app/Http/Requests/Shipment/ShipmentUpdateStatusRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Shipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ShipmentUpdateStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'required|in:scheduled,ready,shipping,completed',
|
||||
|
||||
// 상태별 추가 데이터
|
||||
'loading_time' => 'nullable|date',
|
||||
'loading_completed_at' => 'nullable|date',
|
||||
'vehicle_no' => 'nullable|string|max:20',
|
||||
'driver_name' => 'nullable|string|max:50',
|
||||
'driver_contact' => 'nullable|string|max:50',
|
||||
'confirmed_arrival' => 'nullable|date',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'status.required' => __('validation.required', ['attribute' => '상태']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
];
|
||||
}
|
||||
}
|
||||
210
app/Models/Tenants/Shipment.php
Normal file
210
app/Models/Tenants/Shipment.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Shipment extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'shipment_no',
|
||||
'lot_no',
|
||||
'order_id',
|
||||
'scheduled_date',
|
||||
'status',
|
||||
'priority',
|
||||
'delivery_method',
|
||||
// 발주처/배송 정보
|
||||
'client_id',
|
||||
'customer_name',
|
||||
'site_name',
|
||||
'delivery_address',
|
||||
'receiver',
|
||||
'receiver_contact',
|
||||
// 상태 플래그
|
||||
'can_ship',
|
||||
'deposit_confirmed',
|
||||
'invoice_issued',
|
||||
'customer_grade',
|
||||
// 상차 정보
|
||||
'loading_manager',
|
||||
'loading_completed_at',
|
||||
'loading_time',
|
||||
// 물류/배차 정보
|
||||
'logistics_company',
|
||||
'vehicle_tonnage',
|
||||
'shipping_cost',
|
||||
// 차량/운전자 정보
|
||||
'vehicle_no',
|
||||
'driver_name',
|
||||
'driver_contact',
|
||||
'expected_arrival',
|
||||
'confirmed_arrival',
|
||||
// 기타
|
||||
'remarks',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'scheduled_date' => 'date',
|
||||
'can_ship' => 'boolean',
|
||||
'deposit_confirmed' => 'boolean',
|
||||
'invoice_issued' => 'boolean',
|
||||
'loading_completed_at' => 'datetime',
|
||||
'loading_time' => 'datetime',
|
||||
'expected_arrival' => 'datetime',
|
||||
'confirmed_arrival' => 'datetime',
|
||||
'shipping_cost' => 'decimal:0',
|
||||
'order_id' => 'integer',
|
||||
'client_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 출하 상태 목록
|
||||
*/
|
||||
public const STATUSES = [
|
||||
'scheduled' => '출고예정',
|
||||
'ready' => '출하대기',
|
||||
'shipping' => '배송중',
|
||||
'completed' => '배송완료',
|
||||
];
|
||||
|
||||
/**
|
||||
* 우선순위 목록
|
||||
*/
|
||||
public const PRIORITIES = [
|
||||
'urgent' => '긴급',
|
||||
'normal' => '보통',
|
||||
'low' => '낮음',
|
||||
];
|
||||
|
||||
/**
|
||||
* 배송방식 목록
|
||||
*/
|
||||
public const DELIVERY_METHODS = [
|
||||
'pickup' => '상차',
|
||||
'direct' => '직접배차',
|
||||
'logistics' => '물류사',
|
||||
];
|
||||
|
||||
/**
|
||||
* 출하 품목 관계
|
||||
*/
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(ShipmentItem::class)->orderBy('seq');
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 관계
|
||||
*/
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Clients\Client::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자 관계
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Members\User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자 관계
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Members\User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return self::STATUSES[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 우선순위 라벨
|
||||
*/
|
||||
public function getPriorityLabelAttribute(): string
|
||||
{
|
||||
return self::PRIORITIES[$this->priority] ?? $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배송방식 라벨
|
||||
*/
|
||||
public function getDeliveryMethodLabelAttribute(): string
|
||||
{
|
||||
return self::DELIVERY_METHODS[$this->delivery_method] ?? $this->delivery_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 총 품목 수량
|
||||
*/
|
||||
public function getTotalQuantityAttribute(): float
|
||||
{
|
||||
return $this->items->sum('quantity');
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 수
|
||||
*/
|
||||
public function getItemCountAttribute(): int
|
||||
{
|
||||
return $this->items->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 긴급 여부
|
||||
*/
|
||||
public function getIsUrgentAttribute(): bool
|
||||
{
|
||||
return $this->priority === 'urgent';
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 가능 여부 확인
|
||||
*/
|
||||
public function canProceedToShip(): bool
|
||||
{
|
||||
return $this->can_ship && $this->deposit_confirmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 출하번호 생성
|
||||
*/
|
||||
public static function generateShipmentNo(int $tenantId): string
|
||||
{
|
||||
$today = now()->format('Ymd');
|
||||
$prefix = 'SHP-'.$today.'-';
|
||||
|
||||
$lastShipment = static::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('shipment_no', 'like', $prefix.'%')
|
||||
->orderByDesc('shipment_no')
|
||||
->first();
|
||||
|
||||
if ($lastShipment) {
|
||||
$lastSeq = (int) substr($lastShipment->shipment_no, -4);
|
||||
$newSeq = str_pad($lastSeq + 1, 4, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$newSeq = '0001';
|
||||
}
|
||||
|
||||
return $prefix.$newSeq;
|
||||
}
|
||||
}
|
||||
61
app/Models/Tenants/ShipmentItem.php
Normal file
61
app/Models/Tenants/ShipmentItem.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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 ShipmentItem extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'shipment_id',
|
||||
'seq',
|
||||
'item_code',
|
||||
'item_name',
|
||||
'floor_unit',
|
||||
'specification',
|
||||
'quantity',
|
||||
'unit',
|
||||
'lot_no',
|
||||
'stock_lot_id',
|
||||
'remarks',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'seq' => 'integer',
|
||||
'quantity' => 'decimal:2',
|
||||
'shipment_id' => 'integer',
|
||||
'stock_lot_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 출하 관계
|
||||
*/
|
||||
public function shipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Shipment::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 LOT 관계
|
||||
*/
|
||||
public function stockLot(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StockLot::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 순번 가져오기
|
||||
*/
|
||||
public static function getNextSeq(int $shipmentId): int
|
||||
{
|
||||
$maxSeq = static::where('shipment_id', $shipmentId)->max('seq');
|
||||
|
||||
return ($maxSeq ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
436
app/Services/ShipmentService.php
Normal file
436
app/Services/ShipmentService.php
Normal file
@@ -0,0 +1,436 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\Shipment;
|
||||
use App\Models\Tenants\ShipmentItem;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ShipmentService extends Service
|
||||
{
|
||||
/**
|
||||
* 출하 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Shipment::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('items');
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('shipment_no', 'like', "%{$search}%")
|
||||
->orWhere('lot_no', 'like', "%{$search}%")
|
||||
->orWhere('customer_name', 'like', "%{$search}%")
|
||||
->orWhere('site_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 우선순위 필터
|
||||
if (! empty($params['priority'])) {
|
||||
$query->where('priority', $params['priority']);
|
||||
}
|
||||
|
||||
// 배송방식 필터
|
||||
if (! empty($params['delivery_method'])) {
|
||||
$query->where('delivery_method', $params['delivery_method']);
|
||||
}
|
||||
|
||||
// 예정일 범위 필터
|
||||
if (! empty($params['scheduled_from'])) {
|
||||
$query->where('scheduled_date', '>=', $params['scheduled_from']);
|
||||
}
|
||||
if (! empty($params['scheduled_to'])) {
|
||||
$query->where('scheduled_date', '<=', $params['scheduled_to']);
|
||||
}
|
||||
|
||||
// 출하가능 필터
|
||||
if (isset($params['can_ship'])) {
|
||||
$query->where('can_ship', (bool) $params['can_ship']);
|
||||
}
|
||||
|
||||
// 입금확인 필터
|
||||
if (isset($params['deposit_confirmed'])) {
|
||||
$query->where('deposit_confirmed', (bool) $params['deposit_confirmed']);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'scheduled_date';
|
||||
$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();
|
||||
|
||||
$total = Shipment::where('tenant_id', $tenantId)->count();
|
||||
|
||||
$scheduled = Shipment::where('tenant_id', $tenantId)
|
||||
->where('status', 'scheduled')
|
||||
->count();
|
||||
|
||||
$ready = Shipment::where('tenant_id', $tenantId)
|
||||
->where('status', 'ready')
|
||||
->count();
|
||||
|
||||
$shipping = Shipment::where('tenant_id', $tenantId)
|
||||
->where('status', 'shipping')
|
||||
->count();
|
||||
|
||||
$completed = Shipment::where('tenant_id', $tenantId)
|
||||
->where('status', 'completed')
|
||||
->count();
|
||||
|
||||
$urgent = Shipment::where('tenant_id', $tenantId)
|
||||
->where('priority', 'urgent')
|
||||
->whereIn('status', ['scheduled', 'ready'])
|
||||
->count();
|
||||
|
||||
$todayScheduled = Shipment::where('tenant_id', $tenantId)
|
||||
->whereDate('scheduled_date', now())
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'scheduled' => $scheduled,
|
||||
'ready' => $ready,
|
||||
'shipping' => $shipping,
|
||||
'completed' => $completed,
|
||||
'urgent' => $urgent,
|
||||
'today_scheduled' => $todayScheduled,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 통계 (탭용)
|
||||
*/
|
||||
public function statsByStatus(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$stats = Shipment::where('tenant_id', $tenantId)
|
||||
->selectRaw('status, COUNT(*) as count')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->keyBy('status');
|
||||
|
||||
$result = [];
|
||||
foreach (Shipment::STATUSES as $key => $label) {
|
||||
$data = $stats->get($key);
|
||||
$result[$key] = [
|
||||
'label' => $label,
|
||||
'count' => $data?->count ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 상세 조회
|
||||
*/
|
||||
public function show(int $id): Shipment
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Shipment::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['items' => function ($query) {
|
||||
$query->orderBy('seq');
|
||||
}, 'creator', 'updater'])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 생성
|
||||
*/
|
||||
public function store(array $data): Shipment
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
// 출하번호 자동 생성
|
||||
$shipmentNo = $data['shipment_no'] ?? Shipment::generateShipmentNo($tenantId);
|
||||
|
||||
$shipment = Shipment::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'shipment_no' => $shipmentNo,
|
||||
'lot_no' => $data['lot_no'] ?? null,
|
||||
'order_id' => $data['order_id'] ?? null,
|
||||
'scheduled_date' => $data['scheduled_date'],
|
||||
'status' => $data['status'] ?? 'scheduled',
|
||||
'priority' => $data['priority'] ?? 'normal',
|
||||
'delivery_method' => $data['delivery_method'] ?? 'pickup',
|
||||
// 발주처/배송 정보
|
||||
'client_id' => $data['client_id'] ?? null,
|
||||
'customer_name' => $data['customer_name'] ?? null,
|
||||
'site_name' => $data['site_name'] ?? null,
|
||||
'delivery_address' => $data['delivery_address'] ?? null,
|
||||
'receiver' => $data['receiver'] ?? null,
|
||||
'receiver_contact' => $data['receiver_contact'] ?? null,
|
||||
// 상태 플래그
|
||||
'can_ship' => $data['can_ship'] ?? false,
|
||||
'deposit_confirmed' => $data['deposit_confirmed'] ?? false,
|
||||
'invoice_issued' => $data['invoice_issued'] ?? false,
|
||||
'customer_grade' => $data['customer_grade'] ?? null,
|
||||
// 상차 정보
|
||||
'loading_manager' => $data['loading_manager'] ?? null,
|
||||
'loading_time' => $data['loading_time'] ?? null,
|
||||
// 물류/배차 정보
|
||||
'logistics_company' => $data['logistics_company'] ?? null,
|
||||
'vehicle_tonnage' => $data['vehicle_tonnage'] ?? null,
|
||||
'shipping_cost' => $data['shipping_cost'] ?? null,
|
||||
// 차량/운전자 정보
|
||||
'vehicle_no' => $data['vehicle_no'] ?? null,
|
||||
'driver_name' => $data['driver_name'] ?? null,
|
||||
'driver_contact' => $data['driver_contact'] ?? null,
|
||||
'expected_arrival' => $data['expected_arrival'] ?? null,
|
||||
// 기타
|
||||
'remarks' => $data['remarks'] ?? null,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 출하 품목 추가
|
||||
if (! empty($data['items'])) {
|
||||
$this->syncItems($shipment, $data['items'], $tenantId);
|
||||
}
|
||||
|
||||
return $shipment->load('items');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 수정
|
||||
*/
|
||||
public function update(int $id, array $data): Shipment
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id);
|
||||
|
||||
$shipment->update([
|
||||
'lot_no' => $data['lot_no'] ?? $shipment->lot_no,
|
||||
'order_id' => $data['order_id'] ?? $shipment->order_id,
|
||||
'scheduled_date' => $data['scheduled_date'] ?? $shipment->scheduled_date,
|
||||
'priority' => $data['priority'] ?? $shipment->priority,
|
||||
'delivery_method' => $data['delivery_method'] ?? $shipment->delivery_method,
|
||||
// 발주처/배송 정보
|
||||
'client_id' => $data['client_id'] ?? $shipment->client_id,
|
||||
'customer_name' => $data['customer_name'] ?? $shipment->customer_name,
|
||||
'site_name' => $data['site_name'] ?? $shipment->site_name,
|
||||
'delivery_address' => $data['delivery_address'] ?? $shipment->delivery_address,
|
||||
'receiver' => $data['receiver'] ?? $shipment->receiver,
|
||||
'receiver_contact' => $data['receiver_contact'] ?? $shipment->receiver_contact,
|
||||
// 상태 플래그
|
||||
'can_ship' => $data['can_ship'] ?? $shipment->can_ship,
|
||||
'deposit_confirmed' => $data['deposit_confirmed'] ?? $shipment->deposit_confirmed,
|
||||
'invoice_issued' => $data['invoice_issued'] ?? $shipment->invoice_issued,
|
||||
'customer_grade' => $data['customer_grade'] ?? $shipment->customer_grade,
|
||||
// 상차 정보
|
||||
'loading_manager' => $data['loading_manager'] ?? $shipment->loading_manager,
|
||||
'loading_time' => $data['loading_time'] ?? $shipment->loading_time,
|
||||
// 물류/배차 정보
|
||||
'logistics_company' => $data['logistics_company'] ?? $shipment->logistics_company,
|
||||
'vehicle_tonnage' => $data['vehicle_tonnage'] ?? $shipment->vehicle_tonnage,
|
||||
'shipping_cost' => $data['shipping_cost'] ?? $shipment->shipping_cost,
|
||||
// 차량/운전자 정보
|
||||
'vehicle_no' => $data['vehicle_no'] ?? $shipment->vehicle_no,
|
||||
'driver_name' => $data['driver_name'] ?? $shipment->driver_name,
|
||||
'driver_contact' => $data['driver_contact'] ?? $shipment->driver_contact,
|
||||
'expected_arrival' => $data['expected_arrival'] ?? $shipment->expected_arrival,
|
||||
// 기타
|
||||
'remarks' => $data['remarks'] ?? $shipment->remarks,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
// 출하 품목 동기화
|
||||
if (isset($data['items'])) {
|
||||
$this->syncItems($shipment, $data['items'], $tenantId);
|
||||
}
|
||||
|
||||
return $shipment->load('items');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 상태 변경
|
||||
*/
|
||||
public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id);
|
||||
|
||||
$updateData = [
|
||||
'status' => $status,
|
||||
'updated_by' => $userId,
|
||||
];
|
||||
|
||||
// 상태별 추가 데이터
|
||||
if ($status === 'ready' && isset($additionalData['loading_time'])) {
|
||||
$updateData['loading_time'] = $additionalData['loading_time'];
|
||||
}
|
||||
|
||||
if ($status === 'shipping') {
|
||||
if (isset($additionalData['loading_completed_at'])) {
|
||||
$updateData['loading_completed_at'] = $additionalData['loading_completed_at'];
|
||||
} else {
|
||||
$updateData['loading_completed_at'] = now();
|
||||
}
|
||||
|
||||
if (isset($additionalData['vehicle_no'])) {
|
||||
$updateData['vehicle_no'] = $additionalData['vehicle_no'];
|
||||
}
|
||||
if (isset($additionalData['driver_name'])) {
|
||||
$updateData['driver_name'] = $additionalData['driver_name'];
|
||||
}
|
||||
if (isset($additionalData['driver_contact'])) {
|
||||
$updateData['driver_contact'] = $additionalData['driver_contact'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === 'completed' && isset($additionalData['confirmed_arrival'])) {
|
||||
$updateData['confirmed_arrival'] = $additionalData['confirmed_arrival'];
|
||||
}
|
||||
|
||||
$shipment->update($updateData);
|
||||
|
||||
return $shipment->load('items');
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 삭제
|
||||
*/
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id);
|
||||
|
||||
return DB::transaction(function () use ($shipment, $userId) {
|
||||
// 품목 삭제
|
||||
$shipment->items()->delete();
|
||||
|
||||
// 출하 삭제
|
||||
$shipment->update(['deleted_by' => $userId]);
|
||||
$shipment->delete();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 품목 동기화
|
||||
*/
|
||||
protected function syncItems(Shipment $shipment, array $items, int $tenantId): void
|
||||
{
|
||||
// 기존 품목 삭제
|
||||
$shipment->items()->forceDelete();
|
||||
|
||||
// 새 품목 생성
|
||||
$seq = 1;
|
||||
foreach ($items as $item) {
|
||||
ShipmentItem::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'shipment_id' => $shipment->id,
|
||||
'seq' => $item['seq'] ?? $seq,
|
||||
'item_code' => $item['item_code'] ?? null,
|
||||
'item_name' => $item['item_name'],
|
||||
'floor_unit' => $item['floor_unit'] ?? null,
|
||||
'specification' => $item['specification'] ?? null,
|
||||
'quantity' => $item['quantity'] ?? 0,
|
||||
'unit' => $item['unit'] ?? null,
|
||||
'lot_no' => $item['lot_no'] ?? null,
|
||||
'stock_lot_id' => $item['stock_lot_id'] ?? null,
|
||||
'remarks' => $item['remarks'] ?? null,
|
||||
]);
|
||||
$seq++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LOT 옵션 조회 (출고 가능한 LOT 목록)
|
||||
*/
|
||||
public function getLotOptions(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return \App\Models\Tenants\StockLot::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('status', ['available', 'reserved'])
|
||||
->where('qty', '>', 0)
|
||||
->with('stock:id,item_code,item_name')
|
||||
->orderBy('fifo_order')
|
||||
->get()
|
||||
->map(function ($lot) {
|
||||
return [
|
||||
'id' => $lot->id,
|
||||
'lot_no' => $lot->lot_no,
|
||||
'item_code' => $lot->stock?->item_code,
|
||||
'item_name' => $lot->stock?->item_name,
|
||||
'qty' => $lot->qty,
|
||||
'available_qty' => $lot->available_qty,
|
||||
'unit' => $lot->unit,
|
||||
'location' => $lot->location,
|
||||
'fifo_order' => $lot->fifo_order,
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 물류사 옵션 조회
|
||||
*/
|
||||
public function getLogisticsOptions(): array
|
||||
{
|
||||
// TODO: 별도 물류사 테이블이 있다면 조회
|
||||
// 현재는 기본값 반환
|
||||
return [
|
||||
['value' => '한진택배', 'label' => '한진택배'],
|
||||
['value' => '롯데택배', 'label' => '롯데택배'],
|
||||
['value' => 'CJ대한통운', 'label' => 'CJ대한통운'],
|
||||
['value' => '우체국택배', 'label' => '우체국택배'],
|
||||
['value' => '로젠택배', 'label' => '로젠택배'],
|
||||
['value' => '경동택배', 'label' => '경동택배'],
|
||||
['value' => '직접입력', 'label' => '직접입력'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 톤수 옵션 조회
|
||||
*/
|
||||
public function getVehicleTonnageOptions(): array
|
||||
{
|
||||
return [
|
||||
['value' => '1톤', 'label' => '1톤'],
|
||||
['value' => '2.5톤', 'label' => '2.5톤'],
|
||||
['value' => '5톤', 'label' => '5톤'],
|
||||
['value' => '11톤', 'label' => '11톤'],
|
||||
['value' => '25톤', 'label' => '25톤'],
|
||||
];
|
||||
}
|
||||
}
|
||||
625
app/Swagger/v1/ShipmentApi.php
Normal file
625
app/Swagger/v1/ShipmentApi.php
Normal file
@@ -0,0 +1,625 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Shipments", description="출하 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Shipment",
|
||||
* 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="shipment_no", type="string", example="SHP-20251226-0001", description="출하번호"),
|
||||
* @OA\Property(property="lot_no", type="string", example="LOT-001", nullable=true, description="LOT번호"),
|
||||
* @OA\Property(property="order_id", type="integer", nullable=true, description="수주 ID"),
|
||||
* @OA\Property(property="scheduled_date", type="string", format="date", example="2025-12-26", description="출고예정일"),
|
||||
* @OA\Property(property="status", type="string", enum={"scheduled","ready","shipping","completed"}, example="scheduled", description="상태"),
|
||||
* @OA\Property(property="status_label", type="string", example="출고예정", description="상태 라벨"),
|
||||
* @OA\Property(property="priority", type="string", enum={"urgent","normal","low"}, example="normal", description="우선순위"),
|
||||
* @OA\Property(property="priority_label", type="string", example="보통", description="우선순위 라벨"),
|
||||
* @OA\Property(property="delivery_method", type="string", enum={"pickup","direct","logistics"}, example="pickup", description="배송방식"),
|
||||
* @OA\Property(property="delivery_method_label", type="string", example="상차", description="배송방식 라벨"),
|
||||
* @OA\Property(property="client_id", type="integer", nullable=true, description="거래처 ID"),
|
||||
* @OA\Property(property="customer_name", type="string", example="(주)고객사", nullable=true, description="발주처명"),
|
||||
* @OA\Property(property="site_name", type="string", example="서울현장", nullable=true, description="현장명"),
|
||||
* @OA\Property(property="delivery_address", type="string", example="서울시 강남구", nullable=true, description="배송주소"),
|
||||
* @OA\Property(property="receiver", type="string", example="홍길동", nullable=true, description="인수자"),
|
||||
* @OA\Property(property="receiver_contact", type="string", example="010-1234-5678", nullable=true, description="인수자 연락처"),
|
||||
* @OA\Property(property="can_ship", type="boolean", example=true, description="출하가능 여부"),
|
||||
* @OA\Property(property="deposit_confirmed", type="boolean", example=true, description="입금확인 여부"),
|
||||
* @OA\Property(property="invoice_issued", type="boolean", example=false, description="세금계산서 발행 여부"),
|
||||
* @OA\Property(property="customer_grade", type="string", example="A", nullable=true, description="거래처 등급"),
|
||||
* @OA\Property(property="loading_manager", type="string", example="김상차", nullable=true, description="상차담당자"),
|
||||
* @OA\Property(property="loading_completed_at", type="string", format="date-time", nullable=true, description="상차완료 일시"),
|
||||
* @OA\Property(property="loading_time", type="string", format="date-time", nullable=true, description="상차시간(입차예정)"),
|
||||
* @OA\Property(property="logistics_company", type="string", example="CJ대한통운", nullable=true, description="물류사"),
|
||||
* @OA\Property(property="vehicle_tonnage", type="string", example="5톤", nullable=true, description="차량 톤수"),
|
||||
* @OA\Property(property="shipping_cost", type="number", example=150000, nullable=true, description="운송비"),
|
||||
* @OA\Property(property="vehicle_no", type="string", example="12가1234", nullable=true, description="차량번호"),
|
||||
* @OA\Property(property="driver_name", type="string", example="이운전", nullable=true, description="운전자명"),
|
||||
* @OA\Property(property="driver_contact", type="string", example="010-9876-5432", nullable=true, description="운전자 연락처"),
|
||||
* @OA\Property(property="expected_arrival", type="string", format="date-time", nullable=true, description="입차예정시간"),
|
||||
* @OA\Property(property="confirmed_arrival", type="string", format="date-time", nullable=true, description="입차확정시간"),
|
||||
* @OA\Property(property="remarks", type="string", nullable=true, description="비고"),
|
||||
* @OA\Property(property="is_urgent", type="boolean", example=false, description="긴급 여부"),
|
||||
* @OA\Property(property="item_count", type="integer", example=3, description="품목 수"),
|
||||
* @OA\Property(property="total_quantity", type="number", format="float", example=150.5, description="총 수량"),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ShipmentItem",
|
||||
* type="object",
|
||||
* description="출하 품목 정보",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1, description="품목 ID"),
|
||||
* @OA\Property(property="shipment_id", type="integer", example=1, description="출하 ID"),
|
||||
* @OA\Property(property="seq", type="integer", example=1, description="순번"),
|
||||
* @OA\Property(property="item_code", type="string", example="ITEM-001", nullable=true, description="품목코드"),
|
||||
* @OA\Property(property="item_name", type="string", example="제품 A", description="품목명"),
|
||||
* @OA\Property(property="floor_unit", type="string", example="1층/M01", nullable=true, description="층/M호"),
|
||||
* @OA\Property(property="specification", type="string", example="100x200mm", nullable=true, description="규격"),
|
||||
* @OA\Property(property="quantity", type="number", format="float", example=10.5, description="수량"),
|
||||
* @OA\Property(property="unit", type="string", example="EA", nullable=true, description="단위"),
|
||||
* @OA\Property(property="lot_no", type="string", example="LOT-001", nullable=true, description="LOT번호"),
|
||||
* @OA\Property(property="stock_lot_id", type="integer", nullable=true, description="재고 LOT ID"),
|
||||
* @OA\Property(property="remarks", type="string", nullable=true, description="비고"),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ShipmentWithItems",
|
||||
* type="object",
|
||||
* description="출하 상세 정보 (품목 포함)",
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/Shipment"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="items",
|
||||
* type="array",
|
||||
* description="출하 품목 목록",
|
||||
*
|
||||
* @OA\Items(ref="#/components/schemas/ShipmentItem")
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ShipmentStats",
|
||||
* type="object",
|
||||
* description="출하 통계",
|
||||
*
|
||||
* @OA\Property(property="total", type="integer", example=100, description="전체 건수"),
|
||||
* @OA\Property(property="scheduled", type="integer", example=30, description="출고예정 건수"),
|
||||
* @OA\Property(property="ready", type="integer", example=20, description="출하대기 건수"),
|
||||
* @OA\Property(property="shipping", type="integer", example=15, description="배송중 건수"),
|
||||
* @OA\Property(property="completed", type="integer", example=35, description="배송완료 건수"),
|
||||
* @OA\Property(property="urgent", type="integer", example=5, description="긴급 건수"),
|
||||
* @OA\Property(property="today_scheduled", type="integer", example=10, description="오늘 출고예정 건수")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ShipmentStatsByStatus",
|
||||
* type="object",
|
||||
* description="상태별 출하 통계",
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="scheduled",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="label", type="string", example="출고예정"),
|
||||
* @OA\Property(property="count", type="integer", example=30)
|
||||
* ),
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="ready",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="label", type="string", example="출하대기"),
|
||||
* @OA\Property(property="count", type="integer", example=20)
|
||||
* ),
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="shipping",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="label", type="string", example="배송중"),
|
||||
* @OA\Property(property="count", type="integer", example=15)
|
||||
* ),
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="completed",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="label", type="string", example="배송완료"),
|
||||
* @OA\Property(property="count", type="integer", example=35)
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ShipmentStoreRequest",
|
||||
* type="object",
|
||||
* required={"scheduled_date"},
|
||||
*
|
||||
* @OA\Property(property="shipment_no", type="string", example="SHP-20251226-0001", description="출하번호 (자동 생성 가능)"),
|
||||
* @OA\Property(property="lot_no", type="string", example="LOT-001", description="LOT번호"),
|
||||
* @OA\Property(property="order_id", type="integer", example=1, description="수주 ID"),
|
||||
* @OA\Property(property="scheduled_date", type="string", format="date", example="2025-12-26", description="출고예정일"),
|
||||
* @OA\Property(property="status", type="string", enum={"scheduled","ready","shipping","completed"}, example="scheduled", description="상태"),
|
||||
* @OA\Property(property="priority", type="string", enum={"urgent","normal","low"}, example="normal", description="우선순위"),
|
||||
* @OA\Property(property="delivery_method", type="string", enum={"pickup","direct","logistics"}, example="pickup", description="배송방식"),
|
||||
* @OA\Property(property="client_id", type="integer", description="거래처 ID"),
|
||||
* @OA\Property(property="customer_name", type="string", example="(주)고객사", description="발주처명"),
|
||||
* @OA\Property(property="site_name", type="string", example="서울현장", description="현장명"),
|
||||
* @OA\Property(property="delivery_address", type="string", example="서울시 강남구", description="배송주소"),
|
||||
* @OA\Property(property="receiver", type="string", example="홍길동", description="인수자"),
|
||||
* @OA\Property(property="receiver_contact", type="string", example="010-1234-5678", description="인수자 연락처"),
|
||||
* @OA\Property(property="can_ship", type="boolean", example=true, description="출하가능 여부"),
|
||||
* @OA\Property(property="deposit_confirmed", type="boolean", example=true, description="입금확인 여부"),
|
||||
* @OA\Property(property="invoice_issued", type="boolean", example=false, description="세금계산서 발행 여부"),
|
||||
* @OA\Property(property="loading_manager", type="string", description="상차담당자"),
|
||||
* @OA\Property(property="loading_time", type="string", format="date-time", description="상차시간"),
|
||||
* @OA\Property(property="logistics_company", type="string", description="물류사"),
|
||||
* @OA\Property(property="vehicle_tonnage", type="string", description="차량 톤수"),
|
||||
* @OA\Property(property="shipping_cost", type="number", description="운송비"),
|
||||
* @OA\Property(property="vehicle_no", type="string", description="차량번호"),
|
||||
* @OA\Property(property="driver_name", type="string", description="운전자명"),
|
||||
* @OA\Property(property="driver_contact", type="string", description="운전자 연락처"),
|
||||
* @OA\Property(property="expected_arrival", type="string", format="date-time", description="입차예정시간"),
|
||||
* @OA\Property(property="remarks", type="string", description="비고"),
|
||||
* @OA\Property(
|
||||
* property="items",
|
||||
* type="array",
|
||||
* description="출하 품목",
|
||||
*
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="seq", type="integer", example=1),
|
||||
* @OA\Property(property="item_code", type="string", example="ITEM-001"),
|
||||
* @OA\Property(property="item_name", type="string", example="제품 A"),
|
||||
* @OA\Property(property="floor_unit", type="string", example="1층/M01"),
|
||||
* @OA\Property(property="specification", type="string", example="100x200mm"),
|
||||
* @OA\Property(property="quantity", type="number", example=10.5),
|
||||
* @OA\Property(property="unit", type="string", example="EA"),
|
||||
* @OA\Property(property="lot_no", type="string", example="LOT-001"),
|
||||
* @OA\Property(property="stock_lot_id", type="integer"),
|
||||
* @OA\Property(property="remarks", type="string")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ShipmentUpdateStatusRequest",
|
||||
* type="object",
|
||||
* required={"status"},
|
||||
*
|
||||
* @OA\Property(property="status", type="string", enum={"scheduled","ready","shipping","completed"}, example="shipping", description="변경할 상태"),
|
||||
* @OA\Property(property="loading_time", type="string", format="date-time", description="상차시간 (ready 상태 시)"),
|
||||
* @OA\Property(property="loading_completed_at", type="string", format="date-time", description="상차완료 일시 (shipping 상태 시)"),
|
||||
* @OA\Property(property="vehicle_no", type="string", description="차량번호 (shipping 상태 시)"),
|
||||
* @OA\Property(property="driver_name", type="string", description="운전자명 (shipping 상태 시)"),
|
||||
* @OA\Property(property="driver_contact", type="string", description="운전자 연락처 (shipping 상태 시)"),
|
||||
* @OA\Property(property="confirmed_arrival", type="string", format="date-time", description="입차확정시간 (completed 상태 시)")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="LotOption",
|
||||
* type="object",
|
||||
* description="LOT 옵션",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1, description="LOT ID"),
|
||||
* @OA\Property(property="lot_no", type="string", example="251226-01", description="LOT번호"),
|
||||
* @OA\Property(property="item_code", type="string", example="ITEM-001", description="품목코드"),
|
||||
* @OA\Property(property="item_name", type="string", example="원재료 A", description="품목명"),
|
||||
* @OA\Property(property="qty", type="number", format="float", example=100.0, description="수량"),
|
||||
* @OA\Property(property="available_qty", type="number", format="float", example=80.0, description="가용수량"),
|
||||
* @OA\Property(property="unit", type="string", example="EA", description="단위"),
|
||||
* @OA\Property(property="location", type="string", example="A-01-01", description="위치"),
|
||||
* @OA\Property(property="fifo_order", type="integer", example=1, description="FIFO 순서")
|
||||
* )
|
||||
*/
|
||||
class ShipmentApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/shipments",
|
||||
* tags={"Shipments"},
|
||||
* summary="출하 목록 조회",
|
||||
* description="출하 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="search", in="query", description="검색어 (출하번호, LOT번호, 발주처명, 현장명)", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="status", in="query", description="상태", @OA\Schema(type="string", enum={"scheduled","ready","shipping","completed"})),
|
||||
* @OA\Parameter(name="priority", in="query", description="우선순위", @OA\Schema(type="string", enum={"urgent","normal","low"})),
|
||||
* @OA\Parameter(name="delivery_method", in="query", description="배송방식", @OA\Schema(type="string", enum={"pickup","direct","logistics"})),
|
||||
* @OA\Parameter(name="scheduled_from", in="query", description="예정일 시작", @OA\Schema(type="string", format="date")),
|
||||
* @OA\Parameter(name="scheduled_to", in="query", description="예정일 종료", @OA\Schema(type="string", format="date")),
|
||||
* @OA\Parameter(name="can_ship", in="query", description="출하가능 여부", @OA\Schema(type="boolean")),
|
||||
* @OA\Parameter(name="deposit_confirmed", in="query", description="입금확인 여부", @OA\Schema(type="boolean")),
|
||||
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"scheduled_date","shipment_no","customer_name","priority"}, default="scheduled_date")),
|
||||
* @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/ShipmentWithItems")),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=100)
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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/shipments/stats",
|
||||
* tags={"Shipments"},
|
||||
* 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/ShipmentStats")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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\Get(
|
||||
* path="/api/v1/shipments/stats-by-status",
|
||||
* tags={"Shipments"},
|
||||
* 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/ShipmentStatsByStatus")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function statsByStatus() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/shipments/{id}",
|
||||
* tags={"Shipments"},
|
||||
* 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/ShipmentWithItems")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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\Post(
|
||||
* path="/api/v1/shipments",
|
||||
* tags={"Shipments"},
|
||||
* summary="출하 생성",
|
||||
* description="새 출하를 생성합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ShipmentStoreRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="생성 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ShipmentWithItems")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=422, description="유효성 검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/shipments/{id}",
|
||||
* tags={"Shipments"},
|
||||
* summary="출하 수정",
|
||||
* description="출하 정보를 수정합니다.",
|
||||
* 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/ShipmentStoreRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ShipmentWithItems")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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=422, description="유효성 검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/shipments/{id}/status",
|
||||
* tags={"Shipments"},
|
||||
* summary="출하 상태 변경",
|
||||
* description="출하 상태를 변경합니다.",
|
||||
* 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/ShipmentUpdateStatusRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="상태 변경 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ShipmentWithItems")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @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=422, description="유효성 검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function updateStatus() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/shipments/{id}",
|
||||
* tags={"Shipments"},
|
||||
* 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(ref="#/components/schemas/ApiResponse")
|
||||
* ),
|
||||
*
|
||||
* @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\Get(
|
||||
* path="/api/v1/shipments/options/lots",
|
||||
* tags={"Shipments"},
|
||||
* summary="LOT 옵션 조회",
|
||||
* description="출고 가능한 LOT 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/LotOption"))
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function lotOptions() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/shipments/options/logistics",
|
||||
* tags={"Shipments"},
|
||||
* summary="물류사 옵션 조회",
|
||||
* description="물류사 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="array",
|
||||
*
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="value", type="string", example="CJ대한통운"),
|
||||
* @OA\Property(property="label", type="string", example="CJ대한통운")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function logisticsOptions() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/shipments/options/vehicle-tonnage",
|
||||
* tags={"Shipments"},
|
||||
* summary="차량 톤수 옵션 조회",
|
||||
* description="차량 톤수 목록을 조회합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="array",
|
||||
*
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="value", type="string", example="5톤"),
|
||||
* @OA\Property(property="label", type="string", example="5톤")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function vehicleTonnageOptions() {}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shipments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->comment('테넌트 ID');
|
||||
$table->string('shipment_no', 50)->comment('출고번호');
|
||||
$table->string('lot_no', 50)->nullable()->comment('LOT번호');
|
||||
$table->foreignId('order_id')->nullable()->comment('수주 ID');
|
||||
$table->date('scheduled_date')->comment('출고예정일');
|
||||
$table->enum('status', ['scheduled', 'ready', 'shipping', 'completed'])->default('scheduled')->comment('상태: scheduled=출고예정, ready=출하대기, shipping=배송중, completed=배송완료');
|
||||
$table->enum('priority', ['urgent', 'normal', 'low'])->default('normal')->comment('우선순위');
|
||||
$table->enum('delivery_method', ['pickup', 'direct', 'logistics'])->default('pickup')->comment('배송방식: pickup=상차, direct=직접배차, logistics=물류사');
|
||||
|
||||
// 발주처/배송 정보
|
||||
$table->foreignId('client_id')->nullable()->comment('거래처 ID');
|
||||
$table->string('customer_name', 100)->nullable()->comment('발주처명');
|
||||
$table->string('site_name', 100)->nullable()->comment('현장명');
|
||||
$table->string('delivery_address', 255)->nullable()->comment('배송주소');
|
||||
$table->string('receiver', 50)->nullable()->comment('인수자');
|
||||
$table->string('receiver_contact', 50)->nullable()->comment('인수자 연락처');
|
||||
|
||||
// 상태 플래그
|
||||
$table->boolean('can_ship')->default(false)->comment('출하가능 여부');
|
||||
$table->boolean('deposit_confirmed')->default(false)->comment('입금확인 여부');
|
||||
$table->boolean('invoice_issued')->default(false)->comment('세금계산서 발행 여부');
|
||||
$table->string('customer_grade', 20)->nullable()->comment('거래처 등급');
|
||||
|
||||
// 상차 정보
|
||||
$table->string('loading_manager', 50)->nullable()->comment('상차담당자');
|
||||
$table->datetime('loading_completed_at')->nullable()->comment('상차완료 일시');
|
||||
$table->datetime('loading_time')->nullable()->comment('상차시간(입차예정)');
|
||||
|
||||
// 물류/배차 정보
|
||||
$table->string('logistics_company', 50)->nullable()->comment('물류사');
|
||||
$table->string('vehicle_tonnage', 20)->nullable()->comment('차량 톤수');
|
||||
$table->decimal('shipping_cost', 12, 0)->nullable()->comment('운송비');
|
||||
|
||||
// 차량/운전자 정보
|
||||
$table->string('vehicle_no', 20)->nullable()->comment('차량번호');
|
||||
$table->string('driver_name', 50)->nullable()->comment('운전자명');
|
||||
$table->string('driver_contact', 50)->nullable()->comment('운전자 연락처');
|
||||
$table->datetime('expected_arrival')->nullable()->comment('입차예정시간');
|
||||
$table->datetime('confirmed_arrival')->nullable()->comment('입차확정시간');
|
||||
|
||||
// 기타
|
||||
$table->text('remarks')->nullable()->comment('비고');
|
||||
$table->foreignId('created_by')->nullable()->comment('등록자');
|
||||
$table->foreignId('updated_by')->nullable()->comment('수정자');
|
||||
$table->foreignId('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->unique(['tenant_id', 'shipment_no']);
|
||||
$table->index(['tenant_id', 'status']);
|
||||
$table->index(['tenant_id', 'scheduled_date']);
|
||||
$table->index(['tenant_id', 'lot_no']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shipments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shipment_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->comment('테넌트 ID');
|
||||
$table->foreignId('shipment_id')->comment('출하 ID');
|
||||
$table->integer('seq')->default(1)->comment('순번');
|
||||
$table->string('item_code', 50)->nullable()->comment('품목코드');
|
||||
$table->string('item_name', 100)->comment('품목명');
|
||||
$table->string('floor_unit', 50)->nullable()->comment('층/M호');
|
||||
$table->string('specification', 100)->nullable()->comment('규격');
|
||||
$table->decimal('quantity', 10, 2)->default(0)->comment('수량');
|
||||
$table->string('unit', 20)->nullable()->comment('단위');
|
||||
$table->string('lot_no', 50)->nullable()->comment('LOT번호');
|
||||
$table->foreignId('stock_lot_id')->nullable()->comment('재고 LOT ID');
|
||||
$table->text('remarks')->nullable()->comment('비고');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['shipment_id', 'seq']);
|
||||
$table->index(['tenant_id', 'item_code']);
|
||||
|
||||
// 외래키
|
||||
$table->foreign('shipment_id')->references('id')->on('shipments')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shipment_items');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user