feat:공통코드 글로벌→테넌트 체크박스 선택 및 일괄 복사 기능

This commit is contained in:
2026-01-26 20:52:44 +09:00
parent f0dbb25757
commit 831cdb8332
3 changed files with 181 additions and 4 deletions

View File

@@ -318,6 +318,100 @@ public function copy(Request $request, int $id): RedirectResponse|JsonResponse
->with('success', '글로벌 코드가 테넌트용으로 복사되었습니다.');
}
/**
* 글로벌 코드를 테넌트용으로 일괄 복사
*/
public function bulkCopy(Request $request): RedirectResponse|JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId) {
if ($request->ajax()) {
return response()->json(['error' => '테넌트를 먼저 선택해주세요.'], 400);
}
return redirect()->back()->with('error', '테넌트를 먼저 선택해주세요.');
}
// JSON 문자열로 받은 경우 처리
$idsJson = $request->input('ids_json');
if ($idsJson) {
$ids = json_decode($idsJson, true);
if (! is_array($ids) || empty($ids)) {
if ($request->ajax()) {
return response()->json(['error' => '복사할 코드를 선택해주세요.'], 400);
}
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) {
$globalCode = CommonCode::whereNull('tenant_id')->find($id);
if (! $globalCode) {
continue;
}
$codeGroup = $globalCode->code_group;
// 이미 복사된 코드가 있는지 확인
$exists = CommonCode::query()
->where('tenant_id', $tenantId)
->where('code_group', $globalCode->code_group)
->where('code', $globalCode->code)
->exists();
if ($exists) {
$skippedCount++;
continue;
}
// 복사
CommonCode::create([
'tenant_id' => $tenantId,
'code_group' => $globalCode->code_group,
'code' => $globalCode->code,
'name' => $globalCode->name,
'sort_order' => $globalCode->sort_order,
'attributes' => $globalCode->attributes,
'is_active' => true,
]);
$copiedCount++;
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
if ($request->ajax()) {
return response()->json(['error' => '복사 중 오류가 발생했습니다.'], 500);
}
return redirect()->back()->with('error', '복사 중 오류가 발생했습니다.');
}
$message = "{$copiedCount}개 코드가 복사되었습니다.";
if ($skippedCount > 0) {
$message .= " ({$skippedCount}개는 이미 존재하여 건너뜀)";
}
if ($request->ajax()) {
return response()->json(['success' => true, 'message' => $message, 'copied' => $copiedCount, 'skipped' => $skippedCount]);
}
return redirect()
->route('common-codes.index', ['group' => $codeGroup ?? 'item_type'])
->with('success', $message);
}
/**
* 코드 삭제 (테넌트 코드만)
*/

View File

@@ -92,14 +92,32 @@ class="px-4 py-3 text-sm font-medium border-b-2 whitespace-nowrap transition-col
<h3 class="font-semibold text-gray-800">글로벌 코드</h3>
<span class="text-xs text-gray-500">({{ $globalCodes->count() }})</span>
</div>
@if(!$isHQ)
<span class="text-xs text-gray-400">본사만 편집 가능</span>
@endif
<div class="flex items-center gap-2">
@if($globalCodes->count() > 0)
<button type="button"
id="bulkCopyBtn"
onclick="bulkCopy()"
class="hidden px-3 py-1.5 bg-green-600 hover:bg-green-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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span id="bulkCopyCount">0</span> 선택 복사
</button>
@endif
@if(!$isHQ)
<span class="text-xs text-gray-400">본사만 편집 가능</span>
@endif
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-center w-10">
<input type="checkbox" id="selectAllGlobal"
onchange="toggleSelectAll(this)"
class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-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-center text-xs font-medium text-gray-500 w-16">순서</th>
@@ -110,6 +128,11 @@ class="px-4 py-3 text-sm font-medium border-b-2 whitespace-nowrap transition-col
<tbody class="divide-y divide-gray-100">
@forelse($globalCodes as $code)
<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">
</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>
</td>
@@ -163,7 +186,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">
글로벌 코드가 없습니다.
</td>
</tr>
@@ -365,6 +388,12 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
<form id="copyForm" method="POST" class="hidden">
@csrf
</form>
<!-- 일괄 복사 (hidden) -->
<form id="bulkCopyForm" action="{{ route('common-codes.bulk-copy') }}" method="POST" class="hidden">
@csrf
<input type="hidden" name="ids_json" id="bulkCopyIds">
</form>
@endsection
@push('scripts')
@@ -426,6 +455,59 @@ function copyCode(id) {
form.submit();
}
// 전체 선택 토글
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.global-code-checkbox');
checkboxes.forEach(cb => cb.checked = checkbox.checked);
updateBulkCopyButton();
}
// 선택된 항목 수 업데이트 및 버튼 표시/숨김
function updateBulkCopyButton() {
const checkboxes = document.querySelectorAll('.global-code-checkbox:checked');
const count = checkboxes.length;
const btn = document.getElementById('bulkCopyBtn');
const countSpan = document.getElementById('bulkCopyCount');
if (btn) {
if (count > 0) {
btn.classList.remove('hidden');
btn.classList.add('flex');
countSpan.textContent = count;
} else {
btn.classList.add('hidden');
btn.classList.remove('flex');
}
}
// 전체 선택 체크박스 상태 업데이트
const allCheckboxes = document.querySelectorAll('.global-code-checkbox');
const selectAllCheckbox = document.getElementById('selectAllGlobal');
if (selectAllCheckbox) {
selectAllCheckbox.checked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
selectAllCheckbox.indeterminate = checkboxes.length > 0 && checkboxes.length < allCheckboxes.length;
}
}
// 일괄 복사
function bulkCopy() {
const checkboxes = document.querySelectorAll('.global-code-checkbox:checked');
const ids = Array.from(checkboxes).map(cb => cb.value);
if (ids.length === 0) {
alert('복사할 코드를 선택해주세요.');
return;
}
if (!confirm(`선택한 ${ids.length}개 글로벌 코드를 테넌트용으로 복사하시겠습니까?`)) return;
// hidden form으로 POST
const form = document.getElementById('bulkCopyForm');
const idsInput = document.getElementById('bulkCopyIds');
idsInput.value = JSON.stringify(ids);
form.submit();
}
// 코드 삭제
function deleteCode(id, code) {
if (!confirm(`'${code}' 코드를 삭제하시겠습니까?`)) return;

View File

@@ -301,6 +301,7 @@
Route::prefix('common-codes')->name('common-codes.')->group(function () {
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::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');