diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index f10e731..5440b33 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -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). diff --git a/src/app/admin-panel/page.tsx b/src/app/admin-panel/page.tsx index ba18dd6..1722f89 100644 --- a/src/app/admin-panel/page.tsx +++ b/src/app/admin-panel/page.tsx @@ -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(initialTab); + const [activeTab, setActiveTab] = useState("user"); const [groups, setGroups] = useState([]); const [openSections, setOpenSections] = useState>(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) => {