diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..6173321a6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +.next +Dockerfile +.dockerignore +.env* +apps/**/node_modules +packages/**/node_modules +apps/**/.next +**/.next/cache \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..4f7f87385 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,93 @@ +# ---------------------- # +# STEP 1: Base 설치 환경 # +# ---------------------- # +FROM node:20-alpine AS base + +WORKDIR /repo + +# pnpm 설치 (corepack) +RUN corepack enable && corepack prepare pnpm@10.12.1 --activate + +# 필요한 시스템 패키지 설치 (선택) +RUN apk add --no-cache libc6-compat + +# ---------------------- # +# STEP 2: 의존성 설치용 캐시 레이어 # +# ---------------------- # +FROM base AS deps + +# workspace 정의 복사 +COPY pnpm-workspace.yaml ./ +COPY package.json pnpm-lock.yaml ./ +COPY apps/cs/package.json ./apps/cs/ +COPY packages ./packages + +# 종속성 설치 (only production dependencies) +RUN rm -rf node_modules .next +RUN pnpm install --frozen-lockfile --node-linker=node-modules +#RUN pnpm install --frozen-lockfile --node-linker=node-modules +RUN ls -al . +#RUN pnpm install --prod + +# ---------------------- # +# STEP 3: 빌드 단계 # +# ---------------------- # +FROM base AS builder + +COPY --from=deps /repo /repo +WORKDIR /repo + +# 전체 소스 복사 +COPY . . + +RUN ls -al /repo/apps/cs/.next || true +RUN rm -rf /repo/apps/cs/.next +RUN ls -al /repo/apps/cs/ +# Next.js 빌드 +RUN cat /repo/apps/cs/next.config.ts || true +RUN pnpm --filter ./apps/cs... build + +#RUN ls -al .next/standalone +# 👉 실행에 필요한 모듈만 유지 +#RUN pnpm prune --prod + +# Next.js 빌드 내용 보기 +RUN ls -al ./apps/cs/ +RUN ls -al /repo/apps/cs/.next +RUN ls -al /repo/apps/cs/.next/chunks || true + +# web browser request zip format +#RUN find /repo/apps/cs/.next/static -type f -name '*.js' -exec gzip -k {} \; + +#RUN find ./apps/cs/.next -type f + +# ---------------------- # +# STEP 4: 최종 러너 단계 (최소 런타임) # +# ---------------------- # +FROM node:20-alpine AS runner + +WORKDIR /app + +# corepack 사용 설정 +#RUN corepack enable && corepack prepare pnpm@8.15.5 --activate + +# 환경변수 설정 +ENV NODE_ENV=production +ENV PORT=3000 + +# pnpm은 런타임에 필요 없음 → corepack 설치 생략 가능 + +# 빌드 결과만 복사 +RUN rm -rf .next +COPY --from=builder /repo/apps/cs/.next/standalone ./ +COPY --from=builder /repo/apps/cs/public ./public +COPY --from=builder /repo/apps/cs/.next/static ./apps/cs/.next/static +COPY --from=builder /repo/apps/cs/.env ./apps/cs/.env +COPY --from=builder /repo/apps/cs/package.json ./apps/cs/package.json +COPY --from=builder /repo/apps/cs/next.config.ts ./apps/cs/next.config.ts +# 헬스체크 등 필요 시 추가 가능 +#HEALTHCHECK --interval=30s --timeout=3s CMD wget --spider -q http://localhost:3000 || exit 1 + +EXPOSE 3000 +CMD ["node", "apps/cs/server.js"] +#CMD ["pnpm", "start"] diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsfaq/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/[pstNo]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsfaq/[bbsId]/[pstNo]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsfaq/[bbsId]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsfaq/[bbsId]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsfaq/[bbsId]/register/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/register/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsfaq/[bbsId]/register/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/[pstNo]/modify/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/[pstNo]/modify/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/[pstNo]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/[pstNo]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/register/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/register/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsimage/[bbsId]/register/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/[pstNo]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/register/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/register/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsmovie/[bbsId]/register/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/[pstNo]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/[pstNo]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/register/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnormal/[bbsId]/register/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnormal/[bbsId]/register/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/[pstNo]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/[pstNo]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/register/page.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/register/page.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/bbsnotice/[bbsId]/register/page.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/layout.tsx b/apps/common/features/app/(with-base-layout)/(pst)/layout.tsx similarity index 100% rename from apps/common/features/app/(with-base-layout)/pst/layout.tsx rename to apps/common/features/app/(with-base-layout)/(pst)/layout.tsx diff --git a/apps/common/features/app/api/proxy/[...url]/route.ts b/apps/common/features/app/api/proxy/[...url]/route.ts index d3092c5c1..5d81c93fb 100644 --- a/apps/common/features/app/api/proxy/[...url]/route.ts +++ b/apps/common/features/app/api/proxy/[...url]/route.ts @@ -4,7 +4,6 @@ import { CLAIM_NAME, REFRESH_TOKEN, SERVER_API_URL, - SITE_ID, } from "@/shared/config"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; @@ -15,9 +14,10 @@ async function handler( method: "GET" | "POST" | "PUT" | "DELETE", ) { const cookieStore = await cookies(); + const siteId = cookieStore.get("site-id-key")?.value ?? ""; const headers: Record = { "Content-Type": "application/json", - "X-Site-Id": SITE_ID, + "X-Site-Id": siteId, }; let body = {}; const isRefreshHandler = params.url.includes("refresh"); diff --git a/apps/common/features/next.config.ts b/apps/common/features/next.config.ts index cb651cdc0..34965cf32 100644 --- a/apps/common/features/next.config.ts +++ b/apps/common/features/next.config.ts @@ -1,5 +1,16 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = {}; +const nextConfig: NextConfig = { + async rewrites() { + return { + afterFiles: [ + { + source: "/cms/:path*", + destination: "/:path*", + }, + ], + }; + }, +}; export default nextConfig; 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/entities/menu/lib/menu-wrapper.tsx b/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx index 2855e5ef0..e3007f8be 100644 --- a/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx +++ b/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx @@ -4,10 +4,11 @@ import { useEffect } from "react"; import { usePathname, useRouter } from "next/navigation"; import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu"; import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; -import { SITE_ID } from "@/shared/config"; import { MenuApi } from "../api/menu-service"; import { getCurrentMenu } from "./utils"; +const siteId = getCookie("site-id-key") ?? ""; + const MenuWrapper = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); const pathname = usePathname(); @@ -17,7 +18,7 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { useMenuStore((state) => state); const getCurrentMenuInfo = async () => { - const menuRoles = await MenuApi.getMenuRoles(SITE_ID); + const menuRoles = await MenuApi.getMenuRoles(siteId); if (!menuRoles) return; return getCurrentMenu(menuRoles, pathname ?? ""); }; diff --git a/apps/common/features/src/shared/api/client.ts b/apps/common/features/src/shared/api/client.ts index 709bb44a5..9c41892c0 100644 --- a/apps/common/features/src/shared/api/client.ts +++ b/apps/common/features/src/shared/api/client.ts @@ -1,8 +1,10 @@ import { Client } from "@common/network"; import { BASE_URL } from "../config/api"; -import { SITE_ID } from "../config/env"; +import { getCookie } from "../lib"; + +const siteId = getCookie("site-id-key") ?? ""; export const client = Client.getInstance({ baseURL: BASE_URL, - siteId: SITE_ID, + siteId, }); diff --git a/apps/common/features/src/shared/config/env.ts b/apps/common/features/src/shared/config/env.ts index e8d3161eb..a68868d6b 100644 --- a/apps/common/features/src/shared/config/env.ts +++ b/apps/common/features/src/shared/config/env.ts @@ -2,7 +2,6 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "13000"; export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; -export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS"; export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; 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/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/app/api/auth/logout/route.ts b/apps/cs/app/api/auth/logout/route.ts new file mode 100644 index 000000000..fdcc9df7c --- /dev/null +++ b/apps/cs/app/api/auth/logout/route.ts @@ -0,0 +1,17 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { + ACCESS_TOKEN, + AUTH_USER_ID, + CLAIM_NAME, + REFRESH_TOKEN, +} from "@/shared/config"; + +export async function POST() { + const cookieStore = await cookies(); + cookieStore.delete(ACCESS_TOKEN); + cookieStore.delete(REFRESH_TOKEN); + cookieStore.delete(AUTH_USER_ID); + cookieStore.delete(CLAIM_NAME); + return NextResponse.json({ message: "Logged out" }, { status: 200 }); +} diff --git a/apps/cs/app/api/getClientIp/route.ts b/apps/cs/app/api/getClientIp/route.ts deleted file mode 100644 index ec9ff72d5..000000000 --- a/apps/cs/app/api/getClientIp/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(request: NextRequest) { - const ipAddress = - request.headers.get("x-forwarded-for")?.split(",")[0] || "unknown"; - - const ipv4Regex = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/; - const match = ipAddress.match(ipv4Regex); - - const normalizedIp = match ? match[1] : ipAddress; - - return NextResponse.json({ - originalIp: ipAddress, - normalizedIp: normalizedIp, - }); -} diff --git a/apps/cs/next.config.ts b/apps/cs/next.config.ts index efcddfcd5..ad1ec7949 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: [ @@ -14,15 +15,15 @@ const nextConfig: NextConfig = { { type: "header", key: "referer", - value: "(.*)/pst(.*)", + value: "(.*)/cms(.*)", }, ], }, ], afterFiles: [ { - source: "/pst/:path*", - destination: `${subDomain}/pst/:path*`, + source: "/cms/:path*", + destination: `${subDomain}/cms/:path*`, }, ], fallback: [], 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 b8fabff3d..137f32f80 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -5,42 +5,41 @@ 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"; 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(); return ( - - - - - - - - {children} - - - - - - - - - -
- -
-
+ + + + + + + {children} + + + + + + + + + + +
+ +
); }; diff --git a/apps/cs/src/entities/group-user/model/group-user-interface.ts b/apps/cs/src/entities/group-user/model/group-user-interface.ts index 38daf6fe0..8935bfacc 100644 --- a/apps/cs/src/entities/group-user/model/group-user-interface.ts +++ b/apps/cs/src/entities/group-user/model/group-user-interface.ts @@ -10,6 +10,7 @@ export interface GroupUserListResponseDto { mode: string; groupId: string; userUnqId: string; + userId: string; userName: string; ogdpSeCd: string; ogdpSeNm: string; diff --git a/apps/cs/src/entities/ip/index.ts b/apps/cs/src/entities/ip/index.ts index ef1c5be33..2d244138d 100644 --- a/apps/cs/src/entities/ip/index.ts +++ b/apps/cs/src/entities/ip/index.ts @@ -7,4 +7,3 @@ export { type IpList, type IP, } from "./model/ip-interface"; -export { ClientIpGuard } from "./lib/client-ip-guard"; diff --git a/apps/cs/src/entities/ip/lib/client-ip-guard.tsx b/apps/cs/src/entities/ip/lib/client-ip-guard.tsx deleted file mode 100644 index f3ae452ce..000000000 --- a/apps/cs/src/entities/ip/lib/client-ip-guard.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useEffect, useState } from "react"; -import { useQueryClient } from "@tanstack/react-query"; -import { SiteApi } from "@/entities/site/@x/ip"; -import { ENDPOINT, SITE_ID } from "@/shared/config"; -import { IpApi } from "../api/ip-service"; - -type ClientIpResponse = { - originalIp: string; - normalizedIp: string; -}; - -const ClientIpGuard = ({ children }: { children: React.ReactNode }) => { - const queryClient = useQueryClient(); - const [isAllowed, setIsAllowed] = useState(true); - - useEffect(() => { - (async () => { - const response = await fetch("/api/getClientIp"); - const { normalizedIp } = (await response.json()) as ClientIpResponse; - - if (!normalizedIp || !SITE_ID) return; - - const siteInfo = await queryClient.ensureQueryData({ - queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID], - queryFn: () => SiteApi.siteDetail(SITE_ID), - }); - - if (siteInfo.siteKndCd === "O") return; - - const { content: ipList } = await IpApi.getIpList({ - siteId: SITE_ID, - prmYn: "Y", - }); - - const allowedClientIp = ipList.some((ip) => ip.ipAddr === normalizedIp); - setIsAllowed(allowedClientIp); - })(); - }, []); - - if (!isAllowed) { - alert("허용되지 않은 IP로 접근하였습니다."); - return <>; - } - - return <>{children}; -}; - -export { ClientIpGuard }; 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/groups/model/group-users-table-columns.tsx b/apps/cs/src/pages/groups/model/group-users-table-columns.tsx index 8d1054aac..e525ac7ab 100644 --- a/apps/cs/src/pages/groups/model/group-users-table-columns.tsx +++ b/apps/cs/src/pages/groups/model/group-users-table-columns.tsx @@ -26,7 +26,7 @@ export const columns: ColumnDef[] = [ enableHiding: false, }, { - accessorKey: "groupId", + accessorKey: "userId", header: "사용자ID", }, { 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 ( 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/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..ac62e2520 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", `/cms/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", + `/cms/${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 14fdbab17..ec9fec78a 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/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/apps/cs/src/shared/api/client.ts b/apps/cs/src/shared/api/client.ts index 709bb44a5..cf37e6671 100644 --- a/apps/cs/src/shared/api/client.ts +++ b/apps/cs/src/shared/api/client.ts @@ -1,6 +1,9 @@ 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, diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index ac0f18eca..3159eb23c 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -58,6 +58,7 @@ export const ENDPOINT = { BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", POSTS: "/board-service/api/v1/posts", POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + POST_EVALUATION: "/board-service/api/v1/pstEvl", }, }; 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} +

+
+ +