Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { ENDPOINT } from "../config";
|
||||
import { getApiClient } from "./client";
|
||||
|
||||
interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: 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;
|
||||
}
|
||||
|
||||
interface NoticeUserListParams extends PageableParams {
|
||||
recvUnqId?: string;
|
||||
senderUnqId?: string;
|
||||
notiType?: string;
|
||||
notiTtlKeyword?: string;
|
||||
notiCnKeyword?: string;
|
||||
notReadYn?: string;
|
||||
delYn?: string;
|
||||
}
|
||||
|
||||
interface NoticeUser {
|
||||
notiId: number;
|
||||
recvUnqId: string;
|
||||
senderUnqId: string;
|
||||
notiType: string;
|
||||
notiTtl: string;
|
||||
notReadYn: string;
|
||||
regDt: string;
|
||||
}
|
||||
|
||||
const client = getApiClient();
|
||||
|
||||
const getNoticeUserList = async (
|
||||
recvUnqId: string,
|
||||
params?: NoticeUserListParams,
|
||||
) => {
|
||||
const response = await client.get<PageableList<NoticeUser>>(
|
||||
`${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/list`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getUnreadNoticeCount = async (recvUnqId: string) => {
|
||||
const response = await client.get<number>(
|
||||
`${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/unread/count`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const readNotice = async (notiId: number) => {
|
||||
const response = await client.put(
|
||||
`${ENDPOINT.CMS_SERVICE.NOTICE}/${notiId}/read`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export { getNoticeUserList, getUnreadNoticeCount, readNotice };
|
||||
@@ -7,6 +7,8 @@ export const ENDPOINT = {
|
||||
POPUPS: "/cms-service/api/v1/popups",
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
CONNECTION_LOG: "/cms-service/api/v1/cntnlog",
|
||||
NOTICE: "/cms-service/api/v1/noti",
|
||||
NOTICE_USER: "/cms-service/api/v1/noti/user",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
USERS: "/user-service/api/v1/users",
|
||||
|
||||
@@ -24,7 +24,7 @@ export function Footer() {
|
||||
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
|
||||
|
||||
return (
|
||||
<footer className="flex w-full shrink-0 items-start border-t border-solid border-border bg-background px-6 py-8">
|
||||
<footer className="flex w-full shrink-0 items-start border-t border-solid border-border bg-background px-6 py-8 text-foreground">
|
||||
<div className="w-1/2">
|
||||
<h6 className="mb-4 text-2xl font-bold">{siteInfo?.siteNm ?? ""}</h6>
|
||||
<p className="my-1 text-xl opacity-60">{siteInfo?.siteAddr ?? ""}</p>
|
||||
@@ -32,10 +32,6 @@ export function Footer() {
|
||||
전화: {siteInfo?.telNo ?? ""} / 팩스: {siteInfo?.faxNumber ?? ""}
|
||||
</p>
|
||||
<p className="my-1 text-xl opacity-60">{siteInfo?.lwndCn ?? ""}</p>
|
||||
<p className="my-1 text-xl opacity-60">
|
||||
본 시스템은 Chrome, Firefox, Safari, Edge 브라우저에 최적화되어
|
||||
있습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-1/2 flex-col">
|
||||
<div className="flex justify-end gap-4">
|
||||
|
||||
@@ -13,16 +13,12 @@ import {
|
||||
getUserInfo,
|
||||
type Popup,
|
||||
} from "../../api";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "../dropdown-menu";
|
||||
import { Button } from "../button";
|
||||
import { SidebarTrigger } from "../sidebar";
|
||||
import { Skeleton } from "../skeleton";
|
||||
import { SidebarTrigger } from "../sidebar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
import { SearchForm } from "./search-form";
|
||||
import { NoticePopover } from "./notice-popover";
|
||||
|
||||
const appConfig = getConfig();
|
||||
const siteId = appConfig.SITE_ID ?? "";
|
||||
@@ -140,30 +136,28 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
<SearchForm />
|
||||
{mounted ? (
|
||||
isLoggedIn && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
|
||||
data-trigger="dropdown"
|
||||
>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label">
|
||||
<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" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
|
||||
<div className="flex flex-col items-start gap-0 px-4 py-3">
|
||||
<p className="text-base text-label">최종접속일</p>
|
||||
<p>{userInfo?.lastLoginDate}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-0 px-4 py-3">
|
||||
<p className="text-base text-label">접속 IP</p>
|
||||
<p></p>
|
||||
</div>
|
||||
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-48 border-border p-1">
|
||||
<ul>
|
||||
<li className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent">
|
||||
<NoticePopover />
|
||||
</li>
|
||||
<li
|
||||
onClick={logout}
|
||||
className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent"
|
||||
>
|
||||
로그아웃
|
||||
</li>
|
||||
</ul>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
) : (
|
||||
<Skeleton className="mx-4 h-[3rem] w-[15rem]" />
|
||||
|
||||
@@ -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<number[]>([]);
|
||||
|
||||
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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex w-full items-center gap-2 text-left">
|
||||
알림 <Badge>{unreadNoticeCount ?? 0}</Badge>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[32rem] border-border px-0">
|
||||
<div className="flex items-center gap-4 p-4 pt-0">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-[1.4rem]">
|
||||
전체 선택
|
||||
</Label>
|
||||
</div>
|
||||
<ul>
|
||||
{noticeUserList?.content?.map((item) => (
|
||||
<li
|
||||
key={item.notiId}
|
||||
className={cn(
|
||||
"flex items-center gap-4 border-t border-border p-4",
|
||||
item.notReadYn === "N" && "bg-primary-transparent",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedNoticeIds.includes(item.notiId)}
|
||||
onCheckedChange={() => handleSelectNotice(item.notiId)}
|
||||
/>
|
||||
<div>
|
||||
<span className="text-[1.2rem]">{item.notiType}</span>
|
||||
<p className="text-[1.4rem]">{item.notiTtl}</p>
|
||||
<span className="text-[1.2rem] opacity-50">
|
||||
{dayjs(item.regDt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div
|
||||
className="border-t border-border pt-4 text-center"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Button onClick={handleReadNotice}>확인</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user