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
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 `
- `).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');