feat: 견적 참조 데이터 API, 수주 전환 로직 개선, 검사기준서 필드 통합
- 견적 참조 데이터(현장명, 부호) 조회 API 추가 (GET /quotes/reference-data) - 수주 전환 시 floor_code/symbol_code를 quoteItem.note에서 파싱하도록 변경 - 수주 전환 시 note에 formula_category 저장 - 검사기준서 프리셋: standard + standard_criteria → text_with_criteria로 통합 - tolerance 컬럼 width 조정 (120px → 85px) - LOGICAL_RELATIONSHIPS.md 문서 갱신 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-02-03 17:09:34
|
||||
> **자동 생성**: 2026-02-03 17:39:58
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -224,6 +224,12 @@ ### document_datas
|
||||
|
||||
- **document()**: belongsTo → `documents`
|
||||
|
||||
### document_links
|
||||
**모델**: `App\Models\Documents\DocumentLink`
|
||||
|
||||
- **document()**: belongsTo → `documents`
|
||||
- **linkDefinition()**: belongsTo → `document_template_links`
|
||||
|
||||
### document_templates
|
||||
**모델**: `App\Models\Documents\DocumentTemplate`
|
||||
|
||||
@@ -231,6 +237,8 @@ ### document_templates
|
||||
- **basicFields()**: hasMany → `document_template_basic_fields`
|
||||
- **sections()**: hasMany → `document_template_sections`
|
||||
- **columns()**: hasMany → `document_template_columns`
|
||||
- **sectionFields()**: hasMany → `document_template_section_fields`
|
||||
- **links()**: hasMany → `document_template_links`
|
||||
|
||||
### document_template_approval_lines
|
||||
**모델**: `App\Models\Documents\DocumentTemplateApprovalLine`
|
||||
@@ -247,12 +255,29 @@ ### document_template_columns
|
||||
|
||||
- **template()**: belongsTo → `document_templates`
|
||||
|
||||
### document_template_links
|
||||
**모델**: `App\Models\Documents\DocumentTemplateLink`
|
||||
|
||||
- **template()**: belongsTo → `document_templates`
|
||||
- **linkValues()**: hasMany → `document_template_link_values`
|
||||
|
||||
### document_template_link_values
|
||||
**모델**: `App\Models\Documents\DocumentTemplateLinkValue`
|
||||
|
||||
- **template()**: belongsTo → `document_templates`
|
||||
- **link()**: belongsTo → `document_template_links`
|
||||
|
||||
### document_template_sections
|
||||
**모델**: `App\Models\Documents\DocumentTemplateSection`
|
||||
|
||||
- **template()**: belongsTo → `document_templates`
|
||||
- **items()**: hasMany → `document_template_section_items`
|
||||
|
||||
### document_template_section_fields
|
||||
**모델**: `App\Models\Documents\DocumentTemplateSectionField`
|
||||
|
||||
- **template()**: belongsTo → `document_templates`
|
||||
|
||||
### document_template_section_items
|
||||
**모델**: `App\Models\Documents\DocumentTemplateSectionItem`
|
||||
|
||||
@@ -511,6 +536,7 @@ ### process
|
||||
|
||||
- **classificationRules()**: hasMany → `process_classification_rules`
|
||||
- **processItems()**: hasMany → `process_items`
|
||||
- **steps()**: hasMany → `process_steps`
|
||||
|
||||
### process_classification_rules
|
||||
**모델**: `App\Models\ProcessClassificationRule`
|
||||
@@ -523,6 +549,11 @@ ### process_items
|
||||
- **process()**: belongsTo → `processes`
|
||||
- **item()**: belongsTo → `items`
|
||||
|
||||
### process_steps
|
||||
**모델**: `App\Models\ProcessStep`
|
||||
|
||||
- **process()**: belongsTo → `processes`
|
||||
|
||||
### work_orders
|
||||
**모델**: `App\Models\Production\WorkOrder`
|
||||
|
||||
|
||||
@@ -151,6 +151,16 @@ public function convertToBidding(int $id)
|
||||
}, __('message.bidding.converted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조 데이터 조회 (현장명, 부호 목록)
|
||||
*/
|
||||
public function referenceData()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->quoteService->referenceData();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적번호 미리보기
|
||||
*/
|
||||
|
||||
@@ -470,39 +470,24 @@ public function createFromQuote(int $quoteId, array $data = [])
|
||||
|
||||
$order->save();
|
||||
|
||||
// calculation_inputs에서 제품-부품 매핑 정보 추출
|
||||
$calculationInputs = $quote->calculation_inputs ?? [];
|
||||
$calcInputItems = $calculationInputs['items'] ?? [];
|
||||
|
||||
// 견적 품목을 수주 품목으로 변환
|
||||
foreach ($quote->items as $index => $quoteItem) {
|
||||
// calculation_inputs.items에서 해당 품목의 floor/code 정보 찾기
|
||||
// 1. item_index로 매칭 시도
|
||||
// 2. 없으면 배열 인덱스로 fallback
|
||||
// floor_code/symbol_code 추출:
|
||||
// 1순위: quoteItem->note에서 파싱 (형식: "4F DS-01" → floor=4F, symbol=DS-01)
|
||||
// 2순위: NULL
|
||||
$floorCode = null;
|
||||
$symbolCode = null;
|
||||
|
||||
$itemIndex = $quoteItem->item_index ?? null;
|
||||
if ($itemIndex !== null) {
|
||||
// item_index로 매칭
|
||||
foreach ($calcInputItems as $calcItem) {
|
||||
if (($calcItem['index'] ?? null) === $itemIndex) {
|
||||
$floorCode = $calcItem['floor'] ?? null;
|
||||
$symbolCode = $calcItem['code'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// item_index로 못 찾으면 배열 인덱스로 fallback
|
||||
if ($floorCode === null && $symbolCode === null && isset($calcInputItems[$index])) {
|
||||
$floorCode = $calcInputItems[$index]['floor'] ?? null;
|
||||
$symbolCode = $calcInputItems[$index]['code'] ?? null;
|
||||
$note = trim($quoteItem->note ?? '');
|
||||
if ($note !== '') {
|
||||
$parts = preg_split('/\s+/', $note, 2);
|
||||
$floorCode = $parts[0] ?? null;
|
||||
$symbolCode = $parts[1] ?? null;
|
||||
}
|
||||
|
||||
$order->items()->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'serial_no' => $index + 1, // 1부터 시작하는 순번
|
||||
'serial_no' => $index + 1,
|
||||
'item_id' => $quoteItem->item_id,
|
||||
'item_code' => $quoteItem->item_code,
|
||||
'item_name' => $quoteItem->item_name,
|
||||
@@ -515,6 +500,7 @@ public function createFromQuote(int $quoteId, array $data = [])
|
||||
'supply_amount' => $quoteItem->total_price,
|
||||
'tax_amount' => round($quoteItem->total_price * 0.1, 2),
|
||||
'total_amount' => round($quoteItem->total_price * 1.1, 2),
|
||||
'note' => $quoteItem->formula_category,
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -101,6 +101,64 @@ public function index(array $params): LengthAwarePaginator
|
||||
return $query->paginate($size, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 참조 데이터 조회 (현장명, 부호 목록)
|
||||
*
|
||||
* 기존 견적/수주에서 사용된 현장명과 부호를 DISTINCT로 조회합니다.
|
||||
*/
|
||||
public function referenceData(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 현장명: 견적 테이블에서 DISTINCT
|
||||
$siteNames = Quote::where('tenant_id', $tenantId)
|
||||
->whereNotNull('site_name')
|
||||
->where('site_name', '!=', '')
|
||||
->distinct()
|
||||
->orderBy('site_name')
|
||||
->pluck('site_name')
|
||||
->toArray();
|
||||
|
||||
// 부호(개소코드): calculation_inputs JSON 내 items[].code (예: FSS-01, SD-02)
|
||||
$locationCodes = collect();
|
||||
|
||||
// calculation_inputs JSON에서 items[].code 추출
|
||||
$quotesWithInputs = Quote::where('tenant_id', $tenantId)
|
||||
->whereNotNull('calculation_inputs')
|
||||
->select('calculation_inputs')
|
||||
->get();
|
||||
|
||||
foreach ($quotesWithInputs as $quote) {
|
||||
$inputs = is_string($quote->calculation_inputs)
|
||||
? json_decode($quote->calculation_inputs, true)
|
||||
: $quote->calculation_inputs;
|
||||
|
||||
if (! is_array($inputs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items = $inputs['items'] ?? $inputs['locations'] ?? [];
|
||||
foreach ($items as $item) {
|
||||
$code = $item['code'] ?? null;
|
||||
if ($code && trim($code) !== '') {
|
||||
$locationCodes->push(trim($code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 제거, 정렬
|
||||
$locationCodes = $locationCodes
|
||||
->unique()
|
||||
->sort()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'site_names' => $siteNames,
|
||||
'location_codes' => $locationCodes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 단건 조회
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. 기존 문자열 데이터를 JSON으로 변환
|
||||
DB::table('document_template_section_items')
|
||||
->whereNotNull('tolerance')
|
||||
->where('tolerance', '!=', '')
|
||||
->orderBy('id')
|
||||
->each(function ($row) {
|
||||
// 이미 JSON인 경우 스킵
|
||||
$decoded = json_decode($row->tolerance, true);
|
||||
if (is_array($decoded) && isset($decoded['type'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 문자열 → symmetric 타입으로 변환
|
||||
$value = trim($row->tolerance);
|
||||
$numericValue = preg_replace('/[^\d.]/', '', $value);
|
||||
|
||||
$json = json_encode([
|
||||
'type' => 'symmetric',
|
||||
'value' => $numericValue !== '' ? (float) $numericValue : 0,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
DB::table('document_template_section_items')
|
||||
->where('id', $row->id)
|
||||
->update(['tolerance' => $json]);
|
||||
});
|
||||
|
||||
// 2. 컬럼 타입 변경: VARCHAR → JSON
|
||||
Schema::table('document_template_section_items', function (Blueprint $table) {
|
||||
$table->json('tolerance')->nullable()->comment('공차/허용범위 (JSON)')->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('document_template_section_items', function (Blueprint $table) {
|
||||
$table->string('tolerance', 100)->nullable()->comment('공차/허용범위')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -20,9 +20,8 @@ public function run(): void
|
||||
'fields' => json_encode([
|
||||
['field_key' => 'category', 'label' => '구분', 'field_type' => 'text', 'width' => '65px', 'is_required' => false],
|
||||
['field_key' => 'item', 'label' => '검사항목', 'field_type' => 'text', 'width' => '130px', 'is_required' => true],
|
||||
['field_key' => 'standard', 'label' => '검사기준', 'field_type' => 'text', 'width' => '180px', 'is_required' => false],
|
||||
['field_key' => 'standard_criteria', 'label' => '기준범위', 'field_type' => 'json_criteria', 'width' => '100px', 'is_required' => false],
|
||||
['field_key' => 'tolerance', 'label' => '공차/범위', 'field_type' => 'json_tolerance', 'width' => '120px', 'is_required' => false],
|
||||
['field_key' => 'standard', 'label' => '검사 기준/범위', 'field_type' => 'text_with_criteria', 'width' => '220px', 'is_required' => false],
|
||||
['field_key' => 'tolerance', 'label' => '공차/범위', 'field_type' => 'json_tolerance', 'width' => '85px', 'is_required' => false],
|
||||
['field_key' => 'method', 'label' => '검사방식', 'field_type' => 'select_api', 'width' => '110px', 'is_required' => false, 'options' => [
|
||||
'api_endpoint' => '/api/admin/common-codes/inspection_method',
|
||||
'auto_map' => [
|
||||
@@ -64,9 +63,8 @@ public function run(): void
|
||||
'fields' => json_encode([
|
||||
['field_key' => 'category', 'label' => '구분', 'field_type' => 'text', 'width' => '65px', 'is_required' => false],
|
||||
['field_key' => 'item', 'label' => '검사항목', 'field_type' => 'text', 'width' => '130px', 'is_required' => true],
|
||||
['field_key' => 'standard', 'label' => '검사기준', 'field_type' => 'text', 'width' => '180px', 'is_required' => false],
|
||||
['field_key' => 'standard_criteria', 'label' => '기준범위', 'field_type' => 'json_criteria', 'width' => '100px', 'is_required' => false],
|
||||
['field_key' => 'tolerance', 'label' => '공차/범위', 'field_type' => 'json_tolerance', 'width' => '120px', 'is_required' => false],
|
||||
['field_key' => 'standard', 'label' => '검사 기준/범위', 'field_type' => 'text_with_criteria', 'width' => '220px', 'is_required' => false],
|
||||
['field_key' => 'tolerance', 'label' => '공차/범위', 'field_type' => 'json_tolerance', 'width' => '85px', 'is_required' => false],
|
||||
['field_key' => 'method', 'label' => '검사방식', 'field_type' => 'select_api', 'width' => '110px', 'is_required' => false, 'options' => [
|
||||
'api_endpoint' => '/api/admin/common-codes/inspection_method',
|
||||
'auto_map' => [
|
||||
@@ -108,9 +106,8 @@ public function run(): void
|
||||
'fields' => json_encode([
|
||||
['field_key' => 'category', 'label' => '구분', 'field_type' => 'text', 'width' => '65px', 'is_required' => false],
|
||||
['field_key' => 'item', 'label' => '검사항목', 'field_type' => 'text', 'width' => '130px', 'is_required' => true],
|
||||
['field_key' => 'standard', 'label' => '검사기준', 'field_type' => 'text', 'width' => '180px', 'is_required' => false],
|
||||
['field_key' => 'standard_criteria', 'label' => '기준범위', 'field_type' => 'json_criteria', 'width' => '100px', 'is_required' => false],
|
||||
['field_key' => 'tolerance', 'label' => '공차/범위', 'field_type' => 'json_tolerance', 'width' => '120px', 'is_required' => false],
|
||||
['field_key' => 'standard', 'label' => '검사 기준/범위', 'field_type' => 'text_with_criteria', 'width' => '220px', 'is_required' => false],
|
||||
['field_key' => 'tolerance', 'label' => '공차/범위', 'field_type' => 'json_tolerance', 'width' => '85px', 'is_required' => false],
|
||||
['field_key' => 'method', 'label' => '검사방식', 'field_type' => 'select_api', 'width' => '110px', 'is_required' => false, 'options' => [
|
||||
'api_endpoint' => '/api/admin/common-codes/inspection_method',
|
||||
'auto_map' => [
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
// 견적번호 미리보기
|
||||
Route::get('/number/preview', [QuoteController::class, 'previewNumber'])->name('v1.quotes.number-preview');
|
||||
|
||||
// 참조 데이터 (현장명, 부호 등)
|
||||
Route::get('/reference-data', [QuoteController::class, 'referenceData'])->name('v1.quotes.reference-data');
|
||||
|
||||
// 자동산출
|
||||
Route::get('/calculation/schema', [QuoteController::class, 'calculationSchema'])->name('v1.quotes.calculation-schema');
|
||||
Route::post('/calculate', [QuoteController::class, 'calculate'])->name('v1.quotes.calculate');
|
||||
|
||||
Reference in New Issue
Block a user