diff --git a/apps/cs/.env.example b/apps/cs/.env.example new file mode 100644 index 000000000..8020ee321 --- /dev/null +++ b/apps/cs/.env.example @@ -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= \ No newline at end of file diff --git a/apps/cs/.gitignore b/apps/cs/.gitignore index e8d8437ee..b63718b61 100644 --- a/apps/cs/.gitignore +++ b/apps/cs/.gitignore @@ -37,4 +37,5 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.env* \ No newline at end of file +.env* +!.env.example \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/admin/page.tsx b/apps/cs/app/(with-base-layout)/admin/page.tsx new file mode 100644 index 000000000..a7633e201 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/admin/page.tsx @@ -0,0 +1,2 @@ +import { AdminPage } from "@/pages/admin"; +export default AdminPage; diff --git a/apps/cs/app/(with-base-layout)/groups/page.tsx b/apps/cs/app/(with-base-layout)/groups/page.tsx new file mode 100644 index 000000000..8491e59bb --- /dev/null +++ b/apps/cs/app/(with-base-layout)/groups/page.tsx @@ -0,0 +1,2 @@ +import { GroupsPage } from "@/pages/groups"; +export default GroupsPage; diff --git a/apps/cs/app/(with-base-layout)/loginlog/page.tsx b/apps/cs/app/(with-base-layout)/loginlog/page.tsx new file mode 100644 index 000000000..960f03e05 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/loginlog/page.tsx @@ -0,0 +1,2 @@ +import { LoginlogPage } from "@/pages/loginlog"; +export default LoginlogPage; diff --git a/apps/cs/app/(with-base-layout)/users/[userUnqId]/modify/page.tsx b/apps/cs/app/(with-base-layout)/users/[userUnqId]/modify/page.tsx new file mode 100644 index 000000000..a01387c44 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/users/[userUnqId]/modify/page.tsx @@ -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 ; +} diff --git a/apps/cs/app/(with-base-layout)/users/[userUnqId]/page.tsx b/apps/cs/app/(with-base-layout)/users/[userUnqId]/page.tsx new file mode 100644 index 000000000..da4e79b00 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/users/[userUnqId]/page.tsx @@ -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 ; +} diff --git a/apps/cs/app/(with-base-layout)/users/register/page.tsx b/apps/cs/app/(with-base-layout)/users/register/page.tsx new file mode 100644 index 000000000..31e36e41b --- /dev/null +++ b/apps/cs/app/(with-base-layout)/users/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/users"; +export default CreatePage; diff --git a/apps/cs/app/api/getClientIp/route.ts b/apps/cs/app/api/getClientIp/route.ts new file mode 100644 index 000000000..ec9ff72d5 --- /dev/null +++ b/apps/cs/app/api/getClientIp/route.ts @@ -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, + }); +} diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index d56c1f8ab..6bdfd9545 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -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*", "/"], }; diff --git a/apps/cs/public/favicon.ico b/apps/cs/public/favicon.ico new file mode 100644 index 000000000..d304b6981 Binary files /dev/null and b/apps/cs/public/favicon.ico differ diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index 1a2189b03..b8fabff3d 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -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 ( - - - - - - - {children} - - - - - - - - - -
- -
+ + + + + + + + {children} + + + + + + + + + +
+ +
+
); }; diff --git a/apps/cs/src/entities/code/api/lwpocode-service.ts b/apps/cs/src/entities/code/api/lwpocode-service.ts index b8f11fa22..818ddb120 100644 --- a/apps/cs/src/entities/code/api/lwpocode-service.ts +++ b/apps/cs/src/entities/code/api/lwpocode-service.ts @@ -62,6 +62,12 @@ export class LwpoCodeApiService implements LwpoCodeService { `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, ); } + + public async codeRefreshAll(): Promise { + await client.post( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/cache/refresh-all`, + ); + } } export const LwpoCodeApi = new LwpoCodeApiService(); diff --git a/apps/cs/src/entities/code/model/lwpocode-interface.ts b/apps/cs/src/entities/code/model/lwpocode-interface.ts index 785d86e1e..2538c1c30 100644 --- a/apps/cs/src/entities/code/model/lwpocode-interface.ts +++ b/apps/cs/src/entities/code/model/lwpocode-interface.ts @@ -65,4 +65,5 @@ export interface LwpoCodeService { lwpoCd: string, data: LwpoCodeFormData, ): Promise; + codeRefreshAll(): Promise; } diff --git a/apps/cs/src/entities/code/ui/code-select.tsx b/apps/cs/src/entities/code/ui/code-select.tsx index 850282375..08df4ce51 100644 --- a/apps/cs/src/entities/code/ui/code-select.tsx +++ b/apps/cs/src/entities/code/ui/code-select.tsx @@ -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 ( - - + + {sites?.map((item) => ( diff --git a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts index 25d7c7032..4815541ee 100644 --- a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts +++ b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts @@ -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(), }); diff --git a/apps/cs/src/entities/user/api/user-service.ts b/apps/cs/src/entities/user/api/user-service.ts index d7718c523..482d51e16 100644 --- a/apps/cs/src/entities/user/api/user-service.ts +++ b/apps/cs/src/entities/user/api/user-service.ts @@ -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 { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/admin/list`, + { params }, + ); + return response.data; + } } export const UserApi = new UserApiService(); diff --git a/apps/cs/src/entities/user/index.ts b/apps/cs/src/entities/user/index.ts index 937942481..7b4615e01 100644 --- a/apps/cs/src/entities/user/index.ts +++ b/apps/cs/src/entities/user/index.ts @@ -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"; diff --git a/apps/cs/src/entities/user/model/user-interface.ts b/apps/cs/src/entities/user/model/user-interface.ts index 7860039b4..69320e8c7 100644 --- a/apps/cs/src/entities/user/model/user-interface.ts +++ b/apps/cs/src/entities/user/model/user-interface.ts @@ -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; // 페이지네이션 타입 정의 export type UserList = PageableList; export type UserSttsStatsList = UserSttsStatsResponse[]; +export type AdminUserList = PageableList; // 사용자 서비스 인터페이스 export interface UserService { @@ -305,4 +355,7 @@ export interface UserService { // 사용자 아이디 중복 체크 existsUserId: (data: UserIdData) => Promise; + + // 관리자 목록 조회 + findPageAdminUser: (params: UserSearchParams) => Promise; } diff --git a/apps/cs/src/entities/user/ui/password-reset-dialog.tsx b/apps/cs/src/entities/user/ui/password-reset-dialog.tsx new file mode 100644 index 000000000..0d6aaf94e --- /dev/null +++ b/apps/cs/src/entities/user/ui/password-reset-dialog.tsx @@ -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({ + 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 ( + + {children} + + + 비밀번호 초기화 + +
+ onPasswordReset(data))} + className="space-y-6" + > + ( + + 새 비밀번호 + + + + + + )} + /> + ( + + 새 비밀번호 확인 + + + + + + )} + /> + + + + + + + + +
+
+ ); +} diff --git a/apps/cs/src/pages/(post)/notice/model/notice-table-columns.tsx b/apps/cs/src/pages/(post)/notice/model/notice-table-columns.tsx index 0926a2d9d..e6feb6952 100644 --- a/apps/cs/src/pages/(post)/notice/model/notice-table-columns.tsx +++ b/apps/cs/src/pages/(post)/notice/model/notice-table-columns.tsx @@ -8,6 +8,12 @@ export const columns: ColumnDef[] = [ { 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", diff --git a/apps/cs/src/pages/admin/api/manager-ip-service.ts b/apps/cs/src/pages/admin/api/manager-ip-service.ts new file mode 100644 index 000000000..6275fb714 --- /dev/null +++ b/apps/cs/src/pages/admin/api/manager-ip-service.ts @@ -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 { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MANAGER_IP}`, + { params }, + ); + return response.data; + } + + public async getManagerIpInfo(mngNo: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`, + ); + return response.data; + } + + public async saveManagerIp(data: ManagerIp[]): Promise { + const response = await client.post( + ENDPOINT.CMS_SERVICE.MANAGER_IP, + data, + ); + return response.data; + } + + public async updateManagerIp(mngNo: string, data: ManagerIp): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`, + data, + ); + return response.data; + } + + public async deleteManagerIp(mngNo: string): Promise { + await client.delete(`${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`); + } +} + +export const ManagerIpApi = new ManagerIpApiService(); diff --git a/apps/cs/src/pages/admin/index.ts b/apps/cs/src/pages/admin/index.ts new file mode 100644 index 000000000..b8f41285c --- /dev/null +++ b/apps/cs/src/pages/admin/index.ts @@ -0,0 +1 @@ +export { AdminPage } from "./ui/admin-page"; diff --git a/apps/cs/src/pages/admin/model/manager-ip-interface.ts b/apps/cs/src/pages/admin/model/manager-ip-interface.ts new file mode 100644 index 000000000..f698db533 --- /dev/null +++ b/apps/cs/src/pages/admin/model/manager-ip-interface.ts @@ -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; +export type ManagerIpFormData = z.infer; +export type ManagerIpList = PageableList; +export type ManagerIpInfo = ManagerIp; + +export interface ManagerIpService { + getManagerIpList: (params: ManagerIpParams) => Promise; + getManagerIpInfo: (mngNo: string) => Promise; + saveManagerIp: (data: ManagerIp[]) => Promise; + updateManagerIp: (mngNo: string, data: ManagerIp) => Promise; + deleteManagerIp: (mngNo: string) => Promise; +} diff --git a/apps/cs/src/pages/admin/model/table-columns.tsx b/apps/cs/src/pages/admin/model/table-columns.tsx new file mode 100644 index 000000000..dee012995 --- /dev/null +++ b/apps/cs/src/pages/admin/model/table-columns.tsx @@ -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[] = [ + { + 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 ( + + ); + }, + }, +]; + +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 ( +
+ + + + + +
+ ); + }, + (prevProps, nextProps) => { + return ( + prevProps.userUnqId === nextProps.userUnqId && + prevProps.groupId === nextProps.groupId + ); + }, +); + +ActionCell.displayName = "ActionCell"; diff --git a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx new file mode 100644 index 000000000..327473932 --- /dev/null +++ b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx @@ -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({ + keywordType: "", + keyword: "", + }); + const [selectedGroupId, setSelectedGroupId] = useState(""); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [rowSelection, setRowSelection] = useState({}); + + 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[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> + ), + cell: ({ row }) => ( + 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) => { + e.preventDefault(); + e.stopPropagation(); + setPagination({ pageIndex: 0, pageSize: 10 }); + query.refetch(); + }; + + return ( + + + + + + + 관리자 등록 + + +
+ + + setUserSearchQuery({ + ...userSearchQuery, + keyword: e.target.value, + }) + } + className="!text-[1.3rem]" + placeholder="검색어 입력" + /> + +
+
+ + {tableComponent} +
+
+ + +
+ + + + +
+
+ ); +} + +export { AddAdminDialog }; diff --git a/apps/cs/src/pages/admin/ui/admin-page.tsx b/apps/cs/src/pages/admin/ui/admin-page.tsx new file mode 100644 index 000000000..a4cd5265d --- /dev/null +++ b/apps/cs/src/pages/admin/ui/admin-page.tsx @@ -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({ + keywordType: "", + keyword: "", + grpId: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [rowSelection, setRowSelection] = useState({}); + + 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 }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> + ), + cell: ({ row }: { row: Row }) => ( + 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) => { + 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 ( +
+
+
+
+ + +
+
+ + + + setAdminQuery({ ...adminQuery, keyword: e.target.value }) + } + placeholder="검색어 입력" + /> +
+ +
+
+
+
+
+

+ 관리자 목록(총 {query.data?.totalElements}명) +

+
+ + +
+
+
+
{tableComponent}
+
+
+ ); +} + +export { AdminPage }; diff --git a/apps/cs/src/pages/admin/ui/manager-ip-dialog.tsx b/apps/cs/src/pages/admin/ui/manager-ip-dialog.tsx new file mode 100644 index 000000000..a97947dc5 --- /dev/null +++ b/apps/cs/src/pages/admin/ui/manager-ip-dialog.tsx @@ -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([]); + + 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({ + 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 ( + + + + + + + 접속 IP 관리 + +
+ { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + > +
+ + +
+
+ + + + + + + + + + + IP 주소 + + + + {fields.map((field, i) => { + if (field.mode === "D") return null; + return ( + + + handleSelectRow(i)} + /> + + + ( + + + + + + )} + /> + + + ); + })} + +
+
+ + + + + + +
+ +
+
+ ); +} + +export { ManagerIpDialog }; diff --git a/apps/cs/src/pages/banner/ui/banner-page.tsx b/apps/cs/src/pages/banner/ui/banner-page.tsx index f2dd98d51..db5a5cc15 100644 --- a/apps/cs/src/pages/banner/ui/banner-page.tsx +++ b/apps/cs/src/pages/banner/ui/banner-page.tsx @@ -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) => { 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 (
@@ -126,6 +126,7 @@ function BannerPage() { table.setRowSelection({})} />
-
- -
+
{tableComponent}
); diff --git a/apps/cs/src/pages/banner/ui/modify-page.tsx b/apps/cs/src/pages/banner/ui/modify-page.tsx index 070db0e46..fa9c17705 100644 --- a/apps/cs/src/pages/banner/ui/modify-page.tsx +++ b/apps/cs/src/pages/banner/ui/modify-page.tsx @@ -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 }) { 이력삭제 - + {tableComponent} diff --git a/apps/cs/src/pages/bbs/ui/bbs-page.tsx b/apps/cs/src/pages/bbs/ui/bbs-page.tsx index 6bde82cce..482966fc5 100644 --- a/apps/cs/src/pages/bbs/ui/bbs-page.tsx +++ b/apps/cs/src/pages/bbs/ui/bbs-page.tsx @@ -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) => { 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 (
@@ -134,9 +133,7 @@ function BbsPage() {
-
- -
+
{tableComponent}
); diff --git a/apps/cs/src/pages/bbs/ui/modify-page.tsx b/apps/cs/src/pages/bbs/ui/modify-page.tsx index 881928297..c65b823c2 100644 --- a/apps/cs/src/pages/bbs/ui/modify-page.tsx +++ b/apps/cs/src/pages/bbs/ui/modify-page.tsx @@ -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); }, }); diff --git a/apps/cs/src/pages/code/ui/lwpocode-section.tsx b/apps/cs/src/pages/code/ui/lwpocode-section.tsx index fd1df2d8e..78a51fae5 100644 --- a/apps/cs/src/pages/code/ui/lwpocode-section.tsx +++ b/apps/cs/src/pages/code/ui/lwpocode-section.tsx @@ -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) => { 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 (
@@ -80,15 +98,20 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {

하위코드 목록

+ + +
- + {tableComponent}
diff --git a/apps/cs/src/pages/code/ui/upcode-section.tsx b/apps/cs/src/pages/code/ui/upcode-section.tsx index 7e3baefc9..ffae0f28b 100644 --- a/apps/cs/src/pages/code/ui/upcode-section.tsx +++ b/apps/cs/src/pages/code/ui/upcode-section.tsx @@ -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) => { 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 (
@@ -91,7 +90,7 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
- + {tableComponent}
diff --git a/apps/cs/src/pages/contents/model/table-columns.tsx b/apps/cs/src/pages/contents/model/table-columns.tsx index 19d98a48b..714fa2230 100644 --- a/apps/cs/src/pages/contents/model/table-columns.tsx +++ b/apps/cs/src/pages/contents/model/table-columns.tsx @@ -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", diff --git a/apps/cs/src/pages/contents/ui/contents-page.tsx b/apps/cs/src/pages/contents/ui/contents-page.tsx index 4fc9ae798..45d9f100e 100644 --- a/apps/cs/src/pages/contents/ui/contents-page.tsx +++ b/apps/cs/src/pages/contents/ui/contents-page.tsx @@ -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) => { 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 (
@@ -89,14 +87,7 @@ function ContentsPage() { 등록
-
- -
+
{tableComponent}
); diff --git a/apps/cs/src/pages/file/model/table-columns.tsx b/apps/cs/src/pages/file/model/table-columns.tsx index 35d5507eb..fa30cb0fd 100644 --- a/apps/cs/src/pages/file/model/table-columns.tsx +++ b/apps/cs/src/pages/file/model/table-columns.tsx @@ -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", diff --git a/apps/cs/src/pages/file/ui/file-page.tsx b/apps/cs/src/pages/file/ui/file-page.tsx index 9b58b3828..67a313423 100644 --- a/apps/cs/src/pages/file/ui/file-page.tsx +++ b/apps/cs/src/pages/file/ui/file-page.tsx @@ -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({ - keyword: "", + orgnlFileNm: "", }); const [pagination, setPagination] = useState({ 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) => { e.preventDefault(); - refetch(); + query.refetch(); }; - useEffect(() => { - refetch(); - }, [pagination]); - return (
@@ -91,9 +86,9 @@ function FilePage() { - handleAttachmentQueryChange("keyword")(e.target.value) + handleAttachmentQueryChange("orgnlFileNm")(e.target.value) } placeholder="파일명을 입력하세요" /> @@ -108,9 +103,7 @@ function FilePage() {

파일 목록

-
- -
+
{tableComponent}
); diff --git a/apps/cs/src/pages/groups/index.ts b/apps/cs/src/pages/groups/index.ts new file mode 100644 index 000000000..269abb44a --- /dev/null +++ b/apps/cs/src/pages/groups/index.ts @@ -0,0 +1 @@ +export { GroupsPage } from "./ui/groups-page"; diff --git a/apps/cs/src/pages/groups/model/group-menu-authority-table-columns.tsx b/apps/cs/src/pages/groups/model/group-menu-authority-table-columns.tsx new file mode 100644 index 000000000..0a56a6701 --- /dev/null +++ b/apps/cs/src/pages/groups/model/group-menu-authority-table-columns.tsx @@ -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, +): ColumnDef[] => { + 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 ( + { + 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 ( + { + if (checked) { + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "mdfcnAuthrtYn", + header: "수정권한 ", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "delAuthrtYn", + header: "삭제권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "inqAuthrtYn", + header: "조회권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + ]; +}; diff --git a/apps/cs/src/pages/groups/model/group-users-table-columns.tsx b/apps/cs/src/pages/groups/model/group-users-table-columns.tsx new file mode 100644 index 000000000..8d1054aac --- /dev/null +++ b/apps/cs/src/pages/groups/model/group-users-table-columns.tsx @@ -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[] = [ + { + id: "select", + header: ({ table }: { table: Table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> + ), + cell: ({ row }: { row: Row }) => ( + 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; + } + }, + }, +]; diff --git a/apps/cs/src/pages/groups/model/groups-table-columns.tsx b/apps/cs/src/pages/groups/model/groups-table-columns.tsx new file mode 100644 index 000000000..70f91efa0 --- /dev/null +++ b/apps/cs/src/pages/groups/model/groups-table-columns.tsx @@ -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[] = [ + { + accessorKey: "groupId", + header: "그룹코드", + }, + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "siteNm", + header: "사이트", + }, + { + id: "actions", + header: "관리", + cell: ({ row }) => ( +
e.stopPropagation()} + className="flex justify-center gap-2" + > + + + + +
+ ), + }, +]; diff --git a/apps/cs/src/pages/groups/ui/add-user-dialog.tsx b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx new file mode 100644 index 000000000..aafba70db --- /dev/null +++ b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx @@ -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({ + userName: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [userRowSelection, setUserRowSelection] = useState( + {}, + ); + + 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[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> + ), + cell: ({ row }) => ( + 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 ( + + + + + + + 사용자 추가 + +
+ + setUserSearchQuery({ + ...userSearchQuery, + userName: e.target.value, + }) + } + className="!text-[1.3rem]" + placeholder="사용자 이름으로 검색" + /> + +
+ {tableComponent} + + + + +
+
+ ); +} + +export { AddUserDialog }; diff --git a/apps/cs/src/pages/groups/ui/group-form.tsx b/apps/cs/src/pages/groups/ui/group-form.tsx new file mode 100644 index 000000000..423718435 --- /dev/null +++ b/apps/cs/src/pages/groups/ui/group-form.tsx @@ -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; +}; + +function GroupForm({ form }: GroupFormProps) { + return ( + + + + + + + + 그룹코드 + + ( + + + + + + + )} + /> + + + + 그룹명 + + ( + + + + + + + )} + /> + + + + 그룹구분 + + ( + + + + + + + )} + /> + + + + 그룹설명 + + ( + + +