Skip to content
Binary file added public/images/login-guard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/shield.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion src/api/transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,20 @@ export async function cancelTrade({
}
}

export interface TradeHistory {
id: number;
buyPrice: number;
remainCount: number;
stockCount: number;
stockName: string;
buyOrder: string;
}

// 매수/매도 체결내역
export async function getTradeHistory(
token: string | null,
stockName: string,
): Promise<OrderHistory[]> {
): Promise<TradeHistory[]> {
if (token === null) {
throw new Error();
}
Expand Down
9 changes: 7 additions & 2 deletions src/app/search/[id]/_components/loading-spiner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export default function LoadingSpinner() {
import cn from "@/utils/cn";

interface LoadingSpinnerProps {
className?: string;
}
export default function LoadingSpinner({ className }: LoadingSpinnerProps) {
return (
<div className="flex h-64 items-center justify-center">
<div className={cn("flex h-64 items-center justify-center", className)}>
<div className="loadingSpinner" />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OrderHistory } from "@/api/transaction";
import CheckButton from "@/components/common/check-button";
import cn from "@/utils/cn";
import { getKoreanPrice } from "@/utils/price";

interface EditTableBodyProps {
Expand All @@ -26,7 +27,15 @@ export default function EditTableBody({
</td>
<td className=" py-10 pl-20 text-left">
<div className="pb-6 text-12-400">
<span className="pr-3 text-[#F20000]">{data.type} 정정</span>
<span
className={cn(
"pr-3",
data.type === "매수" && "text-[#F20000]",
data.type === "매도" && "text-[#4882FA]",
)}
>
{data.type} 정정
</span>
<span className="text-[#9B9B9B]">{data.OrderId}</span>
</div>
{data.stockName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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";
Expand All @@ -21,7 +22,11 @@ export default function EditCancel() {
const { showToast } = useToast();
const queryClient = useQueryClient();

const { data: limitOrderData } = useQuery({
const {
data: limitOrderData,
isLoading,
isPending,
} = useQuery({
queryKey: ["limitOrder"],
queryFn: () => getTrade(token, stockName),
enabled: !!isAuthenticated && !!token,
Expand Down Expand Up @@ -65,6 +70,9 @@ export default function EditCancel() {
onSettled: () => {
setIsCancelTable(false);
},
onError: (error) => {
showToast(error.message, "error");
},
Comment on lines +73 to +75
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 메시지 처리 방식 개선이 필요합니다.

현재 에러 메시지를 직접 표시하고 있는데, 사용자 친화적인 에러 메시지로 변환하는 것이 좋겠습니다.

     onError: (error) => {
-      showToast(error.message, "error");
+      const userFriendlyMessage = error.message === "Failed to fetch stocks" 
+        ? "거래 정보를 불러오는데 실패했습니다. 잠시 후 다시 시도해주세요." 
+        : "요청 처리 중 오류가 발생했습니다.";
+      showToast(userFriendlyMessage, "error");
     },

Also applies to: 88-90

});

const { mutate: modifyTradeMutate } = useMutation({
Expand All @@ -77,6 +85,9 @@ export default function EditCancel() {
setIsEditForm(false);
setSelectedOrders([]);
},
onError: (error) => {
showToast(error.message, "error");
},
});

const handleCancelConfirm = (orderId: string) => {
Expand All @@ -90,6 +101,10 @@ export default function EditCancel() {
);
}

if (isLoading || isPending) {
return <LoadingSpinner className="mt-230" />;
}

if (isCancelTable) {
return (
<>
Expand Down Expand Up @@ -125,17 +140,19 @@ export default function EditCancel() {
<table className="w-full text-center text-14-500">
<EditTableHeader />
{limitOrderData && limitOrderData.length > 0 ? (
limitOrderData.map((data) => (
<EditTableBody
key={data.OrderId}
data={data}
isChecked={selectedOrders.includes(data.OrderId.toString())}
toggleSelection={toggleOrderSelection}
/>
))
[...limitOrderData]
.sort((a, b) => b.OrderId - a.OrderId)
.map((data) => (
<EditTableBody
key={data.OrderId}
data={data}
isChecked={selectedOrders.includes(data.OrderId.toString())}
toggleSelection={toggleOrderSelection}
/>
))
Comment on lines +143 to +152
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

성능 최적화가 필요합니다.

데이터 정렬 로직이 렌더링마다 실행되고 있습니다. useMemo를 사용하여 최적화하는 것이 좋겠습니다.

+  const sortedLimitOrderData = useMemo(
+    () => 
+      limitOrderData 
+        ? [...limitOrderData].sort((a, b) => b.OrderId - a.OrderId)
+        : [],
+    [limitOrderData]
+  );

-            [...limitOrderData]
-              .sort((a, b) => b.OrderId - a.OrderId)
-              .map((data) => (
+            sortedLimitOrderData.map((data) => (
📝 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
[...limitOrderData]
.sort((a, b) => b.OrderId - a.OrderId)
.map((data) => (
<EditTableBody
key={data.OrderId}
data={data}
isChecked={selectedOrders.includes(data.OrderId.toString())}
toggleSelection={toggleOrderSelection}
/>
))
const sortedLimitOrderData = useMemo(
() =>
limitOrderData
? [...limitOrderData].sort((a, b) => b.OrderId - a.OrderId)
: [],
[limitOrderData]
);
sortedLimitOrderData.map((data) => (
<EditTableBody
key={data.OrderId}
data={data}
isChecked={selectedOrders.includes(data.OrderId.toString())}
toggleSelection={toggleOrderSelection}
/>
))

) : (
<tr>
<td colSpan={5} className="py-20 text-center text-gray-500">
<td colSpan={5} className="py-20 text-center text-16-500">
<Image
src="/images/green-wallet.png"
width={150}
Expand All @@ -149,18 +166,20 @@ export default function EditCancel() {
)}
</table>
</div>
<div className="mt-20 text-center">
<Button
variant="red"
className="mr-10 w-120 bg-[#1DA65A] hover:bg-[#1DA65A]/95"
onClick={handleEdit}
>
정정
</Button>
<Button variant="red" className="w-120" onClick={handleCancel}>
취소
</Button>
</div>
{limitOrderData && limitOrderData.length > 0 && (
<div className="mt-20 text-center">
<Button
variant="red"
className="mr-10 w-120 text-black bg-[#0FED78] hover:bg-[#0FED78]/95"
onClick={handleEdit}
>
정정
</Button>
Comment on lines +171 to +177
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

버튼 스타일링에 일관성이 필요합니다.

variant="red"로 설정되어 있지만, className에서 초록색 배경을 덮어쓰고 있습니다. 이는 혼란을 야기할 수 있습니다.

다음과 같이 수정하는 것을 추천드립니다:

- <Button
-   variant="red"
-   className="mr-10 w-120 text-black bg-[#0FED78] hover:bg-[#0FED78]/95"
-   onClick={handleEdit}
- >
+ <Button
+   variant="primary"
+   className="mr-10 w-120"
+   onClick={handleEdit}
+ >

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

<Button variant="red" className="w-120" onClick={handleCancel}>
취소
</Button>
</div>
)}
</>
);
}
55 changes: 39 additions & 16 deletions src/app/search/[id]/_components/transaction-form/history.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,63 @@
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 } = useQuery({
const {
data: tradeHistoryData,
isLoading,
isPending,
} = useQuery({
queryKey: ["tradeHistory"],
queryFn: () => getTradeHistory(token, stockName),
enabled: !!isAuthenticated && !!token,
});

if (isLoading || isPending) {
return <LoadingSpinner className="mt-230" />;
}

return (
<div className="flex h-470 flex-col gap-20 overflow-auto">
{tradeHistoryData && tradeHistoryData?.length > 0 ? (
tradeHistoryData.map((history) => (
<TransactionTable
key={history.OrderId}
color="green"
isSubmit={false}
submittedData={{
stockName: history.stockName,
count: history.stockCount,
bidding: history.buyPrice,
totalAmount: history.buyPrice * history.stockCount,
buyOrder: history.type,
}}
/>
))
[...tradeHistoryData]
.sort((a, b) => b.id - a.id)
.map((history) => (
<TransactionTable
key={history.id}
color={history.buyOrder === "매수" ? "red" : "blue"}
isSubmit={false}
submittedData={{
stockName: history.stockName,
count: history.stockCount,
bidding: history.buyPrice,
totalAmount: history.buyPrice * history.stockCount,
buyOrder: history.buyOrder,
}}
/>
))
) : (
<div>체결내역이 없습니다. 거래를 시작해보세요!</div>
<div className="py-20 text-center font-medium leading-5">
<Image
src="/images/green-wallet.png"
width={150}
height={150}
className="mx-auto mb-30 mt-65"
alt="지갑 그림"
/>
체결내역이 없습니다.
<br />
거래를 시작해보세요!
</div>
)}
</div>
);
Expand Down
104 changes: 72 additions & 32 deletions src/app/search/[id]/_components/transaction-form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"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";
Expand All @@ -22,41 +27,76 @@ export default function TransactionForm({
stockName,
stockInfo,
}: TransactionFormProps) {
const { isAuthenticated, isInitialized } = useAuth();

if (!isInitialized) {
return (
<div className="ml-17 h-630 min-w-450 rounded-10 bg-white px-22 py-30">
<h3 className="mb-250 text-20-700">거래하기</h3>
<LoadingSpinner />
</div>
);
}

return (
<div className="ml-17 h-630 min-w-450 rounded-10 bg-white px-22 py-30">
<h3 className="mb-16 text-20-700">거래하기</h3>
<StockInfoProvider
value={{
stockName,
stockInfo,
}}
>
<Tabs defaultValue="buy">
<TabsList>
<TabsTrigger value="buy" buttonColor="red">
매수
</TabsTrigger>
<TabsTrigger value="sell" buttonColor="blue">
매도
</TabsTrigger>
<TabsTrigger value="history">체결내역</TabsTrigger>
<TabsTrigger value="edit-cancel">정정 / 취소</TabsTrigger>
</TabsList>

<TabsContent value="buy">
<Trade type="buy" />
</TabsContent>
<TabsContent value="sell">
<Trade type="sell" />
</TabsContent>
<TabsContent value="history">
<History />
</TabsContent>
<TabsContent value="edit-cancel">
<EditCancel />
</TabsContent>
</Tabs>
</StockInfoProvider>
{isAuthenticated ? (
<StockInfoProvider
value={{
stockName,
stockInfo,
}}
>
<Tabs defaultValue="buy">
<TabsList>
<TabsTrigger value="buy" buttonColor="red">
매수
</TabsTrigger>
<TabsTrigger value="sell" buttonColor="blue">
매도
</TabsTrigger>
<TabsTrigger value="history">체결내역</TabsTrigger>
<TabsTrigger value="edit-cancel">정정 / 취소</TabsTrigger>
</TabsList>
<TabsContent value="buy">
<Trade type="buy" />
</TabsContent>
<TabsContent value="sell">
<Trade type="sell" />
</TabsContent>
<TabsContent value="history">
<History />
</TabsContent>
<TabsContent value="edit-cancel">
<EditCancel />
</TabsContent>
</Tabs>
</StockInfoProvider>
) : (
<div className="relative">
<Image
src="/images/login-guard.png"
alt="보안 아이콘"
width={300}
height={300}
className="absolute right-1/2 top-40 translate-x-1/2"
/>
<div className="relative top-250 w-full text-center leading-8">
<span className="text-20-700">로그인이 필요해요!</span>
<br />
<span className="text-16-500 text-gray-500">
가상 거래를 하기 위해서는 로그인이 필수적이에요!
</span>
<Link
href="/login"
className="m-auto mt-40 block w-150 rounded-4 bg-[#11E977] py-16 text-center text-16-700"
>
로그인 하기
</Link>
</div>
</div>
)}
</div>
);
}
Loading
Loading