feat:문서 데이터 입력 UI 구현 (Phase 2.2)
- 섹션별 동적 검사 테이블 렌더링 (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 <noreply@anthropic.com>
This commit is contained in:
@@ -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}-{순번}
|
||||
|
||||
@@ -108,15 +108,206 @@ class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:rin
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 섹션 (테이블 형태 - Phase 2.2에서 구현) --}}
|
||||
{{-- 섹션 (검사 데이터 테이블) --}}
|
||||
@if($template->sections && $template->sections->count() > 0)
|
||||
@foreach($template->sections as $section)
|
||||
@foreach($template->sections as $sectionIndex => $section)
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ $section->title }}</h2>
|
||||
@if($section->image_path)
|
||||
<img src="{{ asset('storage/' . $section->image_path) }}" alt="{{ $section->title }}" class="max-w-full h-auto mb-4 rounded">
|
||||
@endif
|
||||
<p class="text-sm text-gray-400 italic">검사 데이터 테이블은 Phase 2.2에서 구현됩니다.</p>
|
||||
|
||||
{{-- 검사 데이터 테이블 --}}
|
||||
@if($section->items->count() > 0 && $template->columns->count() > 0)
|
||||
<div class="overflow-x-auto mt-4">
|
||||
<table class="min-w-full border border-gray-300 text-sm" data-section-id="{{ $section->id }}">
|
||||
{{-- 테이블 헤더 --}}
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
@foreach($template->columns as $col)
|
||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||
<th colspan="{{ count($col->sub_labels) }}"
|
||||
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300"
|
||||
style="min-width: {{ $col->width }}">
|
||||
{{ $col->label }}
|
||||
</th>
|
||||
@else
|
||||
<th rowspan="{{ $template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels) ? 2 : 1 }}"
|
||||
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300"
|
||||
style="min-width: {{ $col->width }}">
|
||||
{{ $col->label }}
|
||||
</th>
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
{{-- 서브 라벨 행 (complex 컬럼이 있을 때만) --}}
|
||||
@if($template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels))
|
||||
<tr>
|
||||
@foreach($template->columns as $col)
|
||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||
@foreach($col->sub_labels as $subLabel)
|
||||
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 border border-gray-300 bg-gray-50">
|
||||
{{ $subLabel }}
|
||||
</th>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
@endif
|
||||
</thead>
|
||||
{{-- 테이블 바디 --}}
|
||||
<tbody>
|
||||
@foreach($section->items as $rowIndex => $item)
|
||||
<tr class="hover:bg-blue-50" data-row-index="{{ $rowIndex }}">
|
||||
@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
|
||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||
<input type="text"
|
||||
name="section_data[{{ $fieldKey }}]"
|
||||
value="{{ $savedVal }}"
|
||||
data-section-id="{{ $section->id }}"
|
||||
data-column-id="{{ $col->id }}"
|
||||
data-row-index="{{ $rowIndex }}"
|
||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||
placeholder="{{ $subLabel }}">
|
||||
</td>
|
||||
@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
|
||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||
<select name="section_data[{{ $fieldKey }}]"
|
||||
data-section-id="{{ $section->id }}"
|
||||
data-column-id="{{ $col->id }}"
|
||||
data-row-index="{{ $rowIndex }}"
|
||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
||||
<option value="">-</option>
|
||||
@foreach($options as $opt)
|
||||
<option value="{{ $opt }}" {{ $savedVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
@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
|
||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||
<input type="checkbox"
|
||||
name="section_data[{{ $fieldKey }}]"
|
||||
value="OK"
|
||||
{{ $savedVal === 'OK' ? 'checked' : '' }}
|
||||
data-section-id="{{ $section->id }}"
|
||||
data-column-id="{{ $col->id }}"
|
||||
data-row-index="{{ $rowIndex }}"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
</td>
|
||||
@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
|
||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||
<input type="number" step="any"
|
||||
name="section_data[{{ $fieldKey }}]"
|
||||
value="{{ $savedVal }}"
|
||||
data-section-id="{{ $section->id }}"
|
||||
data-column-id="{{ $col->id }}"
|
||||
data-row-index="{{ $rowIndex }}"
|
||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
||||
</td>
|
||||
@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)
|
||||
<td class="px-2 py-2 border border-gray-300 text-sm text-gray-700 {{ is_numeric($staticValue) ? 'text-center' : '' }}">
|
||||
{{ $staticValue }}
|
||||
</td>
|
||||
@else
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||
<input type="text"
|
||||
name="section_data[{{ $fieldKey }}]"
|
||||
value="{{ $savedVal }}"
|
||||
data-section-id="{{ $section->id }}"
|
||||
data-column-id="{{ $col->id }}"
|
||||
data-row-index="{{ $rowIndex }}"
|
||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
||||
</td>
|
||||
@endif
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 종합판정 / 비고 --}}
|
||||
@if($loop->last && $template->footer_judgement_label)
|
||||
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ $template->footer_remark_label ?? '비고' }}
|
||||
</label>
|
||||
@php
|
||||
$remarkKey = 'footer_remark';
|
||||
$remarkVal = $document?->data->where('field_key', $remarkKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<textarea name="data[{{ $remarkKey }}]" rows="3"
|
||||
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">{{ $remarkVal }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ $template->footer_judgement_label ?? '종합판정' }}
|
||||
</label>
|
||||
@php
|
||||
$judgementKey = 'footer_judgement';
|
||||
$judgementVal = $document?->data->where('field_key', $judgementKey)->first()?->field_value ?? '';
|
||||
$judgementOptions = $template->footer_judgement_options ?? ['적합', '부적합'];
|
||||
@endphp
|
||||
<select name="data[{{ $judgementKey }}]"
|
||||
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($judgementOptions as $opt)
|
||||
<option value="{{ $opt }}" {{ $judgementVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@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';
|
||||
|
||||
Reference in New Issue
Block a user