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
14 changes: 11 additions & 3 deletions src/_MarketDetailPage/components/ChartFilterBar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ChartCandlestick, ChartLine } from "lucide-react";
import { type Period, type ChartType } from "@/_MarketDetailPage/types/stockDataType";

interface ChartFilterBarProps {
period: Period;
chartType: ChartType;
onChangePeriod: (value: string) => void;
onChangeChartType: (value: string) => void;
}

const ChartFilterBar = ({ onChangePeriod, onChangeChartType }: ChartFilterBarProps) => {
const ChartFilterBar = ({
period,
chartType,
onChangePeriod,
onChangeChartType,
}: ChartFilterBarProps) => {
return (
<div className="flex justify-start items-center gap-4 ml-2.5 h-20">
<Tabs defaultValue="1M" onValueChange={onChangePeriod}>
<Tabs value={period} onValueChange={onChangePeriod}>
<TabsList className="bg-white/5 border-white/10">
<TabsTrigger
value="1W"
Expand Down Expand Up @@ -37,7 +45,7 @@ const ChartFilterBar = ({ onChangePeriod, onChangeChartType }: ChartFilterBarPro
</TabsTrigger>
</TabsList>
</Tabs>
<Tabs defaultValue="candlestick" onValueChange={onChangeChartType}>
<Tabs value={chartType} onValueChange={onChangeChartType}>
<TabsList className="bg-white/5 border-white/10">
<TabsTrigger
value="candlestick"
Expand Down
73 changes: 43 additions & 30 deletions src/_MarketDetailPage/components/StockChart.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,30 @@
import ChartFilterBar from "@/_MarketDetailPage/components/ChartFilterBar";
import ReactECharts from "echarts-for-react";
import {
type ChartType,
type Period,
type PriceHistory,
} from "@/_MarketDetailPage/types/stockDataType";
import { useState, useRef } from "react";
import { type ChartType, type Period } from "@/_MarketDetailPage/types/stockDataType";
import { useRef, useEffect, useMemo } from "react";
import { useGetStockDetail } from "@/lib/hooks/useGetStockDetail";
import { Spinner } from "@/components/ui/spinner";

interface StockChartProps {
stockData: PriceHistory[];
stockCode: string;
period: Period;
chartType: ChartType;
}

const StockChart = ({ stockData }: StockChartProps) => {
const [period, setPeriod] = useState<Period>("1M");
const [chartType, setChartType] = useState<ChartType>("candlestick");
const StockChart = ({ stockCode, period, chartType }: StockChartProps) => {
const isFirstRender = useRef(true);

const handleChangePeriod = (value: string) => {
setPeriod(value as Period);
isFirstRender.current = false;
};
// period에 해당하는 API 요청
const { data: stockData, isLoading } = useGetStockDetail(stockCode, period);

const handleChangeChartType = (value: string) => {
setChartType(value as ChartType);
useEffect(() => {
isFirstRender.current = false;
};
}, [chartType, period]);

const formatDate = (dateString: string) => {
const year = dateString.substring(0, 4);
const month = dateString.substring(4, 6);
const day = dateString.substring(6, 8);
const parts = dateString.split("-");
const year = parts[0];
const month = parts[1];
const day = parts[2];

if (period === "10Y") {
return `${year}.${month}`;
Expand All @@ -38,15 +33,34 @@ const StockChart = ({ stockData }: StockChartProps) => {
}
};

const dates = stockData.map((item) => formatDate(item.baseDate));
const candleData = stockData.map((item) => [
// period에 따라 데이터 필터링 (useMemo는 early return 전에 호출)
const filteredDataByPeriod = useMemo(() => {
if (!stockData || !stockData.stockPriceList) {
return [];
}

// 모든 데이터를 역순으로 반환 (10년 데이터도 모두 렌더링)
return [...stockData.stockPriceList].reverse();
}, [stockData, period]);

// API 데이터가 없으면 로딩 표시
if (isLoading || !stockData || !stockData.stockPriceList || filteredDataByPeriod.length === 0) {
return (
<div className="flex justify-center items-center h-500">
<Spinner className="size-12" />
</div>
);
}

const dates = filteredDataByPeriod.map((item) => formatDate(item.baseDate));
const candleData = filteredDataByPeriod.map((item) => [
item.openPrice,
item.closePrice,
item.lowPrice,
item.highPrice,
]);
const lows = stockData.map((d) => d.lowPrice);
const highs = stockData.map((d) => d.highPrice);
const lows = filteredDataByPeriod.map((d) => d.lowPrice);
const highs = filteredDataByPeriod.map((d) => d.highPrice);

const option = {
animation: isFirstRender.current,
Expand Down Expand Up @@ -99,7 +113,10 @@ const StockChart = ({ stockData }: StockChartProps) => {
},
series: {
type: chartType === "candlestick" ? "candlestick" : "line",
data: chartType === "candlestick" ? candleData : stockData.map((item) => item.closePrice),
data:
chartType === "candlestick"
? candleData
: filteredDataByPeriod.map((item) => item.closePrice),
smooth: false,
itemStyle:
chartType === "candlestick"
Expand Down Expand Up @@ -138,10 +155,6 @@ const StockChart = ({ stockData }: StockChartProps) => {
};
return (
<div className="flex-col gap-20 m-10 w-full">
<ChartFilterBar
onChangePeriod={handleChangePeriod}
onChangeChartType={handleChangeChartType}
/>
<ReactECharts option={option} style={{ height: 500 }} />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/_MarketDetailPage/datas/stockSample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const sampleData: StockData = {
listedShares: 5969782550,
marketCap: 437200000000000,
currentPrice: 109800,
priceHistory: [
stockPriceList: [
{
baseDate: "20251021",
openPrice: 108000,
Expand Down
4 changes: 2 additions & 2 deletions src/_MarketDetailPage/types/stockDataType.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface PriceHistory {
export interface StockPriceList {
baseDate: string;
openPrice: number;
closePrice: number;
Expand All @@ -17,7 +17,7 @@ export interface StockData {
listedShares: number;
marketCap: number;
currentPrice: number;
priceHistory: PriceHistory[];
stockPriceList: StockPriceList[];
}

export type Period = "1W" | "1M" | "1Y" | "10Y";
Expand Down
27 changes: 15 additions & 12 deletions src/_MarketsPage/components/MarketList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { MarketItem } from "@/_MarketsPage/types/marketItem";
import type { StockListItem } from "@/_MarketsPage/types/marketItem";
import { useNavigate } from "react-router-dom";
import { formatNumber } from "@/lib/utils";

interface MarketListProps {
items: MarketItem[];
items: StockListItem[];
currentPage: number;
itemsPerPage: number;
}
Expand All @@ -24,39 +25,41 @@ const MarketList = ({ items, currentPage, itemsPerPage }: MarketListProps) => {
<th className="p-4 w-40 font-normal text-gray-400 text-sm text-left">자산명</th>
<th className="p-4 w-32 font-normal text-gray-400 text-sm text-left">현재가</th>
<th className="p-4 w-32 font-normal text-gray-400 text-sm text-left">등락률</th>
<th className="p-4 w-32 font-normal text-gray-400 text-sm text-left">거래대금</th>
<th className="p-4 w-32 font-normal text-gray-400 text-sm text-left">시가총액</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
const { className, icon } = getChangeInfo(item.changeRate);
const latestPrice = item.stockPriceList?.[0];
const changeRate = latestPrice?.changeRate ?? 0;
const { className, icon } = getChangeInfo(changeRate);
return (
<tr
key={item.id}
key={item.stockCode}
className="hover:bg-white/5 border-white/10 border-b cursor-pointer"
onClick={() => navigate(`/markets/${item.code}`)}
onClick={() => navigate(`/markets/${item.stockCode}`)}
>
{/* 순번 */}
<td className="p-4 text-gray-400 align-middle">{startIndex + index + 1}</td>

{/* 종목명과 코드 */}
<td className="p-4 align-middle">
<div className="font-bold">{item.name}</div>
<div className="text-gray-400 text-sm">{item.code}</div>
<div className="font-bold">{item.stockName}</div>
<div className="text-gray-400 text-sm">{item.stockCode}</div>
</td>

{/* 현재가 */}
<td className={`p-4 align-middle font-bold ${className}`}>
{item.price.toLocaleString()}원
{formatNumber(latestPrice?.closePrice ?? 0)}원
</td>

{/* 등락률 */}
<td className={`p-4 align-middle ${className}`}>
{icon} {Math.abs(item.changeRate).toFixed(2)}%
{icon} {Math.abs(changeRate).toFixed(2)}%
</td>

{/* 거래대금 */}
<td className="p-4 align-middle">{item.tradeVolume}</td>
{/* 시가총액 */}
<td className="p-4 align-middle">{formatNumber(item.marketCap)}</td>
</tr>
);
})}
Expand Down
Loading