Skip to content
Open
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
106 changes: 106 additions & 0 deletions src/app/my-account/_components/stock-summary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use client";

import { clsx } from "clsx";

import { TotalStocks } from "../types";

interface StockSummaryProps {
totalStocks: TotalStocks;
}

export default function StockSummary({ totalStocks }: StockSummaryProps) {
const getValueWithSign = (value: number | null | undefined) => {
if (!value) return "0";
return `${value > 0 ? "+" : ""}${value.toLocaleString()}`;
};
Comment on lines +12 to +15
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

getValueWithSign 함수의 타입 처리 개선이 필요합니다

현재 구현은 null과 undefined를 동일하게 처리하고 있지만, 이는 명시적으로 구분되어야 합니다.

다음과 같이 개선하는 것을 제안합니다:

-const getValueWithSign = (value: number | null | undefined) => {
-  if (!value) return "0";
-  return `${value > 0 ? "+" : ""}${value.toLocaleString()}`;
+const getValueWithSign = (value: number | null | undefined) => {
+  if (value === null || value === undefined) return "0";
+  return `${value > 0 ? "+" : ""}${value.toLocaleString()}`;
};
📝 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
const getValueWithSign = (value: number | null | undefined) => {
if (!value) return "0";
return `${value > 0 ? "+" : ""}${value.toLocaleString()}`;
};
const getValueWithSign = (value: number | null | undefined) => {
if (value === null || value === undefined) return "0";
return `${value > 0 ? "+" : ""}${value.toLocaleString()}`;
};


return (
<div className="w-full">
<table className="mb-30 w-full border-collapse">
<tbody>
<tr>
<td className="w-1/4 border border-gray-200 bg-green-50 p-10 text-center text-16-500">
닉네임
</td>
<td className="w-1/4 border border-gray-200 p-10 text-center text-16-500">
{totalStocks?.memberNickname}
</td>
<td className="w-1/4 border border-gray-200 bg-green-50 p-10 text-center text-16-500">
예수금
</td>
<td className="w-1/4 border border-gray-200 p-10 text-center text-16-500">
{totalStocks?.deposit?.toLocaleString() ?? 0}
</td>
</tr>
</tbody>
</table>
Comment on lines +18 to +36
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

테이블의 접근성 개선이 필요합니다

현재 테이블 구조에 의미론적 요소가 부족합니다.

다음 개선사항을 제안합니다:

  1. 테이블에 캡션 추가
  2. aria-label 속성 추가
  3. 테이블 헤더(th) 사용
-<table className="mb-30 w-full border-collapse">
+<table className="mb-30 w-full border-collapse" aria-label="사용자 계좌 정보">
+  <caption className="sr-only">사용자 기본 정보</caption>
   <tbody>
     <tr>
-      <td className="w-1/4 border border-gray-200 bg-green-50 p-10 text-center text-16-500">
+      <th scope="row" className="w-1/4 border border-gray-200 bg-green-50 p-10 text-center text-16-500">
         닉네임
-      </td>
+      </th>

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


<table className="w-full border-collapse">
<tbody>
<tr>
<td
colSpan={2}
className="border border-gray-200 bg-green-50 p-20 text-center text-20-700"
>
총 평가 손익
</td>
<td
colSpan={2}
className={clsx(
"border border-gray-200 p-20 text-center text-20-700",
{
"text-red-500": (totalStocks?.totalEvaluationProfit ?? 0) > 0,
"text-blue-500":
(totalStocks?.totalEvaluationProfit ?? 0) < 0,
},
)}
>
{getValueWithSign(totalStocks?.totalEvaluationProfit)}
</td>
</tr>

<tr>
<td
rowSpan={2}
className="border border-gray-200 p-10 text-center text-16-500"
>
총 평가 금액
</td>
<td
rowSpan={2}
className="border border-gray-200 p-4 text-center text-16-500"
>
{totalStocks?.totalEvaluationAmount?.toLocaleString() ?? 0}
</td>
<td className="border border-gray-200 p-10 text-center text-16-500">
총 매입 금액
</td>
<td className="border border-gray-200 p-10 text-center text-16-500">
{totalStocks?.totalPurchaseAmount?.toLocaleString() ?? 0}
</td>
</tr>
<tr>
<td className="border border-gray-200 p-10 text-center text-16-500">
추정 자산
</td>
<td className="border border-gray-200 p-10 text-center text-16-500">
{totalStocks?.estimatedAsset?.toLocaleString() ?? 0}
</td>
</tr>

<tr>
<td className="border border-gray-200 p-10 text-center text-16-500">
랭킹
</td>
<td
colSpan={3}
className="border border-gray-200 p-10 text-left text-16-500"
>
{totalStocks?.rank ?? 0}등
</td>
</tr>
</tbody>
</table>
</div>
);
}
135 changes: 135 additions & 0 deletions src/app/my-account/_components/stock-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client";

import clsx from "clsx";
import { useId, useMemo } from "react";

import { Stock } from "../types";

interface StockTableProps {
stocks: Stock[] | null | undefined;
}

export default function StockTable({ stocks }: StockTableProps) {
const tableId = useId();

const stockRows = useMemo(() => {
if (!stocks || !Array.isArray(stocks)) return [];
return stocks.map((stock) => ({
...stock,
id: `${tableId}-${stock.stockName}`,
}));
}, [stocks, tableId]);
Comment on lines +15 to +21
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

stockRows 메모이제이션 최적화가 필요합니다

현재 구현은 불필요하게 tableId를 의존성 배열에 포함하고 있습니다.

다음과 같이 개선하는 것을 제안합니다:

-const stockRows = useMemo(() => {
-  if (!stocks || !Array.isArray(stocks)) return [];
-  return stocks.map((stock) => ({
-    ...stock,
-    id: `${tableId}-${stock.stockName}`,
-  }));
-}, [stocks, tableId]);
+const stockRows = useMemo(() => {
+  if (!stocks || !Array.isArray(stocks)) return [];
+  return stocks.map((stock) => ({
+    ...stock,
+    id: stock.stockName, // stockName은 고유해야 합니다
+  }));
+}, [stocks]);
📝 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
const stockRows = useMemo(() => {
if (!stocks || !Array.isArray(stocks)) return [];
return stocks.map((stock) => ({
...stock,
id: `${tableId}-${stock.stockName}`,
}));
}, [stocks, tableId]);
const stockRows = useMemo(() => {
if (!stocks || !Array.isArray(stocks)) return [];
return stocks.map((stock) => ({
...stock,
id: stock.stockName, // stockName은 고유해야 합니다
}));
}, [stocks]);


// 수익률의 부호에 따라 평가손익의 부호를 보정
const getEvaluationProfitWithSign = (profit: number, rate: number) => {
const absProfit = Math.abs(profit);
return rate < 0 ? -absProfit : absProfit;
};
Comment on lines +23 to +27
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

getEvaluationProfitWithSign 함수의 예외 처리가 필요합니다

현재 구현은 입력값의 유효성을 검사하지 않습니다.

다음과 같이 개선하는 것을 제안합니다:

-const getEvaluationProfitWithSign = (profit: number, rate: number) => {
+const getEvaluationProfitWithSign = (profit: number | undefined | null, rate: number | undefined | null) => {
+  if (profit === undefined || profit === null || rate === undefined || rate === null) {
+    return 0;
+  }
   const absProfit = Math.abs(profit);
   return rate < 0 ? -absProfit : absProfit;
 };
📝 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
// 수익률의 부호에 따라 평가손익의 부호를 보정
const getEvaluationProfitWithSign = (profit: number, rate: number) => {
const absProfit = Math.abs(profit);
return rate < 0 ? -absProfit : absProfit;
};
// 수익률의 부호에 따라 평가손익의 부호를 보정
const getEvaluationProfitWithSign = (profit: number | undefined | null, rate: number | undefined | null) => {
if (profit === undefined || profit === null || rate === undefined || rate === null) {
return 0;
}
const absProfit = Math.abs(profit);
return rate < 0 ? -absProfit : absProfit;
};


return (
<div className="mt-40">
<table className="w-full border-collapse">
<thead>
<tr>
<th
rowSpan={2}
className="w-[16%] border-r border-gray-200 bg-green-50 p-8 text-left text-16-600"
>
종목명
</th>
<th
colSpan={2}
className="w-[42%] border-b border-r border-gray-200 bg-green-50 p-8 text-center text-16-600"
>
평가손익
</th>
<th
rowSpan={2}
className="w-[16%] border-r border-gray-200 bg-green-50 p-8 text-center text-16-600"
>
보유수량
</th>
<th
colSpan={2}
className="w-[26%] border-b border-gray-200 bg-green-50 p-8 text-center text-16-600"
>
평가금액
</th>
</tr>
<tr className="bg-green-50">
<th className="border-r border-gray-200 p-8 text-center text-16-600">
금액
</th>
<th className="border-r border-gray-200 p-8 text-center text-16-600">
수익률
</th>
<th className="border-r border-gray-200 p-8 text-center text-16-600">
매입가
</th>
<th className="p-8 text-center text-16-600">현재가</th>
</tr>
</thead>
Comment on lines +31 to +71
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

테이블 헤더의 접근성 개선이 필요합니다

복잡한 테이블 구조에서 스크린 리더 사용자를 위한 접근성이 부족합니다.

다음 개선사항을 제안합니다:

  1. 테이블에 설명적인 캡션 추가
  2. 복잡한 헤더 구조에 대한 aria-describedby 추가
  3. scope 속성의 적절한 사용
-<table className="w-full border-collapse">
+<table 
+  className="w-full border-collapse"
+  aria-label="주식 포트폴리오 현황"
+>
+  <caption className="sr-only">보유 주식 상세 정보</caption>

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

<tbody>
{stockRows.map((stock) => (
<tr key={stock.id} className="border-b border-gray-200">
<td className="border-r border-gray-200 p-4 text-18-600">
{stock.stockName}
</td>
<td
className={clsx(
"border-r border-gray-200 p-8 text-right text-16-600",
{
"text-red-500": stock.ProfitRate > 0,
"text-blue-500": stock.ProfitRate < 0,
},
)}
>
{getEvaluationProfitWithSign(
stock.EvaluationProfit,
stock.ProfitRate,
) > 0 && "+"}
{getEvaluationProfitWithSign(
stock.EvaluationProfit,
stock.ProfitRate,
).toLocaleString()}
</td>
<td
className={clsx(
"border-r border-gray-200 p-8 text-right text-16-600",
{
"text-red-500": stock.ProfitRate > 0,
"text-blue-500": stock.ProfitRate < 0,
},
)}
>
{stock.ProfitRate > 0 && "+"}
{stock.ProfitRate?.toFixed(2) ?? 0} %
</td>
<td className="border-r border-gray-200 p-8 text-center text-16-600">
{stock.stockCount ?? 0}
</td>
<td className="border-r border-gray-200 p-8 text-right text-16-600">
{stock.purchaseAmount?.toLocaleString() ?? 0}
</td>
<td className="p-4">
<div className="mb-5 text-right text-16-600">
{stock.currentPrice?.toLocaleString() ?? 0}
</div>
<div
className={clsx("text-right text-14-600", {
"text-red-500": stock.prevChangeRate > 0,
"text-blue-500": stock.prevChangeRate < 0,
})}
>
<span className="text-14-600 text-gray-500">어제보다 </span>
{stock.prevChangeRate > 0 && "+"}
{stock.prevChangeRate?.toFixed(2) ?? 0} %
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
18 changes: 18 additions & 0 deletions src/app/my-account/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "내 계좌 페이지 | 온라인 투자플랫폼",
description: "내 계좌 페이지입니다",
};

export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="lg:max-w-2000 flex h-screen flex-col bg-[#F5F6F8]">
{children}
</div>
);
}
60 changes: 60 additions & 0 deletions src/app/my-account/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getCookie } from "@/utils/next-cookies";

import StockSummary from "./_components/stock-summary";
import StockTable from "./_components/stock-table";

async function getStocks() {
try {
const token = await getCookie("token");
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/account/stocks`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
return await response.json();
} catch (error) {
console.error("주식 데이터 조회 실패:", error); //eslint-disable-line
return [];
}
}
Comment on lines +6 to +22
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

API 호출 부분의 개선이 필요합니다.

현재 구현에서 다음과 같은 보완이 필요합니다:

  1. 응답 타입이 명시되어 있지 않습니다.
  2. HTTP 상태 코드 처리가 누락되었습니다.
  3. 토큰 만료 처리가 필요합니다.
 async function getStocks() {
   try {
     const token = await getCookie("token");
+    if (!token) {
+      throw new Error("인증 토큰이 없습니다");
+    }
     const response = await fetch(
       `${process.env.NEXT_PUBLIC_API_URL}/account/stocks`,
       {
         headers: {
           Authorization: `Bearer ${token}`,
         },
       },
     );
+    if (!response.ok) {
+      throw new Error(`API 오류: ${response.status}`);
+    }
-    return await response.json();
+    const data = await response.json() as Stock[];
+    return data;
   } catch (error) {
-    console.error("주식 데이터 조회 실패:", error);
+    console.error("주식 데이터 조회 실패:", error);
+    // TODO: 에러 추적을 위한 로깅 서비스 연동 필요
     return [];
   }
 }

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


async function getTotalStocks() {
try {
const token = await getCookie("token");
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/account/all-stocks`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
return await response.json();
} catch (error) {
console.error("총 주식 데이터 조회 실패:", error); //eslint-disable-line
return {
totalEvaluationProfit: 0,
totalPurchaseAmount: 0,
totalProfit: 0,
totalEvaluationAmount: 0,
};
}
}

export default async function StockPortfolioPage() {
const stocks = await getStocks();
const totalStocks = await getTotalStocks();

Comment on lines +47 to +50
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

병렬 처리 및 로딩 상태 처리가 필요합니다.

현재 API 호출이 순차적으로 실행되고 있으며, 로딩 상태 처리가 없습니다.

 export default async function StockPortfolioPage() {
-  const stocks = await getStocks();
-  const totalStocks = await getTotalStocks();
+  const [stocks, totalStocks] = await Promise.all([
+    getStocks(),
+    getTotalStocks(),
+  ]).catch((error) => {
+    console.error("데이터 로딩 실패:", error);
+    return [[], {}];
+  });
📝 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
export default async function StockPortfolioPage() {
const stocks = await getStocks();
const totalStocks = await getTotalStocks();
export default async function StockPortfolioPage() {
const [stocks, totalStocks] = await Promise.all([
getStocks(),
getTotalStocks(),
]).catch((error) => {
console.error("데이터 로딩 실패:", error);
return [[], {}];
});

return (
<div className="p-30">
<h1 className="mb-30 ml-20 text-24-700 ">내 계좌</h1>
<div className="mx-auto max-w-1200 rounded-10 bg-white p-20">
<StockSummary totalStocks={totalStocks} />
<StockTable stocks={stocks} />
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions src/app/my-account/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Stock {
stockName: string; // 종목명
currentPrice: number; // 현재가
stockCount: number; // 보유수량
prevChangeRate: number; // 전일 대비 등락률
EvaluationProfit: number; // 평가손익
ProfitRate: number; // 수익률
purchaseAmount: number; // 매입단가
}
Comment on lines +1 to +9
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Stock 인터페이스 네이밍 컨벤션 개선이 필요합니다.

속성 이름에 일관성이 없습니다. 다음과 같이 개선하는 것을 추천드립니다:

 export interface Stock {
   stockName: string; // 종목명
   currentPrice: number; // 현재가
   stockCount: number; // 보유수량
   prevChangeRate: number; // 전일 대비 등락률
-  EvaluationProfit: number; // 평가손익
-  ProfitRate: number; // 수익률
+  evaluationProfit: number; // 평가손익
+  profitRate: number; // 수익률
   purchaseAmount: number; // 매입단가
 }
📝 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
export interface Stock {
stockName: string; // 종목명
currentPrice: number; // 현재가
stockCount: number; // 보유수량
prevChangeRate: number; // 전일 대비 등락률
EvaluationProfit: number; // 평가손익
ProfitRate: number; // 수익률
purchaseAmount: number; // 매입단가
}
export interface Stock {
stockName: string; // 종목명
currentPrice: number; // 현재가
stockCount: number; // 보유수량
prevChangeRate: number; // 전일 대비 등락률
evaluationProfit: number; // 평가손익
profitRate: number; // 수익률
purchaseAmount: number; // 매입단가
}


export interface TotalStocks {
memberNickname: string;
deposit: number; // 예수금
totalEvaluationProfit: number; // 총 평가손익
totalPurchaseAmount: number; // 총 매입금액
totalEvaluationAmount: number; // 총 평가금액
estimatedAsset: number; // 추정자산
rank: number; // 순위
}
4 changes: 2 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ export default async function Home() {
]);

return (
<div className="flex">
<div className="flex xl:px-230">
{/* 메인 컨텐츠 */}
<div className="flex flex-1 flex-col p-10 pt-30 md:px-25 lg:max-w-[calc(100%-320px)] lg:px-32">
<div className="flex flex-1 flex-col p-10 pt-30 md:px-32 lg:max-w-[calc(100%-320px)]">
<SearchStock />
<StockIndexCarousel initialData={initialStockData} />
<div className="flex flex-col gap-20">
Expand Down
2 changes: 1 addition & 1 deletion src/components/nav-bar/_components/nav-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const NAV_ITEMS = [
activeIcon: ChartActiveIcon,
},
{
href: "/mypage",
href: "/my-account",
name: "내 계좌",
icon: AccountIcon,
activeIcon: AccountActiveIcon,
Expand Down
4 changes: 2 additions & 2 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const config: Config = {
screens: {
sm: { max: "375px" },
md: { min: "744px" },
lg: { min: "1200px" },
xl: { min: "1280px" },
lg: { min: "1440px" },
xl: { min: "1900px" },
Comment on lines +18 to +19
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

브레이크포인트 값 재검토 필요

현재 설정된 브레이크포인트가 일반적인 화면 크기보다 매우 큽니다:

  • lg: 1440px (일반적으로 1024px나 1200px 사용)
  • xl: 1900px (일반적으로 1280px나 1440px 사용)

이로 인해 발생할 수 있는 문제점:

  • 1440px 미만의 일반적인 데스크톱 화면에서 모바일/태블릿 레이아웃이 표시될 수 있음
  • 1900px의 매우 큰 브레이크포인트는 대부분의 사용자에게 적용되지 않을 수 있음

다음과 같은 일반적인 브레이크포인트 사용을 권장드립니다:

-      lg: { min: "1440px" },
-      xl: { min: "1900px" },
+      lg: { min: "1024px" },
+      xl: { min: "1280px" },
📝 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
lg: { min: "1440px" },
xl: { min: "1900px" },
lg: { min: "1024px" },
xl: { min: "1280px" },

},
extend: {
colors: {},
Expand Down