From 17585ae34e007cfdb78f7c6517c832685a8f65e6 Mon Sep 17 00:00:00 2001 From: cindyCho Date: Sat, 14 Dec 2024 10:03:19 +0900 Subject: [PATCH 1/4] =?UTF-8?q?remove:=20=ED=8C=8C=EC=9D=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edit-cancel/edit-table-body.tsx | 48 ---- .../edit-cancel/edit-table-header.tsx | 26 -- .../transaction-form/edit-cancel/index.tsx | 191 ------------- .../_components/transaction-form/history.tsx | 64 ----- .../_components/transaction-form/index.tsx | 102 ------- .../transaction-form/trade/bidding.tsx | 60 ---- .../trade/buy-form-buttons.tsx | 45 --- .../trade/buyable-quantity.tsx | 42 --- .../transaction-form/trade/count-dropdown.tsx | 62 ----- .../transaction-form/trade/current-price.tsx | 25 -- .../transaction-form/trade/index.tsx | 261 ------------------ .../transaction-form/trade/order-field.tsx | 98 ------- .../trade/price-type-dropdown.tsx | 37 --- .../transaction-form/trade/total-amount.tsx | 22 -- .../transaction-form/transaction-table.tsx | 117 -------- 15 files changed, 1200 deletions(-) delete mode 100644 src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-body.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-header.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/edit-cancel/index.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/history.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/index.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/bidding.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/buy-form-buttons.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/buyable-quantity.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/count-dropdown.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/current-price.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/index.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/order-field.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/price-type-dropdown.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/trade/total-amount.tsx delete mode 100644 src/app/search/[id]/_components/transaction-form/transaction-table.tsx diff --git a/src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-body.tsx b/src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-body.tsx deleted file mode 100644 index dd45f29..0000000 --- a/src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-body.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import CheckButton from "@/components/common/check-button"; -import { LimitPriceOrderHistory } from "@/types/transaction"; -import cn from "@/utils/cn"; -import { getKoreanPrice } from "@/utils/price"; - -interface EditTableBodyProps { - data: LimitPriceOrderHistory; - isChecked: boolean; - toggleSelection: (orderId: string) => void; -} - -export default function EditTableBody({ - data, - isChecked, - toggleSelection, -}: EditTableBodyProps) { - return ( - - - toggleSelection(data.OrderId.toString())} - /> - - -
- - {data.type} 정정 - - {data.OrderId} -
- {data.stockName} - - {data.remainCount} - {data.stockCount} - {getKoreanPrice(data.buyPrice)}원 - - ); -} diff --git a/src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-header.tsx b/src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-header.tsx deleted file mode 100644 index 1119901..0000000 --- a/src/app/search/[id]/_components/transaction-form/edit-cancel/edit-table-header.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import CheckButton from "@/components/common/check-button"; - -const titles = [ - , - "주문 정보", - "잔량", - "주문 수량", - "주문 가격", -]; - -export default function EditTableHeader() { - return ( - - - {titles.map((title, index) => ( - - {title} - - ))} - - - ); -} diff --git a/src/app/search/[id]/_components/transaction-form/edit-cancel/index.tsx b/src/app/search/[id]/_components/transaction-form/edit-cancel/index.tsx deleted file mode 100644 index 17c044c..0000000 --- a/src/app/search/[id]/_components/transaction-form/edit-cancel/index.tsx +++ /dev/null @@ -1,191 +0,0 @@ -"use client"; - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import Image from "next/image"; -import { useState } from "react"; - -import { cancelTrade, getTrade, modifyTrade } from "@/api/transaction"; -import Button from "@/components/common/button"; -import { useStockInfoContext } from "@/context/stock-info-context"; -import { useAuth } from "@/hooks/use-auth"; -import { useToast } from "@/store/use-toast-store"; - -import LoadingSpinner from "../../loading-spiner"; -import Trade from "../trade"; -import TransactionTable from "../transaction-table"; -import EditTableBody from "./edit-table-body"; -import EditTableHeader from "./edit-table-header"; - -export default function EditCancel() { - const { stockName } = useStockInfoContext(); - const { token, isAuthenticated } = useAuth(); - const { showToast } = useToast(); - const queryClient = useQueryClient(); - - const { - data: limitOrderData, - isLoading, - isPending, - } = useQuery({ - queryKey: ["limitOrder", `${stockName}`], - queryFn: () => getTrade(token, stockName), - enabled: !!isAuthenticated && !!token, - }); - - const [selectedOrders, setSelectedOrders] = useState([]); - const [isEditForm, setIsEditForm] = useState(false); - const [isCancelTable, setIsCancelTable] = useState(false); - - const findOrder = (orderId: string) => - limitOrderData?.find((data) => data.OrderId.toString() === orderId); - - const toggleOrderSelection = (orderId: string) => { - setSelectedOrders((prev) => - prev.includes(orderId) ? prev.filter((id) => id !== orderId) : [orderId], - ); - }; - - const handleEdit = () => { - if (selectedOrders.length > 0) { - setIsEditForm(true); - } else { - showToast("정정할 데이터를 선택해주세요.", "error"); - } - }; - - const handleCancel = () => { - if (selectedOrders.length > 0) { - setIsCancelTable(true); - } else { - showToast("취소할 데이터를 선택해주세요.", "error"); - } - }; - - const { mutate: cancelTradeMutate } = useMutation({ - mutationFn: cancelTrade, - onSuccess: () => { - showToast("주문이 취소되었습니다", "success"); - queryClient.invalidateQueries({ queryKey: ["limitOrder"] }); - }, - onSettled: () => { - setIsCancelTable(false); - }, - onError: (error) => { - showToast(error.message, "error"); - }, - }); - - const { mutate: modifyTradeMutate } = useMutation({ - mutationFn: modifyTrade, - onSuccess: () => { - showToast("주문을 수정했습니다.", "success"); - queryClient.invalidateQueries({ queryKey: ["limitOrder"] }); - }, - onSettled: () => { - setIsEditForm(false); - setSelectedOrders([]); - }, - onError: (error) => { - showToast(error.message, "error"); - }, - }); - - const handleCancelConfirm = (orderId: string) => { - cancelTradeMutate({ token, orderId }); - }; - - if (isEditForm) { - const order = findOrder(selectedOrders[0]); - return ( - - ); - } - - if (isLoading || isPending) { - return ; - } - - if (isCancelTable) { - return ( - <> - {selectedOrders.map((orderId) => { - const order = findOrder(orderId); - return order ? ( - setIsCancelTable(false)} - onClickConfirm={() => { - handleCancelConfirm(orderId); - }} - /> - ) : ( - "취소 정보를 받아오지 못했습니다." - ); - })} - - ); - } - - return ( - <> -
- - - - {limitOrderData && limitOrderData.length > 0 ? ( - [...limitOrderData] - .sort((a, b) => b.OrderId - a.OrderId) - .map((data) => ( - - )) - ) : ( - - - - )} - -
- 지갑 그림 - 지정가 거래 내역이 없습니다! -
-
- {limitOrderData && limitOrderData.length > 0 && ( -
- - -
- )} - - ); -} diff --git a/src/app/search/[id]/_components/transaction-form/history.tsx b/src/app/search/[id]/_components/transaction-form/history.tsx deleted file mode 100644 index c2a0a48..0000000 --- a/src/app/search/[id]/_components/transaction-form/history.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import Image from "next/image"; - -import { getTradeHistory } from "@/api/transaction"; -import { useStockInfoContext } from "@/context/stock-info-context"; -import { useAuth } from "@/hooks/use-auth"; - -import LoadingSpinner from "../loading-spiner"; -import TransactionTable from "./transaction-table"; - -export default function History() { - const { token, isAuthenticated } = useAuth(); - const { stockName } = useStockInfoContext(); - - const { - data: tradeHistoryData, - isLoading, - isPending, - } = useQuery({ - queryKey: ["tradeHistory", `${stockName}`], - queryFn: () => getTradeHistory(token, stockName), - enabled: !!isAuthenticated && !!token, - }); - - if (isLoading || isPending) { - return ; - } - - return ( -
- {tradeHistoryData && tradeHistoryData?.length > 0 ? ( - [...tradeHistoryData] - .sort((a, b) => b.id - a.id) - .map((history) => ( - - )) - ) : ( -
- 지갑 그림 - 체결내역이 없습니다. -
- 거래를 시작해보세요! -
- )} -
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/index.tsx b/src/app/search/[id]/_components/transaction-form/index.tsx deleted file mode 100644 index 4b7f59c..0000000 --- a/src/app/search/[id]/_components/transaction-form/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import Image from "next/image"; -import Link from "next/link"; - -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/common/tabs"; -import { StockInfoProvider } from "@/context/stock-info-context"; -import { useAuth } from "@/hooks/use-auth"; - -import { StockInfo } from "../../types"; -import LoadingSpinner from "../loading-spiner"; -import EditCancel from "./edit-cancel"; -import History from "./history"; -import Trade from "./trade"; - -interface TransactionFormProps { - stockName: string; - stockInfo: StockInfo; -} - -export default function TransactionForm({ - stockName, - stockInfo, -}: TransactionFormProps) { - const { isAuthenticated, isInitialized } = useAuth(); - - if (!isInitialized) { - return ( -
-

거래하기

- -
- ); - } - - return ( -
-

거래하기

- {isAuthenticated ? ( - - - - - 매수 - - - 매도 - - 체결내역 - 정정 / 취소 - - - - - - - - - - - - - - - - ) : ( -
- 보안 아이콘 -
- 로그인이 필요해요! -
- - 가상 거래를 하기 위해서는 로그인이 필수적이에요! - - - 로그인 하기 - -
-
- )} -
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/bidding.tsx b/src/app/search/[id]/_components/transaction-form/trade/bidding.tsx deleted file mode 100644 index 69cf129..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/bidding.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { - Control, - Controller, - FieldErrors, - UseFormSetValue, -} from "react-hook-form"; - -import Input from "@/components/common/input"; -import { useStockInfoContext } from "@/context/stock-info-context"; -import { getKoreanPrice } from "@/utils/price"; -import { BuyFormData } from "@/validation/schema/transaction-form"; - -import CountDropdown from "./count-dropdown"; - -interface BiddingProps { - state: number | undefined; - setState: UseFormSetValue; - control: Control; - errors: FieldErrors["bidding"]; -} - -export default function Bidding({ - state, - setState, - control, - errors, -}: BiddingProps) { - const { stockInfo } = useStockInfoContext(); - - return ( -
- setState("bidding", value ? Number(value) : 0)} - stockPrice={stockInfo.stockPrice} - /> - ( - - field.onChange( - e.target.value === "" ? undefined : Number(e.target.value), - ) - } - error={errors?.message} - /> - )} - /> -
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/buy-form-buttons.tsx b/src/app/search/[id]/_components/transaction-form/trade/buy-form-buttons.tsx deleted file mode 100644 index eb89430..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/buy-form-buttons.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import Button from "@/components/common/button"; -import cn from "@/utils/cn"; - -interface BuyFormButtonsProps { - orderType: "buy" | "sell" | "edit"; - handleReset: () => void; - handleSubmit: () => void; -} - -export default function BuyFormButtons({ - orderType, - handleReset, - handleSubmit, -}: BuyFormButtonsProps) { - let buttonText; - if (orderType === "buy") { - buttonText = "매수"; - } else if (orderType === "sell") { - buttonText = "매도"; - } else if (orderType === "edit") { - buttonText = "정정"; - } - - return ( -
- - -
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/buyable-quantity.tsx b/src/app/search/[id]/_components/transaction-form/trade/buyable-quantity.tsx deleted file mode 100644 index bdc2559..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/buyable-quantity.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { fetchMyStocks } from "@/api/side-Info"; -import { useStockInfoContext } from "@/context/stock-info-context"; -import { useAuth } from "@/hooks/use-auth"; -import MyStockMap from "@/utils/my-stock-count"; -import { calculateBuyableQuantity, getKoreanPrice } from "@/utils/price"; - -interface BuyableQuantityProps { - type: "buy" | "sell" | "edit"; - bidding: number; -} - -export default function BuyableQuantity({ - type, - bidding, -}: BuyableQuantityProps) { - const { isAuthenticated, token, deposit } = useAuth(); - const { stockName } = useStockInfoContext(); - const { data: stockHoldings } = useQuery({ - queryKey: ["myStocks"], - queryFn: () => fetchMyStocks(token!), - enabled: !!isAuthenticated && !!token, - }); - - const stockMap = new MyStockMap(stockHoldings); - return ( -
- - {type === "buy" ? "매수" : "매도"} 가능 주식 - -
- - {type === "buy" - ? getKoreanPrice(calculateBuyableQuantity(deposit, bidding)) - : stockMap.findStockCount(stockName)} - - 주 -
-
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/count-dropdown.tsx b/src/app/search/[id]/_components/transaction-form/trade/count-dropdown.tsx deleted file mode 100644 index 1f85837..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/count-dropdown.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import clsx from "clsx"; -import { Dispatch, memo, SetStateAction } from "react"; - -import Dropdown from "@/components/common/dropdown"; -import { getKoreanPrice } from "@/utils/price"; - -interface CountDropdownProps { - state: T; - setState: Dispatch>; - title: string; - number?: number | boolean; - stockPrice?: string | boolean; -} - -function CountDropdown({ - state, - setState, - number, - title, - stockPrice, -}: CountDropdownProps) { - return ( - setState(value as T)} - > - - {title} - - - {typeof number === "number" && - number > 0 && - Array.from({ length: number }, (_, index) => ( - - {index + 1} - - ))} - {stockPrice && - Array.from( - { length: 21 }, - (_, index) => Number(stockPrice) - 10000 + index * 1000, - ).map((price) => ( - - {getKoreanPrice(price)} 원 - - ))} - - - ); -} - -export default memo(CountDropdown); diff --git a/src/app/search/[id]/_components/transaction-form/trade/current-price.tsx b/src/app/search/[id]/_components/transaction-form/trade/current-price.tsx deleted file mode 100644 index ade8e15..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/current-price.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable react/no-array-index-key */ - -"use client"; - -import { useStockInfoContext } from "@/context/stock-info-context"; -import { getKoreanPrice } from "@/utils/price"; - -export default function CurrentPrice() { - const { stockInfo } = useStockInfoContext(); - - return ( -
- {Array.from({ length: 4 }).map((_, index) => ( -
- {getKoreanPrice(stockInfo.stockPrice)} -
- ))} - {Array.from({ length: 4 }).map((_, index) => ( -
- {getKoreanPrice(stockInfo.stockPrice)} -
- ))} -
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/index.tsx b/src/app/search/[id]/_components/transaction-form/trade/index.tsx deleted file mode 100644 index b8e2b30..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/index.tsx +++ /dev/null @@ -1,261 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; - -import { - buyAtLimitPrice, - buyAtMarketPrice, - sellAtLimitPrice, - sellAtMarketPrice, -} from "@/api/transaction"; -import { useTabsContext } from "@/components/common/tabs"; -import { useStockInfoContext } from "@/context/stock-info-context"; -import { useAuth } from "@/hooks/use-auth"; -import { useToast } from "@/store/use-toast-store"; -import { - LimitPriceOrderHistory, - ModifyTradeFormData, -} from "@/types/transaction"; -import { calculateTotalOrderAmount } from "@/utils/price"; -import { - BuyFormData, - BuyFormSchema, -} from "@/validation/schema/transaction-form"; - -import TransactionTable from "../transaction-table"; -import BuyFormButtons from "./buy-form-buttons"; -import BuyableQuantity from "./buyable-quantity"; -import CurrentPrice from "./current-price"; -import OrderField from "./order-field"; -import PriceTypeDropdown from "./price-type-dropdown"; -import TotalAmount from "./total-amount"; - -interface TradeProps { - type: "buy" | "sell" | "edit"; - defaultData?: LimitPriceOrderHistory; - handleMutate?: (data: ModifyTradeFormData) => void; -} - -export default function Trade({ type, defaultData, handleMutate }: TradeProps) { - const [priceType, setPriceType] = useState("지정가"); - const [isConfirmationPage, setIsConfirmationPage] = useState(false); - const { setActiveTab } = useTabsContext(); - const { stockName, stockInfo } = useStockInfoContext(); - const { token } = useAuth(); - const { showToast } = useToast(); - const queryClient = useQueryClient(); - - const { - control, - watch, - reset, - handleSubmit, - setValue, - formState: { errors }, - } = useForm({ - resolver: zodResolver(BuyFormSchema), - defaultValues: - type === "edit" && defaultData - ? { - count: defaultData.stockCount, - bidding: defaultData.buyPrice, - } - : { - count: undefined, - bidding: undefined, - }, - }); - - const watchedCount = watch("count"); - const watchedBidding = watch("bidding"); - - const handleReset = () => { - reset({ - count: undefined, - bidding: undefined, - }); - }; - - const handlePriceTypeChange = (newPriceType: string) => { - setPriceType(newPriceType); - if (newPriceType === "현재가") { - setValue("bidding", Number(stockInfo.stockPrice)); - } else { - setValue("bidding", 0); - } - }; - - const handleConfirmPurchase = () => { - setIsConfirmationPage(true); - }; - - const { mutate: buyAtMarketPriceMutate } = useMutation({ - mutationFn: buyAtMarketPrice, - onSuccess: () => { - setActiveTab("history"); - showToast("현재가로 매수가 완료되었습니다.", "success"); - queryClient.invalidateQueries({ queryKey: ["tradeHistory"] }); - }, - onError: (error) => { - showToast(error.message, "error"); - }, - }); - - const { mutate: sellAtMarketPriceMutate } = useMutation({ - mutationFn: sellAtMarketPrice, - onSuccess: () => { - setActiveTab("history"); - showToast("현재가로 매도가 완료되었습니다.", "success"); - queryClient.invalidateQueries({ queryKey: ["tradeHistory"] }); - }, - onError: (error) => { - showToast(error.message, "error"); - }, - }); - - const { mutate: buyAtLimitPriceMutate } = useMutation({ - mutationFn: buyAtLimitPrice, - onSuccess: () => { - setActiveTab("edit-cancel"); - queryClient.invalidateQueries({ queryKey: ["limitOrder"] }); - showToast("지정가로 매수가 완료되었습니다.", "success"); - }, - onError: (error) => { - showToast(error.message, "error"); - }, - }); - - const { mutate: sellAtLimitPriceMutate } = useMutation({ - mutationFn: sellAtLimitPrice, - onSuccess: () => { - setActiveTab("edit-cancel"); - queryClient.invalidateQueries({ queryKey: ["limitOrder"] }); - showToast("지정가로 매도가 완료되었습니다.", "success"); - }, - onError: (error) => { - showToast(error.message, "error"); - }, - }); - - const handleBuy = () => { - if (priceType === "현재가") { - buyAtMarketPriceMutate({ - token, - data: { stockName, quantity: watchedCount }, - }); - } else { - buyAtLimitPriceMutate({ - token, - data: { stockName, limitPrice: watchedBidding, quantity: watchedCount }, - }); - } - }; - - const handleSell = () => { - if (priceType === "현재가") { - sellAtMarketPriceMutate({ - token, - data: { stockName, quantity: watchedCount }, - }); - } else { - sellAtLimitPriceMutate({ - token, - data: { stockName, limitPrice: watchedBidding, quantity: watchedCount }, - }); - } - }; - - const handleEdit = () => { - if (type !== "edit" || !handleMutate) return; - - handleMutate({ - token, - orderId: defaultData?.OrderId, - data: { stockName, limitPrice: watchedBidding, quantity: watchedCount }, - }); - }; - - let onClickConfirmHandler; - - if (type === "buy") { - onClickConfirmHandler = handleBuy; - } else if (type === "sell") { - onClickConfirmHandler = handleSell; - } else if (type === "edit") { - onClickConfirmHandler = handleEdit; - } - - let color; - if (type === "buy") { - color = "red" as const; - } else if (type === "sell") { - color = "blue" as const; - } else if (type === "edit") { - color = "green" as const; - } - - if (isConfirmationPage) { - return ( - setIsConfirmationPage(false)} - onClickConfirm={onClickConfirmHandler} - /> - ); - } - - return ( -
- -
- - - - {type !== "edit" && ( - - )} - - - - handleSubmit(handleConfirmPurchase)()} - /> -
-
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/order-field.tsx b/src/app/search/[id]/_components/transaction-form/trade/order-field.tsx deleted file mode 100644 index 49c836b..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/order-field.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { - Control, - Controller, - FieldErrors, - UseFormSetValue, -} from "react-hook-form"; - -import Input from "@/components/common/input"; -import { useStockInfoContext } from "@/context/stock-info-context"; -import { getKoreanPrice } from "@/utils/price"; -import { BuyFormData } from "@/validation/schema/transaction-form"; - -import CountDropdown from "./count-dropdown"; - -interface OrderFieldProps { - title: string; - type: "count" | "bidding"; - placeholder: string; - inputSuffix: string; - state: number | undefined; - setState: UseFormSetValue; - control: Control; - errors: FieldErrors["count"]; - quantity?: number; -} - -export default function OrderField({ - title, - type, - placeholder, - inputSuffix, - state, - setState, - control, - errors, - quantity, -}: OrderFieldProps) { - const { stockInfo } = useStockInfoContext(); - const numberRegex = /^[0-9,]*$/; - - const handleInputChange = ( - value: string, - field: { - onChange: (value: number | undefined) => void; - value: number | undefined; - }, - ) => { - if (value === "") { - field.onChange(undefined); - return; - } - - if (!numberRegex.test(value)) { - return; - } - - const cleanValue = value.replace(/,/g, ""); - - if (cleanValue === "") { - field.onChange(undefined); - return; - } - - const numValue = Number(cleanValue); - - if (Number.isNaN(numValue) || !Number.isFinite(numValue)) { - return; - } - - field.onChange(numValue); - }; - - return ( -
- setState(type, value ? Number(value) : 0)} - stockPrice={type === "bidding" && stockInfo.stockPrice} - /> - ( - handleInputChange(e.target.value, field)} - error={errors?.message} - /> - )} - /> -
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/price-type-dropdown.tsx b/src/app/search/[id]/_components/transaction-form/trade/price-type-dropdown.tsx deleted file mode 100644 index 5df04f0..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/price-type-dropdown.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Dropdown from "@/components/common/dropdown"; - -interface PriceTypeDropdownProps { - orderType: "buy" | "sell" | "edit"; - priceType: string; - setPriceType: (value: string) => void; -} -export default function PriceTypeDropdown({ - orderType, - priceType, - setPriceType, -}: PriceTypeDropdownProps) { - return ( -
- {orderType === "edit" ? ( - - 지정가 - - ) : ( - setPriceType(value as string)} - className="flex-1" - > - {priceType} - - 지정가 - 현재가 - - - )} - - 시장가 - -
- ); -} diff --git a/src/app/search/[id]/_components/transaction-form/trade/total-amount.tsx b/src/app/search/[id]/_components/transaction-form/trade/total-amount.tsx deleted file mode 100644 index ee74223..0000000 --- a/src/app/search/[id]/_components/transaction-form/trade/total-amount.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { calculateTotalOrderAmount, getKoreanPrice } from "@/utils/price"; - -interface TotalAmountProps { - count: number; - bidding: number; -} - -export default function TotalAmount({ count, bidding }: TotalAmountProps) { - const totalAmount = getKoreanPrice(calculateTotalOrderAmount(count, bidding)); - - return ( - <> -
-
총 주문 금액
-
- {totalAmount}원 -
-
-
{totalAmount}원
- - ); -} diff --git a/src/app/search/[id]/_components/transaction-form/transaction-table.tsx b/src/app/search/[id]/_components/transaction-form/transaction-table.tsx deleted file mode 100644 index cf0680c..0000000 --- a/src/app/search/[id]/_components/transaction-form/transaction-table.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import Button from "@/components/common/button"; -import cn from "@/utils/cn"; -import { getKoreanPrice } from "@/utils/price"; - -interface TransactionTableProps { - color?: "red" | "green" | "blue"; - isSubmit?: boolean; - submittedData: { - stockName: string; - count: number; - bidding: number; - totalAmount: number; - buyOrder?: string; - }; - onClickGoBack?: () => void; - onClickConfirm?: () => void; -} - -export default function TransactionTable({ - color = "red", - isSubmit = true, - submittedData, - onClickGoBack, - onClickConfirm, -}: TransactionTableProps) { - const getBackgroundColor = () => { - const colorMap: { - [key in Exclude]: string; - } = { - red: "bg-[#FDEBEB]", - green: "bg-[#E9FFF0]", - blue: "bg-[#EDF1FC]", - }; - return colorMap[color]; - }; - - return ( -
- - - - - - - - - - - - - - - - - - - -
- 종목명 - {submittedData?.stockName}
- {submittedData.buyOrder} 수량 - - {getKoreanPrice(submittedData.count)} -
- {submittedData.buyOrder} 가격 - - {getKoreanPrice(submittedData.bidding)} -
- 총 주문 금액 - - {getKoreanPrice(submittedData.totalAmount)} -
- {isSubmit && ( -
- - -
- )} -
- ); -} From 9b4270a4041f518c294feb939494c065f7c1fa5e Mon Sep 17 00:00:00 2001 From: cindyCho Date: Sat, 14 Dec 2024 10:31:23 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EC=95=8C=EC=95=84=EB=B3=B4=EA=B8=B0=20=EC=89=BD=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buy-and-sell/buyable-quantity.tsx | 44 +++++++ .../buy-and-sell/current-price.tsx | 25 ++++ .../order-stock/buy-and-sell/form-buttons.tsx | 42 +++++++ .../order-stock/buy-and-sell/input-field.tsx | 98 +++++++++++++++ .../buy-and-sell/number-dropdown.tsx | 62 ++++++++++ .../buy-and-sell/price-type-dropdown.tsx | 39 ++++++ .../order-stock/buy-and-sell/total-amount.tsx | 22 ++++ .../edit-cancel/edit-table-row.tsx | 48 ++++++++ .../order-stock/edit-cancel/edit-table.tsx | 79 ++++++++++++ .../[id]/_components/order-stock/index.tsx | 76 ++++++++++++ .../[id]/_components/order-stock/layout.tsx | 14 +++ .../_components/order-stock/login-notice.tsx | 29 +++++ .../order-stock/order-history/index.tsx | 64 ++++++++++ .../_components/order-stock/trade-table.tsx | 114 ++++++++++++++++++ src/app/search/[id]/page.tsx | 7 +- src/app/search/[id]/types/index.ts | 11 ++ src/constants/trade.ts | 13 ++ 17 files changed, 782 insertions(+), 5 deletions(-) create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/form-buttons.tsx create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/input-field.tsx create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/number-dropdown.tsx create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/price-type-dropdown.tsx create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/total-amount.tsx create mode 100644 src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx create mode 100644 src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx create mode 100644 src/app/search/[id]/_components/order-stock/index.tsx create mode 100644 src/app/search/[id]/_components/order-stock/layout.tsx create mode 100644 src/app/search/[id]/_components/order-stock/login-notice.tsx create mode 100644 src/app/search/[id]/_components/order-stock/order-history/index.tsx create mode 100644 src/app/search/[id]/_components/order-stock/trade-table.tsx create mode 100644 src/constants/trade.ts diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx new file mode 100644 index 0000000..4e42af9 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx @@ -0,0 +1,44 @@ +import { useQuery } from "@tanstack/react-query"; + +import { fetchMyStocks } from "@/api/side-Info"; +import { useStockInfoContext } from "@/context/stock-info-context"; +import { useAuth } from "@/hooks/use-auth"; +import MyStockMap from "@/utils/my-stock-count"; +import { calculateBuyableQuantity, getKoreanPrice } from "@/utils/price"; + +import { TradeType } from "../../../types"; + +interface BuyableQuantityProps { + type: TradeType; + bidding: number; +} + +export default function BuyableQuantity({ + type, + bidding, +}: BuyableQuantityProps) { + const { isAuthenticated, token, deposit } = useAuth(); + const { stockName } = useStockInfoContext(); + const { data: stockHoldings } = useQuery({ + queryKey: ["myStocks"], + queryFn: () => fetchMyStocks(token!), + enabled: !!isAuthenticated && !!token, + }); + + const stockMap = new MyStockMap(stockHoldings); + return ( +
+ + {type === TradeType.Buy ? "매수" : "매도"} 가능 주식 + +
+ + {type === TradeType.Buy + ? getKoreanPrice(calculateBuyableQuantity(deposit, bidding)) + : stockMap.findStockCount(stockName)} + + 주 +
+
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx new file mode 100644 index 0000000..ade8e15 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx @@ -0,0 +1,25 @@ +/* eslint-disable react/no-array-index-key */ + +"use client"; + +import { useStockInfoContext } from "@/context/stock-info-context"; +import { getKoreanPrice } from "@/utils/price"; + +export default function CurrentPrice() { + const { stockInfo } = useStockInfoContext(); + + return ( +
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ {getKoreanPrice(stockInfo.stockPrice)} +
+ ))} + {Array.from({ length: 4 }).map((_, index) => ( +
+ {getKoreanPrice(stockInfo.stockPrice)} +
+ ))} +
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/form-buttons.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/form-buttons.tsx new file mode 100644 index 0000000..57d939b --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/form-buttons.tsx @@ -0,0 +1,42 @@ +import Button from "@/components/common/button"; +import { textMap } from "@/constants/trade"; +import cn from "@/utils/cn"; + +import { TradeType } from "../../../types"; + +interface FormButtonsProps { + orderType: TradeType; + handleReset: () => void; + handleSubmit: () => void; +} + +export default function FormButtons({ + orderType, + handleReset, + handleSubmit, +}: FormButtonsProps) { + return ( +
+ + +
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/input-field.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/input-field.tsx new file mode 100644 index 0000000..f7d14db --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/input-field.tsx @@ -0,0 +1,98 @@ +import { + Control, + Controller, + FieldErrors, + UseFormSetValue, +} from "react-hook-form"; + +import Input from "@/components/common/input"; +import { useStockInfoContext } from "@/context/stock-info-context"; +import { getKoreanPrice } from "@/utils/price"; +import { BuyFormData } from "@/validation/schema/transaction-form"; + +import NumberDropdown from "./number-dropdown"; + +interface InputFieldProps { + title: string; + type: "count" | "bidding"; + placeholder: string; + inputSuffix: string; + state: number | undefined; + setState: UseFormSetValue; + control: Control; + errors: FieldErrors["count"]; + quantity?: number; +} + +export default function InputField({ + title, + type, + placeholder, + inputSuffix, + state, + setState, + control, + errors, + quantity, +}: InputFieldProps) { + const { stockInfo } = useStockInfoContext(); + const numberRegex = /^[0-9,]*$/; + + const handleInputChange = ( + value: string, + field: { + onChange: (value: number | undefined) => void; + value: number | undefined; + }, + ) => { + if (value === "") { + field.onChange(undefined); + return; + } + + if (!numberRegex.test(value)) { + return; + } + + const cleanValue = value.replace(/,/g, ""); + + if (cleanValue === "") { + field.onChange(undefined); + return; + } + + const numValue = Number(cleanValue); + + if (Number.isNaN(numValue) || !Number.isFinite(numValue)) { + return; + } + + field.onChange(numValue); + }; + + return ( +
+ setState(type, value ? Number(value) : 0)} + stockPrice={type === "bidding" && stockInfo.stockPrice} + /> + ( + handleInputChange(e.target.value, field)} + error={errors?.message} + /> + )} + /> +
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/number-dropdown.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/number-dropdown.tsx new file mode 100644 index 0000000..75f84de --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/number-dropdown.tsx @@ -0,0 +1,62 @@ +import clsx from "clsx"; +import { Dispatch, memo, SetStateAction } from "react"; + +import Dropdown from "@/components/common/dropdown"; +import { getKoreanPrice } from "@/utils/price"; + +interface NumberDropdownProps { + state: T; + setState: Dispatch>; + title: string; + number?: number | boolean; + stockPrice?: string | boolean; +} + +function NumberDropdown({ + state, + setState, + number, + title, + stockPrice, +}: NumberDropdownProps) { + return ( + setState(value as T)} + > + + {title} + + + {typeof number === "number" && + number > 0 && + Array.from({ length: number }, (_, index) => ( + + {index + 1} + + ))} + {stockPrice && + Array.from( + { length: 21 }, + (_, index) => Number(stockPrice) - 10000 + index * 1000, + ).map((price) => ( + + {getKoreanPrice(price)} 원 + + ))} + + + ); +} + +export default memo(NumberDropdown); diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/price-type-dropdown.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/price-type-dropdown.tsx new file mode 100644 index 0000000..bc1f5b6 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/price-type-dropdown.tsx @@ -0,0 +1,39 @@ +import Dropdown from "@/components/common/dropdown"; + +import { PriceType, TradeType } from "../../../types"; + +interface PriceTypeDropdownProps { + orderType: TradeType; + priceType: PriceType; + setPriceType: (newPriceType: PriceType) => void; +} +export default function PriceTypeDropdown({ + orderType, + priceType, + setPriceType, +}: PriceTypeDropdownProps) { + return ( +
+ {orderType === TradeType.Edit ? ( + + 지정가 + + ) : ( + setPriceType(value as PriceType)} + className="flex-1" + > + {priceType} + + 지정가 + 현재가 + + + )} + + 시장가 + +
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/total-amount.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/total-amount.tsx new file mode 100644 index 0000000..71f0af0 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/total-amount.tsx @@ -0,0 +1,22 @@ +import { calculateTotalOrderAmount, getKoreanPrice } from "@/utils/price"; + +interface TotalAmountProps { + count: number; + bidding: number; +} + +export default function TotalAmount({ count, bidding }: TotalAmountProps) { + const totalAmount = getKoreanPrice(calculateTotalOrderAmount(count, bidding)); + + return ( + <> +
+
총 주문 금액
+
+ {totalAmount}원 +
+
+
{totalAmount}원
+ + ); +} diff --git a/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx new file mode 100644 index 0000000..5f6053b --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx @@ -0,0 +1,48 @@ +import CheckButton from "@/components/common/check-button"; +import { LimitPriceOrderHistory } from "@/types/transaction"; +import cn from "@/utils/cn"; +import { getKoreanPrice } from "@/utils/price"; + +interface EditTableBodyProps { + data: LimitPriceOrderHistory; + isChecked: boolean; + toggleSelection: (orderId: string) => void; +} + +export default function EditTableRow({ + data, + isChecked, + toggleSelection, +}: EditTableBodyProps) { + return ( + + + toggleSelection(data.OrderId.toString())} + /> + + +
+ + {data.type} 정정 + + {data.OrderId} +
+ {data.stockName} + + {data.remainCount} + {data.stockCount} + {getKoreanPrice(data.buyPrice)}원 + + ); +} diff --git a/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx new file mode 100644 index 0000000..de75bd3 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx @@ -0,0 +1,79 @@ +import Image from "next/image"; +import { Dispatch, SetStateAction } from "react"; + +import CheckButton from "@/components/common/check-button"; +import { LimitPriceOrderHistory } from "@/types/transaction"; + +import EditTableRow from "./edit-table-row"; + +const titles = [ + , + "주문 정보", + "잔량", + "주문 수량", + "주문 가격", +]; + +interface EditTableProps { + limitPriceHistory: LimitPriceOrderHistory[] | undefined; + selectedOrders: string[]; + setSelectedOrders: Dispatch>; +} + +export default function EditTable({ + limitPriceHistory, + selectedOrders, + setSelectedOrders, +}: EditTableProps) { + const toggleOrderSelection = (orderId: string) => { + setSelectedOrders((prev) => + prev.includes(orderId) ? prev.filter((id) => id !== orderId) : [orderId], + ); + }; + + return ( +
+ + + + {titles.map((title, index) => ( + + ))} + + + + {limitPriceHistory && limitPriceHistory.length > 0 ? ( + [...limitPriceHistory] + .sort((a, b) => b.OrderId - a.OrderId) + .map((data) => ( + + )) + ) : ( + + + + )} + +
+ {title} +
+ 지갑 그림 + 지정가 거래 내역이 없습니다! +
+
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/index.tsx b/src/app/search/[id]/_components/order-stock/index.tsx new file mode 100644 index 0000000..3c45dbc --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/index.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/common/tabs"; +import { StockInfoProvider } from "@/context/stock-info-context"; +import { useAuth } from "@/hooks/use-auth"; + +import { StockInfo, TradeType } from "../../types"; +import LoadingSpinner from "../loading-spiner"; +import BuyAndSell from "./buy-and-sell"; +import EditCancel from "./edit-cancel"; +import OrderStockLayout from "./layout"; +import LoginNotice from "./login-notice"; +import OrderHistory from "./order-history"; + +interface OrderStockProps { + stockName: string; + stockInfo: StockInfo; +} + +export default function OrderStock({ stockName, stockInfo }: OrderStockProps) { + const { isAuthenticated, isInitialized } = useAuth(); + + if (!isInitialized) { + return ( + + + + ); + } + + return ( + + {isAuthenticated ? ( + + + + + 매수 + + + 매도 + + 체결내역 + 정정 / 취소 + + + + + + + + + + + + + + + + ) : ( + // 로그인 안되어있으면 로그인 유도 + + )} + + ); +} diff --git a/src/app/search/[id]/_components/order-stock/layout.tsx b/src/app/search/[id]/_components/order-stock/layout.tsx new file mode 100644 index 0000000..5ef2956 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/layout.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from "react"; + +export default function OrderStockLayout({ + children, +}: { + children: ReactNode; +}) { + return ( +
+

거래하기

+ {children} +
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/login-notice.tsx b/src/app/search/[id]/_components/order-stock/login-notice.tsx new file mode 100644 index 0000000..f8021ae --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/login-notice.tsx @@ -0,0 +1,29 @@ +import Image from "next/image"; +import Link from "next/link"; + +export default function LoginNotice() { + return ( +
+ 보안 아이콘 +
+ 로그인이 필요해요! +
+ + 가상 거래를 하기 위해서는 로그인이 필수적이에요! + + + 로그인 하기 + +
+
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/order-history/index.tsx b/src/app/search/[id]/_components/order-stock/order-history/index.tsx new file mode 100644 index 0000000..52ac4b2 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/order-history/index.tsx @@ -0,0 +1,64 @@ +import { useQuery } from "@tanstack/react-query"; +import Image from "next/image"; + +import { getTradeHistory } from "@/api/transaction"; +import { useStockInfoContext } from "@/context/stock-info-context"; +import { useAuth } from "@/hooks/use-auth"; + +import LoadingSpinner from "../../loading-spiner"; +import TradeTable from "../trade-table"; + +export default function OrderHistory() { + const { token, isAuthenticated } = useAuth(); + const { stockName } = useStockInfoContext(); + + const { + data: tradeHistoryData, + isLoading, + isPending, + } = useQuery({ + queryKey: ["tradeHistory", `${stockName}`], + queryFn: () => getTradeHistory(token, stockName), + enabled: !!isAuthenticated && !!token, + }); + + if (isLoading || isPending) { + return ; + } + + return ( +
+ {tradeHistoryData && tradeHistoryData?.length > 0 ? ( + [...tradeHistoryData] + .sort((a, b) => b.id - a.id) + .map((history) => ( + + )) + ) : ( +
+ 지갑 그림 + 체결내역이 없습니다. +
+ 거래를 시작해보세요! +
+ )} +
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/trade-table.tsx b/src/app/search/[id]/_components/order-stock/trade-table.tsx new file mode 100644 index 0000000..d80b7c2 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/trade-table.tsx @@ -0,0 +1,114 @@ +import Button from "@/components/common/button"; +import cn from "@/utils/cn"; +import { getKoreanPrice } from "@/utils/price"; + +const COLOR_VARIANTS = { + red: { + background: "bg-red-100", + button: "bg-red-500 hover:bg-red-500/80", + }, + green: { + background: "bg-green-100", + button: "bg-green-500 hover:bg-green-500/80", + }, + blue: { + background: "bg-blue-100", + button: "bg-blue-500 hover:bg-blue-500/80", + }, +} as const; + +function TableRow({ + label, + value, + color = "red", + className, +}: { + label: string; + value: string | number; + color?: keyof typeof COLOR_VARIANTS; + className?: string; +}) { + return ( + + + {label} + + {value} + + ); +} + +interface TradeTableProps { + color?: keyof typeof COLOR_VARIANTS; + isSubmit?: boolean; + submittedData: { + stockName: string; + count: number; + bidding: number; + totalAmount: number; + buyOrder?: string; + }; + onClickGoBack?: () => void; + onClickConfirm?: () => void; +} + +export default function TradeTable({ + color = "red", + isSubmit = true, + submittedData, + onClickGoBack, + onClickConfirm, +}: TradeTableProps) { + return ( +
+ + + + + + + +
+ {isSubmit && ( +
+ + +
+ )} +
+ ); +} diff --git a/src/app/search/[id]/page.tsx b/src/app/search/[id]/page.tsx index 0c4dcf7..ea14aa3 100644 --- a/src/app/search/[id]/page.tsx +++ b/src/app/search/[id]/page.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; import CandlestickChartContainer from "./_components/candle-chart-container"; +import OrderStock from "./_components/order-stock"; import StockHeader from "./_components/stock-header"; -import TransactionForm from "./_components/transaction-form"; import TutorialContainer from "./_components/tutorial/tutorial-container"; import { ChartResponse, StockInfo, VolumeResponse } from "./types"; @@ -72,10 +72,7 @@ export default async function StockPage({ initialVolumeData={initialData.volumeData} /> - + ); diff --git a/src/app/search/[id]/types/index.ts b/src/app/search/[id]/types/index.ts index da98d03..fa11d05 100644 --- a/src/app/search/[id]/types/index.ts +++ b/src/app/search/[id]/types/index.ts @@ -30,3 +30,14 @@ export interface StockInfo { highStockPrice: string; lowStockPrice: string; } + +export enum TradeType { + Buy = "buy", + Sell = "sell", + Edit = "edit", +} + +export enum PriceType { + Market = "현재가", + Limit = "지정가", +} diff --git a/src/constants/trade.ts b/src/constants/trade.ts new file mode 100644 index 0000000..c39e5a4 --- /dev/null +++ b/src/constants/trade.ts @@ -0,0 +1,13 @@ +import { TradeType } from "@/app/search/[id]/types"; + +export const colorMap = { + [TradeType.Buy]: "red", + [TradeType.Sell]: "blue", + [TradeType.Edit]: "green", +} as const; + +export const textMap = { + [TradeType.Buy]: "매수", + [TradeType.Sell]: "매도", + [TradeType.Edit]: "정정", +} as const; From 472ff7142ce3d47b528e6a11f606f35e0d38eaf6 Mon Sep 17 00:00:00 2001 From: cindyCho Date: Sat, 14 Dec 2024 10:35:55 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=ED=95=9C=20=EC=83=89=EC=83=81=20=ED=85=8C=EC=9D=BC?= =?UTF-8?q?=EC=9C=88=EB=93=9C=EB=A1=9C=20=EC=A0=95=EC=9D=98=20=ED=9B=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buy-and-sell/buyable-quantity.tsx | 4 +- .../buy-and-sell/current-price.tsx | 4 +- .../edit-cancel/edit-table-row.tsx | 2 +- .../order-stock/edit-cancel/edit-table.tsx | 2 +- src/components/common/dropdown/index.tsx | 2 +- src/components/common/tabs/index.tsx | 6 +-- tailwind.config.ts | 39 ++++++++++++++++++- 7 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx index 4e42af9..8e6fdbf 100644 --- a/src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/buyable-quantity.tsx @@ -31,8 +31,8 @@ export default function BuyableQuantity({ {type === TradeType.Buy ? "매수" : "매도"} 가능 주식 -
- +
+ {type === TradeType.Buy ? getKoreanPrice(calculateBuyableQuantity(deposit, bidding)) : stockMap.findStockCount(stockName)} diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx index ade8e15..6c6f525 100644 --- a/src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/current-price.tsx @@ -11,12 +11,12 @@ export default function CurrentPrice() { return (
{Array.from({ length: 4 }).map((_, index) => ( -
+
{getKoreanPrice(stockInfo.stockPrice)}
))} {Array.from({ length: 4 }).map((_, index) => ( -
+
{getKoreanPrice(stockInfo.stockPrice)}
))} diff --git a/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx index 5f6053b..23fcd7b 100644 --- a/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx +++ b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table-row.tsx @@ -36,7 +36,7 @@ export default function EditTableRow({ > {data.type} 정정 - {data.OrderId} + {data.OrderId}
{data.stockName} diff --git a/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx index de75bd3..3b104e0 100644 --- a/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx +++ b/src/app/search/[id]/_components/order-stock/edit-cancel/edit-table.tsx @@ -35,7 +35,7 @@ export default function EditTable({
- + {titles.map((title, index) => (
Date: Sat, 14 Dec 2024 10:36:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20hook=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order-stock/buy-and-sell/index.tsx | 200 ++++++++++++++++++ .../order-stock/edit-cancel/index.tsx | 124 +++++++++++ .../search/[id]/hooks/use-limit-order-data.ts | 24 +++ .../[id]/hooks/use-limit-order-mutations.ts | 33 +++ .../search/[id]/hooks/use-trade-mutations.ts | 66 ++++++ 5 files changed, 447 insertions(+) create mode 100644 src/app/search/[id]/_components/order-stock/buy-and-sell/index.tsx create mode 100644 src/app/search/[id]/_components/order-stock/edit-cancel/index.tsx create mode 100644 src/app/search/[id]/hooks/use-limit-order-data.ts create mode 100644 src/app/search/[id]/hooks/use-limit-order-mutations.ts create mode 100644 src/app/search/[id]/hooks/use-trade-mutations.ts diff --git a/src/app/search/[id]/_components/order-stock/buy-and-sell/index.tsx b/src/app/search/[id]/_components/order-stock/buy-and-sell/index.tsx new file mode 100644 index 0000000..7218a37 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/buy-and-sell/index.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { UseMutateFunction } from "@tanstack/react-query"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; + +import { colorMap } from "@/constants/trade"; +import { useStockInfoContext } from "@/context/stock-info-context"; +import { useAuth } from "@/hooks/use-auth"; +import { + LimitPriceOrderHistory, + ModifyTradeFormData, +} from "@/types/transaction"; +import { calculateTotalOrderAmount } from "@/utils/price"; +import { + BuyFormData, + BuyFormSchema, +} from "@/validation/schema/transaction-form"; + +import useTradeMutations from "../../../hooks/use-trade-mutations"; +import { PriceType, TradeType } from "../../../types"; +import TradeTable from "../trade-table"; +import BuyableQuantity from "./buyable-quantity"; +import CurrentPrice from "./current-price"; +import FormButtons from "./form-buttons"; +import InputField from "./input-field"; +import PriceTypeDropdown from "./price-type-dropdown"; +import TotalAmount from "./total-amount"; + +interface TradeProps { + type: TradeType; + defaultData?: LimitPriceOrderHistory; + handleMutate?: UseMutateFunction; +} + +export default function BuyAndSell({ + type, + defaultData, + handleMutate, +}: TradeProps) { + const [priceType, setPriceType] = useState(PriceType.Limit); + const [isConfirmationPage, setIsConfirmationPage] = useState(false); + + const { stockName, stockInfo } = useStockInfoContext(); + const { token } = useAuth(); + const trades = useTradeMutations(); + + const { + control, + watch, + reset, + handleSubmit, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(BuyFormSchema), + defaultValues: + type === TradeType.Edit && defaultData + ? { + count: defaultData.stockCount, + bidding: defaultData.buyPrice, + } + : { + count: undefined, + bidding: undefined, + }, + }); + + const watchedCount = watch("count"); + const watchedBidding = watch("bidding"); + + const handleReset = () => { + reset({ + count: undefined, + bidding: undefined, + }); + }; + + const handlePriceTypeChange = (newPriceType: PriceType) => { + setPriceType(newPriceType); + setValue( + "bidding", + newPriceType === PriceType.Market ? Number(stockInfo.stockPrice) : 0, + ); + }; + + const handleConfirmPurchase = () => { + setIsConfirmationPage(true); + }; + + const transactionHandlers = { + [TradeType.Buy]: () => + priceType === PriceType.Market + ? trades.buyAtMarketPrice.mutate({ + token, + data: { stockName, quantity: watchedCount }, + }) + : trades.buyAtLimitPrice.mutate({ + token, + data: { + stockName, + limitPrice: watchedBidding, + quantity: watchedCount, + }, + }), + + [TradeType.Sell]: () => + priceType === PriceType.Market + ? trades.sellAtMarketPrice.mutate({ + token, + data: { stockName, quantity: watchedCount }, + }) + : trades.sellAtLimitPrice.mutate({ + token, + data: { + stockName, + limitPrice: watchedBidding, + quantity: watchedCount, + }, + }), + + [TradeType.Edit]: () => { + if (type !== TradeType.Edit || !handleMutate) return; + handleMutate({ + token, + orderId: defaultData?.OrderId, + data: { + stockName, + limitPrice: watchedBidding, + quantity: watchedCount, + }, + }); + }, + }; + + const handleConfirm = transactionHandlers[type]; + + if (isConfirmationPage) { + return ( + setIsConfirmationPage(false)} + onClickConfirm={handleConfirm} + /> + ); + } + + return ( +
+ +
+ + + + {type !== "edit" && ( + + )} + + + + handleSubmit(handleConfirmPurchase)()} + /> + +
+ ); +} diff --git a/src/app/search/[id]/_components/order-stock/edit-cancel/index.tsx b/src/app/search/[id]/_components/order-stock/edit-cancel/index.tsx new file mode 100644 index 0000000..7f33991 --- /dev/null +++ b/src/app/search/[id]/_components/order-stock/edit-cancel/index.tsx @@ -0,0 +1,124 @@ +// "use client"; + +import { useState } from "react"; + +import Button from "@/components/common/button"; +import { useAuth } from "@/hooks/use-auth"; +import { useToast } from "@/store/use-toast-store"; + +import useLimitOrderData from "../../../hooks/use-limit-order-data"; +import useOrderMutations from "../../../hooks/use-limit-order-mutations"; +import { TradeType } from "../../../types"; +import LoadingSpinner from "../../loading-spiner"; +import BuyAndSell from "../buy-and-sell"; +import TradeTable from "../trade-table"; +import EditTable from "./edit-table"; + +export default function EditCancel() { + const [selectedOrders, setSelectedOrders] = useState([]); + const [isEditForm, setIsEditForm] = useState(false); + const [isCancelTable, setIsCancelTable] = useState(false); + + const { showToast } = useToast(); + const { token } = useAuth(); + const { + data: limitOrderData, + isLoading, + isPending, + findOrderById, + } = useLimitOrderData(); + const { cancelTradeMutation, modifyTradeMutation } = useOrderMutations(); + + const handleEdit = () => { + if (selectedOrders.length > 0) { + setIsEditForm(true); + } else { + showToast("정정할 데이터를 선택해주세요.", "error"); + } + }; + + const handleCancel = () => { + if (selectedOrders.length > 0) { + setIsCancelTable(true); + } else { + showToast("취소할 데이터를 선택해주세요.", "error"); + } + }; + + const handleCancelConfirm = (orderId: string) => { + cancelTradeMutation.mutate({ token, orderId }); + setIsCancelTable(false); + }; + + if (isLoading || isPending) { + return ; + } + + if (isEditForm) { + const order = findOrderById(selectedOrders[0]); + return ( + + ); + } + + if (isCancelTable) { + return ( + <> + {selectedOrders.map((orderId) => { + const order = findOrderById(orderId); + return order ? ( + setIsCancelTable(false)} + onClickConfirm={() => { + handleCancelConfirm(orderId); + }} + /> + ) : ( + "취소 정보를 받아오지 못했습니다." + ); + })} + + ); + } + + return ( + <> + + {limitOrderData && limitOrderData.length > 0 && ( +
+ + +
+ )} + + ); +} diff --git a/src/app/search/[id]/hooks/use-limit-order-data.ts b/src/app/search/[id]/hooks/use-limit-order-data.ts new file mode 100644 index 0000000..a99724f --- /dev/null +++ b/src/app/search/[id]/hooks/use-limit-order-data.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; + +import { getTrade } from "@/api/transaction"; +import { useStockInfoContext } from "@/context/stock-info-context"; +import { useAuth } from "@/hooks/use-auth"; + +export default function useLimitOrderData() { + const { stockName } = useStockInfoContext(); + const { token, isAuthenticated } = useAuth(); + + const queryResult = useQuery({ + queryKey: ["limitOrder", stockName], + queryFn: () => getTrade(token, stockName), + enabled: !!isAuthenticated && !!token, + }); + + const findOrderById = (orderId: string) => + queryResult.data?.find((data) => data.OrderId.toString() === orderId); + + return { + ...queryResult, + findOrderById, + }; +} diff --git a/src/app/search/[id]/hooks/use-limit-order-mutations.ts b/src/app/search/[id]/hooks/use-limit-order-mutations.ts new file mode 100644 index 0000000..31c83fd --- /dev/null +++ b/src/app/search/[id]/hooks/use-limit-order-mutations.ts @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { cancelTrade, modifyTrade } from "@/api/transaction"; +import { useToast } from "@/store/use-toast-store"; + +export default function useOrderMutations() { + const { showToast } = useToast(); + const queryClient = useQueryClient(); + + const cancelTradeMutation = useMutation({ + mutationFn: cancelTrade, + onSuccess: () => { + showToast("주문이 취소되었습니다", "success"); + queryClient.invalidateQueries({ queryKey: ["limitOrder"] }); + }, + onError: (error: Error) => { + showToast(error.message, "error"); + }, + }); + + const modifyTradeMutation = useMutation({ + mutationFn: modifyTrade, + onSuccess: () => { + showToast("주문을 수정했습니다.", "success"); + queryClient.invalidateQueries({ queryKey: ["limitOrder"] }); + }, + onError: (error: Error) => { + showToast(error.message, "error"); + }, + }); + + return { cancelTradeMutation, modifyTradeMutation }; +} diff --git a/src/app/search/[id]/hooks/use-trade-mutations.ts b/src/app/search/[id]/hooks/use-trade-mutations.ts new file mode 100644 index 0000000..6401f0a --- /dev/null +++ b/src/app/search/[id]/hooks/use-trade-mutations.ts @@ -0,0 +1,66 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { + buyAtLimitPrice, + buyAtMarketPrice, + sellAtLimitPrice, + sellAtMarketPrice, +} from "@/api/transaction"; +import { useTabsContext } from "@/components/common/tabs"; +import { useToast } from "@/store/use-toast-store"; + +export default function useTradeMutations() { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + const { setActiveTab } = useTabsContext(); + + const createMutationConfig = ( + successMessage: string, + tabName: string, + queryKey: string, + ) => ({ + onSuccess: () => { + setActiveTab(tabName); + showToast(successMessage, "success"); + queryClient.invalidateQueries({ queryKey: [queryKey] }); + }, + onError: (error: Error) => { + showToast(error.message, "error"); + }, + }); + + return { + buyAtMarketPrice: useMutation({ + mutationFn: buyAtMarketPrice, + ...createMutationConfig( + "현재가로 매수가 완료되었습니다.", + "history", + "tradeHistory", + ), + }), + sellAtMarketPrice: useMutation({ + mutationFn: sellAtMarketPrice, + ...createMutationConfig( + "현재가로 매도가 완료되었습니다.", + "history", + "tradeHistory", + ), + }), + buyAtLimitPrice: useMutation({ + mutationFn: buyAtLimitPrice, + ...createMutationConfig( + "지정가로 매수가 완료되었습니다.", + "edit-cancel", + "limitOrder", + ), + }), + sellAtLimitPrice: useMutation({ + mutationFn: sellAtLimitPrice, + ...createMutationConfig( + "지정가로 매도가 완료되었습니다.", + "edit-cancel", + "limitOrder", + ), + }), + }; +}