diff --git a/app/Http/Controllers/Api/Admin/Quote/QuoteFormulaItemController.php b/app/Http/Controllers/Api/Admin/Quote/QuoteFormulaItemController.php new file mode 100644 index 00000000..6f692fc7 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Quote/QuoteFormulaItemController.php @@ -0,0 +1,153 @@ +itemService->getItemsByFormula($formulaId); + + return response()->json([ + 'success' => true, + 'data' => $items, + ]); + } + + /** + * 품목 상세 조회 + */ + public function show(int $formulaId, int $itemId): JsonResponse + { + // 수식 소속 확인 + if (! $this->itemService->belongsToFormula($itemId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '해당 수식에 속하지 않는 품목입니다.', + ], 404); + } + + $item = $this->itemService->getItemById($itemId); + + return response()->json([ + 'success' => true, + 'data' => $item, + ]); + } + + /** + * 품목 생성 + */ + public function store(Request $request, int $formulaId): JsonResponse + { + $validated = $request->validate([ + 'item_code' => 'required|string|max:50', + 'item_name' => 'required|string|max:200', + 'specification' => 'nullable|string|max:200', + 'unit' => 'nullable|string|max:20', + 'quantity_formula' => 'nullable|string|max:500', + 'unit_price_formula' => 'nullable|string|max:500', + 'sort_order' => 'nullable|integer|min:0', + ]); + + $item = $this->itemService->createItem($formulaId, $validated); + + return response()->json([ + 'success' => true, + 'message' => '품목이 추가되었습니다.', + 'data' => $item, + ]); + } + + /** + * 품목 수정 + */ + public function update(Request $request, int $formulaId, int $itemId): JsonResponse + { + // 수식 소속 확인 + if (! $this->itemService->belongsToFormula($itemId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '해당 수식에 속하지 않는 품목입니다.', + ], 404); + } + + $validated = $request->validate([ + 'item_code' => 'nullable|string|max:50', + 'item_name' => 'nullable|string|max:200', + 'specification' => 'nullable|string|max:200', + 'unit' => 'nullable|string|max:20', + 'quantity_formula' => 'nullable|string|max:500', + 'unit_price_formula' => 'nullable|string|max:500', + ]); + + $item = $this->itemService->updateItem($itemId, $validated); + + return response()->json([ + 'success' => true, + 'message' => '품목이 수정되었습니다.', + 'data' => $item, + ]); + } + + /** + * 품목 삭제 + */ + public function destroy(int $formulaId, int $itemId): JsonResponse + { + // 수식 소속 확인 + if (! $this->itemService->belongsToFormula($itemId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '해당 수식에 속하지 않는 품목입니다.', + ], 404); + } + + $this->itemService->deleteItem($itemId); + + return response()->json([ + 'success' => true, + 'message' => '품목이 삭제되었습니다.', + ]); + } + + /** + * 순서 변경 + */ + public function reorder(Request $request, int $formulaId): JsonResponse + { + $validated = $request->validate([ + 'item_ids' => 'required|array', + 'item_ids.*' => 'integer', + ]); + + // 모든 품목이 해당 수식에 속하는지 확인 + foreach ($validated['item_ids'] as $itemId) { + if (! $this->itemService->belongsToFormula($itemId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '유효하지 않은 품목 ID가 포함되어 있습니다.', + ], 400); + } + } + + $this->itemService->reorder($validated['item_ids']); + + return response()->json([ + 'success' => true, + 'message' => '순서가 변경되었습니다.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/Quote/QuoteFormulaMappingController.php b/app/Http/Controllers/Api/Admin/Quote/QuoteFormulaMappingController.php new file mode 100644 index 00000000..7d0c64a0 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Quote/QuoteFormulaMappingController.php @@ -0,0 +1,149 @@ +mappingService->getMappingsByFormula($formulaId); + + return response()->json([ + 'success' => true, + 'data' => $mappings, + ]); + } + + /** + * 매핑 상세 조회 + */ + public function show(int $formulaId, int $mappingId): JsonResponse + { + // 수식 소속 확인 + if (! $this->mappingService->belongsToFormula($mappingId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '해당 수식에 속하지 않는 매핑입니다.', + ], 404); + } + + $mapping = $this->mappingService->getMappingById($mappingId); + + return response()->json([ + 'success' => true, + 'data' => $mapping, + ]); + } + + /** + * 매핑 생성 + */ + public function store(Request $request, int $formulaId): JsonResponse + { + $validated = $request->validate([ + 'source_variable' => 'required|string|max:50', + 'source_value' => 'required|string|max:100', + 'result_value' => 'required|string', + 'result_type' => 'nullable|in:fixed,formula', + 'sort_order' => 'nullable|integer|min:0', + ]); + + $mapping = $this->mappingService->createMapping($formulaId, $validated); + + return response()->json([ + 'success' => true, + 'message' => '매핑이 추가되었습니다.', + 'data' => $mapping, + ]); + } + + /** + * 매핑 수정 + */ + public function update(Request $request, int $formulaId, int $mappingId): JsonResponse + { + // 수식 소속 확인 + if (! $this->mappingService->belongsToFormula($mappingId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '해당 수식에 속하지 않는 매핑입니다.', + ], 404); + } + + $validated = $request->validate([ + 'source_variable' => 'nullable|string|max:50', + 'source_value' => 'nullable|string|max:100', + 'result_value' => 'nullable|string', + 'result_type' => 'nullable|in:fixed,formula', + ]); + + $mapping = $this->mappingService->updateMapping($mappingId, $validated); + + return response()->json([ + 'success' => true, + 'message' => '매핑이 수정되었습니다.', + 'data' => $mapping, + ]); + } + + /** + * 매핑 삭제 + */ + public function destroy(int $formulaId, int $mappingId): JsonResponse + { + // 수식 소속 확인 + if (! $this->mappingService->belongsToFormula($mappingId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '해당 수식에 속하지 않는 매핑입니다.', + ], 404); + } + + $this->mappingService->deleteMapping($mappingId); + + return response()->json([ + 'success' => true, + 'message' => '매핑이 삭제되었습니다.', + ]); + } + + /** + * 순서 변경 + */ + public function reorder(Request $request, int $formulaId): JsonResponse + { + $validated = $request->validate([ + 'mapping_ids' => 'required|array', + 'mapping_ids.*' => 'integer', + ]); + + // 모든 매핑이 해당 수식에 속하는지 확인 + foreach ($validated['mapping_ids'] as $mappingId) { + if (! $this->mappingService->belongsToFormula($mappingId, $formulaId)) { + return response()->json([ + 'success' => false, + 'message' => '유효하지 않은 매핑 ID가 포함되어 있습니다.', + ], 400); + } + } + + $this->mappingService->reorder($validated['mapping_ids']); + + return response()->json([ + 'success' => true, + 'message' => '순서가 변경되었습니다.', + ]); + } +} diff --git a/app/Services/Quote/QuoteFormulaItemService.php b/app/Services/Quote/QuoteFormulaItemService.php new file mode 100644 index 00000000..29d3ed16 --- /dev/null +++ b/app/Services/Quote/QuoteFormulaItemService.php @@ -0,0 +1,97 @@ +orderBy('sort_order') + ->get(); + } + + /** + * 품목 상세 조회 + */ + public function getItemById(int $id): ?QuoteFormulaItem + { + return QuoteFormulaItem::find($id); + } + + /** + * 품목 생성 + */ + public function createItem(int $formulaId, array $data): QuoteFormulaItem + { + // 순서 자동 설정 + if (! isset($data['sort_order'])) { + $maxOrder = QuoteFormulaItem::where('formula_id', $formulaId)->max('sort_order') ?? 0; + $data['sort_order'] = $maxOrder + 1; + } + + return QuoteFormulaItem::create([ + 'formula_id' => $formulaId, + 'item_code' => $data['item_code'], + 'item_name' => $data['item_name'], + 'specification' => $data['specification'] ?? null, + 'unit' => $data['unit'] ?? 'EA', + 'quantity_formula' => $data['quantity_formula'] ?? '1', + 'unit_price_formula' => $data['unit_price_formula'] ?? null, + 'sort_order' => $data['sort_order'], + ]); + } + + /** + * 품목 수정 + */ + public function updateItem(int $itemId, array $data): QuoteFormulaItem + { + $item = QuoteFormulaItem::findOrFail($itemId); + + $item->update([ + 'item_code' => $data['item_code'] ?? $item->item_code, + 'item_name' => $data['item_name'] ?? $item->item_name, + 'specification' => $data['specification'] ?? $item->specification, + 'unit' => $data['unit'] ?? $item->unit, + 'quantity_formula' => $data['quantity_formula'] ?? $item->quantity_formula, + 'unit_price_formula' => $data['unit_price_formula'] ?? $item->unit_price_formula, + ]); + + return $item->fresh(); + } + + /** + * 품목 삭제 + */ + public function deleteItem(int $itemId): void + { + QuoteFormulaItem::destroy($itemId); + } + + /** + * 순서 변경 + */ + public function reorder(array $itemIds): void + { + foreach ($itemIds as $order => $id) { + QuoteFormulaItem::where('id', $id)->update(['sort_order' => $order + 1]); + } + } + + /** + * 품목이 수식에 속하는지 확인 + */ + public function belongsToFormula(int $itemId, int $formulaId): bool + { + return QuoteFormulaItem::where('id', $itemId) + ->where('formula_id', $formulaId) + ->exists(); + } +} diff --git a/app/Services/Quote/QuoteFormulaMappingService.php b/app/Services/Quote/QuoteFormulaMappingService.php new file mode 100644 index 00000000..d6ee4673 --- /dev/null +++ b/app/Services/Quote/QuoteFormulaMappingService.php @@ -0,0 +1,93 @@ +orderBy('sort_order') + ->get(); + } + + /** + * 매핑 상세 조회 + */ + public function getMappingById(int $id): ?QuoteFormulaMapping + { + return QuoteFormulaMapping::find($id); + } + + /** + * 매핑 생성 + */ + public function createMapping(int $formulaId, array $data): QuoteFormulaMapping + { + // 순서 자동 설정 + if (! isset($data['sort_order'])) { + $maxOrder = QuoteFormulaMapping::where('formula_id', $formulaId)->max('sort_order') ?? 0; + $data['sort_order'] = $maxOrder + 1; + } + + return QuoteFormulaMapping::create([ + 'formula_id' => $formulaId, + 'source_variable' => $data['source_variable'], + 'source_value' => $data['source_value'], + 'result_value' => $data['result_value'], + 'result_type' => $data['result_type'] ?? 'fixed', + 'sort_order' => $data['sort_order'], + ]); + } + + /** + * 매핑 수정 + */ + public function updateMapping(int $mappingId, array $data): QuoteFormulaMapping + { + $mapping = QuoteFormulaMapping::findOrFail($mappingId); + + $mapping->update([ + 'source_variable' => $data['source_variable'] ?? $mapping->source_variable, + 'source_value' => $data['source_value'] ?? $mapping->source_value, + 'result_value' => $data['result_value'] ?? $mapping->result_value, + 'result_type' => $data['result_type'] ?? $mapping->result_type, + ]); + + return $mapping->fresh(); + } + + /** + * 매핑 삭제 + */ + public function deleteMapping(int $mappingId): void + { + QuoteFormulaMapping::destroy($mappingId); + } + + /** + * 순서 변경 + */ + public function reorder(array $mappingIds): void + { + foreach ($mappingIds as $order => $id) { + QuoteFormulaMapping::where('id', $id)->update(['sort_order' => $order + 1]); + } + } + + /** + * 매핑이 수식에 속하는지 확인 + */ + public function belongsToFormula(int $mappingId, int $formulaId): bool + { + return QuoteFormulaMapping::where('id', $mappingId) + ->where('formula_id', $formulaId) + ->exists(); + } +} diff --git a/resources/views/quote-formulas/edit.blade.php b/resources/views/quote-formulas/edit.blade.php index 1532b1ea..5fb6ddbf 100644 --- a/resources/views/quote-formulas/edit.blade.php +++ b/resources/views/quote-formulas/edit.blade.php @@ -47,14 +47,14 @@ class="py-4 px-6 border-b-2 font-medium text-sm transition-colors flex items-cen class="py-4 px-6 border-b-2 font-medium text-sm transition-colors flex items-center gap-2" x-show="formula?.type === 'mapping'"> 매핑 설정 - + @@ -71,26 +71,14 @@ class="py-4 px-6 border-b-2 font-medium text-sm transition-colors flex items-cen @include('quote-formulas.partials.ranges-tab') - +
매핑 관리 UI는 준비 중입니다.
-Phase 2에서 구현 예정
-품목 관리 UI는 준비 중입니다.
-Phase 3에서 구현 예정
-수식 결과로 생성되는 출력 품목을 정의합니다.
+| # | +품목코드 | +품목명 | +규격 | +단위 | +수량식 | +단가식 | +액션 | +
|---|---|---|---|---|---|---|---|
| + | + + | ++ | + | + | + + | ++ + | ++ + + | +
설정된 품목이 없습니다
+위의 "품목 추가" 버튼을 클릭하여 첫 번째 품목을 추가하세요.
+예시: 모터 품목
+| 품목코드 | PT-MOTOR-150 |
| 품목명 | 개폐전동기 150kg |
| 규격 | 150K(S) |
| 수량식 | 1 |
| 단가식 | 285000 (또는 마스터 참조) |
소스 변수의 값에 따라 특정 결과를 반환합니다.
+매핑에 사용할 변수명을 입력하세요 (예: 제어기 유형 CONTROL_TYPE)
+| # | +소스값 | +결과값 | +유형 | +액션 | +
|---|---|---|---|---|
| + | + + | ++ + | ++ + | ++ + + | +
설정된 매핑이 없습니다
+위의 "매핑 추가" 버튼을 클릭하여 첫 번째 매핑을 추가하세요.
+예시: 제어기 유형 매핑 (소스 변수: CONTROL_TYPE)
+| EMB | 매립형 |
| EXP | 노출형 |
| BOX_1P | 콘트롤박스 |