feat(API):자재 투입 LOT 조회 API 및 중간검사 데이터 정규화
- materialInputLots: stock_transactions 기반 투입 LOT 조회 엔드포인트 추가 - createInspectionDocument: 정규화 형식(section_id/column_id/field_key) 지원 - 레거시 형식(section_X_item_Y) 자동 변환 로직 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -189,6 +189,16 @@ public function materialInputHistory(int $id)
|
||||
}, __('message.work_order.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 자재 투입 LOT 번호 조회 (stock_transactions 기반)
|
||||
*/
|
||||
public function materialInputLots(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->getMaterialInputLots($id);
|
||||
}, __('message.work_order.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목별 중간검사 데이터 저장
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
use App\Models\Production\WorkOrderStepProgress;
|
||||
use App\Models\Tenants\Shipment;
|
||||
use App\Models\Tenants\ShipmentItem;
|
||||
use App\Models\Tenants\StockTransaction;
|
||||
use App\Services\Audit\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -1538,13 +1539,56 @@ public function getMaterialInputHistory(int $workOrderId): array
|
||||
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'materials' => $after['materials'] ?? [],
|
||||
'materials' => $after['input_results'] ?? [],
|
||||
'created_at' => $log->created_at,
|
||||
'actor_id' => $log->actor_id,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업지시에 투입된 자재 LOT 번호 조회 (stock_transactions 기반)
|
||||
*
|
||||
* stock_transactions에서 reference_type='work_order_input'인 거래를 조회하여
|
||||
* 중복 없는 LOT 번호 목록을 반환합니다.
|
||||
*/
|
||||
public function getMaterialInputLots(int $workOrderId): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$workOrder = WorkOrder::where('tenant_id', $tenantId)->find($workOrderId);
|
||||
if (! $workOrder) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$transactions = DB::table('stock_transactions')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('reference_type', StockTransaction::REASON_WORK_ORDER_INPUT)
|
||||
->where('reference_id', $workOrderId)
|
||||
->orderBy('created_at')
|
||||
->get(['id', 'lot_no', 'item_code', 'item_name', 'qty', 'stock_lot_id', 'created_at']);
|
||||
|
||||
// LOT 번호별 그룹핑 (동일 LOT에서 여러번 투입 가능)
|
||||
$lotMap = [];
|
||||
foreach ($transactions as $tx) {
|
||||
$lotNo = $tx->lot_no;
|
||||
if (! isset($lotMap[$lotNo])) {
|
||||
$lotMap[$lotNo] = [
|
||||
'lot_no' => $lotNo,
|
||||
'item_code' => $tx->item_code,
|
||||
'item_name' => $tx->item_name,
|
||||
'total_qty' => 0,
|
||||
'input_count' => 0,
|
||||
'first_input_at' => $tx->created_at,
|
||||
];
|
||||
}
|
||||
$lotMap[$lotNo]['total_qty'] += abs((float) $tx->qty);
|
||||
$lotMap[$lotNo]['input_count']++;
|
||||
}
|
||||
|
||||
return array_values($lotMap);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 중간검사 관련
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
@@ -1796,9 +1840,10 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
// 프론트 InspectionData를 document_data 레코드 형식으로 변환
|
||||
// 프론트 InspectionData를 document_data 정규화 레코드 형식으로 변환
|
||||
$documentDataRecords = $this->transformInspectionDataToDocumentRecords(
|
||||
$inspectionData['data'] ?? []
|
||||
$inspectionData['data'] ?? [],
|
||||
$templateId
|
||||
);
|
||||
|
||||
if ($existingDocument) {
|
||||
@@ -1841,42 +1886,97 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트 InspectionData 배열을 document_data 레코드 형식으로 변환
|
||||
* 프론트 InspectionData를 정규화된 document_data 레코드로 변환
|
||||
*
|
||||
* 프론트에서 보내는 형식:
|
||||
* [{ productName, specification, judgment, nonConformingContent, templateValues: { section_X_item_Y: "ok"|number } }]
|
||||
* 정규화 형식 (NEW):
|
||||
* [{ section_id, column_id, row_index, field_key, field_value }]
|
||||
* field_key: 'value', 'n1', 'n2', 'n1_ok', 'n1_ng', 'overall_result', 'remark', 'row_judgment'
|
||||
*
|
||||
* document_data 테이블 형식:
|
||||
* [{ field_key, field_value, row_index, section_id?, column_id? }]
|
||||
* 레거시 형식 (WorkerScreen):
|
||||
* [{ templateValues: { section_X_item_Y: "ok"|number }, judgment, nonConformingContent }]
|
||||
*/
|
||||
private function transformInspectionDataToDocumentRecords(array $rawItems): array
|
||||
private function transformInspectionDataToDocumentRecords(array $rawItems, int $templateId): array
|
||||
{
|
||||
if (empty($rawItems)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 정규화 형식 감지: 첫 번째 요소에 field_key가 있으면 새 형식
|
||||
if (isset($rawItems[0]['field_key'])) {
|
||||
return array_map(fn (array $item) => [
|
||||
'section_id' => $item['section_id'] ?? null,
|
||||
'column_id' => $item['column_id'] ?? null,
|
||||
'row_index' => $item['row_index'] ?? 0,
|
||||
'field_key' => $item['field_key'],
|
||||
'field_value' => $item['field_value'] ?? null,
|
||||
], $rawItems);
|
||||
}
|
||||
|
||||
// 레거시 형식: templateValues/values 기반 → 정규화 변환
|
||||
return $this->normalizeOldFormatRecords($rawItems, $templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레거시 형식(section_X_item_Y 키)을 정규화 레코드로 변환
|
||||
*/
|
||||
private function normalizeOldFormatRecords(array $rawItems, int $templateId): array
|
||||
{
|
||||
$template = DocumentTemplate::with(['sections.items', 'columns'])->find($templateId);
|
||||
if (! $template) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// sectionItem.id → { section_id, column_id, measurement_type } 매핑
|
||||
$itemMap = $this->buildItemColumnMap($template);
|
||||
|
||||
$records = [];
|
||||
|
||||
foreach ($rawItems as $rowIdx => $item) {
|
||||
// templateValues의 각 키-값을 document_data 레코드로 변환
|
||||
$templateValues = $item['templateValues'] ?? [];
|
||||
foreach ($templateValues as $key => $value) {
|
||||
$values = $item['values'] ?? $item['templateValues'] ?? [];
|
||||
|
||||
foreach ($values as $key => $cellValue) {
|
||||
// section_{sectionId}_item_{itemId} 또는 item_{itemId} 형식 파싱
|
||||
if (! preg_match('/^(?:section_(\d+)_)?item_(\d+)$/', $key, $m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sectionId = $m[1] ? (int) $m[1] : null;
|
||||
$itemId = (int) $m[2];
|
||||
$info = $itemMap[$itemId] ?? null;
|
||||
$columnId = $info['column_id'] ?? null;
|
||||
$sectionId = $sectionId ?? ($info['section_id'] ?? null);
|
||||
|
||||
$expanded = $this->expandCellValue($cellValue, $info['measurement_type'] ?? '');
|
||||
foreach ($expanded as $rec) {
|
||||
$records[] = [
|
||||
'section_id' => $sectionId,
|
||||
'column_id' => $columnId,
|
||||
'row_index' => $rowIdx,
|
||||
'field_key' => $rec['field_key'],
|
||||
'field_value' => $rec['field_value'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 행 판정
|
||||
$judgment = $item['judgment'] ?? null;
|
||||
if ($judgment !== null) {
|
||||
$records[] = [
|
||||
'field_key' => $key,
|
||||
'field_value' => is_array($value) ? json_encode($value) : (string) $value,
|
||||
'section_id' => null,
|
||||
'column_id' => null,
|
||||
'row_index' => $rowIdx,
|
||||
'field_key' => 'row_judgment',
|
||||
'field_value' => (string) $judgment,
|
||||
];
|
||||
}
|
||||
|
||||
// 판정, 부적합 내용 등 메타 필드도 저장
|
||||
if (isset($item['judgment'])) {
|
||||
$records[] = [
|
||||
'field_key' => '_judgment',
|
||||
'field_value' => (string) $item['judgment'],
|
||||
'row_index' => $rowIdx,
|
||||
];
|
||||
}
|
||||
// 부적합 내용
|
||||
if (! empty($item['nonConformingContent'])) {
|
||||
$records[] = [
|
||||
'field_key' => '_nonConformingContent',
|
||||
'field_value' => (string) $item['nonConformingContent'],
|
||||
'section_id' => null,
|
||||
'column_id' => null,
|
||||
'row_index' => $rowIdx,
|
||||
'field_key' => 'remark',
|
||||
'field_value' => (string) $item['nonConformingContent'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1884,6 +1984,97 @@ private function transformInspectionDataToDocumentRecords(array $rawItems): arra
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 구조에서 sectionItem → (section_id, column_id) 매핑 구축
|
||||
*/
|
||||
private function buildItemColumnMap(DocumentTemplate $template): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($template->sections as $section) {
|
||||
foreach ($section->items as $item) {
|
||||
$itemLabel = $this->normalizeInspectionLabel($item->getFieldValue('item') ?? '');
|
||||
$columnId = null;
|
||||
|
||||
foreach ($template->columns as $col) {
|
||||
$colLabel = $this->normalizeInspectionLabel($col->label);
|
||||
if ($itemLabel && $colLabel === $itemLabel) {
|
||||
$columnId = $col->id;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$map[$item->id] = [
|
||||
'section_id' => $section->id,
|
||||
'column_id' => $columnId,
|
||||
'measurement_type' => $item->getFieldValue('measurement_type') ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* CellValue를 개별 field_key/field_value 레코드로 확장
|
||||
*/
|
||||
private function expandCellValue(mixed $cellValue, string $measurementType): array
|
||||
{
|
||||
if ($cellValue === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 단순 문자열/숫자 → value 레코드
|
||||
if (is_string($cellValue) || is_numeric($cellValue)) {
|
||||
return [['field_key' => 'value', 'field_value' => (string) $cellValue]];
|
||||
}
|
||||
|
||||
if (! is_array($cellValue)) {
|
||||
return [['field_key' => 'value', 'field_value' => (string) $cellValue]];
|
||||
}
|
||||
|
||||
$records = [];
|
||||
|
||||
// measurements 배열: 복합 컬럼 데이터
|
||||
if (isset($cellValue['measurements']) && is_array($cellValue['measurements'])) {
|
||||
foreach ($cellValue['measurements'] as $n => $val) {
|
||||
$nNum = $n + 1;
|
||||
if ($measurementType === 'checkbox') {
|
||||
$lower = strtolower($val ?? '');
|
||||
$records[] = ['field_key' => "n{$nNum}_ok", 'field_value' => $lower === 'ok' ? 'OK' : ''];
|
||||
$records[] = ['field_key' => "n{$nNum}_ng", 'field_value' => $lower === 'ng' ? 'NG' : ''];
|
||||
} else {
|
||||
$records[] = ['field_key' => "n{$nNum}", 'field_value' => (string) ($val ?? '')];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// value 필드: 단일 값
|
||||
if (isset($cellValue['value'])) {
|
||||
$records[] = ['field_key' => 'value', 'field_value' => (string) $cellValue['value']];
|
||||
}
|
||||
|
||||
// text 필드: 텍스트 값
|
||||
if (isset($cellValue['text'])) {
|
||||
$records[] = ['field_key' => 'value', 'field_value' => (string) $cellValue['text']];
|
||||
}
|
||||
|
||||
// 아무 필드도 매칭 안 되면 JSON으로 저장
|
||||
return $records ?: [['field_key' => 'value', 'field_value' => json_encode($cellValue)]];
|
||||
}
|
||||
|
||||
/**
|
||||
* 라벨 정규화 (매칭용)
|
||||
*/
|
||||
private function normalizeInspectionLabel(string $label): string
|
||||
{
|
||||
$label = trim($label);
|
||||
// ①②③ 등 번호 접두사 제거
|
||||
$label = preg_replace('/^[①②③④⑤⑥⑦⑧⑨⑩]+/', '', $label);
|
||||
|
||||
return mb_strtolower(trim($label));
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업지시 기본정보 빌드 (검사 문서 렌더링용)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user