From 48e9ece4f7adf14107ff880f5a3cbbf32bfe6513 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 9 Mar 2026 15:15:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(login):=20POP=20=EB=AA=A8=EB=93=9C=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20=EC=B6=94=EA=B0=80=20-=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20POP/PC=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8F=BC?= =?UTF-8?q?=EC=97=90=20POP=20=EB=AA=A8=EB=93=9C=20Switch=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=98=84=EC=9E=A5=20=EC=9E=91=EC=97=85=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=EC=A0=90=EC=97=90?= =?UTF-8?q?=EC=84=9C=20POP=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20=EC=A7=84=EC=9E=85=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=9C=EB=8B=A4.=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=20=EC=83=81=ED=83=9C=EB=8A=94=20localStorage=EC=97=90?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=EB=90=98=EC=96=B4=20=EB=8B=A4=EC=9D=8C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EC=9C=A0=EC=A7=80?= =?UTF-8?q?=EB=90=9C=EB=8B=A4.=20[=EB=B0=B1=EC=97=94=EB=93=9C]=20-=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=91=EB=8B=B5=EC=97=90=20pop?= =?UTF-8?q?LandingPath=20=EC=B6=94=EA=B0=80=20(getPopMenuList=20=EC=9E=AC?= =?UTF-8?q?=EC=82=AC=EC=9A=A9)=20-=20AdminService/paramMap=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=8A=A4=EC=BD=94=ED=94=84=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=20=20(try=20=EB=B8=94=EB=A1=9D=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EC=84=A0=EC=96=B8=20->=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99)=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C]=20-=20useLogin:=20isPopMode=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20+=20localStorage=20=EC=97=B0=EB=8F=99=20+?= =?UTF-8?q?=20POP=20=EB=B6=84=EA=B8=B0=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20-?= =?UTF-8?q?=20LoginForm:=20POP=20=EB=AA=A8=EB=93=9C=20Switch=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=20UI=20(Monitor=20=EC=95=84=EC=9D=B4=EC=BD=98)=20-=20?= =?UTF-8?q?POP=20=EB=AF=B8=EC=84=A4=EC=A0=95=20=EC=8B=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=ED=9B=84=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=A4=91=EB=8B=A8=20?= =?UTF-8?q?-=20LoginResponse=20=ED=83=80=EC=9E=85=EC=97=90=20popLandingPat?= =?UTF-8?q?h=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/authController.ts | 42 +++++++++++------- frontend/app/(auth)/login/page.tsx | 15 ++++++- frontend/components/auth/LoginForm.tsx | 20 ++++++++- frontend/hooks/useLogin.ts | 44 ++++++++++++++----- frontend/types/auth.ts | 1 + 5 files changed, 93 insertions(+), 29 deletions(-) diff --git a/backend-node/src/controllers/authController.ts b/backend-node/src/controllers/authController.ts index ebf3e8f5..21673369 100644 --- a/backend-node/src/controllers/authController.ts +++ b/backend-node/src/controllers/authController.ts @@ -50,29 +50,24 @@ export class AuthController { logger.debug(`로그인 사용자 정보: ${userInfo.userId} (${userInfo.companyCode})`); + // 메뉴 조회를 위한 공통 파라미터 + const { AdminService } = await import("../services/adminService"); + const paramMap = { + userId: loginResult.userInfo.userId, + userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN", + userType: loginResult.userInfo.userType, + userLang: "ko", + }; + // 사용자의 첫 번째 접근 가능한 메뉴 조회 let firstMenuPath: string | null = null; try { - const { AdminService } = await import("../services/adminService"); - const paramMap = { - userId: loginResult.userInfo.userId, - userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN", - userType: loginResult.userInfo.userType, - userLang: "ko", - }; - const menuList = await AdminService.getUserMenuList(paramMap); logger.debug(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`); - // 접근 가능한 첫 번째 메뉴 찾기 - // 조건: - // 1. LEV (레벨)이 2 이상 (최상위 폴더 제외) - // 2. MENU_URL이 있고 비어있지 않음 - // 3. 이미 PATH, SEQ로 정렬되어 있으므로 첫 번째로 찾은 것이 첫 번째 메뉴 const firstMenu = menuList.find((menu: any) => { const level = menu.lev || menu.level; const url = menu.menu_url || menu.url; - return level >= 2 && url && url.trim() !== "" && url !== "#"; }); @@ -86,13 +81,30 @@ export class AuthController { logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError); } + // POP 랜딩 경로 조회 + let popLandingPath: string | null = null; + try { + const popResult = await AdminService.getPopMenuList(paramMap); + if (popResult.landingMenu?.menu_url) { + popLandingPath = popResult.landingMenu.menu_url; + } else if (popResult.childMenus.length === 1) { + popLandingPath = popResult.childMenus[0].menu_url; + } else if (popResult.childMenus.length > 1) { + popLandingPath = "/pop"; + } + logger.debug(`POP 랜딩 경로: ${popLandingPath}`); + } catch (popError) { + logger.warn("POP 메뉴 조회 중 오류 (무시):", popError); + } + res.status(200).json({ success: true, message: "로그인 성공", data: { userInfo, token: loginResult.token, - firstMenuPath, // 첫 번째 접근 가능한 메뉴 경로 추가 + firstMenuPath, + popLandingPath, }, }); } else { diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx index fe697cee..d105df77 100644 --- a/frontend/app/(auth)/login/page.tsx +++ b/frontend/app/(auth)/login/page.tsx @@ -10,8 +10,17 @@ import { LoginFooter } from "@/components/auth/LoginFooter"; * 비즈니스 로직은 useLogin 훅에서 처리하고, UI 컴포넌트들을 조합하여 구성 */ export default function LoginPage() { - const { formData, isLoading, error, showPassword, handleInputChange, handleLogin, togglePasswordVisibility } = - useLogin(); + const { + formData, + isLoading, + error, + showPassword, + isPopMode, + handleInputChange, + handleLogin, + togglePasswordVisibility, + togglePopMode, + } = useLogin(); return (
@@ -23,9 +32,11 @@ export default function LoginPage() { isLoading={isLoading} error={error} showPassword={showPassword} + isPopMode={isPopMode} onInputChange={handleInputChange} onSubmit={handleLogin} onTogglePassword={togglePasswordVisibility} + onTogglePop={togglePopMode} /> diff --git a/frontend/components/auth/LoginForm.tsx b/frontend/components/auth/LoginForm.tsx index dda3736f..7eadbfeb 100644 --- a/frontend/components/auth/LoginForm.tsx +++ b/frontend/components/auth/LoginForm.tsx @@ -2,7 +2,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Eye, EyeOff, Loader2 } from "lucide-react"; +import { Switch } from "@/components/ui/switch"; +import { Eye, EyeOff, Loader2, Monitor } from "lucide-react"; import { LoginFormData } from "@/types/auth"; import { ErrorMessage } from "./ErrorMessage"; @@ -11,9 +12,11 @@ interface LoginFormProps { isLoading: boolean; error: string; showPassword: boolean; + isPopMode: boolean; onInputChange: (e: React.ChangeEvent) => void; onSubmit: (e: React.FormEvent) => void; onTogglePassword: () => void; + onTogglePop: () => void; } /** @@ -24,9 +27,11 @@ export function LoginForm({ isLoading, error, showPassword, + isPopMode, onInputChange, onSubmit, onTogglePassword, + onTogglePop, }: LoginFormProps) { return ( @@ -82,6 +87,19 @@ export function LoginForm({
+ {/* POP 모드 토글 */} +
+
+ + POP 모드 +
+ +
+ {/* 로그인 버튼 */}