feat: [menu-sync] 순서 동기화 Push + 되돌리기 기능 추가
- pushOrder: 로컬 메뉴 순서를 원격 서버에 일괄 반영 - undoOrder: 순서 동기화 취소하여 이전 상태로 복원 - reorder: 외부 API 엔드포인트 (이름 기반 매칭) - 세션 기반 스냅샷으로 되돌리기 지원
This commit is contained in:
@@ -85,6 +85,7 @@ public function index(Request $request): View|Response
|
||||
'diff' => $diff,
|
||||
'localTenantName' => $localTenant?->company_name ?? '알 수 없음',
|
||||
'remoteTenantName' => $this->remoteTenantName,
|
||||
'hasOrderSnapshot' => session()->has("menu_order_snapshot_{$selectedEnv}"),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -470,6 +471,213 @@ private function filterMenusByName(array $menus, array $names, ?string $parentNa
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 순서 동기화 Push (로컬 순서 → 원격 서버)
|
||||
*/
|
||||
public function pushOrder(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'env' => 'required|string|in:dev,prod',
|
||||
]);
|
||||
|
||||
$environments = $this->getEnvironments();
|
||||
$env = $environments[$validated['env']] ?? null;
|
||||
|
||||
if (! $env || empty($env['url'])) {
|
||||
return response()->json(['error' => '환경 설정이 없습니다.'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 원격 서버 현재 순서 스냅샷 저장 (되돌리기용)
|
||||
$remoteMenus = $this->fetchRemoteMenus($env);
|
||||
$snapshot = $this->buildOrderMap($remoteMenus);
|
||||
session()->put("menu_order_snapshot_{$validated['env']}", $snapshot);
|
||||
|
||||
// 2. 로컬 메뉴 트리에서 순서 매핑 생성
|
||||
$localMenus = $this->getMenuTree();
|
||||
$orderMap = $this->buildOrderMap($localMenus);
|
||||
|
||||
// 3. 원격 서버에 순서 업데이트 전송
|
||||
$response = Http::withHeaders([
|
||||
'X-Menu-Sync-Key' => $env['api_key'],
|
||||
'Accept' => 'application/json',
|
||||
])->timeout(30)->post(rtrim($env['url'], '/').'/menu-sync/reorder', [
|
||||
'menus' => $orderMap,
|
||||
'tenant_id' => $this->getTenantId(),
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $response->json('message', '순서 동기화 완료'),
|
||||
'hasSnapshot' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// 실패 시 스냅샷 삭제
|
||||
session()->forget("menu_order_snapshot_{$validated['env']}");
|
||||
|
||||
return response()->json([
|
||||
'error' => $response->json('error', '원격 서버 오류'),
|
||||
], $response->status());
|
||||
} catch (\Exception $e) {
|
||||
session()->forget("menu_order_snapshot_{$validated['env']}");
|
||||
|
||||
return response()->json(['error' => '연결 실패: '.$e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 순서 동기화 되돌리기
|
||||
*/
|
||||
public function undoOrder(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'env' => 'required|string|in:dev,prod',
|
||||
]);
|
||||
|
||||
$environments = $this->getEnvironments();
|
||||
$env = $environments[$validated['env']] ?? null;
|
||||
|
||||
if (! $env || empty($env['url'])) {
|
||||
return response()->json(['error' => '환경 설정이 없습니다.'], 400);
|
||||
}
|
||||
|
||||
$snapshot = session("menu_order_snapshot_{$validated['env']}");
|
||||
if (empty($snapshot)) {
|
||||
return response()->json(['error' => '되돌릴 스냅샷이 없습니다.'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'X-Menu-Sync-Key' => $env['api_key'],
|
||||
'Accept' => 'application/json',
|
||||
])->timeout(30)->post(rtrim($env['url'], '/').'/menu-sync/reorder', [
|
||||
'menus' => $snapshot,
|
||||
'tenant_id' => $this->getTenantId(),
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
session()->forget("menu_order_snapshot_{$validated['env']}");
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '순서가 이전 상태로 복원되었습니다.',
|
||||
'hasSnapshot' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => $response->json('error', '원격 서버 오류'),
|
||||
], $response->status());
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['error' => '연결 실패: '.$e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 순서 재정렬 API (외부 서버에서 호출)
|
||||
*/
|
||||
public function reorder(Request $request): JsonResponse
|
||||
{
|
||||
$apiKey = $request->header('X-Menu-Sync-Key');
|
||||
$validKey = config('app.menu_sync_api_key');
|
||||
|
||||
if (empty($validKey) || $apiKey !== $validKey) {
|
||||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'menus' => 'required|array',
|
||||
'tenant_id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$tenantId = $validated['tenant_id'] ?? $this->getTenantId();
|
||||
$updated = $this->applyOrder($validated['menus'], $tenantId);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "{$updated}개 메뉴 순서가 업데이트되었습니다.",
|
||||
'updated' => $updated,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 트리를 순서 매핑 배열로 변환
|
||||
*/
|
||||
private function buildOrderMap(array $menus, ?string $parentName = null): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($menus as $menu) {
|
||||
$item = [
|
||||
'name' => $menu['name'],
|
||||
'parent_name' => $parentName,
|
||||
'sort_order' => $menu['sort_order'] ?? 0,
|
||||
'children' => [],
|
||||
];
|
||||
|
||||
if (! empty($menu['children'])) {
|
||||
$item['children'] = $this->buildOrderMap($menu['children'], $menu['name']);
|
||||
}
|
||||
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 순서 매핑을 DB에 적용 (재귀)
|
||||
*/
|
||||
private function applyOrder(array $orderMap, int $tenantId, ?int $parentId = null): int
|
||||
{
|
||||
$updated = 0;
|
||||
|
||||
foreach ($orderMap as $item) {
|
||||
$query = Menu::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('name', $item['name']);
|
||||
|
||||
// parent_name으로 부모 매칭
|
||||
if (! empty($item['parent_name'])) {
|
||||
$parent = Menu::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('name', $item['parent_name'])
|
||||
->first();
|
||||
$resolvedParentId = $parent?->id;
|
||||
} else {
|
||||
$resolvedParentId = null;
|
||||
}
|
||||
|
||||
// parentId 파라미터가 있으면 우선 사용 (재귀 호출 시)
|
||||
$targetParentId = $parentId ?? $resolvedParentId;
|
||||
$query->where('parent_id', $targetParentId);
|
||||
|
||||
$menu = $query->first();
|
||||
if ($menu) {
|
||||
$changes = [];
|
||||
if ($menu->sort_order !== ($item['sort_order'] ?? 0)) {
|
||||
$changes['sort_order'] = $item['sort_order'] ?? 0;
|
||||
}
|
||||
if ($menu->parent_id !== $targetParentId) {
|
||||
$changes['parent_id'] = $targetParentId;
|
||||
}
|
||||
|
||||
if (! empty($changes)) {
|
||||
$menu->update($changes);
|
||||
$updated++;
|
||||
}
|
||||
|
||||
// 자식 메뉴 처리
|
||||
if (! empty($item['children'])) {
|
||||
$updated += $this->applyOrder($item['children'], $tenantId, $menu->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 Import
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user