iipbs_cs #10
@@ -1,44 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { setUserInfo } = useUserStore((state) => state);
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
queryFn: async () => {
|
||||
async function authCheck() {
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
|
||||
);
|
||||
if (!userInfo) setUserInfo(response.data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
|
||||
);
|
||||
setUserInfo(response.data);
|
||||
return response;
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -4,8 +4,10 @@ import Image from "next/image";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
function LoginPage() {
|
||||
const router = useRouter();
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -13,7 +15,8 @@ function LoginPage() {
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
LoginApi.login({ userId, password });
|
||||
await LoginApi.login({ userId, password });
|
||||
router.push("/site");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -53,9 +53,9 @@ function SitePage() {
|
||||
return (
|
||||
<SitePageLayout>
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border !border-[#dbe2e6] !bg-[#eef7ff]">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<label htmlFor="" className="mr-2 text-[#657481]">
|
||||
<label htmlFor="" className="mr-2 text-label">
|
||||
사이트명
|
||||
</label>
|
||||
<Input
|
||||
@@ -72,7 +72,7 @@ function SitePage() {
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-[#666]">사이트 목록</h3>
|
||||
<h3 className="text-2xl font-medium text-label">사이트 목록</h3>
|
||||
<Button onClick={() => router.push("/site/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@minoru/react-dnd-treeview": "^3.5.0",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./client";
|
||||
export * from "./menu";
|
||||
export * from "./user";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { client } from "./client";
|
||||
import { ENDPOINT } from "../config";
|
||||
|
||||
export interface MenuItem {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
export const getMenuList = async (siteId: string) => {
|
||||
const response = await client.get<MenuItem[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ENDPOINT } from "../config";
|
||||
import { client } from "./client";
|
||||
import { ENDPOINT } from "../config";
|
||||
|
||||
export interface IUser {
|
||||
regDt: string;
|
||||
|
||||
@@ -3,6 +3,9 @@ import { PROXY_HOST } from "./env";
|
||||
export const BASE_URL = `${PROXY_HOST}/server`;
|
||||
|
||||
export const ENDPOINT = {
|
||||
CMS_SERVICE: {
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
USERS: "/user-service/api/v1/users",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -20,7 +20,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 box-border flex h-[5rem] w-full items-center justify-between border-b border-solid border-border bg-background">
|
||||
<header className="sticky top-0 z-30 box-border flex h-[5rem] w-full items-center justify-between border-b border-solid border-border bg-background">
|
||||
<div className="flex items-center">
|
||||
<div className="MobMenuBtn">
|
||||
<img src={images.menuBtn} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { LNB } from "./lnb";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
import { Header } from "../header";
|
||||
import { Footer } from "./footer";
|
||||
|
||||
@@ -11,18 +12,20 @@ const queryClient = new QueryClient();
|
||||
export function BaseLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div>
|
||||
<Header />
|
||||
<div className="flex h-[calc(100vh-50px)]">
|
||||
<LNB />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
|
||||
{children}
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
<div className="flex h-[calc(100vh-50px)]">
|
||||
<LNB />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
export function LNB() {
|
||||
return (
|
||||
<div
|
||||
id="lnb"
|
||||
className="!w-[22rem] overflow-auto border-r border-solid border-border bg-background"
|
||||
>
|
||||
<ul>
|
||||
<li id="m_dashboard">
|
||||
<a href="#none" className="!bg-background">
|
||||
Dashboard
|
||||
<i
|
||||
className="diveicon di-diveicon di-chevron-down arrow"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<div className="mline">PAGES</div>
|
||||
<li id="m_page_board">
|
||||
<a href="#none" className="!bg-background">
|
||||
콘텐츠 관리
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_page_mem">
|
||||
<a href="#none" className="!bg-background">
|
||||
로그인 & 회원관리
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_page_manage">
|
||||
<a href="#none" className="!bg-background">
|
||||
페이지 관리
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_page_example">
|
||||
<a href="#none" className="!bg-background">
|
||||
샘플 페이지
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_layouts">
|
||||
<a href="#none" className="!bg-background">
|
||||
페이지 레이아웃
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<div className="mline">COMPONENTS</div>
|
||||
<li id="m_charts">
|
||||
<a href="#none" className="!bg-background">
|
||||
Charts
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_tables">
|
||||
<a href="#none" className="!bg-background">
|
||||
Tables
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_elements">
|
||||
<a href="#none" className="!bg-background">
|
||||
Elements
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_tabs">
|
||||
<a href="#none" className="!bg-background">
|
||||
Tabs
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_popup">
|
||||
<a href="#none" className="!bg-background">
|
||||
Modal
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_swipe">
|
||||
<a href="#none" className="!bg-background">
|
||||
Swipe
|
||||
<i className="diveicon di-chevron-down arrow" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_icon">
|
||||
<a href="#none" className="!bg-background">
|
||||
Font Icon
|
||||
</a>
|
||||
</li>
|
||||
<div className="mline">OTHERS</div>
|
||||
<li id="m_elements">
|
||||
<a href="#none" className="!bg-background">
|
||||
Menu Label <span className="ar_label_box red">NEW</span>
|
||||
</a>
|
||||
</li>
|
||||
<li id="m_elements">
|
||||
<a href="#none" className="!bg-background">
|
||||
External Link
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { LNB } from "./lnb";
|
||||
import { LNB } from "../lnb";
|
||||
import { Header } from "../header";
|
||||
import { Footer } from "./footer";
|
||||
|
||||
@@ -14,7 +14,7 @@ export function VBaseLayout({ children }: { children: React.ReactNode }) {
|
||||
<div>
|
||||
<Header viewOnly />
|
||||
<div className="flex h-[calc(100vh-50px)]">
|
||||
<LNB />
|
||||
<LNB viewOnly />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { ENDPOINT, SITE_ID } from "../../config";
|
||||
import { getMenuList, type MenuItem } from "../../api";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
} from "../sidebar";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../collapsible";
|
||||
import { getParentIds } from "./lib";
|
||||
|
||||
export function LNB({ viewOnly }: { viewOnly?: boolean }) {
|
||||
const { data: menuList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => getMenuList(SITE_ID),
|
||||
enabled: !viewOnly,
|
||||
});
|
||||
|
||||
const [openMenuIds, setOpenMenuIds] = useState<number[]>([]);
|
||||
|
||||
function MenuItem({ item }: { item: MenuItem }) {
|
||||
if (item.children) {
|
||||
return <MenuGroup item={item} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="h-auto py-4 text-[1.4rem] text-label transition-colors hover:bg-accent">
|
||||
<i className={cn("diveicon ml-2", `di-${item.iconNm}`)} />
|
||||
{item.menuKornNm}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuGroup({ item }: { item: MenuItem }) {
|
||||
const isOpen = openMenuIds.includes(item.menuCd);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
if (!openMenuIds.includes(item.menuCd)) {
|
||||
const parentIds = getParentIds(item.menuCd)(menuList ?? []);
|
||||
setOpenMenuIds([...parentIds, item.menuCd]);
|
||||
}
|
||||
} else {
|
||||
setOpenMenuIds((prev) => prev.filter((id) => id !== item.menuCd));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="group flex items-center justify-between rounded-sm py-4 pr-2 hover:bg-accent data-[state=open]:text-primary">
|
||||
<SidebarMenuButton className="group/menu-button text-[1.4rem] group-data-[state=open]:bg-transparent">
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon ml-2 text-label group-data-[state=open]:text-primary",
|
||||
`di-${item.iconNm}`,
|
||||
)}
|
||||
/>
|
||||
<span className="text-label group-hover/menu-button:text-foreground group-data-[state=open]:text-primary">
|
||||
{item.menuKornNm}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
<ChevronRight className="size-6 text-label group-data-[state=open]:rotate-90" />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub className="mr-0">
|
||||
{item.children?.map((child) => (
|
||||
<MenuItem key={child.menuCd} item={child} />
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar className="border-border bg-background px-4 py-[6rem]">
|
||||
<SidebarContent className="bg-background">
|
||||
<SidebarMenu className="gap-0">
|
||||
{menuList?.map((item) => <MenuItem key={item.menuCd} item={item} />)}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { MenuItem } from "../../api";
|
||||
|
||||
function getParentIds(menuCd: number): (menuList: MenuItem[]) => number[] {
|
||||
return (menuList: MenuItem[]) => {
|
||||
const parentIds: number[] = [];
|
||||
let currentId = menuCd;
|
||||
|
||||
while (true) {
|
||||
const menu = findMenuById(currentId)(menuList);
|
||||
if (!menu || !menu.parentId) break;
|
||||
parentIds.push(menu.parentId);
|
||||
currentId = menu.parentId;
|
||||
}
|
||||
|
||||
return parentIds;
|
||||
};
|
||||
}
|
||||
|
||||
function findMenuById(
|
||||
menuCd: number,
|
||||
): (menuList: MenuItem[]) => MenuItem | undefined {
|
||||
function findInMenu(items: MenuItem[]): MenuItem | undefined {
|
||||
for (const item of items) {
|
||||
if (item.menuCd === menuCd) return item;
|
||||
if (item.children) {
|
||||
const found = findInMenu(item.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return findInMenu;
|
||||
}
|
||||
|
||||
export { getParentIds, findMenuById };
|
||||
@@ -3,7 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "../../hooks/use-mobile";
|
||||
import { useIsMobile } from "../../lib/use-mobile";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "../button";
|
||||
import { Input } from "../input";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH = "22rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@apply bg-[#5987bf] px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2] text-[#fff];
|
||||
}
|
||||
.table-ui td {
|
||||
@apply border-b border-b-border px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2] text-[#666];
|
||||
@apply border-b border-b-border px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2] text-label;
|
||||
}
|
||||
|
||||
.table-ui.secondary th {
|
||||
|
||||
Generated
+6
@@ -354,6 +354,9 @@ importers:
|
||||
'@common/assets':
|
||||
specifier: workspace:*
|
||||
version: link:../assets
|
||||
'@frontend-opensource/use-react-hooks':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4
|
||||
'@minoru/react-dnd-treeview':
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -363,6 +366,9 @@ importers:
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-collapsible':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
|
||||
Reference in New Issue
Block a user