Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs_cs
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { SearchPage } from "@/pages/search";
|
||||
export default SearchPage;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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<SearchResult> {
|
||||
const response = await client.get<SearchResult>(
|
||||
ENDPOINT.BOARD_SERVICE.SEARCH_POST,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const SearchApi = new SearchApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { SearchPage } from "./ui/search-page";
|
||||
@@ -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<Board>;
|
||||
contsList: Contents[];
|
||||
}
|
||||
|
||||
export interface SearchService {
|
||||
searchPost(params: SearchParams): Promise<SearchResult>;
|
||||
}
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="-mb-2">
|
||||
<h2 className="text-center text-4xl font-bold">통합검색</h2>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="relative mx-auto my-8 flex w-[52rem] items-center justify-center gap-2"
|
||||
>
|
||||
<Input
|
||||
name="keyword"
|
||||
className="h-[5rem] w-[48rem] rounded-[4rem] pl-8 pr-40 !text-2xl"
|
||||
placeholder="검색어를 입력해주세요."
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
className="absolute right-28 top-2 size-16 rounded-full"
|
||||
>
|
||||
<span className="sr-only">검색</span>
|
||||
<i className="diveicon di-search text-2xl" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
className="absolute right-10 top-2 size-16 rounded-full"
|
||||
>
|
||||
<span className="sr-only">초기화</span>
|
||||
<i className="diveicon di-reset text-2xl" />
|
||||
</Button>
|
||||
</form>
|
||||
<div className="bg-background pb-20">
|
||||
<div className="flex items-center justify-center">
|
||||
<p className="py-16 text-3xl font-bold">
|
||||
검색어 <span className="text-primary">'{keyword}'</span>로
|
||||
검색한 결과입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-[10vw]">
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<div className="flex justify-center">
|
||||
<TabsList className="mx-auto">
|
||||
<TabsTrigger value="all">
|
||||
전체 (
|
||||
{data?.contsList?.length ??
|
||||
0 + (data?.boardList?.totalElements ?? 0)}
|
||||
건)
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="contents">
|
||||
웹페이지 ({data?.contsList?.length ?? 0}건)
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="board">
|
||||
게시판 ({data?.boardList?.totalElements ?? 0}건)
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="all">
|
||||
<div className="space-y-20 p-10">
|
||||
<div>
|
||||
<h3 className="mb-4 border-b-2 border-border pb-4 text-3xl font-bold">
|
||||
웹페이지 ({data?.contsList?.length ?? 0}건)
|
||||
</h3>
|
||||
{renderContents(data?.contsList)}
|
||||
{(data?.contsList?.length ?? 0) > RENDER_LIMIT && (
|
||||
<button
|
||||
className="h-[4rem] w-full hover:underline"
|
||||
onClick={() => setTab("contents")}
|
||||
>
|
||||
검색 결과 더보기
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-4 border-b-2 border-border pb-4 text-3xl font-bold">
|
||||
게시판 ({data?.boardList?.totalElements ?? 0}건)
|
||||
</h3>
|
||||
{renderPost(data?.boardList.content)}
|
||||
{(data?.boardList?.totalElements ?? 0) > RENDER_LIMIT && (
|
||||
<button
|
||||
className="h-[4rem] w-full hover:underline"
|
||||
onClick={() => setTab("board")}
|
||||
>
|
||||
검색 결과 더보기
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="contents">
|
||||
{renderContents(data?.contsList, 100)}
|
||||
</TabsContent>
|
||||
<TabsContent value="board">
|
||||
{renderPost(data?.boardList.content, 100)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContentItemProps {
|
||||
content: Contents;
|
||||
}
|
||||
|
||||
function ContentItem({ content }: ContentItemProps) {
|
||||
return (
|
||||
<li className="border-b border-border px-6 py-8">
|
||||
<Link
|
||||
href={content.urlPath}
|
||||
className="hover:underline hover:underline-offset-1"
|
||||
>
|
||||
<span className="text-lg">{content.fullMenuPath}</span>
|
||||
<div className="mt-2 flex flex-col gap-1">
|
||||
<em
|
||||
className="font-bold not-italic"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: content.contsTtl,
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: content.contsCn,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContents(contents?: Contents[], limit = RENDER_LIMIT) {
|
||||
if (!contents || !contents.length) return null;
|
||||
return (
|
||||
<>
|
||||
{contents.map((content, index) => {
|
||||
if (limit <= index) return null;
|
||||
|
||||
return (
|
||||
<ul key={content.contsSn} className="flex flex-col">
|
||||
<ContentItem key={content.contsSn} content={content} />
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface PostItemProps {
|
||||
post: Post;
|
||||
board: Board;
|
||||
}
|
||||
|
||||
function PostItem({ post, board }: PostItemProps) {
|
||||
const postUrl = !!board.urlPath ? `${board.urlPath}/${post.pstNo}` : "";
|
||||
return (
|
||||
<li className="border-b border-border px-6 py-8">
|
||||
<Link href={postUrl} className="hover:underline hover:underline-offset-1">
|
||||
<span className="text-lg">{board.fullMenuPath}</span>
|
||||
<div className="mt-2 flex flex-col gap-1">
|
||||
<em
|
||||
className="font-bold not-italic"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: post.pstTtl,
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: post.pstCn,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<ul key={board.bbsId} className="flex flex-col">
|
||||
{board.posts.slice(0, postsToShow).map((post) => (
|
||||
<PostItem key={post.pstNo} post={post} board={board} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { SearchPage };
|
||||
@@ -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
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user