Merge commit '16541d6d54b044ec5f6bb60db6ef101401cb81ae' into iipbs/wr/form_fuction
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
@@ -7,15 +9,23 @@ import { MenuWrapper } from "@/entities/menu";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { initializeNextClientConfig } from "@common/config";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
initializeNextClientConfig();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export { AttachmentApi } from "./api/attachment-service";
|
||||
export type {
|
||||
AttachmentParams,
|
||||
Attachment,
|
||||
} from "./model/attachment-interface";
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Board,
|
||||
BoardFormData,
|
||||
BoardList,
|
||||
BoardParams,
|
||||
BoardPopupList,
|
||||
BoardPopupParams,
|
||||
BoardService,
|
||||
} from "../model/board-interface";
|
||||
|
||||
export class BoardApiService implements BoardService {
|
||||
public async getBoardList(params: BoardParams): Promise<BoardList> {
|
||||
const response = await client.get<BoardList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.BBS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getBoardPopupList(
|
||||
params: BoardPopupParams,
|
||||
): Promise<BoardPopupList> {
|
||||
@@ -16,6 +28,33 @@ export class BoardApiService implements BoardService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getBoardInfo(bbsId: string): Promise<Board> {
|
||||
const response = await client.get<Board>(
|
||||
`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createBoard(data: BoardFormData): Promise<Board> {
|
||||
const response = await client.post<Board>(
|
||||
`${ENDPOINT.BOARD_SERVICE.BBS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateBoard(bbsId: string, data: BoardFormData): Promise<Board> {
|
||||
const response = await client.put<Board>(
|
||||
`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteBoard(bbsId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const BoardApi = new BoardApiService();
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
export { BoardApi } from "./api/board-service";
|
||||
export { type BoardPopup } from "./model/board-interface";
|
||||
export {
|
||||
boardFormSchema,
|
||||
type Board,
|
||||
type BoardFormData,
|
||||
type BoardParams,
|
||||
type BoardPopup,
|
||||
} from "./model/board-interface";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const BOARD_TYPE = {
|
||||
@@ -9,13 +10,34 @@ export const BOARD_TYPE = {
|
||||
|
||||
export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE];
|
||||
|
||||
export interface BoardParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
bbsId?: number;
|
||||
bbsTypeCd?: string;
|
||||
bbsNm?: string;
|
||||
bbsExpln?: string;
|
||||
rfrncUseYn?: string;
|
||||
atflUseYn?: string;
|
||||
atflMaxCnt?: number;
|
||||
atflMaxFileSz?: number;
|
||||
ansUseYn?: string;
|
||||
cmntUseYn?: string;
|
||||
dgstfnEvlUseYn?: string;
|
||||
smsUseYn?: string;
|
||||
upndFixUseYn?: string;
|
||||
pstgPrdUseYn?: string;
|
||||
popupUseYn?: string;
|
||||
delYn?: string;
|
||||
}
|
||||
|
||||
export interface BoardPopupParams extends PageableParams {
|
||||
bbsNm?: string;
|
||||
}
|
||||
|
||||
export interface BoardPopup {
|
||||
export interface Board {
|
||||
bbsId: number;
|
||||
bbsTypeCd: BoardType;
|
||||
bbsTypeCd: string;
|
||||
bbsTypeNm: string;
|
||||
bbsNm: string;
|
||||
bbsExpln: string;
|
||||
@@ -24,6 +46,7 @@ export interface BoardPopup {
|
||||
atflMaxCnt: number;
|
||||
atflMaxFileSz: number;
|
||||
ansUseYn: string;
|
||||
ansUseAuthrt: string;
|
||||
cmntUseYn: string;
|
||||
dgstfnEvlUseYn: string;
|
||||
smsUseYn: string;
|
||||
@@ -31,12 +54,43 @@ export interface BoardPopup {
|
||||
pstgPrdUseYn: string;
|
||||
popupUseYn: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
useYn: string;
|
||||
mdfcnDt: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export interface BoardPopup extends Board {
|
||||
pstCnt: number;
|
||||
}
|
||||
|
||||
export const boardFormSchema = z.object({
|
||||
bbsTypeCd: z.string(),
|
||||
bbsNm: z.string(),
|
||||
bbsExpln: z.string(),
|
||||
rfrncUseYn: z.string(),
|
||||
atflUseYn: z.string(),
|
||||
atflMaxCnt: z.number().nullish(),
|
||||
atflMaxFileSz: z.number().nullish(),
|
||||
ansUseYn: z.string(),
|
||||
ansUseAuthrt: z.string().nullish(),
|
||||
cmntUseYn: z.string(),
|
||||
dgstfnEvlUseYn: z.string(),
|
||||
smsUseYn: z.string(),
|
||||
upndFixUseYn: z.string(),
|
||||
pstgPrdUseYn: z.string(),
|
||||
popupUseYn: z.string(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type BoardList = PageableList<Board>;
|
||||
export type BoardPopupList = PageableList<BoardPopup>;
|
||||
export type BoardFormData = z.infer<typeof boardFormSchema>;
|
||||
|
||||
export interface BoardService {
|
||||
getBoardList: (params: BoardParams) => Promise<BoardList>;
|
||||
getBoardPopupList: (params: BoardPopupParams) => Promise<BoardPopupList>;
|
||||
getBoardInfo: (bbsId: string) => Promise<Board>;
|
||||
createBoard: (data: BoardFormData) => Promise<Board>;
|
||||
updateBoard: (bbsId: string, data: BoardFormData) => Promise<Board>;
|
||||
deleteBoard: (bbsId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { CodeApi } from "./api/code-service";
|
||||
export type { ICode } from "./model/code-interface";
|
||||
export { CodeSelect } from "./ui/code-select";
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface ICode {
|
||||
lwpoCd: string;
|
||||
upCd: string;
|
||||
lwpoCdNm: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface CodeService {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
ContentsService,
|
||||
ContentsParams,
|
||||
ContentsList,
|
||||
ContentsFormData,
|
||||
ContentsVersionParams,
|
||||
ContentsVersionList,
|
||||
ContentsDetail,
|
||||
} from "../model/contents-interface";
|
||||
|
||||
export class ContentsApiService implements ContentsService {
|
||||
public async getContentsList(params: ContentsParams): Promise<ContentsList> {
|
||||
const response = await client.get<ContentsList>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getContentsVersionList(
|
||||
params: ContentsVersionParams,
|
||||
): Promise<ContentsVersionList> {
|
||||
const response = await client.get<ContentsVersionList>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLatestContentsInfo(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<ContentsDetail> {
|
||||
const response = await client.get<ContentsDetail>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createContents(data: ContentsFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
data: ContentsFormData,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<void> {
|
||||
await client.delete(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async requestApprove(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<void> {
|
||||
await client.put(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`,
|
||||
);
|
||||
}
|
||||
|
||||
public async approveContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<void> {
|
||||
await client.put(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ContentsApi = new ContentsApiService();
|
||||
@@ -0,0 +1,8 @@
|
||||
export { ContentsApi } from "./api/contents-service";
|
||||
export {
|
||||
CONTENTS_APPROVE_STATUS,
|
||||
contentsFormSchema,
|
||||
type Contents,
|
||||
type ContentsParams,
|
||||
type ContentsFormData,
|
||||
} from "./model/contents-interface";
|
||||
@@ -0,0 +1,100 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const CONTENTS_APPROVE_STATUS = {
|
||||
DRAFT: "01",
|
||||
REQUESTED: "02",
|
||||
APPROVED: "99",
|
||||
} as const;
|
||||
|
||||
export type ContentsApproveStatus =
|
||||
(typeof CONTENTS_APPROVE_STATUS)[keyof typeof CONTENTS_APPROVE_STATUS];
|
||||
|
||||
export interface ContentsParams extends PageableParams {
|
||||
contsTtl?: string;
|
||||
aprvContsVer?: number;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface ContentsVersionParams extends PageableParams {
|
||||
contsSn?: number;
|
||||
contsCn?: string;
|
||||
aprvCd?: ContentsApproveStatus;
|
||||
cclCd?: string;
|
||||
koglCd?: string;
|
||||
tag?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface ContentsVersion {
|
||||
contsSn: number;
|
||||
contsVer: number;
|
||||
contsCn: string;
|
||||
useYn: string;
|
||||
}
|
||||
|
||||
export const contentsSchema = z.object({
|
||||
contsSn: z.number(),
|
||||
contsTtl: z.string(),
|
||||
aprvContsVer: z.number(),
|
||||
useYn: z.string(),
|
||||
mdfcnDt: z.string(),
|
||||
mdfr: z.string(),
|
||||
mdfrNm: z.string(),
|
||||
aprvDt: z.string(),
|
||||
autzrNm: z.string(),
|
||||
workAprvCd: z.string(),
|
||||
workAprvNm: z.string(),
|
||||
workContsVer: z.number(),
|
||||
});
|
||||
|
||||
export const contentsDetailSchema = z.object({
|
||||
contsSn: z.number(),
|
||||
contsVer: z.number(),
|
||||
contsTtl: z.string(),
|
||||
contsCn: z.string(),
|
||||
contsRmrk: z.string().nullish(),
|
||||
aprvCd: z.enum([
|
||||
CONTENTS_APPROVE_STATUS.DRAFT,
|
||||
CONTENTS_APPROVE_STATUS.REQUESTED,
|
||||
CONTENTS_APPROVE_STATUS.APPROVED,
|
||||
]),
|
||||
cclCd: z.string().nullish(),
|
||||
koglCd: z.string().nullish(),
|
||||
tag: z.string().nullish(),
|
||||
aprvDt: z.string(),
|
||||
autzr: z.string(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export const contentsFormSchema = contentsDetailSchema.omit({
|
||||
contsVer: true,
|
||||
aprvDt: true,
|
||||
autzr: true,
|
||||
});
|
||||
|
||||
export type Contents = z.infer<typeof contentsSchema>;
|
||||
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
|
||||
export type ContentsFormData = z.infer<typeof contentsFormSchema>;
|
||||
export type ContentsList = PageableList<Contents>;
|
||||
export type ContentsVersionList = PageableList<ContentsVersion>;
|
||||
|
||||
export interface ContentsService {
|
||||
getContentsList(params: ContentsParams): Promise<ContentsList>;
|
||||
getContentsVersionList(
|
||||
params: ContentsVersionParams,
|
||||
): Promise<ContentsVersionList>;
|
||||
getLatestContentsInfo(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<ContentsDetail>;
|
||||
createContents(data: ContentsFormData): Promise<void>;
|
||||
updateContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
data: ContentsFormData,
|
||||
): Promise<void>;
|
||||
deleteContents(contsSn: number, contsVer: number): Promise<void>;
|
||||
requestApprove(contsSn: number, contsVer: number): Promise<void>;
|
||||
approveContents(contsSn: number, contsVer: number): Promise<void>;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
ContentsService,
|
||||
ContentsParams,
|
||||
ContentsList,
|
||||
Contents,
|
||||
ContentsFormData,
|
||||
} from "../model/contents-interface";
|
||||
|
||||
export class ContentsApiService implements ContentsService {
|
||||
public async getContentsList(params: ContentsParams): Promise<ContentsList> {
|
||||
const response = await client.get<ContentsList>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getContentsInfo(contsSn: number): Promise<Contents> {
|
||||
const response = await client.get<Contents>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createContents(data: ContentsFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateContents(
|
||||
contsSn: number,
|
||||
data: ContentsFormData,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteContents(contsSn: number): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const ContentsApi = new ContentsApiService();
|
||||
@@ -1,2 +0,0 @@
|
||||
export { ContentsApi } from "./api/contents-service";
|
||||
export type { Contents, ContentsParams } from "./model/contents-interface";
|
||||
@@ -1,37 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface ContentsParams extends PageableParams {
|
||||
contsTtl?: string;
|
||||
aprvContsVer?: number;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export const contentsSchema = z.object({
|
||||
contsSn: z.number(),
|
||||
contsTtl: z.string(),
|
||||
aprvContsVer: z.number(),
|
||||
useYn: z.string(),
|
||||
regDt: z.string(),
|
||||
mdfcnDt: z.string(),
|
||||
rgTrNm: z.string(),
|
||||
mdfrNm: z.string(),
|
||||
});
|
||||
|
||||
export const contentsFormSchema = z.object({
|
||||
contsTtl: z.string(),
|
||||
useYn: z.string(),
|
||||
rgtr: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type Contents = z.infer<typeof contentsSchema>;
|
||||
export type ContentsFormData = z.infer<typeof contentsFormSchema>;
|
||||
export type ContentsList = PageableList<Contents>;
|
||||
|
||||
export interface ContentsService {
|
||||
getContentsList(params: ContentsParams): Promise<ContentsList>;
|
||||
getContentsInfo(contsSn: number): Promise<Contents>;
|
||||
createContents(data: ContentsFormData): Promise<void>;
|
||||
updateContents(contsSn: number, data: ContentsFormData): Promise<void>;
|
||||
deleteContents(contsSn: number): Promise<void>;
|
||||
}
|
||||
@@ -49,6 +49,7 @@ const Editor = ({ ...props }: IAllProps) => {
|
||||
}}
|
||||
init={{
|
||||
...baseConfig.init,
|
||||
disabled: props.disabled,
|
||||
images_upload_handler: imagesUploadHandler,
|
||||
file_picker_callback: filePickerCallback,
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
MenuAuthorityList,
|
||||
MenuAuthorityParams,
|
||||
MenuAuthorityPayloadData,
|
||||
MenuAuthorityPayload,
|
||||
MenuAuthorityService,
|
||||
} from "../model/menu-authority-interface";
|
||||
|
||||
@@ -18,9 +18,7 @@ export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveMenuAuthority(
|
||||
data: MenuAuthorityPayloadData[],
|
||||
): Promise<void> {
|
||||
public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
data,
|
||||
|
||||
@@ -30,12 +30,10 @@ export const menuAuthorityFormSchema = z.object({
|
||||
|
||||
export type MenuAuthority = z.infer<typeof menuAuthoritySchema>;
|
||||
export type MenuAuthorityList = PageableList<MenuAuthority>;
|
||||
export type MenuAuthorityPayloadData = z.infer<
|
||||
typeof menuAuthorityPayloadSchema
|
||||
>;
|
||||
export type MenuAuthorityPayload = z.infer<typeof menuAuthorityPayloadSchema>;
|
||||
export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayloadData[]): Promise<void>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type {
|
||||
MenuAuthority,
|
||||
MenuAuthorityFormData,
|
||||
MenuAuthorityPayloadData,
|
||||
MenuAuthorityPayload,
|
||||
} from "./menu-authority-interface";
|
||||
|
||||
export const createColumns = (
|
||||
@@ -13,7 +13,7 @@ export const createColumns = (
|
||||
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
|
||||
|
||||
const handleCheckedChange =
|
||||
(i: number, key: keyof MenuAuthorityPayloadData) => (checked: boolean) => {
|
||||
(i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => {
|
||||
if (getCurrentRow(i)?.mode === "R") {
|
||||
form.setValue(`menuAuthority.${i}.mode`, "U");
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components/ui";
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityFormData,
|
||||
type MenuAuthorityPayloadData,
|
||||
type MenuAuthorityPayload,
|
||||
} from "../model/menu-authority-interface";
|
||||
import { createColumns } from "../model/menu-authority-table-columns";
|
||||
import { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
@@ -42,7 +42,7 @@ function ManagementAuthorityDialog({
|
||||
});
|
||||
|
||||
const { mutateAsync: saveMenuAuthority } = useMutation({
|
||||
mutationFn: (data: MenuAuthorityPayloadData[]) =>
|
||||
mutationFn: (data: MenuAuthorityPayload[]) =>
|
||||
MenuAuthorityApi.saveMenuAuthority(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { client } from "@common/components/api";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { MenuItem, MenuService } from "../model/menu-interface";
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
export type { MenuItem } from "./model/menu-interface";
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
type Organization,
|
||||
type OrganizationFormData,
|
||||
type OrganizationService,
|
||||
type OrganizationTree,
|
||||
type OrganizationTreeForDnd,
|
||||
} from "../model/organization-interface";
|
||||
|
||||
export class OrganizationApiService implements OrganizationService {
|
||||
public async getOrganizationsTreeList(): Promise<OrganizationTree[]> {
|
||||
const response = await client.get<OrganizationTree[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/tree`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getOrganizationInfo(ognzSn: number): Promise<Organization> {
|
||||
const response = await client.get<Organization>(
|
||||
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createOrganization(data: OrganizationFormData): Promise<void> {
|
||||
await client.post<void>(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, data);
|
||||
}
|
||||
|
||||
public async deleteOrganization(ognzSn: number): Promise<void> {
|
||||
await client.delete<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateOrganizationName(
|
||||
ognzSn: number,
|
||||
name: string,
|
||||
): Promise<Organization> {
|
||||
const response = await client.put<Organization>(
|
||||
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}/${name}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateOrganization(
|
||||
ognzSn: number,
|
||||
data: OrganizationFormData,
|
||||
): Promise<void> {
|
||||
await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateOrganizationByDnd(
|
||||
data: OrganizationTreeForDnd[],
|
||||
): Promise<void> {
|
||||
await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/dnd/tree`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const OrganizationApi = new OrganizationApiService();
|
||||
@@ -0,0 +1,8 @@
|
||||
export { OrganizationApi } from "./api/organization-service";
|
||||
export {
|
||||
organizationFormSchema,
|
||||
type OrganizationTree,
|
||||
type OrganizationFormData,
|
||||
type OrganizationTreeForDnd,
|
||||
} from "./model/organization-interface";
|
||||
export { generateOrganizationTreeData, handleDropNode } from "./lib/utils";
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type {
|
||||
OrganizationTree,
|
||||
OrganizationTreeForDnd,
|
||||
} from "../model/organization-interface";
|
||||
|
||||
export const generateOrganizationTreeData = (
|
||||
data: OrganizationTree[],
|
||||
): NodeModel<OrganizationTree>[] => {
|
||||
return data.reduce<NodeModel<OrganizationTree>[]>((acc, menu) => {
|
||||
const node: NodeModel<OrganizationTree> = {
|
||||
id: menu.ognzSn ?? 0,
|
||||
parent: menu.upOgnzSn ?? 0,
|
||||
text: menu.ognzNm,
|
||||
droppable: true,
|
||||
data: menu,
|
||||
};
|
||||
acc.push(node);
|
||||
|
||||
if (menu.children?.length) {
|
||||
acc.push(...generateOrganizationTreeData(menu.children));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const generateDndOrganizationTreeData = (
|
||||
treeData: NodeModel<OrganizationTree>[],
|
||||
options: DropOptions<OrganizationTree>,
|
||||
): OrganizationTreeForDnd | null => {
|
||||
const { dragSourceId, dragSource, dropTargetId } = options;
|
||||
if (!dragSource?.data) return null;
|
||||
|
||||
const siblings = treeData.filter((node) => node.parent === dropTargetId);
|
||||
const dragSourceIndex = siblings.findIndex(
|
||||
(node) => node.id === dragSourceId,
|
||||
);
|
||||
const prevSibling = siblings[dragSourceIndex - 1];
|
||||
const newSortSeq =
|
||||
dragSourceIndex === 0 ? 1 : (prevSibling?.data?.sortSeq ?? 0) + 1;
|
||||
|
||||
const treeNode: OrganizationTreeForDnd = {
|
||||
ognzSn: dragSource.data.ognzSn,
|
||||
ognzNm: dragSource.data.ognzNm,
|
||||
ognzExpln: dragSource.data.ognzExpln,
|
||||
upOgnzSn: dropTargetId === 0 ? null : (dropTargetId as number),
|
||||
sortSeq: newSortSeq,
|
||||
};
|
||||
|
||||
const childNodes = treeData.filter(
|
||||
(node) => node.parent === dragSourceId && node.data,
|
||||
);
|
||||
const result: OrganizationTreeForDnd = {
|
||||
...treeNode,
|
||||
children:
|
||||
childNodes?.map((child) => ({
|
||||
ognzSn: child.data?.ognzSn,
|
||||
ognzNm: child.data?.ognzNm ?? "",
|
||||
ognzExpln: child.data?.ognzExpln,
|
||||
upOgnzSn: child.data?.upOgnzSn === 0 ? null : (dragSourceId as number),
|
||||
sortSeq: child.data?.sortSeq ?? 1,
|
||||
})) ?? [],
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const handleDropNode =
|
||||
(callback: (newTree: OrganizationTreeForDnd) => void) =>
|
||||
(
|
||||
newTree: NodeModel<OrganizationTree>[],
|
||||
options: DropOptions<OrganizationTree>,
|
||||
) => {
|
||||
const newTreeData = generateDndOrganizationTreeData(newTree, options);
|
||||
if (!newTreeData) return;
|
||||
callback(newTreeData);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const organizationSchema = z.object({
|
||||
ognzSn: z.number().nullish(),
|
||||
ognzNm: z.string(),
|
||||
ognzExpln: z.string().nullish(),
|
||||
upOgnzSn: z.number().nullish(),
|
||||
sortSeq: z.number(),
|
||||
useYn: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const organizationFormSchema = organizationSchema.omit({
|
||||
ognzSn: true,
|
||||
});
|
||||
|
||||
export type Organization = z.infer<typeof organizationSchema>;
|
||||
export type OrganizationFormData = z.infer<typeof organizationFormSchema>;
|
||||
export type OrganizationTree = Organization & {
|
||||
children?: OrganizationTree[];
|
||||
};
|
||||
export type OrganizationTreeForDnd = Omit<OrganizationTree, "useYn">;
|
||||
|
||||
export interface OrganizationService {
|
||||
getOrganizationsTreeList(): Promise<OrganizationTree[]>;
|
||||
getOrganizationInfo(ognzSn: number): Promise<Organization>;
|
||||
createOrganization(data: OrganizationFormData): Promise<void>;
|
||||
deleteOrganization(ognzSn: number): Promise<void>;
|
||||
updateOrganizationName(ognzSn: number, name: string): Promise<Organization>;
|
||||
updateOrganization(ognzSn: number, data: OrganizationFormData): Promise<void>;
|
||||
updateOrganizationByDnd(data: OrganizationTreeForDnd[]): Promise<void>;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
PostService,
|
||||
PostingParams,
|
||||
Post,
|
||||
PostPayloadData,
|
||||
PostPayload,
|
||||
} from "../model/posting-interface";
|
||||
|
||||
export class PostingApiService implements PostService {
|
||||
@@ -19,7 +19,7 @@ export class PostingApiService implements PostService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async savePosting(data: PostPayloadData[]): Promise<Post[]> {
|
||||
public async savePosting(data: PostPayload[]): Promise<Post[]> {
|
||||
const response = await client.post<Post[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.POSTINGS}`,
|
||||
data,
|
||||
|
||||
@@ -2,6 +2,6 @@ export { PostingApi } from "./api/posting-service";
|
||||
export {
|
||||
POSTING_TYPE,
|
||||
type Post,
|
||||
type PostPayloadData,
|
||||
type PostPayload,
|
||||
} from "./model/posting-interface";
|
||||
export { PostingDialog } from "./ui/posting-dialog";
|
||||
|
||||
@@ -41,10 +41,10 @@ export const postingFormSchema = z.object({
|
||||
});
|
||||
|
||||
export type Post = z.infer<typeof postSchema>;
|
||||
export type PostPayloadData = z.infer<typeof postPayloadSchema>;
|
||||
export type PostPayload = z.infer<typeof postPayloadSchema>;
|
||||
export type PostingFormData = z.infer<typeof postingFormSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostingHistory: (params: PostingParams) => Promise<PageableList<Post>>;
|
||||
savePosting: (data: PostPayloadData[]) => Promise<Post[]>;
|
||||
savePosting: (data: PostPayload[]) => Promise<Post[]>;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
DateRangePicker,
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
} from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
cn,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code/@x/posting";
|
||||
import { SiteApi, type Site } from "@/entities/site/@x/posting";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
postingFormSchema,
|
||||
type PostingFormData,
|
||||
type PostingType,
|
||||
type PostPayloadData,
|
||||
type PostPayload,
|
||||
} from "../model/posting-interface";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
@@ -76,7 +76,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data),
|
||||
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
TreeUserOrganization,
|
||||
UserOrganizationParams,
|
||||
UserOrganizationList,
|
||||
UserOrganizationService,
|
||||
UserOrganizationPayload,
|
||||
SearchUserParams,
|
||||
SearchUserList,
|
||||
User,
|
||||
MoveUserOrganizationPayload,
|
||||
} from "../model/user-organization-interface";
|
||||
|
||||
export class UserOrganizationApiService implements UserOrganizationService {
|
||||
public async getUserOrganizationList(
|
||||
params: UserOrganizationParams,
|
||||
): Promise<UserOrganizationList> {
|
||||
const response = await client.get<UserOrganizationList>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getUserOrganizationTreeList(): Promise<TreeUserOrganization[]> {
|
||||
const response = await client.get<TreeUserOrganization[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async searchUser(params: SearchUserParams): Promise<SearchUserList> {
|
||||
const response = await client.get<SearchUserList>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/user-popup`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveUserOrganization(
|
||||
data: UserOrganizationPayload[],
|
||||
): Promise<User[]> {
|
||||
const response = await client.post<User[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async moveUserOrganization(
|
||||
data: MoveUserOrganizationPayload[],
|
||||
): Promise<User[]> {
|
||||
const response = await client.post<User[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/move-ognz`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const UserOrganizationApi = new UserOrganizationApiService();
|
||||
@@ -0,0 +1,11 @@
|
||||
export { UserOrganizationApi } from "./api/user-organization-service";
|
||||
export {
|
||||
userOrganizationFormSchema,
|
||||
type TreeUserOrganization,
|
||||
type TreeUser,
|
||||
type UserOrganizationFormData,
|
||||
type UserOrganizationPayload,
|
||||
type User,
|
||||
type SearchUser,
|
||||
type MoveUserOrganizationPayload,
|
||||
} from "./model/user-organization-interface";
|
||||
@@ -0,0 +1,92 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface UserOrganizationParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
ognzSn?: number;
|
||||
userUnqId?: string;
|
||||
jbpsSeCd?: string;
|
||||
jbgdSeCd?: string;
|
||||
}
|
||||
|
||||
export interface SearchUserParams extends PageableParams {
|
||||
userNm?: string;
|
||||
ogdpSeCd?: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
mode: string;
|
||||
ognzSn: number;
|
||||
userUnqId: string;
|
||||
userUnqNm?: string;
|
||||
jbpsSeCd: string;
|
||||
jbpsSeNm?: string;
|
||||
jbgdSeCd: string;
|
||||
jbgdSeNm?: string;
|
||||
sortSeq: number;
|
||||
mdfcnDt?: string;
|
||||
mdfr?: string;
|
||||
mdfrNm?: string;
|
||||
}
|
||||
|
||||
export interface SearchUser {
|
||||
userUnqId: string;
|
||||
userNm: string;
|
||||
ogdpSeCd: string;
|
||||
ogdpSeNm: string;
|
||||
regDt: string;
|
||||
}
|
||||
|
||||
export interface TreeUser {
|
||||
userUnqId: string;
|
||||
userNm: string;
|
||||
jbpsSeCd: string;
|
||||
jbgdSeCd: string;
|
||||
}
|
||||
|
||||
export interface TreeUserOrganization {
|
||||
ognzSn: number;
|
||||
upOgnzSn: number;
|
||||
ognzNm: string;
|
||||
users: TreeUser[];
|
||||
children: TreeUserOrganization[];
|
||||
}
|
||||
|
||||
export interface MoveUserOrganizationPayload {
|
||||
ognzSn: number;
|
||||
toOgnzSn: number;
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
export const userOrganizationPayloadSchema = z.object({
|
||||
mode: z.string(),
|
||||
ognzSn: z.number(),
|
||||
userUnqId: z.string(),
|
||||
jbpsSeCd: z.string(),
|
||||
jbgdSeCd: z.string(),
|
||||
sortSeq: z.number(),
|
||||
});
|
||||
|
||||
export const userOrganizationFormSchema = z.object({
|
||||
userOrganization: z.array(userOrganizationPayloadSchema),
|
||||
});
|
||||
|
||||
export type UserOrganizationList = PageableList<User>;
|
||||
export type SearchUserList = PageableList<SearchUser>;
|
||||
export type UserOrganizationPayload = z.infer<
|
||||
typeof userOrganizationPayloadSchema
|
||||
>;
|
||||
export type UserOrganizationFormData = z.infer<
|
||||
typeof userOrganizationFormSchema
|
||||
>;
|
||||
|
||||
export interface UserOrganizationService {
|
||||
getUserOrganizationList(
|
||||
params: UserOrganizationParams,
|
||||
): Promise<UserOrganizationList>;
|
||||
getUserOrganizationTreeList(): Promise<TreeUserOrganization[]>;
|
||||
searchUser(params: SearchUserParams): Promise<SearchUserList>;
|
||||
saveUserOrganization(data: UserOrganizationPayload[]): Promise<User[]>;
|
||||
moveUserOrganization(data: MoveUserOrganizationPayload[]): Promise<User[]>;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox, Skeleton } from "@common/components/ui";
|
||||
import { Checkbox, Skeleton } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Banner } from "./banner-interface";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: ColumnDef<Post>[] = [
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type PostPayloadData } from "@/entities/posting";
|
||||
import { Button, DataTable } from "@common/components";
|
||||
import { PostingApi, type PostPayload } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
@@ -58,7 +58,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data),
|
||||
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { BbsPage } from "./ui/bbs-page";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
@@ -0,0 +1,61 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { ColumnDef, Row } from "@tanstack/react-table";
|
||||
import { useLocalStorage } from "@frontend-opensource/use-react-hooks";
|
||||
import type { Board } from "@/entities/board/model/board-interface";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
export const columns: ColumnDef<Board>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsNm",
|
||||
header: "게시판명",
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsTypeNm",
|
||||
header: "게시판유형",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) =>
|
||||
row.original.mdfcnDt
|
||||
? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD")
|
||||
: "",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "관리",
|
||||
cell: ({ row }) => <ManageButton row={row} />,
|
||||
},
|
||||
];
|
||||
|
||||
const ManageButton = ({ row }: { row: Row<Board> }) => {
|
||||
const router = useRouter();
|
||||
const { bbsTypeCd, bbsId } = row.original;
|
||||
const [, setBbsInfo] = useLocalStorage("bbs-info", "");
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/pst/${bbsTypeCd.toLowerCase()}/${bbsId}`);
|
||||
setBbsInfo(JSON.stringify(row.original));
|
||||
}}
|
||||
>
|
||||
관리
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,447 @@
|
||||
"use client";
|
||||
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { Board, BoardFormData } from "@/entities/board";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
|
||||
type BbsFormProps = {
|
||||
form: UseFormReturn<BoardFormData>;
|
||||
bbsId?: string;
|
||||
handleSave: (data: BoardFormData) => Promise<Board>;
|
||||
};
|
||||
|
||||
function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!bbsId} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>게시판명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bbsNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="게시판명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시판 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bbsExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="게시판설명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시판 유형</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bbsTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="BBSTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>참조 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rfrncUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="rfrncUseY" />
|
||||
<Label htmlFor="rfrncUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="rfrncUseN" />
|
||||
<Label htmlFor="rfrncUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="atflUseY" />
|
||||
<Label htmlFor="atflUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="atflUseN" />
|
||||
<Label htmlFor="atflUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일 최대 개수</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflMaxCnt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(value === "" ? null : Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="최대 개수 입력"
|
||||
disabled={form.watch("atflUseYn") === "N"}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일 최대 크기</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflMaxFileSz"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(value === "" ? null : Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="최대 크기 입력"
|
||||
disabled={form.watch("atflUseYn") === "N"}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>답글 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ansUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="ansUseY" />
|
||||
<Label htmlFor="ansUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="ansUseN" />
|
||||
<Label htmlFor="ansUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>답글 사용 권한</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ansUseAuthrt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={form.watch("ansUseYn") === "N"}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="A">관리자</SelectItem>
|
||||
<SelectItem value="M">회원</SelectItem>
|
||||
<SelectItem value="N">일반</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>댓글 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cmntUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="cmntUseY" />
|
||||
<Label htmlFor="cmntUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="cmntUseN" />
|
||||
<Label htmlFor="cmntUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시물평가 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dgstfnEvlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="dgstfnEvlUseY" />
|
||||
<Label htmlFor="dgstfnEvlUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="dgstfnEvlUseN" />
|
||||
<Label htmlFor="dgstfnEvlUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>팝업 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="popupUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="popupUseY" />
|
||||
<Label htmlFor="popupUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="popupUseN" />
|
||||
<Label htmlFor="popupUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>SMS알림 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smsUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="smsUseY" />
|
||||
<Label htmlFor="smsUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="smsUseN" />
|
||||
<Label htmlFor="smsUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>상단고정 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="upndFixUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="upndFixUseY" />
|
||||
<Label htmlFor="upndFixUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="upndFixUseN" />
|
||||
<Label htmlFor="upndFixUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시기간 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pstgPrdUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="pstgPrdUseY" />
|
||||
<Label htmlFor="pstgPrdUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="pstgPrdUseN" />
|
||||
<Label htmlFor="pstgPrdUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { BbsForm };
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, 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,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { BoardApi, type BoardParams } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { type Board } from "@/entities/board";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
function BbsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [boardParams, setBoardParams] = useState<BoardParams>({
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: boardList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
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();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Board) => {
|
||||
router.push(`/bbs/register/${row.bbsId}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="bbsType" className="mr-2 text-label">
|
||||
게시판 유형
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="BBSTYPE"
|
||||
value={boardParams.bbsTypeCd}
|
||||
onValueChange={(value) =>
|
||||
setBoardParams({ ...boardParams, bbsTypeCd: value })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="bbsNm" className="ml-4 mr-2 text-label">
|
||||
게시판명
|
||||
</Label>
|
||||
<Input
|
||||
id="bbsNm"
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={boardParams.bbsNm}
|
||||
onChange={(e) =>
|
||||
setBoardParams({ ...boardParams, bbsNm: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">게시판 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => router.push("/bbs/register")}>등록</Button>
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
{pageSize}개씩 보기
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BbsPage };
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
BoardApi,
|
||||
boardFormSchema,
|
||||
type BoardFormData,
|
||||
} from "@/entities/board";
|
||||
import { BbsForm } from "./bbs-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createBoard } = useMutation({
|
||||
mutationFn: (data: BoardFormData) => BoardApi.createBoard(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BoardFormData>({
|
||||
resolver: zodResolver(boardFormSchema),
|
||||
defaultValues: {
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
bbsExpln: "",
|
||||
rfrncUseYn: "Y",
|
||||
atflUseYn: "Y",
|
||||
ansUseYn: "Y",
|
||||
cmntUseYn: "Y",
|
||||
dgstfnEvlUseYn: "Y",
|
||||
smsUseYn: "Y",
|
||||
upndFixUseYn: "Y",
|
||||
pstgPrdUseYn: "Y",
|
||||
popupUseYn: "Y",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">게시판 등록</h3>
|
||||
</div>
|
||||
<BbsForm form={form} handleSave={createBoard} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
BoardApi,
|
||||
boardFormSchema,
|
||||
type BoardFormData,
|
||||
} from "@/entities/board";
|
||||
import { BbsForm } from "./bbs-form";
|
||||
|
||||
function ModifyPage({ bbsId }: { bbsId: string }) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: boardInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId],
|
||||
queryFn: () => BoardApi.getBoardInfo(bbsId),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateBoard } = useMutation({
|
||||
mutationFn: (data: BoardFormData) => BoardApi.updateBoard(bbsId, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BoardFormData>({
|
||||
resolver: zodResolver(boardFormSchema),
|
||||
values: boardInfo,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">게시판 수정</h3>
|
||||
</div>
|
||||
<BbsForm form={form} bbsId={bbsId} handleSave={updateBoard} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1 +1,3 @@
|
||||
export { ContentsPage } from "./ui/contents-page";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
|
||||
@@ -1,47 +1,68 @@
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import type { Contents } from "@/entities/contents";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import type { Contents } from "@/entities/content";
|
||||
|
||||
export const columns: ColumnDef<Contents>[] = [
|
||||
{
|
||||
const columnHelper = createColumnHelper<Contents>();
|
||||
|
||||
export const columns = [
|
||||
columnHelper.display({
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "contsTtl",
|
||||
}),
|
||||
columnHelper.accessor("contsTtl", {
|
||||
id: "contsTtl",
|
||||
header: "콘텐츠명",
|
||||
},
|
||||
{
|
||||
accessorKey: "aprvContsVer",
|
||||
header: "승인여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "rgTrNm",
|
||||
header: "승인자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "승인일자",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.regDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
}),
|
||||
columnHelper.group({
|
||||
id: "workInfo",
|
||||
header: "작성정보",
|
||||
columns: [
|
||||
columnHelper.accessor("workContsVer", {
|
||||
header: "작성버전",
|
||||
cell: ({ row }) =>
|
||||
row.original.workContsVer ? `rev. ${row.original.workContsVer}` : "",
|
||||
}),
|
||||
columnHelper.accessor("workAprvNm", {
|
||||
header: "작성상태",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
columnHelper.group({
|
||||
id: "aprvInfo",
|
||||
header: "승인정보",
|
||||
columns: [
|
||||
columnHelper.accessor("aprvContsVer", {
|
||||
header: "승인버전",
|
||||
cell: ({ row }) =>
|
||||
row.original.aprvContsVer ? `rev. ${row.original.aprvContsVer}` : "",
|
||||
}),
|
||||
columnHelper.accessor("autzrNm", {
|
||||
header: "승인자",
|
||||
}),
|
||||
columnHelper.accessor("aprvDt", {
|
||||
header: "승인일자",
|
||||
cell: ({ row }) =>
|
||||
row.original.aprvDt
|
||||
? dayjs(row.original.aprvDt).format("YYYY-MM-DD")
|
||||
: "",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
columnHelper.accessor("useYn", {
|
||||
id: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
}),
|
||||
columnHelper.accessor("mdfrNm", {
|
||||
id: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
}),
|
||||
columnHelper.accessor("mdfcnDt", {
|
||||
id: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.mdfcnDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
cell: ({ row }) =>
|
||||
row.original.mdfcnDt
|
||||
? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD")
|
||||
: "",
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { ContentsFormData } from "@/entities/content";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import Image from "next/image";
|
||||
|
||||
const Editor = dynamic(
|
||||
() => import("@/entities/editor").then((module) => module.Editor),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface ContentsFormProps {
|
||||
form: UseFormReturn<ContentsFormData>;
|
||||
contsSn?: number;
|
||||
isEditable?: boolean;
|
||||
isContsTtlDisabled?: boolean;
|
||||
handleSave: (data: ContentsFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function ContentsForm({
|
||||
form,
|
||||
contsSn,
|
||||
isEditable,
|
||||
isContsTtlDisabled,
|
||||
...props
|
||||
}: ContentsFormProps) {
|
||||
const { data: cclCode } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "CCLCD"],
|
||||
queryFn: () => CodeApi.getCode("CCLCD"),
|
||||
});
|
||||
|
||||
const { data: koglCode } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "KOGLCD"],
|
||||
queryFn: () => CodeApi.getCode("KOGLCD"),
|
||||
});
|
||||
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!contsSn} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>콘텐츠 제목</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contsTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="콘텐츠 제목을 입력하십시오."
|
||||
disabled={!isEditable || isContsTtlDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>콘텐츠 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contsCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Editor
|
||||
value={field.value ?? ""}
|
||||
onEditorChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>CCL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cclCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="cclCd"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="" id="ccl-none" />
|
||||
<Label
|
||||
htmlFor="ccl-none"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
해당없음
|
||||
</Label>
|
||||
</div>
|
||||
{cclCode?.map((code) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={code.lwpoCd}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={code.lwpoCd}
|
||||
id={`ccl-${code.lwpoCd}`}
|
||||
/>
|
||||
<Label htmlFor={`ccl-${code.lwpoCd}`}>
|
||||
<Image
|
||||
src={`/images/${code.unit}`}
|
||||
alt="CCL"
|
||||
width={77}
|
||||
height={27}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>공공누리</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="koglCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="koglCd"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="" id="koglNone" />
|
||||
<Label
|
||||
htmlFor="koglNone"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
해당없음
|
||||
</Label>
|
||||
</div>
|
||||
{koglCode?.map((code) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={code.lwpoCd}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={code.lwpoCd}
|
||||
id={`kogl-${code.lwpoCd}`}
|
||||
/>
|
||||
<Label htmlFor={`kogl-${code.lwpoCd}`}>
|
||||
<div className="relative h-[2.7rem] w-[11rem]">
|
||||
<Image
|
||||
src={`/images/${code.unit}`}
|
||||
alt="공공누리"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>태그</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tag"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="태그를 입력하십시오."
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContentsForm };
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ContentsApi,
|
||||
type Contents,
|
||||
type ContentsParams,
|
||||
} from "@/entities/contents";
|
||||
} from "@/entities/content";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
@@ -89,8 +89,13 @@ function ContentsPage() {
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[29.2rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
<div className="card !m-[1.2rem] overflow-auto !p-0">
|
||||
<DataTable
|
||||
table={table}
|
||||
onRowClick={handleRowClick}
|
||||
variant="secondary"
|
||||
viewPagination
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
ContentsApi,
|
||||
contentsFormSchema,
|
||||
type ContentsFormData,
|
||||
} from "@/entities/content";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ContentsForm } from "./contents-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createContents } = useMutation({
|
||||
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ContentsFormData>({
|
||||
resolver: zodResolver(contentsFormSchema),
|
||||
defaultValues: {
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
|
||||
</div>
|
||||
<ContentsForm form={form} handleSave={createContents} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
CONTENTS_APPROVE_STATUS,
|
||||
ContentsApi,
|
||||
contentsFormSchema,
|
||||
type ContentsFormData,
|
||||
} from "@/entities/content";
|
||||
import { ContentsForm } from "./contents-form";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
Button,
|
||||
} from "@common/components";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const contsSn = Number(sequential);
|
||||
const [contsVer, setContsVer] = useState(0);
|
||||
|
||||
const { data: contentsVersionList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
queryFn: () =>
|
||||
ContentsApi.getContentsVersionList({
|
||||
contsSn: contsSn,
|
||||
page: 0,
|
||||
size: 100,
|
||||
}),
|
||||
});
|
||||
|
||||
const { data: contentsDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsVer],
|
||||
queryFn: () => ContentsApi.getLatestContentsInfo(contsSn, contsVer),
|
||||
enabled: !!contsVer,
|
||||
});
|
||||
|
||||
const { mutateAsync: createContents } = useMutation({
|
||||
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
|
||||
onSuccess: () => {
|
||||
setContsVer(0);
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateContents } = useMutation({
|
||||
mutationFn: (data: ContentsFormData) =>
|
||||
ContentsApi.updateContents(contsSn, contsVer, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteContents } = useMutation({
|
||||
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
|
||||
onSuccess: () => {
|
||||
setContsVer(0);
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: requestApprove } = useMutation({
|
||||
mutationFn: () => ContentsApi.requestApprove(contsSn, contsVer),
|
||||
onSuccess: () => {
|
||||
alert("승인 요청이 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: approveContents } = useMutation({
|
||||
mutationFn: () => ContentsApi.approveContents(contsSn, contsVer),
|
||||
onSuccess: () => {
|
||||
alert("승인되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ContentsFormData>({
|
||||
resolver: zodResolver(contentsFormSchema),
|
||||
values: contentsDetail,
|
||||
});
|
||||
|
||||
const approveStatus = contentsDetail?.aprvCd;
|
||||
const isContsTtlDisabled =
|
||||
(contentsVersionList?.content?.[0]?.contsVer ?? 0) > 1;
|
||||
|
||||
const onRequestApprove = async () => {
|
||||
const message =
|
||||
"승인 요청하면 더이상 내용을 수정할 수 없습니다.\n요청하시겠습니까?";
|
||||
if (await confirm(message)) requestApprove();
|
||||
};
|
||||
|
||||
const onApprove = async () => {
|
||||
const message = "승인하시겠습니까?";
|
||||
if (await confirm(message)) approveContents();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (contsVer) return;
|
||||
if (!contentsVersionList?.content?.length) return;
|
||||
|
||||
const latestContent = contentsVersionList?.content[0];
|
||||
if (!latestContent) return;
|
||||
|
||||
setContsVer(latestContent?.contsVer ?? 0);
|
||||
}, [contentsVersionList?.content]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">템플릿 수정</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={contsVer.toString()}
|
||||
onValueChange={(value) => {
|
||||
setContsVer(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[28rem]">
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{contentsVersionList?.content?.map((item) => (
|
||||
<SelectItem
|
||||
key={item.contsVer}
|
||||
value={item.contsVer.toString()}
|
||||
>
|
||||
{item.contsCn}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{approveStatus === CONTENTS_APPROVE_STATUS.DRAFT && (
|
||||
<Button onClick={onRequestApprove}>승인요청</Button>
|
||||
)}
|
||||
{approveStatus === CONTENTS_APPROVE_STATUS.REQUESTED && (
|
||||
<>
|
||||
<Button onClick={onApprove}>승인</Button>
|
||||
<Button color="gray">반려</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ContentsForm
|
||||
form={form}
|
||||
contsSn={contsSn}
|
||||
isEditable={approveStatus !== CONTENTS_APPROVE_STATUS.REQUESTED}
|
||||
isContsTtlDisabled={isContsTtlDisabled}
|
||||
handleSave={
|
||||
approveStatus === CONTENTS_APPROVE_STATUS.APPROVED
|
||||
? createContents
|
||||
: updateContents
|
||||
}
|
||||
handleDelete={deleteContents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -3,7 +3,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
import type { Attachment } from "@/entities/attachment/model/attachment-interface";
|
||||
import type { Attachment } from "@/entities/attachment";
|
||||
|
||||
export const createColumns = (
|
||||
handleDeleteAttachment: (atflId: string) => void,
|
||||
@@ -28,6 +28,10 @@ export const createColumns = (
|
||||
{
|
||||
accessorKey: "atflFlieSz",
|
||||
header: "파일크기",
|
||||
cell: ({ row }) => {
|
||||
const size = row.getValue("atflFlieSz");
|
||||
return size ? formatFileSize(Number(size)) : "0 Bytes";
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
@@ -60,3 +64,13 @@ export const createColumns = (
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import type { AttachmentParams } from "@/entities/attachment/model/attachment-interface";
|
||||
import { AttachmentApi } from "@/entities/attachment/api/attachment-service";
|
||||
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { Checkbox } from "@common/components";
|
||||
import { type Image } from "./image-interface";
|
||||
import { IMAGE_TYPE } from "./image-interface";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: ColumnDef<Post>[] = [
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type PostPayloadData } from "@/entities/posting";
|
||||
import { Button, DataTable } from "@common/components";
|
||||
import { PostingApi, type PostPayload } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
@@ -54,7 +54,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data),
|
||||
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Organization,
|
||||
OrganizationService,
|
||||
} from "../model/organization-interface";
|
||||
|
||||
export class OrganizationApiService implements OrganizationService {
|
||||
public async getOrganizationUserTreeList(): Promise<Organization[]> {
|
||||
const response = await client.get<Organization[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const OrganizationApi = new OrganizationApiService();
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type {
|
||||
TreeUserOrganization,
|
||||
TreeUser,
|
||||
} from "@/entities/user-organization";
|
||||
import type { TreeMenu, TreeNode } from "../model/menu-interface";
|
||||
import type { Organization, User } from "../model/organization-interface";
|
||||
|
||||
export const generateMenuTreeData = (
|
||||
data: TreeMenu[],
|
||||
@@ -107,44 +110,47 @@ export const handleNodeTextChange =
|
||||
callback(newTree);
|
||||
};
|
||||
|
||||
export const generateOrganizationTreeData = (
|
||||
organizations: Organization[],
|
||||
): NodeModel<Organization | User>[] => {
|
||||
return organizations.reduce<NodeModel<Organization | User>[]>((acc, org) => {
|
||||
const orgNode: NodeModel<Organization> = {
|
||||
id: org.ognzId,
|
||||
parent: org.upOgnzId ?? 0,
|
||||
text: org.ognzNm,
|
||||
data: org,
|
||||
droppable: false,
|
||||
};
|
||||
export const generateUserOrganizationTreeData = (
|
||||
organizations: TreeUserOrganization[],
|
||||
): NodeModel<TreeUserOrganization | TreeUser>[] => {
|
||||
return organizations.reduce<NodeModel<TreeUserOrganization | TreeUser>[]>(
|
||||
(acc, org) => {
|
||||
const orgNode: NodeModel<TreeUserOrganization> = {
|
||||
id: org.ognzSn,
|
||||
parent: org.upOgnzSn ?? 0,
|
||||
text: org.ognzNm,
|
||||
data: org,
|
||||
droppable: false,
|
||||
};
|
||||
|
||||
acc.push(orgNode);
|
||||
acc.push(orgNode);
|
||||
|
||||
if (org.children?.length) {
|
||||
acc.push(...generateOrganizationTreeData(org.children));
|
||||
}
|
||||
if (org.children?.length) {
|
||||
acc.push(...generateUserOrganizationTreeData(org.children));
|
||||
}
|
||||
|
||||
if (org.users?.length) {
|
||||
acc.push(
|
||||
...org.users.map((user) => ({
|
||||
id: user.userUnqId,
|
||||
parent: org.ognzId,
|
||||
text: user.userNm,
|
||||
data: user,
|
||||
droppable: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (org.users?.length) {
|
||||
acc.push(
|
||||
...org.users.map((user) => ({
|
||||
id: user.userUnqId,
|
||||
parent: org.ognzSn,
|
||||
text: user.userNm,
|
||||
data: user,
|
||||
droppable: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export const filterOrganizationTreeDataByKeyword = (
|
||||
treeData: NodeModel<Organization | User>[],
|
||||
export const filterUserOrganizationTreeDataByKeyword = (
|
||||
treeData: NodeModel<TreeUserOrganization | TreeUser>[],
|
||||
keyword: string,
|
||||
): NodeModel<Organization | User>[] => {
|
||||
): NodeModel<TreeUserOrganization | TreeUser>[] => {
|
||||
return Array.from(
|
||||
treeData.reduce((matchedNodes, node) => {
|
||||
if (!node.text.includes(keyword)) return matchedNodes;
|
||||
@@ -160,6 +166,6 @@ export const filterOrganizationTreeDataByKeyword = (
|
||||
}
|
||||
|
||||
return matchedNodes;
|
||||
}, new Set<NodeModel<Organization | User>>()),
|
||||
}, new Set<NodeModel<TreeUserOrganization | TreeUser>>()),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
|
||||
export const columns: ColumnDef<BoardPopup>[] = [
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Contents } from "@/entities/contents";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type { Contents } from "@/entities/content";
|
||||
|
||||
export const columns: ColumnDef<Contents>[] = [
|
||||
{
|
||||
@@ -19,10 +19,10 @@ export const columns: ColumnDef<Contents>[] = [
|
||||
header: "승인자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
accessorKey: "aprvDt",
|
||||
header: "승인일자",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.regDt);
|
||||
const date = dayjs(row.original.aprvDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
@@ -1,18 +0,0 @@
|
||||
export interface User {
|
||||
userUnqId: string;
|
||||
userNm: string;
|
||||
jbpsSeCd: string;
|
||||
jbgdSeCd: string;
|
||||
}
|
||||
|
||||
export interface Organization {
|
||||
ognzId: string;
|
||||
upOgnzId: string;
|
||||
ognzNm: string;
|
||||
users: User[];
|
||||
children: Organization[];
|
||||
}
|
||||
|
||||
export interface OrganizationService {
|
||||
getOrganizationUserTreeList(): Promise<Organization[]>;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { BoardApi, type BoardPopup } from "@/entities/board";
|
||||
import { columns } from "../model/board-table-columns";
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ContentsApi, type Contents } from "@/entities/contents";
|
||||
import { columns } from "../model/content-table-colums";
|
||||
import { ContentsApi, type Contents } from "@/entities/content";
|
||||
import { columns } from "../model/contents-table-colums";
|
||||
|
||||
type ContentSearchDialogProps = {
|
||||
handleContentSelect: (content: Contents) => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components/ui";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { Button, cn } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { TreeMenu } from "../model/menu-interface";
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components/ui";
|
||||
} from "@common/components";
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
@@ -44,7 +44,7 @@ function MenuPage() {
|
||||
enabled: !!selectedMenu?.menuCd,
|
||||
});
|
||||
|
||||
const { data: treeData } = useQuery({
|
||||
const { data: menuTreeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
queryFn: () => MenuApi.getTreeMenuList(siteId),
|
||||
select: (data) => {
|
||||
@@ -187,8 +187,8 @@ function MenuPage() {
|
||||
};
|
||||
|
||||
const tree = additionalNode
|
||||
? [...(additionalNode ? [additionalNode] : []), ...(treeData ?? [])]
|
||||
: (treeData ?? []);
|
||||
? [...(additionalNode ? [additionalNode] : []), ...(menuTreeData ?? [])]
|
||||
: (menuTreeData ?? []);
|
||||
|
||||
return (
|
||||
<div className="card !mb-0 flex !min-h-[calc(100vh-16.8rem)] gap-2">
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions,
|
||||
type NodeModel,
|
||||
type TreeMethods,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { Button } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface";
|
||||
@@ -61,66 +58,61 @@ function MenuTree({
|
||||
return (
|
||||
<>
|
||||
<div className="max-h-[calc(100vh-33.5rem)] overflow-auto">
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
ref={treeMethods}
|
||||
render={(
|
||||
node: NodeModel<TreeMenu>,
|
||||
{ depth, isOpen, onToggle },
|
||||
) => {
|
||||
if (node.id === -1) {
|
||||
return (
|
||||
<EditableNode
|
||||
value={additionalNodeForm.watch("menuKornNm") ?? ""}
|
||||
onChange={(e) =>
|
||||
additionalNodeForm.setValue("menuKornNm", e.target.value)
|
||||
}
|
||||
depth={depth}
|
||||
onSave={onAdditionalNodeSave}
|
||||
onCancel={onAdditionalNodeCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
ref={treeMethods}
|
||||
render={(node: NodeModel<TreeMenu>, { depth, isOpen, onToggle }) => {
|
||||
if (node.id === -1) {
|
||||
return (
|
||||
<MenuNode
|
||||
node={node}
|
||||
<EditableNode
|
||||
value={additionalNodeForm.watch("menuKornNm") ?? ""}
|
||||
onChange={(e) =>
|
||||
additionalNodeForm.setValue("menuKornNm", e.target.value)
|
||||
}
|
||||
depth={depth}
|
||||
treeMethods={treeMethods.current}
|
||||
isOpen={isOpen}
|
||||
isSelected={selectedMenu?.menuCd === node.id}
|
||||
isAdditionalParent={isAdditionalParent(node)}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode(onSelectNode)}
|
||||
onSave={onAdditionalNodeSave}
|
||||
onCancel={onAdditionalNodeCancel}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onDrop={handleDropNode(updateTreeMenu)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
}}
|
||||
sort={false}
|
||||
dropTargetOffset={5}
|
||||
insertDroppableFirst={false}
|
||||
canDrop={(_, { dragSource, dropTargetId }) => {
|
||||
if (dragSource?.parent === dropTargetId) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
placeholderRender={(_, { depth }) => {
|
||||
return (
|
||||
<div
|
||||
className="absolute right-0 top-0 h-[0.2rem] bg-primary"
|
||||
style={{ left: depth * 10 + 5 }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
}
|
||||
return (
|
||||
<MenuNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
treeMethods={treeMethods.current}
|
||||
isOpen={isOpen}
|
||||
isSelected={selectedMenu?.menuCd === node.id}
|
||||
isAdditionalParent={isAdditionalParent(node)}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode(onSelectNode)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onDrop={handleDropNode(updateTreeMenu)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
}}
|
||||
sort={false}
|
||||
dropTargetOffset={5}
|
||||
insertDroppableFirst={false}
|
||||
canDrop={(_, { dragSource, dropTargetId }) => {
|
||||
if (dragSource?.parent === dropTargetId) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
placeholderRender={(_, { depth }) => {
|
||||
return (
|
||||
<div
|
||||
className="absolute right-0 top-0 h-[0.2rem] bg-primary"
|
||||
style={{ left: depth * 10 + 5 }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
|
||||
@@ -17,20 +17,23 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { OrganizationApi } from "../api/organization-service";
|
||||
import type { Organization, User } from "../model/organization-interface";
|
||||
cn,
|
||||
} from "@common/components";
|
||||
import {
|
||||
filterOrganizationTreeDataByKeyword,
|
||||
generateOrganizationTreeData,
|
||||
UserOrganizationApi,
|
||||
type TreeUserOrganization,
|
||||
type TreeUser,
|
||||
} from "@/entities/user-organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
filterUserOrganizationTreeDataByKeyword,
|
||||
generateUserOrganizationTreeData,
|
||||
} from "../lib/utils";
|
||||
|
||||
type TreeModel = Organization | User;
|
||||
type TreeModel = TreeUserOrganization | TreeUser;
|
||||
|
||||
interface OrganizationSearchDialogProps {
|
||||
handleUserSelect: (user: User) => void;
|
||||
handleUserSelect: (user: TreeUser) => void;
|
||||
}
|
||||
|
||||
function OrganizationSearchDialog({
|
||||
@@ -44,13 +47,13 @@ function OrganizationSearchDialog({
|
||||
|
||||
const { data: treeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION],
|
||||
queryFn: () => OrganizationApi.getOrganizationUserTreeList(),
|
||||
select: (data) => generateOrganizationTreeData(data),
|
||||
queryFn: () => UserOrganizationApi.getUserOrganizationTreeList(),
|
||||
select: (data) => generateUserOrganizationTreeData(data),
|
||||
});
|
||||
|
||||
const handleSelectNode = (node: NodeModel<TreeModel>) => {
|
||||
setSelectedNode(node);
|
||||
handleUserSelect(node.data as User);
|
||||
handleUserSelect(node.data as TreeUser);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,7 +82,7 @@ function OrganizationSearchDialog({
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setTree(filterOrganizationTreeDataByKeyword(tree, keyword))
|
||||
setTree(filterUserOrganizationTreeDataByKeyword(tree, keyword))
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-search text-base" />
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
import type { Contents } from "@/entities/contents";
|
||||
import type { User } from "../model/organization-interface";
|
||||
import type { Contents } from "@/entities/content";
|
||||
import type { TreeUser } from "@/entities/user-organization";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { MenuFormData, MenuType } from "../model/menu-interface";
|
||||
import { OrganizationSearchDialog } from "./organization-search-dialog";
|
||||
import { ContentSearchDialog } from "./content-search-dialog";
|
||||
@@ -37,7 +37,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
form.setValue("menuLnkgSnNm", content.contsTtl);
|
||||
};
|
||||
|
||||
const handleUserSelect = (user: User) => {
|
||||
const handleUserSelect = (user: TreeUser) => {
|
||||
form.setValue("menuPicUserUnqId", user.userUnqId);
|
||||
form.setValue("menuPicUserUnqIdNm", user.userNm);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import type { RoleAuthorizations } from "./role-authorizations-interface";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type { RoleAuthorizations } from "./role-authorizations-interface";
|
||||
import type { RoleAuthorizationsFormData } from "./role-authorizations-interface";
|
||||
|
||||
export const createColumns = (
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components/ui";
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { OrganizationPage } from "./ui/organization-page";
|
||||
@@ -0,0 +1,39 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type { SearchUser } from "@/entities/user-organization";
|
||||
|
||||
export const columns: ColumnDef<SearchUser>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "userNm",
|
||||
header: "이름",
|
||||
},
|
||||
{
|
||||
accessorKey: "ogdpSeNm",
|
||||
header: "소속",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "등록일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
} from "@common/components";
|
||||
import {
|
||||
UserOrganizationApi,
|
||||
type SearchUser,
|
||||
} from "@/entities/user-organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/additional-user-table-columns";
|
||||
|
||||
type AdditionalUserDialogProps = {
|
||||
onAddUsers: (user: SearchUser[]) => void;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function AdditionalUserDialog({
|
||||
onAddUsers,
|
||||
...props
|
||||
}: AdditionalUserDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [userNm, setUserNm] = useState("");
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: userList, refetch } = 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 handleAddUsers = () => {
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const userData = selectedRows.map((row) => row.original);
|
||||
if (!userData) return;
|
||||
|
||||
onAddUsers(userData);
|
||||
table.setRowSelection({});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setRowSelection({});
|
||||
setUserNm("");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) refetch();
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog {...props} open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" color="green">
|
||||
사용자 추가
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>콘텐츠 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
placeholder="검색어를 입력하세요."
|
||||
className="!text-[1.3rem]"
|
||||
value={userNm}
|
||||
onChange={(e) => setUserNm(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button color="white" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={handleAddUsers}>추가</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AdditionalUserDialog };
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import { DndTree } from "@/widgets/dnd-tree";
|
||||
import {
|
||||
generateOrganizationTreeData,
|
||||
type OrganizationTree,
|
||||
} from "@/entities/organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
type MoveOrganizationDialogProps = {
|
||||
buttonDisabled?: boolean;
|
||||
handleMoveOrganization: (organization: OrganizationTree) => Promise<void>;
|
||||
};
|
||||
|
||||
function MoveOrganizationDialog({
|
||||
buttonDisabled,
|
||||
handleMoveOrganization,
|
||||
}: MoveOrganizationDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const queryData = queryClient.getQueryData<OrganizationTree[]>([
|
||||
ENDPOINT.CMS_SERVICE.ORGANIZATIONS,
|
||||
]);
|
||||
const organizationTreeData = generateOrganizationTreeData(queryData ?? []);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedOrganization, setSelectedOrganization] =
|
||||
useState<OrganizationTree | null>(null);
|
||||
|
||||
const handleNodeSelect = (node: NodeModel<OrganizationTree>) => {
|
||||
if (!node.data) return;
|
||||
setSelectedOrganization(node.data);
|
||||
};
|
||||
|
||||
const onMoveOrganization = async () => {
|
||||
if (await confirm("이동하시겠습니까?")) {
|
||||
await handleMoveOrganization(selectedOrganization as OrganizationTree);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" color="white" disabled={buttonDisabled}>
|
||||
조직 이동
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>조직 이동</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DndTree
|
||||
tree={organizationTreeData ?? []}
|
||||
selectedNodeId={selectedOrganization?.ognzSn ?? 0}
|
||||
handleSelectNode={handleNodeSelect}
|
||||
classes={{
|
||||
root: "p-0",
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button color="white">취소</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={onMoveOrganization}>저장</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { MoveOrganizationDialog };
|
||||
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import type { OrganizationFormData } from "@/entities/organization";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
|
||||
type OrganizationFormProps = {
|
||||
form: UseFormReturn<OrganizationFormData>;
|
||||
ognzSn?: number;
|
||||
handleCreate: (data: OrganizationFormData) => Promise<void>;
|
||||
handleUpdate: ({
|
||||
ognzSn,
|
||||
data,
|
||||
}: {
|
||||
ognzSn: number;
|
||||
data: OrganizationFormData;
|
||||
}) => Promise<void>;
|
||||
handleDelete?: (ognzSn: number) => Promise<void>;
|
||||
};
|
||||
|
||||
function OrganizationForm({
|
||||
form,
|
||||
ognzSn,
|
||||
handleCreate,
|
||||
handleUpdate,
|
||||
handleDelete,
|
||||
}: OrganizationFormProps) {
|
||||
const isModifyMode = !!ognzSn;
|
||||
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const onSubmit = async (data: OrganizationFormData) => {
|
||||
if (isModifyMode) {
|
||||
await onUpdate(data);
|
||||
} else {
|
||||
await onCreate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const onCreate = async (data: OrganizationFormData) => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
handleCreate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdate = async (data: OrganizationFormData) => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
handleUpdate({ ognzSn: ognzSn!, data });
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.(ognzSn!);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{isModifyMode ? "조직도 수정" : "조직도 등록"}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>조직명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ognzNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="조직명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
value={field.value ?? 0}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="정렬순서를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>조직설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ognzExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="조직설명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button
|
||||
size="lg"
|
||||
color="red"
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
disabled={!isModifyMode}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export { OrganizationForm };
|
||||
@@ -0,0 +1,238 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@common/components";
|
||||
import { DndTree, EditableNode } from "@/widgets/dnd-tree";
|
||||
import {
|
||||
OrganizationApi,
|
||||
organizationFormSchema,
|
||||
generateOrganizationTreeData,
|
||||
handleDropNode,
|
||||
type OrganizationFormData,
|
||||
type OrganizationTree,
|
||||
type OrganizationTreeForDnd,
|
||||
} from "@/entities/organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { OrganizationForm } from "./organization-form";
|
||||
import { UserOrganizationSection } from "./user-organization-section";
|
||||
|
||||
function OrganizationPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [additionalNode, setAdditionalNode] =
|
||||
useState<NodeModel<OrganizationTree> | null>(null);
|
||||
const [selectedOrganization, setSelectedOrganization] =
|
||||
useState<OrganizationTree | null>(null);
|
||||
|
||||
const { data: organizationTreeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
|
||||
queryFn: () => OrganizationApi.getOrganizationsTreeList(),
|
||||
select: (data) => generateOrganizationTreeData(data),
|
||||
});
|
||||
|
||||
const { data: organizationInfo } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.CMS_SERVICE.ORGANIZATIONS,
|
||||
selectedOrganization?.ognzSn,
|
||||
],
|
||||
queryFn: () =>
|
||||
OrganizationApi.getOrganizationInfo(selectedOrganization!.ognzSn!),
|
||||
enabled: !!selectedOrganization?.ognzSn,
|
||||
});
|
||||
|
||||
const { mutateAsync: createOrganization } = useMutation({
|
||||
mutationFn: (form: OrganizationFormData) =>
|
||||
OrganizationApi.createOrganization(form),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteOrganization } = useMutation({
|
||||
mutationFn: (ognzSn: number) => OrganizationApi.deleteOrganization(ognzSn),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.");
|
||||
setSelectedOrganization(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateOrganizationName } = useMutation({
|
||||
mutationFn: ({ ognzSn, name }: { ognzSn: number; name: string }) =>
|
||||
OrganizationApi.updateOrganizationName(ognzSn, name),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateOrganization } = useMutation({
|
||||
mutationFn: ({
|
||||
ognzSn,
|
||||
data,
|
||||
}: {
|
||||
ognzSn: number;
|
||||
data: OrganizationFormData;
|
||||
}) => OrganizationApi.updateOrganization(ognzSn, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateOrganizationByDnd } = useMutation({
|
||||
mutationFn: (data: OrganizationTreeForDnd) =>
|
||||
OrganizationApi.updateOrganizationByDnd([data]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<OrganizationFormData>({
|
||||
resolver: zodResolver(organizationFormSchema),
|
||||
defaultValues: {
|
||||
ognzNm: "",
|
||||
sortSeq: 1,
|
||||
useYn: "Y",
|
||||
},
|
||||
values: {
|
||||
ognzNm: organizationInfo?.ognzNm ?? "",
|
||||
ognzExpln: organizationInfo?.ognzExpln ?? "",
|
||||
sortSeq: organizationInfo?.sortSeq ?? 1,
|
||||
useYn: organizationInfo?.useYn ?? "Y",
|
||||
},
|
||||
});
|
||||
|
||||
const additionalNodeForm = useForm<OrganizationFormData>({
|
||||
resolver: zodResolver(organizationFormSchema),
|
||||
defaultValues: {
|
||||
ognzNm: "",
|
||||
sortSeq: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const tree = additionalNode
|
||||
? [
|
||||
...(additionalNode ? [additionalNode] : []),
|
||||
...(organizationTreeData ?? []),
|
||||
]
|
||||
: (organizationTreeData ?? []);
|
||||
|
||||
const onSaveAdditionalNode = async () => {
|
||||
await createOrganization({ ...additionalNodeForm.getValues() });
|
||||
resetAdditionalNode();
|
||||
};
|
||||
|
||||
const onDeleteOrganization = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
deleteOrganization(selectedOrganization!.ognzSn!);
|
||||
}
|
||||
};
|
||||
|
||||
const resetAdditionalNode = () => {
|
||||
additionalNodeForm.setValue("ognzNm", "");
|
||||
setAdditionalNode(null);
|
||||
};
|
||||
|
||||
const handleNodeSelect = (node: NodeModel<OrganizationTree>) => {
|
||||
if (!node.data) return;
|
||||
if (node.data.ognzSn === selectedOrganization?.ognzSn) {
|
||||
setSelectedOrganization(null);
|
||||
additionalNodeForm.setValue("upOgnzSn", null);
|
||||
return;
|
||||
}
|
||||
setSelectedOrganization(node.data);
|
||||
additionalNodeForm.setValue("upOgnzSn", (node.id as number) ?? 0);
|
||||
};
|
||||
|
||||
const handleAddOrganization = () => {
|
||||
if (additionalNode) return;
|
||||
setAdditionalNode({
|
||||
id: -1,
|
||||
parent: selectedOrganization?.ognzSn ?? 0,
|
||||
text: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card !mb-0 flex !min-h-[calc(100vh-16.8rem)] gap-2">
|
||||
<div className="card card-border w-[24rem]">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="flex-1"
|
||||
onClick={handleAddOrganization}
|
||||
>
|
||||
조직 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
className="flex-1"
|
||||
onClick={onDeleteOrganization}
|
||||
disabled={!selectedOrganization}
|
||||
>
|
||||
조직 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<DndTree
|
||||
tree={tree}
|
||||
selectedNodeId={selectedOrganization?.ognzSn ?? 0}
|
||||
renderEditableNode={(props) => (
|
||||
<EditableNode
|
||||
value={additionalNodeForm.watch("ognzNm")}
|
||||
onChange={(e) =>
|
||||
additionalNodeForm.setValue("ognzNm", e.target.value)
|
||||
}
|
||||
onSave={onSaveAdditionalNode}
|
||||
onCancel={resetAdditionalNode}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
handleSelectNode={handleNodeSelect}
|
||||
handleDropNode={handleDropNode(updateOrganizationByDnd)}
|
||||
updateNodeName={async (id, name) => {
|
||||
await updateOrganizationName({ ognzSn: id as number, name });
|
||||
}}
|
||||
classes={{ wrapper: "max-h-[calc(100vh-29rem)]" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<OrganizationForm
|
||||
form={form}
|
||||
ognzSn={selectedOrganization?.ognzSn ?? 0}
|
||||
handleCreate={createOrganization}
|
||||
handleUpdate={updateOrganization}
|
||||
handleDelete={deleteOrganization}
|
||||
/>
|
||||
{selectedOrganization?.ognzSn && (
|
||||
<UserOrganizationSection ognzSn={selectedOrganization.ognzSn} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { OrganizationPage };
|
||||
@@ -0,0 +1,383 @@
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useState, useCallback, memo } from "react";
|
||||
import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
useQuery,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
keepPreviousData,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import {
|
||||
UserOrganizationApi,
|
||||
userOrganizationFormSchema,
|
||||
type UserOrganizationFormData,
|
||||
type UserOrganizationPayload,
|
||||
type User,
|
||||
type SearchUser,
|
||||
type MoveUserOrganizationPayload,
|
||||
} from "@/entities/user-organization";
|
||||
import { CodeApi, type ICode } from "@/entities/code";
|
||||
import type { OrganizationTree } from "@/entities/organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { AdditionalUserDialog } from "./additional-user-dialog";
|
||||
import { MoveOrganizationDialog } from "./move-organization-dialog";
|
||||
|
||||
type UserOrganizationSectionProps = {
|
||||
ognzSn: number;
|
||||
};
|
||||
|
||||
function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [userList, setUserList] = useState<User[]>([]);
|
||||
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
|
||||
|
||||
const isAllSelected =
|
||||
userList.length > 0 && selectedUsers.length === userList.length;
|
||||
|
||||
const { data: userOrganizationList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn],
|
||||
queryFn: () => UserOrganizationApi.getUserOrganizationList({ ognzSn }),
|
||||
enabled: !!ognzSn,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { data: jobPositionCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JPBSCLCD"],
|
||||
queryFn: () => CodeApi.getCode("JPBSCLCD"),
|
||||
});
|
||||
|
||||
const { data: jobGradeCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JDGDCLCD"],
|
||||
queryFn: () => CodeApi.getCode("JDGDCLCD"),
|
||||
});
|
||||
|
||||
const { mutate: saveUserOrganization } = useMutation({
|
||||
mutationFn: (data: UserOrganizationPayload[]) =>
|
||||
UserOrganizationApi.saveUserOrganization(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: moveUserOrganization } = useMutation({
|
||||
mutationFn: (data: MoveUserOrganizationPayload[]) =>
|
||||
UserOrganizationApi.moveUserOrganization(data),
|
||||
onSuccess: () => {
|
||||
alert("이동되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<UserOrganizationFormData>({
|
||||
resolver: zodResolver(userOrganizationFormSchema),
|
||||
values: {
|
||||
userOrganization:
|
||||
userOrganizationList?.content.map((user) => ({
|
||||
mode: user.mode,
|
||||
ognzSn: user.ognzSn,
|
||||
userUnqId: user.userUnqId,
|
||||
jbpsSeCd: user.jbpsSeCd,
|
||||
jbgdSeCd: user.jbgdSeCd,
|
||||
sortSeq: user.sortSeq,
|
||||
})) ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const { append } = useFieldArray({
|
||||
name: "userOrganization",
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const handleAddUsers = (users: SearchUser[]) => {
|
||||
const generateUserData = (user: SearchUser) => ({
|
||||
mode: "C",
|
||||
ognzSn,
|
||||
userUnqId: user.userUnqId,
|
||||
jbpsSeCd: "",
|
||||
jbgdSeCd: "",
|
||||
sortSeq: 0,
|
||||
});
|
||||
|
||||
const newFormData = users.map(generateUserData);
|
||||
append(newFormData);
|
||||
|
||||
const newUserList = users.map((user) => ({
|
||||
...generateUserData(user),
|
||||
userUnqNm: user.userNm,
|
||||
}));
|
||||
const existingUserIds = new Set(newUserList.map((user) => user.userUnqId));
|
||||
const filteredUserList = userList.filter(
|
||||
(user) => !existingUserIds.has(user.userUnqId),
|
||||
);
|
||||
setUserList([...filteredUserList, ...newUserList]);
|
||||
};
|
||||
|
||||
const handleToggleSelectUser = useCallback((user: User) => {
|
||||
setSelectedUsers((prevSelected) => {
|
||||
const isSelected = prevSelected.some(
|
||||
(item) => item.userUnqId === user.userUnqId,
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
return prevSelected.filter((item) => item.userUnqId !== user.userUnqId);
|
||||
} else {
|
||||
return [...prevSelected, user];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleSelectAll = () => {
|
||||
if (isAllSelected) {
|
||||
setSelectedUsers([]);
|
||||
} else {
|
||||
setSelectedUsers([...userList]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUsers = () => {
|
||||
const selectedUserIds = selectedUsers.map((user) => user.userUnqId);
|
||||
const updatedFormData = form.getValues("userOrganization").map((item) => {
|
||||
if (selectedUserIds.includes(item.userUnqId)) {
|
||||
return { ...item, mode: "D" };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
form.setValue("userOrganization", updatedFormData);
|
||||
setUserList((prevUsers) =>
|
||||
prevUsers.filter((user) => !selectedUserIds.includes(user.userUnqId)),
|
||||
);
|
||||
setSelectedUsers([]);
|
||||
};
|
||||
|
||||
const handleSaveUsers = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const filteredData = form
|
||||
.watch("userOrganization")
|
||||
.filter((item) => item.mode !== "R");
|
||||
saveUserOrganization(filteredData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveOrganization = async (organization: OrganizationTree) => {
|
||||
const payloadData: MoveUserOrganizationPayload[] = selectedUsers.map(
|
||||
(user) => ({
|
||||
ognzSn: user.ognzSn,
|
||||
toOgnzSn: organization.ognzSn!,
|
||||
userUnqId: user.userUnqId,
|
||||
}),
|
||||
);
|
||||
moveUserOrganization(payloadData);
|
||||
};
|
||||
|
||||
const isUserSelected = useCallback(
|
||||
(userUnqId: string) =>
|
||||
selectedUsers.some((item) => item.userUnqId === userUnqId),
|
||||
[selectedUsers],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (userOrganizationList?.content) {
|
||||
setUserList(userOrganizationList.content);
|
||||
setSelectedUsers([]);
|
||||
}
|
||||
}, [userOrganizationList]);
|
||||
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">소속원 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<MoveOrganizationDialog
|
||||
buttonDisabled={
|
||||
userOrganizationList?.content.length === 0 ||
|
||||
selectedUsers.length === 0
|
||||
}
|
||||
handleMoveOrganization={handleMoveOrganization}
|
||||
/>
|
||||
<AdditionalUserDialog onAddUsers={handleAddUsers} />
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteUsers}
|
||||
disabled={selectedUsers.length === 0}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSaveUsers}>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0">
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleToggleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>NO</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead>직위</TableHead>
|
||||
<TableHead>직급</TableHead>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableHead>최종수정자</TableHead>
|
||||
<TableHead>최종수정일</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{userList.map((user, index) => (
|
||||
<TableRow key={user.userUnqId}>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
checked={isUserSelected(user.userUnqId)}
|
||||
onCheckedChange={() => handleToggleSelectUser(user)}
|
||||
/>
|
||||
</TableCell>
|
||||
<UserCells
|
||||
form={form}
|
||||
user={user}
|
||||
index={index}
|
||||
jobPositionCodeData={jobPositionCodes}
|
||||
jobGradeCodeData={jobGradeCodes}
|
||||
/>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type UserCellsProps = {
|
||||
form: UseFormReturn<UserOrganizationFormData>;
|
||||
user: User;
|
||||
index: number;
|
||||
jobPositionCodeData?: ICode[];
|
||||
jobGradeCodeData?: ICode[];
|
||||
};
|
||||
|
||||
const UserCells = memo(
|
||||
({
|
||||
form,
|
||||
user,
|
||||
index,
|
||||
jobPositionCodeData,
|
||||
jobGradeCodeData,
|
||||
}: UserCellsProps) => {
|
||||
const updateMode = (index: number) => {
|
||||
const mode = form.watch(`userOrganization.${index}.mode`);
|
||||
if (mode === "R") {
|
||||
form.setValue(`userOrganization.${index}.mode`, "U");
|
||||
}
|
||||
};
|
||||
|
||||
const updateFormValue =
|
||||
(index: number, key: keyof UserOrganizationPayload) =>
|
||||
(value: string | number) => {
|
||||
updateMode(index);
|
||||
form.setValue(`userOrganization.${index}.${key}`, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableCell align="center">{index + 1}</TableCell>
|
||||
<TableCell align="center">{user.userUnqNm}</TableCell>
|
||||
<TableCell align="center">
|
||||
<CodeSelect
|
||||
codeData={jobPositionCodeData}
|
||||
defaultValue={user.jbpsSeCd}
|
||||
onValueChange={updateFormValue(index, "jbpsSeCd")}
|
||||
{...form.register(`userOrganization.${index}.jbpsSeCd`)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CodeSelect
|
||||
codeData={jobGradeCodeData}
|
||||
defaultValue={user.jbgdSeCd}
|
||||
onValueChange={updateFormValue(index, "jbgdSeCd")}
|
||||
{...form.register(`userOrganization.${index}.jbgdSeCd`)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
defaultValue={user.sortSeq}
|
||||
onChange={(e) =>
|
||||
updateFormValue(index, "sortSeq")(Number(e.target.value))
|
||||
}
|
||||
className="w-[6rem] !text-[1.3rem]"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">{user.mdfrNm}</TableCell>
|
||||
<TableCell align="center">
|
||||
{dayjs(user.mdfcnDt).format("YYYY-MM-DD")}
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
UserCells.displayName = "UserCells";
|
||||
|
||||
type CodeSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
|
||||
codeData?: ICode[];
|
||||
};
|
||||
|
||||
const CodeSelect = memo(({ codeData, ...props }: CodeSelectProps) => {
|
||||
if (!codeData) return null;
|
||||
|
||||
const placeholderText =
|
||||
codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ??
|
||||
"선택";
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder={placeholderText} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codeData.map((item) => (
|
||||
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
|
||||
{item.lwpoCdNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
});
|
||||
|
||||
CodeSelect.displayName = "CodeSelect";
|
||||
|
||||
export { UserOrganizationSection };
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TemplateApi } from "../api/template-service";
|
||||
@@ -15,6 +15,7 @@ import { TemplateForm } from "./template-form";
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createTemplate } = useMutation({
|
||||
mutationFn: (data: TemplateFormData) => TemplateApi.createTemplate(data),
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const ENDPOINT = {
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
|
||||
CONTENTS: "/cms-service/api/v1/contents",
|
||||
CONTENTS_DETAILS: "/cms-service/api/v1/contents-details",
|
||||
ORGANIZATIONS: "/cms-service/api/v1/ognzs",
|
||||
USER_ORGANIZATION: "/cms-service/api/v1/user-ognz",
|
||||
IMAGES: "/cms-service/api/v1/images",
|
||||
BANNERS: "/cms-service/api/v1/bnrs",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import type { MenuItem } from "@common/components/api";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
export type MenuState = {
|
||||
currentMenuInfo: MenuItem | null;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
} from "@common/components/ui";
|
||||
} from "@common/components";
|
||||
import { useAlertStore } from "../lib/use-alert-store";
|
||||
|
||||
function AlertDialog() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DialogClose,
|
||||
Button,
|
||||
type ButtonProps,
|
||||
} from "@common/components/ui";
|
||||
} from "@common/components";
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
isOpen?: boolean;
|
||||
@@ -24,7 +24,9 @@ function ConfirmDialog({
|
||||
<Dialog open={isOpen ?? isGlobalOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Skeleton } from "@common/components/ui/skeleton";
|
||||
"use client";
|
||||
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
function SkeletonPage() {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { DndTree } from "./ui/dnd-tree";
|
||||
export { EditableNode } from "./ui/editable-node";
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||
import { Button, cn } from "@common/components";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
interface TreeData extends Record<string, unknown> {
|
||||
children?: unknown[];
|
||||
}
|
||||
|
||||
type CustomNodeProps<T extends TreeData> = {
|
||||
node: NodeModel<T>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
isSelected: boolean;
|
||||
isAdditionalParent: boolean;
|
||||
treeMethods: TreeMethods | null;
|
||||
readOnly: boolean;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
onSelect: (node: NodeModel<T>) => void;
|
||||
updateNodeName: (id: NodeModel["id"], nodeName: string) => Promise<void>;
|
||||
};
|
||||
|
||||
function CustomNode<T extends TreeData>({
|
||||
node,
|
||||
depth,
|
||||
isOpen,
|
||||
isSelected,
|
||||
isAdditionalParent,
|
||||
treeMethods,
|
||||
readOnly,
|
||||
onToggle,
|
||||
onSelect,
|
||||
updateNodeName,
|
||||
}: CustomNodeProps<T>) {
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 10 + 5;
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(text);
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
const handleSelect = () => onSelect(node);
|
||||
|
||||
const handleSave = async () => {
|
||||
await updateNodeName(id, nodeName);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditMode(false);
|
||||
setNodeName(text);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdditionalParent) treeMethods?.open(id);
|
||||
}, [isAdditionalParent]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent",
|
||||
isSelected && "!bg-[rgba(32,148,250,0.2)]",
|
||||
!readOnly && "group",
|
||||
)}
|
||||
>
|
||||
{editMode ? (
|
||||
<EditableNode
|
||||
value={nodeName}
|
||||
onChange={(e) => setNodeName(e.target.value)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center" onClick={handleSelect}>
|
||||
{data && data.children && data.children.length > 0 && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">{text}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="invisible !bg-transparent group-hover:visible"
|
||||
onClick={() => setEditMode(true)}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CustomNode };
|
||||
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import React, { type ReactElement, useRef } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions,
|
||||
type NodeModel,
|
||||
type TreeMethods,
|
||||
type DropOptions,
|
||||
type Classes,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { Button, cn } from "@common/components";
|
||||
import { CustomNode } from "./custom-node";
|
||||
|
||||
interface TreeData extends Record<string, unknown> {
|
||||
children?: unknown[];
|
||||
}
|
||||
|
||||
type EditableNodeProps = { depth: number };
|
||||
|
||||
type DndTreeProps<T extends TreeData> = {
|
||||
tree: NodeModel<T>[];
|
||||
selectedNodeId?: number;
|
||||
classes?: {
|
||||
wrapper?: string;
|
||||
} & Classes<T>;
|
||||
renderEditableNode?: (props: EditableNodeProps) => ReactElement;
|
||||
handleSelectNode?: (node: NodeModel<T>) => void;
|
||||
handleDropNode?: (newTree: NodeModel<T>[], options: DropOptions<T>) => void;
|
||||
updateNodeName?: (id: NodeModel["id"], nodeName: string) => Promise<void>;
|
||||
};
|
||||
|
||||
function DndTree<T extends TreeData>({
|
||||
tree,
|
||||
selectedNodeId,
|
||||
classes,
|
||||
renderEditableNode,
|
||||
handleSelectNode,
|
||||
handleDropNode,
|
||||
updateNodeName,
|
||||
}: DndTreeProps<T>) {
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
const isAdditionalParent = (node: NodeModel<T>): boolean => {
|
||||
const additionalNode = tree.find((n) => n.id === -1);
|
||||
if (!additionalNode) return false;
|
||||
return additionalNode?.parent === node.id;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"max-h-[calc(100vh-33.5rem)] overflow-auto",
|
||||
classes?.wrapper,
|
||||
)}
|
||||
>
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
ref={treeMethods}
|
||||
render={(node: NodeModel<T>, { depth, isOpen, onToggle }) => {
|
||||
if (node.id === -1)
|
||||
return renderEditableNode?.({ depth }) ?? <></>;
|
||||
return (
|
||||
<CustomNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
treeMethods={treeMethods.current}
|
||||
isOpen={isOpen}
|
||||
isSelected={selectedNodeId === node.id}
|
||||
isAdditionalParent={isAdditionalParent(node)}
|
||||
readOnly={typeof updateNodeName === "undefined"}
|
||||
onToggle={onToggle}
|
||||
onSelect={(node) => handleSelectNode?.(node)}
|
||||
updateNodeName={(id, nodeName) =>
|
||||
updateNodeName?.(id, nodeName) ?? Promise.resolve()
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onDrop={(newTree, options) => handleDropNode?.(newTree, options)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
...classes,
|
||||
}}
|
||||
sort={false}
|
||||
dropTargetOffset={5}
|
||||
insertDroppableFirst={false}
|
||||
canDrop={(_, { dragSource, dropTargetId }) => {
|
||||
if (dragSource?.parent === dropTargetId) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
placeholderRender={(_, { depth }) => {
|
||||
return (
|
||||
<div
|
||||
className="absolute right-0 top-0 h-[0.2rem] bg-primary"
|
||||
style={{ left: depth * 10 + 5 }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.openAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-expand" />
|
||||
전체펼침
|
||||
</Button>
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.closeAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-contract" />
|
||||
전체접기
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { DndTree };
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
} & ComponentProps<typeof Input>;
|
||||
|
||||
function EditableNode({
|
||||
depth,
|
||||
onSave,
|
||||
onCancel,
|
||||
...props
|
||||
}: EditableNodeProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-sm bg-accent p-2"
|
||||
style={{ marginInlineStart: !depth ? 0 : depth * 5 + 5 }}
|
||||
>
|
||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { EditableNode };
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type FieldValues, type UseFormReturn } from "react-hook-form";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { Button } from "@common/components";
|
||||
import { Form } from "@/shared/ui/form";
|
||||
|
||||
type FormWrapperProps<T extends FieldValues> = {
|
||||
@@ -54,7 +54,7 @@ function FormWrapper<T extends FieldValues>({
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
{isModifyMode && handleDelete && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user