diff --git a/apps/fo/.env.local b/apps/fo/.env.local index adaac0461..a87d13587 100644 --- a/apps/fo/.env.local +++ b/apps/fo/.env.local @@ -1,4 +1,11 @@ NEXT_PUBLIC_SITE_ID = FO +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login +NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login +NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login NEXT_PUBLIC_PORT = 3000 NEXT_PUBLIC_PROXY_HOST = http://localhost:3000 NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082 @@ -8,10 +15,6 @@ NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 #NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver -NEXT_PUBLIC_CLAIM_NAME = Authorization -NEXT_PUBLIC_AUTH_USER_ID = token-id -NEXT_PUBLIC_ACCESS_TOKEN = access-token -NEXT_PUBLIC_REFRESH_TOKEN = refresh-token NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service diff --git a/apps/fo/app/base.tsx b/apps/fo/app/base.tsx deleted file mode 100644 index a01ce8643..000000000 --- a/apps/fo/app/base.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { BaseLayout } from "@common/components"; -import type { MenuItem } from "./store/common"; -import { useFoStore } from "./store"; -import { usePathname, useRouter } from "next/navigation"; - -export default function Base({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { getMenuAuth } = useFoStore((state) => state); - const pathname = usePathname(); - const router = useRouter(); - - const handleClick = (id: MenuItem) => { - getMenuAuth(id.menuCd); - router.push(id.urlPath); - }; - - return ( - - {children} - - ); -} diff --git a/apps/fo/app/layout.tsx b/apps/fo/app/layout.tsx index 842ec5ed1..683e6d0ec 100644 --- a/apps/fo/app/layout.tsx +++ b/apps/fo/app/layout.tsx @@ -1,29 +1,14 @@ -"use client" -import "./globals.css"; -import { StoreProvider } from "./store"; -import { LoginApi } from "@/src/entities/auth"; -import { useEffect } from "react"; +import type { Metadata } from "next"; +import { App } from "@/src/app"; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export const metadata: Metadata = { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다", + keywords: ["하수관로"], + openGraph: { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다" + } +}; - useEffect(() => { - // 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치 - void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }) - .catch((e) => { - console.error("Auto login failed:", e); - }); - }, []); - return ( - - - - {children} - - - - ); -} +export default App; diff --git a/apps/fo/app/page.tsx b/apps/fo/app/page.tsx index 823e5a408..b1a103bca 100644 --- a/apps/fo/app/page.tsx +++ b/apps/fo/app/page.tsx @@ -1,44 +1,28 @@ -"use client" +"use client"; +import { LoginApi } from "@/src/entities/auth"; +import { useEffect } from "react"; +import { AUTH_USER_ID } from "@/src/shared/config"; +import { getCookie } from "@/src/shared/lib/cookie"; +import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { Button } from "@common/components/ui"; export default function Home() { - return ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- ); -} + const router = useRouter(); + const checkToken = getCookie(AUTH_USER_ID); + + useEffect(() => { + if (!checkToken) { + async function doLogin() { + try { + await LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }); + router.push("/pages/dashboard"); + } catch (err) { + console.error("로그인 실패", err); + } + } + doLogin(); + } else { + router.push("/pages/dashboard"); + } + }, []); +} \ No newline at end of file diff --git a/apps/fo/app/pages/(with-layout)/layout.tsx b/apps/fo/app/pages/(with-layout)/layout.tsx index f7c0e62ba..851d79497 100644 --- a/apps/fo/app/pages/(with-layout)/layout.tsx +++ b/apps/fo/app/pages/(with-layout)/layout.tsx @@ -1,13 +1,30 @@ "use client" -import Base from "@/app/base"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/app/store/common"; +import { useFoStore } from "@/app/store"; +import { usePathname, useRouter } from "next/navigation"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const { getMenuAuth } = useFoStore((state) => state); + const pathname = usePathname(); + const router = useRouter(); + + const handleClick = (id: MenuItem) => { + getMenuAuth(id.menuCd); + router.push(id.urlPath); + }; + return ( - {children} + + {children} + ); } diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index e44420eaa..775dd1a45 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -210,13 +210,13 @@ export default function ProcessFacilitySearch() { const cellClick = (index: number) => { const facility = processFacilityList[index]; if (facility) { - const fctCpc = String(facility.fctCpc).replace(/,/g, "").trim(); + const fctCpc = Number(String(facility.fctCpc).replace(/,/g, "").trim()); setModalFcltyNm(facility.bscFctNm); setModalFcltyCd(facility.bscFctCd); setModalSido(facility.sido); setModalGugun(facility.gugun); setModalAddr(facility.daddr); - setModalFctCpc(Number(fctCpc)); + setModalFctCpc(fctCpc); setModalTrtFrfNm(facility.trtFrfNm); setModalRnDt(facility.rnDt); setModalCmplDt(facility.cmplDt); diff --git a/apps/fo/middleware.ts b/apps/fo/middleware.ts index 1d8fe949c..bb9c9effa 100644 --- a/apps/fo/middleware.ts +++ b/apps/fo/middleware.ts @@ -139,10 +139,10 @@ export function middleware(req: NextRequest) { }); } - if (pathname === "/") { - return NextResponse.redirect(new URL("/pages/dashboard", req.url)); - } - return NextResponse.next(); + // if (pathname === "/") { + // return NextResponse.redirect(new URL("/pages/dashboard", req.url)); + // } + // return NextResponse.next(); } export const config = { diff --git a/apps/fo/package.json b/apps/fo/package.json index 8697a116a..53423decf 100644 --- a/apps/fo/package.json +++ b/apps/fo/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@common/assets": "workspace:*", "@common/business_components": "workspace:*", "@common/components": "workspace:*", "@tailwindcss/vite": "^4.0.10", diff --git a/apps/fo/src/app/index.tsx b/apps/fo/src/app/index.tsx new file mode 100644 index 000000000..692b55ef0 --- /dev/null +++ b/apps/fo/src/app/index.tsx @@ -0,0 +1,20 @@ +import "./styles/base.css"; +import "./styles/components.css"; +import "@fontsource-variable/noto-sans-kr"; + +import { AppProvider } from "./providers"; +import { cookies } from "next/headers"; + +const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => { + const theme = (await cookies()).get("site-theme-key"); + + return ( + + + {children} + + + ); +}; + +export { App }; diff --git a/apps/fo/src/app/providers/app-provider.tsx b/apps/fo/src/app/providers/app-provider.tsx new file mode 100644 index 000000000..edce6071e --- /dev/null +++ b/apps/fo/src/app/providers/app-provider.tsx @@ -0,0 +1,50 @@ +"use client"; + +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 "@/src/entities/auth/lib/auth-wrapper"; +import { MenuWrapper } from "@/src/entities/menu"; +import { PromptProvider, StoreProvider } from "@/src/shared/providers"; +import { AlertDialog } from "@/src/shared/ui/alert-dialog"; +import { PromptDialog } from "@/src/shared/ui/prompt-dialog"; +import { ConfirmDialog } from "@/src/shared/ui/confirm-dialog"; +import { StoreProvider as FoStoreProvider } from "@/app/store"; + + +const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + const queryClient = new QueryClient(); + return ( + + + + + + + + + {children} + + + + + + + + + + + +
+ +
+
+ ); +}; + +export { AppProvider }; diff --git a/apps/fo/src/app/providers/index.ts b/apps/fo/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/fo/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/fo/src/app/styles/base.css b/apps/fo/src/app/styles/base.css new file mode 100644 index 000000000..8453ff9e8 --- /dev/null +++ b/apps/fo/src/app/styles/base.css @@ -0,0 +1,12 @@ +@import "@common/assets/styles/base.css"; +@import "@common/assets/styles/diveicon.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; +} diff --git a/apps/fo/src/app/styles/components.css b/apps/fo/src/app/styles/components.css new file mode 100644 index 000000000..4dc30bafd --- /dev/null +++ b/apps/fo/src/app/styles/components.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx index 678ff50d5..65bcf48cc 100644 --- a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx @@ -1,45 +1,54 @@ -// "use client"; +"use client"; -// import { useEffect } from "react"; -// import { useRouter } from "next/navigation"; -// import { LoginApi } from "../api/login-service"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { client } from "@/src/shared/api"; +import type { IUserInfo } from "@/src/shared/store"; +import { useUserStore, getCookie } from "@/src/shared/lib"; +import { ENDPOINT, AUTH_USER_ID } from "@/src/shared/config"; +import { LoginApi } from "../api/login-service"; -// const AuthWrapper = ({ children }: { children: React.ReactNode }) => { -// const router = useRouter(); -// const { userInfo, setUserInfo } = useUserStore((state) => state); +const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3001/auth/login"; // 개발 환경 URL -// async function authCheck() { -// const userId = getCookie(AUTH_USER_ID); -// if (!userId) { -// throw new Error("Missing required authentication tokens"); -// } +const AuthWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const { userInfo, setUserInfo } = useUserStore((state) => state); -// const response = await client.get( -// `${ENDPOINT.USER_SERVICE.USERS}/info/me`, -// ); -// if (!userInfo) setUserInfo(response.data); -// } + async function authCheck() { + const userId = getCookie(AUTH_USER_ID); + if (!userId) { + throw new Error("Missing required authentication tokens"); + } -// useEffect(() => { -// (async () => { -// try { -// await authCheck(); -// } catch (error) { -// console.error(error); + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + if (!userInfo) setUserInfo(response.data); + } -// try { -// await LoginApi.silentRefresh(); -// await authCheck(); -// } catch (refreshError) { -// console.error("Token refresh failed:", refreshError); -// setUserInfo(null); -// router.push("/auth/login"); -// } -// } -// })(); -// }, []); + useEffect(() => { + (async () => { + try { + await authCheck(); + } catch (error) { + console.error(error); -// return <>{children}; -// }; + try { + await LoginApi.silentRefresh(); + await authCheck(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + router.push("/"); // 개발시 홈으로 돌아가기 -// export { AuthWrapper }; + // window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + } + } + })(); + }, []); + + return <>{children}; +}; + +export { AuthWrapper }; diff --git a/apps/fo/src/entities/menu-authority/@x/menu.ts b/apps/fo/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..86479f859 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/fo/src/entities/menu-authority/@x/posting.ts b/apps/fo/src/entities/menu-authority/@x/posting.ts new file mode 100644 index 000000000..bfe84fa0d --- /dev/null +++ b/apps/fo/src/entities/menu-authority/@x/posting.ts @@ -0,0 +1 @@ +export { AuthorityButton } from "../ui/authority-button"; diff --git a/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts b/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..c7ac056f4 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,40 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { + MenuAuthorityList, + MenuAuthorityParams, + MenuAuthorityPayload, + MenuAuthorityService, + PageMenuAuthority, +} from "../model/menu-authority-interface"; + +export class MenuAuthorityApiService implements MenuAuthorityService { + public async getMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + { params }, + ); + return response.data; + } + + public async getPageMenuAuthority( + menuCd: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`, + ); + return response.data; + } + + public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + data, + ); + return response.data; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/fo/src/entities/menu-authority/index.ts b/apps/fo/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..2a555b77e --- /dev/null +++ b/apps/fo/src/entities/menu-authority/index.ts @@ -0,0 +1,2 @@ +export { AuthorityButton } from "./ui/authority-button"; +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..f659f58af --- /dev/null +++ b/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/src/shared/model"; + +export interface MenuAuthorityParams extends PageableParams { + groupId?: string; + menuCd?: number; + 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(), + menuCd: z.number(), + mngAuthrtYn: z.string(), + inqAuthrtYn: z.string(), + regAuthrtYn: z.string(), + mdfcnAuthrtYn: z.string(), + delAuthrtYn: z.string(), + useYn: z.string(), + mode: z.string(), +}); + +export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({ + groupNm: true, +}); + +export const menuAuthorityFormSchema = z.object({ + menuAuthority: z.array(menuAuthorityPayloadSchema), +}); + +export type MenuAuthority = z.infer; +export type MenuAuthorityList = PageableList; +export type MenuAuthorityPayload = z.infer; +export type MenuAuthorityFormData = z.infer; + +export interface MenuAuthorityService { + getMenuAuthorityList(params: MenuAuthorityParams): Promise; + getPageMenuAuthority(menuCd: number): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; +} diff --git a/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..6969a85e9 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -0,0 +1,117 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components"; +import type { + MenuAuthority, + MenuAuthorityFormData, + MenuAuthorityPayload, +} from "./menu-authority-interface"; + +export const createColumns = ( + form: UseFormReturn, +): ColumnDef[] => { + const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; + + const handleCheckedChange = + (i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => { + if (getCurrentRow(i)?.mode === "R") { + form.setValue(`menuAuthority.${i}.mode`, "U"); + } + form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N"); + }; + + return [ + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "mngAuthrtYn", + header: "관리권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "regAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "mdfcnAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "delAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "regAuthrtYn", + header: "등록권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "mdfcnAuthrtYn", + header: "수정권한 ", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "delAuthrtYn", + header: "삭제권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "inqAuthrtYn", + header: "조회권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + ]; +}; diff --git a/apps/fo/src/entities/menu-authority/ui/authority-button.tsx b/apps/fo/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..a4cd666d9 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/ui/authority-button.tsx @@ -0,0 +1,26 @@ +import { useMenuStore } from "@/src/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 ( + + ); +} + +export { AuthorityButton }; diff --git a/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..839b9a7a4 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,103 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + DataTable, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/src/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/src/shared/lib"; +import { + menuAuthorityFormSchema, + type MenuAuthorityFormData, + type MenuAuthorityPayload, +} from "../model/menu-authority-interface"; +import { createColumns } from "../model/menu-authority-table-columns"; +import { MenuAuthorityApi } from "../api/menu-authority-service"; + +type ManagementAuthorityDialogProps = { + menuCd?: number; +} & React.ComponentProps; + +function ManagementAuthorityDialog({ + menuCd, + ...props +}: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: menuAuthorityList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], + queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }), + enabled: !!menuCd, + }); + + const { mutateAsync: saveMenuAuthority } = useMutationWrapper({ + mutationFn: (data: MenuAuthorityPayload[]) => + MenuAuthorityApi.saveMenuAuthority(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: menuAuthorityList?.content ?? [], + }, + }); + + const table = useReactTable({ + data: menuAuthorityList?.content ?? [], + columns: createColumns(form), + getCoreRowModel: getCoreRowModel(), + }); + + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("menuAuthority"); + const filteredData = data.filter((item) => item.mode !== "R"); + await saveMenuAuthority(filteredData); + } + }; + + return ( + + + + + + + 그룹별 메뉴 권한 + +
+ +
+ + + + + + +
+
+ ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/fo/src/entities/menu/api/menu-service.ts b/apps/fo/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..8b08d7fa5 --- /dev/null +++ b/apps/fo/src/entities/menu/api/menu-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { MenuItem, MenuService } from "../model/menu-interface"; + +export class MenuApiService implements MenuService { + async getMenuRoles(siteId: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/fo/src/entities/menu/index.ts b/apps/fo/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/fo/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/fo/src/entities/menu/lib/menu-wrapper.tsx b/apps/fo/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..f445123d3 --- /dev/null +++ b/apps/fo/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/src/entities/menu-authority/@x/menu"; +import { useAlertStore, useMenuStore } from "@/src/shared/lib"; +import { getCookie } from "@/src/shared/lib";; +import { SITE_ID } from "@/src/shared/config"; +import { MenuApi } from "../api/menu-service"; +import { getCurrentMenu } from "./utils"; + +const MenuWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const pathname = usePathname(); + + 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 currentMenu = await getCurrentMenuInfo(); + if (currentMenu) 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}; +}; + +export { MenuWrapper }; diff --git a/apps/fo/src/entities/menu/lib/utils.ts b/apps/fo/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/fo/src/entities/menu/lib/utils.ts @@ -0,0 +1,20 @@ +import type { MenuItem } from "../model/menu-interface"; + +export const getCurrentMenu = ( + menus: MenuItem[], + pathname: string, +): MenuItem | null => { + for (const menu of menus) { + if ( + menu.urlPath !== "" && + (menu.urlPath === pathname || pathname.startsWith(menu.urlPath)) + ) + return menu; + + if (menu.children) { + const children = getCurrentMenu(menu.children, pathname); + if (children) return children; + } + } + return null; +}; diff --git a/apps/fo/src/entities/menu/model/menu-interface.ts b/apps/fo/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/fo/src/entities/menu/model/menu-interface.ts @@ -0,0 +1,23 @@ +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +export interface MenuService { + getMenuRoles(siteId: string): Promise; +} diff --git a/apps/fo/src/shared/api/client.ts b/apps/fo/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/fo/src/shared/api/client.ts @@ -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, +}); diff --git a/apps/fo/src/shared/api/index.ts b/apps/fo/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/fo/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/fo/src/shared/lib/index.ts b/apps/fo/src/shared/lib/index.ts new file mode 100644 index 000000000..b8b4c52c0 --- /dev/null +++ b/apps/fo/src/shared/lib/index.ts @@ -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"; diff --git a/apps/fo/src/shared/lib/use-alert-store.ts b/apps/fo/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/fo/src/shared/lib/use-alert-store.ts @@ -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 = (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); +}; diff --git a/apps/fo/src/shared/lib/use-menu-store.ts b/apps/fo/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/fo/src/shared/lib/use-menu-store.ts @@ -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 = (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); +}; diff --git a/apps/fo/src/shared/lib/use-mutation-wrapper.tsx b/apps/fo/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..ce119d2a7 --- /dev/null +++ b/apps/fo/src/shared/lib/use-mutation-wrapper.tsx @@ -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, +) => { + 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}`); + }, + }); +}; diff --git a/apps/fo/src/shared/lib/use-prompt.tsx b/apps/fo/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/fo/src/shared/lib/use-prompt.tsx @@ -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 }; +}; diff --git a/apps/fo/src/shared/lib/use-user-store.ts b/apps/fo/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/fo/src/shared/lib/use-user-store.ts @@ -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 = (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); +}; diff --git a/apps/fo/src/shared/providers/index.ts b/apps/fo/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/fo/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/fo/src/shared/providers/prompt-provider.tsx b/apps/fo/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..10338c338 --- /dev/null +++ b/apps/fo/src/shared/providers/prompt-provider.tsx @@ -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({ + message: "", + resolve() {}, + setMessage() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + + {children} + + ); +}; diff --git a/apps/fo/src/shared/providers/store-provider.tsx b/apps/fo/src/shared/providers/store-provider.tsx new file mode 100644 index 000000000..a0ec17b61 --- /dev/null +++ b/apps/fo/src/shared/providers/store-provider.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { createContext, useRef } from "react"; +import { + createAlertStore, + createAttachmentStore, + createMenuStore, + createUserStore, +} from "../store"; + +type StoreContextType = { + alertStore: ReturnType; + attachmentStore: ReturnType; + menuStore: ReturnType; + userStore: ReturnType; +}; + +export const StoreContext = createContext( + undefined, +); + +export const StoreProvider = ({ children }: { children: React.ReactNode }) => { + const storeRef = useRef(null); + if (storeRef.current === null) { + storeRef.current = { + alertStore: createAlertStore(), + attachmentStore: createAttachmentStore(), + menuStore: createMenuStore(), + userStore: createUserStore(), + }; + } + + return ( + + {children} + + ); +}; diff --git a/apps/fo/src/shared/store/alert-store.ts b/apps/fo/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/fo/src/shared/store/alert-store.ts @@ -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()((set) => ({ + ...initState, + setMessage: (message: string, callback?: () => void) => + set(() => ({ message, callback })), + })); +}; diff --git a/apps/fo/src/shared/store/attachment-store.ts b/apps/fo/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/fo/src/shared/store/attachment-store.ts @@ -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()((set) => ({ + ...initState, + setAttachmentFileCode: (attachmentFileCode: string) => + set({ attachmentFileCode }), + })); +}; diff --git a/apps/fo/src/shared/store/index.ts b/apps/fo/src/shared/store/index.ts new file mode 100644 index 000000000..b0ca59c6c --- /dev/null +++ b/apps/fo/src/shared/store/index.ts @@ -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"; \ No newline at end of file diff --git a/apps/fo/src/shared/store/menu-store.ts b/apps/fo/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/fo/src/shared/store/menu-store.ts @@ -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()((set) => ({ + ...initState, + setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }), + setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }), + setPageMenuAuthority: (authority: IAuthority) => + set({ pageMenuAuthority: authority }), + })); +}; diff --git a/apps/fo/src/shared/store/user-store.ts b/apps/fo/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/fo/src/shared/store/user-store.ts @@ -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()((set) => ({ + ...initState, + setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), + })); +}; diff --git a/apps/fo/src/shared/ui/alert-dialog.tsx b/apps/fo/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/fo/src/shared/ui/alert-dialog.tsx @@ -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 ( + + +
+

+ {message} +

+
+ + + + + +
+
+ ); +} + +export { AlertDialog }; diff --git a/apps/fo/src/shared/ui/confirm-dialog.tsx b/apps/fo/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/fo/src/shared/ui/confirm-dialog.tsx @@ -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 ( + + +
+

+ {message} +

+
+ + + + + + + + +
+
+ ); +} + +export { ConfirmDialog }; diff --git a/apps/fo/src/shared/ui/map-layout.tsx b/apps/fo/src/shared/ui/map-layout.tsx new file mode 100644 index 000000000..e05666401 --- /dev/null +++ b/apps/fo/src/shared/ui/map-layout.tsx @@ -0,0 +1,13 @@ +import { useMenuStore } from "../lib/use-menu-store"; + +function MapLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + return ( +
+ {children} +
+ ); +} + +export { MapLayout }; diff --git a/apps/fo/src/shared/ui/page-layout.tsx b/apps/fo/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..358d3b5db --- /dev/null +++ b/apps/fo/src/shared/ui/page-layout.tsx @@ -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 ( +
+ + {children} +
+ ); +} + +export { PageLayout }; diff --git a/apps/fo/src/shared/ui/prompt-dialog.tsx b/apps/fo/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..496a18b5b --- /dev/null +++ b/apps/fo/src/shared/ui/prompt-dialog.tsx @@ -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 ( + + +
+

+ {message} +

+ setValue(e.target.value)} + /> +
+ + + + + + + + +
+
+ ); +} + +export { PromptDialog }; diff --git a/apps/fo/tailwind.config.ts b/apps/fo/tailwind.config.ts index 33ac9204f..91db77945 100644 --- a/apps/fo/tailwind.config.ts +++ b/apps/fo/tailwind.config.ts @@ -9,4 +9,11 @@ export default { "../../packages/common/business_components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", "../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", ], + theme: { + extend: { + boxShadow: { + blur: "0 0 10px -1px rgba(0, 0, 0, 0.06)", + }, + }, + }, } satisfies Config; \ No newline at end of file diff --git a/apps/fo/tsconfig.json b/apps/fo/tsconfig.json index 493584782..59bd02446 100644 --- a/apps/fo/tsconfig.json +++ b/apps/fo/tsconfig.json @@ -28,12 +28,8 @@ "@/*": ["./*"], "@common/components/*": ["../../packages/common/components/src/*"], "@common/components": ["../../packages/common/components/src/index"], - "@common/business_components/*": [ - "../../packages/common/business_components/src/*" - ], - "@common/business_components": [ - "../../packages/common/business_components/src/index" - ], + "@common/business_components/*": ["../../packages/common/business_components/src/*"], + "@common/business_components": ["../../packages/common/business_components/src/index"], "@common/network": ["../../packages/common/network"] } },