Merge remote-tracking branch 'origin/main' into iipbs/sn/dev
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# apps/cs/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=
|
||||
NEXT_PUBLIC_SITE_ID=
|
||||
NEXT_PUBLIC_SERVER_API_URL=
|
||||
NEXT_PUBLIC_PROXY_HOST=
|
||||
NEXT_PUBLIC_TINY_API_KEY=
|
||||
+2
-1
@@ -37,4 +37,5 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.env*
|
||||
.env*
|
||||
!.env.example
|
||||
@@ -0,0 +1,2 @@
|
||||
import { AdminPage } from "@/pages/admin";
|
||||
export default AdminPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { GroupsPage } from "@/pages/groups";
|
||||
export default GroupsPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LoginlogPage } from "@/pages/loginlog";
|
||||
export default LoginlogPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/users/ui/modify-page";
|
||||
|
||||
export default async function UserModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ userUnqId: string }>;
|
||||
}) {
|
||||
const { userUnqId } = await params;
|
||||
return <ModifyPage userUnqId={userUnqId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { UserDetail as VUserDetail } from "@/pages/users";
|
||||
|
||||
export default async function UserDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ userUnqId: string }>;
|
||||
}) {
|
||||
const { userUnqId } = await params;
|
||||
return <VUserDetail userUnqId={userUnqId} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { CreatePage } from "@/pages/users";
|
||||
export default CreatePage;
|
||||
@@ -0,0 +1,16 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,6 @@ export function middleware(req: NextRequest) {
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`);
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
@@ -26,17 +25,18 @@ export function middleware(req: NextRequest) {
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
console.log(
|
||||
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`,
|
||||
);
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === "/") {
|
||||
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/attach-service/:path*"],
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/"],
|
||||
};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -5,6 +5,7 @@ import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { ClientIpGuard } from "@/entities/ip";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
@@ -16,28 +17,30 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider
|
||||
backend={MultiBackend}
|
||||
options={getBackendOptions()}
|
||||
>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
<ClientIpGuard>
|
||||
<StoreProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider
|
||||
backend={MultiBackend}
|
||||
options={getBackendOptions()}
|
||||
>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</ClientIpGuard>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,6 +62,12 @@ export class LwpoCodeApiService implements LwpoCodeService {
|
||||
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async codeRefreshAll(): Promise<void> {
|
||||
await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/cache/refresh-all`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const LwpoCodeApi = new LwpoCodeApiService();
|
||||
|
||||
@@ -65,4 +65,5 @@ export interface LwpoCodeService {
|
||||
lwpoCd: string,
|
||||
data: LwpoCodeFormData,
|
||||
): Promise<void>;
|
||||
codeRefreshAll(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ type CodeSelectProps = {
|
||||
|
||||
function CodeSelect({
|
||||
upCd,
|
||||
defaultValue,
|
||||
placeholder = "선택",
|
||||
className,
|
||||
...props
|
||||
@@ -29,13 +28,10 @@ function CodeSelect({
|
||||
enabled: !!upCd,
|
||||
});
|
||||
|
||||
const defaultValueText =
|
||||
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? placeholder;
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
||||
<SelectValue placeholder={defaultValueText} />
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{code?.map((item) => (
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
GroupUserService,
|
||||
GroupUserSearchParams,
|
||||
GroupUserListResponseDto,
|
||||
GroupUserDetailResponse,
|
||||
PageGroupUserList,
|
||||
GroupUserSaveData,
|
||||
GroupUserUpdateData,
|
||||
} from "../model/group-user-interface";
|
||||
|
||||
export class GroupUsersApiService implements GroupUserService {
|
||||
// 그룹 사용자 목록 조회
|
||||
public async findPageGroupUser(
|
||||
params: GroupUserSearchParams,
|
||||
): Promise<PageGroupUserList> {
|
||||
const response = await client.get<PageGroupUserList>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 단건 조회
|
||||
public async findByGroupAndUserId(
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
): Promise<GroupUserDetailResponse> {
|
||||
const response = await client.get<GroupUserDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 저장
|
||||
public async saveGroupUser(
|
||||
data: GroupUserSaveData[],
|
||||
): Promise<GroupUserListResponseDto[]> {
|
||||
const response = await client.post<GroupUserListResponseDto[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 수정
|
||||
public async updateGroupUser(
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
data: GroupUserUpdateData,
|
||||
): Promise<GroupUserDetailResponse> {
|
||||
const response = await client.put<GroupUserDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
public async deleteGroupUser(
|
||||
groupId: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
const response = await client.delete<boolean>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${groupId}/${userUnqId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const GroupUsersApi = new GroupUsersApiService();
|
||||
@@ -0,0 +1,5 @@
|
||||
export { GroupUsersApi } from "./api/group-user-service";
|
||||
export {
|
||||
type GroupUserListResponseDto,
|
||||
type GroupUserSaveData,
|
||||
} from "./model/group-user-interface";
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
// 그룹 사용자 검색 파라미터 인터페이스
|
||||
export interface GroupUserSearchParams extends PageableParams {
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 목록 응답 인터페이스
|
||||
export interface GroupUserListResponseDto {
|
||||
mode: string;
|
||||
groupId: string;
|
||||
userUnqId: string;
|
||||
userName: string;
|
||||
ogdpSeCd: string;
|
||||
ogdpSeNm: string;
|
||||
mphn: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 페이지 응답 인터페이스
|
||||
export type PageGroupUserList = PageableList<GroupUserListResponseDto>;
|
||||
|
||||
// 그룹 사용자 상세 응답 인터페이스
|
||||
export interface GroupUserDetailResponse {
|
||||
grpSn: number;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
mphn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
rgtrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 저장 데이터 인터페이스
|
||||
export interface GroupUserSaveData {
|
||||
mode: string;
|
||||
groupId: string;
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 수정 데이터 인터페이스
|
||||
export interface GroupUserUpdateData {
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 서비스 인터페이스
|
||||
export interface GroupUserService {
|
||||
// 그룹 사용자 목록 조회
|
||||
findPageGroupUser: (
|
||||
params: GroupUserSearchParams,
|
||||
) => Promise<PageGroupUserList>;
|
||||
|
||||
// 그룹 사용자 단건 조회
|
||||
findByGroupAndUserId: (
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
) => Promise<GroupUserDetailResponse>;
|
||||
|
||||
// 그룹 사용자 저장
|
||||
saveGroupUser: (
|
||||
data: GroupUserSaveData[],
|
||||
) => Promise<GroupUserListResponseDto[]>;
|
||||
|
||||
// 그룹 사용자 수정
|
||||
updateGroupUser: (
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
data: GroupUserUpdateData,
|
||||
) => Promise<GroupUserDetailResponse>;
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
deleteGroupUser: (groupId: string, userUnqId: string) => Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
GroupService,
|
||||
GroupSearchParams,
|
||||
GroupList,
|
||||
GroupResponseDto,
|
||||
GroupFormData,
|
||||
} from "../model/group-interface";
|
||||
|
||||
export class GroupsApiService implements GroupService {
|
||||
// 그룹 목록 조회
|
||||
public async findPageGroup(params: GroupSearchParams): Promise<GroupList> {
|
||||
const response = await client.get<GroupList>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 단건 조회
|
||||
public async findGroupById(groupId: string): Promise<GroupResponseDto> {
|
||||
const response = await client.get<GroupResponseDto>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${groupId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 저장
|
||||
public async saveGroup(data: GroupFormData): Promise<GroupResponseDto> {
|
||||
const response = await client.post<GroupResponseDto>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 수정
|
||||
public async updateGroup(
|
||||
groupId: string,
|
||||
data: GroupFormData,
|
||||
): Promise<GroupResponseDto> {
|
||||
const response = await client.put<GroupResponseDto>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${groupId}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 삭제
|
||||
public async deleteGroup(groupId: string): Promise<boolean> {
|
||||
const response = await client.delete<boolean>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${groupId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const GroupApi = new GroupsApiService();
|
||||
@@ -0,0 +1,6 @@
|
||||
export { GroupApi } from "./api/group-service";
|
||||
export {
|
||||
groupFormSchema,
|
||||
type GroupFormData,
|
||||
type GroupListResponseDto,
|
||||
} from "./model/group-interface";
|
||||
@@ -0,0 +1,87 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
// 그룹 검색 파라미터 인터페이스
|
||||
export interface GroupSearchParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
groupId?: string;
|
||||
groupNm?: string;
|
||||
groupSeCd?: string;
|
||||
delYn?: string;
|
||||
authrtId?: string;
|
||||
siteId?: string;
|
||||
groupExpln?: string;
|
||||
roleId?: string;
|
||||
mngrYn?: string;
|
||||
}
|
||||
|
||||
// 그룹 목록 응답 인터페이스
|
||||
export interface GroupListResponseDto {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupSeNm: string;
|
||||
delYn: string;
|
||||
authrtId: string;
|
||||
groupExpln: string;
|
||||
siteId: string;
|
||||
mngrYn: string;
|
||||
roleId: string;
|
||||
roleNm: string;
|
||||
mdfcnDt: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
// 그룹 상세 응답 인터페이스
|
||||
export interface GroupResponseDto {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupSeNm: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
authrtId: string;
|
||||
siteId: string;
|
||||
mngrYn: string;
|
||||
}
|
||||
|
||||
// 그룹 저장 데이터 스키마
|
||||
export const groupFormSchema = z.object({
|
||||
groupId: z.string().min(1, { message: "그룹 ID를 입력해주세요." }),
|
||||
groupNm: z.string().min(1, { message: "그룹명을 입력해주세요." }),
|
||||
groupSeCd: z.string().min(1, { message: "그룹구분을 선택해주세요." }),
|
||||
groupExpln: z.string().optional(),
|
||||
delYn: z.string().default("N"),
|
||||
authrtId: z.string().min(1, { message: "권한ID를 선택해주세요." }),
|
||||
siteId: z.string().optional(),
|
||||
mngrYn: z.string().default("N"),
|
||||
});
|
||||
|
||||
export type GroupList = PageableList<GroupListResponseDto>;
|
||||
export type GroupFormData = z.infer<typeof groupFormSchema>;
|
||||
|
||||
// 그룹 서비스 인터페이스
|
||||
export interface GroupService {
|
||||
// 그룹 목록 조회
|
||||
findPageGroup: (params: GroupSearchParams) => Promise<GroupList>;
|
||||
|
||||
// 그룹 단건 조회
|
||||
findGroupById: (groupId: string) => Promise<GroupResponseDto>;
|
||||
|
||||
// 그룹 저장
|
||||
saveGroup: (data: GroupFormData) => Promise<GroupResponseDto>;
|
||||
|
||||
// 그룹 수정
|
||||
updateGroup: (
|
||||
groupId: string,
|
||||
data: GroupFormData,
|
||||
) => Promise<GroupResponseDto>;
|
||||
|
||||
// 그룹 삭제
|
||||
deleteGroup: (groupId: string) => Promise<boolean>;
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { IpParams, IP, IpList, IpService } from "../model/ip-interface";
|
||||
import type { IpService, IpParams, IpList, IP } from "../model/ip-interface";
|
||||
|
||||
export class IpApiService implements IpService {
|
||||
public async getIpList(params: IpParams): Promise<IpList> {
|
||||
@@ -0,0 +1,10 @@
|
||||
export { IpApi } from "./api/ip-service";
|
||||
export {
|
||||
ipFormSchema,
|
||||
type IpService,
|
||||
type IPFormData,
|
||||
type IpParams,
|
||||
type IpList,
|
||||
type IP,
|
||||
} from "./model/ip-interface";
|
||||
export { ClientIpGuard } from "./lib/client-ip-guard";
|
||||
@@ -0,0 +1,48 @@
|
||||
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 };
|
||||
@@ -0,0 +1,27 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
LoginlogInfo,
|
||||
LoginlogList,
|
||||
LoginlogParams,
|
||||
LoginlogService,
|
||||
} from "../model/loginlog-interface";
|
||||
|
||||
export class LoginlogApiService implements LoginlogService {
|
||||
public async getLoginlogList(params: LoginlogParams): Promise<LoginlogList> {
|
||||
const response = await client.get<LoginlogList>(
|
||||
`${ENDPOINT.USER_SERVICE.LOGINLOG}`,
|
||||
{ params },
|
||||
);
|
||||
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();
|
||||
@@ -0,0 +1,5 @@
|
||||
export { LoginlogApiService } from "./api/loginlog-service";
|
||||
export type {
|
||||
Loginlog,
|
||||
LoginlogParams,
|
||||
} from "./model/loginlog-interface";
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface LoginlogParams extends PageableParams {
|
||||
lgnSn?: string;
|
||||
userId?: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
export interface Loginlog {
|
||||
lgnSn: string;
|
||||
lgnDt: string;
|
||||
userId: string;
|
||||
lgnIp: string;
|
||||
lgnMethodCd: string;
|
||||
errOcrnYn: string;
|
||||
lgnErrCd: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
export interface LoginlogInfo {
|
||||
lgnSn: string;
|
||||
lgnDt: string;
|
||||
userId: string;
|
||||
lgnIp: string;
|
||||
lgnMethodCd: string;
|
||||
errOcrnYn: string;
|
||||
lgnErrCd: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
export type LoginlogList = PageableList<Loginlog>;
|
||||
|
||||
export interface LoginlogService {
|
||||
getLoginlogList: (params: LoginlogParams) => Promise<LoginlogList>;
|
||||
getLoginlogInfo: (lgnSn: string) => Promise<LoginlogInfo>;
|
||||
|
||||
}
|
||||
@@ -35,6 +35,16 @@ export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getGroupMenuAuthorityList(
|
||||
params: MenuAuthorityParams,
|
||||
): Promise<MenuAuthorityList> {
|
||||
const response = await client.get<MenuAuthorityList>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/group`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuAuthorityApi = new MenuAuthorityApiService();
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
export { MenuAuthorityApi } from "./api/menu-authority-service";
|
||||
export {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthority,
|
||||
type MenuAuthorityPayload,
|
||||
type MenuAuthorityFormData,
|
||||
} from "./model/menu-authority-interface";
|
||||
export { AuthorityButton } from "./ui/authority-button";
|
||||
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
|
||||
|
||||
@@ -43,6 +43,9 @@ export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
|
||||
getGroupMenuAuthorityList(
|
||||
params: MenuAuthorityParams,
|
||||
): Promise<MenuAuthorityList>;
|
||||
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityFormData,
|
||||
@@ -28,14 +28,12 @@ type ManagementAuthorityDialogProps = {
|
||||
menuCd?: number;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function ManagementAuthorityDialog({
|
||||
menuCd,
|
||||
...props
|
||||
}: ManagementAuthorityDialogProps) {
|
||||
function ManagementAuthorityDialog({ menuCd }: ManagementAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: menuAuthorityList } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
|
||||
queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }),
|
||||
enabled: !!menuCd,
|
||||
@@ -45,21 +43,23 @@ function ManagementAuthorityDialog({
|
||||
mutationFn: (data: MenuAuthorityPayload[]) =>
|
||||
MenuAuthorityApi.saveMenuAuthority(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
alert("저장되었습니다.", () => setOpen(false));
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuAuthorityFormData>({
|
||||
resolver: zodResolver(menuAuthorityFormSchema),
|
||||
values: {
|
||||
menuAuthority: menuAuthorityList?.content ?? [],
|
||||
menuAuthority: query.data?.content ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: menuAuthorityList?.content ?? [],
|
||||
columns: createColumns(form),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns(form),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -71,7 +71,7 @@ function ManagementAuthorityDialog({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3" disabled={!menuCd}>
|
||||
<i className="diveicon di-personal-information" />
|
||||
@@ -82,9 +82,7 @@ function ManagementAuthorityDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[40rem] overflow-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
<div className="max-h-[40rem] overflow-auto">{tableComponent}</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
저장
|
||||
|
||||
@@ -52,6 +52,7 @@ type PostingTarget = {
|
||||
type PostingDialogProps = {
|
||||
postingType: PostingType;
|
||||
postingTargets: PostingTarget[];
|
||||
resetSelectedPost: () => void;
|
||||
};
|
||||
|
||||
const POSTING_TYPE_LABEL = {
|
||||
@@ -61,7 +62,11 @@ const POSTING_TYPE_LABEL = {
|
||||
SURVEY: "설문",
|
||||
} as const;
|
||||
|
||||
function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
function PostingDialog({
|
||||
postingType,
|
||||
postingTargets,
|
||||
resetSelectedPost,
|
||||
}: PostingDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
@@ -71,6 +76,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
const [rememberPeriodDate, setRememberPeriodDate] = useState<
|
||||
Record<string, DateRange | undefined>
|
||||
>({});
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: siteList } = useQuery<PageableList<Site>>({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
@@ -80,7 +86,10 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
const { mutateAsync: savePosting } = useMutationWrapper({
|
||||
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
alert("저장되었습니다.", () => {
|
||||
setOpen(false);
|
||||
resetSelectedPost();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -218,14 +227,19 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={handleOpenChange}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
handleOpenChange(open);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<AuthorityButton
|
||||
authority="mngAuthrtYn"
|
||||
color="emerald"
|
||||
disabled={!postingTargets.length}
|
||||
>
|
||||
<i className="diveicon di-layer" />
|
||||
사이트반영
|
||||
</AuthorityButton>
|
||||
</DialogTrigger>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import type {
|
||||
} from "../model/role-interface";
|
||||
|
||||
export class RoleApiService implements RoleService {
|
||||
public async getRoles(params: RoleParams): Promise<RoleList> {
|
||||
public async getRoles(params?: RoleParams): Promise<RoleList> {
|
||||
const response = await client.get<RoleList>(ENDPOINT.USER_SERVICE.ROLES, {
|
||||
params,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
export { RoleApi } from "./api/role-service";
|
||||
export {
|
||||
roleFormSchema,
|
||||
type Role,
|
||||
type RoleParams,
|
||||
type RoleFormData,
|
||||
type RoleList,
|
||||
} from "./model/role-interface";
|
||||
+1
-1
@@ -28,7 +28,7 @@ export type RoleList = PageableList<Role>;
|
||||
export type RoleFormData = z.infer<typeof roleFormSchema>;
|
||||
|
||||
export interface RoleService {
|
||||
getRoles: (params: RoleParams) => Promise<RoleList>;
|
||||
getRoles: (params?: RoleParams) => Promise<RoleList>;
|
||||
getRoleInfo: (roleId: string) => Promise<Role>;
|
||||
createRole: (data: RoleFormData) => Promise<void>;
|
||||
updateRole: (roleId: string, data: RoleFormData) => Promise<void>;
|
||||
@@ -0,0 +1 @@
|
||||
export { SiteApi } from "../api/site-service";
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
cn,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
@@ -12,12 +13,13 @@ import { SiteApi } from "../api/site-service";
|
||||
type SiteSelectProps = {
|
||||
placeholder?: string;
|
||||
enabledAllSite?: boolean;
|
||||
className?: string;
|
||||
} & React.ComponentPropsWithoutRef<typeof Select>;
|
||||
|
||||
function SiteSelect({
|
||||
defaultValue,
|
||||
placeholder = "선택",
|
||||
enabledAllSite,
|
||||
className,
|
||||
...props
|
||||
}: SiteSelectProps) {
|
||||
const { data: sites } = useQuery({
|
||||
@@ -31,13 +33,10 @@ function SiteSelect({
|
||||
},
|
||||
});
|
||||
|
||||
const defaultValueText =
|
||||
sites?.find((item) => item.siteId === defaultValue)?.siteNm ?? placeholder;
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder={defaultValueText} />
|
||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sites?.map((item) => (
|
||||
|
||||
@@ -63,8 +63,8 @@ export const userOrganizationPayloadSchema = z.object({
|
||||
mode: z.string(),
|
||||
ognzSn: z.number(),
|
||||
userUnqId: z.string(),
|
||||
jbpsSeCd: z.string(),
|
||||
jbgdSeCd: z.string(),
|
||||
jbpsSeCd: z.string().min(1),
|
||||
jbgdSeCd: z.string().min(1),
|
||||
sortSeq: z.number(),
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
UserVerifyData,
|
||||
UserEmailData,
|
||||
UserIdData,
|
||||
AdminUserList,
|
||||
} from "../model/user-interface";
|
||||
|
||||
export class UserApiService implements UserService {
|
||||
@@ -208,6 +209,17 @@ export class UserApiService implements UserService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 관리자 목록 조회
|
||||
public async findPageAdminUser(
|
||||
params: UserSearchParams,
|
||||
): Promise<AdminUserList> {
|
||||
const response = await client.get<AdminUserList>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/admin/list`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const UserApi = new UserApiService();
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
export { UserApi } from "./api/user-service";
|
||||
export {
|
||||
USER_STATE_CODE,
|
||||
userSaveSchema,
|
||||
userPasswordUpdateSchema,
|
||||
userUpdateSchema,
|
||||
type UserSearchParams,
|
||||
type UserListResponse,
|
||||
type UserSaveData,
|
||||
type UserDetailResponse,
|
||||
type UserPasswordUpdateData,
|
||||
type UserUpdateData,
|
||||
type AdminUserList,
|
||||
type AdminUserListResponse,
|
||||
} from "./model/user-interface";
|
||||
export { PasswordResetDialog } from "./ui/password-reset-dialog";
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const USER_STATE_CODE = {
|
||||
대기: "00",
|
||||
정상: "01",
|
||||
정지: "07",
|
||||
탈퇴: "08",
|
||||
삭제: "09",
|
||||
} as const;
|
||||
|
||||
type UserStateCode = (typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
|
||||
|
||||
// 사용자 검색 파라미터
|
||||
export interface UserSearchParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
@@ -10,6 +20,7 @@ export interface UserSearchParams extends PageableParams {
|
||||
grpId?: string;
|
||||
frDateYmd?: string;
|
||||
toDateYmd?: string;
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
// 사용자 목록 응답 인터페이스
|
||||
@@ -19,7 +30,7 @@ export interface UserListResponse {
|
||||
email: string;
|
||||
roleId: string;
|
||||
roleName: string;
|
||||
userStateCode: string;
|
||||
userStateCode: UserStateCode;
|
||||
userStateCodeName: string;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
@@ -49,7 +60,7 @@ export interface UserDetailResponse {
|
||||
email: string;
|
||||
encryptedPassword: string;
|
||||
refreshToken: string;
|
||||
userStateCode: string;
|
||||
userStateCode: UserStateCode;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
pswdHnt: string;
|
||||
@@ -75,12 +86,14 @@ export interface UserDetailResponse {
|
||||
instSeCd: string;
|
||||
aprvYmd: string;
|
||||
enoCd: string;
|
||||
dtlInstCd: string;
|
||||
laeSeNm: string;
|
||||
ogdpSeNm: string;
|
||||
instSeNm: string;
|
||||
userStateNm: string;
|
||||
pswdHntNm: string;
|
||||
ecoNm: string;
|
||||
dtlInstNm: string;
|
||||
}
|
||||
|
||||
// 사용자 상태 통계 응답 인터페이스
|
||||
@@ -90,6 +103,28 @@ export interface UserSttsStatsResponse {
|
||||
cnt: number;
|
||||
}
|
||||
|
||||
// 관리자 목록 응답 인터페이스
|
||||
export interface AdminUserListResponse {
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
userStateCode: string;
|
||||
userStateCodeName: string;
|
||||
laeSeCd: string;
|
||||
laeSeNm: string;
|
||||
ogdpSeCd: string;
|
||||
ogdpSeNm: string;
|
||||
instSeCd: string;
|
||||
instSeNm: string;
|
||||
dtlInstCd: string;
|
||||
dtlInstNm: string;
|
||||
mdfr: string;
|
||||
mdfrNm: string;
|
||||
mdfcnDt: string;
|
||||
}
|
||||
|
||||
// 사용자 수정 요청 스키마
|
||||
export const userUpdateSchema = z.object({
|
||||
userId: z.string().optional(),
|
||||
@@ -97,7 +132,10 @@ export const userUpdateSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
)
|
||||
.optional(),
|
||||
roleId: z.string().optional(),
|
||||
userStateCode: z.string(),
|
||||
@@ -114,52 +152,61 @@ export const userUpdateSchema = z.object({
|
||||
autzrId: z.string().uuid().optional(),
|
||||
expryYmd: z.string().max(8),
|
||||
instSeCd: z.string().optional(),
|
||||
dtlInstCd: z.string().optional(),
|
||||
aprvYmd: z.string().max(8).optional(),
|
||||
enoCd: z.string().optional(),
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 변경 스키마
|
||||
export const userPasswordUpdateSchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.optional()
|
||||
.describe("기존 패스워드"),
|
||||
provider: z.string().optional().describe("공급자(기본값:password)"),
|
||||
token: z.string().optional(),
|
||||
newPassword: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
|
||||
.describe("신규 패스워드"),
|
||||
newPasswordConfirm: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
|
||||
.describe("신규 패스워드 확인"),
|
||||
});
|
||||
export const userPasswordUpdateSchema = z
|
||||
.object({
|
||||
password: z.string().optional(),
|
||||
provider: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
newPassword: z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
newPasswordConfirm: z.string(),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.newPasswordConfirm, {
|
||||
message: "비밀번호가 일치하지 않습니다.",
|
||||
path: ["newPasswordConfirm"],
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 찾기 변경 스키마
|
||||
export const userFindPasswordUpdateSchema = z.object({
|
||||
tokenValue: z.string(),
|
||||
password: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 확인 스키마
|
||||
export const userPasswordMatchSchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
});
|
||||
|
||||
// 사용자 회원정보 변경 스키마
|
||||
export const userUpdateInfoSchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.optional()
|
||||
.describe("기존 패스워드"),
|
||||
provider: z.string().optional().describe("공급자(기본값:password)"),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
)
|
||||
.optional(),
|
||||
provider: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
userName: z.string(),
|
||||
email: z.string().email().optional(),
|
||||
@@ -167,25 +214,28 @@ export const userUpdateInfoSchema = z.object({
|
||||
|
||||
// 사용자 저장 스키마
|
||||
export const userSaveSchema = z.object({
|
||||
userId: z.string().describe("사용자 ID"),
|
||||
userName: z.string().describe("사용자명"),
|
||||
email: z.string().email().optional().describe("이메일"),
|
||||
userId: z.string(),
|
||||
userName: z.string(),
|
||||
email: z.string().email().optional(),
|
||||
password: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
|
||||
.describe("패스워드"),
|
||||
pswdHnt: z.string().max(10).describe("비밀번호 힌트"),
|
||||
pswdAns: z.string().max(100).optional().describe("비밀번호 답변"),
|
||||
mphn: z.string().max(11).optional().describe("휴대폰 번호"),
|
||||
telno: z.string().max(11).optional().describe("전화번호"),
|
||||
zip: z.string().max(5).optional().describe("우편번호"),
|
||||
addr: z.string().max(200).optional().describe("주소"),
|
||||
daddr: z.string().max(200).optional().describe("상세 주소"),
|
||||
laeSeCd: z.string().max(10).describe("법적 구분 코드"),
|
||||
ogdpSeCd: z.string().max(10).describe("조직 구분 코드"),
|
||||
expryYmd: z.string().max(8).describe("만료 일자(YYYYMMDD)"),
|
||||
instSeCd: z.string().optional().describe("기관구분코드"),
|
||||
enoCd: z.string().optional().describe("환경청코드"),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
pswdHnt: z.string().max(10),
|
||||
pswdAns: z.string().max(100).optional(),
|
||||
mphn: z.string().max(11).optional(),
|
||||
telno: z.string().max(11).optional(),
|
||||
zip: z.string().max(5).optional(),
|
||||
addr: z.string().max(200).optional(),
|
||||
daddr: z.string().max(200).optional(),
|
||||
laeSeCd: z.string().max(10),
|
||||
ogdpSeCd: z.string().max(10),
|
||||
expryYmd: z.string().max(8),
|
||||
instSeCd: z.string().optional(),
|
||||
dtlInstCd: z.string().optional(),
|
||||
enoCd: z.string().optional(),
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 찾기 저장 스키마
|
||||
@@ -201,9 +251,8 @@ export const userVerifySchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.optional()
|
||||
.describe("기존 패스워드"),
|
||||
provider: z.string().optional().describe("공급자(기본값:password)"),
|
||||
.optional(),
|
||||
provider: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -237,6 +286,7 @@ export type UserIdData = z.infer<typeof userIdSchema>;
|
||||
// 페이지네이션 타입 정의
|
||||
export type UserList = PageableList<UserListResponse>;
|
||||
export type UserSttsStatsList = UserSttsStatsResponse[];
|
||||
export type AdminUserList = PageableList<AdminUserListResponse>;
|
||||
|
||||
// 사용자 서비스 인터페이스
|
||||
export interface UserService {
|
||||
@@ -305,4 +355,7 @@ export interface UserService {
|
||||
|
||||
// 사용자 아이디 중복 체크
|
||||
existsUserId: (data: UserIdData) => Promise<boolean>;
|
||||
|
||||
// 관리자 목록 조회
|
||||
findPageAdminUser: (params: UserSearchParams) => Promise<AdminUserList>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
Input,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import {
|
||||
UserApi,
|
||||
userPasswordUpdateSchema,
|
||||
type UserPasswordUpdateData,
|
||||
} from "@/entities/user";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
|
||||
interface PasswordResetDialogProps {
|
||||
userUnqId: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PasswordResetDialog({
|
||||
userUnqId,
|
||||
children,
|
||||
}: PasswordResetDialogProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<UserPasswordUpdateData>({
|
||||
resolver: zodResolver(userPasswordUpdateSchema),
|
||||
defaultValues: {
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
provider: "password",
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: onPasswordReset } = useMutationWrapper({
|
||||
mutationFn: (data: UserPasswordUpdateData) =>
|
||||
UserApi.initializePassword(userUnqId, data),
|
||||
onSuccess: () => {
|
||||
alert("비밀번호가 초기화되었습니다.", () => {
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>비밀번호 초기화</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => onPasswordReset(data))}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>새 비밀번호</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPasswordConfirm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>새 비밀번호 확인</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" className="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">확인</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
ManagerIp,
|
||||
ManagerIpInfo,
|
||||
ManagerIpList,
|
||||
ManagerIpParams,
|
||||
ManagerIpService,
|
||||
} from "../model/manager-ip-interface";
|
||||
|
||||
export class ManagerIpApiService implements ManagerIpService {
|
||||
public async getManagerIpList(
|
||||
params: ManagerIpParams,
|
||||
): Promise<ManagerIpList> {
|
||||
const response = await client.get<ManagerIpList>(
|
||||
`${ENDPOINT.CMS_SERVICE.MANAGER_IP}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getManagerIpInfo(mngNo: string): Promise<ManagerIpInfo> {
|
||||
const response = await client.get<ManagerIpInfo>(
|
||||
`${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveManagerIp(data: ManagerIp[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
ENDPOINT.CMS_SERVICE.MANAGER_IP,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateManagerIp(mngNo: string, data: ManagerIp): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteManagerIp(mngNo: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const ManagerIpApi = new ManagerIpApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { AdminPage } from "./ui/admin-page";
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface ManagerIpParams extends PageableParams {
|
||||
ipAddr?: string;
|
||||
userUnqId?: string;
|
||||
}
|
||||
|
||||
export const managerIpSchema = z.object({
|
||||
mngNo: z.number().optional(),
|
||||
mode: z.string(),
|
||||
ipAddr: z.string().max(15),
|
||||
userUnqId: z.string(),
|
||||
regDt: z.string().optional(),
|
||||
rgtr: z.string().optional(),
|
||||
mdfcnDt: z.string().optional(),
|
||||
mdfr: z.string().optional(),
|
||||
});
|
||||
|
||||
export const managerIpFormSchema = z.object({
|
||||
ips: z.array(
|
||||
managerIpSchema.omit({
|
||||
regDt: true,
|
||||
rgtr: true,
|
||||
mdfcnDt: true,
|
||||
mdfr: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type ManagerIp = z.infer<typeof managerIpSchema>;
|
||||
export type ManagerIpFormData = z.infer<typeof managerIpFormSchema>;
|
||||
export type ManagerIpList = PageableList<ManagerIp>;
|
||||
export type ManagerIpInfo = ManagerIp;
|
||||
|
||||
export interface ManagerIpService {
|
||||
getManagerIpList: (params: ManagerIpParams) => Promise<ManagerIpList>;
|
||||
getManagerIpInfo: (mngNo: string) => Promise<ManagerIpInfo>;
|
||||
saveManagerIp: (data: ManagerIp[]) => Promise<void>;
|
||||
updateManagerIp: (mngNo: string, data: ManagerIp) => Promise<void>;
|
||||
deleteManagerIp: (mngNo: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import dayjs from "dayjs";
|
||||
import { memo } from "react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { PasswordResetDialog, type AdminUserList } from "@/entities/user";
|
||||
import { GroupUsersApi, type GroupUserSaveData } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ManagerIpDialog } from "../ui/manager-ip-dialog";
|
||||
|
||||
export const columns: ColumnDef<AdminUserList["content"][number]>[] = [
|
||||
{
|
||||
id: "no",
|
||||
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: "userId",
|
||||
header: "관리자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "userName",
|
||||
header: "관리자명",
|
||||
},
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "관리자구분",
|
||||
},
|
||||
{
|
||||
accessorKey: "ogdpSeNm",
|
||||
header: "소속",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
header: "관리",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<ActionCell
|
||||
userUnqId={row.original.userUnqId}
|
||||
groupId={row.original.groupId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const ActionCell = memo(
|
||||
({ userUnqId, groupId }: { userUnqId: string; groupId: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { mutate: onDelete } = useMutationWrapper({
|
||||
mutationFn: (data: GroupUserSaveData) =>
|
||||
GroupUsersApi.saveGroupUser([data]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
});
|
||||
alert("삭제되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (await confirm("관리자를 삭제하시겠습니까?")) {
|
||||
onDelete({
|
||||
mode: "D",
|
||||
groupId,
|
||||
userUnqId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center gap-2">
|
||||
<ManagerIpDialog userUnqId={userUnqId} />
|
||||
<PasswordResetDialog userUnqId={userUnqId}>
|
||||
<Button size="sm" color="gray">
|
||||
비밀번호 초기화
|
||||
</Button>
|
||||
</PasswordResetDialog>
|
||||
<Button size="sm" color="red" onClick={handleDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.userUnqId === nextProps.userUnqId &&
|
||||
prevProps.groupId === nextProps.groupId
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ActionCell.displayName = "ActionCell";
|
||||
@@ -0,0 +1,258 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Checkbox,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import {
|
||||
UserApi,
|
||||
type UserListResponse,
|
||||
type UserSearchParams,
|
||||
} from "@/entities/user";
|
||||
import { GroupApi } from "@/entities/group";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
function AddAdminDialog() {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
});
|
||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: groupOptions } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS],
|
||||
queryFn: () =>
|
||||
GroupApi.findPageGroup({
|
||||
delYn: "N",
|
||||
mngrYn: "Y",
|
||||
page: 0,
|
||||
size: 99,
|
||||
}),
|
||||
select: (data) =>
|
||||
data.content.map((item) => ({
|
||||
groupId: item.groupId,
|
||||
groupNm: item.groupNm,
|
||||
})),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination, "normal-user"],
|
||||
queryFn: () =>
|
||||
UserApi.findPageUser({
|
||||
...userSearchQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: open,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const columns: ColumnDef<UserListResponse>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "userName",
|
||||
header: "이름",
|
||||
},
|
||||
{
|
||||
accessorKey: "ogdpSeNm",
|
||||
header: "소속",
|
||||
},
|
||||
{
|
||||
accessorKey: "mphn",
|
||||
header: "연락처",
|
||||
},
|
||||
];
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: columns,
|
||||
state: { pagination, rowSelection },
|
||||
manualPagination: true,
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: (row) => row.userUnqId,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddAdmins = async () => {
|
||||
const selectedRows = Object.keys(rowSelection);
|
||||
|
||||
if (!(await confirm("관리자를 추가하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersToAdd = selectedRows.map((userUnqId) => ({
|
||||
mode: "C",
|
||||
groupId: selectedGroupId,
|
||||
userUnqId,
|
||||
}));
|
||||
|
||||
await GroupUsersApi.saveGroupUser(usersToAdd);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
});
|
||||
alert("관리자 추가를 완료했습니다.", () => {
|
||||
setOpen(false);
|
||||
setRowSelection({});
|
||||
setSelectedGroupId("");
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setPagination({ pageIndex: 0, pageSize: 10 });
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>관리자등록</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>관리자 등록</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSearch} className="flex items-center gap-2">
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
setUserSearchQuery({ ...userSearchQuery, keywordType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem] !text-[1.3rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="userId">아이디</SelectItem>
|
||||
<SelectItem value="userName">이름</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={userSearchQuery.keyword}
|
||||
onChange={(e) =>
|
||||
setUserSearchQuery({
|
||||
...userSearchQuery,
|
||||
keyword: e.target.value,
|
||||
})
|
||||
}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="검색어 입력"
|
||||
/>
|
||||
<Button type="submit">검색</Button>
|
||||
</form>
|
||||
<hr className="my-4" />
|
||||
<Label className="text-label">1. 사용자 선택</Label>
|
||||
{tableComponent}
|
||||
<hr className="my-4" />
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
<Label className="mr-2 text-label">2. 그룹 선택</Label>
|
||||
<Select onValueChange={(value) => setSelectedGroupId(value)}>
|
||||
<SelectTrigger className="h-[3.2rem] w-[24rem] !text-[1.3rem]">
|
||||
<SelectValue placeholder="그룹 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{groupOptions?.map((item) => (
|
||||
<SelectItem key={item.groupId} value={item.groupId}>
|
||||
{item.groupNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button color="white" onClick={() => setOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddAdmins}
|
||||
disabled={
|
||||
Object.keys(rowSelection).length === 0 || !selectedGroupId
|
||||
}
|
||||
>
|
||||
등록
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AddAdminDialog };
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
type Table,
|
||||
type Row,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
import { GroupApi } from "@/entities/group";
|
||||
import {
|
||||
UserApi,
|
||||
type UserSearchParams,
|
||||
type AdminUserListResponse,
|
||||
} from "@/entities/user";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useAlertStore } from "@/shared/lib";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { AddAdminDialog } from "./add-admin-dialog";
|
||||
|
||||
function AdminPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [adminQuery, setAdminQuery] = useState<UserSearchParams>({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
grpId: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: groupOptions } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS],
|
||||
queryFn: () =>
|
||||
GroupApi.findPageGroup({
|
||||
delYn: "N",
|
||||
mngrYn: "Y",
|
||||
page: 0,
|
||||
size: 99,
|
||||
}),
|
||||
select: (data) =>
|
||||
data.content.map((item) => ({
|
||||
groupId: item.groupId,
|
||||
groupNm: item.groupNm,
|
||||
})),
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination],
|
||||
queryFn: () =>
|
||||
UserApi.findPageAdminUser({
|
||||
...adminQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
});
|
||||
|
||||
const adminColumns = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }: { table: Table<AdminUserListResponse> }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: Row<AdminUserListResponse> }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
...columns,
|
||||
];
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: adminColumns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleDeleteAdmins = async () => {
|
||||
const selectedRows = table.getSelectedRowModel().rows.map((row) => ({
|
||||
groupId: row.original.groupId,
|
||||
userUnqId: row.original.userUnqId,
|
||||
}));
|
||||
|
||||
if (!(await confirm("관리자를 삭제하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersToDelete = selectedRows.map((row) => ({
|
||||
mode: "D",
|
||||
groupId: row.groupId,
|
||||
userUnqId: row.userUnqId,
|
||||
}));
|
||||
|
||||
await GroupUsersApi.saveGroupUser(usersToDelete);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
});
|
||||
alert("관리자 삭제를 완료했습니다.");
|
||||
setRowSelection({});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-6 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="mr-2 text-label">관리자구분</Label>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
setAdminQuery({ ...adminQuery, grpId: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[16rem] !text-[1.3rem]">
|
||||
<SelectValue placeholder="그룹 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{groupOptions?.map((item) => (
|
||||
<SelectItem key={item.groupId} value={item.groupId}>
|
||||
{item.groupNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
관리자
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
setAdminQuery({ ...adminQuery, keywordType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem] !text-[1.3rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="userId">아이디</SelectItem>
|
||||
<SelectItem value="userName">이름</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={adminQuery.keyword}
|
||||
onChange={(e) =>
|
||||
setAdminQuery({ ...adminQuery, keyword: e.target.value })
|
||||
}
|
||||
placeholder="검색어 입력"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
관리자 목록(총 {query.data?.totalElements}명)
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<AddAdminDialog />
|
||||
<Button
|
||||
color="red"
|
||||
onClick={handleDeleteAdmins}
|
||||
disabled={Object.keys(rowSelection).length === 0}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { AdminPage };
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Input,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { ManagerIpApi } from "../api/manager-ip-service";
|
||||
import {
|
||||
managerIpFormSchema,
|
||||
type ManagerIp,
|
||||
type ManagerIpFormData,
|
||||
} from "../model/manager-ip-interface";
|
||||
|
||||
type ManagerIpDialogProps = {
|
||||
userUnqId: string;
|
||||
};
|
||||
|
||||
function ManagerIpDialog({ userUnqId }: ManagerIpDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const { data: managerIpList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MANAGER_IP, open],
|
||||
queryFn: () => ManagerIpApi.getManagerIpList({ userUnqId }),
|
||||
select: (data) => {
|
||||
const ips = data.content.map((item) => ({
|
||||
...item,
|
||||
mode: "U",
|
||||
}));
|
||||
initialForm();
|
||||
return ips;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { mutateAsync } = useMutationWrapper({
|
||||
mutationFn: (data: ManagerIp[]) => ManagerIpApi.saveManagerIp(data),
|
||||
});
|
||||
|
||||
const form = useForm<ManagerIpFormData>({
|
||||
resolver: zodResolver(managerIpFormSchema),
|
||||
values: {
|
||||
ips: managerIpList ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove, update } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "ips",
|
||||
});
|
||||
|
||||
const initialForm = () => {
|
||||
form.reset();
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ManagerIpFormData) => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
await mutateAsync(data.ips);
|
||||
alert("저장되었습니다.", () => {
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddIP = () => {
|
||||
append({
|
||||
mngNo: 0,
|
||||
mode: "C",
|
||||
ipAddr: "",
|
||||
userUnqId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedRows.length === fields.length) {
|
||||
setSelectedRows([]);
|
||||
} else {
|
||||
setSelectedRows(fields.map((_, index) => index));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRow = (i: number) => {
|
||||
setSelectedRows((prev) =>
|
||||
prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i],
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteIP = async () => {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
if (await confirm("선택한 IP를 삭제하시겠습니까?")) {
|
||||
const sortedIndices = [...selectedRows].sort((a, b) => b - a);
|
||||
|
||||
for (const i of sortedIndices) {
|
||||
const field = fields[i];
|
||||
if (field?.mngNo) {
|
||||
if (field.mode === "C") {
|
||||
remove(i);
|
||||
} else {
|
||||
update(i, {
|
||||
...field,
|
||||
mode: "D",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedRows([]);
|
||||
}
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">접속 IP</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>접속 IP 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button type="button" size="sm" onClick={handleAddIP}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteIP}
|
||||
disabled={selectedRows.length === 0}
|
||||
>
|
||||
항목 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border max-h-[28.5rem] overflow-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="90%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky -top-[0.6rem]">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>IP 주소</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{fields.map((field, i) => {
|
||||
if (field.mode === "D") return null;
|
||||
return (
|
||||
<TableRow key={field.id}>
|
||||
<TableCell align="center" className="!px-0">
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(i)}
|
||||
onCheckedChange={() => handleSelectRow(i)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ips.${i}.ipAddr`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button type="submit" size="lg">
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ManagerIpDialog };
|
||||
@@ -5,13 +5,13 @@ import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { createColumns } from "../model/banner-table-columns";
|
||||
import { type Banner, type BannerParams } from "../model/banner-interface";
|
||||
@@ -34,35 +34,19 @@ function BannerPage() {
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: bannerList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS],
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, pagination],
|
||||
queryFn: () =>
|
||||
BannerApi.getBannerList({
|
||||
...bannerQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: bannerList?.content ?? [],
|
||||
columns: createColumns(imageRefs),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: bannerList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Banner) => {
|
||||
@@ -78,6 +62,26 @@ function BannerPage() {
|
||||
return selectedImages;
|
||||
};
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns(imageRefs),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
@@ -94,10 +98,6 @@ function BannerPage() {
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
@@ -126,6 +126,7 @@ function BannerPage() {
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.BANNER}
|
||||
postingTargets={postingTargets}
|
||||
resetSelectedPost={() => table.setRowSelection({})}
|
||||
/>
|
||||
<Button onClick={() => router.push("/banner/register")}>
|
||||
등록
|
||||
@@ -133,9 +134,7 @@ function BannerPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,15 +5,11 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components";
|
||||
import { Button } from "@common/components";
|
||||
import { PostingApi, type PostPayload } from "@/entities/posting";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
bannerFormSchema,
|
||||
@@ -36,7 +32,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
queryFn: () => BannerApi.getBannerInfo(sequential),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
queryFn: () =>
|
||||
PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(sequential) }),
|
||||
@@ -46,7 +42,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
mutationFn: (data: BannerFormData) =>
|
||||
BannerApi.updateBanner(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,15 +68,17 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
values: bannerDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
@@ -122,7 +120,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
@@ -23,6 +21,7 @@ import {
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { BoardApi, type BoardParams } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { type Board } from "@/entities/board";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
@@ -39,44 +38,44 @@ function BbsPage() {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: boardList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, pagination],
|
||||
queryFn: () =>
|
||||
BoardApi.getBoardList({
|
||||
...boardParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: boardList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: boardList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Board) => {
|
||||
router.push(`/bbs/register/${row.bbsId}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
@@ -134,9 +133,7 @@ function BbsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import { BbsForm } from "./bbs-form";
|
||||
|
||||
function ModifyPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: boardInfo } = useQuery({
|
||||
@@ -23,7 +25,7 @@ function ModifyPage({ bbsId }: { bbsId: string }) {
|
||||
const { mutateAsync: updateBoard } = useMutationWrapper({
|
||||
mutationFn: (data: BoardFormData) => BoardApi.updateBoard(bbsId, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { LwpoCodeApi, type LwpoCodeParams } from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/lwpocode-table-columns";
|
||||
import { LwpoCodeManagementDialog } from "./lwpocode-dialog";
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
|
||||
type LwpoCodeSectionProps = {
|
||||
upCd?: string;
|
||||
@@ -25,7 +23,7 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: lwpoCodeList, refetch } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, pagination],
|
||||
queryFn: () =>
|
||||
LwpoCodeApi.getLwpoCodeList({
|
||||
@@ -38,23 +36,43 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: lwpoCodeList?.content ?? [],
|
||||
columns,
|
||||
rowCount: lwpoCodeList?.totalElements ?? 0,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
getRowId: (row) => row.lwpoCd,
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
getRowId: (row) => row.lwpoCd,
|
||||
},
|
||||
dataTableOptions: { viewPagination: true },
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPagination({ ...pagination, pageIndex: 0 });
|
||||
}, [upCd]);
|
||||
|
||||
const { mutateAsync: codeRefreshAll } = useMutation({
|
||||
mutationFn: () => LwpoCodeApi.codeRefreshAll(),
|
||||
onSuccess: () => {
|
||||
alert('저장되었습니다.')
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('에러 발생:', error)
|
||||
alert('에러가 발생했습니다.')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
@@ -80,15 +98,20 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">하위코드 목록</h3>
|
||||
<button onClick={() => codeRefreshAll()} className="btn">
|
||||
코드 리프레시
|
||||
</button>
|
||||
|
||||
<LwpoCodeManagementDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
>
|
||||
<Button>등록</Button>
|
||||
</LwpoCodeManagementDialog>
|
||||
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-35rem)] overflow-auto !p-0">
|
||||
<DataTable table={table} viewPagination />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { UpCodeApi, type UpCode, type UpCodeParams } from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/upcode-table-columns";
|
||||
import { UpCodeManagementDialog } from "./upcode-dialog";
|
||||
|
||||
@@ -25,7 +22,7 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: upCodeList, refetch } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, pagination],
|
||||
queryFn: () =>
|
||||
UpCodeApi.getUpCodeList({
|
||||
@@ -36,18 +33,6 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: upCodeList?.content ?? [],
|
||||
columns,
|
||||
rowCount: upCodeList?.totalElements ?? 0,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
getRowId: (row) => row.upCd,
|
||||
});
|
||||
|
||||
const handleRowClick = (row: UpCode) => {
|
||||
table.setRowSelection({ [row.upCd]: true });
|
||||
handleSelectedUpCd(row.upCd);
|
||||
@@ -55,9 +40,23 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
getRowId: (row) => row.upCd,
|
||||
},
|
||||
dataTableOptions: { onRowClick: handleRowClick, viewPagination: true },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
@@ -91,7 +90,7 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
|
||||
</UpCodeManagementDialog>
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-35rem)] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,12 @@ export const columns = [
|
||||
columnHelper.display({
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
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);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("contsTtl", {
|
||||
id: "contsTtl",
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import {
|
||||
ContentsApi,
|
||||
type Contents,
|
||||
type ContentsParams,
|
||||
} from "@/entities/content";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
function ContentsPage() {
|
||||
@@ -28,8 +25,8 @@ function ContentsPage() {
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: contentsList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS],
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, pagination],
|
||||
queryFn: () =>
|
||||
ContentsApi.getContentsList({
|
||||
...contentsQuery,
|
||||
@@ -37,34 +34,35 @@ function ContentsPage() {
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Contents) => {
|
||||
router.push(`/contents/register/${row.contsSn}`);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: contentsList?.content ?? [],
|
||||
columns,
|
||||
rowCount: contentsList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
variant: "secondary",
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
@@ -89,14 +87,7 @@ function ContentsPage() {
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable
|
||||
table={table}
|
||||
onRowClick={handleRowClick}
|
||||
variant="secondary"
|
||||
viewPagination
|
||||
/>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,12 @@ export const createColumns = (
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: (info) => info.row.index + 1,
|
||||
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: "atflId",
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
@@ -19,15 +15,15 @@ function FilePage() {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [attachmentQuery, setAttachmentQuery] = useState<AttachmentParams>({
|
||||
keyword: "",
|
||||
orgnlFileNm: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: attachmentsList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS],
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, pagination],
|
||||
queryFn: () =>
|
||||
AttachmentApi.getAttachmentsList({
|
||||
...attachmentQuery,
|
||||
@@ -35,14 +31,13 @@ function FilePage() {
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const { mutate: deleteAttachment } = useMutationWrapper({
|
||||
mutationFn: (atflId: string) =>
|
||||
AttachmentApi.deleteAttachmentByAtflId(atflId),
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
query.refetch();
|
||||
alert("삭제되었습니다.");
|
||||
},
|
||||
});
|
||||
@@ -53,16 +48,20 @@ function FilePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: attachmentsList?.content ?? [],
|
||||
columns: createColumns(handleDeleteAttachment),
|
||||
rowCount: attachmentsList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns(handleDeleteAttachment),
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleAttachmentQueryChange =
|
||||
@@ -72,13 +71,9 @@ function FilePage() {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
@@ -91,9 +86,9 @@ function FilePage() {
|
||||
<Input
|
||||
id="keyword"
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={attachmentQuery.keyword || ""}
|
||||
value={attachmentQuery.orgnlFileNm || ""}
|
||||
onChange={(e) =>
|
||||
handleAttachmentQueryChange("keyword")(e.target.value)
|
||||
handleAttachmentQueryChange("orgnlFileNm")(e.target.value)
|
||||
}
|
||||
placeholder="파일명을 입력하세요"
|
||||
/>
|
||||
@@ -108,9 +103,7 @@ function FilePage() {
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">파일 목록</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable table={table} viewPagination />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { GroupsPage } from "./ui/groups-page";
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type {
|
||||
MenuAuthority,
|
||||
MenuAuthorityPayload,
|
||||
MenuAuthorityFormData,
|
||||
} from "@/entities/menu-authority";
|
||||
|
||||
export const createColumns = (
|
||||
form: UseFormReturn<MenuAuthorityFormData>,
|
||||
): ColumnDef<MenuAuthority>[] => {
|
||||
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
|
||||
|
||||
const handleCheckedChange =
|
||||
(i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => {
|
||||
if (getCurrentRow(i)?.mode === "R") {
|
||||
form.setValue(`menuAuthority.${i}.mode`, "U");
|
||||
}
|
||||
form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N");
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: "menuNm",
|
||||
header: "메뉴명",
|
||||
},
|
||||
{
|
||||
accessorKey: "mngAuthrtYn",
|
||||
header: "관리권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"regAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"mdfcnAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"delAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "regAuthrtYn",
|
||||
header: "등록권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnAuthrtYn",
|
||||
header: "수정권한 ",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "delAuthrtYn",
|
||||
header: "삭제권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inqAuthrtYn",
|
||||
header: "조회권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Checkbox } from "@common/components";
|
||||
import { type Table, type Row, type ColumnDef } from "@tanstack/react-table";
|
||||
import { type GroupUserListResponseDto } from "@/entities/group-user";
|
||||
|
||||
export const columns: ColumnDef<GroupUserListResponseDto>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }: { table: Table<GroupUserListResponseDto> }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: Row<GroupUserListResponseDto> }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupId",
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "userName",
|
||||
header: "이름",
|
||||
},
|
||||
{
|
||||
accessorKey: "ogdpSeNm",
|
||||
header: "소속",
|
||||
},
|
||||
{
|
||||
accessorKey: "mphn",
|
||||
header: "연락처",
|
||||
cell: ({ row }) => {
|
||||
const phone = row.getValue("mphn") as string;
|
||||
if (!phone) return "";
|
||||
|
||||
const numbers = phone.replace(/\D/g, "");
|
||||
if (numbers.length === 10) {
|
||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 6)}-${numbers.slice(6)}`;
|
||||
} else if (numbers.length === 11) {
|
||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`;
|
||||
} else {
|
||||
return phone;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
import { type GroupListResponseDto } from "@/entities/group";
|
||||
import { MenuAuthorityDialog } from "../ui/menu-authority-dialog";
|
||||
import { GroupsDialog } from "../ui/groups-dialog";
|
||||
|
||||
export const columns: ColumnDef<GroupListResponseDto>[] = [
|
||||
{
|
||||
accessorKey: "groupId",
|
||||
header: "그룹코드",
|
||||
},
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "그룹명",
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "관리",
|
||||
cell: ({ row }) => (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex justify-center gap-2"
|
||||
>
|
||||
<MenuAuthorityDialog groupId={row.original.groupId} />
|
||||
<GroupsDialog groupId={row.original.groupId}>
|
||||
<Button size="sm" color="white">
|
||||
수정
|
||||
</Button>
|
||||
</GroupsDialog>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
import {
|
||||
UserApi,
|
||||
type UserListResponse,
|
||||
type UserSearchParams,
|
||||
} from "@/entities/user";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
|
||||
type AdditionalUserDialogProps = {
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
||||
userName: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [userRowSelection, setUserRowSelection] = useState<RowSelectionState>(
|
||||
{},
|
||||
);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, userSearchQuery, pagination],
|
||||
queryFn: () =>
|
||||
UserApi.findPageUser({
|
||||
...userSearchQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: open,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const userColumns: ColumnDef<UserListResponse>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "userName",
|
||||
header: "이름",
|
||||
},
|
||||
{
|
||||
accessorKey: "ogdpSeNm",
|
||||
header: "소속",
|
||||
},
|
||||
{
|
||||
accessorKey: "mphn",
|
||||
header: "연락처",
|
||||
},
|
||||
];
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: userColumns,
|
||||
state: { pagination, rowSelection: userRowSelection },
|
||||
manualPagination: true,
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setUserRowSelection,
|
||||
getRowId: (row) => row.userUnqId,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddUsers = async () => {
|
||||
const selectedRows = Object.keys(userRowSelection);
|
||||
if (selectedRows.length === 0 || !groupId) return;
|
||||
|
||||
try {
|
||||
const usersToAdd = selectedRows.map((userUnqId) => ({
|
||||
mode: "C",
|
||||
groupId,
|
||||
userUnqId,
|
||||
}));
|
||||
|
||||
await GroupUsersApi.saveGroupUser(usersToAdd);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId],
|
||||
});
|
||||
alert("사용자 추가를 완료했습니다.", () => {
|
||||
setOpen(false);
|
||||
setUserRowSelection({});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setPagination({ pageIndex: 0, pageSize: 10 });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={!groupId}>사용자 추가</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>사용자 추가</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mb-4 flex items-end gap-2">
|
||||
<Input
|
||||
value={userSearchQuery.userName}
|
||||
onChange={(e) =>
|
||||
setUserSearchQuery({
|
||||
...userSearchQuery,
|
||||
userName: e.target.value,
|
||||
})
|
||||
}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사용자 이름으로 검색"
|
||||
/>
|
||||
<Button onClick={handleSearch}>검색</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
<DialogFooter>
|
||||
<Button color="white" onClick={() => setOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddUsers}
|
||||
disabled={table.getSelectedRowModel().rows.length === 0}
|
||||
>
|
||||
추가
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AddUserDialog };
|
||||
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { SiteSelect } from "@/entities/site";
|
||||
import type { GroupFormData } from "@/entities/group";
|
||||
import { RoleSelect } from "./role-select";
|
||||
|
||||
type GroupFormProps = {
|
||||
form: UseFormReturn<GroupFormData>;
|
||||
};
|
||||
|
||||
function GroupForm({ form }: GroupFormProps) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>그룹코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[24rem] !text-[1.3rem]"
|
||||
placeholder="그룹코드를 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[24rem] !text-[1.3rem]"
|
||||
placeholder="그룹명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupSeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
upCd="GROUPSECD"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="h-[10rem] resize-none !text-[1.3rem]"
|
||||
placeholder="그룹설명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SiteSelect
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>관리권한</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mngrYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="mngrYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="mngrY" />
|
||||
<Label htmlFor="mngrY">Y</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="mngrN" />
|
||||
<Label htmlFor="mngrN">N</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>권한</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authrtId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RoleSelect
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
mngrYn={form.watch("mngrYn")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupForm };
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/group-users-table-columns";
|
||||
import { AddUserDialog } from "./add-user-dialog";
|
||||
|
||||
type GroupUsersSectionProps = {
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId, pagination],
|
||||
queryFn: () =>
|
||||
GroupUsersApi.findPageGroupUser({
|
||||
keyword: groupId,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: !!groupId,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination, rowSelection },
|
||||
manualPagination: true,
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: (row) => row.userUnqId,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
const selectedRows = Object.keys(rowSelection);
|
||||
if (selectedRows.length === 0 || !groupId) return;
|
||||
if (!(await confirm("사용자를 삭제하시겠습니까?"))) return;
|
||||
|
||||
try {
|
||||
const usersToDelete = selectedRows.map((userUnqId) => ({
|
||||
mode: "D",
|
||||
groupId,
|
||||
userUnqId,
|
||||
}));
|
||||
|
||||
await GroupUsersApi.saveGroupUser(usersToDelete);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId],
|
||||
});
|
||||
alert("사용자 삭제를 완료했습니다.", () => {
|
||||
setRowSelection({});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPagination({ ...pagination, pageIndex: 0 });
|
||||
}, [groupId]);
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 사용자 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<AddUserDialog groupId={groupId} />
|
||||
<Button
|
||||
color="red"
|
||||
disabled={table.getSelectedRowModel().rows.length === 0}
|
||||
onClick={handleDeleteSelected}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-25.5rem)] overflow-auto !p-0">
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupUsersSection };
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
} from "@common/components";
|
||||
import {
|
||||
GroupApi,
|
||||
groupFormSchema,
|
||||
type GroupFormData,
|
||||
} from "@/entities/group";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { Form } from "@/shared/ui/form";
|
||||
import { GroupForm } from "./group-form";
|
||||
|
||||
type GroupsDialogProps = {
|
||||
groupId?: string;
|
||||
children: React.ReactNode;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
const GroupsDialog = memo(
|
||||
({ groupId, children, onSuccess, ...props }: GroupsDialogProps) => {
|
||||
const isRegistration = !groupId;
|
||||
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: group } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
||||
queryFn: () => GroupApi.findGroupById(groupId!),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
|
||||
const { mutateAsync: createGroup } = useMutationWrapper({
|
||||
mutationFn: (data: GroupFormData) => GroupApi.saveGroup(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", () => {
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateGroup } = useMutationWrapper({
|
||||
mutationFn: (data: GroupFormData) =>
|
||||
GroupApi.updateGroup(groupId ?? "", data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<GroupFormData>({
|
||||
resolver: zodResolver(groupFormSchema),
|
||||
values: {
|
||||
groupId: group?.groupId ?? "",
|
||||
groupNm: group?.groupNm ?? "",
|
||||
groupSeCd: group?.groupSeCd ?? "",
|
||||
groupExpln: group?.groupExpln ?? "",
|
||||
delYn: group?.delYn ?? "",
|
||||
authrtId: group?.authrtId ?? "",
|
||||
siteId: group?.siteId ?? "",
|
||||
mngrYn: group?.mngrYn ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit =
|
||||
(callback: (data: GroupFormData) => void) =>
|
||||
async (data: GroupFormData) => {
|
||||
const confirmMessage = isRegistration
|
||||
? "등록하시겠습니까?"
|
||||
: "수정하시겠습니까?";
|
||||
|
||||
if (await confirm(confirmMessage)) callback(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isRegistration ? "그룹 등록" : "그룹 수정"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(
|
||||
onSubmit(isRegistration ? createGroup : updateGroup),
|
||||
)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<GroupForm form={form} />
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button type="submit" size="lg">
|
||||
{isRegistration ? "등록" : "수정"}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="white" size="lg">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
GroupsDialog.displayName = "GroupsDialog";
|
||||
|
||||
export { GroupsDialog };
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { GroupsSection } from "./groups-section";
|
||||
import { GroupUsersSection } from "./group-users-section";
|
||||
|
||||
function GroupsPage() {
|
||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<GroupsSection handleSelectedGroup={setSelectedGroupId} />
|
||||
<GroupUsersSection groupId={selectedGroupId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupsPage };
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
import { GroupApi } from "@/entities/group";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/groups-table-columns";
|
||||
import { GroupsDialog } from "./groups-dialog";
|
||||
|
||||
type GroupsSectionProps = {
|
||||
handleSelectedGroup: (groupId: string) => void;
|
||||
};
|
||||
|
||||
function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, pagination],
|
||||
queryFn: () =>
|
||||
GroupApi.findPageGroup({
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleRowClick = (row: { groupId: string }) => {
|
||||
table.setRowSelection({ [row.groupId]: true });
|
||||
handleSelectedGroup(row.groupId);
|
||||
};
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
getRowId: (row) => row.groupId,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 목록</h3>
|
||||
<GroupsDialog onSuccess={query.refetch}>
|
||||
<Button>등록</Button>
|
||||
</GroupsDialog>
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-25.5rem)] overflow-auto !p-0">
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupsSection };
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import {
|
||||
MenuAuthorityApi,
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityPayload,
|
||||
type MenuAuthorityFormData,
|
||||
} from "@/entities/menu-authority";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { createColumns } from "../model/group-menu-authority-table-columns";
|
||||
|
||||
type MenuAuthorityDialogProps = {
|
||||
groupId: string;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, groupId],
|
||||
queryFn: () => MenuAuthorityApi.getGroupMenuAuthorityList({ groupId }),
|
||||
enabled: open && !!groupId,
|
||||
});
|
||||
|
||||
const { mutateAsync: saveMenuAuthority } = useMutationWrapper({
|
||||
mutationFn: (data: MenuAuthorityPayload[]) =>
|
||||
MenuAuthorityApi.saveMenuAuthority(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.", () => setOpen(false));
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuAuthorityFormData>({
|
||||
resolver: zodResolver(menuAuthorityFormSchema),
|
||||
values: {
|
||||
menuAuthority: query.data?.content ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns(form),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("menuAuthority");
|
||||
const filteredData = data.filter((item) => item.mode !== "R");
|
||||
const menuAuthorityData = filteredData.map((item) => ({
|
||||
...item,
|
||||
groupId,
|
||||
}));
|
||||
await saveMenuAuthority(menuAuthorityData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" onClick={(e) => e.stopPropagation()}>
|
||||
메뉴권한
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[40rem] overflow-auto">{tableComponent}</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuAuthorityDialog };
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { RoleApi } from "@/entities/role";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
type RoleSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
|
||||
mngrYn?: string;
|
||||
};
|
||||
|
||||
function RoleSelect({ mngrYn = "N", ...props }: RoleSelectProps) {
|
||||
const { data: roles } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES],
|
||||
queryFn: () => RoleApi.getRoles(),
|
||||
});
|
||||
|
||||
const options = roles?.content.filter((role) => role.mngrYn === mngrYn);
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="w-[24rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options?.map((item) => (
|
||||
<SelectItem key={item.roleId} value={item.roleId}>
|
||||
{item.roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export { RoleSelect };
|
||||
@@ -31,7 +31,12 @@ export const columns: ColumnDef<Image>[] = [
|
||||
{
|
||||
accessorKey: "imgSn",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
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: "imgTypeCd",
|
||||
|
||||
@@ -5,13 +5,13 @@ import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { columns } from "../model/image-table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
@@ -32,41 +32,45 @@ function ImagePage() {
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: imageList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES],
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, pagination],
|
||||
queryFn: () =>
|
||||
ImageApi.getImageList({
|
||||
...imageQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: imageList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: imageList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Image) => {
|
||||
router.push(`/image/register/${row.imgSn}`);
|
||||
};
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
@@ -83,10 +87,6 @@ function ImagePage() {
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
@@ -114,6 +114,7 @@ function ImagePage() {
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.IMAGE}
|
||||
postingTargets={postingTargets}
|
||||
resetSelectedPost={() => table.setRowSelection({})}
|
||||
/>
|
||||
<Button onClick={() => router.push("/image/register")}>
|
||||
등록
|
||||
@@ -121,9 +122,7 @@ function ImagePage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,15 +5,11 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components";
|
||||
import { Button } from "@common/components";
|
||||
import { PostingApi, type PostPayload } from "@/entities/posting";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useMutationWrapper, useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { columns } from "../model/posting-table-columns";
|
||||
@@ -33,7 +29,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
queryFn: () => ImageApi.getImageInfo(sequential),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
queryFn: () =>
|
||||
PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(sequential) }),
|
||||
@@ -42,7 +38,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const { mutateAsync: updateImage } = useMutationWrapper({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.updateImage(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,15 +64,17 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
values: imageDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
@@ -118,7 +116,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { LoginlogPage } from "./ui/loginlog-page";
|
||||
@@ -0,0 +1,49 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { Loginlog } from "@/entities/loginlog";
|
||||
|
||||
|
||||
export const columns: ColumnDef<Loginlog>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const { pageIndex, pageSize } = table.getState().pagination;
|
||||
return totalRows - (pageIndex * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "lgnDt",
|
||||
header: "등록일",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.lgnDt);
|
||||
return date.format("YYYY-MM-DD HH24:mi:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "로그인IP",
|
||||
header: "remoteIp",
|
||||
},
|
||||
{
|
||||
accessorKey: "lgnMthd",
|
||||
header: "접속방식",
|
||||
},
|
||||
{
|
||||
accessorKey: "오류여부",
|
||||
header: "errOcrnYn",
|
||||
},
|
||||
{
|
||||
accessorKey: "오류코드",
|
||||
header: "lgnErrCd",
|
||||
},
|
||||
{
|
||||
accessorKey: "사이트ID",
|
||||
header: "siteId",
|
||||
}
|
||||
];
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
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";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
type LoginlogPageProps = {
|
||||
lgnSn?: string;
|
||||
};
|
||||
|
||||
function LoginlogPage({ }: LoginlogPageProps) {
|
||||
const [loginlogQuery, setLoginlogQuery] = useState<LoginlogParams>({
|
||||
userId: "",
|
||||
siteId: "",
|
||||
});
|
||||
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 table = useReactTable({
|
||||
data: loginlogList?.content ?? [],
|
||||
columns,
|
||||
rowCount: loginlogList?.totalElements ?? 0,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
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>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={loginlogQuery.userId}
|
||||
onChange={(e) =>
|
||||
setLoginlogQuery({
|
||||
...loginlogQuery,
|
||||
userId: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">로그인로그 목록</h3>
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-35rem)] overflow-auto !p-0">
|
||||
<DataTable table={table} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginlogPage };
|
||||
@@ -6,7 +6,12 @@ export const columns: ColumnDef<BoardPopup>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
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: "bbsNm",
|
||||
|
||||
@@ -7,7 +7,12 @@ export const columns: ColumnDef<Contents>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
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);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "contsTtl",
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
@@ -16,6 +11,7 @@ import {
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { BoardApi, type BoardPopup } from "@/entities/board";
|
||||
import { columns } from "../model/board-table-columns";
|
||||
|
||||
@@ -30,21 +26,23 @@ function BoardSearchDialog({
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [bbsNm, setBbsNm] = useState("");
|
||||
|
||||
const { data: boardList, refetch } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
queryFn: () => BoardApi.getBoardPopupList({ bbsNm }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: boardList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,7 +53,7 @@ function BoardSearchDialog({
|
||||
}, [table.getSelectedRowModel().rows]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
query.refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -78,13 +76,13 @@ function BoardSearchDialog({
|
||||
value={bbsNm}
|
||||
onChange={(e) => setBbsNm(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<Button onClick={() => query.refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
@@ -16,6 +11,7 @@ import {
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ContentsApi, type Contents } from "@/entities/content";
|
||||
import { columns } from "../model/contents-table-colums";
|
||||
|
||||
@@ -30,21 +26,23 @@ function ContentSearchDialog({
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [contsTtl, setContsTtl] = useState("");
|
||||
|
||||
const { data: contentList, refetch } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS],
|
||||
queryFn: () => ContentsApi.getContentsList({ contsTtl }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: contentList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,7 +53,7 @@ function ContentSearchDialog({
|
||||
}, [table.getSelectedRowModel().rows]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
query.refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -78,13 +76,13 @@ function ContentSearchDialog({
|
||||
value={contsTtl}
|
||||
onChange={(e) => setContsTtl(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<Button onClick={() => query.refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -7,7 +7,12 @@ export const columns: ColumnDef<Authorizations>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
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: "authorizationName",
|
||||
|
||||
@@ -2,15 +2,10 @@ import { useState, memo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
@@ -32,7 +27,7 @@ import {
|
||||
type RoleAuthorizationsData,
|
||||
} from "@/entities/role-authorization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useMutationWrapper, useDataTable } from "@/shared/lib";
|
||||
import { createColumns } from "../model/role-authorizations-table-columns";
|
||||
import type { Authorizations } from "@/entities/authorization";
|
||||
|
||||
@@ -42,25 +37,25 @@ type ManagementAuthorityDialogProps = {
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
const ManagementAuthorityDialog = memo(
|
||||
({ authorizationNo, rowData, ...props }: ManagementAuthorityDialogProps) => {
|
||||
({ authorizationNo, rowData }: ManagementAuthorityDialogProps) => {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: roleAuthorizationList } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, authorizationNo],
|
||||
queryFn: () =>
|
||||
RoleAuthorizationsApi.getRoleAuthorizationsList({ authorizationNo }),
|
||||
enabled: !!authorizationNo && isOpened,
|
||||
enabled: !!authorizationNo && open,
|
||||
});
|
||||
|
||||
const { mutateAsync: saveRoleAuthorization } = useMutationWrapper({
|
||||
mutationFn: (data: RoleAuthorizationsData) =>
|
||||
RoleAuthorizationsApi.createRoleAuthorizations(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
alert("저장되었습니다.", () => setOpen(false));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,24 +63,24 @@ const ManagementAuthorityDialog = memo(
|
||||
resolver: zodResolver(roleAuthorizationsFormSchema),
|
||||
values: {
|
||||
roleAuthorizations:
|
||||
roleAuthorizationList?.content.map(
|
||||
({ roleId, authorizationNo, useYn }) => ({
|
||||
roleId,
|
||||
authorizationNo,
|
||||
authYn: useYn,
|
||||
}),
|
||||
) ?? [],
|
||||
query.data?.content.map(({ roleId, authorizationNo, useYn }) => ({
|
||||
roleId,
|
||||
authorizationNo,
|
||||
authYn: useYn,
|
||||
})) ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: roleAuthorizationList?.content ?? [],
|
||||
columns: createColumns(form),
|
||||
state: {
|
||||
rowSelection,
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns(form),
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -96,11 +91,9 @@ const ManagementAuthorityDialog = memo(
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" onClick={() => setIsOpened(true)}>
|
||||
관리
|
||||
</Button>
|
||||
<Button size="sm">관리</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
@@ -129,9 +122,7 @@ const ManagementAuthorityDialog = memo(
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="max-h-[40rem] overflow-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
<div className="max-h-[40rem] overflow-auto">{tableComponent}</div>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import { ModuleForm } from "./module-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: authorizationInfo } = useQuery({
|
||||
@@ -24,7 +26,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
mutationFn: (data: AuthorizationFormData) =>
|
||||
AuthorizationsApi.updateAuthorization(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
AuthorizationsApi,
|
||||
type Authorizations,
|
||||
@@ -31,8 +28,8 @@ function ModulePage() {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: authorizationsList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, pagination],
|
||||
queryFn: () =>
|
||||
AuthorizationsApi.getAuthorizationsList({
|
||||
...authorizationQuery,
|
||||
@@ -40,19 +37,27 @@ function ModulePage() {
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: authorizationsList?.content ?? [],
|
||||
columns,
|
||||
rowCount: authorizationsList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
const handleRowClick = (row: Authorizations) => {
|
||||
router.push(`/module/register/${row.authorizationNo}`);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleAuthorizationQueryChange =
|
||||
@@ -62,17 +67,9 @@ function ModulePage() {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Authorizations) => {
|
||||
router.push(`/module/register/${row.authorizationNo}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
@@ -113,9 +110,7 @@ function ModulePage() {
|
||||
<h3 className="text-2xl font-medium text-label">모듈API 목록</h3>
|
||||
<Button onClick={() => router.push("/module/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+18
-23
@@ -1,13 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
@@ -22,32 +17,32 @@ import {
|
||||
type SearchUser,
|
||||
} from "@/entities/user-organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/additional-user-table-columns";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/add-user-table-columns";
|
||||
|
||||
type AdditionalUserDialogProps = {
|
||||
type AddUserDialogProps = {
|
||||
onAddUsers: (user: SearchUser[]) => void;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function AdditionalUserDialog({
|
||||
onAddUsers,
|
||||
...props
|
||||
}: AdditionalUserDialogProps) {
|
||||
function AddUserDialog({ onAddUsers, ...props }: AddUserDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [userNm, setUserNm] = useState("");
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: userList, refetch } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION],
|
||||
queryFn: () => UserOrganizationApi.searchUser({ userNm }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: userList?.content ?? [],
|
||||
columns,
|
||||
state: { rowSelection },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { rowSelection },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddUsers = () => {
|
||||
@@ -65,7 +60,7 @@ function AdditionalUserDialog({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) refetch();
|
||||
if (open) query.refetch();
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
@@ -87,13 +82,13 @@ function AdditionalUserDialog({
|
||||
value={userNm}
|
||||
onChange={(e) => setUserNm(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<Button onClick={() => query.refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
@@ -111,4 +106,4 @@ function AdditionalUserDialog({
|
||||
);
|
||||
}
|
||||
|
||||
export { AdditionalUserDialog };
|
||||
export { AddUserDialog };
|
||||
@@ -37,7 +37,7 @@ import { CodeApi, type ICode } from "@/entities/code";
|
||||
import type { OrganizationTree } from "@/entities/organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { AdditionalUserDialog } from "./additional-user-dialog";
|
||||
import { AddUserDialog } from "./add-user-dialog";
|
||||
import { MoveOrganizationDialog } from "./move-organization-dialog";
|
||||
|
||||
type UserOrganizationSectionProps = {
|
||||
@@ -114,6 +114,12 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const formData = form.watch("userOrganization");
|
||||
const formValidation = userOrganizationFormSchema.safeParse({
|
||||
userOrganization: formData,
|
||||
});
|
||||
const isFormValid = formValidation.success;
|
||||
|
||||
const handleAddUsers = (users: SearchUser[]) => {
|
||||
const generateUserData = (user: SearchUser) => ({
|
||||
mode: "C",
|
||||
@@ -220,7 +226,7 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
|
||||
}
|
||||
handleMoveOrganization={handleMoveOrganization}
|
||||
/>
|
||||
<AdditionalUserDialog onAddUsers={handleAddUsers} />
|
||||
<AddUserDialog onAddUsers={handleAddUsers} />
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
@@ -229,7 +235,7 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSaveUsers}>
|
||||
<Button size="sm" onClick={handleSaveUsers} disabled={!isFormValid}>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { DataTable } from "@common/components";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { PostingApi, type PostPayload } from "@/entities/posting";
|
||||
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
|
||||
import {
|
||||
useAlertStore,
|
||||
useUserStore,
|
||||
useMutationWrapper,
|
||||
useDataTable,
|
||||
} from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
popupUpdateFormSchema,
|
||||
@@ -39,7 +39,7 @@ function ModifyPage({ popupSn }: { popupSn: number }) {
|
||||
enabled: !!popupSn,
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, popupSn],
|
||||
queryFn: () => PostingApi.getPostingHistory({ pstgTrgtNo: popupSn }),
|
||||
});
|
||||
@@ -70,15 +70,17 @@ function ModifyPage({ popupSn }: { popupSn: number }) {
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
@@ -117,7 +119,7 @@ function ModifyPage({ popupSn }: { popupSn: number }) {
|
||||
이력삭제
|
||||
</AuthorityButton>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,14 @@ import { useRouter } from "next/navigation";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { PopupApi } from "../api/popup-service";
|
||||
import { columns } from "../model/table-columns";
|
||||
import type { Popup, PopupParams } from "../model/popup-interface";
|
||||
@@ -35,8 +35,8 @@ function PopupPage() {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
|
||||
const { data: popupList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS],
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, pagination],
|
||||
queryFn: () =>
|
||||
PopupApi.getPopupList({
|
||||
...popupParams,
|
||||
@@ -48,25 +48,30 @@ function PopupPage() {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Popup) => {
|
||||
router.push(`/popup/register/${row.popupSn}`);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: popupList?.content ?? [],
|
||||
columns,
|
||||
rowCount: popupList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -111,6 +116,7 @@ function PopupPage() {
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.POPUP}
|
||||
postingTargets={postingTargets}
|
||||
resetSelectedPost={() => table.setRowSelection({})}
|
||||
/>
|
||||
<AuthorityButton
|
||||
authority="regAuthrtYn"
|
||||
@@ -120,9 +126,7 @@ function PopupPage() {
|
||||
</AuthorityButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ function ViewPopup({ popupSn }: { popupSn: string }) {
|
||||
}, [popupSn]);
|
||||
|
||||
return (
|
||||
<div className="h-svh bg-container p-4">
|
||||
<div className="bg-container p-4">
|
||||
<div className="card card-border !m-0">
|
||||
<div className="card-header">
|
||||
<h1 className="text-3xl font-bold text-foreground">
|
||||
|
||||
@@ -32,6 +32,12 @@ export const columns: ColumnDef<RecentPost>[] = [
|
||||
{
|
||||
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);
|
||||
},
|
||||
meta: {
|
||||
width: 80,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -11,22 +12,41 @@ import {
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import type { Board } from "@/entities/board";
|
||||
import type { MovePost } from "@/entities/post";
|
||||
import { PostApi, type MovePost } from "@/entities/post";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
type MovePostDialogProps = {
|
||||
selectedPosts: MovePost[];
|
||||
boardList: Board[];
|
||||
onMovePost: (data: MovePost[]) => void;
|
||||
resetSelectedPost: () => void;
|
||||
};
|
||||
|
||||
function MovePostDialog({
|
||||
selectedPosts,
|
||||
boardList,
|
||||
onMovePost,
|
||||
resetSelectedPost,
|
||||
}: MovePostDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const selectedPostsRef = useRef<MovePost[]>(selectedPosts);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutate: movePost } = useMutationWrapper({
|
||||
mutationFn: (data: MovePost[]) => PostApi.movePost(data),
|
||||
onSuccess: () => {
|
||||
alert("이동이 완료되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||
});
|
||||
resetSelectedPost();
|
||||
setOpen(false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSelectValueChange = (toBbsId: string) => {
|
||||
selectedPostsRef.current = selectedPosts.map((post) => ({
|
||||
...post,
|
||||
@@ -35,7 +55,7 @@ function MovePostDialog({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<AuthorityButton
|
||||
authority="mngAuthrtYn"
|
||||
@@ -61,9 +81,7 @@ function MovePostDialog({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button onClick={() => onMovePost(selectedPostsRef.current)}>
|
||||
이동
|
||||
</Button>
|
||||
<Button onClick={() => movePost(selectedPostsRef.current)}>이동</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
@@ -21,18 +20,16 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
DataTable,
|
||||
} from "@common/components";
|
||||
import {
|
||||
PostApi,
|
||||
type DeletePost,
|
||||
type MovePost,
|
||||
type RecentPost,
|
||||
type RecentPostParams,
|
||||
} from "@/entities/post";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { useAlertStore, useMutationWrapper, usePrompt } from "@/shared/lib";
|
||||
import { useAlertStore, usePrompt, useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { MovePostDialog } from "./move-post-dialog";
|
||||
@@ -53,7 +50,7 @@ function RecentPage() {
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [recentPostParams, setRecentPostParams] = useState<RecentPostParams>({
|
||||
const [recentPostQuery, setRecentPostQuery] = useState<RecentPostParams>({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
bbsId: null,
|
||||
@@ -63,11 +60,11 @@ function RecentPage() {
|
||||
});
|
||||
const [isDeletePostData, setIsDeletePostData] = useState(false);
|
||||
|
||||
const { data: recentPostList, refetch } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, pagination],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
...recentPostParams,
|
||||
...recentPostQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
@@ -79,45 +76,43 @@ function RecentPage() {
|
||||
queryFn: () => BoardApi.getBoardList(),
|
||||
});
|
||||
|
||||
const { mutate: movePost } = useMutationWrapper({
|
||||
mutationFn: (data: MovePost[]) => PostApi.movePost(data),
|
||||
onSuccess: () => {
|
||||
alert("이동이 완료되었습니다.", refetch);
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: recentPostList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: recentPostList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDeletePostData(recentPostParams.delYn === "Y");
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: RecentPost) => {
|
||||
router.push(
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { tableComponent, tableInstance: table } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDeletePostData(recentPostQuery.delYn === "Y");
|
||||
query.refetch();
|
||||
};
|
||||
|
||||
const handleRecentPostParamsChange =
|
||||
(key: keyof RecentPostParams) => (value: unknown) => {
|
||||
setRecentPostParams({ ...recentPostParams, [key]: value });
|
||||
setRecentPostQuery({ ...recentPostQuery, [key]: value });
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
@@ -144,7 +139,16 @@ function RecentPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setDateRange(presetDateRange);
|
||||
handleDateRangeChange(presetDateRange);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (value: DateRange | undefined) => {
|
||||
setDateRange(value);
|
||||
setRecentPostQuery({
|
||||
...recentPostQuery,
|
||||
frDateYmd: dayjs(value?.from).format("YYYYMMDD") ?? "",
|
||||
toDateYmd: dayjs(value?.to).format("YYYYMMDD") ?? "",
|
||||
});
|
||||
};
|
||||
|
||||
const onSoftDeletePost = async () => {
|
||||
@@ -160,7 +164,7 @@ function RecentPage() {
|
||||
delRsn,
|
||||
}));
|
||||
PostApi.softDeletePost(selectedPosts);
|
||||
alert("삭제가 완료되었습니다.", refetch);
|
||||
alert("삭제가 완료되었습니다.", query.refetch);
|
||||
setRowSelection({});
|
||||
};
|
||||
|
||||
@@ -177,7 +181,7 @@ function RecentPage() {
|
||||
pstNo: row.original.pstNo,
|
||||
}));
|
||||
PostApi.hardDeletePost(selectedPosts);
|
||||
alert("삭제가 완료되었습니다.", refetch);
|
||||
alert("삭제가 완료되었습니다.", query.refetch);
|
||||
setRowSelection({});
|
||||
}
|
||||
};
|
||||
@@ -191,7 +195,7 @@ function RecentPage() {
|
||||
pstNo: row.original.pstNo,
|
||||
}));
|
||||
PostApi.restorePost(selectedPosts);
|
||||
alert("복원이 완료되었습니다.", refetch);
|
||||
alert("복원이 완료되었습니다.", query.refetch);
|
||||
setRowSelection({});
|
||||
}
|
||||
};
|
||||
@@ -201,7 +205,10 @@ function RecentPage() {
|
||||
<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={setDateRange}>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={handleDateRangeChange}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
@@ -243,7 +250,7 @@ function RecentPage() {
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={recentPostParams.keywordType}
|
||||
value={recentPostQuery.keywordType}
|
||||
onValueChange={handleRecentPostParamsChange("keywordType")}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
@@ -256,7 +263,7 @@ function RecentPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={recentPostParams.keyword}
|
||||
value={recentPostQuery.keyword}
|
||||
onChange={(e) =>
|
||||
handleRecentPostParamsChange("keyword")(e.target.value)
|
||||
}
|
||||
@@ -300,7 +307,7 @@ function RecentPage() {
|
||||
pstNo: row.original.pstNo,
|
||||
}))}
|
||||
boardList={boardList?.content ?? []}
|
||||
onMovePost={movePost}
|
||||
resetSelectedPost={() => table.setRowSelection({})}
|
||||
/>
|
||||
<AuthorityButton
|
||||
authority="delAuthrtYn"
|
||||
@@ -332,9 +339,7 @@ function RecentPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import type { Role } from "@/entities/role";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { MappingRoleDialog } from "../ui/mapping-role-dialog";
|
||||
import type { Role } from "./role-interface";
|
||||
|
||||
export const columns: ColumnDef<Role>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
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: "roleId",
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { RoleApi, roleFormSchema, type RoleFormData } from "@/entities/role";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { RoleForm } from "./role-form";
|
||||
import { RoleApi } from "../api/role-service";
|
||||
import { roleFormSchema, type RoleFormData } from "../model/role-interface";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -11,7 +7,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Button,
|
||||
DataTable,
|
||||
Switch,
|
||||
} from "@common/components";
|
||||
import {
|
||||
@@ -20,7 +15,7 @@ import {
|
||||
type RoleAuthorizationsByRoleId,
|
||||
} from "@/entities/role-authorization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useMutationWrapper, useDataTable } from "@/shared/lib";
|
||||
|
||||
type MappingRoleDialogProps = {
|
||||
roleId: string;
|
||||
@@ -37,7 +32,7 @@ function MappingRoleDialog({
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: authorizationList } = useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||
queryFn: () =>
|
||||
RoleAuthorizationsApi.getRoleAuthorizationsListByRoleId({
|
||||
@@ -63,7 +58,7 @@ function MappingRoleDialog({
|
||||
|
||||
const handleApplyAll = (authYn: "Y" | "N") => {
|
||||
mutate(
|
||||
authorizationList?.content.map((authorization) => ({
|
||||
query.data?.content.map((authorization) => ({
|
||||
roleId,
|
||||
authorizationNo: authorization.authorizationNo,
|
||||
authYn,
|
||||
@@ -71,10 +66,12 @@ function MappingRoleDialog({
|
||||
);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: authorizationList?.content ?? [],
|
||||
columns: createColumns(isPending, applyRoleAuthorization),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns(isPending, applyRoleAuthorization),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -95,7 +92,7 @@ function MappingRoleDialog({
|
||||
일괄해제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
{tableComponent}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,11 +4,10 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { RoleApi, roleFormSchema, type RoleFormData } from "@/entities/role";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { RoleForm } from "./role-form";
|
||||
import { RoleApi } from "../api/role-service";
|
||||
import { roleFormSchema, type RoleFormData } from "../model/role-interface";
|
||||
|
||||
function ModifyPage({ roleId }: { roleId: string }) {
|
||||
const router = useRouter();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user