This commit is contained in:
2025-06-12 10:31:02 +09:00
234 changed files with 8907 additions and 816 deletions
@@ -1,4 +1,5 @@
export * from "./client";
export * from "./menu";
export * from "./popup";
export * from "./site";
export * from "./user";
@@ -0,0 +1,69 @@
import { getApiClient } from "./client";
import { ENDPOINT } from "../config";
export interface PopupInfo {
popupSn: number;
popupTypeCd: string;
popupTypeNm: string;
contsTypeCd: string;
contsTypeNm: string;
popupTtl: string;
popupCn: string;
popupWdth: number;
popupHgt: number;
popupVtclPstn: number;
popupHrzntltyPstn: number;
popupEndUseYn: string;
popupLnkgUrl: string;
atflCd: string;
useYn: string;
regDt: string;
mdfcnDt: string;
rgTrNm: string;
mdfrNm: string;
}
interface SortConfig {
empty: boolean;
sorted: boolean;
unsorted: boolean;
}
interface PageableConfig {
sort: SortConfig;
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
}
interface PageableList<T> {
content: T[];
pageable: PageableConfig;
sort: SortConfig;
totalPages: number;
totalElements: number;
number: number;
numberOfElements: number;
size: number;
first: boolean;
last: boolean;
empty: boolean;
}
const client = getApiClient();
export const getPopupList = async () => {
const response = await client.get<PageableList<PopupInfo>>(
`${ENDPOINT.CMS_SERVICE.POPUPS}`,
);
return response.data;
};
export const getPopupInfo = async (popupSn: number) => {
const response = await client.get<PopupInfo>(
`${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`,
);
return response.data;
};
@@ -2,6 +2,7 @@ export const ENDPOINT = {
CMS_SERVICE: {
MENU_ROLES: "/cms-service/api/v1/menu-roles",
SITES: "/cms-service/api/v1/sites",
POPUPS: "/cms-service/api/v1/popups",
},
USER_SERVICE: {
USERS: "/user-service/api/v1/users",
@@ -0,0 +1,5 @@
function PopupProvider({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export { PopupProvider };
@@ -2,36 +2,36 @@
@layer components {
button[role="button"] {
@apply text-[1.3rem];
@apply text-[1.3rem] transition-opacity hover:opacity-90;
}
button[role="button"].primary {
@apply bg-[rgba(32,148,250,1)] hover:bg-[rgba(32,148,250,0.9)];
@apply bg-primary;
}
button[role="button"].green {
@apply bg-[rgba(31,208,120,1)] hover:bg-[rgba(31,208,120,0.9)];
@apply bg-[rgba(31,208,120,1)];
}
button[role="button"].sky {
@apply bg-[rgba(50,154,203,1)] hover:bg-[rgba(50,154,203,0.9)];
@apply bg-[rgba(50,154,203,1)];
}
button[role="button"].yellow {
@apply bg-[rgba(243,201,0,1)] hover:bg-[rgba(243,201,0,0.9)];
@apply bg-[rgba(243,201,0,1)];
}
button[role="button"].orange {
@apply bg-[rgba(255,149,0,1)] hover:bg-[rgba(255,149,0,0.9)];
@apply bg-[rgba(255,149,0,1)];
}
button[role="button"].emerald {
@apply bg-[rgba(90,200,250,1)] hover:bg-[rgba(90,200,250,0.9)];
@apply bg-[rgba(90,200,250,1)];
}
button[role="button"].white {
@apply border border-border bg-[rgba(255,255,255,1)] text-[#666] hover:bg-[rgba(255,255,255,0.9)];
@apply border border-border bg-[rgba(255,255,255,1)] text-[#666];
}
button[role="button"].red {
@apply bg-[rgba(232,93,85,1)] hover:bg-[rgba(232,93,85,0.9)];
@apply bg-destructive;
}
button[role="button"].gray {
@apply bg-[rgba(153,153,153,1)] hover:bg-[rgba(153,153,153,0.9)];
@apply bg-[rgba(153,153,153,1)];
}
button[role="button"].lightGray {
@apply bg-[rgba(235,235,235,1)] text-[#929292] hover:bg-[rgba(235,235,235,0.9)];
@apply bg-[rgba(235,235,235,1)] text-[#929292];
}
}
@@ -42,22 +42,31 @@ function DataTable<T>({
<Table className={cn("w-full", className)} variant={variant}>
<THead table={table} />
<tbody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
className={cn(onRowClick && "cursor-pointer hover:bg-accent")}
>
{row.getVisibleCells().map((cell) => {
const { meta } = cell.column.columnDef;
return (
<TableCell key={cell.id} align="center" {...meta}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
))}
{table.getRowModel().rows.map((row) => {
const isSelectedRow = row.getIsSelected();
return (
<TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
className={cn(
onRowClick && "cursor-pointer hover:bg-accent",
isSelectedRow && "!bg-primary-transparent",
)}
>
{row.getVisibleCells().map((cell) => {
const { meta } = cell.column.columnDef;
return (
<TableCell key={cell.id} align="center" {...meta}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
);
})}
</TableRow>
);
})}
</tbody>
</Table>
{viewPagination && <Pagination table={table} />}
@@ -91,7 +100,7 @@ function THead<T>({ table }: { table: TableInstance<T> }) {
return (
<TableHead
key={header.id}
className="sticky top-0"
className="sticky top-0 z-10"
colSpan={header.colSpan}
rowSpan={rowSpan}
{...meta}
@@ -153,7 +162,7 @@ function Pagination<T>({ table }: { table: TableInstance<T> }) {
key={pageNumber}
variant="ghost"
className={cn(
"inline-block size-10 !bg-transparent p-0 text-foreground hover:!bg-accent",
"inline-block h-10 min-w-10 !bg-transparent p-1 text-foreground hover:!bg-accent",
currentPage === pageNumber &&
"!bg-primary !font-bold !text-primary-foreground hover:!bg-primary",
)}
@@ -9,21 +9,30 @@ import { Button } from "../button";
import { Calendar } from "../calendar";
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
type DatePickerProps = DayPickerSingleProps;
type DatePickerProps = {
placeholder?: string;
disabled?: boolean;
} & DayPickerSingleProps;
export function DatePicker({ className, ...props }: DatePickerProps) {
export function DatePicker({
className,
placeholder = "날짜 선택",
disabled,
...props
}: DatePickerProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("date-picker-trigger", className)}
disabled={disabled}
>
<CalendarIcon className="!size-[1.6rem]" />
{props.selected ? (
format(props.selected, "yyyy-MM-dd")
) : (
<span>날짜 선택</span>
<span>{placeholder}</span>
)}
</Button>
</PopoverTrigger>
@@ -0,0 +1,31 @@
@tailwind components;
@layer components {
[data-theme="blue"] {
header {
background: #00afe5;
border: none;
}
header h1 span {
color: #fff;
}
[data-trigger="dropdown"] {
background: transparent !important;
color: #fff;
}
}
[data-theme="green"] {
header {
background: #008e8d;
border: none;
}
header h1 span {
color: #fff;
}
[data-trigger="dropdown"] {
background: transparent !important;
color: #fff;
}
}
}
@@ -1,9 +1,18 @@
import "./header.css";
import { useEffect, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import { images } from "@common/assets";
import { getConfig } from "@common/config";
import { ENDPOINT } from "../../config";
import { setCookie } from "../../lib";
import { getSiteInfo, getUserInfo } from "../../api";
import { setCookie, getCookie } from "../../lib";
import {
getPopupInfo,
getPopupList,
getSiteInfo,
getUserInfo,
type PopupInfo,
} from "../../api";
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -35,6 +44,51 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
enabled: !viewOnly,
});
const { data: popupList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS],
queryFn: () => getPopupList(),
});
const openPopup = useCallback(async (popup: PopupInfo) => {
try {
const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`;
const hasCookie = getCookie(cookieName);
if (hasCookie) return;
const response = await getPopupInfo(popup.popupSn);
if (!response) return;
const width = response.popupWdth || 400;
const height = response.popupHgt || 300;
const left = (response.popupHrzntltyPstn || 0) + window.screenX;
const top = (response.popupVtclPstn || 0) + window.screenY;
const popupWindow = window.open(
`/view/popup/${popup.popupSn}`,
"",
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=no,location=no,menubar=no`,
);
if (popupWindow) {
popupWindow.focus();
}
} catch (error) {
console.error(`Failed to open popup ${popup.popupSn}:`, error);
}
}, []);
useEffect(() => {
if (!popupList?.content.length) return;
const timer = setTimeout(() => {
popupList.content.forEach((popup) => {
openPopup(popup);
});
}, 500);
return () => clearTimeout(timer);
}, [popupList, openPopup]);
return (
<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">
@@ -50,7 +104,10 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label">
<Button
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
data-trigger="dropdown"
>
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
{userInfo?.userName}
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
@@ -20,6 +20,7 @@ export * from "./page-title";
export * from "./popover";
export * from "./progress";
export * from "./radio-group";
export * from "./scroll-area";
export * from "./select";
export * from "./slider";
export * from "./skeleton";
@@ -1,10 +1,11 @@
import type { QueryClient } from "@tanstack/react-query";
import type { MenuItem } from "../../api";
import { QueryProvider } from "../../provider/query-provider";
import { PopupProvider } from "../../provider/popup-provider";
import type { MenuItem } from "../../api";
import { SidebarProvider } from "../sidebar";
import { LNB } from "../lnb";
import { Header } from "../header";
import { Footer } from "./footer";
import { SidebarProvider } from "../sidebar";
import "./common.css";
export function BaseLayout({
@@ -20,20 +21,25 @@ export function BaseLayout({
}) {
return (
<QueryProvider queryClient={queryClient}>
<SidebarProvider>
<div className="flex-1">
<Header />
<div className="flex h-[calc(100vh-50px)]">
<LNB currentPath={currentPath} handleMenuClick={handleMenuClick} />
<div className="flex flex-1 flex-col">
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
{children}
<PopupProvider>
<SidebarProvider>
<div className="flex-1">
<Header />
<div className="flex h-[calc(100vh-50px)]">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<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>
</SidebarProvider>
</PopupProvider>
</QueryProvider>
);
}
@@ -206,3 +206,21 @@
color: #ffffff;
background: #40a5ff;
}
[data-theme="blue"] {
footer {
background: #6ec05d;
}
footer p {
color: #fff;
}
}
[data-theme="green"] {
footer {
background: #24ec7d;
}
footer p {
color: #fff;
}
}
+15 -12
View File
@@ -65,6 +65,7 @@ export function LNB({
"h-auto py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
isMatched && "bg-accent !text-primary",
)}
data-matched={isMatched}
onClick={handleClick}
>
<i className={cn("diveicon ml-2", `di-${item.iconNm}`)} />
@@ -96,19 +97,21 @@ export function LNB({
<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">
<SidebarMenuButton className="group/menu-button text-[1.4rem] group-data-[state=open]:bg-transparent">
<i
className={cn(
"diveicon ml-2 text-label",
`di-${item.iconNm}`,
)}
/>
<span className="text-label group-hover/menu-button:text-foreground">
{item.menuKornNm}
</span>
<div className="group">
<SidebarMenuButton className="group/menu-button flex h-auto items-center justify-between py-4 pr-2 text-[1.4rem] group-data-[state=open]:bg-transparent">
<div className="flex items-center gap-2">
<i
className={cn(
"diveicon ml-2 text-label",
`di-${item.iconNm}`,
)}
/>
<span className="text-label group-hover/menu-button:text-foreground">
{item.menuKornNm}
</span>
</div>
<ChevronRight className="!size-6 text-label group-data-[state=open]:rotate-90" />
</SidebarMenuButton>
<ChevronRight className="size-6 text-label group-data-[state=open]:rotate-90" />
</div>
</CollapsibleTrigger>
<CollapsibleContent>
@@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "../../lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
@@ -1,3 +1,5 @@
import "./sidebar.css";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
@@ -248,6 +250,7 @@ const Sidebar = React.forwardRef<
)}
/>
<div
data-sidebar="sidebar-wrapper"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
@@ -0,0 +1,68 @@
@tailwind components;
@layer components {
[data-theme="blue"] {
[data-sidebar="sidebar-wrapper"] {
background: none;
background-image: linear-gradient(
to bottom,
#00afe5 0%,
#48bfad 65%,
#6ec05d 100%
) !important;
border: none;
}
[data-sidebar="sidebar"],
[data-sidebar="content"] {
background: transparent !important;
}
[data-sidebar="menu-item"] * {
color: #fff !important;
}
[data-sidebar="menu-item"] *:hover {
background: none !important;
color: #fff !important;
}
[data-sidebar="menu-button"]:hover {
background: rgba(0, 0, 0, 0.1) !important;
}
[data-sidebar="menu-button"][data-matched="true"] {
background: rgba(0, 0, 0, 0.2) !important;
}
}
[data-theme="red"] {
[data-sidebar="menu-button"][data-matched="true"] {
@apply bg-primary !text-white;
}
}
[data-theme="green"] {
[data-sidebar="sidebar-wrapper"] {
background: none;
background-image: linear-gradient(
0.4turn,
#008e8d 20%,
#24ec7d 100%
) !important;
border: none;
}
[data-sidebar="sidebar"],
[data-sidebar="content"] {
background: transparent !important;
}
[data-sidebar="menu-item"] * {
color: #fff !important;
}
[data-sidebar="menu-item"] *:hover {
background: none !important;
color: #fff !important;
}
[data-sidebar="menu-button"]:hover {
background: rgba(0, 0, 0, 0.1) !important;
}
[data-sidebar="menu-button"][data-matched="true"] {
background: rgba(0, 0, 0, 0.2) !important;
}
}
}
@@ -47,4 +47,22 @@
@apply text-[#fff];
}
}
[data-theme="red"] {
.table-ui.default th {
@apply bg-[#8d8d94];
}
.table-ui.secondary th {
@apply bg-[#f6f6f9] text-[#808080];
}
}
[data-theme="green"] {
.table-ui.default th {
@apply bg-[#6bafae];
}
.table-ui.secondary th {
@apply bg-[#ebf7f7] text-[#64908f];
}
}
}