421 lines
14 KiB
PHP
421 lines
14 KiB
PHP
<?php
|
|
|
|
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;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class CommonCodeSyncController extends Controller
|
|
{
|
|
private ?string $remoteTenantName = null;
|
|
|
|
/**
|
|
* 현재 선택된 테넌트 ID
|
|
*/
|
|
protected function getTenantId(): int
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return ($tenantId && $tenantId !== 'all') ? (int) $tenantId : 1;
|
|
}
|
|
|
|
/**
|
|
* 환경 설정 조회 (메뉴 동기화와 공유)
|
|
*/
|
|
private function getEnvironments(): array
|
|
{
|
|
$setting = TenantSetting::withoutGlobalScopes()
|
|
->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)),
|
|
];
|
|
}
|
|
}
|