fix(admin-panel): useSearchParams 제거 + deploy 강건성 개선
Deploy momo-erp / deploy (push) Failing after 43s

- useSearchParams 가 Next.js 15 prerender 단계에서 Suspense 경계를 강제해
  /admin-panel 빌드 자체가 실패 → docker image 재빌드 안 됨 →
  컨테이너 swap 누락(2시간째 옛 이미지). window.location.search 직접 읽기로 대체
- deploy.yml: set +e 제거 (빌드 실패가 워크플로우 success 로 묻히는 문제 차단)
- docker compose 에 --force-recreate 추가 (이미지가 같아도 컨테이너 강제 재생성)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-08 13:33:40 +09:00
parent 93f27d70f2
commit faf8315260
2 changed files with 18 additions and 15 deletions
+4 -3
View File
@@ -16,14 +16,14 @@ jobs:
- name: Deploy via SSH (password auth)
run: |
set +e # 배포 단계 실패해도 워크플로우 성공 처리 (실제 결과는 헬스체크가 판단)
set -e # 배포 단계 실패하면 즉시 워크플로우 fail (헬스체크에 의존하지 않음)
export SSHPASS='qlalfqjsgh11'
mkdir -p ~/.ssh
ssh-keyscan -H 183.99.177.40 >> ~/.ssh/known_hosts 2>/dev/null || true
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
chpark@183.99.177.40 'bash -s' <<'REMOTE_SCRIPT'
set +e
set -e # 원격 명령도 fail 즉시 중단
DEPLOY_DIR="$HOME/momo-erp/source"
mkdir -p "$HOME/momo-erp"
@@ -62,7 +62,8 @@ jobs:
DEPLOY_WEBHOOK_TOKEN=momo-deploy-2026-secure
ENVEOF
docker compose -f docker-compose.prod.yml up -d --build
# --force-recreate: docker compose 가 변화 감지 못해 컨테이너 swap 안 하는 케이스 방지
docker compose -f docker-compose.prod.yml up -d --build --force-recreate momo-erp
# 마이그레이션 (idempotent) — 컨테이너 안에 db/migrations + scripts/migrate-momo.mjs 가
# standalone 번들에 포함되어 있어야 동작 (next.config.ts outputFileTracingIncludes).
+14 -12
View File
@@ -1,7 +1,6 @@
"use client";
import { useState, useCallback, useEffect, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import { cn } from "@/lib/utils";
import { DataGrid, type GridColumn } from "@/components/grid/data-grid";
import { SearchForm, SearchField } from "@/components/layout/search-form";
@@ -106,22 +105,25 @@ const VALID_TABS: AdminTab[] = [
"ref-product-group","ref-product","spec-data-category","car-option",
];
function readTabFromUrl(): AdminTab {
if (typeof window === "undefined") return "user";
const t = new URLSearchParams(window.location.search).get("tab");
return t && (VALID_TABS as string[]).includes(t) ? (t as AdminTab) : "user";
}
export default function AdminPanelPage() {
const searchParams = useSearchParams();
const tabParam = searchParams.get("tab");
const initialTab: AdminTab = tabParam && (VALID_TABS as string[]).includes(tabParam)
? (tabParam as AdminTab)
: "user";
const [activeTab, setActiveTab] = useState<AdminTab>(initialTab);
const [activeTab, setActiveTab] = useState<AdminTab>("user");
const [groups, setGroups] = useState<SidebarGroup[]>([]);
const [openSections, setOpenSections] = useState<Set<string>>(new Set(["권한 및 사용자 관리"]));
// 사이드바에서 ?tab= 으로 다른 탭 클릭 시 동기화
// 마운트 + popstate(뒤로가기) 시 ?tab= 으로 activeTab 동기화.
// useSearchParams 는 Next.js 15 의 prerender 단계에서 Suspense 경계를 강제하므로 사용하지 않음.
useEffect(() => {
if (tabParam && (VALID_TABS as string[]).includes(tabParam)) {
setActiveTab(tabParam as AdminTab);
}
}, [tabParam]);
setActiveTab(readTabFromUrl());
const sync = () => setActiveTab(readTabFromUrl());
window.addEventListener("popstate", sync);
return () => window.removeEventListener("popstate", sync);
}, []);
const toggleSection = (label: string) => {
setOpenSections((prev) => {