fix(WEB): 수주 등록/수정 옵션 필드 저장 및 담당자 표시 문제 해결
- FormRequest에 options 필드 validation 추가 (StoreOrderRequest, UpdateOrderRequest) - shipping_cost_code, receiver, receiver_contact, shipping_address 등 - OrderService.show()에서 client 로드 시 manager_name 필드 추가 - 수주확정/생산지시 되돌리기 기능 추가 (revertOrderConfirmation, revertProductionOrder) - 견적 calculation_inputs 포함하여 로드 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-01-16 14:58:07
|
||||
> **자동 생성**: 2026-01-16 20:48:14
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
|
||||
@@ -107,4 +107,24 @@ public function createProductionOrder(CreateProductionOrderRequest $request, int
|
||||
return $this->service->createProductionOrder($id, $request->validated());
|
||||
}, __('message.order.production_order_created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주확정 되돌리기 (수주등록 상태로 변경)
|
||||
*/
|
||||
public function revertOrderConfirmation(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->revertOrderConfirmation($id);
|
||||
}, __('message.order.order_confirmation_reverted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산지시 되돌리기 (작업지시 및 관련 데이터 삭제)
|
||||
*/
|
||||
public function revertProductionOrder(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->revertProductionOrder($id);
|
||||
}, __('message.order.production_order_reverted'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,19 @@ public function store(QuoteStoreRequest $request)
|
||||
*/
|
||||
public function update(QuoteUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->quoteService->update($id, $request->validated());
|
||||
$validated = $request->validated();
|
||||
|
||||
// 🔍 디버깅: 요청 데이터 확인
|
||||
\Log::info('🔍 [QuoteController::update] 요청 수신', [
|
||||
'id' => $id,
|
||||
'raw_options_keys' => $request->input('options') ? array_keys($request->input('options')) : null,
|
||||
'raw_options_detail_items_count' => $request->input('options.detail_items') ? count($request->input('options.detail_items')) : 0,
|
||||
'validated_options_keys' => isset($validated['options']) ? array_keys($validated['options']) : null,
|
||||
'validated_options_detail_items_count' => isset($validated['options']['detail_items']) ? count($validated['options']['detail_items']) : 0,
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($validated, $id) {
|
||||
return $this->quoteService->update($id, $validated);
|
||||
}, __('message.quote.updated'));
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,14 @@ public function rules(): array
|
||||
'remarks' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
|
||||
// 옵션 (운임비용, 수신자 정보 등)
|
||||
'options' => 'nullable|array',
|
||||
'options.shipping_cost_code' => 'nullable|string|max:50',
|
||||
'options.receiver' => 'nullable|string|max:100',
|
||||
'options.receiver_contact' => 'nullable|string|max:100',
|
||||
'options.shipping_address' => 'nullable|string|max:500',
|
||||
'options.shipping_address_detail' => 'nullable|string|max:500',
|
||||
|
||||
// 품목 배열
|
||||
'items' => 'nullable|array',
|
||||
'items.*.item_id' => 'nullable|integer|exists:items,id',
|
||||
|
||||
@@ -41,6 +41,14 @@ public function rules(): array
|
||||
'remarks' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
|
||||
// 옵션 (운임비용, 수신자 정보 등)
|
||||
'options' => 'nullable|array',
|
||||
'options.shipping_cost_code' => 'nullable|string|max:50',
|
||||
'options.receiver' => 'nullable|string|max:100',
|
||||
'options.receiver_contact' => 'nullable|string|max:100',
|
||||
'options.shipping_address' => 'nullable|string|max:500',
|
||||
'options.shipping_address_detail' => 'nullable|string|max:500',
|
||||
|
||||
// 품목 배열 (전체 교체)
|
||||
'items' => 'nullable|array',
|
||||
'items.*.item_id' => 'nullable|integer|exists:items,id',
|
||||
|
||||
@@ -65,6 +65,7 @@ class Order extends Model
|
||||
'memo',
|
||||
'remarks',
|
||||
'note',
|
||||
'options',
|
||||
// 감사
|
||||
'created_by',
|
||||
'updated_by',
|
||||
@@ -80,6 +81,7 @@ class Order extends Model
|
||||
'discount_amount' => 'decimal:2',
|
||||
'received_at' => 'datetime',
|
||||
'delivery_date' => 'date',
|
||||
'options' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
|
||||
@@ -33,6 +33,9 @@ class OrderItem extends Model
|
||||
'item_code',
|
||||
'item_name',
|
||||
'specification',
|
||||
// 제품-부품 매핑용 코드
|
||||
'floor_code',
|
||||
'symbol_code',
|
||||
'unit',
|
||||
// 수량/금액
|
||||
'quantity',
|
||||
@@ -153,8 +156,9 @@ public function recalculateAmounts(): self
|
||||
* 견적 품목에서 수주 품목 생성
|
||||
*
|
||||
* @param int $serialIndex 품목 순번 (1부터 시작)
|
||||
* @param array $productMapping 제품 매핑 정보 ['floor_code' => '10', 'symbol_code' => 'F1']
|
||||
*/
|
||||
public static function createFromQuoteItem(QuoteItem $quoteItem, int $orderId, int $serialIndex = 1): self
|
||||
public static function createFromQuoteItem(QuoteItem $quoteItem, int $orderId, int $serialIndex = 1, array $productMapping = []): self
|
||||
{
|
||||
$qty = $quoteItem->calculated_quantity ?? 1;
|
||||
$supplyAmount = $quoteItem->unit_price * $qty;
|
||||
@@ -170,6 +174,9 @@ public static function createFromQuoteItem(QuoteItem $quoteItem, int $orderId, i
|
||||
'item_code' => $quoteItem->item_code,
|
||||
'item_name' => $quoteItem->item_name,
|
||||
'specification' => $quoteItem->specification,
|
||||
// 제품-부품 매핑 코드
|
||||
'floor_code' => $productMapping['floor_code'] ?? null,
|
||||
'symbol_code' => $productMapping['symbol_code'] ?? null,
|
||||
'unit' => $quoteItem->unit ?? 'EA',
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $quoteItem->unit_price,
|
||||
|
||||
@@ -118,9 +118,9 @@ public function show(int $id)
|
||||
|
||||
$order = Order::where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'client:id,name,contact_person,phone,email',
|
||||
'client:id,name,contact_person,phone,email,manager_name',
|
||||
'items' => fn ($q) => $q->orderBy('sort_order'),
|
||||
'quote:id,quote_number,site_name',
|
||||
'quote:id,quote_number,site_name,calculation_inputs',
|
||||
])
|
||||
->find($id);
|
||||
|
||||
@@ -518,4 +518,101 @@ private function generateWorkOrderNo(int $tenantId): string
|
||||
|
||||
return sprintf('%s%s%04d', $prefix, $date, $seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주확정 되돌리기 (수주등록 상태로 변경)
|
||||
*/
|
||||
public function revertOrderConfirmation(int $orderId): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
// 수주 조회
|
||||
$order = Order::where('tenant_id', $tenantId)
|
||||
->find($orderId);
|
||||
|
||||
if (! $order) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 수주확정 상태에서만 되돌리기 가능
|
||||
if ($order->status_code !== Order::STATUS_CONFIRMED) {
|
||||
throw new BadRequestHttpException(__('error.order.cannot_revert_not_confirmed'));
|
||||
}
|
||||
|
||||
// 상태 변경
|
||||
$previousStatus = $order->status_code;
|
||||
$order->status_code = Order::STATUS_DRAFT;
|
||||
$order->updated_by = $userId;
|
||||
$order->save();
|
||||
|
||||
return [
|
||||
'order' => $order->load(['client:id,name', 'items']),
|
||||
'previous_status' => $previousStatus,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산지시 되돌리기 (작업지시 및 관련 데이터 삭제)
|
||||
*/
|
||||
public function revertProductionOrder(int $orderId): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
// 수주 조회
|
||||
$order = Order::where('tenant_id', $tenantId)
|
||||
->find($orderId);
|
||||
|
||||
if (! $order) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 완료된 수주는 되돌리기 불가
|
||||
if ($order->status_code === Order::STATUS_COMPLETED) {
|
||||
throw new BadRequestHttpException(__('error.order.cannot_revert_completed'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($order, $tenantId, $userId) {
|
||||
// 관련 작업지시 ID 조회
|
||||
$workOrderIds = WorkOrder::where('tenant_id', $tenantId)
|
||||
->where('sales_order_id', $order->id)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
$deletedCounts = [
|
||||
'work_results' => 0,
|
||||
'work_order_items' => 0,
|
||||
'work_orders' => 0,
|
||||
];
|
||||
|
||||
if (count($workOrderIds) > 0) {
|
||||
// 1. 작업결과 삭제
|
||||
$deletedCounts['work_results'] = DB::table('work_results')
|
||||
->whereIn('work_order_id', $workOrderIds)
|
||||
->delete();
|
||||
|
||||
// 2. 작업지시 품목 삭제
|
||||
$deletedCounts['work_order_items'] = DB::table('work_order_items')
|
||||
->whereIn('work_order_id', $workOrderIds)
|
||||
->delete();
|
||||
|
||||
// 3. 작업지시 삭제
|
||||
$deletedCounts['work_orders'] = WorkOrder::whereIn('id', $workOrderIds)
|
||||
->delete();
|
||||
}
|
||||
|
||||
// 4. 수주 상태를 CONFIRMED로 되돌리기
|
||||
$previousStatus = $order->status_code;
|
||||
$order->status_code = Order::STATUS_CONFIRMED;
|
||||
$order->updated_by = $userId;
|
||||
$order->save();
|
||||
|
||||
return [
|
||||
'order' => $order->load(['client:id,name', 'items']),
|
||||
'deleted_counts' => $deletedCounts,
|
||||
'previous_status' => $previousStatus,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,9 +359,7 @@ public function update(int $id, array $data): Quote
|
||||
'calculation_inputs' => $data['calculation_inputs'] ?? $quote->calculation_inputs,
|
||||
// 견적 옵션 (summary_items, expense_items, price_adjustments, detail_items, price_adjustment_data)
|
||||
// 기존 options와 새 options를 병합 (새 데이터가 기존 데이터를 덮어씀)
|
||||
'options' => isset($data['options'])
|
||||
? array_merge($quote->options ?? [], $data['options'])
|
||||
: $quote->options,
|
||||
'options' => $this->mergeOptions($quote->options, $data['options'] ?? null),
|
||||
// 감사
|
||||
'updated_by' => $userId,
|
||||
'current_revision' => $quote->current_revision + 1,
|
||||
@@ -711,4 +709,38 @@ public function findBySiteBriefingId(int $siteBriefingId): ?Quote
|
||||
->where('site_briefing_id', $siteBriefingId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 options 병합
|
||||
* 기존 options와 새 options를 병합하여 반환
|
||||
*/
|
||||
private function mergeOptions(?array $existingOptions, ?array $newOptions): ?array
|
||||
{
|
||||
\Log::info('🔍 [QuoteService::mergeOptions] 시작', [
|
||||
'existingOptions_keys' => $existingOptions ? array_keys($existingOptions) : null,
|
||||
'newOptions_keys' => $newOptions ? array_keys($newOptions) : null,
|
||||
'newOptions_detail_items_count' => isset($newOptions['detail_items']) ? count($newOptions['detail_items']) : 0,
|
||||
'newOptions_price_adjustment_data' => isset($newOptions['price_adjustment_data']) ? 'exists' : 'null',
|
||||
]);
|
||||
|
||||
if ($newOptions === null) {
|
||||
return $existingOptions;
|
||||
}
|
||||
|
||||
if ($existingOptions === null) {
|
||||
\Log::info('✅ [QuoteService::mergeOptions] 기존 없음, 새 options 반환', [
|
||||
'result_keys' => array_keys($newOptions),
|
||||
]);
|
||||
return $newOptions;
|
||||
}
|
||||
|
||||
$merged = array_merge($existingOptions, $newOptions);
|
||||
|
||||
\Log::info('✅ [QuoteService::mergeOptions] 병합 완료', [
|
||||
'merged_keys' => array_keys($merged),
|
||||
'merged_detail_items_count' => isset($merged['detail_items']) ? count($merged['detail_items']) : 0,
|
||||
]);
|
||||
|
||||
return $merged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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::table('orders', function (Blueprint $table) {
|
||||
$table->json('options')->nullable()->after('note')->comment('추가 옵션 (운임비용, 수신자, 수신처 연락처, 주소 등)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('orders', function (Blueprint $table) {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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::table('order_items', function (Blueprint $table) {
|
||||
$table->string('floor_code', 50)->nullable()->after('specification')->comment('층 코드 (제품-부품 매핑용)');
|
||||
$table->string('symbol_code', 50)->nullable()->after('floor_code')->comment('부호 코드 (제품-부품 매핑용)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
$table->dropColumn(['floor_code', 'symbol_code']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -366,6 +366,8 @@
|
||||
'already_created_from_quote' => '이미 해당 견적에서 수주가 생성되었습니다.',
|
||||
'must_be_confirmed_for_production' => '확정 상태의 수주만 생산지시를 생성할 수 있습니다.',
|
||||
'production_order_already_exists' => '이미 생산지시가 존재합니다.',
|
||||
'cannot_revert_completed' => '완료된 수주의 생산지시는 되돌릴 수 없습니다.',
|
||||
'cannot_revert_not_confirmed' => '수주확정 상태에서만 되돌리기가 가능합니다.',
|
||||
],
|
||||
|
||||
// 견적 관련
|
||||
|
||||
@@ -456,5 +456,7 @@
|
||||
'status_updated' => '수주 상태가 변경되었습니다.',
|
||||
'created_from_quote' => '견적에서 수주가 생성되었습니다.',
|
||||
'production_order_created' => '생산지시가 생성되었습니다.',
|
||||
'production_order_reverted' => '생산지시가 되돌려졌습니다.',
|
||||
'order_confirmation_reverted' => '수주확정이 취소되었습니다.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1167,6 +1167,12 @@
|
||||
|
||||
// 생산지시 생성
|
||||
Route::post('/{id}/production-order', [OrderController::class, 'createProductionOrder'])->whereNumber('id')->name('v1.orders.production-order');
|
||||
|
||||
// 수주확정 되돌리기
|
||||
Route::post('/{id}/revert-confirmation', [OrderController::class, 'revertOrderConfirmation'])->whereNumber('id')->name('v1.orders.revert-confirmation');
|
||||
|
||||
// 생산지시 되돌리기
|
||||
Route::post('/{id}/revert-production', [OrderController::class, 'revertProductionOrder'])->whereNumber('id')->name('v1.orders.revert-production');
|
||||
});
|
||||
|
||||
// 작업지시 관리 API (Production)
|
||||
|
||||
Reference in New Issue
Block a user