Merge remote-tracking branch 'origin/iipbs_cs' into iipbs_cs

This commit is contained in:
2025-07-03 10:35:23 +09:00
103 changed files with 1358 additions and 282 deletions
+24 -3
View File
@@ -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");
+12 -1
View File
@@ -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">제목&uarr;</SelectItem>
<SelectItem value="pstTtl,desc">제목&darr;</SelectItem>
<SelectItem value="rgtrNm,asc">작성자&uarr;</SelectItem>
<SelectItem value="rgtrNm,desc">작성자&darr;</SelectItem>
<SelectItem value="regDt,asc">등록일&uarr;</SelectItem>
<SelectItem value="regDt,desc">등록일&darr;</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;
+17
View File
@@ -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 });
}
-16
View File
@@ -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 -2
View File
@@ -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));
}
+3 -3
View File
@@ -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();
+6
View File
@@ -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;
-1
View File
@@ -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();
+2 -1
View File
@@ -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();
+6
View File
@@ -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>;
}
+1
View File
@@ -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",
}
];
+209
View File
@@ -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 };
+29
View File
@@ -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">
+1
View File
@@ -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",
},
{
+4 -1
View File
@@ -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",
}
];
+187 -48
View File
@@ -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>
+2 -2
View File
@@ -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);
+1
View File
@@ -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,
+1
View File
@@ -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,
+2 -1
View File
@@ -17,7 +17,8 @@ function TermsPage() {
const [termsQuery, setTermsQuery] = useState<TermsParams>({
trmsSeCd: "",
keyword: "",
keyword: "",
sort:"regDt,desc"
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
+3 -3
View File
@@ -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>
+3
View File
@@ -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,
+5 -1
View File
@@ -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",
},
};
+1 -1
View File
@@ -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">
+27 -1
View File
@@ -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">제목&uarr;</SelectItem>
<SelectItem value="pstTtl,desc">제목&darr;</SelectItem>
<SelectItem value="rgtrNm,asc">작성자&uarr;</SelectItem>
<SelectItem value="rgtrNm,desc">작성자&darr;</SelectItem>
<SelectItem value="regDt,asc">등록일&uarr;</SelectItem>
<SelectItem value="regDt,desc">등록일&darr;</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
+69 -3
View File
@@ -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)} />
+5 -2
View File
@@ -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