feat:공통코드/카테고리 벌크 글로벌 복사, 동기화 환경설정 공통화
- 공통코드/카테고리 테넌트→글로벌 체크박스 벌크 복사 기능 추가 - 이미 대상에 존재하는 항목 체크박스 disabled 처리 (양방향) - 공통코드 토글 크기 카테고리와 동일하게 축소 - 동기화 환경설정 모달을 공통 partial로 분리 - 동기화 리스트에서 불필요한 타입 컬럼 제거 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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' => '글로벌 카테고리로 복사되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이동 (부모 변경)
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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', '테넌트 코드가 글로벌로 복사되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 삭제 (테넌트 코드만)
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)`);
|
||||
|
||||
185
resources/views/partials/sync-settings-modal.blade.php
Normal file
185
resources/views/partials/sync-settings-modal.blade.php
Normal 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>
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -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');
|
||||
|
||||
// 공통코드 동기화
|
||||
|
||||
Reference in New Issue
Block a user