From df762d6cf47ea171104f316ceae56ea74cfef3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 31 Jan 2026 04:43:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EB=AC=B8=EC=84=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=9E=85=EB=A0=A5=20UI=20=EA=B5=AC=ED=98=84=20(Pha?= =?UTF-8?q?se=202.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 섹션별 동적 검사 테이블 렌더링 (complex/select/check/measurement/text) - 정적 컬럼 자동 매핑 (NO, 검사항목, 검사기준, 검사방식, 검사주기) - complex 컬럼 서브 라벨 행 (측정치 n1/n2/n3) - 종합판정 + 비고 Footer 영역 - JS 폼 데이터 수집 (기본필드 + 섹션 테이블 데이터 + 체크박스) - saveDocumentData() 공통 메서드 (section_id/column_id/row_index EAV 저장) Co-Authored-By: Claude Opus 4.5 --- .../Api/Admin/DocumentApiController.php | 57 +++-- resources/views/documents/edit.blade.php | 230 +++++++++++++++++- 2 files changed, 258 insertions(+), 29 deletions(-) diff --git a/app/Http/Controllers/Api/Admin/DocumentApiController.php b/app/Http/Controllers/Api/Admin/DocumentApiController.php index a364d020..5f61a910 100644 --- a/app/Http/Controllers/Api/Admin/DocumentApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentApiController.php @@ -87,6 +87,9 @@ public function store(Request $request): JsonResponse 'data' => 'nullable|array', 'data.*.field_key' => 'required|string', 'data.*.field_value' => 'nullable|string', + 'data.*.section_id' => 'nullable|integer', + 'data.*.column_id' => 'nullable|integer', + 'data.*.row_index' => 'nullable|integer', ]); try { @@ -121,18 +124,8 @@ public function store(Request $request): JsonResponse } } - // 문서 데이터 저장 - if ($request->filled('data')) { - foreach ($request->data as $item) { - if (! empty($item['field_value'])) { - DocumentData::create([ - 'document_id' => $document->id, - 'field_key' => $item['field_key'], - 'field_value' => $item['field_value'], - ]); - } - } - } + // 문서 데이터 저장 (기본필드 + 섹션 테이블 데이터) + $this->saveDocumentData($document, $request->input('data', [])); DB::commit(); @@ -174,6 +167,9 @@ public function update(int $id, Request $request): JsonResponse 'data' => 'nullable|array', 'data.*.field_key' => 'required|string', 'data.*.field_value' => 'nullable|string', + 'data.*.section_id' => 'nullable|integer', + 'data.*.column_id' => 'nullable|integer', + 'data.*.row_index' => 'nullable|integer', ]); $document->update([ @@ -181,21 +177,10 @@ public function update(int $id, Request $request): JsonResponse 'updated_by' => $userId, ]); - // 문서 데이터 업데이트 + // 문서 데이터 업데이트 (기존 삭제 후 재저장) if ($request->has('data')) { - // 기존 데이터 삭제 $document->data()->delete(); - - // 새 데이터 저장 - foreach ($request->data as $item) { - if (! empty($item['field_value'])) { - DocumentData::create([ - 'document_id' => $document->id, - 'field_key' => $item['field_key'], - 'field_value' => $item['field_value'], - ]); - } - } + $this->saveDocumentData($document, $request->input('data', [])); } return response()->json([ @@ -230,6 +215,28 @@ public function destroy(int $id): JsonResponse ]); } + /** + * 문서 데이터 저장 (기본필드 + 섹션 테이블 데이터) + */ + private function saveDocumentData(Document $document, array $dataItems): void + { + foreach ($dataItems as $item) { + if (empty($item['field_key'])) { + continue; + } + + // 빈 값도 저장 (섹션 데이터 편집 시 빈값으로 클리어 가능) + DocumentData::create([ + 'document_id' => $document->id, + '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'] ?? '', + ]); + } + } + /** * 문서 번호 생성 * 형식: {카테고리prefix}-{YYMMDD}-{순번} diff --git a/resources/views/documents/edit.blade.php b/resources/views/documents/edit.blade.php index dd0dc3cb..33e07518 100644 --- a/resources/views/documents/edit.blade.php +++ b/resources/views/documents/edit.blade.php @@ -108,15 +108,206 @@ class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:rin @endif - {{-- 섹션 (테이블 형태 - Phase 2.2에서 구현) --}} + {{-- 섹션 (검사 데이터 테이블) --}} @if($template->sections && $template->sections->count() > 0) - @foreach($template->sections as $section) + @foreach($template->sections as $sectionIndex => $section)

{{ $section->title }}

@if($section->image_path) {{ $section->title }} @endif -

검사 데이터 테이블은 Phase 2.2에서 구현됩니다.

+ + {{-- 검사 데이터 테이블 --}} + @if($section->items->count() > 0 && $template->columns->count() > 0) +
+ + {{-- 테이블 헤더 --}} + + + @foreach($template->columns as $col) + @if($col->column_type === 'complex' && $col->sub_labels) + + @else + + @endif + @endforeach + + {{-- 서브 라벨 행 (complex 컬럼이 있을 때만) --}} + @if($template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels)) + + @foreach($template->columns as $col) + @if($col->column_type === 'complex' && $col->sub_labels) + @foreach($col->sub_labels as $subLabel) + + @endforeach + @endif + @endforeach + + @endif + + {{-- 테이블 바디 --}} + + @foreach($section->items as $rowIndex => $item) + + @foreach($template->columns as $col) + @php + $colSlug = \Illuminate\Support\Str::slug($col->label, '_'); + @endphp + + @if($col->column_type === 'complex' && $col->sub_labels) + {{-- complex: 서브 라벨별 입력 필드 --}} + @foreach($col->sub_labels as $subIndex => $subLabel) + @php + $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_sub{$subIndex}"; + $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + @endphp + + @endforeach + @elseif($col->column_type === 'select') + {{-- select: 판정 드롭다운 --}} + @php + $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}"; + $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + $options = $template->footer_judgement_options ?? ['적합', '부적합']; + @endphp + + @elseif($col->column_type === 'check') + {{-- check: 체크박스 --}} + @php + $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}"; + $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + @endphp + + @elseif($col->column_type === 'measurement') + {{-- measurement: 수치 입력 --}} + @php + $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}"; + $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + @endphp + + @else + {{-- text: 정적 데이터 (항목정보) 또는 텍스트 입력 --}} + @php + // 정적 컬럼 매핑: NO, 검사항목, 검사기준, 검사방식, 검사주기 + $staticValue = match(true) { + str_contains(strtolower($col->label), 'no') && strlen($col->label) <= 4 => $rowIndex + 1, + in_array($col->label, ['검사항목', '항목']) => $item->item, + in_array($col->label, ['검사기준', '기준']) => $item->standard, + in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->method, + in_array($col->label, ['검사주기', '주기']) => $item->frequency, + in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->regulation, + in_array($col->label, ['분류', '카테고리']) => $item->category, + default => null, + }; + @endphp + @if($staticValue !== null) + + @else + @php + $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}"; + $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + @endphp + + @endif + @endif + @endforeach + + @endforeach + +
+ {{ $col->label }} + + {{ $col->label }} +
+ {{ $subLabel }} +
+ + + + + + + + + {{ $staticValue }} + + +
+
+ @endif + + {{-- 종합판정 / 비고 --}} + @if($loop->last && $template->footer_judgement_label) +
+
+ + @php + $remarkKey = 'footer_remark'; + $remarkVal = $document?->data->where('field_key', $remarkKey)->first()?->field_value ?? ''; + @endphp + +
+
+ + @php + $judgementKey = 'footer_judgement'; + $judgementVal = $document?->data->where('field_key', $judgementKey)->first()?->field_value ?? ''; + $judgementOptions = $template->footer_judgement_options ?? ['적합', '부적합']; + @endphp + +
+
+ @endif
@endforeach @endif @@ -153,7 +344,7 @@ class="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg- data: [] }; - // data 필드 수집 + // 기본 필드 수집 (data[field_key]) for (const [key, value] of formData.entries()) { if (key.startsWith('data[') && key.endsWith(']')) { const fieldKey = key.slice(5, -1); @@ -164,6 +355,37 @@ class="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg- } } + // 섹션 테이블 데이터 수집 (section_data[field_key]) + for (const [key, value] of formData.entries()) { + if (key.startsWith('section_data[') && key.endsWith(']')) { + const fieldKey = key.slice(13, -1); + const el = form.querySelector(`[name="${key}"]`); + // 체크박스는 체크 안된 경우 skip + if (el && el.type === 'checkbox' && !el.checked) continue; + data.data.push({ + field_key: fieldKey, + field_value: value, + section_id: el?.dataset?.sectionId ? parseInt(el.dataset.sectionId) : null, + column_id: el?.dataset?.columnId ? parseInt(el.dataset.columnId) : null, + row_index: el?.dataset?.rowIndex ? parseInt(el.dataset.rowIndex) : 0, + }); + } + } + + // 체크 안된 체크박스도 빈 값으로 전송 + form.querySelectorAll('input[type="checkbox"][name^="section_data["]').forEach(cb => { + if (!cb.checked) { + const fieldKey = cb.name.slice(13, -1); + data.data.push({ + field_key: fieldKey, + field_value: '', + section_id: cb.dataset?.sectionId ? parseInt(cb.dataset.sectionId) : null, + column_id: cb.dataset?.columnId ? parseInt(cb.dataset.columnId) : null, + row_index: cb.dataset?.rowIndex ? parseInt(cb.dataset.rowIndex) : 0, + }); + } + }); + const isCreate = {{ $isCreate ? 'true' : 'false' }}; const url = isCreate ? '/api/admin/documents' : '/api/admin/documents/{{ $document?->id }}'; const method = isCreate ? 'POST' : 'PATCH';