From 82f0be5945aa132fe35ceaf2905a5c357aa54f24 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sat, 1 Aug 2026 21:16:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B3=84=EC=A0=95=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20dev/prod=20API=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EB=9F=B0=ED=83=80=EC=9E=84=20=EC=9E=90=EB=8F=99=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 어드민 웹은 서브도메인이 하나뿐이라 web/university-web처럼 빌드타임 .env.development/.env.production 분리가 불가능하다. 로그인 이메일이 @dev.solid-connection.com 도메인이면 stage API로, 그 외는 prod API로 로그인 요청 및 이후 모든 API 호출을 런타임에 라우팅하도록 변경했다. - 선택된 환경은 localStorage에 저장되어 새로고침 후에도 유지되고, 화면에 현재 환경(DEV/PROD)을 배지로 표시한다. - axios 인스턴스는 요청 인터셉터에서 baseURL을 매번 재계산해 런타임 전환을 지원하며, 로컬 개발용 VITE_API_SERVER_URL override는 vinext dev 모드에서만 적용된다. - 재로그인 시 이전 환경의 토큰을 먼저 제거해 다른 환경으로 토큰이 새어나가는 경합을 차단했다. --- apps/admin/.env.example | 7 +-- .../features/bruno/BrunoApiPageContent.tsx | 4 +- .../chat-socket/ChatSocketPageContent.tsx | 3 +- .../src/components/layout/AdminLayout.tsx | 2 + .../components/layout/EnvironmentBanner.tsx | 50 +++++++++++++++++++ apps/admin/src/lib/api/auth.ts | 27 +++++----- apps/admin/src/lib/api/client.ts | 10 +--- apps/admin/src/lib/auth/environment.test.ts | 28 +++++++++++ apps/admin/src/lib/auth/environment.ts | 15 ++++++ apps/admin/src/lib/auth/session.ts | 8 ++- apps/admin/src/lib/env.ts | 19 ++++++- apps/admin/src/lib/utils/localStorage.ts | 34 +++++++++++++ 12 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 apps/admin/src/components/layout/EnvironmentBanner.tsx create mode 100644 apps/admin/src/lib/auth/environment.test.ts create mode 100644 apps/admin/src/lib/auth/environment.ts diff --git a/apps/admin/.env.example b/apps/admin/.env.example index b676155a4..bd8647fa7 100644 --- a/apps/admin/.env.example +++ b/apps/admin/.env.example @@ -1,6 +1,7 @@ -# API 서버 URL (필수) -# 빌드/프리뷰 시 이 값이 없으면 API 호출은 실패하지만 SSR shell은 500 없이 응답해야 합니다. -VITE_API_SERVER_URL=https://api.example.com +# API 서버 URL (로컬 개발 전용 override, 선택) +# 설정 시 로그인 계정의 이메일 패턴과 무관하게 이 값이 항상 사용됩니다. +# 비워두면 로그인한 계정의 이메일 도메인에 따라 dev/prod API가 런타임에 자동 선택됩니다. +# VITE_API_SERVER_URL=http://localhost:8080 # 업로드 CDN URL (선택) VITE_UPLOADED_IMAGE_URL=https://cdn.upload.solid-connection.com diff --git a/apps/admin/src/components/features/bruno/BrunoApiPageContent.tsx b/apps/admin/src/components/features/bruno/BrunoApiPageContent.tsx index 77c1b54e7..32b239497 100644 --- a/apps/admin/src/components/features/bruno/BrunoApiPageContent.tsx +++ b/apps/admin/src/components/features/bruno/BrunoApiPageContent.tsx @@ -15,6 +15,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { axiosInstance } from "@/lib/api/client"; +import { resolveActiveApiBaseUrl } from "@/lib/env"; import { cn } from "@/lib/utils"; type EndpointItem = BrunoApiDefinitionRegistryItem; @@ -46,7 +47,6 @@ const ALL_ENDPOINTS: EndpointItem[] = [...brunoApiDefinitionRegistry].sort((a, b const METHOD_FILTERS: MethodFilter[] = ["ALL", "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]; const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]); const METHODS_WITHOUT_BODY = new Set(["GET", "HEAD"]); -const apiServerUrl = import.meta.env.VITE_API_SERVER_URL?.trim() ?? ""; const toPrettyJson = (value: unknown): string => { if (value === undefined) { @@ -199,7 +199,7 @@ export function BrunoApiPageContent() { const remoteWarningUrl = selectedEndpoint && isAbsoluteUrl(selectedEndpoint.definition.path) ? selectedEndpoint.definition.path - : apiServerUrl; + : resolveActiveApiBaseUrl(); const showRemoteWarning = isRemoteApiServer(remoteWarningUrl); useEffect(() => { diff --git a/apps/admin/src/components/features/chat-socket/ChatSocketPageContent.tsx b/apps/admin/src/components/features/chat-socket/ChatSocketPageContent.tsx index 17dad5190..eceba4a2c 100644 --- a/apps/admin/src/components/features/chat-socket/ChatSocketPageContent.tsx +++ b/apps/admin/src/components/features/chat-socket/ChatSocketPageContent.tsx @@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; +import { resolveActiveApiBaseUrl } from "@/lib/env"; import { loadAccessToken } from "@/lib/utils/localStorage"; type ConnectionState = "DISCONNECTED" | "CONNECTING" | "CONNECTED" | "ERROR"; @@ -92,7 +93,7 @@ export function ChatSocketPageContent() { const subscriptionRef = useRef(null); const [connectionState, setConnectionState] = useState("DISCONNECTED"); - const [serverUrl, setServerUrl] = useState(import.meta.env.VITE_API_SERVER_URL?.trim() ?? ""); + const [serverUrl, setServerUrl] = useState(resolveActiveApiBaseUrl()); const [token, setToken] = useState(""); const [roomId, setRoomId] = useState(""); const [topicTemplate, setTopicTemplate] = useState(defaultTopicTemplate); diff --git a/apps/admin/src/components/layout/AdminLayout.tsx b/apps/admin/src/components/layout/AdminLayout.tsx index edea0cfc6..785409f91 100644 --- a/apps/admin/src/components/layout/AdminLayout.tsx +++ b/apps/admin/src/components/layout/AdminLayout.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { adminSignOutApi } from "@/lib/api/auth"; import { clearSession } from "@/lib/auth/session"; import { type ActiveAdminMenu, AdminSidebar } from "./AdminSidebar"; +import { EnvironmentBanner } from "./EnvironmentBanner"; interface AdminLayoutProps { children: React.ReactNode; @@ -51,6 +52,7 @@ export function AdminLayout({ children, activeMenu, title, description }: AdminL
+

운영 콘솔