iipbs_cs #10

Merged
chanii8 merged 3 commits from iipbs_cs into main 2025-03-19 13:00:02 +09:00
19 changed files with 234 additions and 145 deletions
+23 -20
View File
@@ -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 (
+3 -3
View File
@@ -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">
+2
View File
@@ -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 -1
View File
@@ -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 {
+6
View File
@@ -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)