From 503eb1908ca12169039b268d27a60745665a74aa Mon Sep 17 00:00:00 2001 From: "using6843@gmail.com" Date: Tue, 3 Dec 2024 15:57:09 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EC=9E=91=EB=AC=BC=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20UI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CropMarket/AskingPrice.tsx | 101 ++++++---- .../src/components/CropMarket/OwnCrop.tsx | 4 +- .../src/components/CropMarket/Pending.tsx | 14 +- .../src/components/CropMarket/Trade.tsx | 182 +++++++++--------- .../components/CropMarket/TradeSection.tsx | 8 +- .../src/components/Intro/SignUpModal.tsx | 2 +- apps/frontend/src/pages/CropMarket.tsx | 8 +- apps/frontend/src/pages/Intro.tsx | 10 +- apps/frontend/src/services/AuthApi.tsx | 22 +-- apps/frontend/src/services/OrderApi.tsx | 4 +- 10 files changed, 197 insertions(+), 158 deletions(-) diff --git a/apps/frontend/src/components/CropMarket/AskingPrice.tsx b/apps/frontend/src/components/CropMarket/AskingPrice.tsx index b0f39ce..a143d12 100644 --- a/apps/frontend/src/components/CropMarket/AskingPrice.tsx +++ b/apps/frontend/src/components/CropMarket/AskingPrice.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react'; + interface AskingPriceProps { validCropName: string; marketData: { @@ -8,52 +10,81 @@ interface AskingPriceProps { } const AskingPrice: React.FC = ({ validCropName, marketData }) => { + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = + containerRef.current.scrollHeight / 2 - containerRef.current.clientHeight / 2; + } + }, [marketData.sellOrders, marketData.buyOrders]); + if (!validCropName) { return
작물 정보가 없습니다.
; } + const maxItems = 4; + const emptyOrder = { price: 0, quantity: 0 }; + + const sellOrdersToDisplay = [ + ...new Array(Math.max(0, maxItems - marketData.sellOrders.length)).fill(emptyOrder), + ...marketData.sellOrders.sort((a, b) => b.price - a.price).slice(0, maxItems) + ]; + + const buyOrdersToDisplay = [ + ...marketData.buyOrders.sort((a, b) => b.price - a.price).slice(0, maxItems), + ...new Array(Math.max(0, maxItems - marketData.buyOrders.length)).fill(emptyOrder) + ]; + return ( -
-
+
+
구분
가격
수량
-
- {marketData.sellOrders - .sort((a, b) => b.price - a.price) - .map((order, idx) => ( -
- {order.price === marketData.nowPrice && ( -
- )} -
팔아요
-
₩ {order.price}
-
{order.quantity}
+
+ {sellOrdersToDisplay.map((order, idx) => ( +
+ {order.price === marketData.nowPrice && order.price !== 0 && ( +
+ )} +
{order.price > 0 ? '팔아요' : '\u00A0'}
+
+ {order.price > 0 ? `₩ ${order.price.toLocaleString()}` : '\u00A0'} +
+
+ {order.quantity > 0 ? order.quantity.toLocaleString() : '\u00A0'} +
+
+ ))} + {(marketData.sellOrders.length > 0 || marketData.buyOrders.length > 0) && ( +
+ )} + {buyOrdersToDisplay.map((order, idx) => ( +
+ {order.price === marketData.nowPrice && order.price !== 0 && ( +
+ )} +
{order.price > 0 ? '살게요' : '\u00A0'}
+
+ {order.price > 0 ? `₩ ${order.price.toLocaleString()}` : '\u00A0'}
- ))} - {marketData.buyOrders - .sort((a, b) => b.price - a.price) - .map((order, idx) => ( -
- {order.price === marketData.nowPrice && ( -
- )} -
살게요
-
₩ {order.price}
-
{order.quantity}
+
+ {order.quantity > 0 ? order.quantity.toLocaleString() : '\u00A0'}
- ))} +
+ ))}
); diff --git a/apps/frontend/src/components/CropMarket/OwnCrop.tsx b/apps/frontend/src/components/CropMarket/OwnCrop.tsx index 0e009b0..bdbcf29 100644 --- a/apps/frontend/src/components/CropMarket/OwnCrop.tsx +++ b/apps/frontend/src/components/CropMarket/OwnCrop.tsx @@ -31,8 +31,8 @@ const OwnCrop: React.FC = ({ ownCrop, cropNameList, nowPrice }) => return ( {cropDisplayName} - {totalQuantity} - ₩ {price * totalQuantity} + {totalQuantity.toLocaleString()} + ₩ {(price * totalQuantity).toLocaleString()} ); })} diff --git a/apps/frontend/src/components/CropMarket/Pending.tsx b/apps/frontend/src/components/CropMarket/Pending.tsx index 4e054c0..2eb46be 100644 --- a/apps/frontend/src/components/CropMarket/Pending.tsx +++ b/apps/frontend/src/components/CropMarket/Pending.tsx @@ -43,7 +43,9 @@ const Pending: React.FC = ({ cropNameList }) => { fetchPending(); }, []); - const handleCancel = async (order: MergedPendingData) => { + const handleCancel = async (e: React.MouseEvent, order: MergedPendingData) => { + e.currentTarget.blur(); + try { const data = { cropId: order.cropId, @@ -89,7 +91,7 @@ const Pending: React.FC = ({ cropNameList }) => {
{error}
) : ( <> -
+
작물명
주문시간
수량
@@ -105,12 +107,14 @@ const Pending: React.FC = ({ cropNameList }) => { >
{cropList[pending.cropName]}
{formatDate(pending.time)}
-
{pending.quantity}
+
{pending.unfilledQuantity.toLocaleString()}
{pending.price.toLocaleString()}
diff --git a/apps/frontend/src/components/CropMarket/Trade.tsx b/apps/frontend/src/components/CropMarket/Trade.tsx index bf44cd1..992209a 100644 --- a/apps/frontend/src/components/CropMarket/Trade.tsx +++ b/apps/frontend/src/components/CropMarket/Trade.tsx @@ -1,6 +1,6 @@ -import { CropData } from '@/types/Crop'; +import { CropData, OwnCropData } from '@/types/Crop'; import { useUser } from '../public/UserContext'; -import { useContext, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { AlertContext } from '@/components/public/AlertContext'; import { postLimitBuyOrder, @@ -15,119 +15,142 @@ interface TradeProps { currentCrop: number; cropNameList: CropData[]; setOrderType: (order: string) => void; + ownCrop: OwnCropData[]; } -const Trade: React.FC = ({ trade, order, currentCrop, cropNameList, setOrderType }) => { +const Trade: React.FC = ({ + trade, + order, + currentCrop, + cropNameList, + setOrderType, + ownCrop +}) => { const { alert } = useContext(AlertContext); const [price, setPrice] = useState(0); const [totalAmount, setTotalAmount] = useState(0); const [quantity, setQuantity] = useState(0); - const { availableCash } = useUser(); + const [isButtonDisabled, setIsButtonDisabled] = useState(true); + const { availableCash, fetch } = useUser(); - const onlyNumber = (e: React.KeyboardEvent) => { - const inputElement = e.target as HTMLInputElement; - inputElement.value = inputElement.value.replace(/[^0-9]/g, ''); - }; - - const handleOrderType = (type: string) => { - setOrderType(type); + useEffect(() => { setQuantity(0); setPrice(0); setTotalAmount(0); + }, [trade, order]); + + useEffect(() => { + if (order === '시장가') { + if (trade === '매수') { + setIsButtonDisabled(totalAmount === 0); + } else { + setIsButtonDisabled(quantity === 0); + } + } else { + setIsButtonDisabled(price === 0 || quantity === 0); + } + }, [price, quantity, totalAmount, order, trade]); + + const handleOrderType = (type: string) => { + setOrderType(type); }; const handleIncrease = () => { - setPrice(prevPrice => prevPrice + 1000); + setPrice(prevPrice => prevPrice + 100); }; const handleDecrease = () => { - setPrice(prevPrice => (prevPrice - 1000 >= 0 ? prevPrice - 1000 : 0)); + setPrice(prevPrice => (prevPrice - 100 >= 0 ? prevPrice - 100 : 0)); }; const handlePriceChange = (e: React.ChangeEvent) => { - setPrice(Number(e.target.value)); + setPrice(Number(e.target.value.replace(/[^0-9]/g, ''))); }; const handleQuantityChange = (e: React.ChangeEvent) => { - setQuantity(Number(e.target.value)); + setQuantity(Number(e.target.value.replace(/[^0-9]/g, ''))); }; const handleMaxQuantity = () => { - if (order === '지정가') { + if (trade === '매수') { if (price > 0) { setQuantity(Math.floor(availableCash / price)); } } else { - // 현재 보유 작물 수 - // TODO - 웹 소켓 연결 후 추가하기 + const maxOwnCrop = ownCrop.find(o => o.cropId === currentCrop); + setQuantity(maxOwnCrop?.availableQuantity || 0); } }; const handleTotalAmountChange = (e: React.ChangeEvent) => { - setTotalAmount(Number(e.target.value)); + setTotalAmount(Number(e.target.value.replace(/[^0-9]/g, ''))); }; const handleMaxTotalAmount = () => { - setTotalAmount(Number(availableCash)); + setTotalAmount(availableCash); }; - const handleOrder = async () => { - const crop = cropNameList.find(crop => crop.cropId === currentCrop); + const handleOrder = async (e: React.MouseEvent) => { + e.currentTarget.blur(); + const crop = cropNameList.find(crop => crop.cropId === currentCrop); if (!crop) { console.error('Crop not found'); return; } - const cropId: number = crop?.cropId; - const tradingType: string = trade === '매수' ? 'buy' : 'sell'; - const orderType: string = order === '지정가' ? 'limit' : 'market'; + const cropId: number = crop.cropId; + const tradingType = trade === '매수' ? 'buy' : 'sell'; + const orderType = order === '지정가' ? 'limit' : 'market'; - let orderData; - let fetchMethod; + const isMarketOrder = orderType === 'market'; + const isBuyOrder = tradingType === 'buy'; - if (orderType === 'market') { - if (tradingType === 'buy') { - orderData = { - cropId, - orderType: tradingType, - tradingType: 'market', - totalAmount - }; - fetchMethod = postMarketBuyOrder; - } else { - orderData = { - cropId, - orderType: tradingType, - tradingType: 'market', - quantity - }; - fetchMethod = postMarketSellOrder; + if (isMarketOrder) { + if (isBuyOrder && totalAmount === 0) { + await alert('주문 총액을 입력해주세요.'); + return; + } + if (!isBuyOrder && quantity === 0) { + await alert('주문 수량을 입력해주세요.'); + return; + } + } else { + if (quantity === 0 || price === 0) { + await alert('가격과 주문 수량을 모두 입력해주세요.'); + return; } - } else if (orderType === 'limit') { - orderData = { - cropId, - orderType: tradingType, - tradingType: 'limit', - quantity, - price - }; - fetchMethod = tradingType === 'buy' ? postLimitBuyOrder : postLimitSellOrder; } - if (!orderData || !fetchMethod) { - console.error('Order data or fetch method is undefined'); - return; - } + const orderData = { + cropId, + orderType: tradingType, + tradingType: orderType, + ...(isMarketOrder ? (isBuyOrder ? { totalAmount } : { quantity }) : { quantity, price }) + }; + + const fetchMethod = isMarketOrder + ? isBuyOrder + ? postMarketBuyOrder + : postMarketSellOrder + : isBuyOrder + ? postLimitBuyOrder + : postLimitSellOrder; try { const response = await fetchMethod(orderData); await alert(response.message); + fetch(); } catch (error) { console.error(error); } }; + const displayValue = + trade === '매수' + ? availableCash.toLocaleString() + : ownCrop.find(o => o.cropId === currentCrop)?.availableQuantity; + return ( <>
@@ -137,9 +160,7 @@ const Trade: React.FC = ({ trade, order, currentCrop, cropNameList, @@ -155,23 +176,14 @@ const Trade: React.FC = ({ trade, order, currentCrop, cropNameList, { - onlyNumber(e); - }} onChange={handlePriceChange} className="w-24 text-center border rounded-md text-sm px-2 py-1" /> - -
@@ -182,15 +194,12 @@ const Trade: React.FC = ({ trade, order, currentCrop, cropNameList, { - onlyNumber(e); - }} onChange={handleQuantityChange} placeholder="0" - className="w-24 px-1 py-1 border border-gray rounded text-xs text-center" + className="w-24 px-1 py-1 border rounded text-xs text-center" />
주문 가능 - {availableCash.toLocaleString()} 원 + {displayValue}
@@ -222,15 +230,12 @@ const Trade: React.FC = ({ trade, order, currentCrop, cropNameList, { - onlyNumber(e); - }} onChange={trade === '매수' ? handleTotalAmountChange : handleQuantityChange} placeholder="0" - className="w-24 px-1 py-1 border border-gray rounded text-xs text-center" + className="w-24 px-1 py-1 border rounded text-xs text-center" />
주문 가능 - {availableCash.toLocaleString()} 원 + {displayValue}
diff --git a/apps/frontend/src/components/CropMarket/TradeSection.tsx b/apps/frontend/src/components/CropMarket/TradeSection.tsx index c0aef28..daf1b0a 100644 --- a/apps/frontend/src/components/CropMarket/TradeSection.tsx +++ b/apps/frontend/src/components/CropMarket/TradeSection.tsx @@ -1,14 +1,15 @@ import { useState } from 'react'; import Trade from '@/components/CropMarket/Trade'; import Pending from '@/components/CropMarket/Pending'; -import { CropData } from '@/types/Crop'; +import { CropData, OwnCropData } from '@/types/Crop'; interface TradeProps { currentCrop: number; cropNameList: CropData[]; + ownCrop: OwnCropData[]; } -const TradeSection: React.FC = ({ currentCrop, cropNameList }) => { +const TradeSection: React.FC = ({ currentCrop, cropNameList, ownCrop }) => { const [tradeType, setTradeType] = useState('매수'); const [orderType, setOrderType] = useState('지정가'); @@ -25,7 +26,7 @@ const TradeSection: React.FC = ({ currentCrop, cropNameList }) => { {modalStep !== ModalStep.None && ( -
-
e.stopPropagation()} +
+
e.stopPropagation()} >
{ export const getIntroduce = async () => { try { - const response = await api.get('auth/introduce'); + const response = await api.get('auth/introduce'); - if (response.data.code === 200) { - const { introduce } = response.data.data; + if (response.data.code === 200) { + const { introduce } = response.data.data; - return { - success: true, - message: response.data.message, - introduce - }; - } + return { + success: true, + message: response.data.message, + introduce + }; + } - return { success: false, message: '알 수 없는 오류가 발생했습니다.' }; + return { success: false, message: '알 수 없는 오류가 발생했습니다.' }; } catch (error) { - return handleError(error, '데이터 로딩 중 오류가 발생했습니다.'); + return handleError(error, '데이터 로딩 중 오류가 발생했습니다.'); } }; diff --git a/apps/frontend/src/services/OrderApi.tsx b/apps/frontend/src/services/OrderApi.tsx index 54f758f..83a7a89 100644 --- a/apps/frontend/src/services/OrderApi.tsx +++ b/apps/frontend/src/services/OrderApi.tsx @@ -72,7 +72,7 @@ export const postMarketBuyOrder = async (data: Order) => { if (response.data.code === 201) { return { success: true, message: response.data.message }; - } else if(response.data.code === 400) { + } else if (response.data.code === 400) { return { success: true, message: response.data.message }; } @@ -88,7 +88,7 @@ export const postMarketSellOrder = async (data: Order) => { if (response.data.code === 201) { return { success: true, message: response.data.message }; - } else if(response.data.code === 400) { + } else if (response.data.code === 400) { return { success: true, message: response.data.message }; }