Skip to content

Commit

Permalink
Merge branch 'feat/search'
Browse files Browse the repository at this point in the history
  • Loading branch information
banhogu committed Sep 2, 2024
2 parents a606966 + 9b7ba83 commit 47c56d5
Show file tree
Hide file tree
Showing 19 changed files with 335 additions and 18 deletions.
6 changes: 6 additions & 0 deletions public/images/search/search_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions public/images/search/xcircle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export default function RootLayout({
src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_MAP_KEY}&autoload=false`}
/>
<Gnb />
<ModalProvider>{children}</ModalProvider>
{children}
<ModalProvider />
<div id="root-portal"></div>
</QueryProvider>
</IntegrateMSW>
Expand Down
2 changes: 1 addition & 1 deletion src/app/report/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function ProductLayout({
export default function ReportLayout({
children
}: Readonly<{
children: React.ReactNode;
Expand Down
11 changes: 11 additions & 0 deletions src/app/search/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function SearchLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="bg-bg w-full">
<div className=" max-w-[600px] h-full min-h-screen w-full mx-auto">{children}</div>
</div>
);
}
8 changes: 8 additions & 0 deletions src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import SearchIndex from '@/components/search/SearchIndex';
import React from 'react';

const SearchPage = () => {
return <SearchIndex />;
};

export default SearchPage;
5 changes: 3 additions & 2 deletions src/components/common/Gnb.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useAuthStore } from '@/store/userAuth.store';

const Gnb = () => {
const pathname = usePathname();
const { isLoggedIn, setIsLoggedIn } = useAuthStore();
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();

useEffect(() => {
const accessToken = localStorage.getItem('access_token');
Expand Down Expand Up @@ -36,7 +37,7 @@ const Gnb = () => {
<img src="/images/logo.svg" alt="logo" className="w-[144px] h-5" />
</Link>
<div className="flex items-center gap-6">
<div className="cursor-pointer">
<div onClick={() => router.push('/search')} className="cursor-pointer">
<img src="/images/gnb/search.svg" alt="search_icon" className="w-6 h-6" />
</div>
<div className="cursor-pointer">
Expand Down
13 changes: 9 additions & 4 deletions src/components/home/MainListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { MainProductItem } from '@/types/homeComponentsType';

import { formatCategory } from '@/utils/formatCategory';
import { useRouter } from 'next/navigation';
import React from 'react';

const MainListItem = ({
Expand All @@ -14,8 +15,12 @@ const MainListItem = ({
dividend,
lastDivide_rate
}: MainProductItem) => {
const router = useRouter();

return (
<div className="mt-5 pb-5 border-b border-gray100 cursor-pointer">
<div
onClick={() => router.push(`/product/detail/${product_Id}`)}
className="mt-5 pb-5 border-b border-gray100 cursor-pointer">
<div className="flex gap-5">
{/* 이미지 */}
<div>
Expand All @@ -35,10 +40,10 @@ const MainListItem = ({
</div>
<div className="text-body1">{name}</div>
<div className="flex items-center gap-[6px]">
<div className="text-body7 text-gray400">{price.toLocaleString()}</div>
<div className="text-error">
<div className="text-body7 text-gray400">{price.toLocaleString()}</div>
<div className={`${priceRate > 0 ? 'text-error' : 'text-success'}`}>
{'('}
{'+'} {priceRate}%{')'}
{`${priceRate > 0 ? '+' : ''}`} {priceRate}%{')'}
</div>
</div>
</div>
Expand Down
13 changes: 11 additions & 2 deletions src/components/home/RealtimeRank.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import type { RealtimeRankType } from '@/types/homeComponentsType';
import { getRealtimeRank } from '@/factory/RealtimeRank';
import React from 'react';
import RealtimeRankSkeleton from '../skeleton/RealtimeSkeleton';
import { useSearchStore } from '@/store/search.store';
import { useRouter } from 'next/navigation';

const RealtimeRank = () => {
const router = useRouter();
const { setKeyword } = useSearchStore();
const { data, isLoading } = getRealtimeRank();

if (isLoading) {
Expand All @@ -28,8 +32,13 @@ const RealtimeRank = () => {
<div className="flex-1 text-body2 text-black truncate">
{item.keyword}
</div>
{/* todo : 돋보기 누르면 그대로 검색 쿼리에 keyword가 들어가게하기 */}
<div className="w-6 h-6 rounded-full bg-gray100 flex items-center justify-center cursor-pointer">

<div
onClick={() => {
setKeyword(item.keyword);
router.push('/search');
}}
className="w-6 h-6 rounded-full bg-gray100 flex items-center justify-center cursor-pointer">
<img src="/images/home/search.svg" alt="search" />
</div>
</li>
Expand Down
2 changes: 1 addition & 1 deletion src/components/recentlyIssue/CategoryNews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CategoryNewsItemSkeleton from '../skeleton/CategoryNewsItemSkeleton';
import { Virtuoso } from 'react-virtuoso';

const CategoryNews = () => {
const [category, setCategory] = useState('building');
const [category, setCategory] = useState('all');
const [sort, setSort] = useState('latest');

const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, isLoading } =
Expand Down
87 changes: 87 additions & 0 deletions src/components/search/SearchIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client';
import React, { ChangeEvent, useCallback, useRef, useState } from 'react';
import SearchRank from './SearchRank';
import useDebounce from '@/hook/useDebounce';
import { getSearchItem } from '@/factory/SearchItem';
import CircleSkeleton from '../skeleton/CircleSkeleton';
import SearchedResultItem from './SearchedResultItem';
import { useSearchStore } from '@/store/search.store';

const SearchIndex = () => {
const { currentKeyword } = useSearchStore();
const [isFocused, setIsFocused] = useState(false);
const [keyWord, setKeyWord] = useState(currentKeyword); // 전역 currentKeyword가 존재한다면 우선사용
const inputRef = useRef<HTMLInputElement>(null);
const debouncedKeyword = useDebounce(keyWord);

const { data, isLoading } = getSearchItem(debouncedKeyword);

const handleKeyword = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setKeyWord(e.target.value);
}, []);

const handleClear = useCallback(() => {
setKeyWord('');
if (inputRef.current) {
inputRef.current.focus();
}
}, []);

return (
<div className="pt-10 flex flex-col gap-10">
{keyWord ? null : (
<div className="text-heading1 text-gray700 flex items-center justify-center">
찾으시는 상품이 있으신가요?
</div>
)}

<div className="flex items-center justify-center">
<div
className={`max-w-[520px] w-full bg-white flex items-center justify-center py-[14px] px-4 rounded-[12px]
${isFocused || keyWord ? 'border-[1.5px] border-normal' : 'border-[1.5px] border-gray100'}
`}>
<input
ref={inputRef}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
value={keyWord}
onChange={handleKeyword}
type="text"
placeholder="조각투자 상품 검색"
className="w-full outline-none text-body2 "
/>
{keyWord && (
<div onClick={handleClear} className="cursor-pointer mr-3">
<img src="/images/search/xcircle.svg" alt="" />
</div>
)}
<div>
<img src="/images/search/search_icon.svg" alt="" />
</div>
</div>
</div>

{keyWord ? null : (
<div>
<div className="text-heading3 text-gray700">실시간 검색</div>
<SearchRank setKeyWord={setKeyWord} />
</div>
)}

{keyWord && isLoading && <CircleSkeleton />}
{keyWord && !isLoading && data && (
<div>
<div className="text-heading2">
<span className="text-gray700">검색 결과</span>{' '}
<span className="text-normal">{data?.length}</span>
</div>
<ul className="mt-10 pb-10 flex flex-col gap-4">
{data?.map((item, i) => <SearchedResultItem key={i} {...item} />)}
</ul>
</div>
)}
</div>
);
};

export default SearchIndex;
50 changes: 50 additions & 0 deletions src/components/search/SearchRank.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';
import { getRealtimeRank } from '@/factory/RealtimeRank';
import React, { Dispatch } from 'react';
import RealtimeRankSkeleton from '../skeleton/RealtimeSkeleton';
import { RealtimeRankType } from '@/types/homeComponentsType';
import SearchRankSkeleton from '../skeleton/SearchRankSkeleton';

interface SearchRankType {
setKeyWord: Dispatch<React.SetStateAction<string>>;
}

const SearchRank = ({ setKeyWord }: SearchRankType) => {
const { data, isLoading } = getRealtimeRank();

const handleClick = (keyword: string) => {
setKeyWord(keyword);
};

if (isLoading) {
return <SearchRankSkeleton />;
}

return (
<div className="mt-3 w-full h-[291px]">
<div className="p-5 shadow-custom-normal rounded-[12px] flex-1 bg-white">
<ul className="flex flex-col gap-5">
{data?.map((item: RealtimeRankType, i: number) => (
<li key={i} className="flex gap-3 items-center">
<div
className={`${i < 3 ? 'text-normal text-body6 ' : 'text-gray400 text-body6'}`}>
{item.rank}
</div>
<div className="flex-1 text-body2 text-black truncate">{item.keyword}</div>
{/* todo : 돋보기 누르면 그대로 검색 쿼리에 keyword가 들어가게하기 */}
<div className="w-6 h-6 rounded-full bg-gray100 flex items-center justify-center cursor-pointer">
<img
onClick={() => handleClick(item.keyword)}
src="/images/home/search.svg"
alt="search"
/>
</div>
</li>
))}
</ul>
</div>
</div>
);
};

export default SearchRank;
44 changes: 44 additions & 0 deletions src/components/search/SearchedResultItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { SearchedItem } from '@/types/homeComponentsType';

import { formatCategory } from '@/utils/formatCategory';
import { useRouter } from 'next/navigation';
import React from 'react';

const SearchedResultItem = ({ productId, name, platform, category }: SearchedItem) => {
const router = useRouter();

return (
<li
onClick={() => router.push(`/product/detail/${productId}`)}
className="p-5 cursor-pointer bg-white rounded-[12px]">
<div className="flex gap-5 items-center">
{/* 이미지 */}
<div>
<img
src={'/images/home/mock.jpeg'}
alt=""
className="w-[82px] h-[82px] rounded-[8px]"
/>
</div>
{/* 메인정보 */}
<div className="flex-1 flex flex-col gap-[10px]">
<div className="flex items-center gap-2">
<div className="text-caption3 text-gray400 p-[6px] bg-bg rounded-[4px] flex items-center justify-center">
{formatCategory(category)}
</div>
<div className="text-body7 text-gray300 ">{platform}</div>
</div>
<div className="text-body1">{name}</div>
</div>
{/* 부가정보 */}
<div className="flex items-center gap-4">
<div className="cursor-pointer">
<img src="/images/home/bookmark.svg" alt="" />
</div>
</div>
</div>
</li>
);
};

export default SearchedResultItem;
7 changes: 7 additions & 0 deletions src/components/skeleton/CircleSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function CircleSkeleton({ className = '' }) {
return (
<div className="fixed inset-0 flex items-center justify-center z-[99999]">
<div className="h-16 w-16 animate-spin rounded-full border-8 border-t-normal border-gray-300" />
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/skeleton/SearchRankSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';

const SearchRankSkeleton = () => {
return (
<div className="mt-3 w-full h-[291px] hidden desk2:block">
<div className="flex flex-col gap-5 animate-pulse">
<div className="p-5 shadow-custom-normal rounded-[12px] flex-1">
<ul className="flex flex-col gap-5">
{Array.from({ length: 5 }).map((_, i) => (
<li key={i} className="flex gap-3 items-center">
<div className={`w-8 h-4 bg-gray-300 rounded-md`} />
<div className="flex-1 h-4 bg-gray-300 rounded-md" />
<div className="w-6 h-6 bg-gray-200 rounded-full" />
</li>
))}
</ul>
</div>
</div>
</div>
);
};

export default SearchRankSkeleton;
Loading

0 comments on commit 47c56d5

Please sign in to comment.