diff --git a/app/Http/Controllers/Api/Admin/DocumentApiController.php b/app/Http/Controllers/Api/Admin/DocumentApiController.php index 149c393b..a364d020 100644 --- a/app/Http/Controllers/Api/Admin/DocumentApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentApiController.php @@ -4,9 +4,12 @@ use App\Http\Controllers\Controller; use App\Models\Documents\Document; +use App\Models\Documents\DocumentApproval; use App\Models\Documents\DocumentData; +use App\Models\DocumentTemplate; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; class DocumentApiController extends Controller { @@ -86,37 +89,66 @@ public function store(Request $request): JsonResponse 'data.*.field_value' => 'nullable|string', ]); - // 문서 번호 생성 - $documentNo = $this->generateDocumentNo($tenantId, $request->template_id); + try { + DB::beginTransaction(); - $document = Document::create([ - 'tenant_id' => $tenantId, - 'template_id' => $request->template_id, - 'document_no' => $documentNo, - 'title' => $request->title, - 'status' => Document::STATUS_DRAFT, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); + // 문서 번호 생성 + $documentNo = $this->generateDocumentNo($tenantId, $request->template_id); - // 문서 데이터 저장 - if ($request->filled('data')) { - foreach ($request->data as $item) { - if (! empty($item['field_value'])) { - DocumentData::create([ + $document = Document::create([ + 'tenant_id' => $tenantId, + 'template_id' => $request->template_id, + 'document_no' => $documentNo, + 'title' => $request->title, + 'status' => Document::STATUS_DRAFT, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + + // 결재라인 초기화 (템플릿의 approvalLines 기반) + $template = DocumentTemplate::with('approvalLines')->find($request->template_id); + if ($template && $template->approvalLines->isNotEmpty()) { + foreach ($template->approvalLines as $line) { + DocumentApproval::create([ 'document_id' => $document->id, - 'field_key' => $item['field_key'], - 'field_value' => $item['field_value'], + 'user_id' => $userId, + 'step' => $line->sort_order + 1, + 'role' => $line->role ?? $line->name, + 'status' => DocumentApproval::STATUS_PENDING, + 'created_by' => $userId, + 'updated_by' => $userId, ]); } } - } - return response()->json([ - 'success' => true, - 'message' => '문서가 저장되었습니다.', - 'data' => $document->fresh(['template', 'data']), - ], 201); + // 문서 데이터 저장 + 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'], + ]); + } + } + } + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => '문서가 저장되었습니다.', + 'data' => $document->fresh(['template', 'data', 'approvals']), + ], 201); + } catch (\Exception $e) { + DB::rollBack(); + + return response()->json([ + 'success' => false, + 'message' => '문서 생성 중 오류가 발생했습니다: '.$e->getMessage(), + ], 500); + } } /** @@ -200,27 +232,50 @@ public function destroy(int $id): JsonResponse /** * 문서 번호 생성 + * 형식: {카테고리prefix}-{YYMMDD}-{순번} + * 예: IQC-260131-01, PRD-260131-01 */ private function generateDocumentNo(int $tenantId, int $templateId): string { - $prefix = 'DOC'; - $date = now()->format('Ymd'); + $template = DocumentTemplate::find($templateId); + $prefix = $this->getCategoryPrefix($template?->category); + $date = now()->format('ymd'); $lastDocument = Document::where('tenant_id', $tenantId) - ->where('template_id', $templateId) - ->whereDate('created_at', now()->toDateString()) + ->where('document_no', 'like', "{$prefix}-{$date}-%") ->orderBy('id', 'desc') ->first(); $sequence = 1; if ($lastDocument) { - // 마지막 문서 번호에서 시퀀스 추출 $parts = explode('-', $lastDocument->document_no); if (count($parts) >= 3) { $sequence = (int) end($parts) + 1; } } - return sprintf('%s-%s-%04d', $prefix, $date, $sequence); + return sprintf('%s-%s-%02d', $prefix, $date, $sequence); + } + + /** + * 카테고리별 문서번호 prefix + * 카테고리가 '품질/수입검사' 등 슬래시 포함 시 상위 카테고리 기준 + */ + private function getCategoryPrefix(?string $category): string + { + if (! $category) { + return 'DOC'; + } + + // 상위 카테고리 추출 (슬래시 포함 시) + $mainCategory = str_contains($category, '/') ? explode('/', $category)[0] : $category; + + return match ($mainCategory) { + '품질' => 'IQC', + '생산' => 'PRD', + '영업' => 'SLS', + '구매' => 'PUR', + default => 'DOC', + }; } } diff --git a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php index dca3a525..a3db8186 100644 --- a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php @@ -217,6 +217,108 @@ public function toggleActive(int $id): JsonResponse ]); } + /** + * 양식 복제 + */ + public function duplicate(Request $request, int $id): JsonResponse + { + $source = DocumentTemplate::with([ + 'approvalLines', + 'basicFields', + 'sections.items', + 'columns', + ])->findOrFail($id); + + $newName = $request->input('name', $source->name.' (복사)'); + + try { + DB::beginTransaction(); + + $newTemplate = DocumentTemplate::create([ + 'tenant_id' => $source->tenant_id, + 'name' => $newName, + 'category' => $source->category, + 'title' => $source->title, + 'company_name' => $source->company_name, + 'company_address' => $source->company_address, + 'company_contact' => $source->company_contact, + 'footer_remark_label' => $source->footer_remark_label, + 'footer_judgement_label' => $source->footer_judgement_label, + 'footer_judgement_options' => $source->footer_judgement_options, + 'is_active' => false, + ]); + + foreach ($source->approvalLines as $line) { + DocumentTemplateApprovalLine::create([ + 'template_id' => $newTemplate->id, + 'name' => $line->name, + 'dept' => $line->dept, + 'role' => $line->role, + 'sort_order' => $line->sort_order, + ]); + } + + foreach ($source->basicFields as $field) { + DocumentTemplateBasicField::create([ + 'template_id' => $newTemplate->id, + 'label' => $field->label, + 'field_type' => $field->field_type, + 'default_value' => $field->default_value, + 'sort_order' => $field->sort_order, + ]); + } + + foreach ($source->sections as $section) { + $newSection = DocumentTemplateSection::create([ + 'template_id' => $newTemplate->id, + 'title' => $section->title, + 'image_path' => $section->image_path, + 'sort_order' => $section->sort_order, + ]); + + foreach ($section->items as $item) { + DocumentTemplateSectionItem::create([ + 'section_id' => $newSection->id, + 'category' => $item->category, + 'item' => $item->item, + 'standard' => $item->standard, + 'method' => $item->method, + 'frequency' => $item->frequency, + 'regulation' => $item->regulation, + 'sort_order' => $item->sort_order, + ]); + } + } + + foreach ($source->columns as $col) { + DocumentTemplateColumn::create([ + 'template_id' => $newTemplate->id, + 'label' => $col->label, + 'width' => $col->width, + 'column_type' => $col->column_type, + 'group_name' => $col->group_name, + 'sub_labels' => $col->sub_labels, + 'sort_order' => $col->sort_order, + ]); + } + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => "'{$newName}' 양식이 복제되었습니다.", + 'data' => $newTemplate->load(['approvalLines', 'basicFields', 'sections.items', 'columns']), + ]); + } catch (\Exception $e) { + DB::rollBack(); + + return response()->json([ + 'success' => false, + 'message' => '복제 중 오류가 발생했습니다: '.$e->getMessage(), + ], 500); + } + } + /** * 이미지 업로드 */ diff --git a/database/seeders/IncomingInspectionTemplateSeeder.php b/database/seeders/IncomingInspectionTemplateSeeder.php new file mode 100644 index 00000000..e44ab4fc --- /dev/null +++ b/database/seeders/IncomingInspectionTemplateSeeder.php @@ -0,0 +1,299 @@ +getTemplateDefinitions(); + + foreach ($templates as $def) { + $this->cleanupExisting($def['name']); + + $template = DocumentTemplate::create([ + 'tenant_id' => $this->tenantId, + 'name' => $def['name'], + 'category' => '품질/수입검사', + 'title' => '수 입 검 사 성 적 서', + 'company_name' => '케이디산업', + 'footer_remark_label' => '부적합 내용', + 'footer_judgement_label' => '종합판정', + 'footer_judgement_options' => ['합격', '불합격'], + 'is_active' => true, + ]); + + $this->createApprovalLines($template->id); + $this->createBasicFields($template->id); + $this->createSection($template->id, $def['section_title'], $def['items']); + $this->createColumns($template->id); + + $this->command->info("✅ {$def['name']} (ID: {$template->id})"); + } + } + + private function getTemplateDefinitions(): array + { + return [ + // ─── EGI (전기아연도금강판) ─── + [ + 'name' => 'EGI 수입검사 성적서', + 'section_title' => '전기 아연도금 강판 (KS D 3528, SECC) "EGI 절곡판"', + 'items' => [ + [ + 'category' => '겉모양', + 'item' => '겉모양', + 'standard' => '사용상 해로울 결함이 없을 것', + 'method' => '육안검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3528', + ], + [ + 'category' => '치수', + 'item' => '두께', + 'standard' => '0.8~1.0: ±0.07 / 1.0~1.25: ±0.08 / 1.25~1.6: ±0.10 / 1.6~2.0: ±0.12', + 'method' => '체크검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3528', + ], + [ + 'category' => '치수', + 'item' => '너비', + 'standard' => '1250 미만: +7/-0', + 'method' => '체크검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3528', + ], + [ + 'category' => '치수', + 'item' => '길이', + 'standard' => '~1250: +10/-0 / 2000~4000: +15/-0 / 4000~6000: +20/-0', + 'method' => '체크검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3528', + ], + [ + 'category' => '기계적성질', + 'item' => '인장강도 (N/mm²)', + 'standard' => '270 이상', + 'method' => '공급업체 밀시트', + 'frequency' => '입고시', + 'regulation' => 'KS D 3528', + ], + [ + 'category' => '기계적성질', + 'item' => '연신율 (%)', + 'standard' => '0.6~1.0: 36이상 / 1.0~1.6: 37이상 / 1.6~2.3: 38이상', + 'method' => '공급업체 밀시트', + 'frequency' => '입고시', + 'regulation' => 'KS D 3528', + ], + [ + 'category' => '도금', + 'item' => '아연 최소 부착량 (g/m²)', + 'standard' => '한면 17 이상', + 'method' => '공급업체 밀시트', + 'frequency' => '입고시', + 'regulation' => 'KS F 4510', + ], + ], + ], + + // ─── SUS (스테인리스강판) ─── + [ + 'name' => 'SUS 수입검사 성적서', + 'section_title' => '냉간 압연 스테인리스 강판 (KS D 3698, STS304) "SUS 절곡판"', + 'items' => [ + [ + 'category' => '겉모양', + 'item' => '겉모양', + 'standard' => '사용상 해로울 결함이 없을 것', + 'method' => '육안검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3698', + ], + [ + 'category' => '치수', + 'item' => '두께', + 'standard' => '1.0~1.25: ±0.10 / 1.25~1.6: ±0.12', + 'method' => '체크검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3698', + ], + [ + 'category' => '치수', + 'item' => '너비', + 'standard' => '1250 미만: +7/-0', + 'method' => '체크검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3698', + ], + [ + 'category' => '치수', + 'item' => '길이', + 'standard' => '~3500: +10/-0 / 3500~6000: +20/-0', + 'method' => '체크검사', + 'frequency' => 'n=3, c=0', + 'regulation' => 'KS D 3698', + ], + [ + 'category' => '기계적성질', + 'item' => '항복강도 (N/mm²)', + 'standard' => '205 이상', + 'method' => '공급업체 밀시트', + 'frequency' => '입고시', + 'regulation' => 'KS D 3698', + ], + [ + 'category' => '기계적성질', + 'item' => '인장강도 (N/mm²)', + 'standard' => '520 이상', + 'method' => '공급업체 밀시트', + 'frequency' => '입고시', + 'regulation' => 'KS D 3698', + ], + [ + 'category' => '기계적성질', + 'item' => '연신율 (%)', + 'standard' => '40 이상', + 'method' => '공급업체 밀시트', + 'frequency' => '입고시', + 'regulation' => 'KS D 3698', + ], + [ + 'category' => '기계적성질', + 'item' => '경도 (HV)', + 'standard' => '200 이하', + 'method' => '공급업체 밀시트', + 'frequency' => '입고시', + 'regulation' => 'KS D 3698', + ], + ], + ], + ]; + } + + /** + * 결재라인: 담당 / 부서장 (5130 동일) + */ + private function createApprovalLines(int $templateId): void + { + $lines = [ + ['name' => '담당', 'dept' => '품질', 'role' => '담당자', 'sort_order' => 1], + ['name' => '부서장', 'dept' => '품질', 'role' => '부서장', 'sort_order' => 2], + ]; + + foreach ($lines as $line) { + DocumentTemplateApprovalLine::create(array_merge( + ['template_id' => $templateId], + $line, + )); + } + } + + /** + * 기본정보 필드 (5130 공통) + */ + private function createBasicFields(int $templateId): void + { + $fields = [ + ['label' => '품명', 'field_type' => 'text', 'sort_order' => 1], + ['label' => '규격 (두께*너비*길이)', 'field_type' => 'text', 'sort_order' => 2], + ['label' => '납품업체', 'field_type' => 'text', 'sort_order' => 3], + ['label' => '제조업체', 'field_type' => 'text', 'sort_order' => 4], + ['label' => '로트번호', 'field_type' => 'text', 'sort_order' => 5], + ['label' => '자재번호', 'field_type' => 'text', 'sort_order' => 6], + ['label' => '검사일자', 'field_type' => 'date', 'sort_order' => 7], + ['label' => '로트크기', 'field_type' => 'text', 'sort_order' => 8], + ['label' => '단위', 'field_type' => 'text', 'sort_order' => 9], + ['label' => '검사자', 'field_type' => 'text', 'sort_order' => 10], + ]; + + foreach ($fields as $field) { + DocumentTemplateBasicField::create(array_merge( + ['template_id' => $templateId], + $field, + )); + } + } + + /** + * 검사 기준서 섹션 + 항목 + */ + private function createSection(int $templateId, string $title, array $items): void + { + $section = DocumentTemplateSection::create([ + 'template_id' => $templateId, + 'title' => $title, + 'sort_order' => 1, + ]); + + foreach ($items as $i => $item) { + DocumentTemplateSectionItem::create(array_merge( + ['section_id' => $section->id, 'sort_order' => $i + 1], + $item, + )); + } + } + + /** + * 데이터 테이블 컬럼 (5130 공통 구조) + */ + private function createColumns(int $templateId): void + { + $columns = [ + ['label' => 'NO', 'column_type' => 'text', 'width' => '50px', 'sort_order' => 1], + ['label' => '검사항목', 'column_type' => 'text', 'width' => '120px', 'sort_order' => 2], + ['label' => '검사기준', 'column_type' => 'text', 'width' => '150px', 'sort_order' => 3], + ['label' => '검사방식', 'column_type' => 'text', 'width' => '100px', 'sort_order' => 4], + ['label' => '검사주기', 'column_type' => 'text', 'width' => '100px', 'sort_order' => 5], + [ + 'label' => '측정치', + 'column_type' => 'complex', + 'group_name' => '측정치', + 'sub_labels' => ['n1', 'n2', 'n3'], + 'width' => '240px', + 'sort_order' => 6, + ], + ['label' => '판정 (적/부)', 'column_type' => 'select', 'width' => '80px', 'sort_order' => 7], + ]; + + foreach ($columns as $col) { + DocumentTemplateColumn::create(array_merge( + ['template_id' => $templateId], + $col, + )); + } + } + + private function cleanupExisting(string $name): void + { + $existing = DocumentTemplate::where('tenant_id', $this->tenantId) + ->where('name', $name) + ->first(); + + if (! $existing) { + return; + } + + DocumentTemplateColumn::where('template_id', $existing->id)->delete(); + $sections = DocumentTemplateSection::where('template_id', $existing->id)->get(); + foreach ($sections as $section) { + DocumentTemplateSectionItem::where('section_id', $section->id)->delete(); + } + DocumentTemplateSection::where('template_id', $existing->id)->delete(); + DocumentTemplateBasicField::where('template_id', $existing->id)->delete(); + DocumentTemplateApprovalLine::where('template_id', $existing->id)->delete(); + $existing->forceDelete(); + } +} \ No newline at end of file diff --git a/resources/views/document-templates/index.blade.php b/resources/views/document-templates/index.blade.php index a287fd99..5aa1e5a0 100644 --- a/resources/views/document-templates/index.blade.php +++ b/resources/views/document-templates/index.blade.php @@ -120,6 +120,35 @@ function initFilterForm() { }); }; + // 양식 복제 + window.duplicateTemplate = function(id, name) { + const newName = prompt('복제할 양식 이름을 입력하세요:', name + ' (복사)'); + if (newName === null) return; // 취소 + + fetch(`/api/admin/document-templates/${id}/duplicate`, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ name: newName }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(data.message || '복제되었습니다.', 'success'); + htmx.trigger('#template-table', 'filterSubmit'); + } else { + showToast(data.message || '복제에 실패했습니다.', 'error'); + } + }) + .catch(error => { + showToast('복제 중 오류가 발생했습니다.', 'error'); + console.error('Duplicate error:', error); + }); + }; + // 활성 토글 window.toggleActive = function(id, buttonEl) { const btn = buttonEl || document.querySelector(`tr[data-template-id="${id}"] button[onclick*="toggleActive"]`); diff --git a/resources/views/document-templates/partials/table.blade.php b/resources/views/document-templates/partials/table.blade.php index 40e7ff6c..5c266f29 100644 --- a/resources/views/document-templates/partials/table.blade.php +++ b/resources/views/document-templates/partials/table.blade.php @@ -65,6 +65,13 @@ class="text-gray-600 hover:text-blue-600 transition" +