diff --git a/apps/cs/app/(with-base-layout)/search/page.tsx b/apps/cs/app/(with-base-layout)/search/page.tsx new file mode 100644 index 000000000..2b74c1283 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/search/page.tsx @@ -0,0 +1,2 @@ +import { SearchPage } from "@/pages/search"; +export default SearchPage; diff --git a/apps/cs/src/pages/search/api/search-service.ts b/apps/cs/src/pages/search/api/search-service.ts new file mode 100644 index 000000000..5bb99e7d1 --- /dev/null +++ b/apps/cs/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/cs/src/pages/search/index.ts b/apps/cs/src/pages/search/index.ts new file mode 100644 index 000000000..c5e44be12 --- /dev/null +++ b/apps/cs/src/pages/search/index.ts @@ -0,0 +1 @@ +export { SearchPage } from "./ui/search-page"; diff --git a/apps/cs/src/pages/search/model/search-interface.ts b/apps/cs/src/pages/search/model/search-interface.ts new file mode 100644 index 000000000..7afb1e9fc --- /dev/null +++ b/apps/cs/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/cs/src/pages/search/ui/search-page.tsx b/apps/cs/src/pages/search/ui/search-page.tsx new file mode 100644 index 000000000..23a70b85e --- /dev/null +++ b/apps/cs/src/pages/search/ui/search-page.tsx @@ -0,0 +1,240 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { + Button, + Input, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@common/components"; +import { ENDPOINT, SITE_ID } from "@/shared/config"; +import { SearchApi } from "../api/search-service"; +import type { Board, Post, Contents } from "../model/search-interface"; + +const RENDER_LIMIT = 5; + +function SearchPage() { + 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: SITE_ID }), + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + setKeyword(formData.get("keyword") as string); + }; + + 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/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index ce6f61381..5f79ff2da 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -63,6 +63,7 @@ export const ENDPOINT = { POSTS: "/board-service/api/v1/posts", POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", POST_EVALUATION: "/board-service/api/v1/pstEvl", + SEARCH_POST: "/board-service/api/v1/isearch/pst", }, };