diff --git a/apps/common/features/package.json b/apps/common/features/package.json index 0bd642b8e..2ca07cfce 100644 --- a/apps/common/features/package.json +++ b/apps/common/features/package.json @@ -39,7 +39,6 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", - "@faker-js/faker": "^9.5.0", "@types/js-cookie": "^3.0.6", "@types/node": "^22.13.1", "@types/webpack": "^5.28.5", diff --git a/apps/common/features/src/widgets/post/ui/post-page.tsx b/apps/common/features/src/widgets/post/ui/post-page.tsx index 8c4c8c998..f53af0ee8 100644 --- a/apps/common/features/src/widgets/post/ui/post-page.tsx +++ b/apps/common/features/src/widgets/post/ui/post-page.tsx @@ -77,7 +77,7 @@ function PostPage({ bbsId }: { bbsId: string }) { isLoading, refetch, } = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId], + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pagination], queryFn: () => PostApi.getPostList(parseInt(bbsId), { ...postParams, diff --git a/apps/cs/next.config.ts b/apps/cs/next.config.ts index e28e02589..3b3038b09 100644 --- a/apps/cs/next.config.ts +++ b/apps/cs/next.config.ts @@ -4,7 +4,7 @@ const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000"; const nextConfig: NextConfig = { - output: 'standalone', // docker image shrink + output: "standalone", async rewrites() { return { beforeFiles: [ diff --git a/apps/cs/package.json b/apps/cs/package.json index a072c1d82..4412e6482 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -34,6 +34,7 @@ "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", "next": "15.3.0", + "next-qrcode": "^2.5.1", "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-hook-form": "^7.54.2", @@ -44,7 +45,6 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", - "@faker-js/faker": "^9.5.0", "@types/js-cookie": "^3.0.6", "@types/node": "^22.13.1", "@types/webpack": "^5.28.5", diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index 1a2189b03..137f32f80 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -7,10 +7,11 @@ 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 { 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 { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog"; +import { PromptProvider, StoreProvider } from "@/shared/providers"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); @@ -31,6 +32,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + diff --git a/apps/cs/src/entities/loginlog/api/loginlog-service.ts b/apps/cs/src/entities/loginlog/api/loginlog-service.ts new file mode 100644 index 000000000..c115aebcb --- /dev/null +++ b/apps/cs/src/entities/loginlog/api/loginlog-service.ts @@ -0,0 +1,27 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + LoginlogInfo, + LoginlogList, + LoginlogParams, + LoginlogService, +} from "../model/loginlog-interface"; + +export class LoginlogApiService implements LoginlogService { + public async getLoginlogList(params: LoginlogParams): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.LOGINLOG}`, + { params }, + ); + return response.data; + } + + public async getLoginlogInfo(lgnSn: string): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.LOGINLOG}/${lgnSn}`, + ); + return response.data; + } +} + +export const LoginlogApi = new LoginlogApiService(); diff --git a/apps/cs/src/entities/loginlog/index.ts b/apps/cs/src/entities/loginlog/index.ts new file mode 100644 index 000000000..72032d92d --- /dev/null +++ b/apps/cs/src/entities/loginlog/index.ts @@ -0,0 +1,5 @@ +export { LoginlogApiService } from "./api/loginlog-service"; +export type { + Loginlog, + LoginlogParams, +} from "./model/loginlog-interface"; diff --git a/apps/cs/src/entities/loginlog/model/loginlog-interface.ts b/apps/cs/src/entities/loginlog/model/loginlog-interface.ts new file mode 100644 index 000000000..850b6ce5d --- /dev/null +++ b/apps/cs/src/entities/loginlog/model/loginlog-interface.ts @@ -0,0 +1,37 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface LoginlogParams extends PageableParams { + lgnSn?: string; + userId?: string; + siteId?: string; +} + +export interface Loginlog { + lgnSn: string; + lgnDt: string; + userId: string; + lgnIp: string; + lgnMethodCd: string; + errOcrnYn: string; + lgnErrCd: string; + siteId: string; +} + +export interface LoginlogInfo { + lgnSn: string; + lgnDt: string; + userId: string; + lgnIp: string; + lgnMethodCd: string; + errOcrnYn: string; + lgnErrCd: string; + siteId: string; +} + +export type LoginlogList = PageableList; + +export interface LoginlogService { + getLoginlogList: (params: LoginlogParams) => Promise; + getLoginlogInfo: (lgnSn: string) => Promise; + +} diff --git a/apps/cs/src/entities/post/index.ts b/apps/cs/src/entities/post/index.ts index 0f8d14e32..87cdd758c 100644 --- a/apps/cs/src/entities/post/index.ts +++ b/apps/cs/src/entities/post/index.ts @@ -13,4 +13,5 @@ export { type PostUpdateFormData, type PostService, } from "./model/post-interface"; +export { deleteReasonMessages } from "./model/delete-reason-messages"; export { PostPageGuard } from "./lib/post-page-guard"; diff --git a/apps/cs/src/entities/post/model/delete-reason-messages.ts b/apps/cs/src/entities/post/model/delete-reason-messages.ts new file mode 100644 index 000000000..3f1587472 --- /dev/null +++ b/apps/cs/src/entities/post/model/delete-reason-messages.ts @@ -0,0 +1,11 @@ +export const deleteReasonMessages = { + "저작권 침해": + "타인의 저작물을 허락 없이 게시하는 경우 저작권 침해로 간주되어 삭제될 수 있습니다.", + 명예훼손: "타인의 명예를 훼손하는 내용의 게시물은 삭제될 수 있습니다.", + "개인정보 노출": "타인의 개인정보를 노출하는 게시물도 삭제될 수 있습니다.", + "불법 정보 유통": + "불법적인 상품이나 서비스를 판매하거나 홍보하는 게시물은 삭제될 수 있습니다.", + "욕설 및 비방": + "심한 욕설이나 비방, 모욕적인 내용이 포함된 게시물은 삭제될 수 있습니다.", + "광고성 정보": "지나치게 광고성이 짙은 게시물은 삭제될 수 있습니다.", +}; diff --git a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx index 1057fd2b2..8ddd3006a 100644 --- a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx +++ b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx @@ -21,20 +21,28 @@ import { Skeleton, } from "@common/components"; import { AttachmentApi } from "@/entities/attachment"; -import { PostApi, type PostParams } from "@/entities/post"; +import { + PostApi, + deleteReasonMessages, + type PostParams, +} from "@/entities/post"; import { AuthorityButton } from "@/entities/menu-authority"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useMenuStore, - usePrompt, + useSelectPrompt, useMutationWrapper, } from "@/shared/lib"; +const selectData = Object.entries(deleteReasonMessages).map( + ([label, value]) => ({ label, value }), +); + function FaqPage({ bbsId }: { bbsId: string }) { const router = useRouter(); const pathname = usePathname(); - const { prompt } = usePrompt(); + const { prompt } = useSelectPrompt(selectData); const alert = useAlertStore((state) => state.setMessage); const bbsInfo = useMenuStore((state) => state.bbsInfo); @@ -75,11 +83,13 @@ function FaqPage({ bbsId }: { bbsId: string }) { const handleDeletePost = async (pstNo: number) => { const delRsn = await getPromptValue("삭제 사유를 입력해주세요."); + if (!delRsn) return; deletePost({ pstNo, delRsn }); }; const onSelectedPostDelete = async () => { const delRsn = await getPromptValue("삭제 사유를 입력해주세요."); + if (!delRsn) return; await Promise.all( selectedPostNumbers.map((pstNo) => PostApi.deletePost(parseInt(bbsId), pstNo, delRsn), diff --git a/apps/cs/src/pages/loginlog/model/table-columns.tsx b/apps/cs/src/pages/loginlog/model/table-columns.tsx new file mode 100644 index 000000000..dbe19f044 --- /dev/null +++ b/apps/cs/src/pages/loginlog/model/table-columns.tsx @@ -0,0 +1,49 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import type { Loginlog } from "@/entities/loginlog"; + + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const { pageIndex, pageSize } = table.getState().pagination; + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "lgnDt", + header: "등록일", + cell: ({ row }) => { + const date = dayjs(row.original.lgnDt); + return date.format("YYYY-MM-DD HH24:mi:ss"); + }, + }, + { + accessorKey: "userId", + header: "사용자ID", + }, + { + accessorKey: "로그인IP", + header: "remoteIp", + }, + { + accessorKey: "lgnMthd", + header: "접속방식", + }, + { + accessorKey: "오류여부", + header: "errOcrnYn", + }, + { + accessorKey: "오류코드", + header: "lgnErrCd", + }, + { + accessorKey: "사이트ID", + header: "siteId", + } +]; + diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx new file mode 100644 index 000000000..598eacee2 --- /dev/null +++ b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx @@ -0,0 +1,92 @@ +"use client"; + +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 { LoginlogApi} from "../../../entities/loginlog/api/loginlog-service"; +import { type LoginlogParams} from "../../../entities/loginlog/model/loginlog-interface"; + +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/table-columns"; + +type LoginlogPageProps = { + lgnSn?: string; +}; + +function LoginlogPage({ }: LoginlogPageProps) { + const [loginlogQuery, setLoginlogQuery] = useState({ + userId: "", + siteId: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: loginlogList, refetch } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, , pagination], + queryFn: () => + LoginlogApi.getLoginlogList({ + ...loginlogQuery, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const table = useReactTable({ + data: loginlogList?.content ?? [], + columns, + rowCount: loginlogList?.totalElements ?? 0, + state: { pagination }, + manualPagination: true, + enableMultiRowSelection: false, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + return ( +
+
+
+ + + setLoginlogQuery({ + ...loginlogQuery, + userId: e.target.value, + }) + } + /> + +
+
+
+
+

로그인로그 목록

+
+
+ +
+
+
+ ); +} + +export { LoginlogPage }; diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index 6ff6f89bc..d212dc970 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -75,6 +75,9 @@ function MenuPage() { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], }); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], + }); }, }); @@ -91,6 +94,9 @@ function MenuPage() { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], }); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], + }); }, }); @@ -101,6 +107,9 @@ function MenuPage() { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], }); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], + }); }, }); @@ -118,8 +127,10 @@ function MenuPage() { menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY, iconNm: menuInfo?.iconName, otptYn: menuInfo?.otptYn ?? "Y", - evlUseYn: menuInfo?.evlUseYn ?? "Y", + evlUseYn: menuInfo?.evlUseYn ?? "N", + qrcd: menuInfo?.qrcd ?? "N", useYn: menuInfo?.useYn ?? "Y", + newWndwYn: menuInfo?.newWndwYn ?? "N", }, }); diff --git a/apps/cs/src/pages/menu/ui/qrcode-dialog.tsx b/apps/cs/src/pages/menu/ui/qrcode-dialog.tsx new file mode 100644 index 000000000..042c48911 --- /dev/null +++ b/apps/cs/src/pages/menu/ui/qrcode-dialog.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useQRCode } from "next-qrcode"; +import { + Button, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@common/components"; +import { useClipboard } from "@frontend-opensource/use-react-hooks"; + +function QRCodeDialog({ url }: { url: string }) { + const { Canvas } = useQRCode(); + const { copied: isCopied, copyText } = useClipboard(); + const canvasRef = useRef(null); + const [originUrl, setOriginUrl] = useState(""); + const qrCodeUrl = originUrl + url; + + const handleDownload = () => { + const canvas = canvasRef.current?.querySelector("canvas"); + if (!canvas) return; + const url = canvas.toDataURL("image/png"); + const link = document.createElement("a"); + link.href = url; + link.download = "qr-code.png"; + link.click(); + }; + + useEffect(() => { + if (typeof window === "undefined") return; + setOriginUrl(window.location.origin); + }, []); + + return ( + + + + + + + QR 코드 스캔 + +
+
+ +
+
+
+

{qrCodeUrl}

+ + + + + + 복사완료 + + +
+ +
+
+ ); +} + +export { QRCodeDialog }; diff --git a/apps/cs/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx index 1f0483d1b..8cbd7b8b0 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { type UseFormReturn } from "react-hook-form"; +import type { UseFormReturn } from "react-hook-form"; import { DatePicker, IconFinder, @@ -21,6 +21,7 @@ import type { MenuFormData, MenuType } from "../model/menu-interface"; import { OrganizationSearchDialog } from "./organization-search-dialog"; import { ContentSearchDialog } from "./content-search-dialog"; import { BoardSearchDialog } from "./board-search-dialog"; +import { QRCodeDialog } from "./qrcode-dialog"; type TypeMenuFormProps = { form: UseFormReturn; @@ -33,6 +34,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { }; const handleContentSelect = (content: Contents) => { + form.setValue("urlPath", `/cntns/${content.contsSn}`); form.setValue("menuLnkgSn", content.contsSn); form.setValue("menuLnkgSnNm", content.contsTtl); }; @@ -43,6 +45,10 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { }; const handleBoardSelect = (board: BoardPopup) => { + form.setValue( + "urlPath", + `/pst/${board.bbsTypeCd.toLowerCase()}/${board.bbsId}`, + ); form.setValue("menuLnkgSn", board.bbsId); form.setValue("menuLnkgSnNm", board.bbsNm); }; @@ -52,6 +58,14 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { form.setValue("menuPicDsgnYmd", dayjs(date).format("YYYYMMDD")); }; + const getUrlPathPlaceholder = () => { + if (menuType === "CONTENTS") return "콘텐츠 선택 시 자동으로 입력됩니다."; + if (menuType === "BOARD") return "게시판 선택 시 자동으로 입력됩니다."; + if (menuType === "INSIDE") + return "메뉴에 연결된 페이지의 경로를 입력하십시오."; + return "'http://' 또는 'https://'를 포함하여 입력하십시오."; + }; + return ( @@ -91,7 +105,10 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { )} - {(menuType === "INSIDE" || menuType === "OUTSIDE") && ( + {(menuType === "CONTENTS" || + menuType === "BOARD" || + menuType === "INSIDE" || + menuType === "OUTSIDE") && ( 연결 URL @@ -105,10 +122,9 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { {...field} value={field.value ?? ""} className="!text-[1.3rem]" - placeholder={ - menuType === "INSIDE" - ? "메뉴에 연결된 페이지의 경로를 입력하십시오." - : "'http://' 또는 'https://'를 포함하여 입력하십시오." + placeholder={getUrlPathPlaceholder()} + disabled={ + menuType === "CONTENTS" || menuType === "BOARD" } /> @@ -293,25 +309,30 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { control={form.control} name="qrcd" render={({ field }) => ( - - - -
-
- - +
+ + + +
+
+ + +
+
+ + +
-
- - -
-
- - - + + + + {field.value === "Y" && ( + + )} +
)} /> diff --git a/apps/cs/src/pages/recent/ui/recent-page.tsx b/apps/cs/src/pages/recent/ui/recent-page.tsx index f99ee2339..681c25e34 100644 --- a/apps/cs/src/pages/recent/ui/recent-page.tsx +++ b/apps/cs/src/pages/recent/ui/recent-page.tsx @@ -23,13 +23,14 @@ import { } from "@common/components"; import { PostApi, + deleteReasonMessages, type DeletePost, type RecentPost, type RecentPostParams, } from "@/entities/post"; import { BoardApi } from "@/entities/board"; import { AuthorityButton } from "@/entities/menu-authority"; -import { useAlertStore, usePrompt, useDataTable } from "@/shared/lib"; +import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { columns } from "../model/table-columns"; import { MovePostDialog } from "./move-post-dialog"; @@ -39,9 +40,13 @@ type DateRange = { to?: Date | undefined; }; +const selectData = Object.entries(deleteReasonMessages).map( + ([label, value]) => ({ label, value }), +); + function RecentPage() { const router = useRouter(); - const { prompt } = usePrompt(); + const { prompt } = useSelectPrompt(selectData); const { confirm } = useConfirm(); const alert = useAlertStore((state) => state.setMessage); const [dateRange, setDateRange] = useState(); diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index e7ee5cbcc..ee168407e 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -19,6 +19,7 @@ export const ENDPOINT = { AUTHORIZATIONS: "/user-service/api/v1/authorizations", ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", ROLES: "/user-service/api/v1/roles", + LOGINLOG: "/user-service//api/v1/login-logs", }, CMS_SERVICE: { SITES: "/cms-service/api/v1/sites", @@ -45,7 +46,6 @@ export const ENDPOINT = { SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", ZIPS: "/cms-service/api/v1/zips", - LOGINLOG: "/cms-service/api/v1/loginlog", GROUPS: "/cms-service/api/v1/groups", GROUP_USERS: "/cms-service/api/v1/group-users", MANAGER_IP: "/cms-service/api/v1/mgrip", diff --git a/apps/cs/src/shared/lib/index.ts b/apps/cs/src/shared/lib/index.ts index 20ba33c30..5ac79bf08 100644 --- a/apps/cs/src/shared/lib/index.ts +++ b/apps/cs/src/shared/lib/index.ts @@ -5,3 +5,4 @@ export { useDataTable } from "./use-data-table"; export { useMenuStore } from "./use-menu-store"; export { useUserStore } from "./use-user-store"; export { usePrompt } from "./use-prompt"; +export { useSelectPrompt } from "./use-select-prompt"; diff --git a/apps/cs/src/shared/lib/use-select-prompt.tsx b/apps/cs/src/shared/lib/use-select-prompt.tsx new file mode 100644 index 000000000..357be7a36 --- /dev/null +++ b/apps/cs/src/shared/lib/use-select-prompt.tsx @@ -0,0 +1,48 @@ +import { useContext, useEffect } from "react"; +import { PromptContext } from "../providers/prompt-provider"; + +type SelectData = { + label: string; + value: string; +}; + +export const useSelectPrompt = (data?: SelectData[]) => { + const { + selectData, + selectionGuide, + resolve, + setResolve, + setSelectData, + setSelectionGuide, + } = useContext(PromptContext); + + const prompt = (message: string) => { + setSelectionGuide(message); + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { + setResolve(() => resolve); + }); + }; + + const onSubmit = (value: string) => { + resolve?.({ resolved: true, value }); + setSelectionGuide(""); + }; + + const onCancel = () => { + resolve?.({ resolved: false }); + setSelectionGuide(""); + }; + + useEffect(() => { + if (data) setSelectData(data); + }, [data]); + + return { + selectData, + selectionGuide, + isOpen: !!selectionGuide, + prompt, + onSubmit, + onCancel, + }; +}; diff --git a/apps/cs/src/shared/providers/prompt-provider.tsx b/apps/cs/src/shared/providers/prompt-provider.tsx index 10338c338..71b86388c 100644 --- a/apps/cs/src/shared/providers/prompt-provider.tsx +++ b/apps/cs/src/shared/providers/prompt-provider.tsx @@ -1,7 +1,14 @@ import { createContext, useState } from "react"; +type SelectData = { + label: string; + value: string; +}; + interface PromptContext { message?: string; + selectData?: SelectData[]; + selectionGuide?: string; resolve?: ({ resolved, value, @@ -10,18 +17,26 @@ interface PromptContext { value?: string; }) => void; setMessage: (message: string) => void; + setSelectData: (selectData: SelectData[]) => void; + setSelectionGuide: (message: string) => void; setResolve: (resolve: () => void) => void; } export const PromptContext = createContext({ message: "", + selectData: [], + selectionGuide: "", resolve() {}, setMessage() {}, + setSelectData() {}, + setSelectionGuide() {}, setResolve() {}, }); export const PromptProvider = ({ children }: { children: React.ReactNode }) => { const [message, setMessage] = useState(); + const [selectData, setSelectData] = useState([]); + const [selectionGuide, setSelectionGuide] = useState(); const [resolve, setResolve] = useState< ({ resolved, value }: { resolved: boolean; value?: string }) => void @@ -29,7 +44,16 @@ export const PromptProvider = ({ children }: { children: React.ReactNode }) => { return ( {children} diff --git a/apps/cs/src/shared/ui/prompt-dialog.tsx b/apps/cs/src/shared/ui/prompt-dialog.tsx index 54c2f3fcc..c7e1ebc57 100644 --- a/apps/cs/src/shared/ui/prompt-dialog.tsx +++ b/apps/cs/src/shared/ui/prompt-dialog.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Button, Dialog, @@ -13,6 +13,10 @@ function PromptDialog() { const { message, isOpen, onSubmit, onCancel } = usePrompt(); const [value, setValue] = useState(""); + useEffect(() => { + if (isOpen) setValue(""); + }, [isOpen]); + return ( diff --git a/apps/cs/src/shared/ui/select-prompt-dialog.tsx b/apps/cs/src/shared/ui/select-prompt-dialog.tsx new file mode 100644 index 000000000..19092c8a1 --- /dev/null +++ b/apps/cs/src/shared/ui/select-prompt-dialog.tsx @@ -0,0 +1,68 @@ +import { useEffect, useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + Textarea, +} from "@common/components"; +import { useSelectPrompt } from "../lib/use-select-prompt"; + +function SelectPromptDialog() { + const { selectData, selectionGuide, isOpen, onSubmit, onCancel } = + useSelectPrompt(); + const [value, setValue] = useState(""); + + useEffect(() => { + if (isOpen) setValue(""); + }, [isOpen]); + + return ( + + +
+

+ {selectionGuide} +

+
+ +