diff --git a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx index b217531dd..65fd0030a 100644 --- a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx @@ -134,7 +134,7 @@ function DashboardPage() { {/* 전체 게시물 카드 */}
-
+

전체 게시물

{data?.pstTotCnt ? data.pstTotCnt.toLocaleString() : "-"} @@ -157,7 +157,7 @@ function DashboardPage() { {/* 오늘 접속자 수 카드 */}
-
+

오늘 접속자 수

{data?.accessTodayCnt @@ -184,7 +184,7 @@ function DashboardPage() { {/* API 호출 건수 카드 */}
-
+

API 호출 건수

{data?.apiTodayCnt ? data.apiTodayCnt.toLocaleString() : "-"} @@ -206,13 +206,13 @@ function DashboardPage() {
{/* 승인 대기 알림 */}
-
+

승인 대기 알림

{/* 사용자 승인 */} -
+
-
+
신규 가입 승인 대기
{/* 매뉴 승인 */} -
+
{(3).toLocaleString()}건
-
+
매뉴 수정 승인 대기
{/* 컨텐츠 승인 */} -
+
-
+
컨텐츠 수정 승인 대기
@@ -318,11 +318,11 @@ function DashboardPage() {
{/* 실시간 알림 */}
-
+

실시간 알림

-
+
신규 사용자 가입 {(1).toLocaleString()}분 전 @@ -344,11 +344,11 @@ function DashboardPage() {
{/* 일정 알림 */}
-
+

일정 알림

-
+
{(3).toLocaleString()}월 정기 시스템 점검 @@ -377,7 +377,7 @@ function DashboardPage() {
{/* 접속자 통계 */}
-
+

접속자 통계

@@ -392,7 +392,7 @@ function DashboardPage() {
{/* 메뉴별 접속 통계 */}
-
+

메뉴별 접속 통계

@@ -412,14 +412,14 @@ function DashboardPage() {
{/* 공지사항 */}
-
+

공지사항

{noticePostTableComponent}
{/* 신규 게시글 */}
-
+

신규 게시글

{recentPostTableComponent} diff --git a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx index 46f3d45f0..1f8de050d 100644 --- a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx +++ b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx @@ -10,12 +10,18 @@ import { Button, Switch, Input, + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, } from "@common/components"; import { RoleAuthorizationsApi, type RoleAuthorizationsData, type RoleAuthorizationsByRoleId, } from "@/entities/role-authorization"; +import { SiteSelect } from "@/entities/site"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useMutationWrapper, useDataTable } from "@/shared/lib"; @@ -107,17 +113,27 @@ function MappingRoleDialog({ - + 모듈 목록
+ + - - - 프로필 설정 -
-

최종접속일

-

{userInfo?.lastLoginDate}

-
-
-

접속 IP

-

-
- 로그아웃 -
- + + +
    +
  • + +
  • +
  • + 로그아웃 +
  • +
+
+ ) ) : ( diff --git a/packages/common/components/src/ui/header/notice-popover.tsx b/packages/common/components/src/ui/header/notice-popover.tsx new file mode 100644 index 000000000..6ee35371b --- /dev/null +++ b/packages/common/components/src/ui/header/notice-popover.tsx @@ -0,0 +1,141 @@ +import { useEffect, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { getConfig } from "@common/config"; +import { ENDPOINT } from "../../config"; +import { cn, getCookie } from "../../lib"; +import { + getNoticeUserList, + getUnreadNoticeCount, + readNotice, +} from "../../api/notice-user"; +import { Label } from "../label"; +import { Badge } from "../badge"; +import { Button } from "../button"; +import { Checkbox } from "../checkbox"; +import { Popover, PopoverContent, PopoverTrigger } from "../popover"; + +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import "dayjs/locale/ko"; + +dayjs.extend(relativeTime); +dayjs.locale("ko"); + +const appConfig = getConfig(); +const userId = getCookie(appConfig.AUTH_USER_ID ?? ""); + +export function NoticePopover() { + const queryClient = useQueryClient(); + + const [open, setOpen] = useState(false); + const [selectedNoticeIds, setSelectedNoticeIds] = useState([]); + + const { data: noticeUserList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER], + queryFn: () => getNoticeUserList(userId!), + enabled: !!userId, + }); + + const { data: unreadNoticeCount } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.NOTICE], + queryFn: () => getUnreadNoticeCount(userId!), + enabled: !!userId, + }); + + const handleSelectNotice = (notiId: number) => { + setSelectedNoticeIds((prevIds) => { + if (prevIds.includes(notiId)) { + return prevIds.filter((id) => id !== notiId); + } else { + return [...prevIds, notiId]; + } + }); + }; + + const handleSelectAll = () => { + if (noticeUserList?.content) { + if (selectedNoticeIds.length === noticeUserList.content.length) { + setSelectedNoticeIds([]); + } else { + const allIds = noticeUserList.content.map((item) => item.notiId); + setSelectedNoticeIds(allIds); + } + } + }; + + const handleReadNotice = async () => { + if (selectedNoticeIds.length === 0) return; + try { + await Promise.all(selectedNoticeIds.map((notiId) => readNotice(notiId))); + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER], + }); + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.NOTICE], + }); + setSelectedNoticeIds([]); + } catch (error) { + console.error("알림 읽음 처리 중 오류 발생:", error); + } + }; + + const isAllSelected = noticeUserList?.content + ? selectedNoticeIds.length === noticeUserList.content.length && + noticeUserList.content.length > 0 + : false; + + useEffect(() => { + if (open) setSelectedNoticeIds([]); + }, [open]); + + return ( + + +
+ 알림 {unreadNoticeCount ?? 0} +
+
+ +
+ + +
+
    + {noticeUserList?.content?.map((item) => ( +
  • + handleSelectNotice(item.notiId)} + /> +
    + {item.notiType} +

    {item.notiTtl}

    + + {dayjs(item.regDt).fromNow()} + +
    +
  • + ))} +
+
setOpen(false)} + > + +
+
+
+ ); +} diff --git a/packages/common/components/src/ui/layout/index.ts b/packages/common/components/src/ui/layout/index.ts index 30c069b90..100f53453 100644 --- a/packages/common/components/src/ui/layout/index.ts +++ b/packages/common/components/src/ui/layout/index.ts @@ -1,2 +1,3 @@ export { BaseLayout } from "./base"; export { VBaseLayout } from "./vbase"; +export { NoFooterLayout } from "./nofooter"; \ No newline at end of file diff --git a/packages/common/components/src/ui/layout/nofooter.tsx b/packages/common/components/src/ui/layout/nofooter.tsx new file mode 100644 index 000000000..2f398ec57 --- /dev/null +++ b/packages/common/components/src/ui/layout/nofooter.tsx @@ -0,0 +1,44 @@ +import type { QueryClient } from "@tanstack/react-query"; +import { QueryProvider } from "../../provider/query-provider"; +import { PopupProvider } from "../../provider/popup-provider"; +import { ClientIpGuard } from "../../lib/client-ip-guard"; +import type { MenuItem } from "../../api"; +import { SidebarProvider } from "../sidebar"; +import { LNB } from "../lnb"; +import { Header } from "../header"; +import "./common.css"; + +export function NoFooterLayout({ + children, + queryClient, + currentPath, + handleMenuClick, +}: { + children: React.ReactNode; + queryClient?: QueryClient; + currentPath?: string; + handleMenuClick?: (item: MenuItem) => void; +}) { + return ( + + + + +
+
+
+ +
+
{children}
+
+
+
+
+
+
+
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e17b5d50..76097f586 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -650,6 +650,9 @@ importers: react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@18.3.1) + react-hot-toast: + specifier: ^2.5.2 + version: 2.5.2(react-dom@18.3.1)(react@18.3.1) tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) @@ -749,7 +752,7 @@ importers: version: 2.5.2(react-dom@18.3.1)(react@18.3.1) react-i18next: specifier: ^15.6.0 - version: 15.6.0(i18next@25.3.2)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3) + version: 15.6.1(i18next@25.3.2)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3) tailwind-merge: specifier: ^3.0.1 version: 3.0.2 @@ -834,7 +837,7 @@ importers: version: 3.6.0 libphonenumber-js: specifier: ^1.12.9 - version: 1.12.9 + version: 1.12.10 lucide-react: specifier: ^0.474.0 version: 0.474.0(react@18.3.1) @@ -1388,8 +1391,8 @@ packages: dependencies: regenerator-runtime: 0.14.1 - /@babel/runtime@7.27.6: - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + /@babel/runtime@7.28.2: + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} dev: false @@ -4484,7 +4487,7 @@ packages: dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.1 - jiti: 2.4.2 + jiti: 2.5.1 lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 @@ -7345,7 +7348,7 @@ packages: typescript: optional: true dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 typescript: 5.6.3 dev: false @@ -7690,8 +7693,8 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - /jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + /jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true dev: false @@ -7817,8 +7820,8 @@ packages: type-check: 0.4.0 dev: true - /libphonenumber-js@1.12.9: - resolution: {integrity: sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==} + /libphonenumber-js@1.12.10: + resolution: {integrity: sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==} dev: false /lie@3.3.0: @@ -9092,8 +9095,8 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false - /react-i18next@15.6.0(i18next@25.3.2)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3): - resolution: {integrity: sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==} + /react-i18next@15.6.1(i18next@25.3.2)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3): + resolution: {integrity: sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==} peerDependencies: i18next: '>= 23.2.3' react: ^18.3.1 @@ -9108,7 +9111,7 @@ packages: typescript: optional: true dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 html-parse-stringify: 3.0.1 i18next: 25.3.2(typescript@5.6.3) react: 18.3.1