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:
2026-02-04 23:07:08 +09:00
parent fa07e5b58a
commit e364239572
7 changed files with 169 additions and 34 deletions

View File

@@ -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'));
}
/**
* 견적번호 미리보기
*/

View File

@@ -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,
]);
}

View File

@@ -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,
];
}
/**
* 견적 단건 조회
*/