diff --git a/index.html b/index.html index a143312..3864351 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + StockPort diff --git a/package.json b/package.json index ae0ec62..4a5e3c7 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,13 @@ "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/vite": "^4.1.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.2", "lodash": "^4.17.21", "lucide-react": "^0.514.0", "pretendard": "^1.3.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7334cc7..e8db027 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ importers: "@radix-ui/react-slot": specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.13)(react@19.1.1) + "@radix-ui/react-tabs": + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) "@tailwindcss/vite": specifier: ^4.1.8 version: 4.1.13(vite@6.3.6(@types/node@24.5.2)(jiti@2.6.0)(lightningcss@1.30.1)) @@ -34,6 +37,12 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + echarts: + specifier: ^6.0.0 + version: 6.0.0 + echarts-for-react: + specifier: ^3.0.2 + version: 3.0.2(echarts@6.0.0)(react@19.1.1) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -1003,6 +1012,22 @@ packages: "@types/react": optional: true + "@radix-ui/react-tabs@1.1.13": + resolution: + { + integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-use-callback-ref@1.1.1": resolution: { @@ -2090,6 +2115,21 @@ packages: } engines: { node: ">= 0.4" } + echarts-for-react@3.0.2: + resolution: + { + integrity: sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==, + } + peerDependencies: + echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 + react: ^15.0.0 || >=16.0.0 + + echarts@6.0.0: + resolution: + { + integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==, + } + electron-to-chromium@1.5.223: resolution: { @@ -3608,6 +3648,12 @@ packages: } engines: { node: ">= 0.4" } + size-sensor@1.0.2: + resolution: + { + integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==, + } + source-map-js@1.2.1: resolution: { @@ -3739,6 +3785,12 @@ packages: peerDependencies: typescript: ">=4.8.4" + tslib@2.3.0: + resolution: + { + integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==, + } + tslib@2.8.1: resolution: { @@ -3983,6 +4035,12 @@ packages: integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==, } + zrender@6.0.0: + resolution: + { + integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==, + } + snapshots: "@alloc/quick-lru@5.2.0": {} @@ -4493,6 +4551,22 @@ snapshots: optionalDependencies: "@types/react": 19.1.13 + "@radix-ui/react-tabs@1.1.13(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": + dependencies: + "@radix-ui/primitive": 1.1.3 + "@radix-ui/react-context": 1.1.2(@types/react@19.1.13)(react@19.1.1) + "@radix-ui/react-direction": 1.1.1(@types/react@19.1.13)(react@19.1.1) + "@radix-ui/react-id": 1.1.1(@types/react@19.1.13)(react@19.1.1) + "@radix-ui/react-presence": 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-roving-focus": 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.1.13)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.13 + "@types/react-dom": 19.1.9(@types/react@19.1.13) + "@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.13)(react@19.1.1)": dependencies: react: 19.1.1 @@ -5140,6 +5214,18 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + echarts-for-react@3.0.2(echarts@6.0.0)(react@19.1.1): + dependencies: + echarts: 6.0.0 + fast-deep-equal: 3.1.3 + react: 19.1.1 + size-sensor: 1.0.2 + + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + electron-to-chromium@1.5.223: {} enhanced-resolve@5.18.3: @@ -6160,6 +6246,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + size-sensor@1.0.2: {} + source-map-js@1.2.1: {} stop-iteration-iterator@1.1.0: @@ -6252,6 +6340,8 @@ snapshots: dependencies: typescript: 5.8.3 + tslib@2.3.0: {} + tslib@2.8.1: {} tw-animate-css@1.4.0: {} @@ -6427,3 +6517,7 @@ snapshots: yocto-queue@0.1.0: {} zod@3.25.76: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..2c9cbb1 Binary files /dev/null and b/public/images/logo.png differ diff --git a/src/App.tsx b/src/App.tsx index d19b555..a4b725e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter as Router, useRoutes } from "react-router-dom"; -import { routes } from "./routes/routes"; +import { routes } from "@/routes/routes"; const AppRoutes = () => { return useRoutes(routes); diff --git a/src/_BacktestingPage/apis/searchAsset.ts b/src/_BacktestingPage/apis/searchAsset.ts index 903047a..4001c66 100644 --- a/src/_BacktestingPage/apis/searchAsset.ts +++ b/src/_BacktestingPage/apis/searchAsset.ts @@ -1,4 +1,4 @@ -import type { SearchResult } from "../types/backtestFormType"; +import type { SearchResult } from "@/_BacktestingPage/types/backtestFormType"; import { BACKEND_BASE_URL, API_ENDPOINTS } from "@/constants/api"; export const searchAsset = async (query: string): Promise => { diff --git a/src/_BacktestingPage/components/AssetAllocation.tsx b/src/_BacktestingPage/components/AssetAllocation.tsx index bf5f9ae..21578c1 100644 --- a/src/_BacktestingPage/components/AssetAllocation.tsx +++ b/src/_BacktestingPage/components/AssetAllocation.tsx @@ -1,7 +1,7 @@ -import type { Asset } from "../types/backtestFormType"; -import AssetItem from "./AssetItem"; +import type { Asset } from "@/_BacktestingPage/types/backtestFormType"; +import AssetItem from "@/_BacktestingPage/components/AssetItem"; import { v4 as uuidv4 } from "uuid"; -import WeightSummary from "./WeightSummary"; +import WeightSummary from "@/_BacktestingPage/components/WeightSummary"; type AssetAllocationProps = { assets: Asset[]; setAssets: React.Dispatch>; diff --git a/src/_BacktestingPage/components/AssetItem.tsx b/src/_BacktestingPage/components/AssetItem.tsx index 23db5f4..a0a5dac 100644 --- a/src/_BacktestingPage/components/AssetItem.tsx +++ b/src/_BacktestingPage/components/AssetItem.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from "react"; -import type { SearchResult, Asset } from "../types/backtestFormType"; +import type { SearchResult, Asset } from "@/_BacktestingPage/types/backtestFormType"; import { debounce } from "lodash"; type AssetItemProps = { AssetIndex: number; diff --git a/src/_BacktestingPage/components/BacktestForm.tsx b/src/_BacktestingPage/components/BacktestForm.tsx index 8fe03a9..8fdc5be 100644 --- a/src/_BacktestingPage/components/BacktestForm.tsx +++ b/src/_BacktestingPage/components/BacktestForm.tsx @@ -10,7 +10,7 @@ import { Input } from "@/components/ui/input"; import { DatePicker } from "@/components/ui/datepicker"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import type { UseFormReturn } from "react-hook-form"; -import type { BacktestFormSchema } from "../utils/backtestFormSchema"; +import type { BacktestFormSchema } from "@/_BacktestingPage/utils/backtestFormSchema"; type BacktestFormProps = { form: UseFormReturn; diff --git a/src/_BacktestingPage/components/Notice.tsx b/src/_BacktestingPage/components/Notice.tsx index fa0c12b..fdcd690 100644 --- a/src/_BacktestingPage/components/Notice.tsx +++ b/src/_BacktestingPage/components/Notice.tsx @@ -1,4 +1,4 @@ -import { notices } from "../datas/notice"; +import { notices } from "@/_BacktestingPage/datas/notice"; const Notice = () => { return ( diff --git a/src/_BacktestingPage/utils/convertAssetsForRequest.ts b/src/_BacktestingPage/utils/convertAssetsForRequest.ts index 65de8b4..24865cf 100644 --- a/src/_BacktestingPage/utils/convertAssetsForRequest.ts +++ b/src/_BacktestingPage/utils/convertAssetsForRequest.ts @@ -1,4 +1,4 @@ -import type { Asset, AssetRequest } from "../types/backtestFormType"; +import type { Asset, AssetRequest } from "@/_BacktestingPage/types/backtestFormType"; // Asset type에서 id를 제거 export const convertAssetsForRequest = (assets: Asset[]): AssetRequest[] => { diff --git a/src/_BacktestingPage/utils/mapToRequest.ts b/src/_BacktestingPage/utils/mapToRequest.ts index 6d401ba..a6a471f 100644 --- a/src/_BacktestingPage/utils/mapToRequest.ts +++ b/src/_BacktestingPage/utils/mapToRequest.ts @@ -1,6 +1,6 @@ // 백엔드측 전송 양식을 맞추기 위해 formDataSchema -> BacktestRequest변한 -import type { BacktestFormSchema } from "./backtestFormSchema"; -import type { Asset, BacktestRequest } from "../types/backtestFormType"; +import type { BacktestFormSchema } from "@/_BacktestingPage/utils/backtestFormSchema"; +import type { Asset, BacktestRequest } from "@/_BacktestingPage/types/backtestFormType"; export function mapToBacktestRequest(values: BacktestFormSchema, assets: Asset[]): BacktestRequest { const formatDate = (date: Date) => date.toLocaleDateString("sv-SE"); diff --git a/src/_MainPage/components/HeroSection.tsx b/src/_MainPage/components/HeroSection.tsx index 037ea16..3397db2 100644 --- a/src/_MainPage/components/HeroSection.tsx +++ b/src/_MainPage/components/HeroSection.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { ArrowRight } from "lucide-react"; -import { HERO_CONTENT } from "../mocks/heroContent"; +import { HERO_CONTENT } from "@/_MainPage/mocks/heroContent"; import React from "react"; import { Link } from "react-router-dom"; diff --git a/src/_MainPage/components/MarketIndexCard.tsx b/src/_MainPage/components/MarketIndexCard.tsx index 728efba..fc1122b 100644 --- a/src/_MainPage/components/MarketIndexCard.tsx +++ b/src/_MainPage/components/MarketIndexCard.tsx @@ -1,6 +1,6 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { YAxis, AreaChart, Area, ResponsiveContainer } from "recharts"; -import type { MarketType, MarketIndex } from "../types/MarketIndexType"; +import type { MarketType, MarketIndex } from "@/_MainPage/types/MarketIndexType"; type MarketIndexCardProps = { marketType: MarketType; diff --git a/src/_MainPage/components/MarketIndexSection.tsx b/src/_MainPage/components/MarketIndexSection.tsx index 639fb4a..5e5997e 100644 --- a/src/_MainPage/components/MarketIndexSection.tsx +++ b/src/_MainPage/components/MarketIndexSection.tsx @@ -1,5 +1,5 @@ -import MarketIndexCard from "./MarketIndexCard"; -import { marketData } from "../mocks/marketData"; +import MarketIndexCard from "@/_MainPage/components/MarketIndexCard"; +import { marketData } from "@/_MainPage/mocks/marketData"; export default function MarketIndexSection() { return ( diff --git a/src/_MainPage/mocks/marketData.ts b/src/_MainPage/mocks/marketData.ts index 5e862ef..9dc8c91 100644 --- a/src/_MainPage/mocks/marketData.ts +++ b/src/_MainPage/mocks/marketData.ts @@ -1,4 +1,4 @@ -import type { MarketIndexData } from "../types/MarketIndexType"; +import type { MarketIndexData } from "@/_MainPage/types/MarketIndexType"; export const marketData: MarketIndexData = { KOSPI: { diff --git a/src/_MarketDetailPage/components/ChartFilterBar.tsx b/src/_MarketDetailPage/components/ChartFilterBar.tsx new file mode 100644 index 0000000..d1e1f88 --- /dev/null +++ b/src/_MarketDetailPage/components/ChartFilterBar.tsx @@ -0,0 +1,59 @@ +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ChartCandlestick, ChartLine } from "lucide-react"; + +interface ChartFilterBarProps { + onChangePeriod: (value: string) => void; + onChangeChartType: (value: string) => void; +} + +const ChartFilterBar = ({ onChangePeriod, onChangeChartType }: ChartFilterBarProps) => { + return ( +
+ + + + 1W + + + 1M + + + 1Y + + + 10Y + + + + + + + + + + + + + +
+ ); +}; +export default ChartFilterBar; diff --git a/src/_MarketDetailPage/components/StockChart.tsx b/src/_MarketDetailPage/components/StockChart.tsx new file mode 100644 index 0000000..2aab303 --- /dev/null +++ b/src/_MarketDetailPage/components/StockChart.tsx @@ -0,0 +1,149 @@ +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"; + +interface StockChartProps { + stockData: PriceHistory[]; +} + +const StockChart = ({ stockData }: StockChartProps) => { + const [period, setPeriod] = useState("1M"); + const [chartType, setChartType] = useState("candlestick"); + const isFirstRender = useRef(true); + + const handleChangePeriod = (value: string) => { + setPeriod(value as Period); + isFirstRender.current = false; + }; + + const handleChangeChartType = (value: string) => { + setChartType(value as ChartType); + isFirstRender.current = false; + }; + + const formatDate = (dateString: string) => { + const year = dateString.substring(0, 4); + const month = dateString.substring(4, 6); + const day = dateString.substring(6, 8); + + if (period === "10Y") { + return `${year}.${month}`; + } else { + return `${month}.${day}`; + } + }; + + const dates = stockData.map((item) => formatDate(item.baseDate)); + const candleData = stockData.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 option = { + animation: isFirstRender.current, + animationDuration: 1000, + animationEasing: "cubicOut", + grid: { + left: 10, + right: 60, + top: 30, + bottom: 30, + }, + tooltip: { + trigger: "axis", + }, + xAxis: { + type: "category", + data: dates, + scale: true, + boundaryGap: ["0%", "10%"], + axisLine: { show: false }, + axisTick: { show: false }, + axisLabel: { show: true, color: "#9aa0a6", fontSize: 12 }, + min: "dataMin", + max: "dataMax", + axisPointer: { + show: true, + }, + }, + yAxis: { + position: "right", + scale: true, + splitNumber: 4, + min: Math.min(...lows), + max: Math.max(...highs), + axisLine: { show: false }, + splitLine: { + show: true, + lineStyle: { + type: "solid", + color: "rgba(255,255,255,0.3)", + width: 1, + }, + }, + axisTick: { show: false }, + axisLabel: { + show: true, + inside: false, + color: "#9aa0a6", + }, + }, + series: { + type: chartType === "candlestick" ? "candlestick" : "line", + data: chartType === "candlestick" ? candleData : stockData.map((item) => item.closePrice), + smooth: false, + itemStyle: + chartType === "candlestick" + ? { + color: "#ef4444", + color0: "#3b82f6", + borderColor: "#ef4444", + borderColor0: "#3b82f6", + } + : { color: "#10b981" }, + lineStyle: chartType === "line" ? { width: 2, color: "#10b981" } : undefined, + areaStyle: + chartType === "line" + ? { + color: { + type: "linear", + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: "rgba(16, 185, 129, 0.4)", + }, + { + offset: 1, + color: "rgba(16, 185, 129, 0)", + }, + ], + }, + } + : undefined, + showSymbol: false, + }, + }; + return ( +
+ + +
+ ); +}; +export default StockChart; diff --git a/src/_MarketDetailPage/datas/stockSample.ts b/src/_MarketDetailPage/datas/stockSample.ts index 2fbff36..a26bd08 100644 --- a/src/_MarketDetailPage/datas/stockSample.ts +++ b/src/_MarketDetailPage/datas/stockSample.ts @@ -1,32 +1,293 @@ -import type { StockData } from "../types/stockDataType"; +import type { StockData } from "@/_MarketDetailPage/types/stockDataType"; export const sampleData: StockData = { - name: "삼성전자", - code: "005930", - currentPrice: 82600, - change: "RISE", - changeAmount: 900, - changeRate: 1.1, - prevClosePrice: 81700, - highPrice: 83000, - lowPrice: 81500, - tradeVolume: 15234876, - tradeValue: 1259897000000, - openPrice: 82000, + stockCode: "005930", + stockName: "삼성전자", + isinCode: "KR7005930003", + market: "KOSPI", + listedDate: "19750611", + listedShares: 5969782550, + marketCap: 437200000000000, + currentPrice: 109800, + priceHistory: [ + { + baseDate: "20251021", + openPrice: 108000, + closePrice: 108900, + highPrice: 109200, + lowPrice: 107280, + changeAmount: 900, + changeRate: 0.83, + }, + { + baseDate: "20251022", + openPrice: 108900, + closePrice: 109800, + highPrice: 109950, + lowPrice: 108000, + changeAmount: 900, + changeRate: 0.83, + }, + { + baseDate: "20251023", + openPrice: 109800, + closePrice: 110250, + highPrice: 110700, + lowPrice: 109500, + changeAmount: 450, + changeRate: 0.41, + }, + { + baseDate: "20251024", + openPrice: 110250, + closePrice: 109500, + highPrice: 110550, + lowPrice: 109200, + changeAmount: -750, + changeRate: -0.68, + }, + { + baseDate: "20251025", + openPrice: 109500, + closePrice: 109950, + highPrice: 110100, + lowPrice: 109050, + changeAmount: 450, + changeRate: 0.41, + }, + { + baseDate: "20251026", + openPrice: 109950, + closePrice: 109650, + highPrice: 110250, + lowPrice: 109350, + changeAmount: -300, + changeRate: -0.27, + }, + { + baseDate: "20251027", + openPrice: 109650, + closePrice: 111000, + highPrice: 111300, + lowPrice: 109200, + changeAmount: 1350, + changeRate: 1.23, + }, + { + baseDate: "20251028", + openPrice: 111000, + closePrice: 111600, + highPrice: 111900, + lowPrice: 110700, + changeAmount: 600, + changeRate: 0.54, + }, + { + baseDate: "20251029", + openPrice: 111600, + closePrice: 112200, + highPrice: 112500, + lowPrice: 111300, + changeAmount: 600, + changeRate: 0.54, + }, + { + baseDate: "20251030", + openPrice: 112200, + closePrice: 111750, + highPrice: 112650, + lowPrice: 111600, + changeAmount: -450, + changeRate: -0.4, + }, + { + baseDate: "20251031", + openPrice: 111750, + closePrice: 112350, + highPrice: 112650, + lowPrice: 111300, + changeAmount: 600, + changeRate: 0.54, + }, + { + baseDate: "20251101", + openPrice: 112350, + closePrice: 112950, + highPrice: 113400, + lowPrice: 111900, + changeAmount: 600, + changeRate: 0.53, + }, + { + baseDate: "20251102", + openPrice: 112950, + closePrice: 112650, + highPrice: 113250, + lowPrice: 112200, + changeAmount: -300, + changeRate: -0.27, + }, + { + baseDate: "20251103", + openPrice: 112650, + closePrice: 113700, + highPrice: 114000, + lowPrice: 112200, + changeAmount: 1050, + changeRate: 0.93, + }, + { + baseDate: "20251104", + openPrice: 113700, + closePrice: 114300, + highPrice: 114750, + lowPrice: 113400, + changeAmount: 600, + changeRate: 0.53, + }, + { + baseDate: "20251105", + openPrice: 114300, + closePrice: 114000, + highPrice: 114600, + lowPrice: 113700, + changeAmount: -300, + changeRate: -0.26, + }, + { + baseDate: "20251106", + openPrice: 114000, + closePrice: 114600, + highPrice: 114900, + lowPrice: 113550, + changeAmount: 600, + changeRate: 0.52, + }, + { + baseDate: "20251107", + openPrice: 114600, + closePrice: 115500, + highPrice: 115950, + lowPrice: 114450, + changeAmount: 900, + changeRate: 0.79, + }, + { + baseDate: "20251108", + openPrice: 115500, + closePrice: 115200, + highPrice: 115800, + lowPrice: 115050, + changeAmount: -300, + changeRate: -0.26, + }, + { + baseDate: "20251109", + openPrice: 115200, + closePrice: 115650, + highPrice: 115950, + lowPrice: 115050, + changeAmount: 450, + changeRate: 0.39, + }, + { + baseDate: "20251110", + openPrice: 115650, + closePrice: 116400, + highPrice: 116700, + lowPrice: 115350, + changeAmount: 750, + changeRate: 0.65, + }, + { + baseDate: "20251111", + openPrice: 116400, + closePrice: 116850, + highPrice: 117000, + lowPrice: 115950, + changeAmount: 450, + changeRate: 0.39, + }, + { + baseDate: "20251112", + openPrice: 116850, + closePrice: 117150, + highPrice: 117600, + lowPrice: 116550, + changeAmount: 300, + changeRate: 0.26, + }, + { + baseDate: "20251113", + openPrice: 117150, + closePrice: 116850, + highPrice: 117450, + lowPrice: 116550, + changeAmount: -300, + changeRate: -0.26, + }, + { + baseDate: "20251114", + openPrice: 116850, + closePrice: 117300, + highPrice: 117600, + lowPrice: 116700, + changeAmount: 450, + changeRate: 0.38, + }, + { + baseDate: "20251115", + openPrice: 117300, + closePrice: 117900, + highPrice: 118350, + lowPrice: 117000, + changeAmount: 600, + changeRate: 0.51, + }, + { + baseDate: "20251116", + openPrice: 117900, + closePrice: 117600, + highPrice: 118200, + lowPrice: 117450, + changeAmount: -300, + changeRate: -0.25, + }, + { + baseDate: "20251117", + openPrice: 117600, + closePrice: 118500, + highPrice: 118800, + lowPrice: 117450, + changeAmount: 900, + changeRate: 0.77, + }, + { + baseDate: "20251118", + openPrice: 118500, + closePrice: 118950, + highPrice: 119250, + lowPrice: 118200, + changeAmount: 450, + changeRate: 0.38, + }, + { + baseDate: "20251119", + openPrice: 118950, + closePrice: 119550, + highPrice: 120000, + lowPrice: 118800, + changeAmount: 600, + changeRate: 0.5, + }, + { + baseDate: "20251120", + openPrice: 119550, + closePrice: 119100, + highPrice: 119850, + lowPrice: 118800, + changeAmount: -450, + changeRate: -0.38, + }, + ], }; - -// // 예시: SK하이닉스 하락 데이터 -// const sampleData: StockData = { -// name: 'SK하이닉스', -// code: '000660', -// currentPrice: 228000, -// change: 'FALL', -// changeAmount: 3000, -// changeRate: -1.30, -// prevClosePrice: 231000, -// highPrice: 232000, -// lowPrice: 227500, -// tradeVolume: 3456789, -// tradeValue: 791654112000, -// openPrice: 230500, -// }; diff --git a/src/_MarketDetailPage/types/stockDataType.ts b/src/_MarketDetailPage/types/stockDataType.ts index f09ff62..503d9ee 100644 --- a/src/_MarketDetailPage/types/stockDataType.ts +++ b/src/_MarketDetailPage/types/stockDataType.ts @@ -1,14 +1,24 @@ -export interface StockData { - name: string; - code: string; - currentPrice: number; - change: "RISE" | "FALL" | "EVEN"; - changeAmount: number; - changeRate: number; - prevClosePrice: number; +export interface PriceHistory { + baseDate: string; + openPrice: number; + closePrice: number; highPrice: number; lowPrice: number; - tradeVolume: number; - tradeValue: number; - openPrice: number; + changeAmount: number; + changeRate: number; +} + +export interface StockData { + stockCode: string; + stockName: string; + isinCode: string; + market: string; + listedDate: string; + listedShares: number; + marketCap: number; + currentPrice: number; + priceHistory: PriceHistory[]; } + +export type Period = "1W" | "1M" | "1Y" | "10Y"; +export type ChartType = "candlestick" | "line"; diff --git a/src/_MarketsPage/components/MarketList.tsx b/src/_MarketsPage/components/MarketList.tsx index 3382d97..d061f2e 100644 --- a/src/_MarketsPage/components/MarketList.tsx +++ b/src/_MarketsPage/components/MarketList.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import type { MarketItem } from "../types/marketItem"; +import type { MarketItem } from "@/_MarketsPage/types/marketItem"; import { useNavigate } from "react-router-dom"; interface MarketListProps { @@ -14,18 +13,18 @@ const getChangeInfo = (changeRate: number) => { return { className: "text-white", icon: "" }; }; -const MarketList: React.FC = ({ items, currentPage, itemsPerPage }) => { +const MarketList = ({ items, currentPage, itemsPerPage }: MarketListProps) => { const navigate = useNavigate(); const startIndex = (currentPage - 1) * itemsPerPage; return ( - +
- - - - - + + + + + diff --git a/src/_MarketsPage/datas/MarketMockData.ts b/src/_MarketsPage/datas/MarketMockData.ts index 3c5ac4d..bb657b6 100644 --- a/src/_MarketsPage/datas/MarketMockData.ts +++ b/src/_MarketsPage/datas/MarketMockData.ts @@ -1,4 +1,4 @@ -import type { MarketItem } from "../types/marketItem"; +import type { MarketItem } from "@/_MarketsPage/types/marketItem"; export const MOCK_DATA: MarketItem[] = [ { diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index f45ff03..099ca98 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -1,6 +1,6 @@ -import Logo from "./Logo"; -import NavItem from "./NavItem"; -import SearchBar from "./SearchBar"; +import Logo from "@/components/Navbar/Logo"; +import NavItem from "@/components/Navbar/NavItem"; +import SearchBar from "@/components/Navbar/SearchBar"; const Navbar = () => { return ( diff --git a/src/components/Navbar/SearchBar.tsx b/src/components/Navbar/SearchBar.tsx index ae600e0..f0542ce 100644 --- a/src/components/Navbar/SearchBar.tsx +++ b/src/components/Navbar/SearchBar.tsx @@ -1,4 +1,4 @@ -import { Input } from "../ui/input"; +import { Input } from "@/components/ui/input"; import { Search } from "lucide-react"; const SearchBar = () => { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 8667a3f..508b85b 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -3,7 +3,7 @@ import { Slot } from "@radix-ui/react-slot"; import { type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -import { buttonVariants } from "./buttonVariants"; +import { buttonVariants } from "@/components/ui/buttonVariants"; function Button({ className, diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..1023a04 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +function Tabs({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function TabsList({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function TabsTrigger({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function TabsContent({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/layouts/Layout.tsx b/src/layouts/Layout.tsx index f17fe7f..2f1294b 100644 --- a/src/layouts/Layout.tsx +++ b/src/layouts/Layout.tsx @@ -1,5 +1,5 @@ import { Outlet } from "react-router-dom"; -import Navbar from "../components/Navbar/Navbar.tsx"; +import Navbar from "@/components/Navbar/Navbar"; const Layout = () => { return ( diff --git a/src/main.tsx b/src/main.tsx index b331976..9b262ea 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import "./styles/tailwind.css"; -import App from "./App.tsx"; +import "@/styles/tailwind.css"; +import App from "@/App"; createRoot(document.getElementById("root")!).render( diff --git a/src/pages/MarketDetailPage.tsx b/src/pages/MarketDetailPage.tsx index e90dbd7..946b271 100644 --- a/src/pages/MarketDetailPage.tsx +++ b/src/pages/MarketDetailPage.tsx @@ -1,47 +1,61 @@ import { useState, useEffect, useMemo } from "react"; -import type { StockData } from "../_MarketDetailPage/types/stockDataType"; +import { format, subDays } from "date-fns"; +import { type PriceHistory, type StockData } from "@/_MarketDetailPage/types/stockDataType"; import DetailItem from "@/_MarketDetailPage/components/DetailItem"; -import { sampleData } from "../_MarketDetailPage/datas/stockSample"; +import { sampleData } from "@/_MarketDetailPage/datas/stockSample"; +import StockChart from "@/_MarketDetailPage/components/StockChart"; const MarketDetailPage = () => { const [stockData, setStockData] = useState(null); + const [todayData, setTodayData] = useState(); + const [prevData, setPrevData] = useState(); // 데이터 로딩 (실제로는 API 호출) useEffect(() => { setStockData(sampleData); + const today = format(new Date(), "yyyyMMdd"); + const yesterday = format(subDays(new Date(), 1), "yyyyMMdd"); + + const foundTodayData = sampleData.priceHistory.find((data) => data.baseDate === today); + if (foundTodayData) { + setTodayData(foundTodayData); + } + + const foundPrevData = sampleData.priceHistory.find((data) => data.baseDate === yesterday); + if (foundPrevData) { + setPrevData(foundPrevData); + } }, []); // 등락 정보에 따른 스타일 계산 const changeInfo = useMemo(() => { - if (!stockData) return null; - switch (stockData.change) { - case "RISE": - return { color: "text-red-500", icon: "▲" }; - case "FALL": - return { color: "text-blue-500", icon: "▼" }; - default: - return { color: "text-white", icon: null }; + if (!stockData || !todayData) return null; + + if (todayData.changeRate > 0) { + return { color: "text-red-500", icon: "▲" }; + } else if (todayData.changeRate < 0) { + return { color: "text-blue-500", icon: "▼" }; + } else { + return { color: "text-white", icon: null }; } - }, [stockData]); + }, [stockData, todayData]); - if (!stockData || !changeInfo) { + if (!stockData || !changeInfo || !todayData || !prevData) { return (
Loading...
); } - const highPriceColor = - stockData.highPrice > stockData.prevClosePrice ? "text-red-500" : "text-white"; - const lowPriceColor = - stockData.lowPrice < stockData.prevClosePrice ? "text-blue-500" : "text-white"; + const highPriceColor = todayData.highPrice > prevData.highPrice ? "text-red-500" : "text-white"; + const lowPriceColor = todayData.lowPrice < prevData.lowPrice ? "text-blue-500" : "text-white"; return ( -
+
{/* 상단: 종목명 및 코드 */}
-

{stockData.name}

-

{stockData.code}

+

{stockData.stockName}

+

{stockData.stockCode}

@@ -55,18 +69,18 @@ const MarketDetailPage = () => {
전일대비 {changeInfo.icon} - {stockData.changeAmount.toLocaleString()} - ({stockData.changeRate.toFixed(2)}%) + {todayData.changeAmount.toLocaleString()} + ({todayData.changeRate.toFixed(2)}%)
{/* 우측: 상세 거래 정보 */}
- + { value={stockData.currentPrice.toLocaleString()} className={changeInfo.color} /> - + -
{/* 하단: 차트 */} -
+
-

여기에 차트 표시

- {/* 예: */} +
-
+
); }; diff --git a/src/pages/MarketsPage.tsx b/src/pages/MarketsPage.tsx index 3e5f8ee..1bc1fb3 100644 --- a/src/pages/MarketsPage.tsx +++ b/src/pages/MarketsPage.tsx @@ -1,17 +1,15 @@ -import { useState, useEffect, useMemo } from "react"; -import MarketList from "../_MarketsPage/components/MarketList"; -import { MOCK_DATA } from "../_MarketsPage/datas/MarketMockData"; -import type { MarketItem } from "../_MarketsPage/types/marketItem"; -import Pagination from "../components/Pagination"; -import Title from "../components/Title"; +import { useState, useEffect } from "react"; +import MarketList from "@/_MarketsPage/components/MarketList"; +import { MOCK_DATA } from "@/_MarketsPage/datas/MarketMockData"; +import type { MarketItem } from "@/_MarketsPage/types/marketItem"; +import Pagination from "@/components/Pagination"; +import Title from "@/components/Title"; const ITEMS_PER_PAGE = 10; -const CATEGORIES = ["거래대금", "거래량", "급상승", "급하락", "인기"]; const MarketsPage = () => { const [currentPage, setCurrentPage] = useState(1); const [marketData, setMarketData] = useState([]); - const [activeCategory, setActiveCategory] = useState("거래대금"); // 주가 데이터 로딩 useEffect(() => { @@ -19,24 +17,9 @@ const MarketsPage = () => { setMarketData(MOCK_DATA); }, []); - // 카테고리 변경 시 데이터 정렬 로직 - const sortedData = useMemo(() => { - const data = [...marketData]; - switch (activeCategory) { - case "급상승": - return data.sort((a, b) => b.changeRate - a.changeRate); - case "급하락": - return data.sort((a, b) => a.changeRate - b.changeRate); - // TODO: '거래대금', '거래량', '인기'에 대한 정렬 로직 구현 - case "거래대금": - default: - return data; - } - }, [activeCategory, marketData]); - // 페이지네이션 계산 - const totalPages = Math.ceil(sortedData.length / ITEMS_PER_PAGE); - const currentItems = sortedData.slice( + const totalPages = Math.ceil(marketData.length / ITEMS_PER_PAGE); + const currentItems = marketData.slice( (currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE ); @@ -52,23 +35,6 @@ const MarketsPage = () => {
- {/* 카테고리 필터 */} -
- {CATEGORIES.map((category) => ( - - ))} -
-
종목자산명현재가등락률거래대금종목자산명현재가등락률거래대금