Merge commit '4ee673204552110924d8c9652cb752d10310cf1b' into iipbs/wr/hasuReg

This commit is contained in:
leedj312
2025-06-18 20:35:08 +09:00
475 changed files with 23304 additions and 2954 deletions
+2
View File
@@ -1,5 +1,7 @@
import "./styles/base.css";
import "./styles/components.css";
import "@fontsource-variable/noto-sans-kr";
import { AppProvider } from "./providers";
import { cookies } from "next/headers";
+23 -16
View File
@@ -3,34 +3,41 @@
import { DndProvider } from "react-dnd";
import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
import { StoreProvider } from "@/shared/providers";
import { PromptProvider, StoreProvider } from "@/shared/providers";
import { AlertDialog } from "@/shared/ui/alert-dialog";
import { PromptDialog } from "@/shared/ui/prompt-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>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
{children}
</DndProvider>
</MenuWrapper>
<AlertDialog />
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
<PromptProvider>
<ConfirmProvider>
<AuthWrapper>
<MenuWrapper>
<DndProvider
backend={MultiBackend}
options={getBackendOptions()}
>
{children}
</DndProvider>
</MenuWrapper>
<AlertDialog />
<PromptDialog />
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
</PromptProvider>
</StoreProvider>
<div className="text-2xl">
<ReactQueryDevtools initialIsOpen={false} />
</div>
</QueryClientProvider>
);
};
+14 -4
View File
@@ -1,11 +1,21 @@
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@import "@common/components/index.css";
@tailwind base;
@tailwind utilities;
html,
body {
font-family: "Noto Sans KR", sans-serif;
font-size: 10px;
@layer base {
:root {
--secondary-background: rgba(0, 0, 0, 0.04);
}
[data-theme="black"] {
--secondary-background: rgba(255, 255, 255, 0.09);
}
html,
body {
font-family: "Noto Sans KR", sans-serif;
font-size: 10px;
}
}
@@ -1,3 +1,4 @@
import type { AxiosResponse } from "axios";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
@@ -25,7 +26,7 @@ export class AttachmentApiService implements AttachmentService {
return response.data;
}
public async deleteAttachment(atflId: string): Promise<void> {
public async deleteAttachmentByAtflId(atflId: string): Promise<void> {
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
}
@@ -35,7 +36,7 @@ export class AttachmentApiService implements AttachmentService {
);
}
public async getImageUrl(atflId: string): Promise<string> {
public async getImageUrlByAtflId(atflId: string): Promise<string> {
if (!atflId) return "";
const response = await fetch(`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${atflId}`);
const blob = await response.blob();
@@ -50,6 +51,40 @@ export class AttachmentApiService implements AttachmentService {
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getStreamVideoUrlByAtflId(atflId: string): Promise<string> {
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.STREAM_VIDEO}/${atflId}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getMovieThumbnailUrlByAtflId(atflId: string): Promise<string> {
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/${atflId}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getMovieThumbnailUrlByAtflCd(atflCd: string): Promise<string> {
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/atflCd/${atflCd}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async downloadAttachmentByAtflCd(
atflCd: string,
): Promise<AxiosResponse> {
const response = await client.get(
`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`,
{ responseType: "blob" },
);
return response;
}
}
export const AttachmentApi = new AttachmentApiService();
@@ -1,3 +1,4 @@
import type { AxiosResponse } from "axios";
import type { PageableList, PageableParams } from "@/shared/model";
export interface AttachmentParams extends PageableParams {
@@ -35,8 +36,12 @@ export type AttachmentList = PageableList<Attachment>;
export interface AttachmentService {
getAttachmentsList: (params: AttachmentParams) => Promise<AttachmentList>;
getAttachmentsInfo: (atflCd: string) => Promise<Attachment[]>;
deleteAttachment: (atflId: string) => Promise<void>;
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
getImageUrl: (atflId: string) => Promise<string>;
getImageUrlByAtflId: (atflId: string) => Promise<string>;
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
getStreamVideoUrlByAtflId: (atflId: string) => Promise<string>;
getMovieThumbnailUrlByAtflId: (atflId: string) => Promise<string>;
getMovieThumbnailUrlByAtflCd: (atflCd: string) => Promise<string>;
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
}
@@ -19,7 +19,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
}
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
);
if (!userInfo) setUserInfo(response.data);
}
@@ -6,11 +6,11 @@ import type {
AuthorizationsList,
AuthorizationFormData,
WithRoleAuthorizations,
} from "../model/authorizations-interface";
} from "../model/authorization-interface";
export class AuthorizationsApiService implements AuthorizationsService {
public async getAuthorizationsList(
params: AuthorizationsParams,
params?: AuthorizationsParams,
): Promise<AuthorizationsList> {
const response = await client.get<AuthorizationsList>(
`${ENDPOINT.USER_SERVICE.AUTHORIZATIONS}`,
@@ -0,0 +1,10 @@
export { AuthorizationsApi } from "./api/authorization-service";
export {
authorizationFormSchema,
type Authorizations,
type AuthorizationsParams,
type AuthorizationFormData,
type WithRoleAuthorizations,
type AuthorizationsService,
type AuthorizationsList,
} from "./model/authorization-interface";
@@ -47,7 +47,7 @@ export type AuthorizationFormData = z.infer<typeof authorizationFormSchema>;
export interface AuthorizationsService {
getAuthorizationsList: (
params: AuthorizationsParams,
params?: AuthorizationsParams,
) => Promise<AuthorizationsList>;
getAuthorizationInfo: (
authorizationNo: string,
+1
View File
@@ -0,0 +1 @@
export { BoardApi } from "../api/board-service";
@@ -11,7 +11,7 @@ import type {
} from "../model/board-interface";
export class BoardApiService implements BoardService {
public async getBoardList(params: BoardParams): Promise<BoardList> {
public async getBoardList(params?: BoardParams): Promise<BoardList> {
const response = await client.get<BoardList>(
`${ENDPOINT.BOARD_SERVICE.BBS}`,
{ params },
@@ -0,0 +1,17 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
BoardTemplate,
BoardTemplateService,
} from "../model/board-template-interface";
export class BoardTemplateApiService implements BoardTemplateService {
public async getBoardTemplate(bbsTypeCd: string): Promise<BoardTemplate> {
const response = await client.get<BoardTemplate>(
`${ENDPOINT.BOARD_SERVICE.BBS_TEMPLATES}/${bbsTypeCd}`,
);
return response.data;
}
}
export const BoardTemplateApi = new BoardTemplateApiService();
+3
View File
@@ -1,8 +1,11 @@
export { BoardApi } from "./api/board-service";
export {
boardFormSchema,
BOARD_POSTING_CODE,
type Board,
type BoardFormData,
type BoardParams,
type BoardPopup,
type BoardPostingCode,
} from "./model/board-interface";
export { BoardTemplateApi } from "./api/board-template-service";
@@ -1,14 +1,15 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
import type { BbsType } from "@/shared/store";
export const BOARD_TYPE = {
BBSNORMAL: "BBSNORMAL",
BBSFAQ: "BBSFAQ",
BBSIMAGE: "BBSIMAGE",
BBSMOVIE: "BBSMOVIE",
export const BOARD_POSTING_CODE = {
팝업게시: "01",
상단게시: "02",
기간게시: "03",
} as const;
export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE];
export type BoardPostingCode =
(typeof BOARD_POSTING_CODE)[keyof typeof BOARD_POSTING_CODE];
export interface BoardParams extends PageableParams {
keywordType?: string;
@@ -37,7 +38,7 @@ export interface BoardPopupParams extends PageableParams {
export interface Board {
bbsId: number;
bbsTypeCd: string;
bbsTypeCd: BbsType;
bbsTypeNm: string;
bbsNm: string;
bbsExpln: string;
@@ -0,0 +1,24 @@
export interface BoardTemplate {
bbsTypeCd: string;
rfrncUseYn: string;
atflUseYn: string;
atflMaxCnt: number;
atflMaxFileSz: number;
ansUseYn: string;
cmntUseYn: string;
dgstfnEvlUseYn: string;
smsUseYn: string;
upndFixUseYn: string;
pstgPrdUseYn: string;
popupUseYn: string;
regDt: string;
rgtr: string;
mdfcnDt: string;
mdfr: string;
rgtrNm: string;
mdfrNm: string;
}
export interface BoardTemplateService {
getBoardTemplate: (bbsTypeCd: string) => Promise<BoardTemplate>;
}
@@ -0,0 +1,67 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
LwpoCodeFormSchema,
type LwpoCodeService,
type LwpoCodeList,
type LwpoCodeDetail,
type LwpoCodeParams,
type LwpoCodeFormData,
} from "../model/lwpocode-interface";
export class LwpoCodeApiService implements LwpoCodeService {
public async getLwpoCodeList(params?: LwpoCodeParams): Promise<LwpoCodeList> {
const response = await client.get<LwpoCodeList>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}`,
{ params },
);
return response.data;
}
public async getUpCodeLwpoCodeList(upCd: string): Promise<LwpoCodeList> {
const response = await client.get<LwpoCodeList>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/up-cd/${upCd}`,
);
return response.data;
}
public async lwpoCodeDetail(
upCd: string,
lwpoCd: string,
): Promise<LwpoCodeDetail> {
const response = await client.get<LwpoCodeDetail>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
);
return response.data;
}
public async createLwpoCode(data: LwpoCodeFormData): Promise<void> {
const validatedData = LwpoCodeFormSchema.parse(data);
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}`,
validatedData,
);
return response.data;
}
public async updateLwpoCode(
upCd: string,
lwpoCd: string,
data: LwpoCodeFormData,
): Promise<void> {
const validatedData = LwpoCodeFormSchema.parse(data);
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
validatedData,
);
return response.data;
}
public async deleteLwpoCode(upCd: string, lwpoCd: string): Promise<void> {
await client.delete<void>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
);
}
}
export const LwpoCodeApi = new LwpoCodeApiService();
@@ -0,0 +1,47 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
upCodeFormSchema,
type UpCodeService,
type UpCodeList,
type UpCodeDetail,
type UpCodeParams,
type UpCodeFormData,
} from "../model/upcode-interface";
export class UpCodeApiService implements UpCodeService {
public async getUpCodeList(params?: UpCodeParams): Promise<UpCodeList> {
const response = await client.get<UpCodeList>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}`,
{ params },
);
return response.data;
}
public async upCodeDetail(upCd: string): Promise<UpCodeDetail> {
const response = await client.get<UpCodeDetail>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`,
);
return response.data;
}
public async createUpCode(data: UpCodeFormData): Promise<void> {
const validatedData = upCodeFormSchema.parse(data);
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}`,
validatedData,
);
return response.data;
}
public async updateUpCode(upCd: string, data: UpCodeFormData): Promise<void> {
const validatedData = upCodeFormSchema.parse(data);
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`,
validatedData,
);
return response.data;
}
}
export const UpCodeApi = new UpCodeApiService();
+16
View File
@@ -1,3 +1,19 @@
export { CodeApi } from "./api/code-service";
export type { ICode } from "./model/code-interface";
export { CodeSelect } from "./ui/code-select";
export { UpCodeApi } from "./api/upcode-service";
export {
upCodeFormSchema,
type UpCode,
type UpCodeFormData,
type UpCodeParams,
} from "./model/upcode-interface";
export { LwpoCodeApi } from "./api/lwpocode-service";
export {
LwpoCodeFormSchema,
type LwpoCode,
type LwpoCodeFormData,
type LwpoCodeParams,
} from "./model/lwpocode-interface";
@@ -0,0 +1,68 @@
import { z } from "zod";
import type { PageableParams, PageableList } from "@/shared/model";
export interface LwpoCodeParams extends PageableParams {
upCd?: string;
lwpoCd?: string;
lwpoCdNm?: string;
useYn?: string;
}
export interface LwpoCode {
lwpoCd: string;
upCd: string;
lwpoCdNm: string;
lwpoCdExpln: string;
unit: string;
prntLwpoCd: string;
useYn: string;
sortSeq: number;
regDt: string;
rgTr: string;
rgTrNm: string;
mdfcnDt: string;
mdFr: string;
}
export interface LwpoCodeDetail extends LwpoCodeFormData {
lwpoCd: string;
upCd: string;
lwpoCdNm: string;
lwpoCdExpln: string;
unit: string;
prntLwpoCd: string;
useYn: string;
sortSeq: 0;
}
export const LwpoCodeFormSchema = z.object({
lwpoCd: z.string().max(10).nonempty(),
upCd: z.string().max(10).nonempty(),
lwpoCdNm: z.string().max(100).nonempty(),
lwpoCdExpln: z.string().max(1000).nullish(),
unit: z.string().max(20).nullish(),
prntLwpoCd: z.string().max(10).nullish(),
useYn: z.string(),
sortSeq: z.number(),
delYn: z.string(),
});
export type LwpoCodeFormData = z.infer<typeof LwpoCodeFormSchema>;
export type LwpoCodeList = PageableList<LwpoCode>;
export interface LwpoCodeService {
getLwpoCodeList(params: LwpoCodeParams): Promise<LwpoCodeList>;
getUpCodeLwpoCodeList(upCd: string): Promise<LwpoCodeList>;
lwpoCodeDetail(upCd: string, lwpoCd: string): Promise<LwpoCodeDetail>;
updateLwpoCode(
upCd: string,
lwpoCd: string,
data: LwpoCodeFormData,
): Promise<void>;
createLwpoCode(data: LwpoCodeFormData): Promise<void>;
deleteLwpoCode(
upCd: string,
lwpoCd: string,
data: LwpoCodeFormData,
): Promise<void>;
}
@@ -0,0 +1,52 @@
import { z } from "zod";
import type { PageableParams, PageableList } from "@/shared/model";
export interface UpCodeParams extends PageableParams {
keyword?: string;
upCd?: string;
cdSe?: string;
useYn?: string;
siteId?: string;
}
export interface UpCode {
upCd: string;
upCdNm: string;
upCdExpln: string;
cdSe: string;
siteId: string;
useYn: string;
regDt: string;
}
export interface UpCodeDetail extends UpCodeFormData {
upCd: string;
upCdNm: string;
upCdExpln: string;
cdSe: string;
siteId: string;
useYn: string;
delYn: string;
regDt: string;
rgTrNm: string;
}
export const upCodeFormSchema = z.object({
upCd: z.string().max(10).nonempty(),
upCdNm: z.string().max(100).nonempty(),
upCdExpln: z.string().max(1000).nullish(),
cdSe: z.string().max(10).nonempty(),
siteId: z.string().max(2).nullish(),
useYn: z.string(),
delYn: z.string(),
});
export type UpCodeFormData = z.infer<typeof upCodeFormSchema>;
export type UpCodeList = PageableList<UpCode>;
export interface UpCodeService {
getUpCodeList(params: UpCodeParams): Promise<UpCodeList>;
upCodeDetail(upCodeId: string): Promise<UpCodeDetail>;
updateUpCode(upCodeId: string, data: UpCodeFormData): Promise<void>;
createUpCode(data: UpCodeFormData): Promise<void>;
}
+12 -3
View File
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import {
cn,
Select,
SelectContent,
SelectItem,
@@ -11,9 +12,17 @@ import { CodeApi } from "../api/code-service";
type CodeSelectProps = {
upCd: string;
placeholder?: string;
className?: string;
} & React.ComponentPropsWithoutRef<typeof Select>;
function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
function CodeSelect({
upCd,
defaultValue,
placeholder = "선택",
className,
...props
}: CodeSelectProps) {
const { data: code } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
queryFn: () => CodeApi.getCode(upCd),
@@ -21,11 +30,11 @@ function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
});
const defaultValueText =
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? "선택";
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? placeholder;
return (
<Select {...props}>
<SelectTrigger className="w-[12rem]">
<SelectTrigger className={cn("w-[12rem]", className)}>
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
@@ -0,0 +1,60 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
commentFormSchema,
commentUpdateFormSchema,
type Comment,
type CommentService,
type CommentList,
type CommentParams,
type CommentFormData,
type CommentUpdateFormData,
} from "../model/comment-interface";
export class CommentApiService implements CommentService {
public async getComments(params: CommentParams): Promise<CommentList> {
const response = await client.get<CommentList>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
{ params },
);
return response.data;
}
public async createComment(data: CommentFormData): Promise<Comment> {
const validatedData = commentFormSchema.parse(data);
const response = await client.post<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
validatedData,
);
return response.data;
}
public async updateComment(
bbsId: number,
pstNo: number,
cmntNo: number,
data: CommentUpdateFormData,
): Promise<Comment> {
const validatedData = commentUpdateFormSchema.parse(data);
const response = await client.put<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`,
validatedData,
);
return response.data;
}
public async deleteComment(
bbsId: number,
pstNo: number,
cmntNo: number,
cmntPswd: string,
): Promise<Comment> {
const response = await client.delete<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`,
{ params: { cmntPswd } },
);
return response.data;
}
}
export const CommentApi = new CommentApiService();
+10
View File
@@ -0,0 +1,10 @@
export { CommentApi } from "./api/comment-service";
export {
commentFormSchema,
commentUpdateFormSchema,
type Comment,
type CommentList,
type CommentParams,
type CommentFormData,
type CommentUpdateFormData,
} from "./model/comment-interface";
@@ -0,0 +1,55 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface CommentParams extends PageableParams {
keywordType?: string;
keyword?: string;
pstNo: number;
delYn?: string;
}
export interface Comment {
cmntEvl: number;
pstNo: number;
cmntNo: number;
cmntCn: string;
wrtrNm: string;
regDt: string;
rgtr: string;
mdfcnDt: string;
mdfr: string;
delYn: string;
}
export const commentFormSchema = z.object({
pstNo: z.number(),
cmntCn: z.string(),
wrtrNm: z.string().nullish(),
cmntPswd: z.string(),
cmntEvl: z.number().nullish(),
});
export const commentUpdateFormSchema = commentFormSchema.omit({
pstNo: true,
});
export type CommentList = PageableList<Comment>;
export type CommentFormData = z.infer<typeof commentFormSchema>;
export type CommentUpdateFormData = z.infer<typeof commentUpdateFormSchema>;
export interface CommentService {
getComments(params: CommentParams): Promise<CommentList>;
createComment(data: CommentFormData): Promise<Comment>;
updateComment(
bbsId: number,
pstNo: number,
cmntNo: number,
data: CommentUpdateFormData,
): Promise<Comment>;
deleteComment(
bbsId: number,
pstNo: number,
cmntNo: number,
cmntPswd: string,
): Promise<Comment>;
}
@@ -0,0 +1 @@
export { MenuAuthorityApi } from "../api/menu-authority-service";
@@ -0,0 +1 @@
export { AuthorityButton } from "../ui/authority-button";
@@ -5,6 +5,7 @@ import type {
MenuAuthorityParams,
MenuAuthorityPayload,
MenuAuthorityService,
PageMenuAuthority,
} from "../model/menu-authority-interface";
export class MenuAuthorityApiService implements MenuAuthorityService {
@@ -18,6 +19,15 @@ export class MenuAuthorityApiService implements MenuAuthorityService {
return response.data;
}
public async getPageMenuAuthority(
menuCd: number,
): Promise<PageMenuAuthority[]> {
const response = await client.get<PageMenuAuthority[]>(
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`,
);
return response.data;
}
public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
@@ -1 +1,2 @@
export { AuthorityButton } from "./ui/authority-button";
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
@@ -7,6 +7,14 @@ export interface MenuAuthorityParams extends PageableParams {
useYn?: string;
}
export interface PageMenuAuthority {
mngAuthrtYn: string;
inqAuthrtYn: string;
regAuthrtYn: string;
mdfcnAuthrtYn: string;
delAuthrtYn: string;
}
export const menuAuthoritySchema = z.object({
groupId: z.string(),
groupNm: z.string(),
@@ -35,5 +43,6 @@ export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
export interface MenuAuthorityService {
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
}
@@ -0,0 +1,26 @@
import { useMenuStore } from "@/shared/lib";
import { Button, cn, type ButtonProps } from "@common/components";
import type { PageMenuAuthority } from "../model/menu-authority-interface";
type AuthorityButtonProps = {
authority: keyof PageMenuAuthority;
} & ButtonProps;
function AuthorityButton({
authority,
className,
children,
...props
}: AuthorityButtonProps) {
const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority);
const isAuthority = pageMenuAuthority?.[authority] === "Y";
return (
<Button className={cn(className, !isAuthority && "hidden")} {...props}>
{children}
</Button>
);
}
export { AuthorityButton };
@@ -1,6 +1,6 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
@@ -15,7 +15,7 @@ import {
DialogTrigger,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import {
menuAuthorityFormSchema,
type MenuAuthorityFormData,
@@ -33,7 +33,7 @@ function ManagementAuthorityDialog({
...props
}: ManagementAuthorityDialogProps) {
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { data: menuAuthorityList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
@@ -41,7 +41,7 @@ function ManagementAuthorityDialog({
enabled: !!menuCd,
});
const { mutateAsync: saveMenuAuthority } = useMutation({
const { mutateAsync: saveMenuAuthority } = useMutationWrapper({
mutationFn: (data: MenuAuthorityPayload[]) =>
MenuAuthorityApi.saveMenuAuthority(data),
onSuccess: () => {
@@ -82,7 +82,9 @@ function ManagementAuthorityDialog({
<DialogHeader>
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
</DialogHeader>
<DataTable table={table} />
<div className="max-h-[40rem] overflow-auto">
<DataTable table={table} />
</div>
<DialogFooter className="!justify-center">
<Button size="lg" onClick={handleSave}>
저장
+38 -9
View File
@@ -1,28 +1,57 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu";
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
import { SITE_ID } from "@/shared/config";
import { useMenuStore } from "@/shared/lib";
import { MenuApi } from "../api/menu-service";
import { getCurrentMenu } from "./utils";
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const pathname = usePathname();
const { setCurrentMenuInfo } = useMenuStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } =
useMenuStore((state) => state);
const getCurrentMenuInfo = async () => {
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
if (!menuRoles) return;
return getCurrentMenu(menuRoles, pathname ?? "");
};
const getPageMenuAuthority = async () => {
const menuCd = getCookie("menu-code-key");
if (!menuCd) return;
const response = await MenuAuthorityApi.getPageMenuAuthority(
parseInt(menuCd),
);
return response;
};
const checkInquiryAuthority = () => {
return pageMenuAuthority?.inqAuthrtYn === "Y";
};
useEffect(() => {
(async () => {
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
if (!menuRoles) return;
const currentMenu = await getCurrentMenuInfo();
if (currentMenu) setCurrentMenuInfo(currentMenu);
const currentMenu = getCurrentMenu(menuRoles, pathname ?? "");
if (!currentMenu) return;
setCurrentMenuInfo(currentMenu);
const response = await getPageMenuAuthority();
if (response?.length) setPageMenuAuthority(response[0]!);
})();
}, []);
useEffect(() => {
if (!pageMenuAuthority) return;
const inquiryAuthority = checkInquiryAuthority();
if (!inquiryAuthority)
alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/"));
}, [pageMenuAuthority]);
return <>{children}</>;
};
@@ -0,0 +1,97 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
postFormSchema,
postUpdateFormSchema,
type Post,
type MovePost,
type DeletePost,
type PostInfo,
type PostList,
type RecentPostList,
type PostParams,
type RecentPostParams,
type PostFormData,
type PostUpdateFormData,
type PostService,
} from "../model/post-interface";
export class PostApiService implements PostService {
public async getPostList(
bbsId: number,
params: PostParams,
): Promise<PostList> {
const response = await client.get<PostList>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}`,
{ params },
);
return response.data;
}
public async getRecentPostList(
params?: RecentPostParams,
): Promise<RecentPostList> {
const response = await client.get<RecentPostList>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
{ params },
);
return response.data;
}
public async getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo> {
const response = await client.get<PostInfo>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`,
);
return response.data;
}
public async createPost(data: PostFormData): Promise<Post> {
const validatedData = postFormSchema.parse(data);
const response = await client.post<Post>(
`${ENDPOINT.BOARD_SERVICE.POSTS}`,
validatedData,
);
return response.data;
}
public async updatePost(
bbsId: number,
pstNo: number,
data: PostUpdateFormData,
): Promise<Post> {
const validatedData = postUpdateFormSchema.parse(data);
const response = await client.put<Post>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`,
validatedData,
);
return response.data;
}
public async deletePost(
bbsId: number,
pstNo: number,
delRsn?: string,
): Promise<void> {
await client.delete(`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, {
params: { delRsn },
});
}
public async softDeletePost(data: DeletePost[]): Promise<void> {
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/softDeleteSels`, data);
}
public async hardDeletePost(data: DeletePost[]): Promise<void> {
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/hardDeleteSels`, data);
}
public async movePost(data: MovePost[]): Promise<void> {
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/movePstByBbsId`, data);
}
public async restorePost(data: DeletePost[]): Promise<void> {
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/restoreSels`, data);
}
}
export const PostApi = new PostApiService();
+16
View File
@@ -0,0 +1,16 @@
export { PostApi } from "./api/post-service";
export {
postFormSchema,
postUpdateFormSchema,
type Post,
type RecentPost,
type MovePost,
type DeletePost,
type PostList,
type PostParams,
type RecentPostParams,
type PostFormData,
type PostUpdateFormData,
type PostService,
} from "./model/post-interface";
export { PostPageGuard } from "./lib/post-page-guard";
@@ -0,0 +1,39 @@
import { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { usePathname, useRouter } from "next/navigation";
import { BoardApi } from "@/entities/board/@x/post";
import { useAlertStore, useMenuStore } from "@/shared/lib";
function PostPageGuard({
bbsId,
children,
}: {
bbsId: string;
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const alert = useAlertStore((state) => state.setMessage);
const setBbsInfo = useMenuStore((state) => state.setBbsInfo);
const { data: boardInfo, isLoading } = useQuery({
queryKey: [bbsId],
queryFn: () => BoardApi.getBoardInfo(bbsId),
});
useEffect(() => {
if (!boardInfo) return;
setBbsInfo(boardInfo);
}, [boardInfo]);
if (!isLoading && pathname && boardInfo) {
if (!pathname.includes(boardInfo.bbsTypeCd.toLowerCase())) {
alert("잘못된 경로로 접속하였습니다.", router.back);
return null;
}
}
return children;
}
export { PostPageGuard };
@@ -0,0 +1,118 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface PostParams extends PageableParams {
keywordType?: string;
keyword?: string;
popupYn?: string;
upndFixYn?: string;
prdPstgYn?: string;
siteId?: string;
bbsTypeCd?: string;
}
export interface RecentPostParams extends PageableParams {
keywordType?: string;
keyword?: string;
bbsId?: number | null;
delYn?: string;
frDateYmd?: string;
toDateYmd?: string;
}
export interface Post {
rowNo: number;
bbsId: number;
pstNo: number;
pstTtl: string;
pstCn: string;
inqNocs: number;
rfrncUrl: string;
popupYn: string;
upndFixYn: string;
prdPstgYn: string;
rootPath: string;
pstPath: string;
ansPstn: number;
wrtrNm: string;
wrtrCplc: string;
atflCd: string;
regDt: string;
rgtrNm: string;
cmntCnt: number;
}
export interface RecentPost extends Post {
bbsNm: string;
bbsTypeCd: string;
}
export interface PostInfo extends Post {
pstCn: string;
rgtr: string;
mdfcnDt: string;
mdfr: string;
}
export interface MovePost {
fromBbsId: number;
toBbsId?: number;
pstNo: number;
}
export interface DeletePost {
bbsId: number;
pstNo: number;
delRsn?: string;
}
const postEtcSchema = z.object({
bbsPstgTypeCd: z.string(),
pstgPrdSeCd: z.string(),
pstgBgngDt: z.string().nullish(),
pstgEndDt: z.string().nullish(),
});
export const postFormSchema = z.object({
bbsId: z.number(),
pstTtl: z.string().nonempty(),
pstCn: z.string().nullish(),
rfrncUrl: z.string().nullish(),
popupYn: z.string().nullish(),
popupYnEtc: z.array(postEtcSchema).nullish(),
upndFixYn: z.string().nullish(),
upndFixYnEtc: z.array(postEtcSchema).nullish(),
prdPstgYn: z.string().nullish(),
prdPstgYnEtc: z.array(postEtcSchema).nullish(),
pstPswd: z.string(),
wrtrNm: z.string().nullish(),
wrtrCplc: z.string().nullish(),
atflCd: z.string().nullish(),
parentPstNo: z.number().nullish(),
});
export const postUpdateFormSchema = postFormSchema.omit({
bbsId: true,
parentPstNo: true,
});
export type PostList = PageableList<Post>;
export type RecentPostList = PageableList<RecentPost>;
export type PostFormData = z.infer<typeof postFormSchema>;
export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
export interface PostService {
getPostList(bbsId: number, params: PostParams): Promise<PostList>;
getRecentPostList(params: RecentPostParams): Promise<RecentPostList>;
getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
createPost(data: PostFormData): Promise<Post>;
updatePost(
bbsId: number,
pstNo: number,
data: PostUpdateFormData,
): Promise<Post>;
deletePost(bbsId: number, pstNo: number, delRsn?: string): Promise<void>;
softDeletePost(data: DeletePost[]): Promise<void>;
hardDeletePost(data: DeletePost[]): Promise<void>;
movePost(data: MovePost[]): Promise<void>;
}
@@ -5,6 +5,7 @@ export const POSTING_TYPE = {
POPUP: "POPUP",
BANNER: "BANNER",
IMAGE: "IMAGE",
SURVEY: "SURVEY",
} as const;
export type PostingType = (typeof POSTING_TYPE)[keyof typeof POSTING_TYPE];
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFieldArray, useForm } from "react-hook-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Dialog,
@@ -26,7 +26,9 @@ import {
cn,
} from "@common/components";
import { CodeSelect } from "@/entities/code/@x/posting";
import { AuthorityButton } from "@/entities/menu-authority/@x/posting";
import { SiteApi, type Site } from "@/entities/site/@x/posting";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import type { PageableList } from "@/shared/model";
import { ENDPOINT } from "@/shared/config";
import { PostingApi } from "../api/posting-service";
@@ -36,7 +38,6 @@ import {
type PostingType,
type PostPayload,
} from "../model/posting-interface";
import { useAlertStore } from "@/shared/lib/use-alert-store";
type DateRange = {
from: string;
@@ -57,11 +58,12 @@ const POSTING_TYPE_LABEL = {
POPUP: "팝업",
BANNER: "배너",
IMAGE: "이미지",
SURVEY: "설문",
} as const;
function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const [rememberPeriodCode, setRememberPeriodCode] = useState<
Record<string, string>
@@ -75,7 +77,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
queryFn: () => SiteApi.getSiteList({}),
});
const { mutateAsync: savePosting } = useMutation({
const { mutateAsync: savePosting } = useMutationWrapper({
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
onSuccess: () => {
alert("저장되었습니다.");
@@ -97,7 +99,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
siteList && selectedRows.length === siteList.content.length;
const postingPeriod = (siteId: string) => {
const periodCode = Number(rememberPeriodCode[siteId]);
const periodCode = parseInt(rememberPeriodCode[siteId] ?? "");
if (periodCode === 99) return "상시";
@@ -175,7 +177,9 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
const handlePeriodCodeChange = (siteId: string) => (value: string) => {
const from = dayjs().format("YYYY-MM-DD 00:00:00");
const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59");
const to = dayjs()
.add(parseInt(value), "day")
.format("YYYY-MM-DD 23:59:59");
setRememberPeriodCode((prev) => ({
...prev,
@@ -216,10 +220,14 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
return (
<Dialog onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button color="emerald" disabled={!postingTargets.length}>
<AuthorityButton
authority="mngAuthrtYn"
color="emerald"
disabled={!postingTargets.length}
>
<i className="diveicon di-layer" />
사이트반영
</Button>
</AuthorityButton>
</DialogTrigger>
<DialogContent className="max-w-[68rem]" aria-describedby={undefined}>
<DialogHeader>
@@ -272,7 +280,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
onValueChange={handlePeriodCodeChange(site.siteId)}
disabled={!selectedRows.includes(i)}
/>
{!!Number(rememberPeriodCode[site.siteId]) && (
{!!parseInt(rememberPeriodCode[site.siteId] ?? "") && (
<span
className={cn(
"text-lg text-label",
@@ -282,9 +290,10 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
게시 기간: <u>{postingPeriod(site.siteId)}</u>
</span>
)}
{Number(rememberPeriodCode[site.siteId]) === 0 && (
{parseInt(rememberPeriodCode[site.siteId] ?? "") ===
0 && (
<DateRangePicker
handleDateChange={(date) =>
onValueChange={(date) =>
handlePeriodDateChange(site.siteId)(
date as DateRange | undefined,
)
@@ -6,7 +6,9 @@ import type {
RoleAuthorizationsList,
RoleAuthorizationsData,
RoleAuthorizations,
} from "../model/role-authorizations-interface";
WithRoleIdRoleAuthorizationsParams,
RoleAuthorizationsByRoleIdList,
} from "../model/role-authorization-interface";
export class RoleAuthorizationsApiService implements RoleAuthorizationsService {
public async getRoleAuthorizationsList(
@@ -19,6 +21,16 @@ export class RoleAuthorizationsApiService implements RoleAuthorizationsService {
return response.data;
}
public async getRoleAuthorizationsListByRoleId(
params: WithRoleIdRoleAuthorizationsParams,
): Promise<RoleAuthorizationsByRoleIdList> {
const response = await client.get<RoleAuthorizationsByRoleIdList>(
`${ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS}`,
{ params },
);
return response.data;
}
public async createRoleAuthorizations(
data: RoleAuthorizationsData,
): Promise<RoleAuthorizations> {
@@ -0,0 +1,11 @@
export { RoleAuthorizationsApi } from "./api/role-authorization-service";
export {
roleAuthorizationsFormSchema,
type RoleAuthorizations,
type RoleAuthorizationsParams,
type RoleAuthorizationsList,
type RoleAuthorizationsFormData,
type RoleAuthorizationsData,
type RoleAuthorizationsByRoleId,
type RoleAuthorizationsByRoleIdList,
} from "./model/role-authorization-interface";
@@ -7,6 +7,12 @@ export interface RoleAuthorizationsParams extends PageableParams {
authorizationNo?: string;
}
export interface WithRoleIdRoleAuthorizationsParams extends PageableParams {
keywordType?: string;
keyword?: string;
roleId?: string;
}
export interface RoleAuthorizations {
roleId: string;
roleName: string;
@@ -16,6 +22,16 @@ export interface RoleAuthorizations {
authYn: string;
}
export interface RoleAuthorizationsByRoleId {
roleId: string;
authorizationNo: number;
authorizationName: string;
urlPatternValue: string;
httpMethodCode: string;
sortSeq: number;
createdAt: boolean;
}
export const roleAuthorizationsSchema = z.object({
roleId: z.string(),
authorizationNo: z.number(),
@@ -27,6 +43,8 @@ export const roleAuthorizationsFormSchema = z.object({
});
export type RoleAuthorizationsList = PageableList<RoleAuthorizations>;
export type RoleAuthorizationsByRoleIdList =
PageableList<RoleAuthorizationsByRoleId>;
export type RoleAuthorizationsData = z.infer<typeof roleAuthorizationsSchema>[];
export type RoleAuthorizationsFormData = z.infer<
typeof roleAuthorizationsFormSchema
@@ -36,6 +54,9 @@ export interface RoleAuthorizationsService {
getRoleAuthorizationsList: (
params: RoleAuthorizationsParams,
) => Promise<RoleAuthorizationsList>;
getRoleAuthorizationsListByRoleId: (
params: WithRoleIdRoleAuthorizationsParams,
) => Promise<RoleAuthorizationsByRoleIdList>;
createRoleAuthorizations: (
data: RoleAuthorizationsData,
) => Promise<RoleAuthorizations>;
+1
View File
@@ -5,3 +5,4 @@ export {
type Site,
type SiteFormData,
} from "./model/site-interface";
export { SiteSelect } from "./ui/site-select";
@@ -0,0 +1,53 @@
import { useQuery } from "@tanstack/react-query";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { SiteApi } from "../api/site-service";
type SiteSelectProps = {
placeholder?: string;
enabledAllSite?: boolean;
} & React.ComponentPropsWithoutRef<typeof Select>;
function SiteSelect({
defaultValue,
placeholder = "선택",
enabledAllSite,
...props
}: SiteSelectProps) {
const { data: sites } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
queryFn: () => SiteApi.getSiteList(),
select: (data) => {
if (enabledAllSite) {
return [{ siteId: "ALL", siteNm: "전체" }, ...data.content];
}
return data.content;
},
});
const defaultValueText =
sites?.find((item) => item.siteId === defaultValue)?.siteNm ?? placeholder;
return (
<Select {...props}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
{sites?.map((item) => (
<SelectItem key={item.siteId} value={item.siteId}>
{item.siteNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { SiteSelect };
+1
View File
@@ -0,0 +1 @@
export { handleTusUpload } from "./lib/utils";
+60
View File
@@ -0,0 +1,60 @@
import { Upload, type HttpResponse } from "tus-js-client";
import { getCookie, setCookie } from "@/shared/lib";
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
type UploadOptions = {
handleUploadStart?: (upload: Upload) => void;
handleProgress?: (percentage: string) => void;
handleSuccess?: (upload: Upload) => void;
handleResponse?: (res: HttpResponse, upload: Upload) => void;
};
export const handleTusUpload = (file: File, options?: UploadOptions) => {
let metadata = {};
if (!file) return;
metadata = {
filename: file.name,
filetype: file.type,
};
const atflCd = getCookie("atfl-cd-key");
if (atflCd) {
metadata = {
...metadata,
atflCd,
};
}
const upload = new Upload(file, {
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
chunkSize: 10 * 1024 * 1024,
headers: {
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
},
metadata,
onError(error) {
console.error(`Failed because: ${error}`);
},
onProgress(bytesUploaded, bytesTotal) {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
options?.handleProgress?.(percentage);
},
onAfterResponse(req, res) {
const fileInfo = res.getHeader("X-File-Info");
if (fileInfo === undefined || fileInfo === null) return;
setCookie("atfl-cd-key", JSON.parse(fileInfo)?.atflCd ?? "");
options?.handleResponse?.(res, upload);
},
onSuccess() {
options?.handleSuccess?.(upload);
},
});
upload.start();
options?.handleUploadStart?.(upload);
console.log("upload start >>>>>", upload);
};
@@ -0,0 +1,213 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
UserService,
UserSearchParams,
UserList,
UserDetailResponse,
UserSttsStatsList,
UserUpdateData,
UserPasswordUpdateData,
UserFindPasswordUpdateData,
UserPasswordMatchData,
UserUpdateInfoData,
UserSaveData,
UserFindPasswordSaveData,
UserVerifyData,
UserEmailData,
UserIdData,
} from "../model/user-interface";
export class UserApiService implements UserService {
// 사용자 목록 조회
public async findPageUser(params: UserSearchParams): Promise<UserList> {
const response = await client.get<UserList>(
`${ENDPOINT.USER_SERVICE.USERS}`,
{ params },
);
return response.data;
}
// 사용자 단건 조회
public async findByUserUnqId(userUnqId: string): Promise<UserDetailResponse> {
const response = await client.get<UserDetailResponse>(
`${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`,
);
return response.data;
}
// 로그인한 사용자 정보 조회
public async findByUserUnqIdByLogin(): Promise<UserDetailResponse> {
const response = await client.get<UserDetailResponse>(
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
);
return response.data;
}
// 사용자 회원상태 통계 조회
public async selectUserSttsStats(): Promise<UserSttsStatsList> {
const response = await client.get<UserSttsStatsList>(
`${ENDPOINT.USER_SERVICE.USERS}/stts/stats`,
);
return response.data;
}
// 사용자 저장
public async saveUser(data: UserSaveData): Promise<string> {
const response = await client.post<string>(
`${ENDPOINT.USER_SERVICE.USERS}`,
data,
);
return response.data;
}
// 사용자 수정
public async updateUser(
userUnqId: string,
data: UserUpdateData,
): Promise<string> {
const response = await client.put<string>(
`${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`,
data,
);
return response.data;
}
// 사용자 삭제
public async deleteUser(userUnqId: string): Promise<boolean> {
const response = await client.delete<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/delete/${userUnqId}`,
);
return response.data;
}
// 사용자 계정잠금 처리
public async lockUser(userUnqId: string): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/lock/${userUnqId}`,
);
return response.data;
}
// 사용자 계정잠금 해제처리
public async unlockUser(userUnqId: string): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/unlock/${userUnqId}`,
);
return response.data;
}
// 리프레시 토큰 정보 재발급
public async refreshToken(): Promise<void> {
await client.put(`${ENDPOINT.USER_SERVICE.USERS}/token/refresh`);
}
// 사용자 비밀번호 변경
public async updatePassword(data: UserPasswordUpdateData): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/update`,
data,
);
return response.data;
}
// 사용자 비밀번호 초기화(관리자용)
public async initializePassword(
userUnqId: string,
data: UserPasswordUpdateData,
): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/initialize/${userUnqId}`,
data,
);
return response.data;
}
// 사용자 비밀번호 찾기 변경
public async changePassword(
data: UserFindPasswordUpdateData,
): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/change`,
data,
);
return response.data;
}
// 사용자 비밀번호 확인
public async matchPassword(data: UserPasswordMatchData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/match`,
data,
);
return response.data;
}
// 사용자 비밀번호 찾기
public async findPasswordUser(
data: UserFindPasswordSaveData,
): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/find`,
data,
);
return response.data;
}
// 사용자 비밀번호 찾기 유효성 확인
public async validPassword(token: string): Promise<boolean> {
const response = await client.get<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/valid/${token}`,
);
return response.data;
}
// 사용자 회원정보 변경
public async updateInfo(
userUnqId: string,
data: UserUpdateInfoData,
): Promise<string> {
const response = await client.put<string>(
`${ENDPOINT.USER_SERVICE.USERS}/info/${userUnqId}`,
data,
);
return response.data;
}
// 사용자 승인처리
public async applyUser(userUnqId: string): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/apply/${userUnqId}`,
);
return response.data;
}
// 사용자 회원탈퇴
public async leaveUser(data: UserVerifyData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/leave`,
data,
);
return response.data;
}
// 사용자 이메일 중복 체크
public async existsEmail(data: UserEmailData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/exists`,
data,
);
return response.data;
}
// 사용자 아이디 중복 체크
public async existsUserId(data: UserIdData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/existsUserId`,
data,
);
return response.data;
}
}
export const UserApi = new UserApiService();
+5
View File
@@ -0,0 +1,5 @@
export { UserApi } from "./api/user-service";
export {
type UserSearchParams,
type UserListResponse,
} from "./model/user-interface";
@@ -0,0 +1,308 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
// 사용자 검색 파라미터
export interface UserSearchParams extends PageableParams {
keywordType?: string;
keyword?: string;
userStateCode?: string;
aprvYn?: string;
grpId?: string;
frDateYmd?: string;
toDateYmd?: string;
}
// 사용자 목록 응답 인터페이스
export interface UserListResponse {
userId: string;
userName: string;
email: string;
roleId: string;
roleName: string;
userStateCode: string;
userStateCodeName: string;
lastLoginDate: string;
loginFailCount: number;
userUnqId: string;
mphn: string;
telno: string;
aprvYn: string;
regDt: string;
laeSeCd: string;
laeSeNm: string;
ogdpSeCd: string;
ogdpSeNm: string;
instSeCd: string;
instSeNm: string;
aprvYmd: string;
}
// 사용자 상세 응답 인터페이스
export interface UserDetailResponse {
regDt: string;
mdfcnDt: string;
rgtr: string;
mdfr: string;
userUnqId: string;
userId: string;
userName: string;
email: string;
encryptedPassword: string;
refreshToken: string;
userStateCode: string;
lastLoginDate: string;
loginFailCount: number;
pswdHnt: string;
pswdAns: string;
mphn: string;
telno: string;
zip: string;
addr: string;
daddr: string;
laeSeCd: string;
ogdpSeCd: string;
aprvYn: string;
autzrId: string;
expryYmd: string;
lastPswdChgDt: string;
googleId: string;
kakaoId: string;
naverId: string;
isSocialUser: boolean;
hasPassword: boolean;
twoFactorEnabled: boolean;
adminRoleYn: string;
instSeCd: string;
aprvYmd: string;
enoCd: string;
laeSeNm: string;
ogdpSeNm: string;
instSeNm: string;
userStateNm: string;
pswdHntNm: string;
ecoNm: string;
}
// 사용자 상태 통계 응답 인터페이스
export interface UserSttsStatsResponse {
sttsCd: string;
sttsNm: string;
cnt: number;
}
// 사용자 수정 요청 스키마
export const userUpdateSchema = z.object({
userId: z.string().optional(),
userName: z.string(),
email: z.string().email().optional(),
password: z
.string()
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
.optional(),
roleId: z.string().optional(),
userStateCode: z.string(),
pswdHnt: z.string().max(10),
pswdAns: z.string().max(100).optional(),
mphn: z.string().max(11).optional(),
telno: z.string().max(11).optional(),
zip: z.string().max(5).optional(),
addr: z.string().max(200).optional(),
daddr: z.string().max(200).optional(),
laeSeCd: z.string().max(10),
ogdpSeCd: z.string().max(10),
aprvYn: z.string(),
autzrId: z.string().uuid().optional(),
expryYmd: z.string().max(8),
instSeCd: z.string().optional(),
aprvYmd: z.string().max(8).optional(),
enoCd: z.string().optional(),
});
// 사용자 비밀번호 변경 스키마
export const userPasswordUpdateSchema = z.object({
password: z
.string()
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
.optional()
.describe("기존 패스워드"),
provider: z.string().optional().describe("공급자(기본값:password)"),
token: z.string().optional(),
newPassword: z
.string()
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
.describe("신규 패스워드"),
newPasswordConfirm: z
.string()
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
.describe("신규 패스워드 확인"),
});
// 사용자 비밀번호 찾기 변경 스키마
export const userFindPasswordUpdateSchema = z.object({
tokenValue: z.string(),
password: z
.string()
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/),
});
// 사용자 비밀번호 확인 스키마
export const userPasswordMatchSchema = z.object({
password: z
.string()
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/),
});
// 사용자 회원정보 변경 스키마
export const userUpdateInfoSchema = z.object({
password: z
.string()
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
.optional()
.describe("기존 패스워드"),
provider: z.string().optional().describe("공급자(기본값:password)"),
token: z.string().optional(),
userName: z.string(),
email: z.string().email().optional(),
});
// 사용자 저장 스키마
export const userSaveSchema = z.object({
userId: z.string().describe("사용자 ID"),
userName: z.string().describe("사용자명"),
email: z.string().email().optional().describe("이메일"),
password: z
.string()
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
.describe("패스워드"),
pswdHnt: z.string().max(10).describe("비밀번호 힌트"),
pswdAns: z.string().max(100).optional().describe("비밀번호 답변"),
mphn: z.string().max(11).optional().describe("휴대폰 번호"),
telno: z.string().max(11).optional().describe("전화번호"),
zip: z.string().max(5).optional().describe("우편번호"),
addr: z.string().max(200).optional().describe("주소"),
daddr: z.string().max(200).optional().describe("상세 주소"),
laeSeCd: z.string().max(10).describe("법적 구분 코드"),
ogdpSeCd: z.string().max(10).describe("조직 구분 코드"),
expryYmd: z.string().max(8).describe("만료 일자(YYYYMMDD)"),
instSeCd: z.string().optional().describe("기관구분코드"),
enoCd: z.string().optional().describe("환경청코드"),
});
// 사용자 비밀번호 찾기 저장 스키마
export const userFindPasswordSaveSchema = z.object({
userName: z.string(),
emailAddr: z.string().email(),
mainUrl: z.string(),
changePasswordUrl: z.string(),
});
// 사용자 인증 요청 스키마
export const userVerifySchema = z.object({
password: z
.string()
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
.optional()
.describe("기존 패스워드"),
provider: z.string().optional().describe("공급자(기본값:password)"),
token: z.string().optional(),
});
// 사용자 이메일 요청 스키마
export const userEmailSchema = z.object({
userId: z.string().optional(),
email: z.string().email(),
});
// 사용자 ID 요청 스키마
export const userIdSchema = z.object({
userId: z.string(),
});
// Zod 스키마 타입 정의
export type UserUpdateData = z.infer<typeof userUpdateSchema>;
export type UserPasswordUpdateData = z.infer<typeof userPasswordUpdateSchema>;
export type UserFindPasswordUpdateData = z.infer<
typeof userFindPasswordUpdateSchema
>;
export type UserPasswordMatchData = z.infer<typeof userPasswordMatchSchema>;
export type UserUpdateInfoData = z.infer<typeof userUpdateInfoSchema>;
export type UserSaveData = z.infer<typeof userSaveSchema>;
export type UserFindPasswordSaveData = z.infer<
typeof userFindPasswordSaveSchema
>;
export type UserVerifyData = z.infer<typeof userVerifySchema>;
export type UserEmailData = z.infer<typeof userEmailSchema>;
export type UserIdData = z.infer<typeof userIdSchema>;
// 페이지네이션 타입 정의
export type UserList = PageableList<UserListResponse>;
export type UserSttsStatsList = UserSttsStatsResponse[];
// 사용자 서비스 인터페이스
export interface UserService {
// 사용자 목록 조회
findPageUser: (params: UserSearchParams) => Promise<UserList>;
// 사용자 단건 조회
findByUserUnqId: (userUnqId: string) => Promise<UserDetailResponse>;
// 로그인한 사용자 정보 조회
findByUserUnqIdByLogin: () => Promise<UserDetailResponse>;
// 사용자 회원상태 통계 조회
selectUserSttsStats: () => Promise<UserSttsStatsList>;
// 사용자 저장
saveUser: (data: UserSaveData) => Promise<string>;
// 사용자 수정
updateUser: (userUnqId: string, data: UserUpdateData) => Promise<string>;
// 사용자 삭제
deleteUser: (userUnqId: string) => Promise<boolean>;
// 사용자 계정잠금 처리
lockUser: (userUnqId: string) => Promise<boolean>;
// 사용자 계정잠금 해제처리
unlockUser: (userUnqId: string) => Promise<boolean>;
// 리프레시 토큰 정보 재발급
refreshToken: () => Promise<void>;
// 사용자 비밀번호 변경
updatePassword: (data: UserPasswordUpdateData) => Promise<boolean>;
// 사용자 비밀번호 초기화(관리자용)
initializePassword: (
userUnqId: string,
data: UserPasswordUpdateData,
) => Promise<boolean>;
// 사용자 비밀번호 찾기 변경
changePassword: (data: UserFindPasswordUpdateData) => Promise<boolean>;
// 사용자 비밀번호 확인
matchPassword: (data: UserPasswordMatchData) => Promise<boolean>;
// 사용자 비밀번호 찾기
findPasswordUser: (data: UserFindPasswordSaveData) => Promise<boolean>;
// 사용자 비밀번호 찾기 유효성 확인
validPassword: (token: string) => Promise<boolean>;
// 사용자 회원정보 변경
updateInfo: (userUnqId: string, data: UserUpdateInfoData) => Promise<string>;
// 사용자 승인처리
applyUser: (userUnqId: string) => Promise<boolean>;
// 사용자 회원탈퇴
leaveUser: (data: UserVerifyData) => Promise<boolean>;
// 사용자 이메일 중복 체크
existsEmail: (data: UserEmailData) => Promise<boolean>;
// 사용자 아이디 중복 체크
existsUserId: (data: UserIdData) => Promise<boolean>;
}
+3
View File
@@ -0,0 +1,3 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { FaqPage } from "./ui/faq-page";
@@ -0,0 +1,42 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { PostForm } from "@/widgets/post";
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function CreatePage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createPost } = useMutationWrapper({
mutationFn: (data: PostFormData) => PostApi.createPost(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
},
});
const form = useForm<PostFormData>({
resolver: zodResolver(postFormSchema),
values: {
bbsId: parseInt(bbsId),
pstTtl: "",
wrtrNm: userInfo?.userName ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">FAQ 게시글 등록</h3>
</div>
<PostForm form={form} handleSave={createPost} />
</div>
);
}
export { CreatePage };
@@ -0,0 +1,323 @@
"use client";
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Skeleton,
} from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi, type PostParams } from "@/entities/post";
import { AuthorityButton } from "@/entities/menu-authority";
import { ENDPOINT } from "@/shared/config";
import {
useAlertStore,
useMenuStore,
usePrompt,
useMutationWrapper,
} from "@/shared/lib";
function FaqPage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const pathname = usePathname();
const { prompt } = usePrompt();
const alert = useAlertStore((state) => state.setMessage);
const bbsInfo = useMenuStore((state) => state.bbsInfo);
const [postParams, setPostParams] = useState<PostParams>({
keywordType: "title",
keyword: "",
});
const [selectedPostNumbers, setSelectedPostNumbers] = useState<number[]>([]);
const {
data: postList,
isFetched,
refetch,
} = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsInfo?.bbsTypeCd],
queryFn: () =>
PostApi.getPostList(parseInt(bbsId), {
...postParams,
bbsTypeCd: bbsInfo?.bbsTypeCd,
}),
enabled: !!bbsInfo?.bbsTypeCd,
});
const { mutate: deletePost } = useMutationWrapper({
mutationFn: ({ pstNo, delRsn }: { pstNo: number; delRsn?: string }) =>
PostApi.deletePost(parseInt(bbsId), pstNo, delRsn),
onSuccess: () => {
alert("삭제되었습니다.");
refetch();
},
});
const getPromptValue = async (message: string) => {
const { resolved, value } = await prompt(message);
if (!resolved) return "";
return value;
};
const handleDeletePost = async (pstNo: number) => {
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
deletePost({ pstNo, delRsn });
};
const onSelectedPostDelete = async () => {
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
await Promise.all(
selectedPostNumbers.map((pstNo) =>
PostApi.deletePost(parseInt(bbsId), pstNo, delRsn),
),
);
refetch();
setSelectedPostNumbers([]);
alert("삭제되었습니다.");
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
refetch();
};
const handleCheckboxChange = (pstNo: number, checked: boolean | string) => {
setSelectedPostNumbers(
checked === true
? [...selectedPostNumbers, pstNo]
: selectedPostNumbers.filter((num) => num !== pstNo),
);
};
const handleAllCheckboxChange = (checked: boolean | string) => {
setSelectedPostNumbers(
checked === true ? postList?.content.map((item) => item.pstNo) || [] : [],
);
};
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 className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<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">
<AuthorityButton
authority="regAuthrtYn"
onClick={() => router.push(`${pathname}/register`)}
>
등록
</AuthorityButton>
<AuthorityButton
authority="delAuthrtYn"
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</AuthorityButton>
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
<Checkbox
id="all-select"
checked={selectedPostNumbers.length === postList?.content.length}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</Label>
</div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div>
</div>
</div>
);
}
function Attachment({ atflCd }: { atflCd: string }) {
const [filename, setFilename] = useState("");
const { data: downloadResponse, isFetching } = useQuery({
queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, atflCd],
queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(atflCd),
enabled: !!atflCd,
});
const handleDownload = async () => {
if (!downloadResponse) return;
try {
const blob = new Blob([downloadResponse.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename || "download";
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
} catch (error) {
console.error(error);
}
};
useEffect(() => {
if (!downloadResponse) return;
try {
let filename = "";
const contentDisposition =
downloadResponse.headers["content-disposition"];
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename\*=UTF-8''([^;]+)/,
);
if (filenameMatch && filenameMatch[1]) {
filename = decodeURIComponent(filenameMatch[1]);
}
}
setFilename(filename);
} catch {
setFilename("");
}
}, [downloadResponse]);
return isFetching ? (
<div className="my-8">
<Skeleton className="h-[2.5rem] w-[32rem]" />
</div>
) : (
filename && (
<div className="my-8 flex items-center gap-4">
<span className="font-bold text-label">첨부파일</span>
<Button
size="sm"
color="white"
className="flex w-fit items-center gap-2"
onClick={handleDownload}
>
<i className="diveicon di-download" />
<span>{filename}</span>
</Button>
</div>
)
);
}
export { FaqPage };
@@ -0,0 +1,53 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { PostForm } from "@/widgets/post";
import {
PostApi,
postUpdateFormSchema,
type PostUpdateFormData,
} from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const userInfo = useUserStore((state) => state.userInfo);
const { data: postInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
});
const { mutateAsync: updatePost } = useMutationWrapper({
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
onSuccess: () => {
alert("수정되었습니다.", router.back);
},
});
const form = useForm<PostUpdateFormData>({
resolver: zodResolver(postUpdateFormSchema),
values: {
...postInfo,
pstTtl: postInfo?.pstTtl ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">FAQ 게시글 수정</h3>
</div>
<PostForm form={form} handleSave={updatePost} />
</div>
);
}
export { ModifyPage };
+4
View File
@@ -0,0 +1,4 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { ImagePage } from "./ui/image-page";
export { ImagePost } from "./ui/image-post";
@@ -0,0 +1,42 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { PostForm } from "@/widgets/post";
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function CreatePage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createPost } = useMutationWrapper({
mutationFn: (data: PostFormData) => PostApi.createPost(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
},
});
const form = useForm<PostFormData>({
resolver: zodResolver(postFormSchema),
values: {
bbsId: parseInt(bbsId),
pstTtl: "",
wrtrNm: userInfo?.userName ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">이미지 게시글 등록</h3>
</div>
<PostForm form={form} handleSave={createPost} />
</div>
);
}
export { CreatePage };
@@ -0,0 +1,3 @@
"use client";
export { PostPage as ImagePage } from "@/widgets/post";
@@ -0,0 +1,3 @@
"use client";
export { Post as ImagePost } from "@/widgets/post";
@@ -0,0 +1,53 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { PostForm } from "@/widgets/post";
import {
PostApi,
postUpdateFormSchema,
type PostUpdateFormData,
} from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const userInfo = useUserStore((state) => state.userInfo);
const { data: postInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
});
const { mutateAsync: updatePost } = useMutationWrapper({
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
onSuccess: () => {
alert("수정되었습니다.", router.back);
},
});
const form = useForm<PostUpdateFormData>({
resolver: zodResolver(postUpdateFormSchema),
values: {
...postInfo,
pstTtl: postInfo?.pstTtl ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">이미지 게시글 수정</h3>
</div>
<PostForm form={form} handleSave={updatePost} />
</div>
);
}
export { ModifyPage };
+4
View File
@@ -0,0 +1,4 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { MoviePage } from "./ui/movie-page";
export { MoviePost } from "./ui/movie-post";
@@ -0,0 +1,42 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { PostForm } from "@/widgets/post";
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function CreatePage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createPost } = useMutationWrapper({
mutationFn: (data: PostFormData) => PostApi.createPost(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
},
});
const form = useForm<PostFormData>({
resolver: zodResolver(postFormSchema),
values: {
bbsId: parseInt(bbsId),
pstTtl: "",
wrtrNm: userInfo?.userName ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">동영상 게시글 등록</h3>
</div>
<PostForm form={form} handleSave={createPost} />
</div>
);
}
export { CreatePage };
@@ -0,0 +1,53 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { PostForm } from "@/widgets/post";
import {
PostApi,
postUpdateFormSchema,
type PostUpdateFormData,
} from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const userInfo = useUserStore((state) => state.userInfo);
const { data: postInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
});
const { mutateAsync: updatePost } = useMutationWrapper({
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
onSuccess: () => {
alert("수정되었습니다.", router.back);
},
});
const form = useForm<PostUpdateFormData>({
resolver: zodResolver(postUpdateFormSchema),
values: {
...postInfo,
pstTtl: postInfo?.pstTtl ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">동영상 게시글 수정</h3>
</div>
<PostForm form={form} handleSave={updatePost} />
</div>
);
}
export { ModifyPage };
@@ -0,0 +1,3 @@
"use client";
export { PostPage as MoviePage } from "@/widgets/post";
@@ -0,0 +1,3 @@
"use client";
export { Post as MoviePost } from "@/widgets/post";
+4
View File
@@ -0,0 +1,4 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { NormalPage } from "./ui/normal-page";
export { NormalPost } from "./ui/normal-post";
@@ -0,0 +1,50 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { PostForm } from "@/widgets/post";
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function CreatePage({ bbsId, pstNo }: { bbsId: string; pstNo?: string }) {
const answerRegisterMode = !!pstNo;
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createPost } = useMutationWrapper({
mutationFn: (data: PostFormData) => PostApi.createPost(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
},
});
const form = useForm<PostFormData>({
resolver: zodResolver(postFormSchema),
values: {
bbsId: parseInt(bbsId),
pstTtl: "",
wrtrNm: userInfo?.userName ?? "",
pstPswd: userInfo?.userUnqId ?? "",
popupYn: "N",
upndFixYn: "N",
prdPstgYn: "N",
parentPstNo: pstNo ? parseInt(pstNo) : null,
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
{answerRegisterMode ? "답글 등록" : "게시글 등록"}
</h3>
</div>
<PostForm form={form} handleSave={createPost} />
</div>
);
}
export { CreatePage };
@@ -0,0 +1,53 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { PostForm } from "@/widgets/post";
import {
PostApi,
postUpdateFormSchema,
type PostUpdateFormData,
} from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const userInfo = useUserStore((state) => state.userInfo);
const { data: postInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
});
const { mutateAsync: updatePost } = useMutationWrapper({
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
onSuccess: () => {
alert("수정되었습니다.", router.back);
},
});
const form = useForm<PostUpdateFormData>({
resolver: zodResolver(postUpdateFormSchema),
values: {
...postInfo,
pstTtl: postInfo?.pstTtl ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">게시글 수정</h3>
</div>
<PostForm form={form} handleSave={updatePost} />
</div>
);
}
export { ModifyPage };
@@ -0,0 +1,3 @@
"use client";
export { PostPage as NormalPage } from "@/widgets/post";
@@ -0,0 +1,3 @@
"use client";
export { Post as NormalPost } from "@/widgets/post";
+4
View File
@@ -0,0 +1,4 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { NoticePage } from "./ui/notice-page";
export { NoticePost } from "./ui/notice-post";
@@ -0,0 +1,90 @@
import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge, Button } from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import type { Post } from "@/entities/post";
export const columns: ColumnDef<Post>[] = [
{
accessorKey: "rowNo",
header: "NO",
},
{
accessorKey: "pstTtl",
header: "제목",
cell: ({ row }) => {
const { pstTtl, inqNocs } = row.original;
return (
<div className="flex min-w-[20vw] items-center gap-2 text-left">
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
<Badge color="blue">NEW</Badge>
)}
{pstTtl}
</div>
);
},
},
{
accessorKey: "inqNocs",
header: "조회수",
},
{
accessorKey: "atflCd",
header: "첨부파일",
cell: ({ row }) => {
const atflCd = row.original.atflCd;
if (!atflCd) return null;
return <AttachmentButton atflCd={atflCd} />;
},
},
{
accessorKey: "rgtrNm",
header: "작성자",
},
{
accessorKey: "regDt",
header: "등록일",
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
},
];
const AttachmentButton = ({ atflCd }: { atflCd: string }) => {
const handleDownload = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
try {
let filename = "";
const response = await AttachmentApi.downloadAttachmentByAtflCd(atflCd);
const contentDisposition = response.headers["content-disposition"];
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename\*=UTF-8''([^;]+)/,
);
if (filenameMatch) {
filename = decodeURIComponent(filenameMatch[1]);
}
}
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
} catch (error) {
console.error(error);
}
};
return (
<Button size="icon" color="white" onClick={handleDownload}>
<i className="diveicon di-download" />
</Button>
);
};
@@ -0,0 +1,45 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { PostForm } from "@/widgets/post";
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function CreatePage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createPost } = useMutationWrapper({
mutationFn: (data: PostFormData) => PostApi.createPost(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
},
});
const form = useForm<PostFormData>({
resolver: zodResolver(postFormSchema),
values: {
bbsId: parseInt(bbsId),
pstTtl: "",
wrtrNm: userInfo?.userName ?? "",
pstPswd: userInfo?.userUnqId ?? "",
popupYn: "N",
upndFixYn: "N",
prdPstgYn: "N",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">공지 게시글 등록</h3>
</div>
<PostForm form={form} handleSave={createPost} />
</div>
);
}
export { CreatePage };
@@ -0,0 +1,53 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { PostForm } from "@/widgets/post";
import {
PostApi,
postUpdateFormSchema,
type PostUpdateFormData,
} from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const userInfo = useUserStore((state) => state.userInfo);
const { data: postInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
});
const { mutateAsync: updatePost } = useMutationWrapper({
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
onSuccess: () => {
alert("수정되었습니다.", router.back);
},
});
const form = useForm<PostUpdateFormData>({
resolver: zodResolver(postUpdateFormSchema),
values: {
...postInfo,
pstTtl: postInfo?.pstTtl ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">공지 게시글 수정</h3>
</div>
<PostForm form={form} handleSave={updatePost} />
</div>
);
}
export { ModifyPage };
@@ -0,0 +1,3 @@
"use client";
export { PostPage as NoticePage } from "@/widgets/post";
@@ -0,0 +1,3 @@
"use client";
export { Post as NoticePost } from "@/widgets/post";
+5 -3
View File
@@ -50,7 +50,7 @@ function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
});
const handleProgress = (percentage: string) => {
setProgress(Number(percentage));
setProgress(parseInt(percentage));
};
const handleUploadResponse = async (res: HttpResponse) => {
@@ -59,7 +59,7 @@ function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
if (atflCd) form.setValue("atflCd", atflCd);
if (atflId) setImageUrl(await AttachmentApi.getImageUrl(atflId));
if (atflId) setImageUrl(await AttachmentApi.getImageUrlByAtflId(atflId));
if (attachmentData)
await AttachmentApi.deleteAttachmentByAtflCd(attachmentData.atflCd);
};
@@ -68,7 +68,9 @@ function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
if (!attachmentData) return;
(async () => {
setImageUrl(await AttachmentApi.getImageUrl(attachmentData.atflId));
setImageUrl(
await AttachmentApi.getImageUrlByAtflId(attachmentData.atflId),
);
})();
}, [attachmentData]);
+1 -1
View File
@@ -133,7 +133,7 @@ function BannerPage() {
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="card !m-[1.2rem] !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
+3 -4
View File
@@ -3,8 +3,7 @@
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 { useAlertStore, useMutationWrapper } from "@/shared/lib";
import {
bannerFormSchema,
type BannerFormData,
@@ -14,9 +13,9 @@ import { BannerForm } from "./banner-form";
function CreatePage() {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createBanner } = useMutation({
const { mutateAsync: createBanner } = useMutationWrapper({
mutationFn: (data: BannerFormData) => BannerApi.createBanner(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
+8 -8
View File
@@ -4,7 +4,7 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
@@ -13,7 +13,7 @@ import {
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button, DataTable } from "@common/components";
import { PostingApi, type PostPayload } from "@/entities/posting";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import {
bannerFormSchema,
@@ -27,7 +27,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
const router = useRouter();
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@@ -39,10 +39,10 @@ function ModifyPage({ sequential }: { sequential: string }) {
const { data: postHistory } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
queryFn: () =>
PostingApi.getPostingHistory({ pstgTrgtNo: Number(sequential) }),
PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(sequential) }),
});
const { mutateAsync: updateBanner } = useMutation({
const { mutateAsync: updateBanner } = useMutationWrapper({
mutationFn: (data: BannerFormData) =>
BannerApi.updateBanner(sequential, data),
onSuccess: () => {
@@ -50,14 +50,14 @@ function ModifyPage({ sequential }: { sequential: string }) {
},
});
const { mutateAsync: deleteBanner } = useMutation({
const { mutateAsync: deleteBanner } = useMutationWrapper({
mutationFn: () => BannerApi.deleteBanner(sequential),
onSuccess: () => {
alert("삭제되었습니다.", router.back);
},
});
const { mutateAsync: savePosting } = useMutation({
const { mutateAsync: savePosting } = useMutationWrapper({
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
onSuccess: () => {
queryClient.invalidateQueries({
@@ -102,7 +102,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
</div>
<BannerForm
form={form}
bnrSn={Number(sequential)}
bnrSn={parseInt(sequential)}
handleSave={updateBanner}
handleDelete={deleteBanner}
/>
+13 -8
View File
@@ -1,15 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import dayjs from "dayjs";
import { useRouter } from "next/navigation";
import { usePathname, 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 type { Board } from "@/entities/board";
import { Button } from "@common/components";
export const columns: ColumnDef<Board>[] = [
{
id: "no",
header: "NO",
cell: ({ row }) => row.index + 1,
cell: ({ row, table }) => {
const totalRows = table.getRowCount();
const currentPage = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
return totalRows - (currentPage * pageSize + row.index);
},
},
{
accessorKey: "bbsNm",
@@ -37,22 +42,22 @@ export const columns: ColumnDef<Board>[] = [
},
{
id: "actions",
header: "관리",
header: "게시물관리",
cell: ({ row }) => <ManageButton row={row} />,
},
];
const ManageButton = ({ row }: { row: Row<Board> }) => {
const router = useRouter();
const pathname = usePathname();
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));
router.push(`${pathname}/${bbsTypeCd.toLowerCase()}/${bbsId}`);
}}
>
관리
+87 -25
View File
@@ -1,5 +1,6 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { UseFormReturn } from "react-hook-form";
import {
Input,
@@ -19,7 +20,12 @@ import {
} from "@common/components";
import { FormWrapper } from "@/widgets/form-wrapper";
import { CodeSelect } from "@/entities/code";
import type { Board, BoardFormData } from "@/entities/board";
import {
BoardTemplateApi,
type Board,
type BoardFormData,
} from "@/entities/board";
import { ENDPOINT } from "@/shared/config";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
type BbsFormProps = {
@@ -29,6 +35,15 @@ type BbsFormProps = {
};
function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
const bbsTypeCd = form.watch("bbsTypeCd");
const disabledForm = !!bbsTypeCd && bbsTypeCd !== "BBSNORMAL";
const { data: boardTemplate } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.BBS_TEMPLATES, bbsTypeCd],
queryFn: () => BoardTemplateApi.getBoardTemplate(bbsTypeCd),
enabled: disabledForm,
});
return (
<FormWrapper form={form} isModifyMode={!!bbsId} {...props}>
<div className="card">
@@ -109,7 +124,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.rfrncUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="rfrncUseY" />
<Label htmlFor="rfrncUseY">사용</Label>
@@ -134,7 +154,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.atflUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="atflUseY" />
<Label htmlFor="atflUseY">사용</Label>
@@ -163,15 +188,15 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
{...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));
}}
value={boardTemplate?.atflMaxCnt ?? field.value ?? ""}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
className="w-[12rem] !text-[1.3rem]"
placeholder="최대 개수 입력"
disabled={form.watch("atflUseYn") === "N"}
disabled={
form.getValues("atflUseYn") === "N" || disabledForm
}
/>
</FormControl>
</FormItem>
@@ -192,15 +217,17 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
{...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));
}}
value={
boardTemplate?.atflMaxFileSz ?? field.value ?? ""
}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
className="w-[12rem] !text-[1.3rem]"
placeholder="최대 크기 입력"
disabled={form.watch("atflUseYn") === "N"}
disabled={
form.getValues("atflUseYn") === "N" || disabledForm
}
/>
</FormControl>
</FormItem>
@@ -217,7 +244,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.ansUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="ansUseY" />
<Label htmlFor="ansUseY">사용</Label>
@@ -245,7 +277,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
<Select
value={field.value ?? ""}
onValueChange={field.onChange}
disabled={form.watch("ansUseYn") === "N"}
disabled={form.getValues("ansUseYn") === "N"}
>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="선택" />
@@ -271,7 +303,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.cmntUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="cmntUseY" />
<Label htmlFor="cmntUseY">사용</Label>
@@ -296,7 +333,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.dgstfnEvlUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="dgstfnEvlUseY" />
<Label htmlFor="dgstfnEvlUseY">사용</Label>
@@ -321,7 +363,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.popupUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="popupUseY" />
<Label htmlFor="popupUseY">사용</Label>
@@ -346,7 +393,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.smsUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="smsUseY" />
<Label htmlFor="smsUseY">사용</Label>
@@ -371,7 +423,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.upndFixUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="upndFixUseY" />
<Label htmlFor="upndFixUseY">사용</Label>
@@ -396,7 +453,12 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={boardTemplate?.pstgPrdUseYn ?? field.value}
onValueChange={field.onChange}
disabled={disabledForm}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="pstgPrdUseY" />
<Label htmlFor="pstgPrdUseY">사용</Label>
+2 -2
View File
@@ -117,7 +117,7 @@ function BbsPage() {
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(Number(value));
table.setPageSize(parseInt(value));
}}
>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
@@ -134,7 +134,7 @@ function BbsPage() {
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="card !m-[1.2rem] !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
+3 -4
View File
@@ -3,8 +3,7 @@
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 { useAlertStore, useMutationWrapper } from "@/shared/lib";
import {
BoardApi,
boardFormSchema,
@@ -14,9 +13,9 @@ import { BbsForm } from "./bbs-form";
function CreatePage() {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createBoard } = useMutation({
const { mutateAsync: createBoard } = useMutationWrapper({
mutationFn: (data: BoardFormData) => BoardApi.createBoard(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
+4 -4
View File
@@ -2,8 +2,8 @@
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 { useQuery } from "@tanstack/react-query";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import {
BoardApi,
@@ -13,14 +13,14 @@ import {
import { BbsForm } from "./bbs-form";
function ModifyPage({ bbsId }: { bbsId: string }) {
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { data: boardInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId],
queryFn: () => BoardApi.getBoardInfo(bbsId),
});
const { mutateAsync: updateBoard } = useMutation({
const { mutateAsync: updateBoard } = useMutationWrapper({
mutationFn: (data: BoardFormData) => BoardApi.updateBoard(bbsId, data),
onSuccess: () => {
alert("수정되었습니다.");
+1
View File
@@ -0,0 +1 @@
export { CodePage } from "./ui/code-page";
@@ -0,0 +1,70 @@
import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "@common/components";
import type { LwpoCode } from "@/entities/code";
import { LwpoCodeManagementDialog } from "../ui/lwpocode-dialog";
import { useState } from "react";
export const columns: ColumnDef<LwpoCode>[] = [
{
id: "no",
header: "NO",
cell: ({ row, table }) => {
const totalRows = table.getRowCount();
const { pageIndex, pageSize } = table.getState().pagination;
return totalRows - (pageIndex * pageSize + row.index);
},
},
{
accessorKey: "lwpoCd",
header: "하위코드",
},
{
accessorKey: "lwpoCdNm",
header: "하위코드명",
},
{
accessorKey: "useYn",
header: "사용 여부",
},
{
accessorKey: "regDt",
header: "등록일",
cell: ({ row }) => {
const date = dayjs(row.original.regDt);
return date.format("YYYY-MM-DD");
},
},
{
id: "actions",
header: "관리",
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
<LwpoCodeManageButton
lwpoCd={row.original.lwpoCd}
upCd={row.original.upCd}
/>
</div>
),
},
];
const LwpoCodeManageButton = ({
lwpoCd,
upCd,
}: {
lwpoCd: string;
upCd: string;
}) => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<LwpoCodeManagementDialog
lwpoCd={lwpoCd}
upCd={upCd}
open={dialogOpen}
onOpenChange={setDialogOpen}
>
<Button size="sm">관리</Button>
</LwpoCodeManagementDialog>
);
};
@@ -0,0 +1,64 @@
import dayjs from "dayjs";
import { useState } from "react";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "@common/components";
import type { UpCode } from "@/entities/code";
import { UpCodeManagementDialog } from "../ui/upcode-dialog";
export const columns: ColumnDef<UpCode>[] = [
{
id: "no",
header: "NO",
cell: ({ row, table }) => {
const totalRows = table.getRowCount();
const { pageIndex, pageSize } = table.getState().pagination;
return totalRows - (pageIndex * pageSize + row.index);
},
},
{
accessorKey: "upCd",
header: "상위코드",
},
{
accessorKey: "upCdNm",
header: "상위코드명",
},
{
accessorKey: "cdSe",
header: "코드구분",
},
{
accessorKey: "useYn",
header: "사용여부",
},
{
accessorKey: "regDt",
header: "등록일",
cell: ({ row }) => {
const date = dayjs(row.original.regDt);
return date.format("YYYY-MM-DD");
},
},
{
id: "actions",
header: "관리",
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
<UpCodeManageButton upCd={row.original.upCd} />
</div>
),
},
];
const UpCodeManageButton = ({ upCd }: { upCd: string }) => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<UpCodeManagementDialog
upCd={upCd}
open={dialogOpen}
onOpenChange={setDialogOpen}
>
<Button size="sm">관리</Button>
</UpCodeManagementDialog>
);
};
+20
View File
@@ -0,0 +1,20 @@
"use client";
import { useState } from "react";
import { UpCodeSection } from "./upcode-section";
import { LwpoCodeSection } from "./lwpocode-section";
function CodePage() {
const [selectedUpCd, setSelectedUpCd] = useState("");
return (
<div className="card card-border !mt-6">
<div className="flex">
<UpCodeSection handleSelectedUpCd={setSelectedUpCd} />
<LwpoCodeSection upCd={selectedUpCd} />
</div>
</div>
);
}
export { CodePage };
@@ -0,0 +1,321 @@
import { memo } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Input,
RadioGroup,
RadioGroupItem,
Label,
DialogTrigger,
} from "@common/components";
import {
UpCodeApi,
LwpoCodeApi,
LwpoCodeFormSchema,
type LwpoCodeFormData,
} from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form";
type ManagementLwpoCodeDialogProps = {
lwpoCd?: string;
upCd?: string;
children?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
};
const LwpoCodeManagementDialog = memo(
({
lwpoCd,
upCd,
children,
open,
onOpenChange,
...props
}: ManagementLwpoCodeDialogProps) => {
const isRegistration = !lwpoCd;
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { data: detailData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, lwpoCd],
queryFn: () => LwpoCodeApi.lwpoCodeDetail(upCd!, lwpoCd!),
enabled: !!upCd && !isRegistration,
});
const { data: upCodeData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd],
queryFn: () => UpCodeApi.upCodeDetail(upCd!),
enabled: !!upCd,
});
const { mutateAsync: createLwpoCode } = useMutationWrapper({
mutationFn: (data: LwpoCodeFormData) => LwpoCodeApi.createLwpoCode(data),
onSuccess: () => {
alert("등록되었습니다.", () => onOpenChange?.(false));
},
});
const { mutateAsync: updateLwpoCode } = useMutationWrapper({
mutationFn: (data: LwpoCodeFormData) =>
LwpoCodeApi.updateLwpoCode(upCd!, lwpoCd!, data),
onSuccess: () => {
alert("수정되었습니다.", () => onOpenChange?.(false));
},
});
const form = useForm<LwpoCodeFormData>({
resolver: zodResolver(LwpoCodeFormSchema),
values: {
lwpoCd: detailData?.lwpoCd ?? "",
upCd: upCd ?? "",
lwpoCdNm: detailData?.lwpoCdNm ?? "",
lwpoCdExpln: detailData?.lwpoCdExpln ?? "",
unit: detailData?.unit ?? "",
prntLwpoCd: detailData?.prntLwpoCd ?? "",
useYn: detailData?.useYn ?? "Y",
sortSeq: detailData?.sortSeq ?? 0,
delYn: "N",
},
});
const onSubmit =
(callback: (data: LwpoCodeFormData) => void) =>
async (data: LwpoCodeFormData) => {
const confirmMessage = isRegistration
? "등록하시겠습니까?"
: "수정하시겠습니까?";
if (await confirm(confirmMessage)) callback(data);
};
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>
{isRegistration ? "하위코드 등록" : "하위코드 관리"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(
onSubmit(isRegistration ? createLwpoCode : updateLwpoCode),
)}
className="space-y-4"
>
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>하위코드</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="lwpoCd"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="하위코드를 입력해주세요"
disabled={!isRegistration}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>하위코드명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="lwpoCdNm"
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="lwpoCdExpln"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="하위코드 설명을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{upCodeData?.cdSe === "CODECLUNIT" && (
<TableRow>
<TableHead>단위</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="unit"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="단위를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{upCodeData?.cdSe === "CODECLTREE" && (
<TableRow>
<TableHead>상위하위코드</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="prntLwpoCd"
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="sortSeq"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="number"
value={field.value ?? 0}
className="!text-[1.3rem]"
placeholder="정렬순서를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="useYn"
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>
<DialogFooter className="!justify-center">
<Button size="lg" type="submit">
{isRegistration ? "등록" : "저장"}
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
},
(prevProps, nextProps) => {
return (
prevProps.lwpoCd === nextProps.lwpoCd &&
prevProps.upCd === nextProps.upCd &&
prevProps.open === nextProps.open
);
},
);
LwpoCodeManagementDialog.displayName = "LwpoCodeManagementDialog";
export { LwpoCodeManagementDialog };
@@ -0,0 +1,98 @@
import { useState } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
type PaginationState,
} from "@tanstack/react-table";
import { Button, DataTable, Input, Label } from "@common/components";
import { LwpoCodeApi, type LwpoCodeParams } from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { columns } from "../model/lwpocode-table-columns";
import { LwpoCodeManagementDialog } from "./lwpocode-dialog";
type LwpoCodeSectionProps = {
upCd?: string;
};
function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [lwpoCodeQuery, setLwpoCodeQuery] = useState<LwpoCodeParams>({
lwpoCdNm: "",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { data: lwpoCodeList, refetch } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, pagination],
queryFn: () =>
LwpoCodeApi.getLwpoCodeList({
...lwpoCodeQuery,
upCd,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
enabled: !!upCd,
placeholderData: keepPreviousData,
});
const table = useReactTable({
data: lwpoCodeList?.content ?? [],
columns,
rowCount: lwpoCodeList?.totalElements ?? 0,
state: { pagination },
manualPagination: true,
enableMultiRowSelection: false,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
getRowId: (row) => row.lwpoCd,
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
refetch();
};
return (
<div className="flex-1">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label htmlFor="" className="mr-2 text-label">
하위코드명
</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={lwpoCodeQuery.lwpoCdNm}
onChange={(e) =>
setLwpoCodeQuery({
...lwpoCodeQuery,
lwpoCdNm: e.target.value,
})
}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">하위코드 목록</h3>
<LwpoCodeManagementDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
>
<Button>등록</Button>
</LwpoCodeManagementDialog>
</div>
<div className="card !m-4 h-[calc(100vh-35rem)] overflow-auto !p-0">
<DataTable table={table} viewPagination />
</div>
</div>
</div>
);
}
export { LwpoCodeSection };
+280
View File
@@ -0,0 +1,280 @@
import { memo } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Input,
RadioGroup,
RadioGroupItem,
Label,
DialogTrigger,
} from "@common/components";
import {
CodeSelect,
UpCodeApi,
upCodeFormSchema,
type UpCodeFormData,
} from "@/entities/code";
import { SiteSelect } from "@/entities/site";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form";
type UpCodeManagementDialogProps = {
upCd?: string;
children?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
};
const UpCodeManagementDialog = memo(
({
upCd,
children,
open,
onOpenChange,
...props
}: UpCodeManagementDialogProps) => {
const isRegistration = !upCd;
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { data: detailData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd],
queryFn: () => UpCodeApi.upCodeDetail(upCd!),
enabled: open && !!upCd,
});
const { mutateAsync: createUpCode } = useMutationWrapper({
mutationFn: (data: UpCodeFormData) => UpCodeApi.createUpCode(data),
onSuccess: () => {
alert("등록되었습니다.", () => onOpenChange?.(false));
},
});
const { mutateAsync: updateUpCode } = useMutationWrapper({
mutationFn: (data: UpCodeFormData) => UpCodeApi.updateUpCode(upCd!, data),
onSuccess: () => {
alert("수정되었습니다.", () => onOpenChange?.(false));
},
});
const form = useForm<UpCodeFormData>({
resolver: zodResolver(upCodeFormSchema),
values: {
upCd: detailData?.upCd ?? "",
upCdNm: detailData?.upCdNm ?? "",
cdSe: detailData?.cdSe ?? "",
siteId: detailData?.siteId ?? "",
upCdExpln: detailData?.upCdExpln ?? "",
useYn: detailData?.useYn ?? "Y",
delYn: "N",
},
});
const onSubmit =
(callback: (data: UpCodeFormData) => void) =>
async (data: UpCodeFormData) => {
const confirmMessage = isRegistration
? "등록하시겠습니까?"
: "수정하시겠습니까?";
if (await confirm(confirmMessage)) callback(data);
};
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>
{isRegistration ? "상위코드 등록" : "상위코드 관리"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(
onSubmit(isRegistration ? createUpCode : updateUpCode),
)}
className="space-y-4"
>
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="85%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>상위코드</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="upCd"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="상위코드를를 입력해주세요"
disabled={!isRegistration}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>상위코드명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="upCdNm"
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="upCdExpln"
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="cdSe"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormControl>
<CodeSelect
upCd="CODECL"
value={field.value || ""}
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormControl>
<SiteSelect
value={field.value || ""}
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="useYn"
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>
<DialogFooter className="!justify-center">
<Button size="lg" type="submit">
{isRegistration ? "등록" : "저장"}
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
},
(prevProps, nextProps) => {
return (
prevProps.upCd === nextProps.upCd && prevProps.open === nextProps.open
);
},
);
UpCodeManagementDialog.displayName = "UpCodeManagementDialog";
export { UpCodeManagementDialog };
@@ -0,0 +1,101 @@
import { useState } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
type PaginationState,
} from "@tanstack/react-table";
import { Button, DataTable, Input, Label } from "@common/components";
import { UpCodeApi, type UpCode, type UpCodeParams } from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { columns } from "../model/upcode-table-columns";
import { UpCodeManagementDialog } from "./upcode-dialog";
type UpCodeSectionProps = {
handleSelectedUpCd: (upCd: string) => void;
};
function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [upCodeQuery, setUpCodeQuery] = useState<UpCodeParams>({
keyword: "",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { data: upCodeList, refetch } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, pagination],
queryFn: () =>
UpCodeApi.getUpCodeList({
...upCodeQuery,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
placeholderData: keepPreviousData,
});
const table = useReactTable({
data: upCodeList?.content ?? [],
columns,
rowCount: upCodeList?.totalElements ?? 0,
state: { pagination },
manualPagination: true,
enableMultiRowSelection: false,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
getRowId: (row) => row.upCd,
});
const handleRowClick = (row: UpCode) => {
table.setRowSelection({ [row.upCd]: true });
handleSelectedUpCd(row.upCd);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
refetch();
};
return (
<div className="flex-1">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label htmlFor="" className="mr-2 text-label">
상위코드명
</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={upCodeQuery.keyword}
onChange={(e) =>
setUpCodeQuery({
...upCodeQuery,
keyword: e.target.value,
})
}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">상위코드 목록</h3>
<UpCodeManagementDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
>
<Button>등록</Button>
</UpCodeManagementDialog>
</div>
<div className="card !m-4 h-[calc(100vh-35rem)] overflow-auto !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
</div>
);
}
export { UpCodeSection };
@@ -1,4 +1,6 @@
import Image from "next/image";
import dynamic from "next/dynamic";
import { useQuery } from "@tanstack/react-query";
import { type UseFormReturn } from "react-hook-form";
import {
Input,
@@ -12,12 +14,10 @@ import {
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 { CodeApi } from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "@/entities/code/api/code-service";
import Image from "next/image";
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
const Editor = dynamic(
() => import("@/entities/editor").then((module) => module.Editor),
@@ -89,7 +89,7 @@ function ContentsPage() {
등록
</Button>
</div>
<div className="card !m-[1.2rem] overflow-auto !p-0">
<div className="card !m-[1.2rem] !p-0">
<DataTable
table={table}
onRowClick={handleRowClick}
@@ -2,21 +2,20 @@
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 { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ContentsForm } from "./contents-form";
function CreatePage() {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createContents } = useMutation({
const { mutateAsync: createContents } = useMutationWrapper({
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
+18 -18
View File
@@ -1,19 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { 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,
@@ -22,13 +13,22 @@ import {
SelectItem,
Button,
} from "@common/components";
import {
CONTENTS_APPROVE_STATUS,
ContentsApi,
contentsFormSchema,
type ContentsFormData,
} from "@/entities/content";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ContentsForm } from "./contents-form";
function ModifyPage({ sequential }: { sequential: string }) {
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const contsSn = Number(sequential);
const contsSn = parseInt(sequential);
const [contsVer, setContsVer] = useState(0);
const { data: contentsVersionList } = useQuery({
@@ -47,7 +47,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
enabled: !!contsVer,
});
const { mutateAsync: createContents } = useMutation({
const { mutateAsync: createContents } = useMutationWrapper({
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
onSuccess: () => {
setContsVer(0);
@@ -58,7 +58,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
},
});
const { mutateAsync: updateContents } = useMutation({
const { mutateAsync: updateContents } = useMutationWrapper({
mutationFn: (data: ContentsFormData) =>
ContentsApi.updateContents(contsSn, contsVer, data),
onSuccess: () => {
@@ -66,7 +66,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
},
});
const { mutateAsync: deleteContents } = useMutation({
const { mutateAsync: deleteContents } = useMutationWrapper({
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
onSuccess: () => {
setContsVer(0);
@@ -77,7 +77,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
},
});
const { mutateAsync: requestApprove } = useMutation({
const { mutateAsync: requestApprove } = useMutationWrapper({
mutationFn: () => ContentsApi.requestApprove(contsSn, contsVer),
onSuccess: () => {
alert("승인 요청이 완료되었습니다.");
@@ -87,7 +87,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
},
});
const { mutateAsync: approveContents } = useMutation({
const { mutateAsync: approveContents } = useMutationWrapper({
mutationFn: () => ContentsApi.approveContents(contsSn, contsVer),
onSuccess: () => {
alert("승인되었습니다.");
@@ -135,7 +135,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
<Select
value={contsVer.toString()}
onValueChange={(value) => {
setContsVer(Number(value));
setContsVer(parseInt(value));
}}
>
<SelectTrigger className="w-[28rem]">
@@ -30,7 +30,7 @@ export const createColumns = (
header: "파일크기",
cell: ({ row }) => {
const size = row.getValue("atflFlieSz");
return size ? formatFileSize(Number(size)) : "0 Bytes";
return size ? formatFileSize(size as number) : "0 Bytes";
},
},
+7 -6
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
@@ -10,13 +10,13 @@ import {
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button, DataTable, Input, Label } from "@common/components";
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { createColumns } from "../model/table-columns";
function FilePage() {
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const [attachmentQuery, setAttachmentQuery] = useState<AttachmentParams>({
keyword: "",
@@ -38,8 +38,9 @@ function FilePage() {
enabled: false,
});
const { mutate: deleteAttachment } = useMutation({
mutationFn: (atflId: string) => AttachmentApi.deleteAttachment(atflId),
const { mutate: deleteAttachment } = useMutationWrapper({
mutationFn: (atflId: string) =>
AttachmentApi.deleteAttachmentByAtflId(atflId),
onSuccess: () => {
refetch();
alert("삭제되었습니다.");
@@ -107,7 +108,7 @@ function FilePage() {
<div className="card-header">
<h3 className="text-2xl font-medium text-label">파일 목록</h3>
</div>
<div className="card !m-[1.2rem] overflow-auto !p-0">
<div className="card !m-[1.2rem] !p-0">
<DataTable table={table} viewPagination />
</div>
</div>
+3 -4
View File
@@ -3,17 +3,16 @@
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 { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
import { ImageApi } from "../api/image-service";
import { ImageForm } from "./image-form";
function CreatePage() {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createImage } = useMutation({
const { mutateAsync: createImage } = useMutationWrapper({
mutationFn: (data: ImageFormData) => ImageApi.createImage(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
+7 -7
View File
@@ -44,7 +44,9 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
progress,
setFile,
} = useTus();
const { setAttachmentFileCode } = useAttachmentStore((state) => state);
const setAttachmentFileCode = useAttachmentStore(
(state) => state.setAttachmentFileCode,
);
const [imageUrl, setImageUrl] = useState("");
const { data: attachmentData } = useQuery({
@@ -60,7 +62,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
if (atflCd) form.setValue("atflCd", atflCd);
if (atflId) setImageUrl(await AttachmentApi.getImageUrl(atflId));
if (atflId) setImageUrl(await AttachmentApi.getImageUrlByAtflId(atflId));
if (attachmentData)
await AttachmentApi.deleteAttachmentByAtflCd(attachmentData.atflCd);
};
@@ -122,11 +124,9 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
type="number"
min={0}
value={field.value ?? ""}
onChange={(e) => {
const value = e.target.value;
if (isNaN(Number(value))) return;
field.onChange(Number(value));
}}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
className="w-[12rem] !text-[1.3rem]"
placeholder="정렬순서 입력"
/>
+1 -1
View File
@@ -121,7 +121,7 @@ function ImagePage() {
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="card !m-[1.2rem] !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
+8 -8
View File
@@ -4,7 +4,7 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
@@ -13,7 +13,7 @@ import {
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button, DataTable } from "@common/components";
import { PostingApi, type PostPayload } from "@/entities/posting";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
import { columns } from "../model/posting-table-columns";
@@ -24,7 +24,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
const router = useRouter();
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@@ -36,24 +36,24 @@ function ModifyPage({ sequential }: { sequential: string }) {
const { data: postHistory } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
queryFn: () =>
PostingApi.getPostingHistory({ pstgTrgtNo: Number(sequential) }),
PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(sequential) }),
});
const { mutateAsync: updateImage } = useMutation({
const { mutateAsync: updateImage } = useMutationWrapper({
mutationFn: (data: ImageFormData) => ImageApi.updateImage(sequential, data),
onSuccess: () => {
alert("수정되었습니다.");
},
});
const { mutateAsync: deleteImage } = useMutation({
const { mutateAsync: deleteImage } = useMutationWrapper({
mutationFn: () => ImageApi.deleteImage(sequential),
onSuccess: () => {
alert("삭제되었습니다.", router.back);
},
});
const { mutateAsync: savePosting } = useMutation({
const { mutateAsync: savePosting } = useMutationWrapper({
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
onSuccess: () => {
queryClient.invalidateQueries({
@@ -98,7 +98,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
</div>
<ImageForm
form={form}
imgSn={Number(sequential)}
imgSn={parseInt(sequential)}
handleSave={updateImage}
handleDelete={deleteImage}
/>

Some files were not shown because too many files have changed in this diff Show More