diff --git a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php index d64be324..a27e3d6a 100644 --- a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php @@ -30,6 +30,12 @@ public function index(Request $request): View $query->where('tenant_id', $tenantId); } + // 슈퍼관리자 휴지통 조회 + $showTrashed = $request->filled('trashed') && auth()->user()?->is_super_admin; + if ($showTrashed) { + $query->onlyTrashed(); + } + // 검색 if ($search = $request->input('search')) { $query->where(function ($q) use ($search) { @@ -45,14 +51,14 @@ public function index(Request $request): View } // 활성 상태 필터 - if ($request->filled('is_active')) { + if ($request->filled('is_active') && !$showTrashed) { $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')); + return view('document-templates.partials.table', compact('templates', 'showTrashed')); } /** @@ -89,6 +95,9 @@ public function store(Request $request): JsonResponse 'footer_judgement_label' => 'nullable|string|max:50', 'footer_judgement_options' => 'nullable|array', 'is_active' => 'boolean', + 'linked_item_ids' => 'nullable|array', + 'linked_item_ids.*' => 'integer', + 'linked_process_id' => 'nullable|integer', // 관계 데이터 'approval_lines' => 'nullable|array', 'basic_fields' => 'nullable|array', @@ -111,6 +120,8 @@ public function store(Request $request): JsonResponse 'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정', 'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'], 'is_active' => $validated['is_active'] ?? true, + 'linked_item_ids' => $validated['linked_item_ids'] ?? null, + 'linked_process_id' => $validated['linked_process_id'] ?? null, ]); // 관계 데이터 저장 @@ -151,6 +162,9 @@ public function update(Request $request, int $id): JsonResponse 'footer_judgement_label' => 'nullable|string|max:50', 'footer_judgement_options' => 'nullable|array', 'is_active' => 'boolean', + 'linked_item_ids' => 'nullable|array', + 'linked_item_ids.*' => 'integer', + 'linked_process_id' => 'nullable|integer', // 관계 데이터 'approval_lines' => 'nullable|array', 'basic_fields' => 'nullable|array', @@ -172,6 +186,8 @@ public function update(Request $request, int $id): JsonResponse 'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정', 'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'], 'is_active' => $validated['is_active'] ?? true, + 'linked_item_ids' => $validated['linked_item_ids'] ?? null, + 'linked_process_id' => $validated['linked_process_id'] ?? null, ]); // 관계 데이터 저장 (기존 데이터 삭제 후 재생성) @@ -195,11 +211,12 @@ public function update(Request $request, int $id): JsonResponse } /** - * 삭제 + * 삭제 (소프트 삭제) */ public function destroy(int $id): JsonResponse { $template = DocumentTemplate::findOrFail($id); + $template->update(['deleted_by' => auth()->id()]); $template->delete(); return response()->json([ @@ -208,6 +225,77 @@ public function destroy(int $id): JsonResponse ]); } + /** + * 영구삭제 (슈퍼관리자 전용) + */ + public function forceDestroy(int $id): JsonResponse + { + if (!auth()->user()?->is_super_admin) { + return response()->json([ + 'success' => false, + 'message' => '슈퍼관리자만 영구 삭제할 수 있습니다.', + ], 403); + } + + $tenantId = session('selected_tenant_id'); + $template = DocumentTemplate::withTrashed() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + // 이 양식을 참조하는 문서가 있는지 확인 (소프트삭제 포함) + $documentCount = \App\Models\Documents\Document::withTrashed() + ->where('template_id', $template->id) + ->count(); + + if ($documentCount > 0) { + return response()->json([ + 'success' => false, + 'message' => "이 양식을 사용한 문서가 {$documentCount}건 있어 영구 삭제할 수 없습니다. 문서를 먼저 삭제해주세요.", + ], 422); + } + + // 관련 데이터도 영구삭제 + $template->approvalLines()->delete(); + $template->basicFields()->delete(); + $template->sections()->each(function ($section) { + $section->items()->delete(); + $section->delete(); + }); + $template->columns()->delete(); + $template->forceDelete(); + + return response()->json([ + 'success' => true, + 'message' => '문서양식이 영구 삭제되었습니다.', + ]); + } + + /** + * 삭제된 문서양식 복원 (슈퍼관리자 전용) + */ + public function restore(int $id): JsonResponse + { + if (!auth()->user()?->is_super_admin) { + return response()->json([ + 'success' => false, + 'message' => '슈퍼관리자만 복원할 수 있습니다.', + ], 403); + } + + $tenantId = session('selected_tenant_id'); + $template = DocumentTemplate::onlyTrashed() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $template->update(['deleted_by' => null]); + $template->restore(); + + return response()->json([ + 'success' => true, + 'message' => '문서양식이 복원되었습니다.', + ]); + } + /** * 활성 상태 토글 */ @@ -252,6 +340,8 @@ public function duplicate(Request $request, int $id): JsonResponse 'footer_judgement_label' => $source->footer_judgement_label, 'footer_judgement_options' => $source->footer_judgement_options, 'is_active' => false, + 'linked_item_ids' => $source->linked_item_ids, + 'linked_process_id' => $source->linked_process_id, ]); foreach ($source->approvalLines as $line) { @@ -260,6 +350,7 @@ public function duplicate(Request $request, int $id): JsonResponse 'name' => $line->name, 'dept' => $line->dept, 'role' => $line->role, + 'user_id' => $line->user_id, 'sort_order' => $line->sort_order, ]); } @@ -395,6 +486,7 @@ private function saveRelations(DocumentTemplate $template, array $data, bool $de 'name' => $line['name'] ?? '', 'dept' => $line['dept'] ?? '', 'role' => $line['role'] ?? '', + 'user_id' => $line['user_id'] ?? null, 'sort_order' => $index, ]); } diff --git a/app/Http/Controllers/Api/Admin/ItemApiController.php b/app/Http/Controllers/Api/Admin/ItemApiController.php new file mode 100644 index 00000000..d28be952 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/ItemApiController.php @@ -0,0 +1,44 @@ +input('q', ''); + + $items = Item::query() + ->where('tenant_id', $tenantId) + ->where('is_active', true) + ->when($query, function ($q) use ($query) { + $q->where(function ($sub) use ($query) { + $sub->where('name', 'like', "%{$query}%") + ->orWhere('code', 'like', "%{$query}%"); + }); + }) + ->when($request->input('item_type'), function ($q, $types) { + $q->whereIn('item_type', explode(',', $types)); + }) + ->when($request->input('ids'), function ($q, $ids) { + $q->whereIn('id', explode(',', $ids)); + }) + ->orderBy('name') + ->limit(30) + ->get(['id', 'code', 'name', 'item_type', 'unit']); + + return response()->json([ + 'success' => true, + 'data' => $items, + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/ProcessApiController.php b/app/Http/Controllers/Api/Admin/ProcessApiController.php new file mode 100644 index 00000000..26f9d06d --- /dev/null +++ b/app/Http/Controllers/Api/Admin/ProcessApiController.php @@ -0,0 +1,38 @@ +input('q', ''); + + $processes = Process::query() + ->where('tenant_id', $tenantId) + ->where('is_active', true) + ->when($query, function ($q) use ($query) { + $q->where(function ($sub) use ($query) { + $sub->where('process_name', 'like', "%{$query}%") + ->orWhere('process_code', 'like', "%{$query}%"); + }); + }) + ->orderBy('process_name') + ->limit(30) + ->get(['id', 'process_code', 'process_name']); + + return response()->json([ + 'success' => true, + 'data' => $processes, + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/TenantUserApiController.php b/app/Http/Controllers/Api/Admin/TenantUserApiController.php new file mode 100644 index 00000000..02d1d437 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/TenantUserApiController.php @@ -0,0 +1,54 @@ +input('q', ''); + + $users = DB::table('users') + ->join('user_tenants', function ($join) use ($tenantId) { + $join->on('users.id', '=', 'user_tenants.user_id') + ->where('user_tenants.tenant_id', $tenantId) + ->where('user_tenants.is_active', true); + }) + ->leftJoin('departments', function ($join) use ($tenantId) { + $join->on('departments.id', '=', DB::raw('( + SELECT du.department_id FROM department_user du + WHERE du.user_id = users.id AND du.is_primary = 1 + LIMIT 1 + )')); + }) + ->whereNull('users.deleted_at') + ->when($query, function ($q) use ($query) { + $q->where(function ($sub) use ($query) { + $sub->where('users.name', 'like', "%{$query}%") + ->orWhere('users.email', 'like', "%{$query}%"); + }); + }) + ->orderBy('users.name') + ->limit(30) + ->select([ + 'users.id', + 'users.name', + 'departments.name as department_name', + ]) + ->get(); + + return response()->json([ + 'success' => true, + 'data' => $users, + ]); + } +} diff --git a/app/Http/Controllers/DocumentTemplateController.php b/app/Http/Controllers/DocumentTemplateController.php index 1e896368..ec73da7e 100644 --- a/app/Http/Controllers/DocumentTemplateController.php +++ b/app/Http/Controllers/DocumentTemplateController.php @@ -4,6 +4,7 @@ use App\Models\DocumentTemplate; use App\Models\Tenants\Tenant; +use App\Models\User; use Illuminate\Http\Request; use Illuminate\View\View; @@ -103,12 +104,21 @@ private function prepareTemplateData(DocumentTemplate $template): array 'footer_judgement_label' => $template->footer_judgement_label, 'footer_judgement_options' => $template->footer_judgement_options, 'is_active' => $template->is_active, + 'linked_item_ids' => $template->linked_item_ids, + 'linked_process_id' => $template->linked_process_id, 'approval_lines' => $template->approvalLines->map(function ($l) { + $userName = null; + if ($l->user_id) { + $userName = User::where('id', $l->user_id)->value('name'); + } + return [ 'id' => $l->id, 'name' => $l->name, 'dept' => $l->dept, 'role' => $l->role, + 'user_id' => $l->user_id, + 'user_name' => $userName, ]; })->toArray(), 'basic_fields' => $template->basicFields->map(function ($f) { diff --git a/app/Models/DocumentTemplate.php b/app/Models/DocumentTemplate.php index 0a79e777..77159b78 100644 --- a/app/Models/DocumentTemplate.php +++ b/app/Models/DocumentTemplate.php @@ -24,10 +24,13 @@ class DocumentTemplate extends Model 'footer_judgement_label', 'footer_judgement_options', 'is_active', + 'linked_item_ids', + 'linked_process_id', ]; protected $casts = [ 'footer_judgement_options' => 'array', + 'linked_item_ids' => 'array', 'is_active' => 'boolean', ]; diff --git a/app/Models/DocumentTemplateApprovalLine.php b/app/Models/DocumentTemplateApprovalLine.php index 6997cdc8..0984f0e8 100644 --- a/app/Models/DocumentTemplateApprovalLine.php +++ b/app/Models/DocumentTemplateApprovalLine.php @@ -15,6 +15,7 @@ class DocumentTemplateApprovalLine extends Model 'name', 'dept', 'role', + 'user_id', 'sort_order', ]; diff --git a/app/Models/Process.php b/app/Models/Process.php new file mode 100644 index 00000000..1907c1c8 --- /dev/null +++ b/app/Models/Process.php @@ -0,0 +1,23 @@ + 'boolean', + ]; +} diff --git a/resources/views/document-templates/edit.blade.php b/resources/views/document-templates/edit.blade.php index 0deda018..004c1e89 100644 --- a/resources/views/document-templates/edit.blade.php +++ b/resources/views/document-templates/edit.blade.php @@ -72,14 +72,47 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
- - - @foreach($categories as $category) -
+ + + + + +
@@ -238,6 +271,8 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc footer_judgement_label: '종합판정', footer_judgement_options: ['합격', '불합격', '조건부합격'], is_active: true, + linked_item_ids: null, + linked_process_id: null, approval_lines: [], basic_fields: [], sections: [], @@ -263,6 +298,8 @@ function generateId() { templateState.footer_judgement_label = loadedData.footer_judgement_label || ''; templateState.footer_judgement_options = loadedData.footer_judgement_options || []; templateState.is_active = loadedData.is_active || false; + templateState.linked_item_ids = loadedData.linked_item_ids || null; + templateState.linked_process_id = loadedData.linked_process_id || null; templateState.approval_lines = loadedData.approval_lines || []; templateState.basic_fields = loadedData.basic_fields || []; templateState.sections = loadedData.sections || []; @@ -278,18 +315,56 @@ function generateId() { renderColumns(); }); + // ===== 연결 데이터 상태 ===== + let linkedItems = []; // 수입검사 연결 품목 [{id, code, name}] + let linkedProcess = null; // 품질검사 연결 공정 {id, process_code, process_name} + let itemSearchTimer = null; + let processSearchTimer = null; + let userSearchTimer = null; + // ===== 기본정보 ===== function initBasicInfo() { document.getElementById('name').value = templateState.name || ''; - document.getElementById('category').value = templateState.category || ''; document.getElementById('title').value = templateState.title || ''; - document.getElementById('company_name').value = templateState.company_name || ''; document.getElementById('footer_remark_label').value = templateState.footer_remark_label || ''; document.getElementById('footer_judgement_label').value = templateState.footer_judgement_label || ''; document.getElementById('is_active').checked = templateState.is_active; + // 회사명 자동채움 (생성 시) + @if($isCreate && $tenant) + document.getElementById('company_name').value = '{{ $tenant->company_name ?? '' }}'; + templateState.company_name = '{{ $tenant->company_name ?? '' }}'; + @else + document.getElementById('company_name').value = templateState.company_name || ''; + @endif + + // 분류 초기값 설정 + const categorySelect = document.getElementById('category'); + const categoryValue = templateState.category || ''; + const predefined = ['수입검사', '중간검사', '품질검사']; + const existingOptions = Array.from(categorySelect.options).map(o => o.value); + + if (categoryValue && !existingOptions.includes(categoryValue) && categoryValue !== '__custom__') { + // 기존 카테고리 목록에 없는 커스텀 값 + categorySelect.value = '__custom__'; + document.getElementById('category-custom').value = categoryValue; + document.getElementById('category-custom').classList.remove('hidden'); + } else { + categorySelect.value = categoryValue; + } + onCategoryChange(categorySelect.value, true); + + // 연결 품목 로드 (수입검사) + if (templateState.linked_item_ids && templateState.linked_item_ids.length > 0) { + loadLinkedItems(templateState.linked_item_ids); + } + // 연결 공정 로드 (품질검사) + if (templateState.linked_process_id) { + loadLinkedProcess(templateState.linked_process_id); + } + // 변경 이벤트 바인딩 - ['name', 'category', 'title', 'company_name', 'footer_remark_label', 'footer_judgement_label'].forEach(field => { + ['name', 'title', 'company_name', 'footer_remark_label', 'footer_judgement_label'].forEach(field => { document.getElementById(field).addEventListener('input', function() { templateState[field] = this.value; }); @@ -299,6 +374,258 @@ function initBasicInfo() { }); } + // ===== 분류 변경 핸들러 ===== + function onCategoryChange(value, isInit = false) { + const customInput = document.getElementById('category-custom'); + const itemsSection = document.getElementById('linked-items-section'); + const processSection = document.getElementById('linked-process-section'); + + // 직접 입력 모드 + if (value === '__custom__') { + customInput.classList.remove('hidden'); + customInput.focus(); + templateState.category = customInput.value; + } else { + customInput.classList.add('hidden'); + templateState.category = value; + } + + // 조건부 UI 토글 + itemsSection.classList.toggle('hidden', value !== '수입검사'); + processSection.classList.toggle('hidden', value !== '품질검사'); + + // 분류 변경 시 연결 데이터 초기화 (초기 로드 제외) + if (!isInit) { + if (value !== '수입검사') { + linkedItems = []; + templateState.linked_item_ids = null; + renderLinkedItemTags(); + } + if (value !== '품질검사') { + linkedProcess = null; + templateState.linked_process_id = null; + renderLinkedProcessDisplay(); + } + } + } + + // ===== 수입검사: 품목 검색 ===== + function searchLinkedItems(query) { + clearTimeout(itemSearchTimer); + if (query.length < 1) { + document.getElementById('item-search-results').classList.add('hidden'); + return; + } + itemSearchTimer = setTimeout(() => { + fetch(`/api/admin/items/search?q=${encodeURIComponent(query)}&item_type=RM,SM`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }) + .then(r => r.json()) + .then(result => { + const container = document.getElementById('item-search-results'); + if (!result.success || !result.data.length) { + container.innerHTML = '
결과 없음
'; + container.classList.remove('hidden'); + return; + } + // RM, SM 필터링 (서버에서 못할 경우 클라이언트 필터) + const filtered = result.data.filter(item => + ['RM', 'SM'].includes(item.item_type) && !linkedItems.find(li => li.id === item.id) + ); + container.innerHTML = filtered.map(item => ` +
+ ${escapeHtml(item.name)} + ${escapeHtml(item.code)} (${item.item_type}) +
+ `).join(''); + container.classList.remove('hidden'); + }); + }, 300); + } + + function addLinkedItem(id, code, name) { + if (linkedItems.find(li => li.id === id)) return; + linkedItems.push({ id, code, name }); + templateState.linked_item_ids = linkedItems.map(li => li.id); + document.getElementById('item-search-input').value = ''; + document.getElementById('item-search-results').classList.add('hidden'); + renderLinkedItemTags(); + } + + function removeLinkedItem(id) { + linkedItems = linkedItems.filter(li => li.id !== id); + templateState.linked_item_ids = linkedItems.length > 0 ? linkedItems.map(li => li.id) : null; + renderLinkedItemTags(); + } + + function renderLinkedItemTags() { + const container = document.getElementById('linked-items-tags'); + container.innerHTML = linkedItems.map(item => ` + + ${escapeHtml(item.name)} (${escapeHtml(item.code)}) + + + `).join(''); + } + + function loadLinkedItems(ids) { + // 기존 연결 품목 정보 로드 (ids 파라미터로 한번에 조회) + fetch(`/api/admin/items/search?ids=${ids.join(',')}`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }) + .then(r => r.json()) + .then(result => { + if (result.success && result.data.length) { + result.data.forEach(item => { + if (!linkedItems.find(li => li.id === item.id)) { + linkedItems.push({ id: item.id, code: item.code, name: item.name }); + } + }); + renderLinkedItemTags(); + } + }); + } + + // ===== 품질검사: 공정 검색 ===== + function searchProcesses(query) { + clearTimeout(processSearchTimer); + if (query.length < 1) { + document.getElementById('process-search-results').classList.add('hidden'); + return; + } + processSearchTimer = setTimeout(() => { + fetch(`/api/admin/processes/search?q=${encodeURIComponent(query)}`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }) + .then(r => r.json()) + .then(result => { + const container = document.getElementById('process-search-results'); + if (!result.success || !result.data.length) { + container.innerHTML = '
결과 없음
'; + container.classList.remove('hidden'); + return; + } + container.innerHTML = result.data.map(proc => ` +
+ ${escapeHtml(proc.process_name)} + ${escapeHtml(proc.process_code)} +
+ `).join(''); + container.classList.remove('hidden'); + }); + }, 300); + } + + function selectLinkedProcess(id, code, name) { + linkedProcess = { id, process_code: code, process_name: name }; + templateState.linked_process_id = id; + document.getElementById('process-search-input').value = ''; + document.getElementById('process-search-results').classList.add('hidden'); + renderLinkedProcessDisplay(); + } + + function removeLinkedProcess() { + linkedProcess = null; + templateState.linked_process_id = null; + renderLinkedProcessDisplay(); + } + + function renderLinkedProcessDisplay() { + const container = document.getElementById('linked-process-display'); + if (!linkedProcess) { + container.innerHTML = ''; + return; + } + container.innerHTML = ` + + ${escapeHtml(linkedProcess.process_name)} (${escapeHtml(linkedProcess.process_code)}) + + + `; + } + + function loadLinkedProcess(id) { + fetch(`/api/admin/processes/search?q=`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }) + .then(r => r.json()) + .then(result => { + if (result.success && result.data.length) { + const proc = result.data.find(p => p.id === id); + if (proc) { + linkedProcess = { id: proc.id, process_code: proc.process_code, process_name: proc.process_name }; + renderLinkedProcessDisplay(); + } + } + }); + } + + // ===== 결재라인 단계명 변경 핸들러 ===== + function onApprovalNameChange(id, value) { + const line = templateState.approval_lines.find(l => l.id == id); + if (!line) return; + line.name = value; + if (value === '작성') { + line.dept = '(작성자)'; + line.role = '(작성자)'; + line.user_id = null; + line.user_name = null; + } else { + if (line.dept === '(작성자)') line.dept = ''; + if (line.role === '(작성자)') line.role = ''; + } + renderApprovalLines(); + } + + // ===== 결재라인 사용자 검색 ===== + function searchApprovalUser(lineId, query) { + clearTimeout(userSearchTimer); + const resultsContainer = document.getElementById(`user-results-${lineId}`); + if (query.length < 1) { + resultsContainer.classList.add('hidden'); + return; + } + userSearchTimer = setTimeout(() => { + fetch(`/api/admin/tenant-users/search?q=${encodeURIComponent(query)}`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }) + .then(r => r.json()) + .then(result => { + if (!result.success || !result.data.length) { + resultsContainer.innerHTML = '
결과 없음
'; + resultsContainer.classList.remove('hidden'); + return; + } + resultsContainer.innerHTML = result.data.map(user => ` +
+ ${escapeHtml(user.name)} + ${user.department_name ? `${escapeHtml(user.department_name)}` : ''} +
+ `).join(''); + resultsContainer.classList.remove('hidden'); + }); + }, 300); + } + + function selectApprovalUser(lineId, userId, userName, deptName) { + const line = templateState.approval_lines.find(l => l.id == lineId); + if (!line) return; + line.user_id = userId; + line.user_name = userName; + if (deptName && !line.dept) line.dept = deptName; + + // UI 업데이트 + const searchInput = document.getElementById(`user-search-${lineId}`); + if (searchInput) searchInput.value = userName; + const hiddenInput = document.getElementById(`user-id-${lineId}`); + if (hiddenInput) hiddenInput.value = userId; + const resultsContainer = document.getElementById(`user-results-${lineId}`); + if (resultsContainer) resultsContainer.classList.add('hidden'); + } + // ===== 종합판정 옵션 ===== function renderJudgementOptions() { const container = document.getElementById('judgement-options-container'); @@ -378,6 +705,7 @@ class="w-28 px-3 py-2 border border-gray-300 rounded-lg text-sm"> + ` + container.innerHTML = templateState.approval_lines.map((line, idx) => { + const isWriter = (line.name === '작성'); + const userDisplay = isWriter + ? '(작성자)' + : `
+ + + +
`; + const deptRoleDisplay = isWriter + ? '(작성자)' + : ` + `; + + return `
⋮⋮ ${idx + 1} - - - + + ${deptRoleDisplay} + ${userDisplay} -
- `).join(''); +
`; + }).join(''); } // ===== 섹션 관리 ===== @@ -915,6 +1265,8 @@ function saveTemplate() { footer_judgement_label: document.getElementById('footer_judgement_label').value, footer_judgement_options: templateState.footer_judgement_options.filter(o => o.trim() !== ''), is_active: document.getElementById('is_active').checked, + linked_item_ids: templateState.linked_item_ids, + linked_process_id: templateState.linked_process_id, approval_lines: templateState.approval_lines, basic_fields: templateState.basic_fields, sections: templateState.sections, @@ -1410,6 +1762,24 @@ function initSortable() { setTimeout(initSortable, 500); }); + // ===== 검색 드롭다운 닫기 ===== + document.addEventListener('click', function(e) { + // 품목 검색 결과 닫기 + if (!e.target.closest('#linked-items-section')) { + const ir = document.getElementById('item-search-results'); + if (ir) ir.classList.add('hidden'); + } + // 공정 검색 결과 닫기 + if (!e.target.closest('#linked-process-section')) { + const pr = document.getElementById('process-search-results'); + if (pr) pr.classList.add('hidden'); + } + // 사용자 검색 결과 닫기 + if (!e.target.closest('[id^="user-search-"]') && !e.target.closest('[id^="user-results-"]')) { + document.querySelectorAll('[id^="user-results-"]').forEach(el => el.classList.add('hidden')); + } + }); + // ===== 유틸리티 ===== function escapeHtml(text) { if (!text) return ''; diff --git a/routes/api.php b/routes/api.php index cfd3b858..c46a88b0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -787,6 +787,8 @@ 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::delete('/{id}/force', [DocumentTemplateApiController::class, 'forceDestroy'])->name('force-destroy'); + Route::post('/{id}/restore', [DocumentTemplateApiController::class, 'restore'])->name('restore'); Route::post('/{id}/toggle-active', [DocumentTemplateApiController::class, 'toggleActive'])->name('toggle-active'); Route::post('/{id}/duplicate', [DocumentTemplateApiController::class, 'duplicate'])->name('duplicate'); Route::post('/upload-image', [DocumentTemplateApiController::class, 'uploadImage'])->name('upload-image'); @@ -801,6 +803,33 @@ Route::get('/{group}', [DocumentTemplateApiController::class, 'getCommonCodes'])->name('group'); }); +/* +|-------------------------------------------------------------------------- +| 품목 검색 API (문서 작성 시 품명 자동완성) +|-------------------------------------------------------------------------- +*/ +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/items')->name('api.admin.items.')->group(function () { + Route::get('/search', [\App\Http\Controllers\Api\Admin\ItemApiController::class, 'search'])->name('search'); +}); + +/* +|-------------------------------------------------------------------------- +| 공정 검색 API (문서양식 품질검사 연결용) +|-------------------------------------------------------------------------- +*/ +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/processes')->name('api.admin.processes.')->group(function () { + Route::get('/search', [\App\Http\Controllers\Api\Admin\ProcessApiController::class, 'search'])->name('search'); +}); + +/* +|-------------------------------------------------------------------------- +| 테넌트 사용자 검색 API (결재라인 담당자 선택용) +|-------------------------------------------------------------------------- +*/ +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/tenant-users')->name('api.admin.tenant-users.')->group(function () { + Route::get('/search', [\App\Http\Controllers\Api\Admin\TenantUserApiController::class, 'search'])->name('search'); +}); + /* |-------------------------------------------------------------------------- | 문서 관리 API @@ -812,6 +841,8 @@ Route::get('/{id}', [DocumentApiController::class, 'show'])->name('show'); Route::patch('/{id}', [DocumentApiController::class, 'update'])->name('update'); Route::delete('/{id}', [DocumentApiController::class, 'destroy'])->name('destroy'); + Route::delete('/{id}/force', [DocumentApiController::class, 'forceDestroy'])->name('force-destroy'); + Route::post('/{id}/restore', [DocumentApiController::class, 'restore'])->name('restore'); // 결재 워크플로우 Route::post('/{id}/submit', [DocumentApiController::class, 'submit'])->name('submit'); Route::post('/{id}/approve', [DocumentApiController::class, 'approve'])->name('approve');