feat:공통코드/카테고리 벌크 글로벌 복사, 동기화 환경설정 공통화

- 공통코드/카테고리 테넌트→글로벌 체크박스 벌크 복사 기능 추가
- 이미 대상에 존재하는 항목 체크박스 disabled 처리 (양방향)
- 공통코드 토글 크기 카테고리와 동일하게 축소
- 동기화 환경설정 모달을 공통 partial로 분리
- 동기화 리스트에서 불필요한 타입 컬럼 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 13:16:44 +09:00
parent 0ea373e8e3
commit b6a3c4b506
11 changed files with 531 additions and 231 deletions

View File

@@ -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' => '글로벌 카테고리로 복사되었습니다.',
]);
}
/**
* 이동 (부모 변경)
*/

View File

@@ -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,
]);

View File

@@ -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', '테넌트 코드가 글로벌로 복사되었습니다.');
}
/**
* 코드 삭제 (테넌트 코드만)
*/

View File

@@ -172,11 +172,31 @@ class="relative inline-flex h-4 w-7 items-center rounded-full transition-colors
<h3 class="font-semibold text-gray-800">테넌트 카테고리</h3>
<span class="text-xs text-gray-500">({{ $tenantCategories->count() }})</span>
</div>
<div class="flex items-center gap-2">
@if($tenantCategories->count() > 0 && ($isHQ || auth()->user()?->isSuperAdmin()))
<button type="button"
id="bulkPromoteBtn"
onclick="bulkPromoteToGlobal()"
class="hidden px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs font-medium rounded-lg transition-colors flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span id="bulkPromoteCount">0</span> 글로벌로 복사
</button>
@endif
</div>
</div>
<div class="overflow-auto max-h-[600px] p-2">
@forelse($tenantCategories as $cat)
@php $existsInGlobal = in_array($cat->code, $globalCodes); @endphp
<div class="border rounded-lg mb-1 {{ !$cat->is_active ? 'opacity-50 bg-gray-50' : 'hover:bg-blue-50' }}" style="margin-left: {{ $cat->depth * 1.5 }}rem">
<div class="flex items-center gap-2 px-3 py-2">
@if($isHQ || auth()->user()?->isSuperAdmin())
<input type="checkbox" name="tenant_cat_ids[]" value="{{ $cat->id }}"
onchange="updateBulkPromoteBtn()"
class="tenant-cat-checkbox w-4 h-4 rounded focus:ring-purple-500 {{ $existsInGlobal ? 'bg-gray-200 border-gray-300 cursor-not-allowed' : 'text-purple-600 border-gray-300' }}"
{{ $existsInGlobal ? 'disabled' : '' }}>
@endif
<span class="font-mono text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">{{ $cat->code }}</span>
<span class="flex-1 text-sm {{ $cat->depth > 0 ? 'text-gray-600' : 'font-medium text-gray-800' }}">{{ $cat->name }}</span>
@if($cat->depth > 0)
@@ -459,6 +479,52 @@ function copyToTenant(globalId) {
.catch(() => showToast('복사 중 오류가 발생했습니다.', 'error'));
}
// 벌크 글로벌 복사 버튼 상태 업데이트
function updateBulkPromoteBtn() {
const checked = document.querySelectorAll('.tenant-cat-checkbox:checked');
const btn = document.getElementById('bulkPromoteBtn');
if (btn) {
if (checked.length > 0) {
btn.classList.remove('hidden');
btn.classList.add('flex');
document.getElementById('bulkPromoteCount').textContent = checked.length;
} else {
btn.classList.add('hidden');
btn.classList.remove('flex');
}
}
}
// 벌크 테넌트 → 글로벌 복사
function bulkPromoteToGlobal() {
const ids = Array.from(document.querySelectorAll('.tenant-cat-checkbox:checked')).map(cb => cb.value);
if (ids.length === 0) return;
if (!confirm(`선택한 ${ids.length}개 테넌트 카테고리를 글로벌로 복사하시겠습니까?`)) return;
let completed = 0, failed = 0, skipped = 0;
const promises = ids.map(id =>
fetch(`/api/admin/categories/${id}/promote-to-global`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' }
})
.then(r => r.json())
.then(result => {
if (result.success) completed++;
else skipped++;
})
.catch(() => failed++)
);
Promise.all(promises).then(() => {
let msg = `완료: ${completed}개 복사`;
if (skipped > 0) msg += `, ${skipped}개 건너뜀`;
if (failed > 0) msg += `, ${failed}개 실패`;
showToast(msg, failed > 0 ? 'error' : 'success');
location.reload();
});
}
// 전체 선택
function toggleSelectAll(checkbox) {
document.querySelectorAll('.global-checkbox').forEach(cb => cb.checked = checkbox.checked);

View File

@@ -18,14 +18,14 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
</svg>
카테고리 관리
</a>
<a href="{{ route('menus.sync.index') }}"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 text-sm">
<button type="button" onclick="openSettingsModal()"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
환경 설정
</a>
</button>
</div>
</div>
@@ -88,7 +88,7 @@ class="px-6 py-3 text-sm font-medium border-b-2 transition-colors
<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<a href="{{ route('menus.sync.index') }}" class="underline">메뉴 동기화 환경 설정</a>에서 {{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }} 서버 URL을 설정해주세요.
<button type="button" onclick="openSettingsModal()" class="underline">환경 설정</button>에서 {{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }} 서버 URL을 설정해주세요.
</div>
@endif
@@ -168,7 +168,6 @@ class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs font-mediu
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-3 py-2 text-center w-10"></th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">타입</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">그룹</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">코드</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">이름</th>
@@ -189,13 +188,6 @@ class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs font-mediu
class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
{{ $inBoth ? 'disabled' : '' }}>
</td>
<td class="px-3 py-2">
@if($cat['is_global'])
<span class="px-1.5 py-0.5 bg-purple-100 text-purple-700 text-xs rounded">글로벌</span>
@else
<span class="px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded">테넌트</span>
@endif
</td>
<td class="px-3 py-2 font-mono text-xs text-gray-600">{{ $cat['code_group'] }}</td>
<td class="px-3 py-2 font-mono text-xs">{{ $cat['code'] }}</td>
<td class="px-3 py-2">{{ $cat['name'] }}</td>
@@ -203,7 +195,7 @@ class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
</tr>
@empty
<tr>
<td colspan="6" class="px-3 py-8 text-center text-gray-400">카테고리가 없습니다.</td>
<td colspan="5" class="px-3 py-8 text-center text-gray-400">카테고리가 없습니다.</td>
</tr>
@endforelse
</tbody>
@@ -253,7 +245,6 @@ class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs font-med
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-3 py-2 text-center w-10"></th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">타입</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">그룹</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">코드</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">이름</th>
@@ -274,13 +265,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' : '' }}>
</td>
<td class="px-3 py-2">
@if($cat['is_global'])
<span class="px-1.5 py-0.5 bg-purple-100 text-purple-700 text-xs rounded">글로벌</span>
@else
<span class="px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded">테넌트</span>
@endif
</td>
<td class="px-3 py-2 font-mono text-xs text-gray-600">{{ $cat['code_group'] }}</td>
<td class="px-3 py-2 font-mono text-xs">{{ $cat['code'] }}</td>
<td class="px-3 py-2">{{ $cat['name'] }}</td>
@@ -294,6 +278,8 @@ class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
</div>
</div>
</div>
@include('partials.sync-settings-modal')
@endsection
@push('scripts')

View File

@@ -140,11 +140,13 @@ class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500">
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($globalCodes as $code)
@php $existsInTenant = in_array($code->code, $tenantCodeKeys); @endphp
<tr class="hover:bg-gray-50 {{ !$code->is_active ? 'opacity-50' : '' }}">
<td class="px-3 py-2 text-center">
<input type="checkbox" name="global_code_ids[]" value="{{ $code->id }}"
onchange="updateBulkCopyButton()"
class="global-code-checkbox w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500">
class="global-code-checkbox w-4 h-4 rounded focus:ring-green-500 {{ $existsInTenant ? 'bg-gray-200 border-gray-300 cursor-not-allowed' : 'text-green-600 border-gray-300' }}"
{{ $existsInTenant ? 'disabled' : '' }}>
</td>
<td class="px-3 py-2">
<span class="font-mono text-xs bg-gray-100 px-1.5 py-0.5 rounded">{{ $code->code }}</span>
@@ -155,8 +157,8 @@ class="global-code-checkbox w-4 h-4 text-green-600 border-gray-300 rounded focus
@if($isHQ)
<button type="button"
onclick="toggleActive({{ $code->id }})"
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors {{ $code->is_active ? 'bg-green-500' : 'bg-gray-300' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $code->is_active ? 'translate-x-4' : 'translate-x-0.5' }}"></span>
class="relative inline-flex h-4 w-7 items-center rounded-full transition-colors {{ $code->is_active ? 'bg-green-500' : 'bg-gray-300' }}">
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform {{ $code->is_active ? 'translate-x-3' : 'translate-x-0.5' }}"></span>
</button>
@else
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs {{ $code->is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500' }}">
@@ -221,11 +223,31 @@ class="p-1 text-gray-400 hover:text-red-600 transition-colors"
<h3 class="font-semibold text-gray-800">테넌트 코드</h3>
<span class="text-xs text-gray-500">({{ $tenantCodes->count() }})</span>
</div>
<div class="flex items-center gap-2">
@if($tenantCodes->count() > 0 && ($isHQ || auth()->user()?->isSuperAdmin()))
<button type="button"
id="bulkPromoteBtn"
onclick="bulkPromote()"
class="hidden px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs font-medium rounded-lg transition-colors flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span id="bulkPromoteCount">0</span> 글로벌로 복사
</button>
@endif
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
@if($isHQ || auth()->user()?->isSuperAdmin())
<th class="px-3 py-2 text-center w-10">
<input type="checkbox" id="selectAllTenant"
onchange="toggleSelectAllTenant(this)"
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500">
</th>
@endif
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">코드</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">이름</th>
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 w-16">순서</th>
@@ -235,7 +257,16 @@ class="p-1 text-gray-400 hover:text-red-600 transition-colors"
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($tenantCodes as $code)
@php $existsInGlobal = in_array($code->code, $globalCodeKeys); @endphp
<tr class="hover:bg-gray-50 {{ !$code->is_active ? 'opacity-50' : '' }}">
@if($isHQ || auth()->user()?->isSuperAdmin())
<td class="px-3 py-2 text-center">
<input type="checkbox" name="tenant_code_ids[]" value="{{ $code->id }}"
onchange="updateBulkPromoteButton()"
class="tenant-code-checkbox w-4 h-4 rounded focus:ring-purple-500 {{ $existsInGlobal ? 'bg-gray-200 border-gray-300 cursor-not-allowed' : 'text-purple-600 border-gray-300' }}"
{{ $existsInGlobal ? 'disabled' : '' }}>
</td>
@endif
<td class="px-3 py-2">
<span class="font-mono text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">{{ $code->code }}</span>
</td>
@@ -244,8 +275,8 @@ class="p-1 text-gray-400 hover:text-red-600 transition-colors"
<td class="px-3 py-2 text-center">
<button type="button"
onclick="toggleActive({{ $code->id }})"
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors {{ $code->is_active ? 'bg-green-500' : 'bg-gray-300' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $code->is_active ? 'translate-x-4' : 'translate-x-0.5' }}"></span>
class="relative inline-flex h-4 w-7 items-center rounded-full transition-colors {{ $code->is_active ? 'bg-green-500' : 'bg-gray-300' }}">
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform {{ $code->is_active ? 'translate-x-3' : 'translate-x-0.5' }}"></span>
</button>
</td>
<td class="px-3 py-2 text-center">
@@ -271,7 +302,7 @@ class="p-1 text-gray-400 hover:text-red-600 transition-colors"
</tr>
@empty
<tr>
<td colspan="5" class="px-3 py-8 text-center text-gray-400">
<td colspan="6" class="px-3 py-8 text-center text-gray-400">
테넌트 코드가 없습니다.<br>
<span class="text-xs">글로벌 코드를 복사하거나 새로 추가하세요.</span>
</td>
@@ -403,6 +434,12 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
@csrf
</form>
<!-- 일괄 글로벌 승격 (hidden) -->
<form id="bulkPromoteForm" action="{{ route('common-codes.bulk-promote') }}" method="POST" class="hidden">
@csrf
<input type="hidden" name="ids_json" id="bulkPromoteIds">
</form>
<!-- 일괄 복사 (hidden) -->
<form id="bulkCopyForm" action="{{ route('common-codes.bulk-copy') }}" method="POST" class="hidden">
@csrf
@@ -469,6 +506,32 @@ function copyCode(id) {
form.submit();
}
function toggleSelectAllTenant(checkbox) {
document.querySelectorAll('.tenant-code-checkbox').forEach(cb => cb.checked = checkbox.checked);
updateBulkPromoteButton();
}
function updateBulkPromoteButton() {
const checked = document.querySelectorAll('.tenant-code-checkbox:checked');
const btn = document.getElementById('bulkPromoteBtn');
const count = document.getElementById('bulkPromoteCount');
if (btn) {
btn.classList.toggle('hidden', checked.length === 0);
btn.classList.toggle('flex', checked.length > 0);
}
if (count) count.textContent = checked.length;
}
function bulkPromote() {
const ids = Array.from(document.querySelectorAll('.tenant-code-checkbox:checked')).map(cb => parseInt(cb.value));
if (ids.length === 0) return;
if (!confirm(`선택한 ${ids.length}개 테넌트 코드를 글로벌로 복사하시겠습니까?`)) return;
const form = document.getElementById('bulkPromoteForm');
document.getElementById('bulkPromoteIds').value = JSON.stringify(ids);
form.submit();
}
// 전체 선택 토글
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.global-code-checkbox');

View File

@@ -18,14 +18,14 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
</svg>
코드 관리
</a>
<a href="{{ route('menus.sync.index') }}"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 text-sm">
<button type="button" onclick="openSettingsModal()"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
환경 설정
</a>
</button>
</div>
</div>
@@ -88,7 +88,7 @@ class="px-6 py-3 text-sm font-medium border-b-2 transition-colors
<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<a href="{{ route('menus.sync.index') }}" class="underline">메뉴 동기화 환경 설정</a>에서 {{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }} 서버 URL을 설정해주세요.
<button type="button" onclick="openSettingsModal()" class="underline">환경 설정</button>에서 {{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }} 서버 URL을 설정해주세요.
</div>
@endif
@@ -168,7 +168,6 @@ class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs font-mediu
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-3 py-2 text-center w-10"></th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">타입</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">그룹</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">코드</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">이름</th>
@@ -194,20 +193,13 @@ class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs font-mediu
class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
{{ $inBoth ? 'disabled' : '' }}>
</td>
<td class="px-3 py-2">
@if($code['is_global'] ?? false)
<span class="px-1.5 py-0.5 bg-purple-100 text-purple-700 text-xs rounded">글로벌</span>
@else
<span class="px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded">테넌트</span>
@endif
</td>
<td class="px-3 py-2 font-mono text-xs text-gray-600">{{ $code['code_group'] }}</td>
<td class="px-3 py-2 font-mono text-xs">{{ $code['code'] }}</td>
<td class="px-3 py-2">{{ $code['name'] }}</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-3 py-8 text-center text-gray-400">코드가 없습니다.</td>
<td colspan="4" class="px-3 py-8 text-center text-gray-400">코드가 없습니다.</td>
</tr>
@endforelse
</tbody>
@@ -257,7 +249,6 @@ class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs font-med
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-3 py-2 text-center w-10"></th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">타입</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">그룹</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">코드</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500">이름</th>
@@ -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' : '' }}>
</td>
<td class="px-3 py-2">
@if($code['is_global'] ?? false)
<span class="px-1.5 py-0.5 bg-purple-100 text-purple-700 text-xs rounded">글로벌</span>
@else
<span class="px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded">테넌트</span>
@endif
</td>
<td class="px-3 py-2 font-mono text-xs text-gray-600">{{ $code['code_group'] }}</td>
<td class="px-3 py-2 font-mono text-xs">{{ $code['code'] }}</td>
<td class="px-3 py-2">{{ $code['name'] }}</td>
@@ -295,6 +279,8 @@ class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
</div>
</div>
</div>
@include('partials.sync-settings-modal')
@endsection
@push('scripts')

View File

@@ -193,96 +193,7 @@ class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs font-med
</div>
</div>
<!-- 환경 설정 모달 -->
<div id="settingsModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-800">환경 설정</h3>
<button type="button" onclick="closeSettingsModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="px-6 py-4 space-y-6">
<!-- 개발 환경 -->
<div class="space-y-3">
<h4 class="font-medium text-gray-700 flex items-center gap-2">
<span class="w-2 h-2 bg-yellow-500 rounded-full"></span>
개발 환경
</h4>
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">URL</label>
<input type="url" id="devUrl" value="{{ $environments['dev']['url'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="https://dev-mng.example.com">
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">API Key</label>
<input type="password" id="devApiKey" value="{{ $environments['dev']['api_key'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="API Key">
</div>
</div>
<button type="button" onclick="testConnection('dev')"
class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
연결 테스트
</button>
</div>
<!-- 운영 환경 -->
<div class="space-y-3">
<h4 class="font-medium text-gray-700 flex items-center gap-2">
<span class="w-2 h-2 bg-red-500 rounded-full"></span>
운영 환경
</h4>
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">URL</label>
<input type="url" id="prodUrl" value="{{ $environments['prod']['url'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="https://mng.example.com">
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">API Key</label>
<input type="password" id="prodApiKey" value="{{ $environments['prod']['api_key'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="API Key">
</div>
</div>
<button type="button" onclick="testConnection('prod')"
class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
연결 테스트
</button>
</div>
<div class="bg-gray-50 rounded-lg p-3 text-xs text-gray-600">
<p class="font-medium mb-1">API Key 설정 방법</p>
<p> 환경의 <code class="bg-gray-200 px-1 rounded">.env</code> 파일에 다음을 추가하세요:</p>
<code class="block bg-gray-200 px-2 py-1 rounded mt-1">MENU_SYNC_API_KEY=your-secret-key</code>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
<button type="button" onclick="closeSettingsModal()"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium transition-colors">
취소
</button>
<button type="button" onclick="saveSettings()"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors">
저장
</button>
</div>
</div>
</div>
@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)`);

View File

@@ -0,0 +1,185 @@
<!-- 환경 설정 모달 (공통) -->
<div id="settingsModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-800">환경 설정</h3>
<button type="button" onclick="closeSettingsModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="px-6 py-4 space-y-6">
<!-- 개발 환경 -->
<div class="space-y-3">
<h4 class="font-medium text-gray-700 flex items-center gap-2">
<span class="w-2 h-2 bg-yellow-500 rounded-full"></span>
개발 환경
</h4>
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">URL</label>
<input type="url" id="devUrl" value="{{ $environments['dev']['url'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="https://dev-mng.example.com">
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">API Key</label>
<input type="password" id="devApiKey" value="{{ $environments['dev']['api_key'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="API Key">
</div>
</div>
<button type="button" onclick="testConnection('dev')"
class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
연결 테스트
</button>
</div>
<!-- 운영 환경 -->
<div class="space-y-3">
<h4 class="font-medium text-gray-700 flex items-center gap-2">
<span class="w-2 h-2 bg-red-500 rounded-full"></span>
운영 환경
</h4>
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">URL</label>
<input type="url" id="prodUrl" value="{{ $environments['prod']['url'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="https://mng.example.com">
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">API Key</label>
<input type="password" id="prodApiKey" value="{{ $environments['prod']['api_key'] ?? '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="API Key">
</div>
</div>
<button type="button" onclick="testConnection('prod')"
class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
연결 테스트
</button>
</div>
<div class="bg-gray-50 rounded-lg p-3 text-xs text-gray-600">
<p class="font-medium mb-1">API Key 설정 방법</p>
<p> 환경의 <code class="bg-gray-200 px-1 rounded">.env</code> 파일에 다음을 추가하세요:</p>
<code class="block bg-gray-200 px-2 py-1 rounded mt-1">MENU_SYNC_API_KEY=your-secret-key</code>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
<button type="button" onclick="closeSettingsModal()"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium transition-colors">
취소
</button>
<button type="button" onclick="saveSettings()"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors">
저장
</button>
</div>
</div>
</div>
<script>
// 설정 모달
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': '{{ csrf_token() }}',
'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': '{{ csrf_token() }}',
'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);
}
}
// 모달 외부 클릭 시 닫기
document.getElementById('settingsModal').addEventListener('click', function(e) {
if (e.target === this) closeSettingsModal();
});
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeSettingsModal();
});
</script>

View File

@@ -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');
});
/*

View File

@@ -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');
// 공통코드 동기화