Merge remote-tracking branch 'origin/iipbs/fo/dev'

This commit is contained in:
swcho
2025-10-02 14:00:43 +09:00
50 changed files with 1253 additions and 150 deletions
+11
View File
@@ -0,0 +1,11 @@
import { Client } from "@common/network";
import { BASE_URL } from "../config/api";
import { SITE_ID } from "../config/env";
import { setCookie } from "../lib";
setCookie("site-id-key", SITE_ID);
export const client = Client.getInstance({
baseURL: BASE_URL,
siteId: SITE_ID,
});
+1
View File
@@ -0,0 +1 @@
export { client } from "./client";
+6
View File
@@ -0,0 +1,6 @@
export { useMutationWrapper } from "./use-mutation-wrapper";
export { useAlertStore } from "./use-alert-store";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
export { usePrompt } from "./use-prompt";
export { getCookie, setCookie, removeCookie } from "./cookie";
+14
View File
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { AlertStore } from "../store/alert-store";
import { StoreContext } from "../providers/store-provider";
export const useAlertStore = <T>(selector: (store: AlertStore) => T): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useAlertStore must be used within StoreProvider");
}
return useStore(storeContext.alertStore, selector);
};
+14
View File
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { MenuStore } from "../store/menu-store";
import { StoreContext } from "../providers/store-provider";
export const useMenuStore = <T>(selector: (store: MenuStore) => T): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useMenuStore must be used within StoreProvider");
}
return useStore(storeContext.menuStore, selector);
};
@@ -0,0 +1,31 @@
import { AxiosError } from "axios";
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
import { useAlertStore } from "./use-alert-store";
export const useMutationWrapper = <
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown,
>(
mutationOptions: UseMutationOptions<TData, TError, TVariables, TContext>,
) => {
const alert = useAlertStore((state) => state.setMessage);
return useMutation({
...mutationOptions,
onError: (error) => {
let errorMessage = "";
if (error instanceof AxiosError) {
if (error.response?.status)
errorMessage += `status: ${error.response.status}\n`;
if (error.response?.data?.message)
errorMessage += `${error.response.data.message}\n`;
if (error.response?.data?.errors[0]?.message)
errorMessage += `${error.response.data.errors[0].message}\n`;
}
alert(`[Error] ${errorMessage}`);
},
});
};
+26
View File
@@ -0,0 +1,26 @@
import { useContext } from "react";
import { PromptContext } from "../providers/prompt-provider";
export const usePrompt = () => {
const { message, resolve, setMessage, setResolve } =
useContext(PromptContext);
const prompt = (message: string) => {
setMessage(message);
return new Promise<{ resolved: boolean; value?: string }>((resolve) => {
setResolve(() => resolve);
});
};
const onSubmit = (value: string) => {
resolve?.({ resolved: true, value });
setMessage("");
};
const onCancel = () => {
resolve?.({ resolved: false });
setMessage("");
};
return { message, isOpen: !!message, prompt, onSubmit, onCancel };
};
+14
View File
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { UserStore } from "../store/user-store";
import { StoreContext } from "../providers/store-provider";
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useUserStore must be used within StoreProvider");
}
return useStore(storeContext.userStore, selector);
};
+2
View File
@@ -0,0 +1,2 @@
export { StoreProvider } from "./store-provider";
export { PromptProvider } from "./prompt-provider";
@@ -0,0 +1,37 @@
import { createContext, useState } from "react";
interface PromptContext {
message?: string;
resolve?: ({
resolved,
value,
}: {
resolved: boolean;
value?: string;
}) => void;
setMessage: (message: string) => void;
setResolve: (resolve: () => void) => void;
}
export const PromptContext = createContext<PromptContext>({
message: "",
resolve() {},
setMessage() {},
setResolve() {},
});
export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
const [message, setMessage] = useState<string>();
const [resolve, setResolve] =
useState<
({ resolved, value }: { resolved: boolean; value?: string }) => void
>();
return (
<PromptContext.Provider
value={{ message, resolve, setMessage, setResolve }}
>
{children}
</PromptContext.Provider>
);
};
@@ -0,0 +1,38 @@
"use client";
import { createContext, useRef } from "react";
import {
createAlertStore,
createAttachmentStore,
createMenuStore,
createUserStore,
} from "../store";
type StoreContextType = {
alertStore: ReturnType<typeof createAlertStore>;
attachmentStore: ReturnType<typeof createAttachmentStore>;
menuStore: ReturnType<typeof createMenuStore>;
userStore: ReturnType<typeof createUserStore>;
};
export const StoreContext = createContext<StoreContextType | undefined>(
undefined,
);
export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
const storeRef = useRef<StoreContextType | null>(null);
if (storeRef.current === null) {
storeRef.current = {
alertStore: createAlertStore(),
attachmentStore: createAttachmentStore(),
menuStore: createMenuStore(),
userStore: createUserStore(),
};
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
);
};
+25
View File
@@ -0,0 +1,25 @@
import { createStore } from "zustand/vanilla";
export type AlertState = {
message: string;
callback?: () => void;
};
export type AlertActions = {
setMessage: (message: string, callback?: () => void) => void;
};
export type AlertStore = AlertState & AlertActions;
export const defaultInitState: AlertState = {
message: "",
callback: undefined,
};
export const createAlertStore = (initState: AlertState = defaultInitState) => {
return createStore<AlertStore>()((set) => ({
...initState,
setMessage: (message: string, callback?: () => void) =>
set(() => ({ message, callback })),
}));
};
@@ -0,0 +1,25 @@
import { createStore } from "zustand/vanilla";
export type AttachmentState = {
attachmentFileCode: string;
};
export type AttachmentActions = {
setAttachmentFileCode: (attachmentFileCode: string) => void;
};
export type AttachmentStore = AttachmentState & AttachmentActions;
export const defaultInitState: AttachmentState = {
attachmentFileCode: "",
};
export const createAttachmentStore = (
initState: AttachmentState = defaultInitState,
) => {
return createStore<AttachmentStore>()((set) => ({
...initState,
setAttachmentFileCode: (attachmentFileCode: string) =>
set({ attachmentFileCode }),
}));
};
+4
View File
@@ -0,0 +1,4 @@
export { createAlertStore } from "./alert-store";
export { createAttachmentStore } from "./attachment-store";
export { createMenuStore, type BbsType } from "./menu-store";
export { createUserStore, type IUserInfo } from "./user-store";
+93
View File
@@ -0,0 +1,93 @@
import { createStore } from "zustand/vanilla";
export const BBS_TYPE = {
BBSNOTICE: "BBSNOTICE",
BBSNORMAL: "BBSNORMAL",
BBSFAQ: "BBSFAQ",
BBSIMAGE: "BBSIMAGE",
BBSMOVIE: "BBSMOVIE",
} as const;
export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE];
export interface IBbs {
bbsId: number;
bbsTypeCd: BbsType;
bbsTypeNm: string;
bbsNm: string;
bbsExpln: string;
rfrncUseYn: string;
atflUseYn: string;
atflMaxCnt: number;
atflMaxFileSz: number;
ansUseYn: string;
ansUseAuthrt: string;
cmntUseYn: string;
dgstfnEvlUseYn: string;
smsUseYn: string;
upndFixUseYn: string;
pstgPrdUseYn: string;
popupUseYn: string;
delYn: string;
useYn: string;
mdfcnDt: string;
mdfrNm: string;
}
interface IMenu {
menuCd: number;
menuKornNm: string;
menuEngNm: string;
parentId: number | null;
sortSeq: number;
iconNm?: string;
children: IMenu[] | null;
levelNo: number;
urlPath: string;
menuLnkgTypeCd: string;
menuLnkgSn: number;
menuPath: string;
qrcd: string;
useYn: string;
otptYn: string;
newWndwYn: string;
requiredUrlPath: boolean;
}
interface IAuthority {
mngAuthrtYn: string;
inqAuthrtYn: string;
regAuthrtYn: string;
mdfcnAuthrtYn: string;
delAuthrtYn: string;
}
export type MenuState = {
bbsInfo: IBbs | null;
currentMenuInfo: IMenu | null;
pageMenuAuthority: IAuthority | null;
};
export type MenuActions = {
setBbsInfo: (bbs: IBbs) => void;
setCurrentMenuInfo: (menu: IMenu) => void;
setPageMenuAuthority: (authority: IAuthority) => void;
};
export type MenuStore = MenuState & MenuActions;
export const defaultInitState: MenuState = {
currentMenuInfo: null,
bbsInfo: null,
pageMenuAuthority: null,
};
export const createMenuStore = (initState: MenuState = defaultInitState) => {
return createStore<MenuStore>()((set) => ({
...initState,
setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }),
setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }),
setPageMenuAuthority: (authority: IAuthority) =>
set({ pageMenuAuthority: authority }),
}));
};
+57
View File
@@ -0,0 +1,57 @@
import { createStore } from "zustand/vanilla";
export interface IUserInfo {
regDt: string;
mdfcnDt: string;
rgtr: string;
mdfr: string;
adminRoleYn: 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;
}
export type UserState = {
userInfo: IUserInfo | null;
};
export type UserActions = {
setUserInfo: (userInfo: IUserInfo | null) => void;
};
export type UserStore = UserState & UserActions;
export const defaultInitState: UserState = {
userInfo: null,
};
export const createUserStore = (initState: UserState = defaultInitState) => {
return createStore<UserStore>()((set) => ({
...initState,
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
}));
};
+34
View File
@@ -0,0 +1,34 @@
import {
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Button,
} from "@common/components";
import { useAlertStore } from "../lib/use-alert-store";
function AlertDialog() {
const { message, callback, setMessage } = useAlertStore((state) => state);
const handleOpenChange = () => {
setMessage("");
callback?.();
};
return (
<Dialog open={!!message} onOpenChange={handleOpenChange}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
{message}
</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button>확인</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { AlertDialog };
+48
View File
@@ -0,0 +1,48 @@
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Button,
type ButtonProps,
} from "@common/components";
type ConfirmDialogProps = {
isOpen?: boolean;
confirmButtonProps?: ButtonProps & { label?: string };
cancelButtonProps?: ButtonProps & { label?: string };
};
function ConfirmDialog({
isOpen,
confirmButtonProps,
cancelButtonProps,
}: ConfirmDialogProps) {
const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm();
return (
<Dialog open={isOpen ?? isGlobalOpen} onOpenChange={onCancel}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
{message}
</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button onClick={onConfirm} {...confirmButtonProps}>
{confirmButtonProps?.label ?? "확인"}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={onCancel} color="white" {...cancelButtonProps}>
{cancelButtonProps?.label ?? "취소"}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { ConfirmDialog };
+13
View File
@@ -0,0 +1,13 @@
import { useMenuStore } from "../lib/use-menu-store";
function MapLayout({ children }: { children: React.ReactNode }) {
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
return (
<div className="h-auto min-h-full bg-container pb-2">
{children}
</div>
);
}
export { MapLayout };
+22
View File
@@ -0,0 +1,22 @@
import { PageTitle } from "@common/components";
import { useMenuStore } from "../lib/use-menu-store";
function PageLayout({ children }: { children: React.ReactNode }) {
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
const menuPaths = currentMenuInfo?.menuPath
?.split(">")
.map((path) => path.trim());
return (
<div className="h-auto min-h-full bg-container pb-2">
<PageTitle
title={currentMenuInfo?.menuKornNm ?? ""}
menuPaths={menuPaths ?? []}
/>
{children}
</div>
);
}
export { PageLayout };
+45
View File
@@ -0,0 +1,45 @@
import { useState } from "react";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Input,
} from "@common/components";
import { usePrompt } from "@/src/shared/lib/use-prompt";
function PromptDialog() {
const { message, isOpen, onSubmit, onCancel } = usePrompt();
const [value, setValue] = useState("");
return (
<Dialog open={isOpen} onOpenChange={onCancel}>
<DialogContent aria-describedby={undefined}>
<div className="flex flex-col gap-6 pb-4 pt-8">
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
{message}
</p>
<Input
type="text"
value={value}
className="!text-[1.3rem]"
onChange={(e) => setValue(e.target.value)}
/>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button onClick={() => onSubmit(value)}>확인</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={onCancel} color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { PromptDialog };