[common/features] refactor: 통합 검색 기능 공통 기능으로 변경

This commit is contained in:
2025-08-02 02:42:20 +09:00
parent 98cc45162d
commit aabd810571
17 changed files with 126 additions and 43 deletions
@@ -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 (
<PageLayout title={data?.menuKornNm ?? ""} menuPaths={menuPaths ?? []}>
{children}
</PageLayout>
);
}
-4
View File
@@ -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,
});
@@ -12,11 +12,13 @@ import {
TabsList,
TabsTrigger,
} from "@common/components";
import { ENDPOINT, SITE_ID } from "@/shared/config";
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();
@@ -28,7 +30,7 @@ function SearchPage() {
const { data } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.SEARCH_POST, keyword],
queryFn: () => SearchApi.searchPost({ keyword: keyword, siteId: SITE_ID }),
queryFn: () => SearchApi.searchPost({ keyword: keyword, siteId }),
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
@@ -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",
},
};
@@ -12,6 +12,8 @@ type PostActionProps = {
handleDelete: () => Promise<void>;
};
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)}
>
목록
</Button>
@@ -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 (
<>
<PostAction registerId={postData?.rgtr} handleDelete={handleDelete}>
@@ -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<BoardStatistics> {
console.log("params: ", params);
try {
// 실제 API 연동 시 아래 코드 사용
// const response = await client.get<BoardStatistics>(
@@ -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;
@@ -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";
-1
View File
@@ -63,7 +63,6 @@ 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",
},
};
+1 -1
View File
@@ -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",
@@ -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 }) {
</span>
</h1>
</div>
{mounted ? (
isLoggedIn && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
data-trigger="dropdown"
>
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
{userInfo?.userName}
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">최종접속일</p>
<p>{userInfo?.lastLoginDate}</p>
</div>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">접속 IP</p>
<p></p>
</div>
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
) : (
<Skeleton className="mx-4 h-[3rem] w-[15rem]" />
)}
<div className="flex items-center gap-2">
<SearchForm />
{mounted ? (
isLoggedIn && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
data-trigger="dropdown"
>
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
{userInfo?.userName}
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">최종접속일</p>
<p>{userInfo?.lastLoginDate}</p>
</div>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">접속 IP</p>
<p></p>
</div>
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
) : (
<Skeleton className="mx-4 h-[3rem] w-[15rem]" />
)}
</div>
</header>
);
}
@@ -0,0 +1,32 @@
import { Button } from "../button";
import { Input } from "../input";
export function SearchForm() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
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 (
<form onSubmit={handleSubmit} className="relative flex items-center gap-2">
<Input
type="text"
name="keyword"
className="h-[3.6rem] w-96 rounded-[3.6rem] pl-4 pr-12"
placeholder="검색어를 입력해주세요."
/>
<Button
type="submit"
size="icon"
className="absolute right-[0.4rem] top-[0.4rem] size-[2.8rem] rounded-full p-2"
>
<i className="diveicon di-search" />
</Button>
</form>
);
}