where('tenant_id', $this->getTenantId()) ->where('setting_group', 'menu_sync') ->where('setting_key', 'environments') ->first(); return $setting?->setting_value ?? [ 'dev' => ['name' => '개발', 'url' => '', 'api_key' => ''], 'prod' => ['name' => '운영', 'url' => '', 'api_key' => ''], ]; } /** * 공통코드 동기화 페이지 */ public function index(Request $request): View|Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('common-codes.sync.index')); } $environments = $this->getEnvironments(); $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 { $remoteCodes = $this->fetchRemoteCodes($environments[$selectedEnv], $selectedType); } catch (\Exception $e) { $remoteError = $e->getMessage(); } } // 차이점 계산 $diff = $this->calculateDiff($localCodes, $remoteCodes); return view('common-codes.sync', [ 'environments' => $environments, 'selectedEnv' => $selectedEnv, 'selectedType' => $selectedType, 'localCodes' => $localCodes, 'remoteCodes' => $remoteCodes, 'remoteError' => $remoteError, 'diff' => $diff, 'localTenantName' => $localTenant?->company_name ?? '알 수 없음', 'remoteTenantName' => $this->remoteTenantName, ]); } /** * 공통코드 Export API (다른 환경에서 호출) */ public function export(Request $request): JsonResponse { // API Key 검증 $apiKey = $request->header('X-Menu-Sync-Key'); $validKey = config('app.menu_sync_api_key', env('MENU_SYNC_API_KEY')); if (empty($validKey) || $apiKey !== $validKey) { return response()->json(['error' => 'Unauthorized'], 401); } $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, ]); } /** * 공통코드 Import API (다른 환경에서 호출) */ public function import(Request $request): JsonResponse { // API Key 검증 $apiKey = $request->header('X-Menu-Sync-Key'); $validKey = config('app.menu_sync_api_key', env('MENU_SYNC_API_KEY')); if (empty($validKey) || $apiKey !== $validKey) { return response()->json(['error' => 'Unauthorized'], 401); } $validated = $request->validate([ 'codes' => 'required|array', 'codes.*.tenant_id' => 'nullable|integer', 'codes.*.code_group' => 'required|string|max:50', 'codes.*.code' => 'required|string|max:50', 'codes.*.name' => 'required|string|max:100', 'codes.*.sort_order' => 'nullable|integer', 'codes.*.attributes' => 'nullable|array', 'codes.*.is_active' => 'nullable|boolean', ]); $imported = 0; $skipped = 0; foreach ($validated['codes'] as $codeData) { // 동일 코드 존재 확인 $exists = CommonCode::query() ->where('tenant_id', $codeData['tenant_id'] ?? null) ->where('code_group', $codeData['code_group']) ->where('code', $codeData['code']) ->exists(); if ($exists) { $skipped++; continue; } CommonCode::create([ 'tenant_id' => $codeData['tenant_id'] ?? null, 'code_group' => $codeData['code_group'], 'code' => $codeData['code'], 'name' => $codeData['name'], 'sort_order' => $codeData['sort_order'] ?? 0, 'attributes' => $codeData['attributes'] ?? null, 'is_active' => $codeData['is_active'] ?? true, ]); $imported++; } return response()->json([ 'success' => true, 'message' => "{$imported}개 코드가 동기화되었습니다." . ($skipped > 0 ? " ({$skipped}개 스킵)" : ''), 'imported' => $imported, 'skipped' => $skipped, ]); } /** * Push (로컬 → 원격) */ public function push(Request $request): JsonResponse { $validated = $request->validate([ 'env' => 'required|string|in:dev,prod', 'type' => 'required|string|in:global,tenant', 'code_keys' => 'required|array|min:1', 'code_keys.*' => 'string', ]); $environments = $this->getEnvironments(); $env = $environments[$validated['env']] ?? null; if (! $env || empty($env['url'])) { return response()->json(['error' => '환경 설정이 없습니다.'], 400); } // 선택된 코드 조회 (타입 필터 적용) $localCodes = $this->getCodeList($validated['type']); $selectedCodes = array_filter($localCodes, function ($code) use ($validated) { $key = $this->makeCodeKey($code); return in_array($key, $validated['code_keys']); }); if (empty($selectedCodes)) { return response()->json(['error' => '선택된 코드가 없습니다.'], 400); } // 원격 서버로 전송 try { $response = Http::withHeaders([ 'X-Menu-Sync-Key' => $env['api_key'], 'Accept' => 'application/json', ])->post(rtrim($env['url'], '/') . '/common-code-sync/import', [ 'codes' => array_values($selectedCodes), ]); if ($response->successful()) { return response()->json([ 'success' => true, 'message' => $response->json('message', '동기화 완료'), 'imported' => $response->json('imported', 0), 'skipped' => $response->json('skipped', 0), ]); } return response()->json([ 'error' => $response->json('error', '원격 서버 오류'), ], $response->status()); } catch (\Exception $e) { return response()->json(['error' => '연결 실패: ' . $e->getMessage()], 500); } } /** * Pull (원격 → 로컬) */ public function pull(Request $request): JsonResponse { $validated = $request->validate([ 'env' => 'required|string|in:dev,prod', 'type' => 'required|string|in:global,tenant', 'code_keys' => 'required|array|min:1', 'code_keys.*' => 'string', ]); $environments = $this->getEnvironments(); $env = $environments[$validated['env']] ?? null; if (! $env || empty($env['url'])) { return response()->json(['error' => '환경 설정이 없습니다.'], 400); } // 원격 코드 조회 (타입 필터 적용) try { $remoteCodes = $this->fetchRemoteCodes($env, $validated['type']); } catch (\Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } // 선택된 코드만 필터링 $selectedCodes = array_filter($remoteCodes, function ($code) use ($validated) { $key = $this->makeCodeKey($code); return in_array($key, $validated['code_keys']); }); if (empty($selectedCodes)) { return response()->json(['error' => '선택된 코드를 찾을 수 없습니다.'], 400); } // 로컬에 Import $imported = 0; $skipped = 0; foreach ($selectedCodes as $codeData) { // 동일 코드 존재 확인 $exists = CommonCode::query() ->where('tenant_id', $codeData['tenant_id'] ?? null) ->where('code_group', $codeData['code_group']) ->where('code', $codeData['code']) ->exists(); if ($exists) { $skipped++; continue; } CommonCode::create([ 'tenant_id' => $codeData['tenant_id'] ?? null, 'code_group' => $codeData['code_group'], 'code' => $codeData['code'], 'name' => $codeData['name'], 'sort_order' => $codeData['sort_order'] ?? 0, 'attributes' => $codeData['attributes'] ?? null, 'is_active' => $codeData['is_active'] ?? true, ]); $imported++; } return response()->json([ 'success' => true, 'message' => "{$imported}개 코드가 동기화되었습니다." . ($skipped > 0 ? " ({$skipped}개 스킵)" : ''), 'imported' => $imported, 'skipped' => $skipped, ]); } /** * 코드 목록 조회 * @param string $type 'global', 'tenant', or 'all' */ private function getCodeList(string $type = 'all'): array { $tenantId = $this->getTenantId(); $codes = []; // 글로벌 코드 (tenant_id IS NULL) if ($type === 'global' || $type === 'all') { $globalCodes = CommonCode::query() ->whereNull('tenant_id') ->orderBy('code_group') ->orderBy('sort_order') ->get(); foreach ($globalCodes as $code) { $codes[] = [ 'tenant_id' => null, 'code_group' => $code->code_group, 'code' => $code->code, 'name' => $code->name, 'sort_order' => $code->sort_order, 'attributes' => $code->attributes, 'is_active' => $code->is_active, 'is_global' => true, ]; } } // 테넌트 코드 if ($type === 'tenant' || $type === 'all') { $tenantCodes = CommonCode::query() ->where('tenant_id', $tenantId) ->orderBy('code_group') ->orderBy('sort_order') ->get(); foreach ($tenantCodes as $code) { $codes[] = [ 'tenant_id' => $code->tenant_id, 'code_group' => $code->code_group, 'code' => $code->code, 'name' => $code->name, 'sort_order' => $code->sort_order, 'attributes' => $code->attributes, 'is_active' => $code->is_active, 'is_global' => false, ]; } } return $codes; } /** * 원격 코드 조회 */ private function fetchRemoteCodes(array $env, string $type = 'all'): array { $response = Http::withHeaders([ 'X-Menu-Sync-Key' => $env['api_key'], 'Accept' => 'application/json', ])->timeout(10)->get(rtrim($env['url'], '/') . '/common-code-sync/export', [ 'type' => $type, ]); if (! $response->successful()) { throw new \Exception('API 오류: HTTP ' . $response->status()); } $data = $response->json(); if (! isset($data['codes'])) { throw new \Exception('잘못된 응답 형식'); } $this->remoteTenantName = $data['tenant_name'] ?? null; return $data['codes']; } /** * 코드 키 생성 (유니크 식별자) */ private function makeCodeKey(array $code): string { $tenantPart = $code['tenant_id'] ?? 'global'; return "{$tenantPart}:{$code['code_group']}:{$code['code']}"; } /** * 차이점 계산 */ private function calculateDiff(array $localCodes, array $remoteCodes): array { $localKeys = array_map(fn($c) => $this->makeCodeKey($c), $localCodes); $remoteKeys = array_map(fn($c) => $this->makeCodeKey($c), $remoteCodes); return [ 'local_only' => array_values(array_diff($localKeys, $remoteKeys)), 'remote_only' => array_values(array_diff($remoteKeys, $localKeys)), 'both' => array_values(array_intersect($localKeys, $remoteKeys)), ]; } }