Skip to content

Commit

Permalink
Merge pull request #100 from boostcampwm-2024/feature/connect/search-#64
Browse files Browse the repository at this point in the history
[FE] 검색 API 연동 & 검색 레이아웃 수정 & 거래 현황 API 연동
  • Loading branch information
dongree authored Nov 13, 2024
2 parents 904cc13 + 2996bc3 commit 168fcbd
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 68 deletions.
20 changes: 20 additions & 0 deletions FE/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions FE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@heroicons/react": "^2.1.5",
"@tanstack/react-query": "^4.36.1",
"lottie-react": "^2.4.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
Expand Down
1 change: 1 addition & 0 deletions FE/public/searchAnimation.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion FE/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function Header() {
<input
type='text'
placeholder='Search...'
value={searchInput}
defaultValue={searchInput}
className='h-[36px] w-[280px] rounded-lg bg-juga-grayscale-50 px-4 py-2'
onClick={toggleSearchModal}
/>
Expand Down
55 changes: 38 additions & 17 deletions FE/src/components/Search/SearchCard.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
export default function SearchCard() {
const companyName = '회사명';
const previousClose = 50000;
const priceChange = 2.5;
import { searchDataType } from './searchDataType.ts';
import { useNavigate } from 'react-router-dom';
import useSearchModalStore from '../../store/useSearchModalStore.ts';
import useSearchInputStore from '../../store/useSearchInputStore.ts';
import { SearchCardHighLight } from './SearchCardHighlight.tsx';

type SearchCardProps = {
data: searchDataType;
};

export default function SearchCard({ data }: SearchCardProps) {
const { code, name, market } = data;
const { isOpen, toggleSearchModal } = useSearchModalStore();
const { searchInput } = useSearchInputStore();

const navigation = useNavigate();

const handleClick = () => {
navigation(`/stocks/${code}`);
if (isOpen) toggleSearchModal();
};

return (
<li className='h-[52px] w-full rounded-xl hover:cursor-pointer hover:bg-gray-50'>
<div className='my-2 flex w-full items-center justify-between px-4'>
<div className='flex-1'>
<p className='text-left font-medium text-juga-grayscale-black'>
{companyName}
</p>
<li
className={
'h-[52px] w-full rounded-xl hover:cursor-pointer hover:bg-gray-100'
}
onClick={handleClick}
>
<div className={'my-2 flex w-full items-center justify-between px-4'}>
<div className={'flex-1 flex-col'}>
<div className='text-left font-medium text-gray-900'>
<SearchCardHighLight text={name} highlight={searchInput} />
</div>
<div className={'text-left text-xs font-normal text-gray-500'}>
{code}
</div>
</div>

<div className='flex flex-col items-end justify-center gap-0.5'>
<p className='text-right text-sm font-medium text-gray-900'>
{previousClose.toLocaleString()}
</p>

<p className={'text-right text-xs font-medium text-red-500'}>
+{Math.abs(priceChange).toFixed(2)}%
<div className={'flex flex-col items-end justify-center gap-0.5'}>
<p className={'text-right text-xs font-medium text-gray-600'}>
{market}
</p>
</div>
</div>
Expand Down
29 changes: 29 additions & 0 deletions FE/src/components/Search/SearchCardHighlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type SearchCardHighLightProps = {
text: string;
highlight: string;
};

export const SearchCardHighLight = ({
text,
highlight,
}: SearchCardHighLightProps) => {
if (!highlight.trim()) {
return <div>{text}</div>;
}

const parts = text.split(new RegExp(`(${highlight})`, 'gi'));

return (
<div>
{parts.map((part, index) =>
part.toLowerCase() === highlight.toLowerCase() ? (
<span key={index} className={'font-medium text-juga-blue-50'}>
{part}
</span>
) : (
<span key={index}>{part}</span>
),
)}
</div>
);
};
22 changes: 11 additions & 11 deletions FE/src/components/Search/SearchList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import SearchCard from './SearchCard.tsx';
import { searchDataType } from './searchDataType.ts';

export default function SearchList() {
type SearchListProps = {
searchData: searchDataType[];
};

export default function SearchList({ searchData }: SearchListProps) {
return (
<>
<div className={'my-4 flex items-center justify-between'}>
<div className={'text-start text-sm font-bold'}>검색 결과</div>
</div>
<ul className='flex h-full w-full flex-col items-center justify-between overflow-y-auto'>
{Array.from({ length: 30 }, (_, index) => {
return <SearchCard key={index} />;
})}
</ul>
</>
<ul>
{searchData.map((data, index) => (
<SearchCard key={index} data={data} />
))}
</ul>
);
}
64 changes: 50 additions & 14 deletions FE/src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,30 @@ import { SearchInput } from './SearchInput';
import { SearchHistoryList } from './SearchHistoryList';
import SearchList from './SearchList.tsx';
import useSearchInputStore from '../../store/useSearchInputStore.ts';
import { useDebounce } from '../../utils/useDebounce.ts';
import { useQuery } from '@tanstack/react-query';
import { searchApi } from '../../service/searchApi.ts';
import Lottie from 'lottie-react';
import searchAnimation from '../../../public/searchAnimation.json';

export default function SearchModal() {
const { isOpen, toggleSearchModal } = useSearchModalStore();
const { searchInput, setSearchInput } = useSearchInputStore();
const [searchHistory, setSearchHistory] = useState<string[]>([]);

const shouldSearch = searchInput.trim().length >= 2;

const { debounceValue, isDebouncing } = useDebounce(
shouldSearch ? searchInput : '',
500,
);

const { data, isLoading, isFetching } = useQuery({
queryKey: ['search', debounceValue],
queryFn: () => searchApi(debounceValue),
enabled: !!debounceValue && !isDebouncing,
});

useEffect(() => {
setSearchHistory(['서산증권', '삼성화재', '삼성전기']);
}, []);
Expand All @@ -19,31 +37,49 @@ export default function SearchModal() {
setSearchHistory((prev) => prev.filter((history) => history !== item));
};

if (!isOpen) return;
if (!isOpen) return null;

const isSearching = isLoading || isFetching || isDebouncing;
const showSearchResults = searchInput && !isSearching && data;

return (
<>
<Overlay onClick={() => toggleSearchModal()} />
<section
className={`${searchInput === '' ? '' : 'h-[520px]'} fixed left-1/2 top-3 flex w-[640px] -translate-x-1/2 flex-col rounded-2xl bg-white shadow-lg`}
className={`${searchInput.length ? 'h-[520px]' : 'h-[160px]'} fixed left-1/2 top-3 w-[640px] -translate-x-1/2 rounded-2xl bg-white shadow-xl`}
>
<div className='flex h-full flex-col p-3'>
<div className='mb-5'>
<SearchInput value={searchInput} onChange={setSearchInput} />
</div>
<div className='flex-1 overflow-hidden'>
<div
className={
'absolute left-0 right-0 top-0 z-10 rounded-t-2xl bg-white p-3'
}
>
<SearchInput value={searchInput} onChange={setSearchInput} />
</div>

<div className={'h-full px-3 pb-3 pt-[68px]'}>
{' '}
{!searchInput ? (
<SearchHistoryList
searchHistory={searchHistory}
onDeleteItem={handleDeleteHistoryItem}
/>
{searchInput === '' ? (
<></>
) : (
<div className='h-full overflow-y-auto'>
<SearchList />
) : (
<div className={'h-full'}>
<div className={'mb-4 text-start text-sm font-bold'}>
검색 결과
</div>

<div className={'h-[400px] overflow-y-auto'}>
{isSearching ? (
<div className={'flex h-full items-center justify-center'}>
<Lottie animationData={searchAnimation} />
</div>
) : (
showSearchResults && <SearchList searchData={data} />
)}
</div>
)}
</div>
</div>
)}
</div>
</section>
</>
Expand Down
5 changes: 5 additions & 0 deletions FE/src/components/Search/searchDataType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type searchDataType = {
code: string;
name: string;
market: string;
};
7 changes: 7 additions & 0 deletions FE/src/components/StocksDetail/PriceDataType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type PriceDataType = {
stck_cntg_hour: string;
stck_prpr: 'string';
prdy_vrss_sign: 'string';
cntg_vol: 'string';
prdy_ctrt: 'string';
};
44 changes: 31 additions & 13 deletions FE/src/components/StocksDetail/PriceSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,31 @@ import { useEffect, useRef, useState } from 'react';
import PriceTableColumn from './PriceTableColumn.tsx';
import PriceTableLiveCard from './PriceTableLiveCard.tsx';
import PriceTableDayCard from './PriceTableDayCard.tsx';
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { PriceDataType } from './PriceDataType.ts';

export const tradeHistoryApi = async (id: string) => {

Check warning on line 9 in FE/src/components/StocksDetail/PriceSection.tsx

View workflow job for this annotation

GitHub Actions / FE-test-and-build

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const response = await fetch(
`http://223.130.151.42:3000/api/stocks/${id}/trade-history`,
);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};

export default function PriceSection() {
const [buttonFlag, setButtonFlag] = useState(true);
const indicatorRef = useRef<HTMLDivElement>(null);
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
const { id } = useParams();

const { data } = useQuery({
queryKey: ['detail', id],
queryFn: () => tradeHistoryApi(id as string),
enabled: !!id,
});

useEffect(() => {
const tmpIndex = buttonFlag ? 0 : 1;
Expand Down Expand Up @@ -44,22 +64,22 @@ export default function PriceSection() {
style={{ height: '28px' }}
/>
<button
className={`${
className={`z-7 relative w-full rounded-lg px-4 py-1 ${
buttonFlag
? 'text-juga-grayscale-black'
: 'text-juga-grayscale-400'
} relative z-10 w-full rounded-lg px-4 py-1`}
}`}
onClick={() => setButtonFlag(true)}
ref={(el) => (buttonRefs.current[0] = el)}
>
실시간
</button>
<button
className={`relative z-10 w-full rounded-lg ${
className={`z-7 relative w-full rounded-lg ${
!buttonFlag
? 'text-juga-grayscale-black'
: 'text-juga-grayscale-400'
} relative z-10 w-full rounded-lg px-4 py-1`}
} z-7 relative w-full rounded-lg px-4 py-1`}
onClick={() => setButtonFlag(false)}
ref={(el) => (buttonRefs.current[1] = el)}
>
Expand All @@ -71,15 +91,13 @@ export default function PriceSection() {
<table className={'w-full table-fixed text-xs font-normal'}>
<PriceTableColumn viewMode={buttonFlag} />
<tbody>
{Array(30)
.fill(null)
.map((_, index) =>
buttonFlag ? (
<PriceTableLiveCard key={index} />
) : (
<PriceTableDayCard key={index} />
),
)}
{data?.map((eachData: PriceDataType, index: number) =>
buttonFlag ? (
<PriceTableLiveCard key={index} data={eachData} />
) : (
<PriceTableDayCard key={index} />
),
)}
</tbody>
</table>
</div>
Expand Down
6 changes: 3 additions & 3 deletions FE/src/components/StocksDetail/PriceTableColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ type Props = {
export default function PriceTableColumn({ viewMode }: Props) {
if (!viewMode) {
return (
<thead className={'sticky top-0 z-10 bg-white'}>
<thead className={'z-1 sticky top-0 bg-white'}>
<tr className={'h-10 border-b text-gray-500'}>
<th className={'px-4 py-1 text-left font-medium'}>일자</th>
<th className={'px-4 py-1 text-right font-medium'}>종가</th>
Expand All @@ -19,12 +19,12 @@ export default function PriceTableColumn({ viewMode }: Props) {
);
}
return (
<thead className={'sticky top-0 z-10 bg-white'}>
<thead className={'z-1 sticky top-0 bg-white'}>
<tr className={'h-10 border-b text-gray-500'}>
<th className={'px-4 py-1 text-left font-medium'}>채결가</th>
<th className={'px-4 py-1 text-right font-medium'}>채결량(주)</th>
<th className={'px-4 py-1 text-right font-medium'}>등락률</th>
<th className={'px-4 py-1 text-right font-medium'}>거래량(주)</th>
{/*<th className={'px-4 py-1 text-right font-medium'}>거래량(주)</th>*/}
<th className={'px-4 py-1 text-right font-medium'}>시간</th>
</tr>
</thead>
Expand Down
Loading

0 comments on commit 168fcbd

Please sign in to comment.