feat:코드그룹 DB기반 관리, 스코프 필터, 동기화 테넌트명 표시
- 공통코드/카테고리: 하드코딩 그룹 라벨 제거, DB description 기반으로 전환 - 코드그룹 신규 생성 기능 추가 (사이드바 + 모달, TenantSetting 저장) - 글로벌/테넌트 스코프 분류 및 필터 버튼 (전체/글로벌/테넌트) - 사이드바 컴팩트 레이아웃 (100+ 그룹 대응) - 동기화 페이지 3종(메뉴/공통코드/카테고리) 테넌트 회사명 표시 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,87 @@
|
||||
use App\Models\Category;
|
||||
use App\Models\GlobalCategory;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantSetting;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
/**
|
||||
* 커스텀 그룹 라벨 조회
|
||||
*/
|
||||
private function getCustomGroupLabels(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
if (! $tenantId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$setting = TenantSetting::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('setting_group', 'category')
|
||||
->where('setting_key', 'custom_group_labels')
|
||||
->first();
|
||||
|
||||
return $setting?->setting_value ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 그룹 추가
|
||||
*/
|
||||
public function storeGroup(Request $request): RedirectResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
if (! $tenantId) {
|
||||
return redirect()->back()->with('error', '테넌트를 먼저 선택해주세요.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'group_code' => ['required', 'string', 'max:50', 'regex:/^[a-z][a-z0-9_]*$/'],
|
||||
'group_label' => 'required|string|max:50',
|
||||
], [
|
||||
'group_code.regex' => '그룹 코드는 영문 소문자, 숫자, 언더스코어만 사용 가능합니다.',
|
||||
]);
|
||||
|
||||
$groupCode = $validated['group_code'];
|
||||
$groupLabel = $validated['group_label'];
|
||||
|
||||
// DB에 이미 존재하는 그룹인지 체크
|
||||
$existsInGlobal = GlobalCategory::whereNull('deleted_at')
|
||||
->where('code_group', $groupCode)
|
||||
->exists();
|
||||
$existsInTenant = Category::where('code_group', $groupCode)->exists();
|
||||
|
||||
if ($existsInGlobal || $existsInTenant) {
|
||||
return redirect()->back()->with('error', '이미 존재하는 그룹 코드입니다.');
|
||||
}
|
||||
|
||||
// 커스텀 라벨 중복 체크
|
||||
$customLabels = $this->getCustomGroupLabels();
|
||||
if (isset($customLabels[$groupCode])) {
|
||||
return redirect()->back()->with('error', '이미 존재하는 커스텀 그룹 코드입니다.');
|
||||
}
|
||||
|
||||
$customLabels[$groupCode] = $groupLabel;
|
||||
|
||||
TenantSetting::withoutGlobalScopes()->updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'setting_group' => 'category',
|
||||
'setting_key' => 'custom_group_labels',
|
||||
],
|
||||
[
|
||||
'setting_value' => $customLabels,
|
||||
'description' => '카테고리 커스텀 그룹 라벨',
|
||||
]
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('categories.index', ['group' => $groupCode])
|
||||
->with('success', "'{$groupLabel}' 그룹이 추가되었습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 관리 페이지
|
||||
@@ -26,35 +101,64 @@ public function index(Request $request): View|Response
|
||||
$tenant = $tenantId ? Tenant::find($tenantId) : null;
|
||||
$isHQ = $tenantId == 1;
|
||||
|
||||
// 모든 code_group 조회 (글로벌 + 테넌트)
|
||||
$globalGroups = GlobalCategory::whereNull('deleted_at')
|
||||
->select('code_group')
|
||||
->distinct()
|
||||
->pluck('code_group')
|
||||
// 글로벌 그룹 + description
|
||||
$globalGroupDescs = GlobalCategory::whereNull('deleted_at')
|
||||
->selectRaw('code_group, MIN(description) as description')
|
||||
->groupBy('code_group')
|
||||
->pluck('description', 'code_group')
|
||||
->toArray();
|
||||
|
||||
$tenantGroups = [];
|
||||
// 테넌트 그룹 + description
|
||||
$tenantGroupDescs = [];
|
||||
if ($tenantId) {
|
||||
$tenantGroups = Category::where('tenant_id', $tenantId)
|
||||
$tenantGroupDescs = Category::where('tenant_id', $tenantId)
|
||||
->whereNull('deleted_at')
|
||||
->select('code_group')
|
||||
->distinct()
|
||||
->pluck('code_group')
|
||||
->selectRaw('code_group, MIN(description) as description')
|
||||
->groupBy('code_group')
|
||||
->pluck('description', 'code_group')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$allGroups = array_unique(array_merge($globalGroups, $tenantGroups));
|
||||
sort($allGroups);
|
||||
// 커스텀 그룹 라벨 (빈 그룹용 — TenantSetting)
|
||||
$customLabels = $this->getCustomGroupLabels();
|
||||
|
||||
$codeGroupLabels = config('categories.code_group_labels', []);
|
||||
// config 라벨 (하위호환 폴백)
|
||||
$configLabels = config('categories.code_group_labels', []);
|
||||
|
||||
// 그룹별 스코프 분류
|
||||
$allGroupKeys = array_unique(array_merge(
|
||||
array_keys($globalGroupDescs),
|
||||
array_keys($tenantGroupDescs),
|
||||
array_keys($customLabels)
|
||||
));
|
||||
sort($allGroupKeys);
|
||||
|
||||
$codeGroups = [];
|
||||
foreach ($allGroups as $group) {
|
||||
$codeGroups[$group] = $codeGroupLabels[$group] ?? $group;
|
||||
$groupScopes = [];
|
||||
foreach ($allGroupKeys as $group) {
|
||||
$inGlobal = isset($globalGroupDescs[$group]);
|
||||
$inTenant = isset($tenantGroupDescs[$group]);
|
||||
|
||||
$codeGroups[$group] = ! empty($globalGroupDescs[$group])
|
||||
? $globalGroupDescs[$group]
|
||||
: (! empty($tenantGroupDescs[$group])
|
||||
? $tenantGroupDescs[$group]
|
||||
: ($customLabels[$group] ?? $configLabels[$group] ?? $group));
|
||||
|
||||
if ($inGlobal && $inTenant) {
|
||||
$groupScopes[$group] = 'both';
|
||||
} elseif ($inGlobal) {
|
||||
$groupScopes[$group] = 'global';
|
||||
} elseif ($inTenant) {
|
||||
$groupScopes[$group] = 'tenant';
|
||||
} else {
|
||||
$groupScopes[$group] = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($codeGroups)) {
|
||||
$codeGroups['product'] = $codeGroupLabels['product'] ?? 'product';
|
||||
$codeGroups['product'] = $configLabels['product'] ?? 'product';
|
||||
$groupScopes['product'] = 'custom';
|
||||
}
|
||||
|
||||
$selectedGroup = $request->get('group', array_key_first($codeGroups));
|
||||
@@ -86,7 +190,8 @@ public function index(Request $request): View|Response
|
||||
|
||||
return view('categories.index', [
|
||||
'codeGroups' => $codeGroups,
|
||||
'codeGroupLabels' => $codeGroupLabels,
|
||||
'groupScopes' => $groupScopes,
|
||||
'codeGroupLabels' => $codeGroups,
|
||||
'selectedGroup' => $selectedGroup,
|
||||
'globalCategories' => $globalCategories,
|
||||
'tenantCategories' => $tenantCategories,
|
||||
@@ -123,4 +228,4 @@ private function addChildrenRecursive(&$result, $byParent, $parentId, $depth): v
|
||||
$this->addChildrenRecursive($result, $byParent, $child->id, $depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\GlobalCategory;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantSetting;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -13,6 +14,8 @@
|
||||
|
||||
class CategorySyncController extends Controller
|
||||
{
|
||||
private ?string $remoteTenantName = null;
|
||||
|
||||
/**
|
||||
* 현재 선택된 테넌트 ID
|
||||
*/
|
||||
@@ -52,12 +55,16 @@ public function index(Request $request): View|Response
|
||||
$selectedEnv = $request->get('env', 'dev');
|
||||
$selectedType = $request->get('type', 'global'); // global or tenant
|
||||
|
||||
// 로컬 테넌트 정보
|
||||
$localTenant = Tenant::find($this->getTenantId());
|
||||
|
||||
// 로컬 카테고리 조회 (타입 필터 적용)
|
||||
$localCategories = $this->getCategoryList($selectedType);
|
||||
|
||||
// 원격 카테고리 조회
|
||||
$remoteCategories = [];
|
||||
$remoteError = null;
|
||||
$this->remoteTenantName = null;
|
||||
|
||||
if (! empty($environments[$selectedEnv]['url'])) {
|
||||
try {
|
||||
@@ -78,6 +85,8 @@ public function index(Request $request): View|Response
|
||||
'remoteCategories' => $remoteCategories,
|
||||
'remoteError' => $remoteError,
|
||||
'diff' => $diff,
|
||||
'localTenantName' => $localTenant?->company_name ?? '알 수 없음',
|
||||
'remoteTenantName' => $this->remoteTenantName,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,9 +106,12 @@ public function export(Request $request): JsonResponse
|
||||
$type = $request->get('type', 'all'); // global, tenant, or all
|
||||
$categories = $this->getCategoryList($type);
|
||||
|
||||
$tenant = Tenant::find($this->getTenantId());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'environment' => config('app.env'),
|
||||
'tenant_name' => $tenant?->company_name ?? '알 수 없음',
|
||||
'exported_at' => now()->toIso8601String(),
|
||||
'categories' => $categories,
|
||||
]);
|
||||
@@ -473,6 +485,8 @@ private function fetchRemoteCategories(array $env, string $type = 'all'): array
|
||||
throw new \Exception('잘못된 응답 형식');
|
||||
}
|
||||
|
||||
$this->remoteTenantName = $data['tenant_name'] ?? null;
|
||||
|
||||
return $data['categories'];
|
||||
}
|
||||
|
||||
@@ -499,4 +513,4 @@ private function calculateDiff(array $localCategories, array $remoteCategories):
|
||||
'both' => array_values(array_intersect($localKeys, $remoteKeys)),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Products\CommonCode;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantSetting;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -14,29 +15,6 @@
|
||||
|
||||
class CommonCodeController extends Controller
|
||||
{
|
||||
/**
|
||||
* 코드 그룹 라벨
|
||||
*/
|
||||
private const CODE_GROUP_LABELS = [
|
||||
'item_type' => '품목유형',
|
||||
'material_type' => '자재유형',
|
||||
'client_type' => '거래처유형',
|
||||
'order_status' => '주문상태',
|
||||
'order_type' => '주문유형',
|
||||
'delivery_method' => '배송방법',
|
||||
'tenant_type' => '테넌트유형',
|
||||
'product_category' => '제품분류',
|
||||
'motor_type' => '모터유형',
|
||||
'controller_type' => '컨트롤러유형',
|
||||
'painting_type' => '도장유형',
|
||||
'position_type' => '위치유형',
|
||||
'capability_profile' => '생산능력',
|
||||
'bad_debt_progress' => '대손진행',
|
||||
'height_construction_cost' => '높이시공비',
|
||||
'width_construction_cost' => '폭시공비',
|
||||
'document_type' => '문서분류',
|
||||
];
|
||||
|
||||
/**
|
||||
* 공통코드 관리 페이지
|
||||
*/
|
||||
@@ -54,16 +32,65 @@ public function index(Request $request): View|Response
|
||||
// 선택된 코드 그룹 (기본: item_type)
|
||||
$selectedGroup = $request->get('group', 'item_type');
|
||||
|
||||
// 코드 그룹 목록 (실제 존재하는 그룹만)
|
||||
$existingGroups = CommonCode::query()
|
||||
->select('code_group')
|
||||
->distinct()
|
||||
->pluck('code_group')
|
||||
// 글로벌 그룹 (tenant_id IS NULL)
|
||||
$globalGroupDescs = CommonCode::query()
|
||||
->whereNull('tenant_id')
|
||||
->selectRaw('code_group, MIN(description) as description')
|
||||
->groupBy('code_group')
|
||||
->pluck('description', 'code_group')
|
||||
->toArray();
|
||||
|
||||
$codeGroups = collect(self::CODE_GROUP_LABELS)
|
||||
->filter(fn($label, $group) => in_array($group, $existingGroups))
|
||||
->toArray();
|
||||
// 테넌트 그룹
|
||||
$tenantGroupDescs = [];
|
||||
if ($tenantId) {
|
||||
$tenantGroupDescs = CommonCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->selectRaw('code_group, MIN(description) as description')
|
||||
->groupBy('code_group')
|
||||
->pluck('description', 'code_group')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
// 커스텀 그룹 라벨 (빈 그룹용 — TenantSetting)
|
||||
$customLabels = $this->getCustomGroupLabels();
|
||||
|
||||
// 그룹별 스코프 분류
|
||||
$allGroupKeys = array_unique(array_merge(
|
||||
array_keys($globalGroupDescs),
|
||||
array_keys($tenantGroupDescs),
|
||||
array_keys($customLabels)
|
||||
));
|
||||
sort($allGroupKeys);
|
||||
|
||||
$codeGroups = [];
|
||||
$groupScopes = [];
|
||||
foreach ($allGroupKeys as $group) {
|
||||
$inGlobal = isset($globalGroupDescs[$group]);
|
||||
$inTenant = isset($tenantGroupDescs[$group]);
|
||||
|
||||
// 라벨: 글로벌 description 우선 → 테넌트 → 커스텀 → 키 자체
|
||||
$codeGroups[$group] = ! empty($globalGroupDescs[$group])
|
||||
? $globalGroupDescs[$group]
|
||||
: (! empty($tenantGroupDescs[$group])
|
||||
? $tenantGroupDescs[$group]
|
||||
: ($customLabels[$group] ?? $group));
|
||||
|
||||
// 스코프: global, both, tenant, custom
|
||||
if ($inGlobal && $inTenant) {
|
||||
$groupScopes[$group] = 'both';
|
||||
} elseif ($inGlobal) {
|
||||
$groupScopes[$group] = 'global';
|
||||
} elseif ($inTenant) {
|
||||
$groupScopes[$group] = 'tenant';
|
||||
} else {
|
||||
$groupScopes[$group] = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 그룹이 목록에 없으면 첫 번째 그룹으로 대체
|
||||
if (! isset($codeGroups[$selectedGroup]) && ! empty($codeGroups)) {
|
||||
$selectedGroup = array_key_first($codeGroups);
|
||||
}
|
||||
|
||||
// 선택된 그룹의 코드 목록
|
||||
$globalCodes = collect();
|
||||
@@ -92,6 +119,7 @@ public function index(Request $request): View|Response
|
||||
'tenant' => $tenant,
|
||||
'isHQ' => $isHQ,
|
||||
'codeGroups' => $codeGroups,
|
||||
'groupScopes' => $groupScopes,
|
||||
'selectedGroup' => $selectedGroup,
|
||||
'globalCodes' => $globalCodes,
|
||||
'tenantCodes' => $tenantCodes,
|
||||
@@ -100,6 +128,80 @@ public function index(Request $request): View|Response
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 커스텀 그룹 라벨 조회
|
||||
*/
|
||||
private function getCustomGroupLabels(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
if (! $tenantId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$setting = TenantSetting::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('setting_group', 'common_code')
|
||||
->where('setting_key', 'custom_group_labels')
|
||||
->first();
|
||||
|
||||
return $setting?->setting_value ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 그룹 추가
|
||||
*/
|
||||
public function storeGroup(Request $request): RedirectResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
if (! $tenantId) {
|
||||
return redirect()->back()->with('error', '테넌트를 먼저 선택해주세요.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'group_code' => ['required', 'string', 'max:50', 'regex:/^[a-z][a-z0-9_]*$/'],
|
||||
'group_label' => 'required|string|max:50',
|
||||
], [
|
||||
'group_code.regex' => '그룹 코드는 영문 소문자, 숫자, 언더스코어만 사용 가능합니다.',
|
||||
]);
|
||||
|
||||
$groupCode = $validated['group_code'];
|
||||
$groupLabel = $validated['group_label'];
|
||||
|
||||
// DB에 이미 존재하는 그룹인지 체크
|
||||
$existsInDb = CommonCode::query()
|
||||
->where('code_group', $groupCode)
|
||||
->exists();
|
||||
|
||||
if ($existsInDb) {
|
||||
return redirect()->back()->with('error', '이미 존재하는 그룹 코드입니다.');
|
||||
}
|
||||
|
||||
// 커스텀 라벨 조회 후 중복 체크
|
||||
$customLabels = $this->getCustomGroupLabels();
|
||||
if (isset($customLabels[$groupCode])) {
|
||||
return redirect()->back()->with('error', '이미 존재하는 커스텀 그룹 코드입니다.');
|
||||
}
|
||||
|
||||
// 커스텀 라벨에 추가
|
||||
$customLabels[$groupCode] = $groupLabel;
|
||||
|
||||
TenantSetting::withoutGlobalScopes()->updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'setting_group' => 'common_code',
|
||||
'setting_key' => 'custom_group_labels',
|
||||
],
|
||||
[
|
||||
'setting_value' => $customLabels,
|
||||
'description' => '공통코드 커스텀 그룹 라벨',
|
||||
]
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('common-codes.index', ['group' => $groupCode])
|
||||
->with('success', "'{$groupLabel}' 그룹이 추가되었습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 저장 (신규/수정)
|
||||
*/
|
||||
@@ -623,4 +725,4 @@ public function destroy(Request $request, int $id): RedirectResponse|JsonRespons
|
||||
->route('common-codes.index', ['group' => $codeGroup])
|
||||
->with('success', '코드가 삭제되었습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Products\CommonCode;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantSetting;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -12,6 +13,8 @@
|
||||
|
||||
class CommonCodeSyncController extends Controller
|
||||
{
|
||||
private ?string $remoteTenantName = null;
|
||||
|
||||
/**
|
||||
* 현재 선택된 테넌트 ID
|
||||
*/
|
||||
@@ -51,12 +54,16 @@ public function index(Request $request): View|Response
|
||||
$selectedEnv = $request->get('env', 'dev');
|
||||
$selectedType = $request->get('type', 'global'); // global or tenant
|
||||
|
||||
// 로컬 테넌트 정보
|
||||
$localTenant = Tenant::find($this->getTenantId());
|
||||
|
||||
// 로컬 코드 조회 (타입 필터 적용)
|
||||
$localCodes = $this->getCodeList($selectedType);
|
||||
|
||||
// 원격 코드 조회
|
||||
$remoteCodes = [];
|
||||
$remoteError = null;
|
||||
$this->remoteTenantName = null;
|
||||
|
||||
if (! empty($environments[$selectedEnv]['url'])) {
|
||||
try {
|
||||
@@ -77,6 +84,8 @@ public function index(Request $request): View|Response
|
||||
'remoteCodes' => $remoteCodes,
|
||||
'remoteError' => $remoteError,
|
||||
'diff' => $diff,
|
||||
'localTenantName' => $localTenant?->company_name ?? '알 수 없음',
|
||||
'remoteTenantName' => $this->remoteTenantName,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -96,9 +105,12 @@ public function export(Request $request): JsonResponse
|
||||
$type = $request->get('type', 'all'); // global, tenant, or all
|
||||
$codes = $this->getCodeList($type);
|
||||
|
||||
$tenant = Tenant::find($this->getTenantId());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'environment' => config('app.env'),
|
||||
'tenant_name' => $tenant?->company_name ?? '알 수 없음',
|
||||
'exported_at' => now()->toIso8601String(),
|
||||
'codes' => $codes,
|
||||
]);
|
||||
@@ -370,6 +382,8 @@ private function fetchRemoteCodes(array $env, string $type = 'all'): array
|
||||
throw new \Exception('잘못된 응답 형식');
|
||||
}
|
||||
|
||||
$this->remoteTenantName = $data['tenant_name'] ?? null;
|
||||
|
||||
return $data['codes'];
|
||||
}
|
||||
|
||||
@@ -396,4 +410,4 @@ private function calculateDiff(array $localCodes, array $remoteCodes): array
|
||||
'both' => array_values(array_intersect($localKeys, $remoteKeys)),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Commons\Menu;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantSetting;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -12,6 +13,8 @@
|
||||
|
||||
class MenuSyncController extends Controller
|
||||
{
|
||||
private ?string $remoteTenantName = null;
|
||||
|
||||
/**
|
||||
* 현재 선택된 테넌트 ID
|
||||
*/
|
||||
@@ -50,12 +53,16 @@ public function index(Request $request): View|Response
|
||||
$environments = $this->getEnvironments();
|
||||
$selectedEnv = $request->get('env', 'dev');
|
||||
|
||||
// 로컬 테넌트 정보
|
||||
$localTenant = Tenant::find($this->getTenantId());
|
||||
|
||||
// 로컬 메뉴 조회 (트리 구조)
|
||||
$localMenus = $this->getMenuTree();
|
||||
|
||||
// 원격 메뉴 조회
|
||||
$remoteMenus = [];
|
||||
$remoteError = null;
|
||||
$this->remoteTenantName = null;
|
||||
|
||||
if (! empty($environments[$selectedEnv]['url'])) {
|
||||
try {
|
||||
@@ -75,6 +82,8 @@ public function index(Request $request): View|Response
|
||||
'remoteMenus' => $remoteMenus,
|
||||
'remoteError' => $remoteError,
|
||||
'diff' => $diff,
|
||||
'localTenantName' => $localTenant?->company_name ?? '알 수 없음',
|
||||
'remoteTenantName' => $this->remoteTenantName,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -119,10 +128,12 @@ public function export(Request $request): JsonResponse
|
||||
}
|
||||
|
||||
$menus = $this->getMenuTree();
|
||||
$tenant = Tenant::find($this->getTenantId());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'environment' => config('app.env'),
|
||||
'tenant_name' => $tenant?->company_name ?? '알 수 없음',
|
||||
'exported_at' => now()->toIso8601String(),
|
||||
'menus' => $menus,
|
||||
]);
|
||||
@@ -383,6 +394,8 @@ private function fetchRemoteMenus(array $env): array
|
||||
throw new \Exception('잘못된 응답 형식');
|
||||
}
|
||||
|
||||
$this->remoteTenantName = $data['tenant_name'] ?? null;
|
||||
|
||||
return $data['menus'];
|
||||
}
|
||||
|
||||
|
||||
@@ -55,19 +55,48 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
|
||||
<!-- 메인 레이아웃: 좌측 탭 + 우측 콘텐츠 -->
|
||||
<div class="flex gap-4 flex-1 min-h-0">
|
||||
<!-- 좌측: 코드 그룹 탭 (세로) -->
|
||||
<div class="w-40 flex-shrink-0 bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">코드 그룹</span>
|
||||
<div class="w-44 flex-shrink-0 bg-white rounded-lg shadow-sm overflow-hidden flex flex-col">
|
||||
<div class="px-2 py-1.5 bg-gray-50 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
|
||||
<span class="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">코드 그룹</span>
|
||||
<button type="button" onclick="openGroupModal()"
|
||||
class="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="그룹 추가">
|
||||
<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="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="overflow-y-auto max-h-[calc(100vh-220px)]" aria-label="Tabs">
|
||||
<!-- 스코프 필터 -->
|
||||
<div class="px-1.5 py-1 border-b border-gray-200 flex flex-wrap gap-0.5 flex-shrink-0">
|
||||
<button type="button" onclick="filterGroups('all')"
|
||||
class="group-filter-btn active text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-700 text-white" data-scope="all">전체</button>
|
||||
<button type="button" onclick="filterGroups('global-based')"
|
||||
class="group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-100 text-gray-600 hover:bg-gray-200" data-scope="global-based">글로벌</button>
|
||||
<button type="button" onclick="filterGroups('tenant-based')"
|
||||
class="group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-100 text-gray-600 hover:bg-gray-200" data-scope="tenant-based">테넌트</button>
|
||||
</div>
|
||||
<nav class="overflow-y-auto flex-1 min-h-0" aria-label="Tabs">
|
||||
@foreach($codeGroups as $group => $label)
|
||||
@php $scope = $groupScopes[$group] ?? 'custom'; @endphp
|
||||
<a href="{{ route('categories.index', ['group' => $group]) }}"
|
||||
class="block px-3 py-2.5 border-l-4 transition-colors
|
||||
data-scope="{{ $scope }}"
|
||||
class="group-item block px-2 py-1.5 border-l-3 transition-colors
|
||||
{{ $selectedGroup === $group
|
||||
? 'border-l-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-l-transparent text-gray-600 hover:bg-gray-50 hover:text-gray-900' }}">
|
||||
<span class="block text-sm font-medium truncate">{{ $label }}</span>
|
||||
<span class="block text-xs font-mono {{ $selectedGroup === $group ? 'text-blue-400' : 'text-gray-400' }}">{{ $group }}</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="block text-xs font-medium truncate flex-1">{{ $label }}</span>
|
||||
@if($scope === 'global')
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 flex-shrink-0" title="글로벌"></span>
|
||||
@elseif($scope === 'both')
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-400 flex-shrink-0" title="글로벌+테넌트"></span>
|
||||
@elseif($scope === 'tenant')
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-400 flex-shrink-0" title="테넌트"></span>
|
||||
@else
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-gray-300 flex-shrink-0" title="빈 그룹"></span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="block text-[10px] font-mono {{ $selectedGroup === $group ? 'text-blue-400' : 'text-gray-400' }} truncate">{{ $group }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
@@ -295,6 +324,46 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 코드 그룹 추가 모달 -->
|
||||
<div id="groupModal" 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-sm mx-4">
|
||||
<form action="{{ route('categories.store-group') }}" method="POST">
|
||||
@csrf
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800">코드 그룹 추가</h3>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">그룹 코드 *</label>
|
||||
<input type="text" name="group_code" required
|
||||
pattern="[a-z][a-z0-9_]*" maxlength="50"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="예: shipping_cost">
|
||||
<p class="mt-1 text-xs text-gray-400">영문 소문자, 숫자, 언더스코어만 사용</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">그룹명 *</label>
|
||||
<input type="text" name="group_label" required maxlength="50"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="예: 배송비 유형">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
||||
<button type="button" onclick="closeGroupModal()"
|
||||
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="submit"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@@ -302,6 +371,46 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
|
||||
const selectedGroup = '{{ $selectedGroup }}';
|
||||
const csrfToken = '{{ csrf_token() }}';
|
||||
|
||||
// 그룹 스코프 필터
|
||||
function filterGroups(filter) {
|
||||
const items = document.querySelectorAll('.group-item');
|
||||
items.forEach(item => {
|
||||
const scope = item.dataset.scope;
|
||||
let show = false;
|
||||
switch (filter) {
|
||||
case 'all':
|
||||
show = true;
|
||||
break;
|
||||
case 'global-based':
|
||||
show = (scope === 'global' || scope === 'both');
|
||||
break;
|
||||
case 'tenant-based':
|
||||
show = (scope === 'tenant' || scope === 'both');
|
||||
break;
|
||||
}
|
||||
item.style.display = show ? '' : 'none';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.group-filter-btn').forEach(btn => {
|
||||
if (btn.dataset.scope === filter) {
|
||||
btn.className = 'group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-700 text-white';
|
||||
} else {
|
||||
btn.className = 'group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-100 text-gray-600 hover:bg-gray-200';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 그룹 모달
|
||||
function openGroupModal() {
|
||||
document.getElementById('groupModal').classList.remove('hidden');
|
||||
document.getElementById('groupModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeGroupModal() {
|
||||
document.getElementById('groupModal').classList.add('hidden');
|
||||
document.getElementById('groupModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
// 모달 열기 (추가)
|
||||
function openAddModal() {
|
||||
document.getElementById('modalTitle').textContent = '카테고리 추가';
|
||||
@@ -642,10 +751,16 @@ function deleteGlobalCategory(id, name) {
|
||||
document.getElementById('categoryModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
document.getElementById('groupModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeGroupModal();
|
||||
});
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
closeGroupModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endpush
|
||||
|
||||
@@ -150,7 +150,8 @@ class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3 class="font-semibold text-gray-800">로컬 (현재)</h3>
|
||||
<h3 class="font-semibold text-gray-800">로컬</h3>
|
||||
<span class="text-xs text-gray-500">- {{ $localTenantName }}</span>
|
||||
<span class="text-xs text-gray-500">({{ count($localCategories) }}개)</span>
|
||||
</div>
|
||||
@if(!empty($environments[$selectedEnv]['url']) && !$remoteError)
|
||||
@@ -215,6 +216,9 @@ class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500">
|
||||
</svg>
|
||||
</span>
|
||||
<h3 class="font-semibold text-gray-800">{{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }}</h3>
|
||||
@if($remoteTenantName)
|
||||
<span class="text-xs text-gray-500">- {{ $remoteTenantName }}</span>
|
||||
@endif
|
||||
<span class="text-xs text-gray-500">({{ count($remoteCategories) }}개)</span>
|
||||
</div>
|
||||
@if(!empty($environments[$selectedEnv]['url']) && !$remoteError)
|
||||
@@ -391,4 +395,4 @@ function updateSelectedCount(side) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endpush
|
||||
|
||||
@@ -73,19 +73,48 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
|
||||
<!-- 메인 레이아웃: 좌측 탭 + 우측 콘텐츠 -->
|
||||
<div class="flex gap-4 flex-1 min-h-0">
|
||||
<!-- 좌측: 코드 그룹 탭 (세로) -->
|
||||
<div class="w-40 flex-shrink-0 bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">코드 그룹</span>
|
||||
<div class="w-44 flex-shrink-0 bg-white rounded-lg shadow-sm overflow-hidden flex flex-col">
|
||||
<div class="px-2 py-1.5 bg-gray-50 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
|
||||
<span class="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">코드 그룹</span>
|
||||
<button type="button" onclick="openGroupModal()"
|
||||
class="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="그룹 추가">
|
||||
<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="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="overflow-y-auto max-h-[calc(100vh-220px)]" aria-label="Tabs">
|
||||
<!-- 스코프 필터 -->
|
||||
<div class="px-1.5 py-1 border-b border-gray-200 flex flex-wrap gap-0.5 flex-shrink-0">
|
||||
<button type="button" onclick="filterGroups('all')"
|
||||
class="group-filter-btn active text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-700 text-white" data-scope="all">전체</button>
|
||||
<button type="button" onclick="filterGroups('global-based')"
|
||||
class="group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-100 text-gray-600 hover:bg-gray-200" data-scope="global-based">글로벌</button>
|
||||
<button type="button" onclick="filterGroups('tenant-based')"
|
||||
class="group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-100 text-gray-600 hover:bg-gray-200" data-scope="tenant-based">테넌트</button>
|
||||
</div>
|
||||
<nav class="overflow-y-auto flex-1 min-h-0" aria-label="Tabs">
|
||||
@foreach($codeGroups as $group => $label)
|
||||
@php $scope = $groupScopes[$group] ?? 'custom'; @endphp
|
||||
<a href="{{ route('common-codes.index', ['group' => $group]) }}"
|
||||
class="block px-3 py-2.5 border-l-4 transition-colors
|
||||
data-scope="{{ $scope }}"
|
||||
class="group-item block px-2 py-1.5 border-l-3 transition-colors
|
||||
{{ $selectedGroup === $group
|
||||
? 'border-l-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-l-transparent text-gray-600 hover:bg-gray-50 hover:text-gray-900' }}">
|
||||
<span class="block text-sm font-medium truncate">{{ $label }}</span>
|
||||
<span class="block text-xs font-mono {{ $selectedGroup === $group ? 'text-blue-400' : 'text-gray-400' }}">{{ $group }}</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="block text-xs font-medium truncate flex-1">{{ $label }}</span>
|
||||
@if($scope === 'global')
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 flex-shrink-0" title="글로벌"></span>
|
||||
@elseif($scope === 'both')
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-400 flex-shrink-0" title="글로벌+테넌트"></span>
|
||||
@elseif($scope === 'tenant')
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-400 flex-shrink-0" title="테넌트"></span>
|
||||
@else
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-gray-300 flex-shrink-0" title="빈 그룹"></span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="block text-[10px] font-mono {{ $selectedGroup === $group ? 'text-blue-400' : 'text-gray-400' }} truncate">{{ $group }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
@@ -445,10 +474,80 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
|
||||
@csrf
|
||||
<input type="hidden" name="ids_json" id="bulkCopyIds">
|
||||
</form>
|
||||
|
||||
<!-- 코드 그룹 추가 모달 -->
|
||||
<div id="groupModal" 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-sm mx-4">
|
||||
<form action="{{ route('common-codes.store-group') }}" method="POST">
|
||||
@csrf
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800">코드 그룹 추가</h3>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">그룹 코드 *</label>
|
||||
<input type="text" name="group_code" required
|
||||
pattern="[a-z][a-z0-9_]*" maxlength="50"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="예: payment_method">
|
||||
<p class="mt-1 text-xs text-gray-400">영문 소문자, 숫자, 언더스코어만 사용</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">그룹명 *</label>
|
||||
<input type="text" name="group_label" required maxlength="50"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="예: 결제방법">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
||||
<button type="button" onclick="closeGroupModal()"
|
||||
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="submit"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// 그룹 스코프 필터
|
||||
function filterGroups(filter) {
|
||||
const items = document.querySelectorAll('.group-item');
|
||||
items.forEach(item => {
|
||||
const scope = item.dataset.scope;
|
||||
let show = false;
|
||||
switch (filter) {
|
||||
case 'all':
|
||||
show = true;
|
||||
break;
|
||||
case 'global-based':
|
||||
show = (scope === 'global' || scope === 'both');
|
||||
break;
|
||||
case 'tenant-based':
|
||||
show = (scope === 'tenant' || scope === 'both');
|
||||
break;
|
||||
}
|
||||
item.style.display = show ? '' : 'none';
|
||||
});
|
||||
|
||||
// 버튼 활성 상태
|
||||
document.querySelectorAll('.group-filter-btn').forEach(btn => {
|
||||
if (btn.dataset.scope === filter) {
|
||||
btn.className = 'group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-700 text-white';
|
||||
} else {
|
||||
btn.className = 'group-filter-btn text-[10px] px-1.5 py-0.5 rounded font-medium transition-colors bg-gray-100 text-gray-600 hover:bg-gray-200';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 모달 열기/닫기
|
||||
function openAddModal() {
|
||||
document.getElementById('addModal').classList.remove('hidden');
|
||||
@@ -460,6 +559,16 @@ function closeAddModal() {
|
||||
document.getElementById('addModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
function openGroupModal() {
|
||||
document.getElementById('groupModal').classList.remove('hidden');
|
||||
document.getElementById('groupModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeGroupModal() {
|
||||
document.getElementById('groupModal').classList.add('hidden');
|
||||
document.getElementById('groupModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
function openEditModal(code) {
|
||||
document.getElementById('editForm').action = `/common-codes/${code.id}`;
|
||||
document.getElementById('editCode').value = code.code;
|
||||
@@ -601,13 +710,17 @@ function deleteCode(id, code) {
|
||||
document.getElementById('editModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeEditModal();
|
||||
});
|
||||
document.getElementById('groupModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeGroupModal();
|
||||
});
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeAddModal();
|
||||
closeEditModal();
|
||||
closeGroupModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endpush
|
||||
|
||||
@@ -150,7 +150,8 @@ class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3 class="font-semibold text-gray-800">로컬 (현재)</h3>
|
||||
<h3 class="font-semibold text-gray-800">로컬</h3>
|
||||
<span class="text-xs text-gray-500">- {{ $localTenantName }}</span>
|
||||
<span class="text-xs text-gray-500">({{ count($localCodes) }}개)</span>
|
||||
</div>
|
||||
@if(!empty($environments[$selectedEnv]['url']) && !$remoteError)
|
||||
@@ -219,6 +220,9 @@ class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500">
|
||||
</svg>
|
||||
</span>
|
||||
<h3 class="font-semibold text-gray-800">{{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }}</h3>
|
||||
@if($remoteTenantName)
|
||||
<span class="text-xs text-gray-500">- {{ $remoteTenantName }}</span>
|
||||
@endif
|
||||
<span class="text-xs text-gray-500">({{ count($remoteCodes) }}개)</span>
|
||||
</div>
|
||||
@if(!empty($environments[$selectedEnv]['url']) && !$remoteError)
|
||||
@@ -392,4 +396,4 @@ function updateSelectedCount(side) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endpush
|
||||
|
||||
@@ -116,7 +116,7 @@ class="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3 class="font-semibold text-gray-800">로컬 (현재)</h3>
|
||||
<h3 class="font-semibold text-gray-800">로컬 - {{ $localTenantName }}</h3>
|
||||
<span class="text-xs text-gray-500">({{ count($localMenus) }}개 그룹)</span>
|
||||
</div>
|
||||
@if(!empty($environments[$selectedEnv]['url']) && !$remoteError)
|
||||
@@ -152,7 +152,7 @@ class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3 class="font-semibold text-gray-800">{{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }}</h3>
|
||||
<h3 class="font-semibold text-gray-800">{{ $environments[$selectedEnv]['name'] ?? strtoupper($selectedEnv) }}{{ $remoteTenantName ? ' - ' . $remoteTenantName : '' }}</h3>
|
||||
<span class="text-xs text-gray-500">({{ count($remoteMenus) }}개 그룹)</span>
|
||||
</div>
|
||||
@if(!empty($environments[$selectedEnv]['url']) && !$remoteError)
|
||||
|
||||
Reference in New Issue
Block a user