Skip to content

Commit

Permalink
Merge pull request #173 from boostcampwm-2024/feature/trade
Browse files Browse the repository at this point in the history
[FE] 주문 요청 현황 레이아웃 구현 & api 연동
  • Loading branch information
dannysir authored Nov 21, 2024
2 parents 725566e + e97765f commit 948720c
Show file tree
Hide file tree
Showing 17 changed files with 400 additions and 196 deletions.
21 changes: 18 additions & 3 deletions FE/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,26 @@ import useLoginModalStore from 'store/useLoginModalStore';
import useSearchModalStore from '../store/useSearchModalStore.ts';
import useSearchInputStore from '../store/useSearchInputStore.ts';
import logo from 'assets/Logo.png';
import { deleteCookie } from 'utils/common.ts';
import { checkAuth } from 'service/auth.ts';
import { useEffect } from 'react';

export default function Header() {
const { toggleModal } = useLoginModalStore();
const { isLogin, resetToken } = useAuthStore();
const { isLogin, setIsLogin } = useAuthStore();
const { toggleSearchModal } = useSearchModalStore();
const { searchInput } = useSearchInputStore();

useEffect(() => {
const check = async () => {
const res = await checkAuth();
if (res.ok) setIsLogin(true);
else setIsLogin(false);
};

check();
}, [setIsLogin]);

return (
<header className='fixed left-0 top-0 h-[60px] w-full bg-white'>
<div className='mx-auto flex h-full max-w-[1280px] items-center justify-between px-8'>
Expand All @@ -24,7 +37,6 @@ export default function Header() {
<Link to={'/'}></Link>
<Link to={'/rank'}>랭킹</Link>
<Link to={'/mypage'}>마이페이지</Link>

</nav>
<div className='relative'>
<input
Expand All @@ -40,7 +52,10 @@ export default function Header() {
{isLogin ? (
<button
className='px-4 py-2 text-sm text-juga-grayscale-500'
onClick={resetToken}
onClick={() => {
setIsLogin(false);
deleteCookie('accessToken');
}}
>
로그아웃
</button>
Expand Down
5 changes: 3 additions & 2 deletions FE/src/components/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function Login() {
const { isOpen, toggleModal } = useLoginModalStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { setAccessToken } = useAuthStore();
const { setIsLogin } = useAuthStore();
const [errorCode, setErrorCode] = useState<number>(200);

useEffect(() => {
Expand All @@ -29,7 +29,7 @@ export default function Login() {
return;
}

setAccessToken(res.accessToken);
setIsLogin(true);
toggleModal();
};

Expand All @@ -46,6 +46,7 @@ export default function Login() {
}

document.cookie = `accessToken=${res.accessToken}; path=/;`;
setIsLogin(true);
toggleModal();
return;
}
Expand Down
23 changes: 16 additions & 7 deletions FE/src/components/Mypage/AccountCondition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,25 @@ export default function AccountCondition({ asset }: AccountConditionProps) {
total_asset,
total_profit,
total_profit_rate,
is_positive,
} = asset;

return (
<div className='flex flex-wrap gap-4 p-3 text-base font-semibold shadow-sm rounded-xl bg-gray-50'>
<div className='flex min-w-[250px] flex-1 flex-col rounded-lg bg-white p-6 shadow-sm'>
<h3 className='mb-4 text-xl text-center text-gray-700'>자산 현황</h3>
<div className='flex justify-between mb-6'>
<p className='text-gray-600'>총 자산</p>
<p className='text-juga-grayscale-500'>총 자산</p>
<p className='text-gray-900'>{stringToLocaleString(total_asset)}</p>
</div>
<div className='flex justify-between mb-2'>
<p className='text-gray-600'>가용 자산</p>
<p className='text-juga-grayscale-500'>가용 자산</p>
<p className='text-gray-900'>
{stringToLocaleString(cash_balance)}
</p>
</div>
<div className='flex justify-between'>
<p className='text-gray-600'>주식 자산</p>
<p className='text-juga-grayscale-500'>주식 자산</p>
<p className='text-gray-900'>
{stringToLocaleString(stock_balance)}
</p>
Expand All @@ -39,12 +40,20 @@ export default function AccountCondition({ asset }: AccountConditionProps) {
<div className='flex min-w-[250px] flex-1 flex-col rounded-lg bg-white p-6 shadow-sm'>
<h3 className='mb-4 text-xl text-center text-gray-700'>투자 성과</h3>
<div className='flex justify-between mb-6'>
<p className='text-gray-600'>투자 손익</p>
<p className='text-red-600'>{stringToLocaleString(total_profit)}</p>
<p className='text-juga-grayscale-500'>투자 손익</p>
<p
className={`${is_positive ? 'text-juga-blue-50' : 'text-juga-red-60'}`}
>
{stringToLocaleString(total_profit)}
</p>
</div>
<div className='flex justify-between'>
<p className='text-gray-600'>수익률</p>
<p className='text-red-600'>{total_profit_rate}%</p>
<p className='text-juga-grayscale-500'>수익률</p>
<p
className={`${is_positive ? 'text-juga-blue-50' : 'text-juga-red-60'}`}
>
{total_profit_rate}%
</p>
</div>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion FE/src/components/Mypage/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { MypageSectionType } from 'types';

const mapping = {
account: '보유 자산 현황',
order: '주문 요청 현황',
info: '내 정보',
};
const sections: MypageSectionType[] = ['account', 'info'];
const sections: MypageSectionType[] = ['account', 'order', 'info'];

export default function Nav() {
const [searchParams, setSearchParams] = useSearchParams();
Expand Down
68 changes: 68 additions & 0 deletions FE/src/components/Mypage/Order.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import useOrders from 'hooks/useOrder';
import { parseTimestamp } from 'utils/common';

export default function Order() {
const { orderQuery, removeOrder } = useOrders();

const { data, isLoading, isError } = orderQuery;

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

const handleCancelOrder = (id: number) => {
removeOrder.mutate(id);
};

return (
<div className='flex flex-col w-full p-4 mx-auto bg-white rounded-md shadow-md'>
<div className='flex pb-2 text-sm font-bold border-b'>
<p className='w-1/3 text-left truncate'>종목</p>
<p className='w-1/4 text-center'>요청 유형</p>
<p className='w-1/4 text-center'>수량</p>
<p className='w-1/4 text-center'>요청 가격</p>
<p className='w-1/4 text-right'>요청 시간</p>
<p className='w-1/6 text-right'></p>
</div>

<ul className='flex flex-col text-sm divide-y min-h-48'>
{data.map((order) => {
const {
id,
stock_code,
stock_name,
price,
amount,
trade_type,
created_at,
} = order;

return (
<li className='flex py-2' key={id}>
<div className='flex w-1/3 gap-2 text-left truncate'>
<p className='font-semibold'>{stock_name}</p>
<p className='text-gray-500'>{stock_code}</p>
</div>
<p className='w-1/4 text-center'>
{trade_type === 'BUY' ? '매수' : '매도'}
</p>
<p className='w-1/4 text-center truncate'>{amount}</p>
<p className='w-1/4 text-center'>{price.toLocaleString()}</p>
<p className='w-1/4 text-right truncate'>
{parseTimestamp(created_at)}
</p>
<p className='w-1/6 text-right'>
<button
onClick={() => handleCancelOrder(id)}
className='px-2 py-1 text-xs text-white transition rounded-lg bg-juga-red-60 hover:bg-red-600'
>
취소
</button>
</p>
</li>
);
})}
</ul>
</div>
);
}
145 changes: 145 additions & 0 deletions FE/src/components/StocksDetail/BuySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { ChangeEvent, FocusEvent, FormEvent, useRef, useState } from 'react';
import useTradeAlertModalStore from 'store/tradeAlertModalStore';
import { StockDetailType } from 'types';
import { isNumericString } from 'utils/common';
import TradeAlertModal from './TradeAlertModal';
const MyAsset = 10000000;

type BuySectionProps = {
code: string;
data: StockDetailType;
};

export default function BuySection({ code, data }: BuySectionProps) {
const { stck_prpr, stck_mxpr, stck_llam } = data;

const [currPrice, setCurrPrice] = useState<string>(stck_prpr);

const { isOpen, toggleModal } = useTradeAlertModalStore();

const [count, setCount] = useState<number>(0);

const [upperLimitFlag, setUpperLimitFlag] = useState<boolean>(false);
const [lowerLimitFlag, setLowerLimitFlag] = useState<boolean>(false);
const [lackAssetFlag, setLackAssetFlag] = useState<boolean>(false);
const timerRef = useRef<number | null>(null);

const handlePriceChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!isNumericString(e.target.value)) return;

setCurrPrice(e.target.value);
};

const handlePriceInputBlur = (e: FocusEvent<HTMLInputElement>) => {
const n = +e.target.value;
if (n > +stck_mxpr) {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setCurrPrice(stck_mxpr);

setUpperLimitFlag(true);
timerRef.current = window.setTimeout(() => {
setUpperLimitFlag(false);
}, 2000);
return;
}

if (n < +stck_llam) {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setCurrPrice(stck_llam);

setLowerLimitFlag(true);
timerRef.current = window.setTimeout(() => {
setLowerLimitFlag(false);
}, 2000);
return;
}
};

const handleBuy = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

const price = +currPrice * count;

if (price > MyAsset) {
setLackAssetFlag(true);
timerRef.current = window.setTimeout(() => {
setLackAssetFlag(false);
}, 2000);
return;
}
toggleModal();
};

return (
<>
<form className='flex flex-col' onSubmit={handleBuy}>
<div className='my-4'>
<div className='flex items-center justify-between h-12'>
<p className='mr-3 w-14'>매수 가격</p>
<input
type='text'
value={currPrice}
onChange={handlePriceChange}
onBlur={handlePriceInputBlur}
className='flex-1 py-1 rounded-lg'
/>
</div>
{lowerLimitFlag && (
<div className='text-sm text-juga-red-60'>
이 주식의 최소 가격은 {(+stck_llam).toLocaleString()}입니다.
</div>
)}
{upperLimitFlag && (
<div className='text-xs text-juga-red-60'>
이 주식의 최대 가격은 {(+stck_mxpr).toLocaleString()}입니다.
</div>
)}
<div className='flex items-center justify-between h-12'>
<p className='mr-3 w-14'> 수량</p>
<input
type='number'
value={count}
onChange={(e) => setCount(+e.target.value)}
className='flex-1 py-1 rounded-lg'
min={1}
/>
</div>
</div>

<div className='my-5 h-[0.5px] w-full bg-juga-grayscale-200'></div>

<div className='flex flex-col gap-2'>
<div className='flex justify-between'>
<p>매수 가능 금액</p>
<p>0원</p>
</div>
<div className='flex justify-between'>
<p>총 주문 금액</p>
<p>{(+currPrice * count).toLocaleString()}</p>
</div>
</div>

<div className='flex flex-col justify-center h-10'>
{lackAssetFlag && (
<p className='text-xs text-juga-red-60'>잔액이 부족해요!</p>
)}
</div>
<button className='py-2 text-white rounded-lg bg-juga-red-60'>
매수하기
</button>
</form>
{isOpen && (
<TradeAlertModal
code={code}
stockName={data.hts_kor_isnm}
price={currPrice}
count={count}
/>
)}
</>
);
}
15 changes: 15 additions & 0 deletions FE/src/components/StocksDetail/SellSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Lottie from 'lottie-react';
import emptyAnimation from 'assets/emptyAnimation.json';

export default function SellSection() {
return (
<div className='flex flex-col items-center justify-center h-full'>
<Lottie
animationData={emptyAnimation}
className='w-40 h-40'
loop={false}
/>
<p>매도할 주식이 없어요</p>
</div>
);
}
Loading

0 comments on commit 948720c

Please sign in to comment.