Merge branch 'refs/heads/main' into feature/0612_sentry_fr_merge

# Conflicts:
#	apps/as/next.config.ts
This commit is contained in:
2026-09-11 10:33:13 +09:00
904 changed files with 75682 additions and 20891 deletions
@@ -42,6 +42,18 @@ export const getMenuList = async (siteId: string): Promise<MenuItem[]> => {
return response.json();
};
export const getMenuDetail = async (menuCd: number): Promise<MenuItem> => {
const response = await fetch(
`${BASE_URL}${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
{
method: "GET",
headers: getAuthHeaders(),
credentials: "include",
},
);
return response.json();
};
export const getPreviewTreeMenuList = async (
siteId: string,
): Promise<TreeMenu[]> => {
@@ -1,55 +0,0 @@
"use client";
import { createContext, useContext, useState, useCallback } from "react";
interface HelpDialogContextType {
openHelp: () => void;
closeHelp: () => void;
isOpen: boolean;
setHelpContent: (content: string) => void;
helpContent: string;
}
const HelpDialogContext = createContext<HelpDialogContextType | undefined>(
undefined,
);
export function HelpDialogProvider({
children,
}: {
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(false);
const [helpContent, setHelpContent] = useState<string>("");
const openHelp = useCallback(() => {
setIsOpen(true);
}, []);
const closeHelp = useCallback(() => {
setIsOpen(false);
}, []);
return (
<HelpDialogContext.Provider
value={{
openHelp,
closeHelp,
isOpen,
setHelpContent,
helpContent,
}}
>
{children}
</HelpDialogContext.Provider>
);
}
export function useHelpDialog() {
const context = useContext(HelpDialogContext);
if (context === undefined) {
throw new Error("useHelpDialog must be used within a HelpDialogProvider");
}
return context;
}
@@ -77,47 +77,69 @@ function DataTable<T>({
function THead<T>({ table }: { table: TableInstance<T> }) {
return (
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const columnRelativeDepth = header.depth - header.column.depth;
if (
!header.isPlaceholder &&
columnRelativeDepth > 1 &&
header.id === header.column.id
) {
return null;
}
{table
.getHeaderGroups()
.map((headerGroup, headerGroupIndex, headerGroups) => (
<TableRow
key={headerGroup.id}
className={cn(
headerGroups.length > 1 &&
headerGroupIndex === 0 &&
"!border-b-0",
)}
>
{headerGroup.headers.map((header) => {
const columnRelativeDepth = header.depth - header.column.depth;
if (
!header.isPlaceholder &&
columnRelativeDepth > 1 &&
header.id === header.column.id
) {
return null;
}
let rowSpan = 1;
if (header.isPlaceholder) {
const leafs = header.getLeafHeaders();
rowSpan = (leafs?.[leafs.length - 1]?.depth ?? 0) - header.depth;
}
let rowSpan = 1;
if (header.isPlaceholder) {
const leafs = header.getLeafHeaders();
rowSpan =
(leafs?.[leafs.length - 1]?.depth ?? 0) - header.depth;
}
const { meta } = header.column.columnDef;
const { meta } = header.column.columnDef;
return (
<TableHead
key={header.id}
className="sticky top-0 z-10"
colSpan={header.colSpan}
rowSpan={rowSpan}
{...meta}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
const isNestedGroupHeader =
!header.isPlaceholder && hasNestedColumns(header.column);
return (
<TableHead
key={header.id}
className={cn(
"sticky top-0 z-10",
isNestedGroupHeader && "data-table-group-header",
)}
colSpan={header.colSpan}
rowSpan={rowSpan}
{...meta}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
);
}
function hasNestedColumns<T>(
column: import("@tanstack/react-table").Column<T, unknown>,
) {
return column.columns.length > 0;
}
function Pagination<T>({ table }: { table: TableInstance<T> }) {
const pageCount = table.getPageCount();
const currentPage = table.getState().pagination.pageIndex;
@@ -2,7 +2,6 @@ import type { AuthService, TokenManager } from "@common/auth";
import type { QueryClient } from "@tanstack/react-query";
import { QueryProvider } from "../../provider/query-provider";
import { PopupProvider } from "../../provider/popup-provider";
import { HelpDialogProvider } from "../../provider/help-dialog-context";
import type { MenuItem } from "../../api";
import { SidebarProvider } from "../sidebar";
import { LNB } from "../lnb";
@@ -29,26 +28,24 @@ export function BaseLayout({
return (
<QueryProvider queryClient={queryClient}>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header
authService={authService}
tokenManager={authService ? undefined : tokenManager}
<SidebarProvider>
<div className="flex-1">
<Header
authService={authService}
tokenManager={authService ? undefined : tokenManager}
/>
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col">
<div className="flex-1">{children}</div>
<Footer />
</div>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col">
<div className="flex-1">{children}</div>
<Footer />
</div>
</div>
</SidebarProvider>
</HelpDialogProvider>
</div>
</SidebarProvider>
</PopupProvider>
</QueryProvider>
);
@@ -1,7 +1,6 @@
import type { QueryClient } from "@tanstack/react-query";
import { QueryProvider } from "../../provider/query-provider";
import { PopupProvider } from "../../provider/popup-provider";
import { HelpDialogProvider } from "../../provider/help-dialog-context";
import type { MenuItem } from "../../api";
import { SidebarProvider } from "../sidebar";
import { LNB } from "../lnb";
@@ -25,22 +24,20 @@ export function NoFooterLayout({
return (
<QueryProvider queryClient={queryClient}>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
</div>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
</div>
</div>
</SidebarProvider>
</HelpDialogProvider>
</div>
</SidebarProvider>
</PopupProvider>
</QueryProvider>
);
@@ -2,7 +2,6 @@ import type { AuthService } from "@common/auth";
import type { QueryClient } from "@tanstack/react-query";
import { QueryProvider } from "../../provider/query-provider";
import { PopupProvider } from "../../provider/popup-provider";
import { HelpDialogProvider } from "../../provider/help-dialog-context";
import type { MenuItem } from "../../api";
import { SidebarProvider } from "../sidebar";
import { Header } from "../header";
@@ -23,19 +22,17 @@ export function NoLNBLayout({
return (
<QueryProvider queryClient={queryClient}>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
<Footer />
</div>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
<Footer />
</div>
</div>
</SidebarProvider>
</HelpDialogProvider>
</div>
</SidebarProvider>
</PopupProvider>
</QueryProvider>
);
@@ -1,5 +1,4 @@
import { QueryProvider } from "../../provider/query-provider";
import { HelpDialogProvider } from "../../provider/help-dialog-context";
import { SidebarProvider } from "../sidebar";
import { LNB } from "../lnb";
import { Header } from "../header";
@@ -17,8 +16,7 @@ export function VBaseLayout({
}) {
return (
<QueryProvider queryClient={queryClient}>
<HelpDialogProvider>
<SidebarProvider>
<SidebarProvider>
<div className="flex-1">
<Header viewOnly />
<div className="flex h-[calc(100vh-50px)]">
@@ -32,7 +30,6 @@ export function VBaseLayout({
</div>
</div>
</SidebarProvider>
</HelpDialogProvider>
</QueryProvider>
);
}
@@ -22,16 +22,8 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "../collapsible";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../dialog";
import { ENDPOINT } from "../../config";
import { cn, getCookie, setCookie } from "../../lib";
import { useHelpDialog } from "../../provider/help-dialog-context";
import {
getMenuList,
getPreviewTreeMenuList,
@@ -65,13 +57,6 @@ export function LNB({
const [currentMenuCode, setCurrentMenuCode] = useState<number | null>(null);
const prevPathRef = useRef(currentPath);
const { browser } = useDetectDevice();
const {
isOpen: isHelpModalOpen,
openHelp,
closeHelp,
setHelpContent,
helpContent,
} = useHelpDialog();
const appSiteId = getCookie("site-id-key") ?? "";
const siteId = propSiteId ?? appSiteId;
@@ -110,7 +95,6 @@ export function LNB({
const matchedMenuIds: number[] = [];
let matchedMenuCode: number | null = null;
let matchedMenuHelp: string = "";
const findMatchedMenus = (items: MenuItem[]) => {
items.forEach((item) => {
@@ -127,7 +111,6 @@ export function LNB({
if (matchedMenuCode) return;
if (currentPath === item.urlPath) {
matchedMenuCode = item.menuCd;
matchedMenuHelp = item.menuHlpCn || "";
}
if (item.children) findMatchedMenuCode(item.children);
});
@@ -148,7 +131,6 @@ export function LNB({
if (matchedMenuCode) {
setCurrentMenuCode(matchedMenuCode);
setHelpContent(matchedMenuHelp);
}
// 접속 로그 저장
@@ -298,46 +280,6 @@ export function LNB({
<MenuItem key={item.menuCd} item={item} />
))}
</SidebarMenu>
{/* 도움말 아이콘 */}
<div className="mt-auto pb-4">
<Dialog
open={isHelpModalOpen}
onOpenChange={(open) => (open ? openHelp() : closeHelp())}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="flex max-h-[90vh] flex-col sm:max-w-[85rem]">
<DialogHeader>
<DialogTitle>도움말</DialogTitle>
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto py-4">
{helpContent ? (
<div
style={{
fontSize: "16px",
lineHeight: "1.5",
}}
>
<div
dangerouslySetInnerHTML={{
__html: helpContent
.replace(/<body[^>]*>/gi, "<div>")
.replace(/<\/body>/gi, "</div>"),
}}
style={{
fontSize: "inherit",
}}
/>
</div>
) : (
<p className="text-sm text-muted-foreground">
현재 페이지에 대한 도움말이 없습니다.
</p>
)}
</div>
</DialogContent>
</Dialog>
</div>
</SidebarContent>
</Sidebar>
);
@@ -1,43 +1,95 @@
"use client";
import React from "react";
import { useHelpDialog } from "../../provider/help-dialog-context";
import React, { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../dialog";
import { useQuery } from "@tanstack/react-query";
import { getMenuDetail } from "../../api";
import { ENDPOINT } from "../../config";
const PageTitle = ({
title,
menuCd,
menuPaths,
}: {
title: string;
menuCd?: number;
menuPaths: string[];
}) => {
const { openHelp } = useHelpDialog();
const [isHelpDialogOpen, setIsHelpDialogOpen] = useState(false);
const { data: menuDetail } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, menuCd],
queryFn: () => getMenuDetail(menuCd!),
enabled: !!menuCd,
});
return (
<div className="flex w-full items-center justify-between px-[1.8rem] pt-[2rem]">
<div className="flex items-center gap-3">
<h2 className="text-[2.4rem] font-semibold text-title">{title}</h2>
<>
<div className="flex w-full items-center justify-between px-[1.8rem] pt-[2rem]">
<div className="flex items-center gap-3">
<h2 className="text-[2.4rem] font-semibold text-title">{title}</h2>
</div>
<div className="flex items-center gap-3">
{menuPaths.length > 0 && (
<div className="flex items-center gap-3">
<i className="diveicon di-home-fill text-foreground" />
{menuPaths?.map((path, i) => (
<React.Fragment key={`${i}-${path}`}>
{i !== 0 && (
<i className="diveicon di-arrow-forward text-foreground" />
)}
<span className="text-xl text-foreground">{path}</span>
</React.Fragment>
))}
</div>
)}
<button
onClick={() => setIsHelpDialogOpen(true)}
className="flex cursor-pointer items-center gap-3 transition-opacity hover:opacity-80"
>
<img
src="/images/help.png"
alt="도움말"
className="h-[20px] w-[20px]"
/>
</button>
</div>
</div>
<div className="flex items-center gap-3">
{menuPaths.length > 0 && (
<div className="flex items-center gap-3">
<i className="diveicon di-home-fill text-foreground" />
{menuPaths?.map((path, i) => (
<React.Fragment key={`${i}-${path}`}>
{i !== 0 && (
<i className="diveicon di-arrow-forward text-foreground" />
)}
<span className="text-xl text-foreground">{path}</span>
</React.Fragment>
))}
</div>
)}
<button
onClick={openHelp}
className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
>
<img src="/images/help.png" alt="도움말" className="w-[20px] h-[20px]" />
</button>
</div>
</div>
{/* 도움말 다이얼로그 */}
<Dialog open={isHelpDialogOpen} onOpenChange={setIsHelpDialogOpen}>
<DialogContent className="flex max-h-[90vh] flex-col sm:max-w-[85rem]">
<DialogHeader>
<DialogTitle>도움말</DialogTitle>
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto py-4">
{menuDetail?.menuHlpCn ? (
<div
style={{
fontSize: "16px",
lineHeight: "1.5",
}}
>
<div
dangerouslySetInnerHTML={{
__html: menuDetail.menuHlpCn
.replace(/<body[^>]*>/gi, "<div>")
.replace(/<\/body>/gi, "</div>"),
}}
style={{
fontSize: "inherit",
}}
/>
</div>
) : (
<p className="text-lg text-muted-foreground">
현재 페이지에 대한 도움말이 없습니다.
</p>
)}
</div>
</DialogContent>
</Dialog>
</>
);
};
@@ -18,6 +18,18 @@
.table-ui.default th {
@apply bg-[#5987bf] text-[#fff];
}
.table-ui th.data-table-group-header {
background-image: linear-gradient(
to right,
transparent 0.2rem,
var(--background) 0.2rem,
var(--background) calc(100% - 0.2rem),
transparent calc(100% - 0.2rem)
);
background-position: left bottom;
background-repeat: no-repeat;
background-size: 100% 1px;
}
.table-ui.default td {
@apply border-b border-b-border text-label;
}