diff --git a/apps/common/features/app/(with-base-layout)/search/layout.tsx b/apps/common/features/app/(with-base-layout)/search/layout.tsx new file mode 100644 index 000000000..7fc1dfc1f --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/search/layout.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { MenuApi } from "@/entities/menu"; +import { useMenuStore } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { PageLayout } from "@/shared/ui/page-layout"; + +export default function SearchLayout({ + children, +}: { + children: React.ReactNode; +}) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + const { data } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENUS, currentMenuInfo?.menuCd], + queryFn: () => + MenuApi.getMenuInfo(currentMenuInfo?.menuCd.toString() ?? ""), + enabled: !!currentMenuInfo, + }); + const menuPaths = data?.menuPath?.split(">").map((path) => path.trim()); + + return ( + + {children} + + ); +} diff --git a/apps/common/features/app/(with-base-layout)/search/page.tsx b/apps/common/features/app/(with-base-layout)/search/page.tsx new file mode 100644 index 000000000..2b74c1283 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/search/page.tsx @@ -0,0 +1,2 @@ +import { SearchPage } from "@/pages/search"; +export default SearchPage; diff --git a/apps/common/features/middleware.ts b/apps/common/features/middleware.ts index 018aab11a..e428dfa74 100644 --- a/apps/common/features/middleware.ts +++ b/apps/common/features/middleware.ts @@ -13,7 +13,6 @@ export function middleware(req: NextRequest) { const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ queryParams ? `?${queryParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } @@ -26,9 +25,6 @@ export function middleware(req: NextRequest) { if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); - console.log( - `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, - ); return NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, }); diff --git a/apps/common/features/src/pages/search/api/search-service.ts b/apps/common/features/src/pages/search/api/search-service.ts new file mode 100644 index 000000000..5bb99e7d1 --- /dev/null +++ b/apps/common/features/src/pages/search/api/search-service.ts @@ -0,0 +1,21 @@ +import { client } from "@/shared/api"; +import type { + SearchParams, + SearchResult, + SearchService, +} from "../model/search-interface"; +import { ENDPOINT } from "@/shared/config"; + +export class SearchApiService implements SearchService { + public async searchPost(params: SearchParams): Promise { + const response = await client.get( + ENDPOINT.BOARD_SERVICE.SEARCH_POST, + { + params, + }, + ); + return response.data; + } +} + +export const SearchApi = new SearchApiService(); diff --git a/apps/common/features/src/pages/search/index.ts b/apps/common/features/src/pages/search/index.ts new file mode 100644 index 000000000..c5e44be12 --- /dev/null +++ b/apps/common/features/src/pages/search/index.ts @@ -0,0 +1 @@ +export { SearchPage } from "./ui/search-page"; diff --git a/apps/common/features/src/pages/search/model/search-interface.ts b/apps/common/features/src/pages/search/model/search-interface.ts new file mode 100644 index 000000000..7afb1e9fc --- /dev/null +++ b/apps/common/features/src/pages/search/model/search-interface.ts @@ -0,0 +1,45 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface SearchParams extends PageableParams { + keywordType?: string; + keyword?: string; + siteId?: string; +} + +export interface Post { + bbsId: number; + bbsNm: string; + pstNo: number; + pstTtl: string; + pstCn: string; + inqNocs: number; + wrtrNm: string; + wrtrCplc: string; + regDt: string; + rgtrNm: string; +} + +export interface Board { + bbsId: number; + bbsNm: string; + fullMenuPath: string; + urlPath: string; + posts: Post[]; +} + +export interface Contents { + contsSn: number; + contsTtl: string; + contsCn: string; + fullMenuPath: string; + urlPath: string; +} + +export interface SearchResult { + boardList: PageableList; + contsList: Contents[]; +} + +export interface SearchService { + searchPost(params: SearchParams): Promise; +} diff --git a/apps/common/features/src/pages/search/ui/search-page.tsx b/apps/common/features/src/pages/search/ui/search-page.tsx new file mode 100644 index 000000000..407d2301c --- /dev/null +++ b/apps/common/features/src/pages/search/ui/search-page.tsx @@ -0,0 +1,247 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { + Button, + Input, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { SearchApi } from "../api/search-service"; +import type { Board, Post, Contents } from "../model/search-interface"; +import { getCookie } from "@/shared/lib"; + +const RENDER_LIMIT = 5; +const siteId = getCookie("site-id-key") ?? ""; + +function SearchPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const keywordParam = searchParams?.get("keyword"); + + const [keyword, setKeyword] = useState(keywordParam ?? ""); + const [tab, setTab] = useState("all"); + + const { data } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.SEARCH_POST, keyword], + queryFn: () => SearchApi.searchPost({ keyword: keyword, siteId }), + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + setKeyword(formData.get("keyword") as string); + + const params = new URLSearchParams(searchParams?.toString()); + params.set("keyword", formData.get("keyword") as string); + router.replace(`?${params.toString()}`); + }; + + return ( +
+

통합검색

+
+ + + + +
+
+

+ 검색어 '{keyword}'로 + 검색한 결과입니다. +

+
+
+ +
+ + + 전체 ( + {data?.contsList?.length ?? + 0 + (data?.boardList?.totalElements ?? 0)} + 건) + + + 웹페이지 ({data?.contsList?.length ?? 0}건) + + + 게시판 ({data?.boardList?.totalElements ?? 0}건) + + +
+ +
+
+

+ 웹페이지 ({data?.contsList?.length ?? 0}건) +

+ {renderContents(data?.contsList)} + {(data?.contsList?.length ?? 0) > RENDER_LIMIT && ( + + )} +
+
+

+ 게시판 ({data?.boardList?.totalElements ?? 0}건) +

+ {renderPost(data?.boardList.content)} + {(data?.boardList?.totalElements ?? 0) > RENDER_LIMIT && ( + + )} +
+
+
+ + {renderContents(data?.contsList, 100)} + + + {renderPost(data?.boardList.content, 100)} + +
+
+
+
+ ); +} + +interface ContentItemProps { + content: Contents; +} + +function ContentItem({ content }: ContentItemProps) { + return ( +
  • + + {content.fullMenuPath} +
    + +

    +

    + +
  • + ); +} + +function renderContents(contents?: Contents[], limit = RENDER_LIMIT) { + if (!contents || !contents.length) return null; + return ( + <> + {contents.map((content, index) => { + if (limit <= index) return null; + + return ( +
      + +
    + ); + })} + + ); +} + +interface PostItemProps { + post: Post; + board: Board; +} + +function PostItem({ post, board }: PostItemProps) { + const postUrl = !!board.urlPath ? `${board.urlPath}/${post.pstNo}` : ""; + return ( +
  • + + {board.fullMenuPath} +
    + +

    +

    + +
  • + ); +} + +function renderPost(boards?: Board[], limit = RENDER_LIMIT) { + if (!boards || !boards.length) return null; + + let remainingPosts = limit; + + return ( + <> + {boards.map((board) => { + if (remainingPosts <= 0) return null; + + const postsToShow = Math.min(board.posts.length, remainingPosts); + if (postsToShow <= 0) return null; + + remainingPosts -= postsToShow; + + return ( +
      + {board.posts.slice(0, postsToShow).map((post) => ( + + ))} +
    + ); + })} + + ); +} + +export { SearchPage }; diff --git a/apps/common/features/src/shared/config/api.ts b/apps/common/features/src/shared/config/api.ts index 4fef249d5..67b8948d7 100644 --- a/apps/common/features/src/shared/config/api.ts +++ b/apps/common/features/src/shared/config/api.ts @@ -44,6 +44,7 @@ export const ENDPOINT = { POSTS_USER: "/board-service/api/v1/posts/user", POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", POST_EVALUATION: "/board-service/api/v1/pstEvl", + SEARCH_POST: "/board-service/api/v1/isearch/pst", }, }; diff --git a/apps/common/features/src/widgets/post/ui/post-action.tsx b/apps/common/features/src/widgets/post/ui/post-action.tsx index a2160d2e9..695ddb721 100644 --- a/apps/common/features/src/widgets/post/ui/post-action.tsx +++ b/apps/common/features/src/widgets/post/ui/post-action.tsx @@ -12,6 +12,8 @@ type PostActionProps = { handleDelete: () => Promise; }; +const basePath = process.env.NODE_ENV === "production" ? "/cms" : ""; + function PostAction({ registerId, children, handleDelete }: PostActionProps) { const router = useRouter(); const pathname = usePathname(); @@ -19,6 +21,7 @@ function PostAction({ registerId, children, handleDelete }: PostActionProps) { const { copyText, copied } = useClipboard(); const bbsInfo = useMenuStore((state) => state.bbsInfo); const userInfo = useUserStore((state) => state.userInfo); + const postPageLink = `${basePath}/${bbsInfo?.bbsTypeCd.toLowerCase()}/${bbsInfo?.bbsId}`; const actionButtonAuthrt = userInfo?.adminRoleYn === "Y" || userInfo?.userUnqId === registerId; @@ -60,7 +63,7 @@ function PostAction({ registerId, children, handleDelete }: PostActionProps) { size="lg" color="white" type="button" - onClick={() => router.back()} + onClick={() => router.push(postPageLink)} > 목록 diff --git a/apps/common/features/src/widgets/post/ui/post.tsx b/apps/common/features/src/widgets/post/ui/post.tsx index 7c1c4c3b6..b8928a3e2 100644 --- a/apps/common/features/src/widgets/post/ui/post.tsx +++ b/apps/common/features/src/widgets/post/ui/post.tsx @@ -13,6 +13,7 @@ import { Skeleton, } from "@common/components"; import { AttachmentApi } from "@/entities/attachment"; +import { BoardApi } from "@/entities/board"; import { PostApi } from "@/entities/post"; import { useAlertStore, @@ -31,10 +32,17 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const alert = useAlertStore((state) => state.setMessage); const userInfo = useUserStore((state) => state.userInfo); const bbsInfo = useMenuStore((state) => state.bbsInfo); + const setBbsInfo = useMenuStore((state) => state.setBbsInfo); const [filename, setFilename] = useState(""); const [evlScr, setEvlScr] = useState(5); + const { data: boardInfo } = useQuery({ + queryKey: [bbsId], + queryFn: () => BoardApi.getBoardInfo(bbsId), + enabled: !bbsInfo, + }); + const { data: postData } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), @@ -142,6 +150,12 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { } }, [downloadResponse]); + useEffect(() => { + if (!boardInfo) return; + if (bbsInfo) return; + setBbsInfo(boardInfo); + }, [boardInfo, bbsInfo]); + return ( <> diff --git a/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts b/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts index 1897e39ba..03291223c 100644 --- a/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts +++ b/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts @@ -1,5 +1,5 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; +// import { client } from "@/shared/api"; +// import { ENDPOINT } from "@/shared/config"; import { type BoardStatisticsParams, type BoardStatistics, @@ -37,6 +37,7 @@ class BoardStatisticsApiService implements BoardStatisticsService { async getBoardStatistics( params: BoardStatisticsParams, ): Promise { + console.log("params: ", params); try { // 실제 API 연동 시 아래 코드 사용 // const response = await client.get( diff --git a/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts b/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts index 8ec52dabf..1f113475f 100644 --- a/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts +++ b/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts @@ -1,4 +1,7 @@ -import type { PageableList, PageableParams } from "@/shared/model"; +import type { + // PageableList, + PageableParams, +} from "@/shared/model"; export interface BoardStatisticsParams extends PageableParams { keywordType?: string; diff --git a/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx b/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx index d879c34d6..13dcbe812 100644 --- a/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx +++ b/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx @@ -26,7 +26,7 @@ import { SelectValue, } from "@common/components"; import { SiteSelect } from "@/entities/site"; -import { ENDPOINT } from "@/shared/config"; +// import { ENDPOINT } from "@/shared/config"; import { NoData } from "@/shared/ui/no-data"; import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; import { BoardStatisticsApi } from "../api/board-statistics-service"; diff --git a/package.json b/package.json index 89154a13d..f468722e7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "packages/*" ], "scripts": { - "build": "turbo run build", + "build": "turbo run build --filter=!dm", "build:common": "turbo run build --filter=@common/*", "dev": "turbo run dev", "dev:cs": "turbo run dev --filter=cs", diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index f7637b7f0..52028da32 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -22,6 +22,7 @@ import { import { Button } from "../button"; import { SidebarTrigger } from "../sidebar"; import { Skeleton } from "../skeleton"; +import { SearchForm } from "./search-form"; const appConfig = getConfig(); const siteId = appConfig.SITE_ID ?? ""; @@ -135,36 +136,39 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { - {mounted ? ( - isLoggedIn && ( - - - - - - 프로필 설정 -
    -

    최종접속일

    -

    {userInfo?.lastLoginDate}

    -
    -
    -

    접속 IP

    -

    -
    - 로그아웃 -
    -
    - ) - ) : ( - - )} +
    + + {mounted ? ( + isLoggedIn && ( + + + + + + 프로필 설정 +
    +

    최종접속일

    +

    {userInfo?.lastLoginDate}

    +
    +
    +

    접속 IP

    +

    +
    + 로그아웃 +
    +
    + ) + ) : ( + + )} +
    ); } diff --git a/packages/common/components/src/ui/header/search-form.tsx b/packages/common/components/src/ui/header/search-form.tsx new file mode 100644 index 000000000..66ffbadb1 --- /dev/null +++ b/packages/common/components/src/ui/header/search-form.tsx @@ -0,0 +1,32 @@ +import { Button } from "../button"; +import { Input } from "../input"; + +export function SearchForm() { + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const keyword = formData.get("keyword") as string; + + if (keyword && keyword.trim().length > 0) { + window.location.href = `/cms/search?keyword=${encodeURIComponent(keyword)}`; + } + }; + + return ( +
    + + + + ); +}