diff --git a/app/Http/Controllers/Api/Admin/CategoryApiController.php b/app/Http/Controllers/Api/Admin/CategoryApiController.php index 8685861e..cfd5506f 100644 --- a/app/Http/Controllers/Api/Admin/CategoryApiController.php +++ b/app/Http/Controllers/Api/Admin/CategoryApiController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\Category; +use App\Models\GlobalCategory; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -214,6 +215,52 @@ public function toggle(int $id): JsonResponse ]); } + /** + * 테넌트 카테고리를 글로벌로 복사 (HQ 또는 슈퍼관리자) + */ + public function promoteToGlobal(int $id): JsonResponse + { + $user = Auth::user(); + $tenantId = session('selected_tenant_id'); + $tenant = $tenantId ? \App\Models\Tenants\Tenant::find($tenantId) : null; + $isHQ = $tenant?->tenant_type === 'HQ'; + $isSuperAdmin = $user?->isSuperAdmin() ?? false; + + if (! $isHQ && ! $isSuperAdmin) { + return response()->json(['success' => false, 'message' => '본사 또는 슈퍼관리자만 글로벌로 복사할 수 있습니다.'], 403); + } + + $category = Category::findOrFail($id); + + // 이미 글로벌에 동일 코드가 있는지 확인 + $exists = GlobalCategory::query() + ->where('code_group', $category->code_group) + ->where('code', $category->code) + ->whereNull('deleted_at') + ->exists(); + + if ($exists) { + return response()->json(['success' => false, 'message' => '이미 동일한 글로벌 카테고리가 존재합니다.'], 400); + } + + GlobalCategory::create([ + 'parent_id' => null, + 'code_group' => $category->code_group, + 'code' => $category->code, + 'name' => $category->name, + 'profile_code' => $category->profile_code, + 'description' => $category->description, + 'is_active' => true, + 'sort_order' => $category->sort_order, + 'created_by' => Auth::id(), + ]); + + return response()->json([ + 'success' => true, + 'message' => '글로벌 카테고리로 복사되었습니다.', + ]); + } + /** * 이동 (부모 변경) */ diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 3379634c..d53927e2 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -82,6 +82,8 @@ public function index(Request $request): View|Response $tenantCodes = $tenantCategoriesRaw->pluck('code')->toArray(); } + $globalCodes = $globalCategoriesRaw->pluck('code')->toArray(); + return view('categories.index', [ 'codeGroups' => $codeGroups, 'codeGroupLabels' => $codeGroupLabels, @@ -89,6 +91,7 @@ public function index(Request $request): View|Response 'globalCategories' => $globalCategories, 'tenantCategories' => $tenantCategories, 'tenantCodes' => $tenantCodes, + 'globalCodes' => $globalCodes, 'tenant' => $tenant, 'isHQ' => $isHQ, ]); diff --git a/app/Http/Controllers/CommonCodeController.php b/app/Http/Controllers/CommonCodeController.php index 47d6d33e..96a69ec4 100644 --- a/app/Http/Controllers/CommonCodeController.php +++ b/app/Http/Controllers/CommonCodeController.php @@ -85,6 +85,9 @@ public function index(Request $request): View|Response ->get(); } + $globalCodeKeys = $globalCodes->pluck('code')->toArray(); + $tenantCodeKeys = $tenantCodes->pluck('code')->toArray(); + return view('common-codes.index', [ 'tenant' => $tenant, 'isHQ' => $isHQ, @@ -92,6 +95,8 @@ public function index(Request $request): View|Response 'selectedGroup' => $selectedGroup, 'globalCodes' => $globalCodes, 'tenantCodes' => $tenantCodes, + 'globalCodeKeys' => $globalCodeKeys, + 'tenantCodeKeys' => $tenantCodeKeys, ]); } @@ -273,6 +278,86 @@ public function toggle(Request $request, int $id): JsonResponse ]); } + /** + * 테넌트 코드를 글로벌로 일괄 복사 (HQ 또는 슈퍼관리자) + */ + public function bulkPromoteToGlobal(Request $request): RedirectResponse|JsonResponse + { + $tenant = session('selected_tenant_id') ? Tenant::find(session('selected_tenant_id')) : null; + $isHQ = $tenant?->tenant_type === 'HQ'; + $isSuperAdmin = auth()->user()?->isSuperAdmin() ?? false; + + if (! $isHQ && ! $isSuperAdmin) { + if ($request->ajax()) { + return response()->json(['error' => '본사 또는 슈퍼관리자만 글로벌로 복사할 수 있습니다.'], 403); + } + return redirect()->back()->with('error', '본사 또는 슈퍼관리자만 글로벌로 복사할 수 있습니다.'); + } + + $idsJson = $request->input('ids_json'); + if ($idsJson) { + $ids = json_decode($idsJson, true); + if (! is_array($ids) || empty($ids)) { + return redirect()->back()->with('error', '복사할 코드를 선택해주세요.'); + } + } else { + $validated = $request->validate(['ids' => 'required|array|min:1', 'ids.*' => 'integer']); + $ids = $validated['ids']; + } + + $codeGroup = null; + $copiedCount = 0; + $skippedCount = 0; + + DB::beginTransaction(); + try { + foreach ($ids as $id) { + $tenantCode = CommonCode::whereNotNull('tenant_id')->find($id); + if (! $tenantCode) { + continue; + } + + $codeGroup = $tenantCode->code_group; + + $exists = CommonCode::query() + ->whereNull('tenant_id') + ->where('code_group', $tenantCode->code_group) + ->where('code', $tenantCode->code) + ->exists(); + + if ($exists) { + $skippedCount++; + continue; + } + + CommonCode::create([ + 'tenant_id' => null, + 'code_group' => $tenantCode->code_group, + 'code' => $tenantCode->code, + 'name' => $tenantCode->name, + 'sort_order' => $tenantCode->sort_order, + 'attributes' => $tenantCode->attributes, + 'is_active' => true, + ]); + + $copiedCount++; + } + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + return redirect()->back()->with('error', '복사 중 오류가 발생했습니다.'); + } + + $message = "{$copiedCount}개 코드가 글로벌로 복사되었습니다."; + if ($skippedCount > 0) { + $message .= " ({$skippedCount}개는 이미 존재하여 건너뜀)"; + } + + return redirect() + ->route('common-codes.index', ['group' => $codeGroup ?? 'item_type']) + ->with('success', $message); + } + /** * 글로벌 코드를 테넌트용으로 복사 */ @@ -423,6 +508,64 @@ public function bulkCopy(Request $request): RedirectResponse|JsonResponse ->with('success', $message); } + /** + * 테넌트 코드를 글로벌로 복사 (HQ 또는 슈퍼관리자) + */ + public function promoteToGlobal(Request $request, int $id): RedirectResponse|JsonResponse + { + $tenantId = session('selected_tenant_id'); + $tenant = $tenantId ? Tenant::find($tenantId) : null; + $isHQ = $tenant?->tenant_type === 'HQ'; + $isSuperAdmin = auth()->user()?->isSuperAdmin() ?? false; + + if (! $isHQ && ! $isSuperAdmin) { + if ($request->ajax()) { + return response()->json(['error' => '본사 또는 슈퍼관리자만 글로벌로 복사할 수 있습니다.'], 403); + } + return redirect()->back()->with('error', '본사 또는 슈퍼관리자만 글로벌로 복사할 수 있습니다.'); + } + + $tenantCode = CommonCode::whereNotNull('tenant_id')->find($id); + if (! $tenantCode) { + if ($request->ajax()) { + return response()->json(['error' => '테넌트 코드를 찾을 수 없습니다.'], 404); + } + return redirect()->back()->with('error', '테넌트 코드를 찾을 수 없습니다.'); + } + + // 이미 글로벌에 동일 코드가 있는지 확인 + $exists = CommonCode::query() + ->whereNull('tenant_id') + ->where('code_group', $tenantCode->code_group) + ->where('code', $tenantCode->code) + ->exists(); + + if ($exists) { + if ($request->ajax()) { + return response()->json(['error' => '이미 동일한 글로벌 코드가 존재합니다.'], 400); + } + return redirect()->back()->with('error', '이미 동일한 글로벌 코드가 존재합니다.'); + } + + CommonCode::create([ + 'tenant_id' => null, + 'code_group' => $tenantCode->code_group, + 'code' => $tenantCode->code, + 'name' => $tenantCode->name, + 'sort_order' => $tenantCode->sort_order, + 'attributes' => $tenantCode->attributes, + 'is_active' => true, + ]); + + if ($request->ajax()) { + return response()->json(['success' => true, 'message' => '글로벌 코드로 복사되었습니다.']); + } + + return redirect() + ->route('common-codes.index', ['group' => $tenantCode->code_group]) + ->with('success', '테넌트 코드가 글로벌로 복사되었습니다.'); + } + /** * 코드 삭제 (테넌트 코드만) */ diff --git a/resources/views/categories/index.blade.php b/resources/views/categories/index.blade.php index cdc37599..881a7240 100644 --- a/resources/views/categories/index.blade.php +++ b/resources/views/categories/index.blade.php @@ -172,11 +172,31 @@ class="relative inline-flex h-4 w-7 items-center rounded-full transition-colors
| + + | + @endif코드 | 이름 | 순서 | @@ -235,7 +257,16 @@ class="p-1 text-gray-400 hover:text-red-600 transition-colors"|||||||
|---|---|---|---|---|---|---|---|---|---|---|
| + + | + @endif{{ $code->code }} | @@ -244,8 +275,8 @@ class="p-1 text-gray-400 hover:text-red-600 transition-colors"@@ -271,7 +302,7 @@ class="p-1 text-gray-400 hover:text-red-600 transition-colors" | ||||||||
| + |
테넌트 코드가 없습니다. 글로벌 코드를 복사하거나 새로 추가하세요. |
@@ -403,6 +434,12 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
@csrf
+
+
+
|||||||||
| - | 타입 | 그룹 | 코드 | 이름 | @@ -276,13 +267,6 @@ class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs font-med class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500" {{ $inBoth ? 'disabled' : '' }}> -- @if($code['is_global'] ?? false) - 글로벌 - @else - 테넌트 - @endif - | {{ $code['code_group'] }} | {{ $code['code'] }} | {{ $code['name'] }} | @@ -295,6 +279,8 @@ class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500" + +@include('partials.sync-settings-modal') @endsection @push('scripts') diff --git a/resources/views/menus/sync.blade.php b/resources/views/menus/sync.blade.php index 4f864e45..2cfed22a 100644 --- a/resources/views/menus/sync.blade.php +++ b/resources/views/menus/sync.blade.php @@ -193,96 +193,7 @@ class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs font-med - - +@include('partials.sync-settings-modal') @endsection @push('scripts') @@ -290,89 +201,6 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon const selectedEnv = '{{ $selectedEnv }}'; const csrfToken = '{{ csrf_token() }}'; - // 설정 모달 - function openSettingsModal() { - document.getElementById('settingsModal').classList.remove('hidden'); - document.getElementById('settingsModal').classList.add('flex'); - } - - function closeSettingsModal() { - document.getElementById('settingsModal').classList.add('hidden'); - document.getElementById('settingsModal').classList.remove('flex'); - } - - // 설정 저장 - async function saveSettings() { - const data = { - environments: { - dev: { - name: '개발', - url: document.getElementById('devUrl').value, - api_key: document.getElementById('devApiKey').value - }, - prod: { - name: '운영', - url: document.getElementById('prodUrl').value, - api_key: document.getElementById('prodApiKey').value - } - } - }; - - try { - const response = await fetch('{{ route("menus.sync.settings") }}', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfToken, - 'Accept': 'application/json' - }, - body: JSON.stringify(data) - }); - - const result = await response.json(); - if (result.success) { - alert('설정이 저장되었습니다.'); - location.reload(); - } else { - alert(result.error || '저장 실패'); - } - } catch (e) { - alert('오류 발생: ' + e.message); - } - } - - // 연결 테스트 - async function testConnection(env) { - const url = document.getElementById(env + 'Url').value; - const apiKey = document.getElementById(env + 'ApiKey').value; - - if (!url || !apiKey) { - alert('URL과 API Key를 입력해주세요.'); - return; - } - - try { - const response = await fetch('{{ route("menus.sync.test") }}', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfToken, - 'Accept': 'application/json' - }, - body: JSON.stringify({ url, api_key: apiKey }) - }); - - const result = await response.json(); - if (result.success) { - alert(`연결 성공!\n환경: ${result.environment}\n메뉴 수: ${result.menu_count}개`); - } else { - alert('연결 실패: ' + result.message); - } - } catch (e) { - alert('오류 발생: ' + e.message); - } - } - // 선택된 메뉴 Push async function pushSelected() { const checkboxes = document.querySelectorAll('input[name="local_menu"]:checked'); @@ -449,16 +277,6 @@ function closeSettingsModal() { } } - // 모달 외부 클릭 시 닫기 - document.getElementById('settingsModal').addEventListener('click', function(e) { - if (e.target === this) closeSettingsModal(); - }); - - // ESC 키로 모달 닫기 - document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') closeSettingsModal(); - }); - // 전체 선택 (활성화된 체크박스만) function toggleSelectAll(side, checked) { const checkboxes = document.querySelectorAll(`input[name="${side}_menu"]:not(:disabled)`); diff --git a/resources/views/partials/sync-settings-modal.blade.php b/resources/views/partials/sync-settings-modal.blade.php new file mode 100644 index 00000000..8080d219 --- /dev/null +++ b/resources/views/partials/sync-settings-modal.blade.php @@ -0,0 +1,185 @@ + + + + \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 8d060e20..76fceb16 100644 --- a/routes/api.php +++ b/routes/api.php @@ -824,6 +824,7 @@ // 추가 액션 Route::post('/{id}/toggle', [CategoryApiController::class, 'toggle'])->name('toggle'); Route::post('/{id}/move', [CategoryApiController::class, 'move'])->name('move'); + Route::post('/{id}/promote-to-global', [CategoryApiController::class, 'promoteToGlobal'])->name('promoteToGlobal'); }); /* diff --git a/routes/web.php b/routes/web.php index ccee5b18..61a10d45 100644 --- a/routes/web.php +++ b/routes/web.php @@ -318,9 +318,11 @@ Route::get('/', [CommonCodeController::class, 'index'])->name('index'); Route::post('/', [CommonCodeController::class, 'store'])->name('store'); Route::post('/bulk-copy', [CommonCodeController::class, 'bulkCopy'])->name('bulk-copy'); + Route::post('/bulk-promote', [CommonCodeController::class, 'bulkPromoteToGlobal'])->name('bulk-promote'); Route::put('/{id}', [CommonCodeController::class, 'update'])->name('update'); Route::post('/{id}/toggle', [CommonCodeController::class, 'toggle'])->name('toggle'); Route::post('/{id}/copy', [CommonCodeController::class, 'copy'])->name('copy'); + Route::post('/{id}/promote', [CommonCodeController::class, 'promoteToGlobal'])->name('promote'); Route::delete('/{id}', [CommonCodeController::class, 'destroy'])->name('destroy'); // 공통코드 동기화||