diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 9e5c7e18..7a7fa11e 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -27,28 +27,11 @@ app.use(compression()); app.use(express.json({ limit: "10mb" })); app.use(express.urlencoded({ extended: true, limit: "10mb" })); -// CORS 설정 +// CORS 설정 - environment.ts에서 이미 올바른 형태로 처리됨 app.use( cors({ - origin: function (origin, callback) { - const allowedOrigins = config.cors.origin - .split(",") - .map((url) => url.trim()); - // Allow requests with no origin (like mobile apps or curl requests) - if (!origin) return callback(null, true); - if (allowedOrigins.indexOf(origin) !== -1) { - return callback(null, true); - } else { - console.log(`CORS rejected origin: ${origin}`); - return callback( - new Error( - "CORS policy does not allow access from the specified Origin." - ), - false - ); - } - }, - credentials: true, + origin: config.cors.origin, // 이미 배열 또는 boolean으로 처리됨 + credentials: config.cors.credentials, methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], allowedHeaders: [ "Content-Type", diff --git a/backend-node/src/config/environment.ts b/backend-node/src/config/environment.ts index be936f76..62fe0635 100644 --- a/backend-node/src/config/environment.ts +++ b/backend-node/src/config/environment.ts @@ -26,7 +26,7 @@ interface Config { // CORS 설정 cors: { - origin: string; + origin: string | string[] | boolean; // 타입을 확장하여 배열과 boolean도 허용 credentials: boolean; }; @@ -58,6 +58,26 @@ interface Config { showErrorDetails: boolean; } +// CORS origin 처리 함수 +const getCorsOrigin = (): string[] | boolean => { + // 개발 환경에서는 모든 origin 허용 + if (process.env.NODE_ENV === "development") { + return true; + } + + // 환경변수가 있으면 쉼표로 구분하여 배열로 변환 + if (process.env.CORS_ORIGIN) { + return process.env.CORS_ORIGIN.split(",").map((origin) => origin.trim()); + } + + // 기본값: 허용할 도메인들 + return [ + "http://localhost:9771", // 로컬 개발 환경 + "http://192.168.0.70:5555", // 내부 네트워크 접근 + "http://39.117.244.52:5555", // 외부 네트워크 접근 + ]; +}; + const config: Config = { // 서버 설정 port: parseInt(process.env.PORT || "3000", 10), @@ -82,8 +102,8 @@ const config: Config = { // CORS 설정 cors: { - origin: process.env.CORS_ORIGIN || "http://localhost:9771", - credentials: process.env.CORS_CREDENTIALS === "true", + origin: getCorsOrigin(), + credentials: true, // 쿠키 및 인증 정보 포함 허용 }, // 로깅 설정 diff --git a/docker/prod/docker-compose.backend.prod.yml b/docker/prod/docker-compose.backend.prod.yml index cd29965c..85a0d189 100644 --- a/docker/prod/docker-compose.backend.prod.yml +++ b/docker/prod/docker-compose.backend.prod.yml @@ -5,20 +5,17 @@ services: context: ../../backend-node dockerfile: ../docker/prod/backend.Dockerfile # 운영용 Dockerfile container_name: pms-backend-prod - ports: - - "8080:8080" + network_mode: "host" # 호스트 네트워크 모드 environment: - NODE_ENV=production - PORT=8080 + - HOST=0.0.0.0 # 모든 인터페이스에서 바인딩 - DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm - JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024 - JWT_EXPIRES_IN=24h - - CORS_ORIGIN=http://192.168.0.70:5555 + - CORS_ORIGIN=http://192.168.0.70:5555,http://39.117.244.52:5555,http://localhost:9771 - CORS_CREDENTIALS=true - LOG_LEVEL=info - # 운영용에서는 볼륨 마운트 없음 (보안상 이유) - networks: - - pms-network restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] diff --git a/docker/prod/docker-compose.frontend.prod.yml b/docker/prod/docker-compose.frontend.prod.yml index 7878cde0..de07bec4 100644 --- a/docker/prod/docker-compose.frontend.prod.yml +++ b/docker/prod/docker-compose.frontend.prod.yml @@ -5,13 +5,13 @@ services: context: ../../frontend dockerfile: ../docker/prod/frontend.Dockerfile args: - - NEXT_PUBLIC_API_URL=http://192.168.0.70:8080/api + - NEXT_PUBLIC_API_URL=http://39.117.244.52:8080/api container_name: pms-frontend-linux ports: - "5555:5555" environment: - NODE_ENV=production - - NEXT_PUBLIC_API_URL=http://192.168.0.70:8080/api + - NEXT_PUBLIC_API_URL=http://39.117.244.52:8080/api networks: - pms-network restart: unless-stopped diff --git a/docker/prod/frontend.Dockerfile b/docker/prod/frontend.Dockerfile index c3703011..17df01e2 100644 --- a/docker/prod/frontend.Dockerfile +++ b/docker/prod/frontend.Dockerfile @@ -22,6 +22,10 @@ COPY . . # Disable telemetry during the build ENV NEXT_TELEMETRY_DISABLED 1 +# 빌드 시 환경변수 설정 (ARG로 받아서 ENV로 설정) +ARG NEXT_PUBLIC_API_URL=http://192.168.0.70:8080/api +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL + # Build the application ENV DISABLE_ESLINT_PLUGIN=true RUN npm run build diff --git a/frontend/components/screen/MenuAssignmentModal.tsx b/frontend/components/screen/MenuAssignmentModal.tsx new file mode 100644 index 00000000..00c74a6b --- /dev/null +++ b/frontend/components/screen/MenuAssignmentModal.tsx @@ -0,0 +1,546 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; +import { Search, Monitor, Settings, X, Plus } from "lucide-react"; +import { menuScreenApi } from "@/lib/api/screen"; +import { apiClient } from "@/lib/api/client"; +import type { MenuItem } from "@/lib/api/menu"; +import { ScreenDefinition } from "@/types/screen"; + +interface MenuAssignmentModalProps { + isOpen: boolean; + onClose: () => void; + screenInfo: ScreenDefinition | null; + onAssignmentComplete?: () => void; + onBackToList?: () => void; // 화면 목록으로 돌아가는 콜백 추가 +} + +export const MenuAssignmentModal: React.FC = ({ + isOpen, + onClose, + screenInfo, + onAssignmentComplete, + onBackToList, +}) => { + const [menus, setMenus] = useState([]); + const [selectedMenuId, setSelectedMenuId] = useState(""); + const [selectedMenu, setSelectedMenu] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [loading, setLoading] = useState(false); + const [assigning, setAssigning] = useState(false); + const [existingScreens, setExistingScreens] = useState([]); + const [showReplaceDialog, setShowReplaceDialog] = useState(false); + const [assignmentSuccess, setAssignmentSuccess] = useState(false); + const [assignmentMessage, setAssignmentMessage] = useState(""); + + // 메뉴 목록 로드 (관리자 메뉴만) + const loadMenus = async () => { + try { + setLoading(true); + + // 화면관리는 관리자 전용 기능이므로 관리자 메뉴만 가져오기 + const adminResponse = await apiClient.get("/admin/menus", { params: { menuType: "0" } }); + const adminMenus = adminResponse.data?.data || []; + + // 관리자 메뉴 정규화 + const normalizedAdminMenus = adminMenus.map((menu: any) => ({ + objid: menu.objid || menu.OBJID, + parent_obj_id: menu.parent_obj_id || menu.PARENT_OBJ_ID, + menu_name_kor: menu.menu_name_kor || menu.MENU_NAME_KOR, + menu_url: menu.menu_url || menu.MENU_URL, + menu_desc: menu.menu_desc || menu.MENU_DESC, + seq: menu.seq || menu.SEQ, + menu_type: "0", // 관리자 메뉴 + status: menu.status || menu.STATUS, + lev: menu.lev || menu.LEV, + company_code: menu.company_code || menu.COMPANY_CODE, + company_name: menu.company_name || menu.COMPANY_NAME, + })); + + console.log("로드된 관리자 메뉴 목록:", { + total: normalizedAdminMenus.length, + sample: normalizedAdminMenus.slice(0, 3), + }); + setMenus(normalizedAdminMenus); + } catch (error) { + console.error("메뉴 목록 로드 실패:", error); + toast.error("메뉴 목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }; + + // 모달이 열릴 때 메뉴 목록 로드 + useEffect(() => { + if (isOpen) { + loadMenus(); + setSelectedMenuId(""); + setSelectedMenu(null); + setSearchTerm(""); + setAssignmentSuccess(false); + setAssignmentMessage(""); + } + }, [isOpen]); + + // 메뉴 선택 처리 + const handleMenuSelect = async (menuId: string) => { + // 유효하지 않은 메뉴 ID인 경우 처리하지 않음 + if (!menuId || menuId === "no-menu") { + setSelectedMenuId(""); + setSelectedMenu(null); + setExistingScreens([]); + return; + } + + setSelectedMenuId(menuId); + const menu = menus.find((m) => m.objid?.toString() === menuId); + setSelectedMenu(menu || null); + + // 선택된 메뉴에 할당된 화면들 확인 + if (menu) { + try { + const menuObjid = parseInt(menu.objid?.toString() || "0"); + if (menuObjid > 0) { + const screens = await menuScreenApi.getScreensByMenu(menuObjid); + setExistingScreens(screens); + console.log(`메뉴 "${menu.menu_name_kor}"에 할당된 화면:`, screens); + } + } catch (error) { + console.error("할당된 화면 조회 실패:", error); + setExistingScreens([]); + } + } + }; + + // 화면 할당 처리 + const handleAssignScreen = async () => { + if (!selectedMenu || !screenInfo) { + toast.error("메뉴와 화면 정보가 필요합니다."); + return; + } + + // 기존에 할당된 화면이 있는지 확인 + if (existingScreens.length > 0) { + // 이미 같은 화면이 할당되어 있는지 확인 + const alreadyAssigned = existingScreens.some((screen) => screen.screenId === screenInfo.screenId); + if (alreadyAssigned) { + toast.info("이미 해당 메뉴에 할당된 화면입니다."); + return; + } + + // 다른 화면이 할당되어 있으면 교체 확인 + setShowReplaceDialog(true); + return; + } + + // 기존 화면이 없으면 바로 할당 + await performAssignment(); + }; + + // 실제 할당 수행 + const performAssignment = async (replaceExisting = false) => { + if (!selectedMenu || !screenInfo) return; + + try { + setAssigning(true); + + const menuObjid = parseInt(selectedMenu.objid?.toString() || "0"); + if (menuObjid === 0) { + toast.error("유효하지 않은 메뉴 ID입니다."); + return; + } + + // 기존 화면 교체인 경우 기존 화면들 먼저 제거 + if (replaceExisting && existingScreens.length > 0) { + console.log("기존 화면들 제거 중...", existingScreens); + for (const existingScreen of existingScreens) { + try { + await menuScreenApi.unassignScreenFromMenu(existingScreen.screenId, menuObjid); + console.log(`기존 화면 "${existingScreen.screenName}" 제거 완료`); + } catch (error) { + console.error(`기존 화면 "${existingScreen.screenName}" 제거 실패:`, error); + } + } + } + + // 새 화면 할당 + await menuScreenApi.assignScreenToMenu(screenInfo.screenId, menuObjid); + + const successMessage = replaceExisting + ? `기존 화면을 제거하고 "${screenInfo.screenName}" 화면이 "${selectedMenu.menu_name_kor}" 메뉴에 할당되었습니다.` + : `"${screenInfo.screenName}" 화면이 "${selectedMenu.menu_name_kor}" 메뉴에 성공적으로 할당되었습니다.`; + + // 성공 상태 설정 + setAssignmentSuccess(true); + setAssignmentMessage(successMessage); + + // 할당 완료 콜백 호출 + if (onAssignmentComplete) { + onAssignmentComplete(); + } + + // 3초 후 자동으로 화면 목록으로 이동 + setTimeout(() => { + if (onBackToList) { + onBackToList(); + } else { + onClose(); + } + }, 3000); + } catch (error: any) { + console.error("화면 할당 실패:", error); + const errorMessage = error.response?.data?.message || "화면 할당에 실패했습니다."; + toast.error(errorMessage); + } finally { + setAssigning(false); + } + }; + + // 필터된 메뉴 목록 + const filteredMenus = menus.filter((menu) => { + if (!searchTerm) return true; + const searchLower = searchTerm.toLowerCase(); + return ( + menu.menu_name_kor?.toLowerCase().includes(searchLower) || + menu.menu_url?.toLowerCase().includes(searchLower) || + menu.menu_desc?.toLowerCase().includes(searchLower) + ); + }); + + // 메뉴 옵션 생성 (계층 구조 표시) + const getMenuOptions = (): JSX.Element[] => { + if (loading) { + return [ + + 메뉴 로딩 중... + , + ]; + } + + if (filteredMenus.length === 0) { + return [ + + {searchTerm ? `"${searchTerm}"에 대한 검색 결과가 없습니다` : "메뉴가 없습니다"} + , + ]; + } + + return filteredMenus + .filter((menu) => menu.objid && menu.objid.toString().trim() !== "") // objid가 유효한 메뉴만 필터링 + .map((menu) => { + const indent = " ".repeat(Math.max(0, menu.lev || 0)); + const menuId = menu.objid!.toString(); // 이미 필터링했으므로 non-null assertion 사용 + + return ( + + {indent} + {menu.menu_name_kor} + + ); + }); + }; + + return ( + <> + + + {assignmentSuccess ? ( + // 성공 화면 + <> + + +
+ + + +
+ 화면 할당 완료 +
+ 화면이 성공적으로 메뉴에 할당되었습니다. +
+ +
+
+
+
+ +
+
+

{assignmentMessage}

+

3초 후 자동으로 화면 목록으로 이동합니다...

+
+
+
+ +
+
+
+
+
+
+ + + + + + ) : ( + // 기본 할당 화면 + <> + + + + 메뉴에 화면 할당 + + + 저장된 화면을 메뉴에 할당하여 사용자가 접근할 수 있도록 설정합니다. + + {screenInfo && ( +
+
+ + {screenInfo.screenName} + + {screenInfo.screenCode} + +
+ {screenInfo.description &&

{screenInfo.description}

} +
+ )} +
+ +
+ {/* 메뉴 선택 (검색 기능 포함) */} +
+ + { + e.stopPropagation(); // 이벤트 전파 방지 + setSearchTerm(e.target.value); + }} + onKeyDown={(e) => { + e.stopPropagation(); // 키보드 이벤트 전파 방지 + }} + onClick={(e) => { + e.stopPropagation(); // 클릭 이벤트 전파 방지 + }} + className="h-8 pr-8 pl-10 text-sm" + /> + {searchTerm && ( + + )} +
+
+ {/* 메뉴 옵션들 */} +
{getMenuOptions()}
+ + + + + {/* 선택된 메뉴 정보 */} + {selectedMenu && ( +
+
+
+
+

{selectedMenu.menu_name_kor}

+ 관리자 + + {selectedMenu.status === "active" ? "활성" : "비활성"} + +
+
+ {selectedMenu.menu_url &&

URL: {selectedMenu.menu_url}

} + {selectedMenu.menu_desc &&

설명: {selectedMenu.menu_desc}

} + {selectedMenu.company_name &&

회사: {selectedMenu.company_name}

} +
+ + {/* 기존 할당된 화면 정보 */} + {existingScreens.length > 0 && ( +
+

+ ⚠️ 이미 할당된 화면 ({existingScreens.length}개) +

+
+ {existingScreens.map((screen) => ( +
+ + {screen.screenName} + + {screen.screenCode} + +
+ ))} +
+

새 화면을 할당하면 기존 화면들이 제거됩니다.

+
+ )} +
+
+
+ )} + + + + + + + + )} +
+
+ + {/* 화면 교체 확인 대화상자 */} + + + + + + 화면 교체 확인 + + 선택한 메뉴에 이미 할당된 화면이 있습니다. + + +
+ {/* 기존 화면 목록 */} +
+

제거될 화면 ({existingScreens.length}개):

+
+ {existingScreens.map((screen) => ( +
+ + {screen.screenName} + + {screen.screenCode} + +
+ ))} +
+
+ + {/* 새로 할당될 화면 */} + {screenInfo && ( +
+

새로 할당될 화면:

+
+ + {screenInfo.screenName} + + {screenInfo.screenCode} + +
+
+ )} + +
+

+ 주의: 기존 화면들이 메뉴에서 제거되고 새 화면으로 교체됩니다. 이 작업은 되돌릴 수 + 없습니다. +

+
+
+ + + + + +
+
+ + ); +}; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index b4ed5c44..3e77db90 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -37,6 +37,7 @@ import { import { GroupingToolbar } from "./GroupingToolbar"; import { screenApi, tableTypeApi } from "@/lib/api/screen"; import { toast } from "sonner"; +import { MenuAssignmentModal } from "./MenuAssignmentModal"; import StyleEditor from "./StyleEditor"; import { RealtimePreview } from "./RealtimePreview"; @@ -133,6 +134,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); const [isSaving, setIsSaving] = useState(false); + // 메뉴 할당 모달 상태 + const [showMenuAssignmentModal, setShowMenuAssignmentModal] = useState(false); + // 해상도 설정 상태 const [screenResolution, setScreenResolution] = useState( SCREEN_RESOLUTIONS[0], // 기본값: Full HD @@ -802,13 +806,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); toast.success("화면이 저장되었습니다."); + + // 저장 성공 후 메뉴 할당 모달 열기 + setShowMenuAssignmentModal(true); } catch (error) { console.error("저장 실패:", error); toast.error("저장 중 오류가 발생했습니다."); } finally { setIsSaving(false); } - }, [selectedScreen?.screenId, layout]); + }, [selectedScreen?.screenId, layout, screenResolution]); // 템플릿 드래그 처리 const handleTemplateDrop = useCallback( @@ -2989,6 +2996,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD /> )} + + {/* 메뉴 할당 모달 */} + setShowMenuAssignmentModal(false)} + screenInfo={selectedScreen} + onAssignmentComplete={() => { + console.log("메뉴 할당 완료"); + // 필요시 추가 작업 수행 + }} + onBackToList={onBackToList} + /> ); } diff --git a/frontend/constants/auth.ts b/frontend/constants/auth.ts index 3d9b67eb..c1880a69 100644 --- a/frontend/constants/auth.ts +++ b/frontend/constants/auth.ts @@ -3,7 +3,7 @@ */ export const AUTH_CONFIG = { - API_BASE_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080", + API_BASE_URL: process.env.NEXT_PUBLIC_API_URL || "http://39.117.244.52:8080/api", ENDPOINTS: { LOGIN: "/auth/login", STATUS: "/auth/status", @@ -15,18 +15,21 @@ export const AUTH_CONFIG = { }, } as const; +export const UI_CONFIG = { + COMPANY_NAME: "WACE 솔루션", + COPYRIGHT: "© 2024 WACE 솔루션. All rights reserved.", + POWERED_BY: "Powered by WACE PLM System", +} as const; + export const FORM_VALIDATION = { MESSAGES: { + REQUIRED: "필수 입력 항목입니다.", + INVALID_FORMAT: "형식이 올바르지 않습니다.", + PASSWORD_MISMATCH: "비밀번호가 일치하지 않습니다.", + INVALID_CREDENTIALS: "아이디 또는 비밀번호가 올바르지 않습니다.", USER_ID_REQUIRED: "사용자 ID를 입력해주세요.", PASSWORD_REQUIRED: "비밀번호를 입력해주세요.", LOGIN_FAILED: "로그인에 실패했습니다.", - CONNECTION_FAILED: "서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요.", - BACKEND_CONNECTION_FAILED: "백엔드 서버에 연결할 수 없습니다.", + CONNECTION_FAILED: "서버 연결에 실패했습니다.", }, } as const; - -export const UI_CONFIG = { - COMPANY_NAME: "WACE 솔루션", - COPYRIGHT: "© 2025 WACE PLM Solution. All rights reserved.", - POWERED_BY: "Powered by Spring Boot + Next.js", -} as const; diff --git a/frontend/constants/layout.ts b/frontend/constants/layout.ts index 8268dbba..e245b1de 100644 --- a/frontend/constants/layout.ts +++ b/frontend/constants/layout.ts @@ -4,7 +4,7 @@ export const LAYOUT_CONFIG = { COMPANY_NAME: "WACE 솔루션", - API_BASE_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080", + API_BASE_URL: process.env.NEXT_PUBLIC_API_URL || "http://39.117.244.52:8080/api", ENDPOINTS: { USER_MENUS: "/admin/user-menus", @@ -24,18 +24,9 @@ export const LAYOUT_CONFIG = { export const MESSAGES = { LOADING: "로딩 중...", - NO_MENUS: "메뉴가 없습니다.", - PROFILE_SAVE_SUCCESS: "프로필이 성공적으로 저장되었습니다.", - PROFILE_SAVE_ERROR: "프로필 저장 중 오류가 발생했습니다.", - FILE_SIZE_ERROR: "파일 크기는 5MB를 초과할 수 없습니다.", - FILE_TYPE_ERROR: "이미지 파일만 업로드 가능합니다.", -} as const; - -export const MENU_ICONS = { - DEFAULT: "FileText", - HOME: ["홈", "메인"], - DOCUMENT: ["문서", "게시"], - USERS: ["사용자", "회원"], - STATISTICS: ["통계", "현황"], - SETTINGS: ["설정", "관리"], + ERROR: "오류가 발생했습니다.", + SUCCESS: "성공적으로 처리되었습니다.", + CONFIRM: "정말로 진행하시겠습니까?", + NO_DATA: "데이터가 없습니다.", + NO_MENUS: "사용 가능한 메뉴가 없습니다.", } as const; diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index deb05ab3..99376992 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; -import { apiCall } from "@/lib/api/client"; +import { apiCall, API_BASE_URL } from "@/lib/api/client"; // 사용자 정보 타입 정의 interface UserInfo { @@ -98,8 +98,7 @@ export const useAuth = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // API 기본 URL 설정 - const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"; + // API 기본 URL 설정 (동적으로 결정) /** * 현재 사용자 정보 조회 diff --git a/frontend/hooks/useLogin.ts b/frontend/hooks/useLogin.ts index 733716d6..5abffac9 100644 --- a/frontend/hooks/useLogin.ts +++ b/frontend/hooks/useLogin.ts @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { LoginFormData, LoginResponse } from "@/types/auth"; import { AUTH_CONFIG, FORM_VALIDATION } from "@/constants/auth"; +import { API_BASE_URL } from "@/lib/api/client"; /** * 로그인 관련 비즈니스 로직을 관리하는 커스텀 훅 @@ -60,7 +61,7 @@ export const useLogin = () => { * API 호출 공통 함수 */ const apiCall = useCallback(async (endpoint: string, options: RequestInit = {}): Promise => { - const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${endpoint}`, { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { credentials: "include", headers: { "Content-Type": "application/json", diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index e3eef389..ecd20973 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -1,7 +1,43 @@ import axios, { AxiosResponse, AxiosError } from "axios"; -// API 기본 URL 설정 -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api"; +// API URL 동적 설정 - 환경별 명확한 분리 +const getApiBaseUrl = (): string => { + console.log("🔍 API URL 결정 시작!"); + + if (typeof window !== "undefined") { + const currentHost = window.location.hostname; + const currentPort = window.location.port; + const fullUrl = window.location.href; + + console.log("🌐 현재 접속 정보:", { + hostname: currentHost, + fullUrl: fullUrl, + port: currentPort, + }); + + // 로컬 개발환경: localhost:9771 → localhost:8080 + if ((currentHost === "localhost" || currentHost === "127.0.0.1") && currentPort === "9771") { + console.log("🏠 로컬 개발 환경 감지 → localhost:8080/api"); + return "http://localhost:8080/api"; + } + + // 서버 환경에서 localhost:5555 → 39.117.244.52:8080 + if ((currentHost === "localhost" || currentHost === "127.0.0.1") && currentPort === "5555") { + console.log("🌍 서버 환경 (localhost:5555) 감지 → 39.117.244.52:8080/api"); + return "http://39.117.244.52:8080/api"; + } + + // 기타 서버 환경 (내부/외부 IP): → 39.117.244.52:8080 + console.log("🌍 서버 환경 감지 → 39.117.244.52:8080/api"); + return "http://39.117.244.52:8080/api"; + } + + // 서버 사이드 렌더링 기본값 + console.log("🖥️ SSR 기본값 → 39.117.244.52:8080/api"); + return "http://39.117.244.52:8080/api"; +}; + +export const API_BASE_URL = getApiBaseUrl(); // JWT 토큰 관리 유틸리티 const TokenManager = { diff --git a/frontend/lib/api/company.ts b/frontend/lib/api/company.ts index 0d9e85ca..ea5aa700 100644 --- a/frontend/lib/api/company.ts +++ b/frontend/lib/api/company.ts @@ -5,7 +5,7 @@ import { Company, CompanyFormData } from "@/types/company"; import { apiClient } from "./client"; -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api"; +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://39.117.244.52:8080/api"; // API 응답 타입 정의 interface ApiResponse { diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 286faf4a..5323571a 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -43,7 +43,7 @@ const nextConfig = { // 환경 변수 (런타임에 읽기) env: { - NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "http://192.168.0.70:8080/api", + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "http://39.117.244.52:8080/api", }, };