diff --git a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php new file mode 100644 index 00000000..dca3a525 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php @@ -0,0 +1,320 @@ +withCount(['sections', 'columns']); + + // 검색 + if ($search = $request->input('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('title', 'like', "%{$search}%") + ->orWhere('category', 'like', "%{$search}%"); + }); + } + + // 카테고리 필터 + if ($category = $request->input('category')) { + $query->where('category', $category); + } + + // 활성 상태 필터 + if ($request->has('is_active') && $request->input('is_active') !== '') { + $query->where('is_active', $request->boolean('is_active')); + } + + $templates = $query->orderBy('updated_at', 'desc') + ->paginate($request->input('per_page', 10)); + + return view('document-templates.partials.table', compact('templates')); + } + + /** + * 단일 조회 + */ + public function show(int $id): JsonResponse + { + $template = DocumentTemplate::with([ + 'approvalLines', + 'basicFields', + 'sections.items', + 'columns', + ])->findOrFail($id); + + return response()->json([ + 'success' => true, + 'data' => $template, + ]); + } + + /** + * 생성 + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'category' => 'nullable|string|max:50', + 'title' => 'nullable|string|max:200', + 'company_name' => 'nullable|string|max:100', + 'company_address' => 'nullable|string|max:255', + 'company_contact' => 'nullable|string|max:100', + 'footer_remark_label' => 'nullable|string|max:50', + 'footer_judgement_label' => 'nullable|string|max:50', + 'footer_judgement_options' => 'nullable|array', + 'is_active' => 'boolean', + // 관계 데이터 + 'approval_lines' => 'nullable|array', + 'basic_fields' => 'nullable|array', + 'sections' => 'nullable|array', + 'columns' => 'nullable|array', + ]); + + try { + DB::beginTransaction(); + + $template = DocumentTemplate::create([ + 'tenant_id' => session('selected_tenant_id'), + 'name' => $validated['name'], + 'category' => $validated['category'] ?? null, + 'title' => $validated['title'] ?? null, + 'company_name' => $validated['company_name'] ?? '경동기업', + 'company_address' => $validated['company_address'] ?? null, + 'company_contact' => $validated['company_contact'] ?? null, + 'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용', + 'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정', + 'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'], + 'is_active' => $validated['is_active'] ?? true, + ]); + + // 관계 데이터 저장 + $this->saveRelations($template, $validated); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => '문서양식이 생성되었습니다.', + 'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns']), + ]); + } catch (\Exception $e) { + DB::rollBack(); + + return response()->json([ + 'success' => false, + 'message' => '생성 중 오류가 발생했습니다: '.$e->getMessage(), + ], 500); + } + } + + /** + * 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $template = DocumentTemplate::findOrFail($id); + + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'category' => 'nullable|string|max:50', + 'title' => 'nullable|string|max:200', + 'company_name' => 'nullable|string|max:100', + 'company_address' => 'nullable|string|max:255', + 'company_contact' => 'nullable|string|max:100', + 'footer_remark_label' => 'nullable|string|max:50', + 'footer_judgement_label' => 'nullable|string|max:50', + 'footer_judgement_options' => 'nullable|array', + 'is_active' => 'boolean', + // 관계 데이터 + 'approval_lines' => 'nullable|array', + 'basic_fields' => 'nullable|array', + 'sections' => 'nullable|array', + 'columns' => 'nullable|array', + ]); + + try { + DB::beginTransaction(); + + $template->update([ + 'name' => $validated['name'], + 'category' => $validated['category'] ?? null, + 'title' => $validated['title'] ?? null, + 'company_name' => $validated['company_name'] ?? null, + 'company_address' => $validated['company_address'] ?? null, + 'company_contact' => $validated['company_contact'] ?? null, + 'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용', + 'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정', + 'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'], + 'is_active' => $validated['is_active'] ?? true, + ]); + + // 관계 데이터 저장 (기존 데이터 삭제 후 재생성) + $this->saveRelations($template, $validated, true); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => '문서양식이 수정되었습니다.', + 'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns']), + ]); + } catch (\Exception $e) { + DB::rollBack(); + + return response()->json([ + 'success' => false, + 'message' => '수정 중 오류가 발생했습니다: '.$e->getMessage(), + ], 500); + } + } + + /** + * 삭제 + */ + public function destroy(int $id): JsonResponse + { + $template = DocumentTemplate::findOrFail($id); + $template->delete(); + + return response()->json([ + 'success' => true, + 'message' => '문서양식이 삭제되었습니다.', + ]); + } + + /** + * 활성 상태 토글 + */ + public function toggleActive(int $id): JsonResponse + { + $template = DocumentTemplate::findOrFail($id); + $template->update(['is_active' => ! $template->is_active]); + + return response()->json([ + 'success' => true, + 'message' => $template->is_active ? '활성화되었습니다.' : '비활성화되었습니다.', + 'is_active' => $template->is_active, + ]); + } + + /** + * 이미지 업로드 + */ + public function uploadImage(Request $request): JsonResponse + { + $request->validate([ + 'image' => 'required|image|max:5120', // 5MB + ]); + + $path = $request->file('image')->store('document-templates', 'public'); + + return response()->json([ + 'success' => true, + 'path' => $path, + 'url' => asset('storage/'.$path), + ]); + } + + /** + * 관계 데이터 저장 + */ + private function saveRelations(DocumentTemplate $template, array $data, bool $deleteExisting = false): void + { + // 기존 데이터 삭제 (수정 시) + if ($deleteExisting) { + $template->approvalLines()->delete(); + $template->basicFields()->delete(); + // sections는 cascade로 items도 함께 삭제됨 + $template->sections()->delete(); + $template->columns()->delete(); + } + + // 결재라인 + if (! empty($data['approval_lines'])) { + foreach ($data['approval_lines'] as $index => $line) { + DocumentTemplateApprovalLine::create([ + 'template_id' => $template->id, + 'name' => $line['name'] ?? '', + 'dept' => $line['dept'] ?? '', + 'role' => $line['role'] ?? '', + 'sort_order' => $index, + ]); + } + } + + // 기본 필드 + if (! empty($data['basic_fields'])) { + foreach ($data['basic_fields'] as $index => $field) { + DocumentTemplateBasicField::create([ + 'template_id' => $template->id, + 'label' => $field['label'] ?? '', + 'field_type' => $field['field_type'] ?? 'text', + 'default_value' => $field['default_value'] ?? '', + 'sort_order' => $index, + ]); + } + } + + // 섹션 및 항목 + if (! empty($data['sections'])) { + foreach ($data['sections'] as $sIndex => $section) { + $newSection = DocumentTemplateSection::create([ + 'template_id' => $template->id, + 'title' => $section['title'] ?? '', + 'image_path' => $section['image_path'] ?? null, + 'sort_order' => $sIndex, + ]); + + if (! empty($section['items'])) { + foreach ($section['items'] as $iIndex => $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' => $iIndex, + ]); + } + } + } + } + + // 컬럼 + if (! empty($data['columns'])) { + foreach ($data['columns'] as $index => $column) { + DocumentTemplateColumn::create([ + 'template_id' => $template->id, + 'label' => $column['label'] ?? '', + 'width' => $column['width'] ?? '100px', + 'column_type' => $column['column_type'] ?? 'text', + 'group_name' => $column['group_name'] ?? null, + 'sub_labels' => $column['sub_labels'] ?? null, + 'sort_order' => $index, + ]); + } + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/CommonCodeController.php b/app/Http/Controllers/CommonCodeController.php index 734a9372..236f6e04 100644 --- a/app/Http/Controllers/CommonCodeController.php +++ b/app/Http/Controllers/CommonCodeController.php @@ -34,6 +34,7 @@ class CommonCodeController extends Controller 'bad_debt_progress' => '대손진행', 'height_construction_cost' => '높이시공비', 'width_construction_cost' => '폭시공비', + 'document_type' => '문서분류', ]; /** diff --git a/app/Http/Controllers/DocumentTemplateController.php b/app/Http/Controllers/DocumentTemplateController.php new file mode 100644 index 00000000..22d18f54 --- /dev/null +++ b/app/Http/Controllers/DocumentTemplateController.php @@ -0,0 +1,133 @@ + $this->getDocumentTypes(), + ]); + } + + /** + * 문서양식 생성 페이지 + */ + public function create(): View + { + return view('document-templates.edit', [ + 'template' => null, + 'templateData' => null, + 'isCreate' => true, + 'documentTypes' => $this->getDocumentTypes(), + ]); + } + + /** + * 문서양식 수정 페이지 + */ + public function edit(int $id): View + { + $template = DocumentTemplate::with([ + 'approvalLines', + 'basicFields', + 'sections.items', + 'columns', + ])->findOrFail($id); + + // JavaScript용 데이터 변환 + $templateData = $this->prepareTemplateData($template); + + return view('document-templates.edit', [ + 'template' => $template, + 'templateData' => $templateData, + 'isCreate' => false, + 'documentTypes' => $this->getDocumentTypes(), + ]); + } + + /** + * 문서분류 목록 조회 (글로벌 + 테넌트) + */ + private function getDocumentTypes(): array + { + $tenantId = session('selected_tenant_id'); + + return CommonCode::query() + ->where(function ($query) use ($tenantId) { + $query->whereNull('tenant_id'); + if ($tenantId) { + $query->orWhere('tenant_id', $tenantId); + } + }) + ->where('code_group', 'document_type') + ->where('is_active', true) + ->orderBy('sort_order') + ->pluck('name', 'code') + ->toArray(); + } + + /** + * JavaScript용 템플릿 데이터 준비 + */ + private function prepareTemplateData(DocumentTemplate $template): array + { + return [ + 'name' => $template->name, + 'category' => $template->category, + 'title' => $template->title, + 'company_name' => $template->company_name, + 'company_address' => $template->company_address, + 'company_contact' => $template->company_contact, + 'footer_remark_label' => $template->footer_remark_label, + 'footer_judgement_label' => $template->footer_judgement_label, + 'footer_judgement_options' => $template->footer_judgement_options, + 'is_active' => $template->is_active, + 'approval_lines' => $template->approvalLines->map(function ($l) { + return [ + 'id' => $l->id, + 'name' => $l->name, + 'dept' => $l->dept, + 'role' => $l->role, + ]; + })->toArray(), + 'sections' => $template->sections->map(function ($s) { + return [ + 'id' => $s->id, + 'title' => $s->title, + 'image_path' => $s->image_path, + 'items' => $s->items->map(function ($i) { + return [ + 'id' => $i->id, + 'category' => $i->category, + 'item' => $i->item, + 'standard' => $i->standard, + 'method' => $i->method, + 'frequency' => $i->frequency, + 'regulation' => $i->regulation, + ]; + })->toArray(), + ]; + })->toArray(), + 'columns' => $template->columns->map(function ($c) { + return [ + 'id' => $c->id, + 'label' => $c->label, + 'width' => $c->width, + 'column_type' => $c->column_type, + 'group_name' => $c->group_name, + 'sub_labels' => $c->sub_labels, + ]; + })->toArray(), + ]; + } +} \ No newline at end of file diff --git a/app/Models/DocumentTemplate.php b/app/Models/DocumentTemplate.php new file mode 100644 index 00000000..0a79e777 --- /dev/null +++ b/app/Models/DocumentTemplate.php @@ -0,0 +1,69 @@ + 'array', + 'is_active' => 'boolean', + ]; + + /** + * 결재라인 + */ + public function approvalLines(): HasMany + { + return $this->hasMany(DocumentTemplateApprovalLine::class, 'template_id') + ->orderBy('sort_order'); + } + + /** + * 기본 필드 + */ + public function basicFields(): HasMany + { + return $this->hasMany(DocumentTemplateBasicField::class, 'template_id') + ->orderBy('sort_order'); + } + + /** + * 검사 기준서 섹션 + */ + public function sections(): HasMany + { + return $this->hasMany(DocumentTemplateSection::class, 'template_id') + ->orderBy('sort_order'); + } + + /** + * 테이블 컬럼 + */ + public function columns(): HasMany + { + return $this->hasMany(DocumentTemplateColumn::class, 'template_id') + ->orderBy('sort_order'); + } +} \ No newline at end of file diff --git a/app/Models/DocumentTemplateApprovalLine.php b/app/Models/DocumentTemplateApprovalLine.php new file mode 100644 index 00000000..6997cdc8 --- /dev/null +++ b/app/Models/DocumentTemplateApprovalLine.php @@ -0,0 +1,29 @@ + 'integer', + ]; + + public function template(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class, 'template_id'); + } +} \ No newline at end of file diff --git a/app/Models/DocumentTemplateBasicField.php b/app/Models/DocumentTemplateBasicField.php new file mode 100644 index 00000000..0f2c3ef9 --- /dev/null +++ b/app/Models/DocumentTemplateBasicField.php @@ -0,0 +1,29 @@ + 'integer', + ]; + + public function template(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class, 'template_id'); + } +} \ No newline at end of file diff --git a/app/Models/DocumentTemplateColumn.php b/app/Models/DocumentTemplateColumn.php new file mode 100644 index 00000000..ab1a0c67 --- /dev/null +++ b/app/Models/DocumentTemplateColumn.php @@ -0,0 +1,32 @@ + 'array', + 'sort_order' => 'integer', + ]; + + public function template(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class, 'template_id'); + } +} \ No newline at end of file diff --git a/app/Models/DocumentTemplateSection.php b/app/Models/DocumentTemplateSection.php new file mode 100644 index 00000000..b808c774 --- /dev/null +++ b/app/Models/DocumentTemplateSection.php @@ -0,0 +1,35 @@ + 'integer', + ]; + + public function template(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class, 'template_id'); + } + + public function items(): HasMany + { + return $this->hasMany(DocumentTemplateSectionItem::class, 'section_id') + ->orderBy('sort_order'); + } +} \ No newline at end of file diff --git a/app/Models/DocumentTemplateSectionItem.php b/app/Models/DocumentTemplateSectionItem.php new file mode 100644 index 00000000..589f17aa --- /dev/null +++ b/app/Models/DocumentTemplateSectionItem.php @@ -0,0 +1,32 @@ + 'integer', + ]; + + public function section(): BelongsTo + { + return $this->belongsTo(DocumentTemplateSection::class, 'section_id'); + } +} \ No newline at end of file diff --git a/database/seeders/MngMenuSeeder.php b/database/seeders/MngMenuSeeder.php index f146e39b..c848078c 100644 --- a/database/seeders/MngMenuSeeder.php +++ b/database/seeders/MngMenuSeeder.php @@ -299,6 +299,14 @@ protected function seedMainMenus(): void 'hidden' => true, 'options' => ['route_name' => 'categories.index', 'section' => 'main', 'meta' => ['status' => 'preparing']], ]); + $this->createMenu([ + 'parent_id' => $productionGroup->id, + 'name' => '문서양식관리', + 'url' => '/document-templates', + 'icon' => 'document-duplicate', + 'sort_order' => $prodSubOrder++, + 'options' => ['route_name' => 'document-templates.index', 'section' => 'main'], + ]); // ======================================== // 콘텐츠 관리 그룹 diff --git a/resources/views/department-permissions/partials/permission-matrix.blade.php b/resources/views/department-permissions/partials/permission-matrix.blade.php index 357e8206..3de478b3 100644 --- a/resources/views/department-permissions/partials/permission-matrix.blade.php +++ b/resources/views/department-permissions/partials/permission-matrix.blade.php @@ -74,8 +74,7 @@ class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outl {{ isset($permissions[$menu->id][$type]) && $permissions[$menu->id][$type] ? 'checked' : '' }} class="h-5 w-5 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" hx-post="/api/admin/department-permissions/toggle" - hx-trigger="click" - hx-target="#permission-matrix" + hx-swap="none" hx-include="[name='department_id'],[name='guard_name']" hx-vals='{"menu_id": {{ $menu->id }}, "permission_type": "{{ $type }}"}' > diff --git a/resources/views/document-templates/edit.blade.php b/resources/views/document-templates/edit.blade.php new file mode 100644 index 00000000..0d3f02db --- /dev/null +++ b/resources/views/document-templates/edit.blade.php @@ -0,0 +1,845 @@ +@extends('layouts.app') + +@section('title', $isCreate ? '문서양식 등록' : '문서양식 편집') + +@section('content') +
+ +
+
+

+ {{ $isCreate ? '문서양식 등록' : '문서양식 편집' }} +

+

+ 검사 성적서, 작업지시서 등의 문서 양식을 설정합니다. +

+
+
+ + + 목록 + + +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + + + + + + + +
+ + + +@endsection + +@push('scripts') + + + + +@endpush \ No newline at end of file diff --git a/resources/views/document-templates/index.blade.php b/resources/views/document-templates/index.blade.php new file mode 100644 index 00000000..deb85370 --- /dev/null +++ b/resources/views/document-templates/index.blade.php @@ -0,0 +1,165 @@ +@extends('layouts.app') + +@section('title', '문서양식 관리') + +@section('content') + +
+
+

문서양식 관리

+ +
+
+ + + + + 새 양식 + +
+
+ + + +
+ + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+ + +
+ +
+
+
+
+@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/document-templates/partials/table.blade.php b/resources/views/document-templates/partials/table.blade.php new file mode 100644 index 00000000..40e7ff6c --- /dev/null +++ b/resources/views/document-templates/partials/table.blade.php @@ -0,0 +1,102 @@ +
+ + + + + + + + + + + + + + + @forelse($templates as $template) + + + + + + + + + + + @empty + + + + @endforelse + +
양식명분류문서 제목섹션컬럼활성수정일작업
+ + {{ $template->name }} + + + @if($template->category) + + {{ $template->category }} + + @else + - + @endif + + {{ $template->title ?: '-' }} + + {{ $template->sections_count ?? 0 }} + + {{ $template->columns_count ?? 0 }} + + + + {{ $template->updated_at->format('Y-m-d') }} + +
+ + + + + + +
+
+
+ + + +

등록된 문서양식이 없습니다.

+ + + 새 양식 만들기 + +
+
+
+ + +@if($templates->hasPages()) +
+ @include('partials.pagination', ['paginator' => $templates, 'htmxTarget' => '#template-table', 'htmxTrigger' => 'filterSubmit']) +
+@endif \ No newline at end of file diff --git a/resources/views/role-permissions/partials/permission-matrix.blade.php b/resources/views/role-permissions/partials/permission-matrix.blade.php index bf576594..a6bfa256 100644 --- a/resources/views/role-permissions/partials/permission-matrix.blade.php +++ b/resources/views/role-permissions/partials/permission-matrix.blade.php @@ -74,7 +74,7 @@ class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outl {{ isset($permissions[$menu->id][$type]) && $permissions[$menu->id][$type] ? 'checked' : '' }} class="h-5 w-5 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" hx-post="/api/admin/role-permissions/toggle" - hx-target="#permission-matrix" + hx-swap="none" hx-include="[name='role_id'],[name='guard_name']" hx-vals='{"menu_id": {{ $menu->id }}, "permission_type": "{{ $type }}"}' > diff --git a/routes/api.php b/routes/api.php index aefccab6..1f170fd1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,7 +3,7 @@ use App\Http\Controllers\Api\Admin\BoardController; use App\Http\Controllers\Api\Admin\CustomerCenterController; use App\Http\Controllers\Api\Admin\DailyLogController; -use App\Http\Controllers\Api\Admin\DepartmentController; +use App\Http\Controllers\Api\Admin\DepartmentController;use App\Http\Controllers\Api\Admin\DocumentTemplateApiController; use App\Http\Controllers\Api\Admin\GlobalMenuController; use App\Http\Controllers\Api\Admin\ItemFieldController; use App\Http\Controllers\Api\Admin\MeetingLogController; @@ -733,6 +733,21 @@ Route::delete('/{id}', [\App\Http\Controllers\Api\BizCertController::class, 'destroy'])->name('destroy'); }); +/* +|-------------------------------------------------------------------------- +| 문서양식 관리 API +|-------------------------------------------------------------------------- +*/ +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/document-templates')->name('api.admin.document-templates.')->group(function () { + Route::get('/', [DocumentTemplateApiController::class, 'index'])->name('index'); + Route::post('/', [DocumentTemplateApiController::class, 'store'])->name('store'); + Route::get('/{id}', [DocumentTemplateApiController::class, 'show'])->name('show'); + Route::put('/{id}', [DocumentTemplateApiController::class, 'update'])->name('update'); + Route::delete('/{id}', [DocumentTemplateApiController::class, 'destroy'])->name('destroy'); + Route::post('/{id}/toggle-active', [DocumentTemplateApiController::class, 'toggleActive'])->name('toggle-active'); + Route::post('/upload-image', [DocumentTemplateApiController::class, 'uploadImage'])->name('upload-image'); +}); + /* |-------------------------------------------------------------------------- | 웹 녹음 AI 요약 API diff --git a/routes/web.php b/routes/web.php index 90d1dedc..ec522c52 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,6 +26,7 @@ use App\Http\Controllers\TenantController; use App\Http\Controllers\TenantSettingController; use App\Http\Controllers\CommonCodeController; +use App\Http\Controllers\DocumentTemplateController; use App\Http\Controllers\MenuSyncController; use App\Http\Controllers\UserController; use Illuminate\Support\Facades\Route; @@ -308,6 +309,13 @@ Route::delete('/{id}', [CommonCodeController::class, 'destroy'])->name('destroy'); }); + // 문서양식 관리 + Route::prefix('document-templates')->name('document-templates.')->group(function () { + Route::get('/', [DocumentTemplateController::class, 'index'])->name('index'); + Route::get('/create', [DocumentTemplateController::class, 'create'])->name('create'); + Route::get('/{id}/edit', [DocumentTemplateController::class, 'edit'])->name('edit'); + }); + /* |-------------------------------------------------------------------------- | 바로빌 Routes