Skip to content

Commit

Permalink
Merge pull request #222 from boostcampwm-2024/feature/bookmark
Browse files Browse the repository at this point in the history
[FE] 즐겨찾기 관련 레이아웃 구현 및 API 연동
  • Loading branch information
dannysir authored Nov 28, 2024
2 parents 0fc4cfe + 6c1ae58 commit 4ca6cbb
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 11 deletions.
21 changes: 21 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 @@ -17,6 +17,7 @@
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2",
"react-router-dom": "^6.27.0",
"react-toastify": "^10.0.6",
"socket.io-client": "^4.8.1",
"vite-tsconfig-paths": "^5.0.1",
"zustand": "^5.0.1"
Expand Down
3 changes: 3 additions & 0 deletions FE/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Login from 'components/Login';
import SearchModal from './components/Search';
import MyPage from 'page/MyPage';
import Rank from 'page/Rank.tsx';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

function App() {
return (
Expand Down Expand Up @@ -39,6 +41,7 @@ function Layout() {
</main>
<Login />
<SearchModal />
<ToastContainer />
</>
);
}
2 changes: 0 additions & 2 deletions FE/src/components/Mypage/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export default function Account() {

const { asset, stocks } = data;

console.log(asset, stocks);

return (
<div className='flex min-h-[500px] flex-col gap-3'>
<AccountCondition asset={asset} />
Expand Down
60 changes: 60 additions & 0 deletions FE/src/components/Mypage/BookMark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { getBookmarkedStocks } from 'service/bookmark';

export default function BookMark() {
const navigation = useNavigate();

const handleClick = (code: string) => {
navigation(`/stocks/${code}`);
};

const { data, isLoading, isError } = useQuery(
['bookmark', 'stock'],
() => getBookmarkedStocks(),
{
staleTime: 1000,
},
);

if (isLoading) return <div>loading</div>;
if (!data) return <div>No data</div>;
if (isError) return <div>error</div>;

return (
<div className='mx-auto flex min-h-[500px] w-full flex-1 flex-col rounded-md bg-white p-4 shadow-md'>
<div className='flex pb-2 text-sm font-bold border-b'>
<p className='w-1/2 text-left truncate'>종목</p>
<p className='w-1/4 text-center'>현재가</p>
<p className='w-1/4 text-right'>등락률</p>
</div>

<ul className='flex flex-col text-sm divide-y'>
{data.map((stock) => {
const { code, name, stck_prpr, prdy_ctrt, prdy_vrss_sign } = stock;

return (
<li
className='flex py-2 transition-colors hover:cursor-pointer hover:bg-gray-50'
key={code}
onClick={() => handleClick(code)}
>
<div className='flex w-1/2 gap-2 text-left truncate'>
<p className='font-semibold'>{name}</p>
<p className='text-gray-500'>{code}</p>
</div>
<p className='w-1/4 text-center truncate'>
{(+stck_prpr).toLocaleString()}
</p>
<p
className={`w-1/4 truncate text-right ${+prdy_vrss_sign > 3 ? 'text-juga-blue-50' : 'text-juga-red-60'}`}
>
{prdy_ctrt}%
</p>
</li>
);
})}
</ul>
</div>
);
}
3 changes: 2 additions & 1 deletion FE/src/components/Mypage/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { MypageSectionType } from 'types';
const mapping = {
account: '보유 자산 현황',
order: '주문 요청 현황',
bookmark: '즐겨찾기',
info: '내 정보',
};
const sections: MypageSectionType[] = ['account', 'order', 'info'];
const sections: MypageSectionType[] = ['account', 'order', 'bookmark', 'info'];

export default function Nav() {
const [searchParams, setSearchParams] = useSearchParams();
Expand Down
14 changes: 12 additions & 2 deletions FE/src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,18 @@ export default function SearchModal() {

<div className={'h-[400px] overflow-y-auto'}>
{isSearching ? (
<div className={'flex h-full items-center justify-center'}>
<Lottie animationData={searchAnimation} />
<div
className={
'flex h-[320px] flex-col items-center justify-center'
}
>
<Lottie
animationData={searchAnimation}
className='h-[200px]'
/>
<p className='font-bold text-juga-grayscale-black'>
두 글자 이상의 검색어를 입력해주세요.
</p>
</div>
) : (
showSearchResults && <SearchList searchData={data} />
Expand Down
49 changes: 47 additions & 2 deletions FE/src/components/StocksDetail/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { HeartIcon } from '@heroicons/react/16/solid';
import Toast from 'components/Toast';
import { useEffect, useState } from 'react';
import { bookmark, unbookmark } from 'service/bookmark';
import { unsubscribe } from 'service/stocks';
import useAuthStore from 'store/authStore';
import useLoginModalStore from 'store/useLoginModalStore';
import { StockDetailType } from 'types';
import { stringToLocaleString } from 'utils/common';
import { socket } from 'utils/socket';
// import { useDebounce } from 'utils/useDebounce';

type StocksDetailHeaderProps = {
code: string;
Expand All @@ -18,12 +24,32 @@ export default function Header({ code, data }: StocksDetailHeaderProps) {
prdy_ctrt,
hts_avls,
per,
is_bookmarked,
} = data;

const [currPrice, setCurrPrice] = useState(stck_prpr);
const [currPrdyVrssSign, setCurrPrdyVrssSign] = useState(prdy_vrss_sign);
const [currPrdyVrss, setCurrPrdyVrss] = useState(prdy_vrss);
const [currPrdyRate, setCurrPrdyRate] = useState(prdy_ctrt);
const [isBookmarked, setIsBookmarked] = useState(is_bookmarked);
const { isLogin } = useAuthStore();
const { toggleModal } = useLoginModalStore();

// const { debounceValue } = useDebounce(isBookmarked, 1000);
// const isInitialMount = useRef(true);

// useEffect(() => {
// if (isInitialMount.current) {
// isInitialMount.current = false;
// return;
// }

// if (debounceValue) {
// bookmark(code);
// } else {
// unbookmark(code);
// }
// }, [code, debounceValue]);

useEffect(() => {
const handleSocketData = (data: {
Expand Down Expand Up @@ -65,7 +91,7 @@ export default function Header({ code, data }: StocksDetailHeaderProps) {
currPrdyVrssSign === '3' ? '' : currPrdyVrssSign < '3' ? '+' : '-';

return (
<div className='flex h-16 w-full items-center justify-between px-2'>
<div className='flex items-center justify-between w-full h-16 px-2'>
<div className='flex flex-col font-semibold'>
<div className='flex gap-2 text-sm'>
<h2>{hts_kor_isnm}</h2>
Expand All @@ -81,13 +107,32 @@ export default function Header({ code, data }: StocksDetailHeaderProps) {
</p>
</div>
</div>
<div className='flex gap-4 text-xs font-semibold'>
<div className='flex items-center gap-4 text-xs font-semibold'>
{stockInfo.map((e, idx) => (
<div key={`stockdetailinfo${idx}`} className='flex gap-2'>
<p className='text-juga-grayscale-200'>{e.label}</p>
<p>{e.value}</p>
</div>
))}
<button
onClick={() => {
if (!isLogin) {
toggleModal();
Toast({ message: '로그인을 해주세요!', type: 'warning' });
return;
}
if (isBookmarked) unbookmark(code);
else bookmark(code);

setIsBookmarked((prev) => !prev);
}}
>
{isLogin && isBookmarked ? (
<HeartIcon className='size-6 fill-juga-red-60' />
) : (
<HeartIcon className='size-6 fill-juga-grayscale-200' />
)}
</button>
</div>
</div>
);
Expand Down
23 changes: 23 additions & 0 deletions FE/src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { toast } from 'react-toastify';

type ToastType = 'success' | 'error' | 'warning' | 'info';

type ToastProps = {
message: string;
type: ToastType;
};

export default function Toast({ message, type }: ToastProps) {
switch (type) {
case 'success':
return toast.success(message, { position: 'top-right', autoClose: 1000 });
case 'error':
return toast.error(message, { position: 'top-right', autoClose: 1000 });
case 'warning':
return toast.warning(message, { position: 'top-right', autoClose: 1000 });
case 'info':
return toast.info(message, { position: 'top-right', autoClose: 1000 });
default:
return null;
}
}
2 changes: 2 additions & 0 deletions FE/src/page/MyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Account from 'components/Mypage/Account';
import BookMark from 'components/Mypage/BookMark';
import MyInfo from 'components/Mypage/MyInfo';
import Nav from 'components/Mypage/Nav';
import Order from 'components/Mypage/Order';
Expand All @@ -16,6 +17,7 @@ export default function MyPage() {
{
account: <Account />,
order: <Order />,
bookmark: <BookMark />,
info: <MyInfo />,
}[currentPage]
}
Expand Down
42 changes: 42 additions & 0 deletions FE/src/service/bookmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BookmakredStock } from 'types';

export async function bookmark(code: string) {
const url = import.meta.env.PROD
? `${import.meta.env.VITE_API_URL}/stocks/bookmark/${code}`
: `/api/stocks/bookmark/${code}`;

return fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
});
}

export async function unbookmark(code: string) {
const url = import.meta.env.PROD
? `${import.meta.env.VITE_API_URL}/stocks/bookmark/${code}`
: `/api/stocks/bookmark/${code}`;

return fetch(url, {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
});
}

export async function getBookmarkedStocks(): Promise<BookmakredStock[]> {
const url = import.meta.env.PROD
? `${import.meta.env.VITE_API_URL}/stocks/bookmark`
: '/api/stocks/bookmark';

return fetch(url, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json());
}
12 changes: 11 additions & 1 deletion FE/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type StockDetailType = {
per: string;
stck_mxpr: string;
stck_llam: string;
is_bookmarked: boolean;
};

export type StockChartUnit = {
Expand All @@ -42,7 +43,7 @@ export type StockChartUnit = {
mov_avg_20?: string;
};

export type MypageSectionType = 'account' | 'order' | 'info';
export type MypageSectionType = 'account' | 'order' | 'bookmark' | 'info';

export type Asset = {
cash_balance: string;
Expand Down Expand Up @@ -91,3 +92,12 @@ export type Profile = {
name: string;
email: string;
};

export type BookmakredStock = {
name: string;
code: string;
stck_prpr: string;
prdy_vrss: string;
prdy_vrss_sign: string;
prdy_ctrt: string;
};
5 changes: 2 additions & 3 deletions FE/src/utils/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useEffect, useState } from 'react';
import { formatNoSpecialChar } from './formatNoSpecialChar.ts';

export const useDebounce = (value: string, delay: number) => {
export const useDebounce = <T>(value: T, delay: number) => {
const [debounceValue, setDebounceValue] = useState(value);
const [isDebouncing, setIsDebouncing] = useState(false);

useEffect(() => {
setIsDebouncing(true);

const handler = setTimeout(() => {
setDebounceValue(formatNoSpecialChar(value));
setDebounceValue(value);
setIsDebouncing(false);
}, delay);

Expand Down

0 comments on commit 4ca6cbb

Please sign in to comment.