Merge remote-tracking branch 'origin/iipbs_cs' into iipbs_cs
This commit is contained in:
@@ -110,15 +110,15 @@ const nextConfig: NextConfig = {
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/pst(.*)",
|
||||
value: "(.*)/cms(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/pst/:path*",
|
||||
destination: `${subDomain}/pst/:path*`,
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
@@ -127,6 +127,7 @@ const nextConfig: NextConfig = {
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
```
|
||||
|
||||
### 6. globals.css(또는 전역 css) 수정
|
||||
@@ -150,6 +151,26 @@ rm -rf node_modules pnpm-lock.yaml
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 8. 로그아웃 처리 가이드
|
||||
|
||||
* 참고 파일
|
||||
- apps/cs/app/api/auth/logout/route.ts
|
||||
- packages/common/components/src/ui/header/index.tsx (55-65 line)
|
||||
|
||||
각 서비스 폴더에 ‘/app/api/auth/logout/route.ts’ 생성해야 합니다(코드는 cs에서 확인).
|
||||
|
||||
** 로그아웃 처리되면 ‘/auth/login’ 페이지로 이동됩니다.
|
||||
|
||||
### 9. 'SITE_ID’ 환경변수를 쿠키로 대체하는 가이드(공통 모듈 사용하기위한 필수 사항)
|
||||
|
||||
* 참고 파일: apps/cs/src/shared/api/client.ts (쿠키 저장 처리)
|
||||
apps/cs/src/shared/lib/cookie.ts (쿠키 공통 모듈)
|
||||
|
||||
`SITE_ID` 환경변수를 처음 사용하는 파일 또는 최상단 클라이언트 컴포넌트에서 “site-id-key” 쿠키에 `SITE_ID` 저장
|
||||
|
||||
|
||||
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- React(^18)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { MenuApi } from "@/entities/menu";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { getCookie, useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
|
||||
export default function PostLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const menuCd = getCookie("menu-code-key");
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, menuCd],
|
||||
queryFn: () => MenuApi.getMenuInfo(menuCd!),
|
||||
enabled: !!menuCd,
|
||||
});
|
||||
|
||||
const menuPaths = data?.menuPath?.split(">").map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<PageLayout title={bbsInfo?.bbsNm ?? ""} menuPaths={menuPaths ?? []}>
|
||||
{children}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import type { MenuRole } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
|
||||
export default function WithBaseLayout({
|
||||
@@ -17,7 +17,7 @@ export default function WithBaseLayout({
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
const handleMenuClick = (item: MenuRole) => {
|
||||
setCurrentMenuInfo(item);
|
||||
router.push(item.urlPath);
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
|
||||
export default function PostLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={bbsInfo?.bbsNm ?? ""}
|
||||
menuPaths={["서비스관리", "게시판관리"]}
|
||||
>
|
||||
{children}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
REFRESH_TOKEN,
|
||||
} from "@/shared/config";
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(ACCESS_TOKEN);
|
||||
cookieStore.delete(REFRESH_TOKEN);
|
||||
cookieStore.delete(AUTH_USER_ID);
|
||||
cookieStore.delete(CLAIM_NAME);
|
||||
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
CLAIM_NAME,
|
||||
REFRESH_TOKEN,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "@/shared/config";
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -15,9 +14,10 @@ async function handler(
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
) {
|
||||
const cookieStore = await cookies();
|
||||
const siteId = cookieStore.get("site-id-key")?.value ?? "";
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
"X-Site-Id": siteId,
|
||||
};
|
||||
let body = {};
|
||||
const isRefreshHandler = params.url.includes("refresh");
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
const nextConfig: NextConfig = {
|
||||
async rewrites() {
|
||||
return {
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/cms/:path*",
|
||||
destination: "/:path*",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { MenuAuthorityApi } from "./api/menu-authority-service";
|
||||
export { type PageMenuAuthority } from "./model/menu-authority-interface";
|
||||
export { AuthorityButton } from "./ui/authority-button";
|
||||
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { Button, cn, type ButtonProps } from "@common/components";
|
||||
import type { PageMenuAuthority } from "../model/menu-authority-interface";
|
||||
|
||||
type AuthorityButtonProps = {
|
||||
authority: keyof PageMenuAuthority;
|
||||
} & ButtonProps;
|
||||
|
||||
function AuthorityButton({
|
||||
authority,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AuthorityButtonProps) {
|
||||
const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority);
|
||||
|
||||
const isAuthority = pageMenuAuthority?.[authority] === "Y";
|
||||
|
||||
return (
|
||||
<Button className={cn(className, !isAuthority && "hidden")} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { AuthorityButton };
|
||||
@@ -1,14 +1,21 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { MenuItem, MenuService } from "../model/menu-interface";
|
||||
import type { MenuInfo, MenuRole, MenuService } from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
async getMenuRoles(siteId: string): Promise<MenuItem[]> {
|
||||
const response = await client.get<MenuItem[]>(
|
||||
async getMenuRoles(siteId: string): Promise<MenuRole[]> {
|
||||
const response = await client.get<MenuRole[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getMenuInfo(menuCd: string): Promise<MenuInfo> {
|
||||
const response = await client.get<MenuInfo>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { MenuApi } from "./api/menu-service";
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
export type { MenuItem } from "./model/menu-interface";
|
||||
export type { MenuRole } from "./model/menu-interface";
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu";
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import { getCurrentMenu } from "./utils";
|
||||
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -17,7 +18,7 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
useMenuStore((state) => state);
|
||||
|
||||
const getCurrentMenuInfo = async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
const menuRoles = await MenuApi.getMenuRoles(siteId);
|
||||
if (!menuRoles) return;
|
||||
return getCurrentMenu(menuRoles, pathname ?? "");
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MenuItem } from "../model/menu-interface";
|
||||
import type { MenuRole } from "../model/menu-interface";
|
||||
|
||||
export const getCurrentMenu = (
|
||||
menus: MenuItem[],
|
||||
menus: MenuRole[],
|
||||
pathname: string,
|
||||
): MenuItem | null => {
|
||||
): MenuRole | null => {
|
||||
for (const menu of menus) {
|
||||
if (
|
||||
menu.urlPath !== "" &&
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
export interface MenuItem {
|
||||
interface Menu {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
@@ -15,9 +12,30 @@ export interface MenuItem {
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
}
|
||||
|
||||
export interface MenuRole extends Menu {
|
||||
parentId: number | null;
|
||||
iconNm?: string;
|
||||
children: MenuRole[] | null;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
export interface MenuService {
|
||||
getMenuRoles(siteId: string): Promise<MenuItem[]>;
|
||||
export interface MenuInfo extends Menu {
|
||||
siteId: string;
|
||||
menuSubKornNm: string;
|
||||
menuSubEngNm: string;
|
||||
iconName: string;
|
||||
menuLnkgTypeCdNm: string;
|
||||
prntMenuCd: number;
|
||||
evlUseYn: string;
|
||||
menuLnkgSnNm: string;
|
||||
menuPicUserUnqId: string;
|
||||
menuPicUserUnqIdNm: string;
|
||||
menuPicDsgnYmd: string;
|
||||
}
|
||||
|
||||
export interface MenuService {
|
||||
getMenuRoles(siteId: string): Promise<MenuRole[]>;
|
||||
getMenuInfo(menuCd: string): Promise<MenuInfo>;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@ export class PostApiService implements PostService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPostEditInfo(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
): Promise<PostInfo> {
|
||||
const response = await client.get<PostInfo>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/edit/${bbsId}/${pstNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createPost(data: PostFormData): Promise<Post> {
|
||||
const validatedData = postFormSchema.parse(data);
|
||||
const response = await client.post<Post>(
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PostParams extends PageableParams {
|
||||
prdPstgYn?: string;
|
||||
siteId?: string;
|
||||
bbsTypeCd?: string;
|
||||
ansUseYn?: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
@@ -77,6 +78,7 @@ export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
|
||||
export interface PostService {
|
||||
getPostList(bbsId: number, params: PostParams): Promise<PostList>;
|
||||
getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
getPostEditInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
createPost(data: PostFormData): Promise<Post>;
|
||||
updatePost(
|
||||
bbsId: number,
|
||||
|
||||
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
|
||||
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
|
||||
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
|
||||
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
|
||||
@@ -8,6 +8,12 @@ export const columns: ColumnDef<Post>[] = [
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
|
||||
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Client } from "@common/network";
|
||||
import { BASE_URL } from "../config/api";
|
||||
import { SITE_ID } from "../config/env";
|
||||
import { getCookie } from "../lib";
|
||||
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
export const client = Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
siteId: SITE_ID,
|
||||
siteId,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "13000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
|
||||
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN =
|
||||
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
|
||||
|
||||
@@ -30,5 +30,5 @@ export interface PageableList<T> {
|
||||
export interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
cell: ({ row }) => {
|
||||
cell: ({ row, table }) => {
|
||||
if (row.original.isUpndFixPost) {
|
||||
return (
|
||||
<Badge
|
||||
@@ -40,19 +40,23 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return row.original.rowNo;
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
cell: ({ row }) => {
|
||||
const { ansPstn, pstTtl, inqNocs, cmntCnt } = row.original;
|
||||
const { ansPstn = 0, pstTtl, inqNocs, cmntCnt } = row.original;
|
||||
const indentPadding = ansPstn > 0 ? (ansPstn - 1) * 20 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-w-[20vw] items-center gap-2 text-left"
|
||||
style={{ paddingInlineStart: `${(ansPstn - 1) * 20}px` }}
|
||||
style={{ paddingInlineStart: `${indentPadding}px` }}
|
||||
>
|
||||
{ansPstn > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { useMenuStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
type PostActionProps = {
|
||||
bbsId: string;
|
||||
pstNo: string;
|
||||
registerId?: string;
|
||||
children: React.ReactNode;
|
||||
handleDelete: () => Promise<void>;
|
||||
};
|
||||
|
||||
function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
function PostAction({ registerId, children, handleDelete }: PostActionProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { confirm } = useConfirm();
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const actionButtonAuthrt =
|
||||
userInfo?.adminRoleYn === "Y" || userInfo?.userUnqId === registerId;
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) handleDelete();
|
||||
};
|
||||
@@ -46,15 +49,19 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => router.push(`${pathname}/modify`)}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
{actionButtonAuthrt && (
|
||||
<AuthorityButton
|
||||
authority="mdfcnAuthrtYn"
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => router.push(`${pathname}/modify`)}
|
||||
>
|
||||
수정
|
||||
</AuthorityButton>
|
||||
)}
|
||||
{bbsInfo?.ansUseYn === "Y" && (
|
||||
<Button
|
||||
<AuthorityButton
|
||||
authority="regAuthrtYn"
|
||||
size="lg"
|
||||
type="button"
|
||||
color="gray"
|
||||
@@ -62,11 +69,19 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
disabled={!checkAnsUseAuthrt()}
|
||||
>
|
||||
답글
|
||||
</Button>
|
||||
</AuthorityButton>
|
||||
)}
|
||||
{actionButtonAuthrt && (
|
||||
<AuthorityButton
|
||||
authority="delAuthrtYn"
|
||||
size="lg"
|
||||
color="red"
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
>
|
||||
삭제
|
||||
</AuthorityButton>
|
||||
)}
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -42,6 +42,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state);
|
||||
const thumbRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
const [tableSort, setTableSort] = useState("");
|
||||
const [postParams, setPostParams] = useState<PostParams>({
|
||||
keywordType: "title",
|
||||
keyword: "",
|
||||
@@ -77,12 +78,14 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pagination],
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pagination, tableSort],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
ansUseYn: bbsInfo?.ansUseYn,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: tableSort,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -200,6 +203,24 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={tableSort}
|
||||
onValueChange={(value) => {
|
||||
setTableSort(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[8rem]">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pstTtl,asc">제목↑</SelectItem>
|
||||
<SelectItem value="pstTtl,desc">제목↓</SelectItem>
|
||||
<SelectItem value="rgtrNm,asc">작성자↑</SelectItem>
|
||||
<SelectItem value="rgtrNm,desc">작성자↓</SelectItem>
|
||||
<SelectItem value="regDt,asc">등록일↑</SelectItem>
|
||||
<SelectItem value="regDt,desc">등록일↓</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostAction bbsId={bbsId} pstNo={pstNo} handleDelete={handleDelete}>
|
||||
<PostAction registerId={postData?.rgtr} handleDelete={handleDelete}>
|
||||
<div className="card card-border !p-8">
|
||||
<h3 className="text-[2rem] font-bold text-foreground">
|
||||
{postData?.pstTtl}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { ApiLogPage } from "@/pages/apilog";
|
||||
export default ApiLogPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { PrivacyLogPage } from "@/pages/privacylog";
|
||||
export default PrivacyLogPage;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
REFRESH_TOKEN,
|
||||
} from "@/shared/config";
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(ACCESS_TOKEN);
|
||||
cookieStore.delete(REFRESH_TOKEN);
|
||||
cookieStore.delete(AUTH_USER_ID);
|
||||
cookieStore.delete(CLAIM_NAME);
|
||||
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const ipAddress =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0] || "unknown";
|
||||
|
||||
const ipv4Regex = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
|
||||
const match = ipAddress.match(ipv4Regex);
|
||||
|
||||
const normalizedIp = match ? match[1] : ipAddress;
|
||||
|
||||
return NextResponse.json({
|
||||
originalIp: ipAddress,
|
||||
normalizedIp: normalizedIp,
|
||||
});
|
||||
}
|
||||
@@ -7,11 +7,16 @@ const serverApiUrl =
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const queryParams = searchParams.toString();
|
||||
|
||||
const decodedParams = Array.from(searchParams.entries())
|
||||
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
decodedParams ? `?${decodedParams}` : ""
|
||||
}`;
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
@@ -15,15 +15,15 @@ const nextConfig: NextConfig = {
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/pst(.*)",
|
||||
value: "(.*)/cms(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/pst/:path*",
|
||||
destination: `${subDomain}/pst/:path*`,
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
ApiLogList,
|
||||
ApiLogParams,
|
||||
ApiLogService,
|
||||
} from "../model/apilog-interface";
|
||||
|
||||
export class ApiLogApiService implements ApiLogService {
|
||||
public async getApiLogList(params: ApiLogParams): Promise<ApiLogList> {
|
||||
const response = await client.get<ApiLogList>(
|
||||
`${ENDPOINT.USER_SERVICE.APILOG}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ApiLogApi = new ApiLogApiService();
|
||||
@@ -0,0 +1,6 @@
|
||||
export { ApiLogApi } from "./api/apilog-service";
|
||||
export type {
|
||||
ApiLog,
|
||||
ApiLogParams,
|
||||
} from "./model/apilog-interface";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface ApiLogParams extends PageableParams {
|
||||
siteId?: string;
|
||||
userId?: string;
|
||||
userNm?: string;
|
||||
apiHttpPmhd?: string;
|
||||
dmndUrl?: string;
|
||||
ipAddr?: string;
|
||||
dmndDtStart?: string;
|
||||
dmndDtEnd?: string;
|
||||
}
|
||||
|
||||
export interface ApiLog {
|
||||
logId: string;
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
userId: string;
|
||||
userNm: string;
|
||||
apiHttpPmhd: string;
|
||||
dmndUrl: string;
|
||||
ipAddr: string;
|
||||
dmndDt: string;
|
||||
}
|
||||
|
||||
export type ApiLogList = PageableList<ApiLog>;
|
||||
|
||||
export interface ApiLogService {
|
||||
getApiLogList: (params: ApiLogParams) => Promise<ApiLogList>;
|
||||
}
|
||||
@@ -56,6 +56,7 @@ export interface Board {
|
||||
popupUseYn: string;
|
||||
delYn: string;
|
||||
useYn: string;
|
||||
prvcIndctYn: string;
|
||||
mdfcnDt: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
@@ -81,6 +82,7 @@ export const boardFormSchema = z.object({
|
||||
pstgPrdUseYn: z.string(),
|
||||
popupUseYn: z.string(),
|
||||
useYn: z.string(),
|
||||
prvcIndctYn: z.string(),
|
||||
});
|
||||
|
||||
export type BoardList = PageableList<Board>;
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface GroupUserListResponseDto {
|
||||
mode: string;
|
||||
groupId: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
ogdpSeCd: string;
|
||||
ogdpSeNm: string;
|
||||
|
||||
@@ -7,4 +7,3 @@ export {
|
||||
type IpList,
|
||||
type IP,
|
||||
} from "./model/ip-interface";
|
||||
export { ClientIpGuard } from "./lib/client-ip-guard";
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { SiteApi } from "@/entities/site/@x/ip";
|
||||
import { ENDPOINT, SITE_ID } from "@/shared/config";
|
||||
import { IpApi } from "../api/ip-service";
|
||||
|
||||
type ClientIpResponse = {
|
||||
originalIp: string;
|
||||
normalizedIp: string;
|
||||
};
|
||||
|
||||
const ClientIpGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [isAllowed, setIsAllowed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch("/api/getClientIp");
|
||||
const { normalizedIp } = (await response.json()) as ClientIpResponse;
|
||||
|
||||
if (!normalizedIp || !SITE_ID) return;
|
||||
|
||||
const siteInfo = await queryClient.ensureQueryData({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID],
|
||||
queryFn: () => SiteApi.siteDetail(SITE_ID),
|
||||
});
|
||||
|
||||
if (siteInfo.siteKndCd === "O") return;
|
||||
|
||||
const { content: ipList } = await IpApi.getIpList({
|
||||
siteId: SITE_ID,
|
||||
prmYn: "Y",
|
||||
});
|
||||
|
||||
const allowedClientIp = ipList.some((ip) => ip.ipAddr === normalizedIp);
|
||||
setIsAllowed(allowedClientIp);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (!isAllowed) {
|
||||
alert("허용되지 않은 IP로 접근하였습니다.");
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { ClientIpGuard };
|
||||
@@ -1,7 +1,6 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
LoginlogInfo,
|
||||
LoginlogList,
|
||||
LoginlogParams,
|
||||
LoginlogService,
|
||||
@@ -16,12 +15,6 @@ export class LoginlogApiService implements LoginlogService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLoginlogInfo(lgnSn: string): Promise<LoginlogInfo> {
|
||||
const response = await client.get<LoginlogInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.LOGINLOG}/${lgnSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const LoginlogApi = new LoginlogApiService();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { LoginlogApiService } from "./api/loginlog-service";
|
||||
export { LoginlogApi } from "./api/loginlog-service";
|
||||
export type {
|
||||
Loginlog,
|
||||
LoginlogParams,
|
||||
} from "./model/loginlog-interface";
|
||||
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface LoginlogParams extends PageableParams {
|
||||
lgnSn?: string;
|
||||
userId?: string;
|
||||
siteId?: string;
|
||||
userId?: string;
|
||||
errOcrnYn?: string;
|
||||
remoteIp?: string;
|
||||
lgnMthd?: string;
|
||||
lgnErrCd?: string;
|
||||
searchDateFrom?: string;
|
||||
searchDateTo?: string;
|
||||
}
|
||||
|
||||
export interface Loginlog {
|
||||
lgnSn: string;
|
||||
lgnDt: string;
|
||||
userId: string;
|
||||
lgnIp: string;
|
||||
lgnMethodCd: string;
|
||||
errOcrnYn: string;
|
||||
lgnErrCd: string;
|
||||
id: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
export interface LoginlogInfo {
|
||||
lgnSn: string;
|
||||
lgnDt: string;
|
||||
userId: string;
|
||||
lgnIp: string;
|
||||
lgnMethodCd: string;
|
||||
errOcrnYn: string;
|
||||
remoteIp: string;
|
||||
lgnMthd: string;
|
||||
lgnErrCd: string;
|
||||
siteId: string;
|
||||
lgnDt: string;
|
||||
}
|
||||
|
||||
export type LoginlogList = PageableList<Loginlog>;
|
||||
|
||||
export interface LoginlogService {
|
||||
getLoginlogList: (params: LoginlogParams) => Promise<LoginlogList>;
|
||||
getLoginlogInfo: (lgnSn: string) => Promise<LoginlogInfo>;
|
||||
|
||||
getLoginlogList: (params: LoginlogParams) => Promise<LoginlogList>;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const response = await getPageMenuAuthority();
|
||||
if (response?.length) setPageMenuAuthority(response[0]!);
|
||||
})();
|
||||
}, []);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageMenuAuthority) return;
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PostParams extends PageableParams {
|
||||
prdPstgYn?: string;
|
||||
siteId?: string;
|
||||
bbsTypeCd?: string;
|
||||
ansUseYn?: string;
|
||||
}
|
||||
|
||||
export interface RecentPostParams extends PageableParams {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
PrivacyLogList,
|
||||
PrivacyLogParams,
|
||||
PrivacyLogService,
|
||||
} from "../model/privacylog-interface";
|
||||
|
||||
export class PrivacyLogPrivacyService implements PrivacyLogService {
|
||||
public async getPrivacyLogList(params: PrivacyLogParams): Promise<PrivacyLogList> {
|
||||
const response = await client.get<PrivacyLogList>(
|
||||
`${ENDPOINT.USER_SERVICE.PRIVACYLOG}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const PrivacyLogPrivacy = new PrivacyLogPrivacyService();
|
||||
@@ -0,0 +1,6 @@
|
||||
export { PrivacyLogPrivacy } from "./api/privacylog-service";
|
||||
export type {
|
||||
PrivacyLog,
|
||||
PrivacyLogParams,
|
||||
} from "./model/privacylog-interface";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface PrivacyLogParams extends PageableParams {
|
||||
siteId?: string;
|
||||
rqstrIp?: string;
|
||||
inqInfoKeywordType?: string;
|
||||
inqInfoKeyword?: string;
|
||||
inqDtStart?: string;
|
||||
inqDtEnd?: string;
|
||||
}
|
||||
|
||||
export interface PrivacyLog {
|
||||
inqSn: number;
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
inqDt: string;
|
||||
rqstrIp: string;
|
||||
rqstrId: string;
|
||||
rqstrNm: string;
|
||||
inqInfoSummary: string;
|
||||
}
|
||||
|
||||
export type PrivacyLogList = PageableList<PrivacyLog>;
|
||||
|
||||
export interface PrivacyLogService {
|
||||
getPrivacyLogList: (params: PrivacyLogParams) => Promise<PrivacyLogList>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ApiLogPage } from "./ui/apilog-page";
|
||||
@@ -0,0 +1,59 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { ApiLog } from "@/entities/apilog";
|
||||
|
||||
|
||||
export const columns: ColumnDef<ApiLog>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const { pageIndex, pageSize } = table.getState().pagination;
|
||||
return totalRows - (pageIndex * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "dmndDt",
|
||||
header: "요청일시",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.dmndDt);
|
||||
return date.format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "ipAddr",
|
||||
header: "요청IP",
|
||||
},
|
||||
{
|
||||
accessorKey: "apiHttpPmhd",
|
||||
header: "요청방식",
|
||||
cell: ({ row }) => {
|
||||
const val = row.original.apiHttpPmhd;
|
||||
if (val === "POST") {
|
||||
return "저장";
|
||||
} else if (val === "PUT") {
|
||||
return "수정";
|
||||
} else if (val === "DELETE") {
|
||||
return "삭제";
|
||||
} else if (val === "GET") {
|
||||
return "조회";
|
||||
} else {
|
||||
return "기타";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "dmndUrl",
|
||||
header: "요청URL",
|
||||
},
|
||||
{
|
||||
accessorKey: "siteId",
|
||||
header: "사이트ID",
|
||||
}
|
||||
];
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { ApiLogApi } from "@/entities/apilog";
|
||||
import { type ApiLogParams } from "@/entities/apilog/model/apilog-interface";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/apilog/model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
function ApiLogPage({}) {
|
||||
// 1주일 전부터 오늘까지 기본 날짜 설정
|
||||
const defaultDateRange: DateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const [loginlogPostQuery, setLoginlogPostQuery] = useState<ApiLogParams>({
|
||||
userId: "",
|
||||
siteId: "",
|
||||
apiHttpPmhd: "",
|
||||
dmndUrl: "",
|
||||
ipAddr: "",
|
||||
dmndDtStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
dmndDtEnd: dayjs().format("YYYY-MM-DD"),
|
||||
sort: "lgnDt,asc",
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, pagination],
|
||||
queryFn: () =>
|
||||
ApiLogApi.getApiLogList({
|
||||
...loginlogPostQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleLoginlogParamsChange =
|
||||
(key: keyof ApiLogParams) => (value: unknown) => {
|
||||
setLoginlogPostQuery({ ...loginlogPostQuery, [key]: value });
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
switch (value) {
|
||||
case "1W":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "1M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(30, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "3M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(90, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
handleDateRangeChange(presetDateRange);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (value: DateRange | undefined) => {
|
||||
setDateRange(value || defaultDateRange);
|
||||
setLoginlogPostQuery({
|
||||
...loginlogPostQuery,
|
||||
dmndDtStart:
|
||||
dayjs(value?.from).format("YYYY-MM-DD") ??
|
||||
dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
dmndDtEnd:
|
||||
dayjs(value?.to).format("YYYY-MM-DD") ?? dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={handleDateRangeChange}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={loginlogPostQuery.userId}
|
||||
onChange={(e) =>
|
||||
handleLoginlogParamsChange("userId")(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||
<Select onValueChange={handleLoginlogParamsChange("apiHttpPmhd")}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">조회</SelectItem>
|
||||
<SelectItem value="POST">저장</SelectItem>
|
||||
<SelectItem value="PUT">수정</SelectItem>
|
||||
<SelectItem value="DELETE">삭제</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">로그인로그 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(parseInt(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
{pageSize}개씩 보기
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ApiLogPage };
|
||||
@@ -478,6 +478,35 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>개인정보 표시 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="prvcIndctYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
disabled={disabledForm}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="prvcIndctN" />
|
||||
<Label htmlFor="prvcIndctN">표시함</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="prvcIndctY" />
|
||||
<Label htmlFor="prvcIndctY">표시안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
|
||||
@@ -38,6 +38,7 @@ function CreatePage() {
|
||||
pstgPrdUseYn: "Y",
|
||||
popupUseYn: "Y",
|
||||
useYn: "Y",
|
||||
prvcIndctYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const columns: ColumnDef<GroupUserListResponseDto>[] = [
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupId",
|
||||
accessorKey: "userId",
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,7 +26,10 @@ function ImagePage() {
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({ keyword: "" });
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({
|
||||
keyword: "" ,
|
||||
sort:"regDt,desc"
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
|
||||
@@ -18,7 +18,7 @@ export const columns: ColumnDef<Loginlog>[] = [
|
||||
header: "등록일",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.lgnDt);
|
||||
return date.format("YYYY-MM-DD HH24:mi:ss");
|
||||
return date.format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -26,24 +26,24 @@ export const columns: ColumnDef<Loginlog>[] = [
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "로그인IP",
|
||||
header: "remoteIp",
|
||||
accessorKey: "remoteIp",
|
||||
header: "로그인IP",
|
||||
},
|
||||
{
|
||||
accessorKey: "lgnMthd",
|
||||
header: "접속방식",
|
||||
},
|
||||
{
|
||||
accessorKey: "오류여부",
|
||||
header: "errOcrnYn",
|
||||
accessorKey: "errOcrnYn",
|
||||
header: "오류여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "오류코드",
|
||||
header: "lgnErrCd",
|
||||
accessorKey: "lgnErrCd",
|
||||
header: "오류코드",
|
||||
},
|
||||
{
|
||||
accessorKey: "사이트ID",
|
||||
header: "siteId",
|
||||
accessorKey: "siteId",
|
||||
header: "사이트ID",
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,88 +1,227 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { LoginlogApi} from "../../../entities/loginlog/api/loginlog-service";
|
||||
import { type LoginlogParams} from "../../../entities/loginlog/model/loginlog-interface";
|
||||
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
LoadingWithImage,
|
||||
} from "@common/components";
|
||||
import { LoginlogApi } from "@/entities/loginlog";
|
||||
import { type LoginlogParams } from "@/entities/loginlog/model/loginlog-interface";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||
|
||||
type LoginlogPageProps = {
|
||||
lgnSn?: string;
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
function LoginlogPage({ }: LoginlogPageProps) {
|
||||
const [loginlogQuery, setLoginlogQuery] = useState<LoginlogParams>({
|
||||
userId: "",
|
||||
siteId: "",
|
||||
});
|
||||
function LoginlogPage({}) {
|
||||
// 1주일 전부터 오늘까지 기본 날짜 설정
|
||||
const defaultDateRange: DateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [isManualLoading, setIsManualLoading] = useState(false);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: loginlogList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, , pagination],
|
||||
queryFn: () =>
|
||||
LoginlogApi.getLoginlogList({
|
||||
...loginlogQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
const [loginlogPostQuery, setLoginlogPostQuery] = useState<LoginlogParams>({
|
||||
userId: "",
|
||||
siteId: "",
|
||||
errOcrnYn: "N",
|
||||
remoteIp: "",
|
||||
lgnMthd: "",
|
||||
lgnErrCd: "",
|
||||
searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||
sort: "lgnDt,desc",
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: loginlogList?.content ?? [],
|
||||
columns,
|
||||
rowCount: loginlogList?.totalElements ?? 0,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, pagination],
|
||||
queryFn: () =>
|
||||
LoginlogApi.getLoginlogList({
|
||||
...loginlogPostQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
setIsManualLoading(true);
|
||||
query.refetch().finally(() => {
|
||||
setIsManualLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLoginlogParamsChange =
|
||||
(key: keyof LoginlogParams) => (value: unknown) => {
|
||||
setLoginlogPostQuery({ ...loginlogPostQuery, [key]: value });
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
switch (value) {
|
||||
case "1W":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "1M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(30, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "3M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(90, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
handleDateRangeChange(presetDateRange);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (value: DateRange | undefined) => {
|
||||
setDateRange(value || defaultDateRange);
|
||||
setLoginlogPostQuery({
|
||||
...loginlogPostQuery,
|
||||
searchDateFrom:
|
||||
dayjs(value?.from).format("YYYY-MM-DD") ??
|
||||
dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
searchDateTo:
|
||||
dayjs(value?.to).format("YYYY-MM-DD") ?? dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
로그인로그
|
||||
</Label>
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={handleDateRangeChange}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={loginlogQuery.userId}
|
||||
value={loginlogPostQuery.userId}
|
||||
onChange={(e) =>
|
||||
setLoginlogQuery({
|
||||
...loginlogQuery,
|
||||
userId: e.target.value,
|
||||
})
|
||||
handleLoginlogParamsChange("userId")(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||
<Select onValueChange={handleLoginlogParamsChange("errOcrnYn")}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="N">정상</SelectItem>
|
||||
<SelectItem value="Y">오류</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
className="h-[3.2rem]"
|
||||
type="submit"
|
||||
disabled={query.isLoading || query.isPending || isManualLoading}
|
||||
>
|
||||
{query.isLoading || query.isPending || isManualLoading
|
||||
? "조회 중..."
|
||||
: "조회"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">로그인로그 목록</h3>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">로그인로그 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(parseInt(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
{pageSize}개씩 보기
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-35rem)] overflow-auto !p-0">
|
||||
<DataTable table={table} viewPagination />
|
||||
<div className="card relative !m-[1.2rem] !p-0">
|
||||
{/* Debug: 로딩 상태 표시 */}
|
||||
{(query.isLoading || query.isPending || isManualLoading) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/80">
|
||||
<div className="[data-theme='black']:bg-transparent flex h-[10rem] w-[10rem] items-center justify-center rounded-lg bg-secondary p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithImage color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
};
|
||||
|
||||
const handleContentSelect = (content: Contents) => {
|
||||
form.setValue("urlPath", `/cntns/${content.contsSn}`);
|
||||
form.setValue("urlPath", `/cms/cntns/${content.contsSn}`);
|
||||
form.setValue("menuLnkgSn", content.contsSn);
|
||||
form.setValue("menuLnkgSnNm", content.contsTtl);
|
||||
};
|
||||
@@ -47,7 +47,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
const handleBoardSelect = (board: BoardPopup) => {
|
||||
form.setValue(
|
||||
"urlPath",
|
||||
`/pst/${board.bbsTypeCd.toLowerCase()}/${board.bbsId}`,
|
||||
`/cms/${board.bbsTypeCd.toLowerCase()}/${board.bbsId}`,
|
||||
);
|
||||
form.setValue("menuLnkgSn", board.bbsId);
|
||||
form.setValue("menuLnkgSnNm", board.bbsNm);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { PrivacyLogPage } from "./ui/privacylog-page";
|
||||
@@ -0,0 +1,41 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { PrivacyLog } from "@/entities/privacylog";
|
||||
|
||||
|
||||
export const columns: ColumnDef<PrivacyLog>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const { pageIndex, pageSize } = table.getState().pagination;
|
||||
return totalRows - (pageIndex * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inqDt",
|
||||
header: "요청일시",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.inqDt);
|
||||
return date.format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트명",
|
||||
},
|
||||
{
|
||||
accessorKey: "rqstrNm",
|
||||
header: "조회자명",
|
||||
},
|
||||
{
|
||||
accessorKey: "rqstrIp",
|
||||
header: "조회IP",
|
||||
},
|
||||
{
|
||||
accessorKey: "inqInfoSummary",
|
||||
header: "조회정보",
|
||||
}
|
||||
];
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { PrivacyLogPrivacy } from "@/entities/privacylog";
|
||||
import { type PrivacyLogParams} from "@/entities/privacylog/model/privacylog-interface";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/privacylog/model/table-columns";
|
||||
|
||||
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
function PrivacyLogPage({ }) {
|
||||
|
||||
// 1주일 전부터 오늘까지 기본 날짜 설정
|
||||
const defaultDateRange: DateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const [privacylogPostQuery, setprivacylogPostQuery] = useState< PrivacyLogParams>({
|
||||
siteId: "",
|
||||
inqInfoKeywordType: "userName",
|
||||
inqInfoKeyword: "",
|
||||
rqstrIp: "",
|
||||
inqDtStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
inqDtEnd: dayjs().format("YYYY-MM-DD"),
|
||||
sort:"inqDt,asc"
|
||||
});
|
||||
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.PRIVACYLOG, pagination],
|
||||
queryFn: () =>
|
||||
PrivacyLogPrivacy.getPrivacyLogList({
|
||||
...privacylogPostQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleprivacylogParamsChange =
|
||||
(key: keyof PrivacyLogParams) => (value: unknown) => {
|
||||
setprivacylogPostQuery({ ...privacylogPostQuery, [key]: value });
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
switch (value) {
|
||||
case "1W":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "1M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(30, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "3M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(90, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
handleDateRangeChange(presetDateRange);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleDateRangeChange = (value: DateRange | undefined) => {
|
||||
setDateRange(value || defaultDateRange);
|
||||
setprivacylogPostQuery({
|
||||
...privacylogPostQuery,
|
||||
inqDtStart: dayjs(value?.from).format("YYYY-MM-DD") ?? dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
inqDtEnd: dayjs(value?.to).format("YYYY-MM-DD") ?? dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={handleDateRangeChange}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<Label className="ml-4 mr-2 text-label">조회자명</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={privacylogPostQuery.inqInfoKeyword}
|
||||
onChange={(e) =>
|
||||
handleprivacylogParamsChange("inqInfoKeyword")(e.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">로그인로그 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(parseInt(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
{pageSize}개씩 보기
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PrivacyLogPage };
|
||||
@@ -62,6 +62,7 @@ function RecentPage() {
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort:"regDt,desc"
|
||||
});
|
||||
const [isDeletePostData, setIsDeletePostData] = useState(false);
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ function IPManagementDialog({
|
||||
|
||||
const { data: ipList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open],
|
||||
queryFn: () => IpApi.getIpList({ siteId, sort: [] }),
|
||||
queryFn: () => IpApi.getIpList({ siteId, sort: "" }),
|
||||
select: (data) => {
|
||||
const ips = data.content.map((item) => ({
|
||||
...item,
|
||||
|
||||
@@ -17,6 +17,7 @@ function SitePage() {
|
||||
|
||||
const [siteQuery, setSiteQuery] = useState<SiteParams>({
|
||||
siteNm: "",
|
||||
sort:"regDt,desc"
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
|
||||
@@ -18,6 +18,7 @@ function TemplatePage() {
|
||||
const [templateQuery, setTemplateQuery] = useState<TemplateParams>({
|
||||
tmpltSeCd: "",
|
||||
keyword: "",
|
||||
sort:"regDt,desc"
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
|
||||
@@ -17,7 +17,8 @@ function TermsPage() {
|
||||
|
||||
const [termsQuery, setTermsQuery] = useState<TermsParams>({
|
||||
trmsSeCd: "",
|
||||
keyword: "",
|
||||
keyword: "",
|
||||
sort:"regDt,desc"
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
|
||||
@@ -51,7 +51,7 @@ function UsersPage() {
|
||||
grpId: "",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: ["aprvYmd,desc"],
|
||||
sort: "regDt,desc"
|
||||
},
|
||||
});
|
||||
|
||||
@@ -288,9 +288,9 @@ function UsersPage() {
|
||||
<h3 className="text-2xl font-medium text-label">회원 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
defaultValue="aprvYmd,desc"
|
||||
defaultValue="regDt,desc"
|
||||
onValueChange={(value) => {
|
||||
setValue("sort", [value]);
|
||||
setValue("sort", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Client } from "@common/network";
|
||||
import { BASE_URL } from "../config/api";
|
||||
import { SITE_ID } from "../config/env";
|
||||
import { setCookie } from "../lib";
|
||||
|
||||
setCookie("site-id-key", SITE_ID);
|
||||
|
||||
export const client = Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
|
||||
@@ -19,7 +19,10 @@ export const ENDPOINT = {
|
||||
AUTHORIZATIONS: "/user-service/api/v1/authorizations",
|
||||
ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations",
|
||||
ROLES: "/user-service/api/v1/roles",
|
||||
LOGINLOG: "/user-service//api/v1/login-logs",
|
||||
LOGINLOG: "/user-service/api/v1/login-logs",
|
||||
APILOG: "/user-service/api/v1/api-logs",
|
||||
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
|
||||
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
@@ -55,6 +58,7 @@ export const ENDPOINT = {
|
||||
BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt",
|
||||
POSTS: "/board-service/api/v1/posts",
|
||||
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
|
||||
POST_EVALUATION: "/board-service/api/v1/pstEvl",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -30,5 +30,5 @@ export interface PageableList<T> {
|
||||
export interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
PostEvaluationPayload,
|
||||
PostEvaluationService,
|
||||
} from "../model/post-evaluation-interface";
|
||||
|
||||
export class PostEvaluationApiService implements PostEvaluationService {
|
||||
public async postEvaluation(payload: PostEvaluationPayload) {
|
||||
await client.post(ENDPOINT.BOARD_SERVICE.POST_EVALUATION, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export const PostEvaluationApi = new PostEvaluationApiService();
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface PostEvaluationPayload {
|
||||
pstNo: number;
|
||||
evlIp?: string;
|
||||
evlDt?: string;
|
||||
evlScr: number;
|
||||
}
|
||||
|
||||
export interface PostEvaluationService {
|
||||
postEvaluation: (payload: PostEvaluationPayload) => Promise<void>;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
cell: ({ row }) => {
|
||||
cell: ({ row, table }) => {
|
||||
if (row.original.isUpndFixPost) {
|
||||
return (
|
||||
<Badge
|
||||
@@ -40,19 +40,24 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return row.original.rowNo;
|
||||
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
cell: ({ row }) => {
|
||||
const { ansPstn, pstTtl, inqNocs, cmntCnt } = row.original;
|
||||
const { ansPstn = 0, pstTtl, inqNocs, cmntCnt } = row.original;
|
||||
const indentPadding = ansPstn > 0 ? (ansPstn - 1) * 20 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-w-[20vw] items-center gap-2 text-left"
|
||||
style={{ paddingInlineStart: `${(ansPstn - 1) * 20}px` }}
|
||||
style={{ paddingInlineStart: `${indentPadding}px` }}
|
||||
>
|
||||
{ansPstn > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -46,6 +46,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const thumbRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
const [tableSort, setTableSort] = useState("");
|
||||
const [postParams, setPostParams] = useState<PostParams>({
|
||||
keywordType: "title",
|
||||
keyword: "",
|
||||
@@ -78,13 +79,20 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsInfo?.bbsTypeCd],
|
||||
queryKey: [
|
||||
ENDPOINT.BOARD_SERVICE.POSTS,
|
||||
bbsInfo?.bbsTypeCd,
|
||||
pagination,
|
||||
tableSort,
|
||||
],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
bbsTypeCd: bbsInfo?.bbsTypeCd,
|
||||
ansUseYn: bbsInfo?.ansUseYn,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: tableSort,
|
||||
}),
|
||||
enabled: !!bbsInfo?.bbsTypeCd,
|
||||
});
|
||||
@@ -204,6 +212,24 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={tableSort}
|
||||
onValueChange={(value) => {
|
||||
setTableSort(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[8rem]">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pstTtl,asc">제목↑</SelectItem>
|
||||
<SelectItem value="pstTtl,desc">제목↓</SelectItem>
|
||||
<SelectItem value="rgtrNm,asc">작성자↑</SelectItem>
|
||||
<SelectItem value="rgtrNm,desc">작성자↓</SelectItem>
|
||||
<SelectItem value="regDt,asc">등록일↑</SelectItem>
|
||||
<SelectItem value="regDt,desc">등록일↓</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,21 @@ import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button, Skeleton } from "@common/components";
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi } from "@/entities/post";
|
||||
import { useAlertStore, useMutationWrapper, useMenuStore } from "@/shared/lib";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper, useMenuStore } from "@/shared/lib";
|
||||
import { PostAction } from "./post-action";
|
||||
import { PostComment } from "./post-comment";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { PostEvaluationApi } from "../api/post-evaluation-service";
|
||||
|
||||
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
@@ -20,6 +27,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const { bbsInfo, setBbsInfo } = useMenuStore((state) => state);
|
||||
|
||||
const [filename, setFilename] = useState("");
|
||||
const [evlScr, setEvlScr] = useState(5);
|
||||
|
||||
const { data: boardInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId],
|
||||
@@ -72,6 +80,17 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: postEvaluation } = useMutationWrapper({
|
||||
mutationFn: (evlScr: number) =>
|
||||
PostEvaluationApi.postEvaluation({
|
||||
pstNo: parseInt(pstNo),
|
||||
evlScr,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
alert("게시물 평가가 완료되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
@@ -187,6 +206,53 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
</>
|
||||
)
|
||||
)}
|
||||
{bbsInfo?.dgstfnEvlUseYn === "Y" && (
|
||||
<>
|
||||
<hr />
|
||||
<div className="mt-8 flex items-center gap-2 px-4">
|
||||
<p>
|
||||
이 페이지에서 제공하는
|
||||
<br />
|
||||
정보에 대하여 만족하시나요?
|
||||
</p>
|
||||
<div className="ml-4 rounded-sm border border-border bg-accent px-8 py-4">
|
||||
<RadioGroup
|
||||
value={evlScr.toString()}
|
||||
onValueChange={(value) => setEvlScr(parseInt(value))}
|
||||
className="flex items-center space-x-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="5" id="evl5" />
|
||||
<Label htmlFor="evl5">매우 만족 😊</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="4" id="evl4" />
|
||||
<Label htmlFor="evl4">만족 🙂</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="3" id="evl3" />
|
||||
<Label htmlFor="evl3">보통 😐</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="2" id="evl2" />
|
||||
<Label htmlFor="evl2">불만족 🙁</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="1" id="evl1" />
|
||||
<Label htmlFor="evl1">매우 불만족 😞</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
onClick={() => postEvaluation(evlScr)}
|
||||
>
|
||||
평가하기
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PostAction>
|
||||
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Client } from "@common/network";
|
||||
import { getConfig } from "@common/config";
|
||||
import { getCookie } from "../lib";
|
||||
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const getClientInstance = () => {
|
||||
const config = getConfig();
|
||||
const { PROXY_HOST, SITE_ID } = config;
|
||||
const { PROXY_HOST } = config;
|
||||
const BASE_URL = `${PROXY_HOST}/server`;
|
||||
|
||||
return Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
siteId: SITE_ID ?? "",
|
||||
siteId,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getConfig } from "@common/config";
|
||||
import { getSiteInfo } from "../api/site";
|
||||
import { getIpList } from "../api/ip";
|
||||
import { ENDPOINT } from "../config";
|
||||
import { getCookie } from "../lib";
|
||||
|
||||
const { SITE_ID } = getConfig();
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const ClientIpGuard = ({
|
||||
currentPath,
|
||||
@@ -29,17 +29,17 @@ const ClientIpGuard = ({
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!SITE_ID || !clientIp) return;
|
||||
if (!siteId || !clientIp) return;
|
||||
|
||||
const siteInfo = await queryClient.ensureQueryData({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID],
|
||||
queryFn: () => getSiteInfo(SITE_ID),
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
|
||||
queryFn: () => getSiteInfo(siteId),
|
||||
});
|
||||
|
||||
if (siteInfo.siteKndCd === "O") return;
|
||||
|
||||
const { content: ipList } = await getIpList({
|
||||
siteId: SITE_ID,
|
||||
siteId,
|
||||
prmYn: "Y",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "./header.css";
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useEffect, useCallback, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { images } from "@common/assets";
|
||||
import { getConfig } from "@common/config";
|
||||
import { ENDPOINT } from "../../config";
|
||||
import { setCookie, getCookie } from "../../lib";
|
||||
import {
|
||||
@@ -21,10 +20,13 @@ import {
|
||||
} from "../dropdown-menu";
|
||||
import { Button } from "../button";
|
||||
import { SidebarTrigger } from "../sidebar";
|
||||
import { Skeleton } from "../skeleton";
|
||||
|
||||
const { SITE_ID } = getConfig();
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const { data: userInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
queryFn: () => getUserInfo(),
|
||||
@@ -32,8 +34,8 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
});
|
||||
|
||||
const { data: siteInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID],
|
||||
queryFn: () => getSiteInfo(SITE_ID ?? ""),
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
|
||||
queryFn: () => getSiteInfo(siteId),
|
||||
select: (data) => {
|
||||
if (data.siteSkn) {
|
||||
const siteSkin = data.siteSkn.toLowerCase();
|
||||
@@ -50,6 +52,18 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
queryFn: () => getUserPopupList(),
|
||||
});
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to logout");
|
||||
window.location.href = "/auth/login";
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const openPopup = useCallback(async (popup: Popup) => {
|
||||
try {
|
||||
const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`;
|
||||
@@ -97,12 +111,20 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
return () => clearTimeout(timer);
|
||||
}, [userPopupList, openPopup]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
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 px-6">
|
||||
<SidebarTrigger className="size-12 !bg-transparent p-2 hover:!bg-accent [&>svg]:!size-full [&>svg]:text-foreground" />
|
||||
<h1 className="flex items-center gap-4">
|
||||
<img src={getLogoImage()} className="h-[3rem]" alt="logo" />
|
||||
{mounted ? (
|
||||
<img src={getLogoImage()} className="h-[3rem]" alt="logo" />
|
||||
) : (
|
||||
<Skeleton className="h-[3rem] w-[15rem]" />
|
||||
)}
|
||||
<span className="text-[1.8rem] font-semibold text-foreground">
|
||||
{siteInfo?.siteNm}
|
||||
</span>
|
||||
@@ -129,7 +151,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
<p className="text-base text-label">접속 IP</p>
|
||||
<p></p>
|
||||
</div>
|
||||
<DropdownMenuItem>로그아웃</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user