Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ 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: "buy" | "sell" | "edit";
type: TradeType;
bidding: number;
}

Expand All @@ -27,11 +29,11 @@ export default function BuyableQuantity({
return (
<div className="relative flex justify-between gap-6">
<span className="w-110">
{type === "buy" ? "매수" : "매도"} 가능 주식
{type === TradeType.Buy ? "매수" : "매도"} 가능 주식
</span>
<div className="flex-1 cursor-not-allowed border-b border-solid border-[#505050] pb-2 text-right">
<span className="pr-5 text-[#B7B7B7]">
{type === "buy"
<div className="flex-1 cursor-not-allowed border-b border-solid border-gray-600 pb-2 text-right">
<span className="pr-5 text-gray-200">
{type === TradeType.Buy
? getKoreanPrice(calculateBuyableQuantity(deposit, bidding))
: stockMap.findStockCount(stockName)}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ export default function CurrentPrice() {
return (
<div>
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="w-127 bg-[#EDF1FC] py-11 text-center">
<div key={index} className="w-127 bg-blue-100 py-11 text-center">
{getKoreanPrice(stockInfo.stockPrice)}
</div>
))}
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="w-127 bg-[#FDEBEB] py-11 text-center">
<div key={index} className="w-127 bg-red-100 py-11 text-center">
{getKoreanPrice(stockInfo.stockPrice)}
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={cn(
"relative top-90 w-300",
orderType === TradeType.Edit && "top-110",
)}
>
<Button variant="outline-gray" onClick={handleReset}>
초기화
</Button>
<Button
variant="custom"
className={cn(
"ml-7 w-160",
orderType === TradeType.Buy && "bg-red-500 hover:bg-red-500/80",
orderType === TradeType.Sell && "bg-blue-500 hover:bg-blue-500/80",
orderType === TradeType.Edit && "bg-green-500 hover:bg-green-500/80",
)}
onClick={handleSubmit}
>
{textMap[orderType]}
</Button>
</div>
);
}
200 changes: 200 additions & 0 deletions src/app/search/[id]/_components/order-stock/buy-and-sell/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string, Error, ModifyTradeFormData, unknown>;
}

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<BuyFormData>({
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,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

[속성 명명 규칙 일관성 유지 필요]

TypeScript의 명명 규칙을 따르기 위해, defaultData?.OrderIdOrderId를 소문자 카멜케이스인 orderId로 변경하는 것이 좋습니다.

수정 제안:

- orderId: defaultData?.OrderId,
+ orderId: defaultData?.orderId,

또한, 관련된 인터페이스나 타입 정의에서도 속성 이름을 일관성 있게 변경해야 합니다.

Committable suggestion skipped: line range outside the PR's diff.

data: {
stockName,
limitPrice: watchedBidding,
quantity: watchedCount,
},
});
},
};

const handleConfirm = transactionHandlers[type];

if (isConfirmationPage) {
return (
<TradeTable
color={colorMap[type]}
submittedData={{
stockName,
count: watchedCount,
bidding: watchedBidding,
totalAmount: calculateTotalOrderAmount(watchedCount, watchedBidding),
buyOrder: type === TradeType.Buy ? "매수" : "매도",
}}
onClickGoBack={() => setIsConfirmationPage(false)}
onClickConfirm={handleConfirm}
/>
);
}

return (
<div className="flex">
<CurrentPrice />
<form className="flex w-270 flex-col gap-16 pl-11">
<PriceTypeDropdown
orderType={type}
priceType={priceType}
setPriceType={handlePriceTypeChange}
/>
<InputField
title="수량"
type="count"
placeholder="수량 입력"
inputSuffix="주"
state={watchedCount}
setState={setValue}
control={control}
errors={errors.count}
quantity={10}
/>

{type !== "edit" && (
<BuyableQuantity bidding={watchedBidding} type={type} />
)}
Comment on lines +177 to +179
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

[타입 비교 시 Enum을 사용하도록 수정 필요]

TradeType은 Enum으로 정의되어 있으므로, 문자열 리터럴 대신 Enum 값을 사용하여 비교하는 것이 좋습니다. 현재 코드에서는 type !== "edit"로 비교하고 있는데, 이는 type !== TradeType.Edit로 수정하는 것이 바람직합니다.

수정 제안:

- {type !== "edit" && (
+ {type !== TradeType.Edit && (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{type !== "edit" && (
<BuyableQuantity bidding={watchedBidding} type={type} />
)}
{type !== TradeType.Edit && (
<BuyableQuantity bidding={watchedBidding} type={type} />
)}


<InputField
title="호가"
type="bidding"
placeholder="호가 입력"
inputSuffix="원"
state={watchedBidding}
setState={setValue}
control={control}
errors={errors.bidding}
/>
<TotalAmount count={watchedCount} bidding={watchedBidding} />
<FormButtons
orderType={type}
handleReset={handleReset}
handleSubmit={() => handleSubmit(handleConfirmPurchase)()}
/>
</form>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { useStockInfoContext } from "@/context/stock-info-context";
import { getKoreanPrice } from "@/utils/price";
import { BuyFormData } from "@/validation/schema/transaction-form";

import CountDropdown from "./count-dropdown";
import NumberDropdown from "./number-dropdown";

interface OrderFieldProps {
interface InputFieldProps {
title: string;
type: "count" | "bidding";
placeholder: string;
Expand All @@ -24,7 +24,7 @@ interface OrderFieldProps {
quantity?: number;
}

export default function OrderField({
export default function InputField({
title,
type,
placeholder,
Expand All @@ -34,7 +34,7 @@ export default function OrderField({
control,
errors,
quantity,
}: OrderFieldProps) {
}: InputFieldProps) {
const { stockInfo } = useStockInfoContext();
const numberRegex = /^[0-9,]*$/;

Expand Down Expand Up @@ -72,7 +72,7 @@ export default function OrderField({

return (
<div className="relative flex justify-between gap-6">
<CountDropdown
<NumberDropdown
title={title}
number={type === "count" && quantity}
state={state ?? ""}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import { Dispatch, memo, SetStateAction } from "react";
import Dropdown from "@/components/common/dropdown";
import { getKoreanPrice } from "@/utils/price";

interface CountDropdownProps<T> {
interface NumberDropdownProps<T> {
state: T;
setState: Dispatch<SetStateAction<T>>;
title: string;
number?: number | boolean;
stockPrice?: string | boolean;
}

function CountDropdown<T extends string | number>({
function NumberDropdown<T extends string | number>({
state,
setState,
number,
title,
stockPrice,
}: CountDropdownProps<T>) {
}: NumberDropdownProps<T>) {
return (
<Dropdown
className="w-120"
Expand Down Expand Up @@ -59,4 +59,4 @@ function CountDropdown<T extends string | number>({
);
}

export default memo(CountDropdown);
export default memo(NumberDropdown);
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Dropdown from "@/components/common/dropdown";

import { PriceType, TradeType } from "../../../types";

interface PriceTypeDropdownProps {
orderType: "buy" | "sell" | "edit";
priceType: string;
setPriceType: (value: string) => void;
orderType: TradeType;
priceType: PriceType;
setPriceType: (newPriceType: PriceType) => void;
}
export default function PriceTypeDropdown({
orderType,
Expand All @@ -12,14 +14,14 @@ export default function PriceTypeDropdown({
}: PriceTypeDropdownProps) {
return (
<div className="flex gap-8">
{orderType === "edit" ? (
{orderType === TradeType.Edit ? (
<span className="mb-4 grow rounded-2 border border-solid border-[#B6B6B6] p-13 text-left">
지정가
</span>
) : (
<Dropdown
selectedValue={priceType}
onSelect={(value) => setPriceType(value as string)}
onSelect={(value) => setPriceType(value as PriceType)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

타입 캐스팅 대신 제네릭 사용을 권장합니다.

value as PriceType 타입 캐스팅은 런타임 에러의 위험이 있습니다. Dropdown 컴포넌트를 제네릭으로 구현하여 타입 안전성을 높이는 것이 좋습니다.

<Dropdown<PriceType>
  selectedValue={priceType}
  onSelect={(value) => setPriceType(value)}
  className="flex-1"
>

className="flex-1"
>
<Dropdown.Toggle>{priceType}</Dropdown.Toggle>
Expand Down
Loading
Loading