From 599c3590fe6e01f19215a827aa89f536885f60c4 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 24 Jun 2025 13:42:27 +0900 Subject: [PATCH 01/28] =?UTF-8?q?chore:=20=ED=8C=9D=EC=97=85=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/components/src/api/popup.ts | 47 +++++++------------ .../common/components/src/ui/header/index.tsx | 8 ++-- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/packages/common/components/src/api/popup.ts b/packages/common/components/src/api/popup.ts index 0fee0f5b7..23f2eb808 100644 --- a/packages/common/components/src/api/popup.ts +++ b/packages/common/components/src/api/popup.ts @@ -1,6 +1,22 @@ import { getApiClient } from "./client"; import { ENDPOINT } from "../config"; +export interface Popup { + popupSn: number; + popupTypeCd: string; + popupTypeNm: string; + contsTypeCd: string; + contsTypeNm: string; + popupTtl: string; + popupWdth: number; + popupHgt: number; + popupEndUseYn: string; + useYn: string; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; +} + export interface PopupInfo { popupSn: number; popupTypeCd: string; @@ -23,39 +39,10 @@ export interface PopupInfo { mdfrNm: string; } -interface SortConfig { - empty: boolean; - sorted: boolean; - unsorted: boolean; -} - -interface PageableConfig { - sort: SortConfig; - offset: number; - pageNumber: number; - pageSize: number; - paged: boolean; - unpaged: boolean; -} - -interface PageableList { - content: T[]; - pageable: PageableConfig; - sort: SortConfig; - totalPages: number; - totalElements: number; - number: number; - numberOfElements: number; - size: number; - first: boolean; - last: boolean; - empty: boolean; -} - const client = getApiClient(); export const getUserPopupList = async () => { - const response = await client.get>( + const response = await client.get( `${ENDPOINT.CMS_SERVICE.POPUPS}/user/list`, ); return response.data; diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 8457b28dd..5733c4168 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -11,7 +11,7 @@ import { getUserPopupList, getSiteInfo, getUserInfo, - type PopupInfo, + type Popup, } from "../../api"; import { DropdownMenu, @@ -50,7 +50,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { queryFn: () => getUserPopupList(), }); - const openPopup = useCallback(async (popup: PopupInfo) => { + const openPopup = useCallback(async (popup: Popup) => { try { const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`; const hasCookie = getCookie(cookieName); @@ -79,10 +79,10 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { }, []); useEffect(() => { - if (!userPopupList?.content?.length) return; + if (!userPopupList?.length) return; const timer = setTimeout(() => { - userPopupList.content.forEach((popup) => { + userPopupList.forEach((popup) => { openPopup(popup); }); }, 500); From d22034a237226c401a3b81b367a6e64fa146b130 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 24 Jun 2025 14:02:41 +0900 Subject: [PATCH 02/28] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0(loginlog=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(with-base-layout)/loginlog/page.tsx | 2 - .../pages/loginlog/api/loginlog-service.ts | 27 -- apps/cs/src/pages/loginlog/index.ts | 1 - .../loginlog/model/loginlog-interface.ts | 38 --- .../pages/loginlog/model/table-columns.tsx | 64 ---- .../src/pages/loginlog/ui/loginlog-dialog.tsx | 280 ------------------ .../src/pages/loginlog/ui/loginlog-page.tsx | 95 ------ 7 files changed, 507 deletions(-) delete mode 100644 apps/cs/app/(with-base-layout)/loginlog/page.tsx delete mode 100644 apps/cs/src/pages/loginlog/api/loginlog-service.ts delete mode 100644 apps/cs/src/pages/loginlog/index.ts delete mode 100644 apps/cs/src/pages/loginlog/model/loginlog-interface.ts delete mode 100644 apps/cs/src/pages/loginlog/model/table-columns.tsx delete mode 100644 apps/cs/src/pages/loginlog/ui/loginlog-dialog.tsx delete mode 100644 apps/cs/src/pages/loginlog/ui/loginlog-page.tsx diff --git a/apps/cs/app/(with-base-layout)/loginlog/page.tsx b/apps/cs/app/(with-base-layout)/loginlog/page.tsx deleted file mode 100644 index 960f03e05..000000000 --- a/apps/cs/app/(with-base-layout)/loginlog/page.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { LoginlogPage } from "@/pages/loginlog"; -export default LoginlogPage; diff --git a/apps/cs/src/pages/loginlog/api/loginlog-service.ts b/apps/cs/src/pages/loginlog/api/loginlog-service.ts deleted file mode 100644 index a813ba897..000000000 --- a/apps/cs/src/pages/loginlog/api/loginlog-service.ts +++ /dev/null @@ -1,27 +0,0 @@ -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.CMS_SERVICE.LOGINLOG}`, - { params }, - ); - return response.data; - } - - public async getLoginlogInfo(lgnSn: string): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.LOGINLOG}/${lgnSn}`, - ); - return response.data; - } -} - -export const LoginlogApi = new LoginlogApiService(); diff --git a/apps/cs/src/pages/loginlog/index.ts b/apps/cs/src/pages/loginlog/index.ts deleted file mode 100644 index 4e5e70ea8..000000000 --- a/apps/cs/src/pages/loginlog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginlogPage } from "./ui/loginlog-page"; diff --git a/apps/cs/src/pages/loginlog/model/loginlog-interface.ts b/apps/cs/src/pages/loginlog/model/loginlog-interface.ts deleted file mode 100644 index 37d6a33b9..000000000 --- a/apps/cs/src/pages/loginlog/model/loginlog-interface.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z } from "zod"; -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/pages/loginlog/model/table-columns.tsx b/apps/cs/src/pages/loginlog/model/table-columns.tsx deleted file mode 100644 index 298ee7d32..000000000 --- a/apps/cs/src/pages/loginlog/model/table-columns.tsx +++ /dev/null @@ -1,64 +0,0 @@ -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 { LoginlogDialog } from "../ui/loginlog-dialog"; - -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: "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 }) => ( -
e.stopPropagation()}> - -
- ), - }, -]; - -const LoginlogButton = ({ upCd }: { upCd: string }) => { - const [dialogOpen, setDialogOpen] = useState(false); - return ( - - - - ); -}; diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-dialog.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-dialog.tsx deleted file mode 100644 index 55aea3ac2..000000000 --- a/apps/cs/src/pages/loginlog/ui/loginlog-dialog.tsx +++ /dev/null @@ -1,280 +0,0 @@ -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 LoginlogDialogProps = { - upCd?: string; - children?: React.ReactNode; - open?: boolean; - onOpenChange?: (open: boolean) => void; -}; - -const LoginlogDialog = memo( - ({ - upCd, - children, - open, - onOpenChange, - ...props - }: LoginlogDialogProps) => { - 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({ - 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 ( - - {children} - - - - {isRegistration ? "상위코드 등록" : "상위코드 관리"} - - -
- - - - - - - - - 상위코드 - - ( - - - - - - )} - /> - - - - 상위코드명 - - ( - - - - - - )} - /> - - - - 상위코드설명 - - ( - - - - - - )} - /> - - - - 코드구분 - - ( - - - - - - )} - /> - - - - 사이트 - - ( - - - - - - )} - /> - - - - 사용여부 - - ( - - - -
- - -
-
- - -
-
-
-
- )} - /> -
-
-
-
- - - - - - -
- -
-
- ); - }, - (prevProps, nextProps) => { - return ( - prevProps.upCd === nextProps.upCd && prevProps.open === nextProps.open - ); - }, -); - -LoginlogDialog.displayName = "LoginlogDialog"; - -export { LoginlogDialog }; diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx deleted file mode 100644 index 01fcf07fd..000000000 --- a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -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 "../api/loginlog-service"; -import { type LoginlogParams} from "../model/loginlog-interface"; - -import { ENDPOINT } from "@/shared/config"; -import { columns } from "../model/table-columns"; -import { LoginlogDialog } from "./loginlog-dialog"; - -type LoginlogPageProps = { - lgnSn?: string; -}; - -function LoginlogPage({ lgnSn }: LoginlogPageProps) { - const [dialogOpen, setDialogOpen] = useState(false); - const [loginlogQuery, setLoginlogQuery] = useState({ - userId: "", - siteId: "", - }); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, - }); - - const { data: loginlogList, refetch } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, lgnSn, pagination], - queryFn: () => - LoginlogApi.getLoginlogList({ - ...loginlogQuery, - lgnSn, - page: pagination.pageIndex, - size: pagination.pageSize, - }), - enabled: !!lgnSn, - placeholderData: keepPreviousData, - }); - - const table = useReactTable({ - data: loginlogList?.content ?? [], - columns, - rowCount: loginlogList?.totalElements ?? 0, - state: { pagination }, - manualPagination: true, - enableMultiRowSelection: false, - getCoreRowModel: getCoreRowModel(), - onPaginationChange: setPagination, - getRowId: (row) => row.lwpoCd, - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - refetch(); - }; - - return ( -
-
-
- - - setLoginlogQuery({ - ...loginlogQuery, - userId: e.target.value, - }) - } - /> - -
-
-
-
-

하위코드 목록

-
-
- -
-
-
- ); -} - -export { LoginlogPage }; From 4696e2b710455189dfb9ac8ca7af6d293ddec1e0 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 24 Jun 2025 16:41:56 +0900 Subject: [PATCH 03/28] =?UTF-8?q?fix:=20=EA=B7=B8=EB=A3=B9=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20key=20=EC=A4=91=EB=B3=B5=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/groups/ui/group-users-section.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/cs/src/pages/groups/ui/group-users-section.tsx b/apps/cs/src/pages/groups/ui/group-users-section.tsx index 7b077a570..dc16fda60 100644 --- a/apps/cs/src/pages/groups/ui/group-users-section.tsx +++ b/apps/cs/src/pages/groups/ui/group-users-section.tsx @@ -57,7 +57,6 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) { getCoreRowModel: getCoreRowModel(), onPaginationChange: setPagination, onRowSelectionChange: setRowSelection, - getRowId: (row) => row.userUnqId, }, dataTableOptions: { viewPagination: true, @@ -65,15 +64,18 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) { }); const handleDeleteSelected = async () => { - const selectedRows = Object.keys(rowSelection); + const selectedRows = table.getSelectedRowModel().rows.map((row) => ({ + groupId: row.original.groupId, + userUnqId: row.original.userUnqId, + })); if (selectedRows.length === 0 || !groupId) return; if (!(await confirm("사용자를 삭제하시겠습니까?"))) return; try { - const usersToDelete = selectedRows.map((userUnqId) => ({ + const usersToDelete = selectedRows.map((row) => ({ mode: "D", - groupId, - userUnqId, + groupId: row.groupId, + userUnqId: row.userUnqId, })); await GroupUsersApi.saveGroupUser(usersToDelete); @@ -90,6 +92,7 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) { useEffect(() => { setPagination({ ...pagination, pageIndex: 0 }); + setRowSelection({}); }, [groupId]); return ( From 5aadc384410541073e7b0ac7349f1d0e693b44b0 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 25 Jun 2025 13:37:01 +0900 Subject: [PATCH 04/28] =?UTF-8?q?feat:=20ip=20=EA=B0=80=EB=93=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B3=B5=ED=86=B5=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/app/providers/app-provider.tsx | 47 +++++++------ apps/cs/src/pages/site/ui/modify-page.tsx | 15 +++-- apps/cs/src/pages/site/ui/site-form.tsx | 1 + packages/common/assets/src/images/index.ts | 1 + .../common/assets/src/images/logo_white.png | Bin 0 -> 1763 bytes packages/common/components/src/api/ip.ts | 62 ++++++++++++++++++ packages/common/components/src/config/api.ts | 1 + .../components/src/lib/client-ip-guard.tsx | 59 +++++++++++++++++ .../common/components/src/lib/use-pathname.ts | 43 ++++++++++++ .../common/components/src/ui/header/index.tsx | 9 ++- .../common/components/src/ui/layout/base.tsx | 35 +++++----- .../components/src/ui/sidebar/sidebar.css | 16 +++++ 12 files changed, 241 insertions(+), 48 deletions(-) create mode 100644 packages/common/assets/src/images/logo_white.png create mode 100644 packages/common/components/src/api/ip.ts create mode 100644 packages/common/components/src/lib/client-ip-guard.tsx create mode 100644 packages/common/components/src/lib/use-pathname.ts diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index b8fabff3d..1a2189b03 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -5,7 +5,6 @@ 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 { ClientIpGuard } from "@/entities/ip"; import { AuthWrapper } from "@/entities/auth"; import { MenuWrapper } from "@/entities/menu"; import { PromptProvider, StoreProvider } from "@/shared/providers"; @@ -17,30 +16,28 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); return ( - - - - - - - - {children} - - - - - - - - - -
- -
-
+ + + + + + + {children} + + + + + + + + + +
+ +
); }; diff --git a/apps/cs/src/pages/site/ui/modify-page.tsx b/apps/cs/src/pages/site/ui/modify-page.tsx index 0f36bc58d..fdc40c477 100644 --- a/apps/cs/src/pages/site/ui/modify-page.tsx +++ b/apps/cs/src/pages/site/ui/modify-page.tsx @@ -18,12 +18,8 @@ function ModifyPage({ siteId }: { siteId: string }) { queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId], queryFn: () => SiteApi.siteDetail(siteId), select: (data) => { - const keys = Object.keys(siteFormSchema.shape); - const filteredData = Object.fromEntries( - Object.entries(data).filter(([key]) => keys.includes(key)), - ); return { - ...filteredData, + ...data, faxNo: data.faxNumber, bscSiteYn: data.basicSiteYn, }; @@ -50,7 +46,14 @@ function ModifyPage({ siteId }: { siteId: string }) { const form = useForm({ resolver: zodResolver(siteFormSchema), - values: siteDetail as SiteFormData, + values: { + ...siteDetail, + siteId: siteDetail?.siteId ?? "", + siteNm: siteDetail?.siteNm ?? "", + siteKndCd: siteDetail?.siteKndCd ?? "", + bscSiteYn: siteDetail?.bscSiteYn ?? "", + useYn: siteDetail?.useYn ?? "", + }, }); return ( diff --git a/apps/cs/src/pages/site/ui/site-form.tsx b/apps/cs/src/pages/site/ui/site-form.tsx index c76f23ae5..ffd03e7f4 100644 --- a/apps/cs/src/pages/site/ui/site-form.tsx +++ b/apps/cs/src/pages/site/ui/site-form.tsx @@ -82,6 +82,7 @@ function SiteForm({ form, siteId, ...props }: SiteFormProps) { diff --git a/packages/common/assets/src/images/index.ts b/packages/common/assets/src/images/index.ts index 7abbeb673..e9626cef8 100644 --- a/packages/common/assets/src/images/index.ts +++ b/packages/common/assets/src/images/index.ts @@ -1,5 +1,6 @@ export const images = { logo: require("./logo.png"), + logoWhite: require("./logo_white.png"), menuBtn: require("./mob_menuBtn.svg"), menuBtnClose: require("./mob_menuBtn_close.svg"), iconFolderOff: require("./icon_folder_off.png"), diff --git a/packages/common/assets/src/images/logo_white.png b/packages/common/assets/src/images/logo_white.png new file mode 100644 index 0000000000000000000000000000000000000000..e76dd65d5797a8408b751c9f4d97c499a3acffa9 GIT binary patch literal 1763 zcmV<91|0c`P)JKf&)5zummZyo9JzT*smm2Xzv1&%=FP;s$jj`VEPW zG44}Q`X;Ea^cFSk^!7?p{B@aULK~;AEje(4yzKL`!1deVx43!Pp+vYBq3#6NTl|jD zz7Rpm0_`1gFCu-?BD6Z3c%;30?+VWx=9!0Q62B+7mbg~>sy+e~b?p)T^yeUIC~+206lH z7Xv!R0CTd&0DEMB*BWqK^fT1_oOev%2zn`|bMgM~U^vPeO7}kTi@5hjhVP#e;0>Vn z9QFH#b}62n3OFn18ebdZ%*jNrF&I-Q$~k`?VU!gn)Wf6-;#jM?BVqlD`s zVV;5=eDtFn@>DZ989;{G?vR;%XXENxVbW)SCc6W8y1b)KrO^Ttr+jwrtTh27uBVaQ-YA z0yNGMePoDuF9M#2+Z=W-K0srVlmsR{hLMzM-dxh(xiL<+;=%vom?nhtOBp1BNmD@K z{7#Af-n#}47gF{u+qZ2y5oIDymyNJb4oW!oZ7F{$%B_QiAsn}1kddKZUABxfog&b% zb*debcA#wZWawjIh>mF>64roy@t-z^)V^)%xlLOKTLM+$TJ$3i$BV4!E3zDeprYTp zdRuKf#W{2itKD?k_;s3k!x=SSH5)PcmYBm2pcyWo$56UQ6BHM8mC;uAO5S z)NT`XNMX*Fdd!E?#GkEn8IjC2u)m7qcG1V8+zz7PY}2Pols;W6a5$Do0Fa4xe^vn; z$KbDoSIjse8G{1k%Om?!dx=AB!A&jOddy;6i89gI3OKQ$pqiLP}nZ9XaF?W$g zovNfPmuw8q?c>Z0xs#(>=+dWKb>Sg==W838p( zDz;^?Dred6+BuenmyRnYQH9@gWqCSTPbFpiDt<*7+;1VBj1I}e7unhOV!_4gnV3y)`SpAP@ zA0Np)*N|U%BKez8T)}^H-hgvmqE0q?S`-;yo1%XUFaX!cS>jUu2gv{c002ovPDHLk FV1i44L#_Y- literal 0 HcmV?d00001 diff --git a/packages/common/components/src/api/ip.ts b/packages/common/components/src/api/ip.ts new file mode 100644 index 000000000..398f5ac1d --- /dev/null +++ b/packages/common/components/src/api/ip.ts @@ -0,0 +1,62 @@ +import { getApiClient } from "./client"; +import { ENDPOINT } from "../config"; + +interface PageableParams { + page?: number; + size?: number; + sort?: string[]; +} + +export interface IpParams extends PageableParams { + siteId: string; + ipAddr?: string; + prmYn?: string; +} + +export interface IP { + mngNo: number; + siteId: string; + ipAddr: string; + prmYn: string; + mode: string; +} + +interface SortConfig { + empty: boolean; + sorted: boolean; + unsorted: boolean; +} + +interface PageableConfig { + sort: SortConfig; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; +} + +interface PageableList { + content: T[]; + pageable: PageableConfig; + sort: SortConfig; + totalPages: number; + totalElements: number; + number: number; + numberOfElements: number; + size: number; + first: boolean; + last: boolean; + empty: boolean; +} + +export type IpList = PageableList; + +const client = getApiClient(); + +export const getIpList = async (params: IpParams) => { + const response = await client.get(`${ENDPOINT.CMS_SERVICE.IPS}`, { + params, + }); + return response.data; +}; diff --git a/packages/common/components/src/config/api.ts b/packages/common/components/src/config/api.ts index 2b2612ffb..12efdd8bd 100644 --- a/packages/common/components/src/config/api.ts +++ b/packages/common/components/src/config/api.ts @@ -3,6 +3,7 @@ export const ENDPOINT = { MENU_ROLES: "/cms-service/api/v1/menu-roles", SITES: "/cms-service/api/v1/sites", POPUPS: "/cms-service/api/v1/popups", + IPS: "/cms-service/api/v1/site-ips", }, USER_SERVICE: { USERS: "/user-service/api/v1/users", diff --git a/packages/common/components/src/lib/client-ip-guard.tsx b/packages/common/components/src/lib/client-ip-guard.tsx new file mode 100644 index 000000000..e6b30fe81 --- /dev/null +++ b/packages/common/components/src/lib/client-ip-guard.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { getConfig } from "@common/config"; +import { getSiteInfo } from "../api/site"; +import { getIpList } from "../api/ip"; +import { ENDPOINT } from "../config"; + +const { SITE_ID } = getConfig(); + +const ClientIpGuard = ({ + currentPath, + children, +}: { + currentPath?: string; + children: React.ReactNode; +}) => { + const queryClient = useQueryClient(); + const [clientIp, setClientIp] = useState(""); + const [isAllowed, setIsAllowed] = useState(true); + + useEffect(() => { + (async () => { + const response = await fetch("https://api.ipify.org?format=json"); + const data = await response.json(); + if (!data?.ip) return; + setClientIp(data.ip); + })(); + }, []); + + useEffect(() => { + (async () => { + if (!SITE_ID || !clientIp) return; + + const siteInfo = await queryClient.ensureQueryData({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID], + queryFn: () => getSiteInfo(SITE_ID), + }); + + if (siteInfo.siteKndCd === "O") return; + + const { content: ipList } = await getIpList({ + siteId: SITE_ID, + prmYn: "Y", + }); + + const allowedClientIp = ipList.some((ip) => ip.ipAddr === clientIp); + setIsAllowed(allowedClientIp); + })(); + }, [queryClient, clientIp, currentPath]); + + if (!isAllowed) { + alert("허용되지 않은 IP로 접근하였습니다."); + return <>; + } + + return <>{children}; +}; + +export { ClientIpGuard }; diff --git a/packages/common/components/src/lib/use-pathname.ts b/packages/common/components/src/lib/use-pathname.ts new file mode 100644 index 000000000..9e14c0d89 --- /dev/null +++ b/packages/common/components/src/lib/use-pathname.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; + +const initHistoryPatch = (() => { + let initialized = false; + return () => { + if (initialized) return; + initialized = true; + + const patch = (method: "pushState" | "replaceState") => { + const original = history[method]; + history[method] = function (...args) { + const result = original.apply(this, args); + window.dispatchEvent(new Event("locationchange")); + return result; + }; + }; + + patch("pushState"); + patch("replaceState"); + }; +})(); + +export const usePathname = () => { + const [pathname, setPathname] = useState(""); + + useEffect(() => { + initHistoryPatch(); + + const update = () => { + queueMicrotask(() => setPathname(window.location.pathname)); + }; + + window.addEventListener("popstate", update); + window.addEventListener("locationchange", update); + + return () => { + window.removeEventListener("popstate", update); + window.removeEventListener("locationchange", update); + }; + }, []); + + return pathname; +}; diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 5733c4168..8852d29f0 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -78,6 +78,13 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { } }, []); + const getLogoImage = () => { + const siteThemeKey = getCookie("site-theme-key"); + if (siteThemeKey === "blue" || siteThemeKey === "green") + return images.logoWhite; + return images.logo; + }; + useEffect(() => { if (!userPopupList?.length) return; @@ -98,7 +105,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {

- logo + logo {siteInfo?.siteNm} diff --git a/packages/common/components/src/ui/layout/base.tsx b/packages/common/components/src/ui/layout/base.tsx index f5db1c50a..6eb1322b0 100644 --- a/packages/common/components/src/ui/layout/base.tsx +++ b/packages/common/components/src/ui/layout/base.tsx @@ -1,6 +1,7 @@ import type { QueryClient } from "@tanstack/react-query"; import { QueryProvider } from "../../provider/query-provider"; import { PopupProvider } from "../../provider/popup-provider"; +import { ClientIpGuard } from "../../lib/client-ip-guard"; import type { MenuItem } from "../../api"; import { SidebarProvider } from "../sidebar"; import { LNB } from "../lnb"; @@ -21,25 +22,27 @@ export function BaseLayout({ }) { return ( - - -
-
-
- -
-
- {children} + + + +
+
+
+ +
+
+ {children} +
+
-
-
- - + + + ); } diff --git a/packages/common/components/src/ui/sidebar/sidebar.css b/packages/common/components/src/ui/sidebar/sidebar.css index b41ea1ace..b7ed79d73 100644 --- a/packages/common/components/src/ui/sidebar/sidebar.css +++ b/packages/common/components/src/ui/sidebar/sidebar.css @@ -36,6 +36,14 @@ [data-sidebar="trigger"] > svg { color: #fff !important; } + [data-radix-popper-content-wrapper] > * { + color: #0a0a0a !important; + background: #fff !important; + } + [data-radix-popper-content-wrapper] > *:hover { + color: #0a0a0a !important; + background: #fff !important; + } } [data-theme="red"] { @@ -78,5 +86,13 @@ [data-sidebar="trigger"] > svg { color: #fff !important; } + [data-radix-popper-content-wrapper] > * { + color: #0a0a0a !important; + background: #fff !important; + } + [data-radix-popper-content-wrapper] > *:hover { + color: #0a0a0a !important; + background: #fff !important; + } } } From b126d62f42ddb342831bbb51770805853accf3e6 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 25 Jun 2025 16:06:32 +0900 Subject: [PATCH 05/28] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=ED=8F=BC=20url=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/widgets/post/ui/post-page.tsx | 2 +- apps/cs/src/pages/menu/ui/type-menu-form.tsx | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) 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/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx index 1f0483d1b..ebf5988e6 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, @@ -43,6 +43,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); }; @@ -91,7 +95,9 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { )} - {(menuType === "INSIDE" || menuType === "OUTSIDE") && ( + {(menuType === "BOARD" || + menuType === "INSIDE" || + menuType === "OUTSIDE") && ( 연결 URL @@ -106,10 +112,13 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { value={field.value ?? ""} className="!text-[1.3rem]" placeholder={ - menuType === "INSIDE" - ? "메뉴에 연결된 페이지의 경로를 입력하십시오." - : "'http://' 또는 'https://'를 포함하여 입력하십시오." + menuType === "BOARD" + ? "게시판 선택 시 자동으로 입력됩니다." + : menuType === "INSIDE" + ? "메뉴에 연결된 페이지의 경로를 입력하십시오." + : "'http://' 또는 'https://'를 포함하여 입력하십시오." } + disabled={menuType === "BOARD"} /> From 749176a37b1a92ae6dbc27048b3b61477e3fbac9 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 25 Jun 2025 16:30:25 +0900 Subject: [PATCH 06/28] =?UTF-8?q?feat:=20=EB=A9=94=EB=89=B4=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=8B=9C=20menu-role=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20invalidate=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/menu/ui/menu-page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index 6ff6f89bc..e8b92f905 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], + }); }, }); From c667d5b0a18ceda2fa4c3330fba26f50d75dfe70 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 26 Jun 2025 14:12:11 +0900 Subject: [PATCH 07/28] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?qrcode=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/features/package.json | 1 - apps/cs/next.config.ts | 1 + apps/cs/package.json | 2 +- apps/cs/src/pages/menu/ui/menu-page.tsx | 4 +- apps/cs/src/pages/menu/ui/qrcode-dialog.tsx | 81 +++++++++++ apps/cs/src/pages/menu/ui/type-menu-form.tsx | 74 +++++----- packages/common/components/src/ui/index.ts | 1 + pnpm-lock.yaml | 143 +++++++++++++++++-- 8 files changed, 263 insertions(+), 44 deletions(-) create mode 100644 apps/cs/src/pages/menu/ui/qrcode-dialog.tsx 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/cs/next.config.ts b/apps/cs/next.config.ts index efcddfcd5..3b3038b09 100644 --- a/apps/cs/next.config.ts +++ b/apps/cs/next.config.ts @@ -4,6 +4,7 @@ const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000"; const nextConfig: NextConfig = { + 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/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index e8b92f905..d212dc970 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -127,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 ebf5988e6..3e1277fea 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -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); }; @@ -56,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 ( @@ -95,7 +105,8 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { )} - {(menuType === "BOARD" || + {(menuType === "CONTENTS" || + menuType === "BOARD" || menuType === "INSIDE" || menuType === "OUTSIDE") && ( @@ -111,14 +122,10 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { {...field} value={field.value ?? ""} className="!text-[1.3rem]" - placeholder={ - menuType === "BOARD" - ? "게시판 선택 시 자동으로 입력됩니다." - : menuType === "INSIDE" - ? "메뉴에 연결된 페이지의 경로를 입력하십시오." - : "'http://' 또는 'https://'를 포함하여 입력하십시오." + placeholder={getUrlPathPlaceholder()} + disabled={ + menuType === "CONTENTS" || menuType === "BOARD" } - disabled={menuType === "BOARD"} /> @@ -298,31 +305,34 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { QR코드 - ( - - - -
-
- - +
+ ( + + + +
+
+ + +
+
+ + +
-
- - -
-
- - - - )} - /> + + + + )} + /> + +
diff --git a/packages/common/components/src/ui/index.ts b/packages/common/components/src/ui/index.ts index 37cc10704..8839d2298 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -28,3 +28,4 @@ export * from "./switch"; export * from "./table"; export * from "./tabs"; export * from "./textarea"; +export * from "./tooltip"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c9b54a0c..a71d8c004 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,9 +124,6 @@ importers: '@eslint/eslintrc': specifier: ^3 version: 3.3.0 - '@faker-js/faker': - specifier: ^9.5.0 - version: 9.5.0 '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -238,6 +235,9 @@ importers: next: specifier: 15.3.0 version: 15.3.0(react-dom@18.3.1)(react@18.3.1) + next-qrcode: + specifier: ^2.5.1 + version: 2.5.1(react@18.3.1) react-chartjs-2: specifier: ^5.3.0 version: 5.3.0(chart.js@4.4.8)(react@18.3.1) @@ -263,9 +263,6 @@ importers: '@eslint/eslintrc': specifier: ^3 version: 3.3.0 - '@faker-js/faker': - specifier: ^9.5.0 - version: 9.5.0 '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -5185,6 +5182,11 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: false + /caniuse-lite@1.0.30001701: resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} @@ -5271,6 +5273,14 @@ packages: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -5545,6 +5555,11 @@ packages: ms: 2.1.3 dev: true + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: false + /decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} dev: false @@ -5695,6 +5710,10 @@ packages: /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + /dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dev: false + /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -6509,6 +6528,14 @@ packages: dependencies: to-regex-range: 5.0.1 + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: false + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -6649,7 +6676,6 @@ packages: /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - dev: true /get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} @@ -7316,6 +7342,13 @@ packages: engines: {node: '>=6.11.5'} dev: true + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: false + /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -7560,6 +7593,16 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true + /next-qrcode@2.5.1(react@18.3.1): + resolution: {integrity: sha512-ibiS1+p+myjurC1obVIgo7UCjFhxm0SNYTkikahQVmnyzlfREOdUCf2f77Vbz6W6q2zfx5Eww2/20vbgkLNdLw==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + react: ^18.3.1 + dependencies: + qrcode: 1.5.4 + react: 18.3.1 + dev: false + /next@15.1.7(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -7794,6 +7837,13 @@ packages: safe-push-apply: 1.0.0 dev: true + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: false + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -7801,6 +7851,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: false + /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -7808,6 +7865,11 @@ packages: p-limit: 3.1.0 dev: true + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: false + /package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -7843,7 +7905,6 @@ packages: /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -7917,6 +7978,11 @@ packages: engines: {node: '>=4'} dev: true + /pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + dev: false + /polished@4.3.1: resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} engines: {node: '>=10'} @@ -8189,6 +8255,16 @@ packages: engines: {node: '>=6'} dev: true + /qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -8596,13 +8672,16 @@ packages: /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - dev: true /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} dev: true + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: false + /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: false @@ -8763,6 +8842,10 @@ packages: randombytes: 2.1.0 dev: true + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -10008,6 +10091,10 @@ packages: is-weakset: 2.0.4 dev: true + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: false + /which-typed-array@1.1.18: resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} engines: {node: '>= 0.4'} @@ -10046,6 +10133,15 @@ packages: engines: {node: '>=0.8'} dev: false + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -10102,6 +10198,10 @@ packages: resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==} dev: false + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -10116,11 +10216,36 @@ packages: engines: {node: '>= 14'} hasBin: true + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} dev: true + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: false + /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} From 9779934f0847f4f04db0f5821c02f0ef6c02dd04 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 26 Jun 2025 14:18:38 +0900 Subject: [PATCH 08/28] =?UTF-8?q?feat:=20QR=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=85=B8=EC=B6=9C=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/menu/ui/type-menu-form.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 3e1277fea..8cbd7b8b0 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -305,11 +305,11 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { QR코드 -
- ( + ( +
- )} - /> - -
+ {field.value === "Y" && ( + + )} +
+ )} + />
From 78724e9d61bd4271251f7ce8e594bab0a5076fa6 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 26 Jun 2025 17:26:28 +0900 Subject: [PATCH 09/28] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=82=AC=EC=9C=A0=20=EC=85=80=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/app/providers/app-provider.tsx | 4 +- apps/cs/src/entities/post/index.ts | 1 + .../post/model/delete-reason-messages.ts | 11 +++ apps/cs/src/pages/(post)/faq/ui/faq-page.tsx | 16 ++++- apps/cs/src/pages/recent/ui/recent-page.tsx | 9 ++- apps/cs/src/shared/lib/index.ts | 1 + apps/cs/src/shared/lib/use-select-prompt.tsx | 48 +++++++++++++ .../src/shared/providers/prompt-provider.tsx | 26 ++++++- apps/cs/src/shared/ui/prompt-dialog.tsx | 6 +- .../cs/src/shared/ui/select-prompt-dialog.tsx | 68 +++++++++++++++++++ apps/cs/src/widgets/post/ui/post-action.tsx | 9 ++- apps/cs/src/widgets/post/ui/post-page.tsx | 9 ++- 12 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 apps/cs/src/entities/post/model/delete-reason-messages.ts create mode 100644 apps/cs/src/shared/lib/use-select-prompt.tsx create mode 100644 apps/cs/src/shared/ui/select-prompt-dialog.tsx 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/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/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/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} +

+
+ +