diff --git a/package-lock.json b/package-lock.json index 902bfeb9..b183c732 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "0.1.0", "dependencies": { "@datadog/browser-rum": "^5.30.1", - "@tanstack/react-query": "^5.60.6", + "@tanstack/react-query": "^5.62.7", + "@tanstack/react-query-devtools": "^5.62.7", "@toast-ui/chart": "^4.6.1", "@toast-ui/editor": "^3.2.2", "@toast-ui/editor-plugin-chart": "^3.0.1", "@toast-ui/editor-plugin-color-syntax": "^3.1.0", "@toast-ui/react-editor": "^3.2.3", "apexcharts": "^4.1.0", - "axios": "^1.7.7", "cookie": "^1.0.1", "dayjs": "^1.11.13", "dd-trace": "^5.26.0", @@ -1294,6 +1294,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-devtools": { + "version": "5.61.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.61.4.tgz", + "integrity": "sha512-21Tw+u8E3IJJj4A/Bct4H0uBaDTEu7zBrR79FeSyY+mS2gx5/m316oDtJiKkILc819VSTYt+sFzODoJNcpPqZQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.62.7", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.7.tgz", @@ -1310,6 +1320,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.62.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.62.8.tgz", + "integrity": "sha512-SwjXjQTRONd9WPeKVQQ9framG7YNqPV8PS+EGNVNXAyz2XThulMRCvZnh2+3DggnjcYM7YcpnuoZ4RH7q13p0g==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.61.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.62.8", + "react": "^18 || ^19" + } + }, "node_modules/@toast-ui/chart": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/@toast-ui/chart/-/chart-4.6.1.tgz", @@ -2175,12 +2202,6 @@ "dev": true, "license": "MIT" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -2245,17 +2266,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2720,18 +2730,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -3021,15 +3019,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4250,26 +4239,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4297,20 +4266,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -7128,27 +7083,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -8209,12 +8143,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 1380e524..df0999cc 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,14 @@ }, "dependencies": { "@datadog/browser-rum": "^5.30.1", - "@tanstack/react-query": "^5.60.6", + "@tanstack/react-query": "^5.62.7", + "@tanstack/react-query-devtools": "^5.62.7", "@toast-ui/chart": "^4.6.1", "@toast-ui/editor": "^3.2.2", "@toast-ui/editor-plugin-chart": "^3.0.1", "@toast-ui/editor-plugin-color-syntax": "^3.1.0", "@toast-ui/react-editor": "^3.2.3", "apexcharts": "^4.1.0", - "axios": "^1.7.7", "cookie": "^1.0.1", "dayjs": "^1.11.13", "dd-trace": "^5.26.0", diff --git a/public/images/4c.png b/public/images/4c.png deleted file mode 100644 index 5f5be2ff..00000000 Binary files a/public/images/4c.png and /dev/null differ diff --git "a/public/images/stocks/\353\204\244\354\235\264\353\262\204.png" b/public/images/stocks/0.png similarity index 100% rename from "public/images/stocks/\353\204\244\354\235\264\353\262\204.png" rename to public/images/stocks/0.png diff --git "a/public/images/stocks/\355\217\254\354\212\244\354\275\224.png" b/public/images/stocks/1.png similarity index 100% rename from "public/images/stocks/\355\217\254\354\212\244\354\275\224.png" rename to public/images/stocks/1.png diff --git "a/public/images/stocks/SK\355\225\230\354\235\264\353\213\211\354\212\244.png" b/public/images/stocks/2.png similarity index 100% rename from "public/images/stocks/SK\355\225\230\354\235\264\353\213\211\354\212\244.png" rename to public/images/stocks/2.png diff --git a/public/images/stocks/3.png b/public/images/stocks/3.png new file mode 100644 index 00000000..23bacb56 Binary files /dev/null and b/public/images/stocks/3.png differ diff --git "a/public/images/stocks/kb\352\270\210\354\234\265.png" "b/public/images/stocks/kb\352\270\210\354\234\265.png" deleted file mode 100644 index 1289fb5c..00000000 Binary files "a/public/images/stocks/kb\352\270\210\354\234\265.png" and /dev/null differ diff --git "a/public/images/stocks/lg\355\231\224\355\225\231.png" "b/public/images/stocks/lg\355\231\224\355\225\231.png" deleted file mode 100644 index a7a72a0f..00000000 Binary files "a/public/images/stocks/lg\355\231\224\355\225\231.png" and /dev/null differ diff --git "a/public/images/stocks/\354\202\274\354\204\261\354\240\204\354\236\220.png" "b/public/images/stocks/\354\202\274\354\204\261\354\240\204\354\236\220.png" deleted file mode 100644 index e26073f0..00000000 Binary files "a/public/images/stocks/\354\202\274\354\204\261\354\240\204\354\236\220.png" and /dev/null differ diff --git "a/public/images/stocks/\355\230\204\353\214\200.png" "b/public/images/stocks/\355\230\204\353\214\200.png" deleted file mode 100644 index 21896dc8..00000000 Binary files "a/public/images/stocks/\355\230\204\353\214\200.png" and /dev/null differ diff --git a/public/images/thumbnail/stock1.png b/public/images/thumbnail/stock1.png new file mode 100644 index 00000000..04cce2bd Binary files /dev/null and b/public/images/thumbnail/stock1.png differ diff --git a/public/images/thumbnail/stock2.png b/public/images/thumbnail/stock2.png new file mode 100644 index 00000000..784d01fb Binary files /dev/null and b/public/images/thumbnail/stock2.png differ diff --git a/public/images/thumbnail/stock3.png b/public/images/thumbnail/stock3.png new file mode 100644 index 00000000..f6d27493 Binary files /dev/null and b/public/images/thumbnail/stock3.png differ diff --git a/public/images/3c.png b/public/images/thumbnail/stock4.png similarity index 100% rename from public/images/3c.png rename to public/images/thumbnail/stock4.png diff --git a/src/app/(404)/[...404]/Compass.tsx b/src/app/(404)/[...404]/Compass.tsx deleted file mode 100644 index 30eccbd7..00000000 --- a/src/app/(404)/[...404]/Compass.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; - -import Icons from '@/app/components/common/Icons'; -import { needleIcon } from '@/app/constants/iconPath'; -import Link from 'next/link'; -import { useState } from 'react'; - -const Compass = () => { - const [angle, setAngle] = useState(0); - - const handleMouseMove = (e: React.MouseEvent) => { - const compass = e.currentTarget.getBoundingClientRect(); - const centerX = compass.left + compass.width / 2; - const centerY = compass.top + compass.height / 2; - const radians = Math.atan2(e.clientY - centerY, e.clientX - centerX); - const degrees = radians * (180 / Math.PI) + 90; - setAngle(degrees); - }; - - return ( -
-
-
- -
- - 홈 - - - 로그인 - - - 블로그 - - - 모의투자 - -
-
- ); -}; - -export default Compass; diff --git a/src/app/(404)/[...404]/layout.tsx b/src/app/(404)/[...404]/layout.tsx deleted file mode 100644 index 709f5382..00000000 --- a/src/app/(404)/[...404]/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Metadata } from 'next'; -import localFont from 'next/font/local'; -import '../../styles/globals.css'; - -export const metadata: Metadata = { - title: 'Flex', - description: '내 곁에 든든한 재테크 친구! Flex', -}; - -const pretendard = localFont({ - src: '../../static/fonts/PretendardVariable.woff2', - display: 'swap', - weight: '45 920', - variable: '--font-pretendard', -}); - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - -
{children}
- - - ); -} diff --git a/src/app/(404)/[...404]/page.tsx b/src/app/(404)/[...404]/page.tsx deleted file mode 100644 index 075b4bb0..00000000 --- a/src/app/(404)/[...404]/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Compass from './Compass'; - -export default function NotFound() { - return ( -
-
-
-

404

-

존재하지 않는 페이지...

-
-
-

찾을 수 없는 페이지에 도착하셨어요

-

- 이제 길을 다시 찾아볼까요? 원하는 페이지를 찾도록 도와 드릴게요! -

-
-
- -
- ); -} diff --git a/src/app/(route)/layout.tsx b/src/app/(route)/layout.tsx index 6ecdbbc6..62d0f8fa 100644 --- a/src/app/(route)/layout.tsx +++ b/src/app/(route)/layout.tsx @@ -1,10 +1,10 @@ import type { Metadata } from 'next'; import localFont from 'next/font/local'; import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; import Header from '../components/common/layout/Header'; -import Rum from '../components/common/layout/Rum'; +import TanStackProvider from '../components/common/layout/TanstackProvider'; import UserProvider from '../components/common/layout/useProvider'; -import 'react-toastify/dist/ReactToastify.css'; import '../styles/globals.css'; export const metadata: Metadata = { @@ -29,13 +29,13 @@ export default function RootLayout({ - +
{children} - + ); diff --git a/src/app/(route)/prediction/page.tsx b/src/app/(route)/prediction/page.tsx index 9d0db252..43c9b9ed 100644 --- a/src/app/(route)/prediction/page.tsx +++ b/src/app/(route)/prediction/page.tsx @@ -4,9 +4,9 @@ import DownSideBar from '@/app/components/simulation/side/DownSideBar'; function SimulationPage() { return ( -
-
-
+
+
+
diff --git a/src/app/(route)/simulation/page.tsx b/src/app/(route)/simulation/page.tsx index 5d799da2..13b9a2b5 100644 --- a/src/app/(route)/simulation/page.tsx +++ b/src/app/(route)/simulation/page.tsx @@ -6,9 +6,9 @@ import TradeContainer from '@/app/components/simulation/trade/TradeContainer'; function SimulationPage() { return ( -
-
-
+
+
+
diff --git a/src/app/(route)/user/[blogName]/page.tsx b/src/app/(route)/user/[blogName]/page.tsx index 80d005ce..53b9c615 100644 --- a/src/app/(route)/user/[blogName]/page.tsx +++ b/src/app/(route)/user/[blogName]/page.tsx @@ -6,8 +6,6 @@ import { useParams } from 'next/navigation'; const UserPage = () => { const { blogName } = useParams(); const decodedBlogName = decodeURIComponent(blogName as string); - console.log(decodedBlogName); - return (
diff --git a/src/app/_types/main.ts b/src/app/_types/main.ts index 5e4e8f38..2762d600 100644 --- a/src/app/_types/main.ts +++ b/src/app/_types/main.ts @@ -133,3 +133,12 @@ interface UserBalanceTypes { totalProfit: number; recentTransactionAt: string; } + +interface RankDataTypes { + rank: number; + userId: number; + nickname: string; + blogName: string; + profileImageUrl: string; + totalProfit: number; +} diff --git a/src/app/_types/simulation.ts b/src/app/_types/simulation.ts index 5f675d32..8e182d54 100644 --- a/src/app/_types/simulation.ts +++ b/src/app/_types/simulation.ts @@ -56,6 +56,7 @@ interface HoldStockTypes { avgPrice: number; principal: number; createdAt: string; + symbolImageUrl: string; } interface MinPriceTypes { @@ -98,3 +99,19 @@ interface LivePriceTypes { currentPrice: string; dateTime: string; } + +interface BackTestTypes { + startDate: string; + endDate: string; + periodType: string; + totalPrice: number; + totalProfit: number; + profit: number; +} + +type BackTestOrderTypes = '매일' | '매주' | '매월' | '매년'; + +type StockPriceTypes = { + stockCode: string; + price: string; +}; diff --git a/src/app/_types/stock.ts b/src/app/_types/stock.ts index 7a78f46a..af7eb51a 100644 --- a/src/app/_types/stock.ts +++ b/src/app/_types/stock.ts @@ -3,7 +3,7 @@ interface StockInfoTypes { stockName: string; symbolImageUrl: string; corpInfo: CorpInfoTypes; - isInterested: boolean; + isInterested: string | null; date: string; closePrice: 0; volume: 0; @@ -43,6 +43,17 @@ interface StockDetailInfoTypes { }; } +interface EntValueTypes { + date: string; + stockcode: string; + BPS: number; + PER: number; + PBR: number; + EPS: number; + DIV: number; + DPS: number; +} + interface InterestedStockTypes { interestStockId: string; stockcode: string; @@ -156,9 +167,14 @@ interface TransactionDataTypes { transactionId: number; userId: number; investment: InvestmentDataTypes; - credit: number; + credit: { + creditId: number; + credits: number; + creditType: string; + }; totalProfit: number; balance: number; + createdAt: string; } interface InvestmentDataTypes { @@ -172,6 +188,13 @@ interface InvestmentDataTypes { profit: number; } +interface TransactionResponse { + content: TransactionDataTypes[]; + hasNext: true; + first: true; + last: true; +} + interface InterestedPriceTypes { stockcode: string; currentPrice: string; diff --git a/src/app/api/analysis/route.ts b/src/app/api/analysis/route.ts index c3682494..4b7092c0 100644 --- a/src/app/api/analysis/route.ts +++ b/src/app/api/analysis/route.ts @@ -3,7 +3,5 @@ import { NextResponse } from 'next/server'; export async function GET(req: Request) { const data = await getAnalysis(req); - console.log(data); - return NextResponse.json(data); } diff --git a/src/app/api/blog/search/route.ts b/src/app/api/blog/search/route.ts index b61c4b64..1c6ed61c 100644 --- a/src/app/api/blog/search/route.ts +++ b/src/app/api/blog/search/route.ts @@ -7,8 +7,6 @@ export async function GET(req: Request) { const query = searchParams.get('query') || ''; // 검색어 const page = parseInt(searchParams.get('page') || '0', 10); // 페이지 번호를 정수로 변환 - console.log('Parsed Page:', page); // 디버깅용 로그 - const data = await getSearchPost(req, page, query); return NextResponse.json(data); diff --git a/src/app/api/main/stockRank/route.ts b/src/app/api/main/stockRank/route.ts index 242fdf77..0fb644ee 100644 --- a/src/app/api/main/stockRank/route.ts +++ b/src/app/api/main/stockRank/route.ts @@ -6,7 +6,6 @@ export async function POST(req: Request) { const { searchParams } = new URL(req.url); const type = searchParams.get('type') || ''; - const response = await postStockRank(body, req, type); - - return NextResponse.json(response); + const data = await postStockRank(body, req, type); + return NextResponse.json(data); } diff --git a/src/app/api/main/userRank/route.ts b/src/app/api/main/userRank/route.ts new file mode 100644 index 00000000..0a646ad7 --- /dev/null +++ b/src/app/api/main/userRank/route.ts @@ -0,0 +1,8 @@ +import { getUserRanking } from '@/app/service/getRequest'; +import { NextResponse } from 'next/server'; + +export async function GET(req: Request) { + const data = await getUserRanking(req); + + return NextResponse.json(data); +} diff --git a/src/app/api/mypage/liked/route.ts b/src/app/api/mypage/liked/route.ts index 2299dcb4..bb88284d 100644 --- a/src/app/api/mypage/liked/route.ts +++ b/src/app/api/mypage/liked/route.ts @@ -3,7 +3,5 @@ import { NextResponse } from 'next/server'; export async function GET(req: Request) { const data = await getMyLikedPosts(req); - console.log(data); - return NextResponse.json(data); } diff --git a/src/app/api/stocks/info/ent/route.ts b/src/app/api/stocks/info/ent/route.ts new file mode 100644 index 00000000..1c822575 --- /dev/null +++ b/src/app/api/stocks/info/ent/route.ts @@ -0,0 +1,10 @@ +import { getEntInfo } from '@/app/service/getRequest'; +import { NextResponse } from 'next/server'; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const code = searchParams.get('code') || '005930'; + + const data = await getEntInfo(req, code); + return NextResponse.json(data); +} diff --git a/src/app/api/users/profile/route.ts b/src/app/api/users/profile/route.ts index 098add09..a00c0875 100644 --- a/src/app/api/users/profile/route.ts +++ b/src/app/api/users/profile/route.ts @@ -3,7 +3,5 @@ import { NextResponse } from 'next/server'; export async function GET(req: Request) { const data = await getUsersProfile(req); - console.log(data); - return NextResponse.json(data); } diff --git a/src/app/components/blog/detail/BlogComment.tsx b/src/app/components/blog/detail/BlogComment.tsx index 0c7f0357..7e5b8984 100644 --- a/src/app/components/blog/detail/BlogComment.tsx +++ b/src/app/components/blog/detail/BlogComment.tsx @@ -16,7 +16,6 @@ const BlogComment = ({ postId, currentUserId }: BlogCommentProps) => { try { const response = await callGet(`/api/comment?id=${postId}`); if (response.isSuccess) { - console.log('댓글 데이터 갱신:', response.result); setComments(response.result); } } catch (error) { diff --git a/src/app/components/blog/detail/BlogContent.tsx b/src/app/components/blog/detail/BlogContent.tsx index ae7efb96..77577ff1 100644 --- a/src/app/components/blog/detail/BlogContent.tsx +++ b/src/app/components/blog/detail/BlogContent.tsx @@ -25,7 +25,6 @@ const CustomLi = ({ node, ...props }: any) => ( const BlogContent = ({ content }: BlogContentProps) => { const sanitizedContent = content ? DOMPurify.sanitize(content) : ''; - console.log(content); return (
{ }, []); useEffect(() => { - console.log(postId, '받아온 아이디'); - console.log(currentUserId, '유저 아이디'); - const fetchData = async () => { try { const resData = await callGet(`/api/detail?id=${postId}`); @@ -42,7 +39,6 @@ const BlogDetailContainer = ({ postId }: PostDetailProps) => { tags: tagsArray, createdAt: formattedCreatedAt, }); - console.log(resData.result.content); } catch (error) { console.error('Error fetching post details:', error); } @@ -51,7 +47,6 @@ const BlogDetailContainer = ({ postId }: PostDetailProps) => { }, [postId]); if (!blogData) { - console.log(blogData); return (
{[0, 1, 2].map((index) => ( diff --git a/src/app/components/blog/detail/CommentInput.tsx b/src/app/components/blog/detail/CommentInput.tsx index 2dfeb539..923e48c1 100644 --- a/src/app/components/blog/detail/CommentInput.tsx +++ b/src/app/components/blog/detail/CommentInput.tsx @@ -23,7 +23,6 @@ const CommentInput = ({ parentCommentId, }); if (response.isSuccess) { - console.log('댓글 작성 성공:', response.result); setCommentInput(''); onAddComment(); } else { diff --git a/src/app/components/blog/main/all/BlogContainer.tsx b/src/app/components/blog/main/all/BlogContainer.tsx index 01d7c070..18bb800a 100644 --- a/src/app/components/blog/main/all/BlogContainer.tsx +++ b/src/app/components/blog/main/all/BlogContainer.tsx @@ -8,8 +8,8 @@ import { SALARY_RANGE_MAP, } from '@/app/constants/blog'; import { callGet } from '@/app/utils/callApi'; -import { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; +import { useEffect, useState } from 'react'; import FollowerFilter from '../following/FollowerFilter'; import RecommendFilter from '../recommend/RecommendFilter'; import BlogPost from './BlogPost'; diff --git a/src/app/components/blog/main/all/BlogPost.tsx b/src/app/components/blog/main/all/BlogPost.tsx index a8616ac4..797e1b8f 100644 --- a/src/app/components/blog/main/all/BlogPost.tsx +++ b/src/app/components/blog/main/all/BlogPost.tsx @@ -42,7 +42,7 @@ const BlogPost = ({ post }: BlogPostProps) => { >
{post.title} { if (inputValue.trim() !== '' && !tags.includes(inputValue.trim())) { const updatedTags = [...tags, inputValue.trim()]; - setTags(updatedTags); - console.log(tags); + setTags(updatedTags); // 여기서 EditBlogContainer로 전달된 setTags 함수를 사용합니다. setInputValue(''); } }; diff --git a/src/app/components/blog/post/markdown/MyEditor.tsx b/src/app/components/blog/post/markdown/MyEditor.tsx index b3995684..1dd0084a 100644 --- a/src/app/components/blog/post/markdown/MyEditor.tsx +++ b/src/app/components/blog/post/markdown/MyEditor.tsx @@ -19,7 +19,6 @@ const MyEditor = ({ setContent }: MyEditorProps) => { const handleImageUpload = async (blob: File, callback: Function) => { try { - console.log('fileName', blob.name); const response = await fetch( `/api/blog/images?bucketName=dev-blog&fileName=${blob.name}`, ); @@ -28,8 +27,6 @@ const MyEditor = ({ setContent }: MyEditorProps) => { } const resData = await response.json(); const presignedUrl = resData.result; - console.log(presignedUrl); - await fetch(presignedUrl, { method: 'PUT', headers: { @@ -46,7 +43,6 @@ const MyEditor = ({ setContent }: MyEditorProps) => { `Failed to fetch image: ${res.status} ${res.statusText}`, ); } - console.log('GET Request Successful', res); return res.blob(); }) .catch((error) => { diff --git a/src/app/components/common/ProfileDropdown.tsx b/src/app/components/common/ProfileDropdown.tsx index 7e6cabcf..e971abda 100644 --- a/src/app/components/common/ProfileDropdown.tsx +++ b/src/app/components/common/ProfileDropdown.tsx @@ -21,7 +21,7 @@ const ProfileDropdown = () => {
{LOGIN_TEXT[0]} diff --git a/src/app/components/common/layout/Header.tsx b/src/app/components/common/layout/Header.tsx index ea8bb14d..c11917f8 100644 --- a/src/app/components/common/layout/Header.tsx +++ b/src/app/components/common/layout/Header.tsx @@ -28,10 +28,14 @@ function Header() { path === '/auth/complete'; return ( -
+
{!isChecked && } -

{TITLE}

+

+ {TITLE} +

{HEADER_TEXT.map((text, i) => ( diff --git a/src/app/components/common/layout/TanstackProvider.tsx b/src/app/components/common/layout/TanstackProvider.tsx new file mode 100644 index 00000000..8ff2eac0 --- /dev/null +++ b/src/app/components/common/layout/TanstackProvider.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { ReactNode } from 'react'; + +export default function TanStackProvider({ + children, +}: { + children: ReactNode; +}) { + const queryClient = new QueryClient(); + + return ( + + {children} + {/* */} + + ); +} diff --git a/src/app/components/main/MainContainer.tsx b/src/app/components/main/MainContainer.tsx index 4ea6cc8f..32a4c657 100644 --- a/src/app/components/main/MainContainer.tsx +++ b/src/app/components/main/MainContainer.tsx @@ -31,12 +31,12 @@ const MainContainer = () => { - +
- - +
+
- +
); diff --git a/src/app/components/main/MainHeader.tsx b/src/app/components/main/MainHeader.tsx index 53e8606a..f0a154c7 100644 --- a/src/app/components/main/MainHeader.tsx +++ b/src/app/components/main/MainHeader.tsx @@ -1,7 +1,11 @@ -'use client'; - import { CATCH_PHRASE } from '@/app/constants/main'; -import { dela } from '../common/layout/Header'; +import { Dela_Gothic_One } from 'next/font/google'; + +export const dela = Dela_Gothic_One({ + subsets: ['latin'], + weight: '400', + display: 'swap', +}); const MainHeader = () => { return ( @@ -11,7 +15,7 @@ const MainHeader = () => { {CATCH_PHRASE[0]}
-
+
{CATCH_PHRASE[1]} diff --git a/src/app/components/main/right/RecommendPost.tsx b/src/app/components/main/right/RecommendPost.tsx index 85fd18c5..f18dafcb 100644 --- a/src/app/components/main/right/RecommendPost.tsx +++ b/src/app/components/main/right/RecommendPost.tsx @@ -2,6 +2,7 @@ import { likeSmall } from '@/app/constants/iconPath'; import { MAIN_CONTENTS_TITLE } from '@/app/constants/main'; +import { MockHashTag, MockTag, MockTitle } from '@/app/data/blogdata'; import { useUserStore } from '@/app/store/store'; import { callGet } from '@/app/utils/callApi'; import Image from 'next/image'; @@ -57,12 +58,12 @@ const RecommendPost = () => { >

- {data.tags} + {MockTag[i]}

-

{data.title}

+

{MockTitle[i]}

- {data.commonInterests.map((interest) => ( + {MockHashTag[i].map((interest) => (
{ thumbnail diff --git a/src/app/components/main/right/SimulateRank.tsx b/src/app/components/main/right/SimulateRank.tsx index 600a0658..09b0e37b 100644 --- a/src/app/components/main/right/SimulateRank.tsx +++ b/src/app/components/main/right/SimulateRank.tsx @@ -5,13 +5,25 @@ import { RANKING_COLOR, } from '@/app/constants/main'; import { MOOK_RANKINGS } from '@/app/data/main'; +import { callGet } from '@/app/utils/callApi'; import { getTodayDateBar } from '@/app/utils/date'; +import { formatCurrency } from '@/app/utils/formatNum'; import Image from 'next/image'; import Link from 'next/link'; +import { useEffect, useState } from 'react'; import Icons from '../../common/Icons'; const SimulateRank = () => { const today = getTodayDateBar(); + const [rank, setRank] = useState([]); + useEffect(() => { + const fetchPost = async () => { + const response = await callGet(`/api/main/userRank`); + response.isSuccess && setRank(response.result); + }; + fetchPost(); + }, []); + return (
@@ -19,18 +31,18 @@ const SimulateRank = () => {

{today}

- {MOOK_RANKINGS.map((ranker, i) => ( + {MOOK_RANKINGS.map((rankData, i) => (

{i + 1}

{ranker.title} {
-

{ranker.title}

+

{rankData.title}

{MAIN_LEFT_ETC[1]}

- 총 수익 : {ranker.profit}원 + 총 수익 : {formatCurrency(Math.floor(rankData.profit))}

diff --git a/src/app/components/mypage/MyPageContainer.tsx b/src/app/components/mypage/MyPageContainer.tsx index a59aa197..63236414 100644 --- a/src/app/components/mypage/MyPageContainer.tsx +++ b/src/app/components/mypage/MyPageContainer.tsx @@ -66,19 +66,13 @@ const MyPageContainer = () => {
profile - {/*
- -
*/}
{myData?.nickname}
diff --git a/src/app/components/mypage/MyPostCard.tsx b/src/app/components/mypage/MyPostCard.tsx index a254b101..367428cd 100644 --- a/src/app/components/mypage/MyPostCard.tsx +++ b/src/app/components/mypage/MyPostCard.tsx @@ -45,7 +45,9 @@ const MyPostCard = ({ mypost }: MyPostCardsProps) => { objectFit="cover" className="rounded-[10px]" src={ - mypost.imageUrls.length > 0 ? mypost.imageUrls[0] : '/images/3c.png' + mypost.imageUrls.length > 0 + ? mypost.imageUrls[0] + : '/images/thumbnail/stock3.png' } alt="thumbnail" /> diff --git a/src/app/components/mypage/myaccount/AccountContainer.tsx b/src/app/components/mypage/myaccount/AccountContainer.tsx index dd7f77e4..714c807b 100644 --- a/src/app/components/mypage/myaccount/AccountContainer.tsx +++ b/src/app/components/mypage/myaccount/AccountContainer.tsx @@ -73,8 +73,7 @@ function AccountContainer() { const handleProfileImageUpload = async (file: File) => { try { - console.log('Uploading File:', file.name); - + // Presigned URL 요청 const response = await fetch( `/api/blog/images?bucketName=dev-user&fileName=${file.name}`, ); @@ -85,7 +84,6 @@ function AccountContainer() { const resData = await response.json(); const presignedUrl = resData.result; - console.log('Presigned URL:', presignedUrl); const uploadResponse = await fetch(presignedUrl, { method: 'PUT', @@ -102,13 +100,13 @@ function AccountContainer() { const imageUrl = presignedUrl.split('?')[0]; console.log('Uploaded Image URL:', imageUrl); + // 이미지 URL 확인 (선택적 검증) const verifyResponse = await fetch(imageUrl); if (!verifyResponse.ok) { throw new Error('이미지 확인 실패'); } - console.log('Image Verified Successfully'); - + // formData에 이미지 URL 업데이트 updateFormData('profileImageUrl', imageUrl); toast.success('프로필 이미지가 성공적으로 업로드되었습니다.'); } catch (error) { diff --git a/src/app/components/news/NewList.tsx b/src/app/components/news/NewList.tsx index c9fbc41e..a7dfb07d 100644 --- a/src/app/components/news/NewList.tsx +++ b/src/app/components/news/NewList.tsx @@ -67,17 +67,17 @@ const NewsList = ({ newsData, keyword, dateRange }: NewsListProps) => { variants={fadeInVariants} className="flex justify-between items-start pb-2 text-sm text-gray-500" > - + {formatDate(news.date)} - {truncateString(news.title, 40)} + {truncateString(news.title, 36)} handleNewsClick(news.url)} > - {truncateString(news.content, 46)} + {truncateString(news.content, 42)} { const [data, setData] = useState([]); const { stockCode } = useStockStore(); - // const addFutureData = () => { - // const startDate = new Date(2024, 11, 9); - - // const futureData = []; - // for (let i = 1; i <= 10; i++) { - // const futureDate = new Date(startDate); - // futureDate.setDate(startDate.getDate() + i); - - // const formattedDate = `${futureDate.getFullYear()}${String( - // futureDate.getMonth() + 1, - // ).padStart(2, '0')}${String(futureDate.getDate()).padStart(2, '0')}`; - - // futureData.push({ - // stck_bsop_date: String(formattedDate), - // stck_oprc: String(Math.floor(Math.random() * 1000 + 24000)), // 랜덤 데이터 예시 - // stck_hgpr: String(Math.floor(Math.random() * 1000 + 25000)), - // stck_lwpr: String(Math.floor(Math.random() * 1000 + 23900)), - // stck_clpr: String(Math.floor(Math.random() * 1000 + 24000)), - // acml_vol: Math.floor(Math.random() * 1000), - // }); - // } - // console.log(futureData,'생성된 추가 데이터'); - - // setData((prevData) => [...prevData, ...futureData]); // 기존 데이터에 추가 - // }; - - // console.log(data, '합쳐진 데이터'); - const fetchData = useCallback(async () => { const reqBodyTemplate = { marketDivCode: 'J', @@ -105,17 +78,12 @@ const PrChartContainer = () => { return (
- {/*
); }; diff --git a/src/app/components/prediction/predictionSide/PredictIndicator.tsx b/src/app/components/prediction/predictionSide/PredictIndicator.tsx index 412749ff..6977cc4e 100644 --- a/src/app/components/prediction/predictionSide/PredictIndicator.tsx +++ b/src/app/components/prediction/predictionSide/PredictIndicator.tsx @@ -1,6 +1,5 @@ 'use client'; -import { infoIcon } from '@/app/constants/iconPath'; import { PREDICTION_INDICATION_SORT, PREDICTION_SIDEBAR_TEXT, @@ -9,7 +8,6 @@ import useStockStore from '@/app/store/store'; import { callPost } from '@/app/utils/callApi'; import { useState } from 'react'; import Button from '../../common/Button'; -import Icons from '../../common/Icons'; interface PredictionData { time: number; @@ -55,9 +53,7 @@ const PredictIndicator: React.FC = ({ window.alert('예측 방법과 종목을 선택해 주세요.'); return; } - setLoading(true); - const reqBody = { dateFrom: '20240101', dateTo: '20241231', @@ -71,8 +67,6 @@ const PredictIndicator: React.FC = ({ ); if (response?.result) { - console.log('API 응답 데이터:', response.result); - const newChartData = response.result.dates.map( (date: string, index: number) => ({ time: new Date(date).getTime() / 1000, @@ -97,7 +91,6 @@ const PredictIndicator: React.FC = ({ chartData: newChartData, }); } else { - console.error('API 응답 에러:', response); window.alert('예측 데이터를 가져오지 못했습니다.'); } } catch (error) { @@ -124,35 +117,12 @@ const PredictIndicator: React.FC = ({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > -

각기 다른 예측 결과를 확인해보세요

- {isTooltipVisible && ( -
-

- {PREDICTION_SIDEBAR_TEXT[3]} - - {PREDICTION_SIDEBAR_TEXT[4]} - -

-

- {PREDICTION_SIDEBAR_TEXT[5]} - - {PREDICTION_SIDEBAR_TEXT[6]} - -

-

- {PREDICTION_SIDEBAR_TEXT[7]} - - {PREDICTION_SIDEBAR_TEXT[8]} - -

-
- )}
-
+
{PREDICTION_INDICATION_SORT.map((method, index) => (
-

+

{PREDICTION_SIDEBAR_TEXT[index + 3]}

))}
-
+
diff --git a/src/app/components/prediction/predictionSide/alert/SetWebhook.tsx b/src/app/components/prediction/predictionSide/alert/SetWebhook.tsx index e29f8d48..ddf21899 100644 --- a/src/app/components/prediction/predictionSide/alert/SetWebhook.tsx +++ b/src/app/components/prediction/predictionSide/alert/SetWebhook.tsx @@ -29,7 +29,7 @@ const SetWebhook: React.FC = ({ const handleRegisterAlert = async () => { const standard = selectedIndex === 0 ? 'PR' : ALARM_STANDARD[selectedIndex]; - if (!stockCode || !target || !selectedIndex) { + if (!stockCode || !target) { window.alert('모든 필드를 입력해주세요.'); return; } @@ -44,7 +44,6 @@ const SetWebhook: React.FC = ({ `/api/stocks/predictions/notification?operation=${standard}`, payload, ); - console.log(response, '요청완료 다음 값으로 요청함', payload); if (response.isSuccess) { window.alert('알림이 성공적으로 등록되었습니다.'); diff --git a/src/app/components/simulation/SimulationContainer.tsx b/src/app/components/simulation/SimulationContainer.tsx index b46bfdba..8a070619 100644 --- a/src/app/components/simulation/SimulationContainer.tsx +++ b/src/app/components/simulation/SimulationContainer.tsx @@ -9,7 +9,9 @@ const SimulationContainer = () => { const { selectedItem } = useSidebarStore(); const chartWidth = selectedItem === null ? 'w-[72%]' : 'w-[60%]'; return ( -
+
diff --git a/src/app/components/simulation/chart/ChartContainer.tsx b/src/app/components/simulation/chart/ChartContainer.tsx index fa9f1c5e..22f1f642 100644 --- a/src/app/components/simulation/chart/ChartContainer.tsx +++ b/src/app/components/simulation/chart/ChartContainer.tsx @@ -1,74 +1,44 @@ 'use client'; -import useStockStore from '@/app/store/store'; -import { isOpenTime } from '@/app/utils/date'; import { - fetchAdditionalData, - fetchDailyAdditional, - fetchInitialData, - fetchInitialDay, -} from '@/app/utils/fetchStockData'; -import { useEffect, useState } from 'react'; + useInvalidateStockData, + useStockData, +} from '@/app/hooks/useStockChart'; +import useStockStore, { useLiveDataStore } from '@/app/store/store'; +import { isOpenTime } from '@/app/utils/date'; +import { useState } from 'react'; import ChartEmpty from './ChartEmpty'; import DayChart from './DayChart'; import MinChart from './MinChart'; import WebSocketChart from './SocketChart'; const ChartContainer = () => { - const [mindata, setMinData] = useState([]); - const [dailyData, setDailyData] = useState([]); - const [liveData, setLiveData] = useState(null); + const { liveData } = useLiveDataStore(); const { stockCode } = useStockStore(); const [isLack, setIsLack] = useState(false); const [timeFrame, setTimeFrame] = useState(1); - const isDay = typeof timeFrame === 'string'; const isOpen = isOpenTime(); - useEffect(() => { - const fetchData = async () => { - if (!stockCode) return; - if (isDay) { - const initData = await fetchInitialDay(stockCode, timeFrame); - setDailyData(initData); - } else { - const initData = await fetchInitialData(stockCode); - setMinData(initData); - } - }; - fetchData(); - }, [stockCode, timeFrame]); + const { data, isLoading, fetchAdditionalData, isFetchingAdditionalData } = + useStockData(stockCode, timeFrame); - useEffect(() => { - const fetchMoreData = async () => { - if (timeFrame === '년') return; - if (!isLack || !stockCode) return; + const { invalidateMinData } = useInvalidateStockData(); - if (!isDay) { - const additionalData = await fetchAdditionalData(mindata, stockCode); - setMinData((prev) => [...prev, ...additionalData]); - } else { - const additionalData = await fetchDailyAdditional( - dailyData, - stockCode, - timeFrame, - ); - setDailyData((prev) => [...prev, ...additionalData]); - } - - setIsLack(false); - }; - - fetchMoreData(); - }, [isLack]); + const handleRefresh = () => { + fetchAdditionalData(); + if (stockCode) { + invalidateMinData(stockCode); + } + }; return ( -
+
{!stockCode || stockCode === 'null' ? ( ) : isDay ? ( { /> ) : ( { liveData={liveData} /> )} - {isOpen && !isDay && ( - - )} + {isOpen && !isDay && }
); }; diff --git a/src/app/components/simulation/chart/ChartEmpty.tsx b/src/app/components/simulation/chart/ChartEmpty.tsx index 0fbe4939..f83b3f71 100644 --- a/src/app/components/simulation/chart/ChartEmpty.tsx +++ b/src/app/components/simulation/chart/ChartEmpty.tsx @@ -4,7 +4,7 @@ import Icons from '../../common/Icons'; const ChartEmpty = () => { return ( -
+

{CHART_EMPTY_TEXT}

diff --git a/src/app/components/simulation/chart/ChartTypeDropdown.tsx b/src/app/components/simulation/chart/ChartTypeDropdown.tsx index b1b2f474..20d395c8 100644 --- a/src/app/components/simulation/chart/ChartTypeDropdown.tsx +++ b/src/app/components/simulation/chart/ChartTypeDropdown.tsx @@ -23,10 +23,10 @@ const ChartTypeDropdown = ({ option, setOption }: ChartTypeDropdownProps) => { return (
-
+
{typeof option !== 'number' ? 1 : option}분
@@ -50,7 +50,7 @@ const ChartTypeDropdown = ({ option, setOption }: ChartTypeDropdownProps) => {
handleSelectValue(dayType)} - className={`flex-center w-7 h-7 rounded hover:bg-gray-5 text-black-1 ${option === dayType && 'bg-gray-5'}`} + className={`flex-center w-7 h-7 rounded hover:bg-gray-5 text-black-1 dark:bg-black-1 dark:text-gray-2 ${option === dayType && 'bg-gray-5'}`} > {dayType}
diff --git a/src/app/components/simulation/chart/DayChart.tsx b/src/app/components/simulation/chart/DayChart.tsx index 5bcf5309..7145988b 100644 --- a/src/app/components/simulation/chart/DayChart.tsx +++ b/src/app/components/simulation/chart/DayChart.tsx @@ -1,6 +1,6 @@ import { applyOptions } from '@/app/utils/chart'; import { convertToUnixTimesDay } from '@/app/utils/date'; -import { createChart, Time } from 'lightweight-charts'; +import { ColorType, createChart, Time } from 'lightweight-charts'; import { Dispatch, SetStateAction, useLayoutEffect, useRef } from 'react'; import ChartTypeDropdown from './ChartTypeDropdown'; @@ -49,11 +49,26 @@ const DayChart = ({ useLayoutEffect(() => { if (!chartContainerRef.current || data.length === 0) return; + const isDarkMode = document.documentElement.classList.contains('dark'); - // 차트 초기 생성 const chart = createChart(chartContainerRef.current, { width: chartContainerRef.current.clientWidth, height: chartContainerRef.current.clientHeight, + layout: { + background: { + type: ColorType.Solid, + color: isDarkMode ? '#1E1E1E' : '#FFFFFF', + }, + textColor: isDarkMode ? '#CBCACA' : '#000000', + }, + grid: { + vertLines: { + color: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', // 다크모드: 밝은 세로선, 라이트모드: 어두운 세로선 + }, + horzLines: { + color: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', // 다크모드: 밝은 가로선, 라이트모드: 어두운 가로선 + }, + }, timeScale: { timeVisible: true, secondsVisible: false, diff --git a/src/app/components/simulation/chart/MinChart.tsx b/src/app/components/simulation/chart/MinChart.tsx index b0c14d9f..8b52caa5 100644 --- a/src/app/components/simulation/chart/MinChart.tsx +++ b/src/app/components/simulation/chart/MinChart.tsx @@ -1,12 +1,13 @@ import { applyOptions, groupDataByInterval } from '@/app/utils/chart'; import { convertToUnixTimestamp } from '@/app/utils/date'; -import { createChart, Time } from 'lightweight-charts'; +import { ColorType, createChart, Time } from 'lightweight-charts'; import { Dispatch, SetStateAction, useEffect, useLayoutEffect, useRef, + useState, } from 'react'; import ChartTypeDropdown from './ChartTypeDropdown'; @@ -31,6 +32,19 @@ const MinChart = ({ const chartRef = useRef | null>(null); const candleSeriesRef = useRef(null); // 캔들 데이터 참조 const volumeSeriesRef = useRef(null); // 거래량 데이터 참조 + const [isDarkMode, setIsDarkMode] = useState( + window.matchMedia('(prefers-color-scheme: dark)').matches, + ); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = (e: MediaQueryListEvent) => { + setIsDarkMode(e.matches); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); const transformCandle = (arr: MinPriceTypes[]) => { return arr @@ -71,10 +85,24 @@ const MinChart = ({ useLayoutEffect(() => { if (!chartContainerRef.current || data.length === 0) return; - const chart = createChart(chartContainerRef.current, { width: chartContainerRef.current.clientWidth, height: chartContainerRef.current.clientHeight, + layout: { + background: { + type: ColorType.Solid, + color: isDarkMode ? '#1E1E1E' : '#FFFFFF', + }, + textColor: isDarkMode ? '#CBCACA' : '#000000', + }, + grid: { + vertLines: { + color: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', // 다크모드: 밝은 세로선, 라이트모드: 어두운 세로선 + }, + horzLines: { + color: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', // 다크모드: 밝은 가로선, 라이트모드: 어두운 가로선 + }, + }, timeScale: { timeVisible: true, secondsVisible: false, @@ -86,12 +114,12 @@ const MinChart = ({ chartRef.current = chart; const candleSeries = chart.addCandlestickSeries({ - upColor: '#0065D1', - downColor: '#F12C2C', - borderUpColor: '#0065D1', - borderDownColor: '#F12C2C', - wickUpColor: '#0065D1', - wickDownColor: '#F12C2C', + upColor: isDarkMode ? '#3363CB' : '#0065D1', + downColor: isDarkMode ? '#DB3D2A' : '#F12C2C', + borderUpColor: isDarkMode ? '#3363CB' : '#0065D1', + borderDownColor: isDarkMode ? '#DB3D2A' : '#F12C2C', + wickUpColor: isDarkMode ? '#3363CB' : '#0065D1', + wickDownColor: isDarkMode ? '#DB3D2A' : '#F12C2C', priceFormat: { type: 'custom', minMove: 1, diff --git a/src/app/components/simulation/chart/SocketChart.tsx b/src/app/components/simulation/chart/SocketChart.tsx index cfca27da..c877a1a5 100644 --- a/src/app/components/simulation/chart/SocketChart.tsx +++ b/src/app/components/simulation/chart/SocketChart.tsx @@ -1,20 +1,21 @@ +import { useLiveDataStore } from '@/app/store/store'; import { callPost } from '@/app/utils/callApi'; import { extractDateTimeAndPrice } from '@/app/utils/formatStock'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; interface WebSocketChartProps { - setLiveData: Dispatch>; stockCode: string; } -const WebSocketChart = ({ stockCode, setLiveData }: WebSocketChartProps) => { +const WebSocketChart = ({ stockCode }: WebSocketChartProps) => { const [isConnected, setIsConnected] = useState(false); const [socketError, setSocketError] = useState(null); + const { setLiveData } = useLiveDataStore(); useEffect(() => { if (!stockCode) return; - let socket: WebSocket; + let socket: WebSocket | null = null; let lastUpdateTime = 0; const connectWebSocket = async () => { @@ -40,7 +41,7 @@ const WebSocketChart = ({ stockCode, setLiveData }: WebSocketChartProps) => { }, }, }; - socket.send(JSON.stringify(requestData)); + socket?.send(JSON.stringify(requestData)); }; socket.onmessage = (event) => { @@ -63,7 +64,6 @@ const WebSocketChart = ({ stockCode, setLiveData }: WebSocketChartProps) => { }; socket.onclose = () => { - console.log('WebSocket 연결 종료'); setIsConnected(false); }; } catch (error) { @@ -77,11 +77,13 @@ const WebSocketChart = ({ stockCode, setLiveData }: WebSocketChartProps) => { return () => { if (socket) { socket.close(); + socket = null; } }; }, [stockCode]); + if (socketError) { - return
WebSocket 에러: {socketError}
; + return
; } return
; diff --git a/src/app/components/simulation/chart/stockInfo/EnterpriseInfo.tsx b/src/app/components/simulation/chart/stockInfo/EnterpriseInfo.tsx index 9471c4b4..dc60a224 100644 --- a/src/app/components/simulation/chart/stockInfo/EnterpriseInfo.tsx +++ b/src/app/components/simulation/chart/stockInfo/EnterpriseInfo.tsx @@ -18,7 +18,6 @@ const EnterpriseInfo = () => { useEffect(() => { getStockDetail(); }, [stockCode]); - console.log(corpInfo, '기업정보'); return (
diff --git a/src/app/components/simulation/chart/stockInfo/StockInfoContainer.tsx b/src/app/components/simulation/chart/stockInfo/StockInfoContainer.tsx index 247b5bc3..d5a761b2 100644 --- a/src/app/components/simulation/chart/stockInfo/StockInfoContainer.tsx +++ b/src/app/components/simulation/chart/stockInfo/StockInfoContainer.tsx @@ -9,12 +9,13 @@ const StockInfoContainer = () => { const { stockCode } = useStockStore(); return ( -
+
{STOCK_INFO_TITLE.map((title, i) => (
setInfoType(title)} - className={`w-[76px] h-8 text-black-0 flex-center cursor-pointer font-light ${infoType === title && 'bg-gray-5 font-medium rounded-lg'}`} + className={`w-[76px] h-8 text-black-0 dark:text-gray-3 flex-center cursor-pointer font-light ${infoType === title && 'bg-gray-5 dark:bg-black-1 dark:text-black-1 font-medium rounded-lg'}`} > {title}
diff --git a/src/app/components/simulation/chart/stockInfo/StockInfoEmpty.tsx b/src/app/components/simulation/chart/stockInfo/StockInfoEmpty.tsx index 5378dbd9..d78118d9 100644 --- a/src/app/components/simulation/chart/stockInfo/StockInfoEmpty.tsx +++ b/src/app/components/simulation/chart/stockInfo/StockInfoEmpty.tsx @@ -4,7 +4,7 @@ import { CHART_EMPTY_TEXT } from '@/app/constants/prediction'; const StockInfoEmpty = () => { return ( -
+

{CHART_EMPTY_TEXT}

diff --git a/src/app/components/simulation/chart/stockInfo/entValue/EntValue.tsx b/src/app/components/simulation/chart/stockInfo/entValue/EntValue.tsx index 87b0f76e..83e66f3b 100644 --- a/src/app/components/simulation/chart/stockInfo/entValue/EntValue.tsx +++ b/src/app/components/simulation/chart/stockInfo/entValue/EntValue.tsx @@ -6,26 +6,32 @@ import { STOCK_INFO_TEXT, } from '@/app/constants/simulation'; import useStockStore from '@/app/store/store'; -import { formatCurrencyNoUnit } from '@/app/utils/formatNum'; -import { formatStockData } from '@/app/utils/formatStock'; -import { plusUnitforEnt } from '@/app/utils/truncate'; +import { callGet } from '@/app/utils/callApi'; +import { + formatCurrencyNoUnit, + formatEntValueUnit, +} from '@/app/utils/formatNum'; +import { formatEntValue } from '@/app/utils/formatStock'; import { useEffect, useRef, useState } from 'react'; import { useHover } from 'usehooks-ts'; import StockGuideModal from './StockGuideModal'; -interface EntValueProps { - data: StockDetailInfoTypes; -} - -const EntValue = ({ data }: EntValueProps) => { +const EntValue = () => { + const [entValue, setEntValue] = useState(null); const [hoverRefs, setHoverRefs] = useState<(HTMLDivElement | null)[]>([]); const { stockCode } = useStockStore(); useEffect(() => { - setHoverRefs((prev) => STOCK_INFO_TEXT.map(() => null)); + const getStockDetail = async () => { + const response = await callGet(`api/stocks/info/ent?code=${stockCode}`); + setEntValue(response.result); + }; + getStockDetail(); }, [stockCode]); - const StockInfoArr = data ? formatStockData(data) : []; + useEffect(() => { + setHoverRefs((prev) => STOCK_INFO_TEXT.map(() => null)); + }, []); return (
@@ -38,9 +44,12 @@ const EntValue = ({ data }: EntValueProps) => { className="w-[108px] h-[66px] py-1 px-2 flex flex-col gap-y-2" key={info} > -
-

{info}

-
+
+

{info}

+
{isHover && ( {

- {formatCurrencyNoUnit(Number(StockInfoArr[i]))} - {plusUnitforEnt(i)} + {entValue && + formatCurrencyNoUnit( + Math.floor(Number(formatEntValue(entValue)[i])), + )} + {formatEntValueUnit(i)}

diff --git a/src/app/components/simulation/chart/stockInfo/entValue/StockDetail.tsx b/src/app/components/simulation/chart/stockInfo/entValue/StockDetail.tsx index cb9e15f4..52793e0e 100644 --- a/src/app/components/simulation/chart/stockInfo/entValue/StockDetail.tsx +++ b/src/app/components/simulation/chart/stockInfo/entValue/StockDetail.tsx @@ -4,7 +4,6 @@ import { STOCK_INFO_TEXT, STOCK_INFO_TOOLTIP, } from '@/app/constants/simulation'; -import useStockStore from '@/app/store/store'; import { formatCurrencyNoUnit } from '@/app/utils/formatNum'; import { formatStockData } from '@/app/utils/formatStock'; import { plusUnit } from '@/app/utils/truncate'; @@ -18,11 +17,10 @@ interface StockDetailProps { const StockDetail = ({ data }: StockDetailProps) => { const [hoverRefs, setHoverRefs] = useState<(HTMLDivElement | null)[]>([]); - const { stockCode } = useStockStore(); useEffect(() => { setHoverRefs((prev) => STOCK_INFO_TEXT.map(() => null)); - }, [stockCode]); + }, []); const StockInfoArr = data ? formatStockData(data) : []; @@ -37,8 +35,8 @@ const StockDetail = ({ data }: StockDetailProps) => { className="w-[108px] h-[66px] py-1 px-2 flex flex-col gap-y-2" key={info} > -
-

{info}

+
+

{info}

{isHover && ( diff --git a/src/app/components/simulation/chart/stockInfo/entValue/StockInfo.tsx b/src/app/components/simulation/chart/stockInfo/entValue/StockInfo.tsx index 5a473e5e..d75782e3 100644 --- a/src/app/components/simulation/chart/stockInfo/entValue/StockInfo.tsx +++ b/src/app/components/simulation/chart/stockInfo/entValue/StockInfo.tsx @@ -1,6 +1,7 @@ import { STOCKINFO_TITLE } from '@/app/constants/simulation'; import useStockStore from '@/app/store/store'; import { callGet } from '@/app/utils/callApi'; +import { getTodayDateBar } from '@/app/utils/date'; import { motion } from 'framer-motion'; import { useEffect, useState } from 'react'; import EntValue from './EntValue'; @@ -14,12 +15,11 @@ const StockInfo = () => { useEffect(() => { const getStockDetail = async () => { - // const date = getTodayDateBar(); + const date = getTodayDateBar(); const response = await callGet( - `api/stocks/info?code=${stockCode}&date=${'2024-12-11'}`, + `api/stocks/info?code=${stockCode}&date=${date}`, ); setStockInfo(response.result); - console.log(response, '종목정보'); }; getStockDetail(); @@ -48,7 +48,7 @@ const StockInfo = () => {
{infoType === '종목정보' ? stockInfo && - : stockInfo && } + : stockInfo && }
diff --git a/src/app/components/simulation/chart/stockInfo/entValue/TradeRecord.tsx b/src/app/components/simulation/chart/stockInfo/entValue/TradeRecord.tsx index 4bbb7a6a..d80ef4b9 100644 --- a/src/app/components/simulation/chart/stockInfo/entValue/TradeRecord.tsx +++ b/src/app/components/simulation/chart/stockInfo/entValue/TradeRecord.tsx @@ -26,19 +26,24 @@ const TradeRecord = () => { }, [stockCode]); return ( -
+

거래현황

{STOCK_TRADE_TEXT[0]}

- {STOCK_TRADE_TEXT.slice(1, 3).map((text, i) => ( -

{text}

+ {STOCK_TRADE_TEXT.slice(1, 3).map((text) => ( +

+ {text} +

))}
{record?.output2.map((data) => ( -
+

{data.tradingDate}

{formatNumberCommas(data.dailyBuyVolume)} diff --git a/src/app/components/simulation/search/PreopenSearchInfo.tsx b/src/app/components/simulation/search/PreopenSearchInfo.tsx index 632f7e73..c52238f5 100644 --- a/src/app/components/simulation/search/PreopenSearchInfo.tsx +++ b/src/app/components/simulation/search/PreopenSearchInfo.tsx @@ -1,6 +1,9 @@ import { likeSmall, noneStockSearch } from '@/app/constants/iconPath'; import { STOCK_SEARCH_EMPTY_TEXT } from '@/app/constants/prediction'; -import { callDelete, callPost } from '@/app/utils/callApi'; +import { + useAddInterestStock, + useDeleteInterestStock, +} from '@/app/hooks/useInterestStock'; import { valueColor } from '@/app/utils/qualify'; import Image from 'next/image'; import Icons from '../../common/Icons'; @@ -16,13 +19,15 @@ const PreopenSearchInfo = ({ stockCode, getStockInfo, }: PreopenSearchInfoProps) => { - const interestStock = async () => { - await callPost(`api/stocks/interest?code=${stockInfo?.stockcode}`); - getStockInfo(stockInfo?.stockcode || ''); - }; + const { mutate: addInterestStock } = useAddInterestStock(); + const { mutate: removeInterestStock } = useDeleteInterestStock(); - const deleteInterest = async () => { - await callDelete(`api/stocks/interest?id=${stockInfo?.isInterested}`); + const handleInterestClick = () => { + if (stockInfo?.isInterested) { + removeInterestStock(stockInfo.isInterested); + } else { + addInterestStock(stockInfo?.stockcode || ''); + } getStockInfo(stockInfo?.stockcode || ''); }; @@ -61,7 +66,7 @@ const PreopenSearchInfo = ({

-
+
코스피

{stockInfo.date.slice(5)} 장 종료

diff --git a/src/app/components/simulation/search/SearchInfo.tsx b/src/app/components/simulation/search/SearchInfo.tsx index 2a06bb00..4964f7a8 100644 --- a/src/app/components/simulation/search/SearchInfo.tsx +++ b/src/app/components/simulation/search/SearchInfo.tsx @@ -1,6 +1,11 @@ import { likeSmall, noneStockSearch } from '@/app/constants/iconPath'; import { STOCK_SEARCH_EMPTY_TEXT } from '@/app/constants/prediction'; -import { callDelete, callPost } from '@/app/utils/callApi'; +import { + useAddInterestStock, + useDeleteInterestStock, +} from '@/app/hooks/useInterestStock'; +import { useLiveDataStore } from '@/app/store/store'; +import { valueColor } from '@/app/utils/qualify'; import Image from 'next/image'; import Icons from '../../common/Icons'; @@ -15,18 +20,21 @@ const SearchInfo = ({ stockCode, getStockInfo, }: SearchInfoProps) => { - const interestStock = async () => { - await callPost(`api/stocks/interest?code=${stockInfo?.stockcode}`); - getStockInfo(stockInfo?.stockcode || ''); - }; + const { mutate: addInterestStock } = useAddInterestStock(); + const { mutate: removeInterestStock } = useDeleteInterestStock(); + const { liveData } = useLiveDataStore(); - const deleteInterest = async () => { - await callDelete(`api/stocks/interest?id=${stockInfo?.isInterested}`); + const handleInterestClick = () => { + if (stockInfo?.isInterested) { + removeInterestStock(stockInfo.isInterested); + } else { + addInterestStock(stockInfo?.stockcode || ''); + } getStockInfo(stockInfo?.stockcode || ''); }; return stockInfo && stockCode !== 'null' ? ( -
+
{stockInfo.symbolImageUrl === null ? ( @@ -45,16 +53,38 @@ const SearchInfo = ({

{stockInfo.stockName}

{stockInfo.stockcode}

+
+

{liveData?.close || 0}원

+
+

+ +{Math.floor((liveData?.close || 0) - (liveData?.open || 0))} +

+

+ {`(+${Math.floor( + (((liveData?.close || 0) - (liveData?.open || 0)) / + (liveData?.open || 0)) * + 100, + )}%)`} +

+
+
+
+
+
+
+ 코스피
+
-
) : (
diff --git a/src/app/components/simulation/search/SimulateSearch.tsx b/src/app/components/simulation/search/SimulateSearch.tsx index 1b2d5cf6..4b0d985d 100644 --- a/src/app/components/simulation/search/SimulateSearch.tsx +++ b/src/app/components/simulation/search/SimulateSearch.tsx @@ -2,7 +2,7 @@ import useStockCodeStore from '@/app/store/store'; import { callGet } from '@/app/utils/callApi'; -import { isOpenTime } from '@/app/utils/date'; +import { getTodayDateBar, isOpenTime } from '@/app/utils/date'; import { useEffect, useState } from 'react'; import PreopenSearchInfo from './PreopenSearchInfo'; import SearchInfo from './SearchInfo'; @@ -11,11 +11,11 @@ import StockSearchBar from './StockSearchBar'; const SimulateSearch = () => { const [stockInfo, setStockInfo] = useState(null); const { setStockCode, stockCode } = useStockCodeStore(); - + const today = getTodayDateBar(); const apiURL = (code: string) => { return isOpenTime() ? `api/stocks?code=${code}` - : `api/stocks/offHour?code=${code}&date=${'2024-12-04'}`; + : `api/stocks/offHour?code=${code}&date=${today}`; }; const getStockInfo = async (code: string) => { diff --git a/src/app/components/simulation/search/StockSearchBar.tsx b/src/app/components/simulation/search/StockSearchBar.tsx index 3828e417..6ed753b5 100644 --- a/src/app/components/simulation/search/StockSearchBar.tsx +++ b/src/app/components/simulation/search/StockSearchBar.tsx @@ -65,10 +65,10 @@ const StockSearchBar = ({ getStockInfo }: StockSearchBarProps) => { onClick={() => searchKeyword(searchText)} /> {autoComplete.length !== 0 && ( -
+
{autoComplete.map((stockInfo, i) => (
setSelectedIndex(i)} onClick={() => searchKeyword(stockInfo.stockcode)} diff --git a/src/app/components/simulation/side/DownSideBar.tsx b/src/app/components/simulation/side/DownSideBar.tsx index 996bdbb3..2a7591c9 100644 --- a/src/app/components/simulation/side/DownSideBar.tsx +++ b/src/app/components/simulation/side/DownSideBar.tsx @@ -18,10 +18,10 @@ export default function DownSideBar() { cssEase: 'linear', }; return ( -
+
{MARKET_CONDITIONS.map((data, index) => ( { const { selectedItem, setSelectedItem } = useSidebarStore(); return ( -
+
{SIDE_NAV_TYPES.map((type, i) => (
{ const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); - const [orderType, setOrderType] = useState(''); + const [orderType, setOrderType] = useState('매일'); const [orderCnt, setOrderSnt] = useState(''); const { stockName, stockCode } = useStockStore(); + const [testResult, setTestResult] = useState(null); + const [isFinish, setIsFinish] = useState(false); + + const isQualified = + orderCnt !== '0' && startDate !== '' && endDate !== '' && stockName; const handleStartDateChange = (e: React.ChangeEvent) => { const selectedDate = e.target.value; @@ -40,22 +47,32 @@ const BackTest = () => { const backReq = { corpName: stockName, + startDate, endDate, periodType: ORDER_TYPE_MAP[orderType], quantity: Number(orderCnt), - startDate, stockcode: stockCode, }; const postBackTest = async () => { const res = await callPost('/api/stocks/backtest', backReq); - console.log(res, '응답'); + setTestResult(res.result); + setIsFinish(true); + }; + + const resetTest = () => { + setStartDate(''); + setEndDate(''); + setOrderType('매일'); + setOrderSnt('0'); + setIsFinish(false); + setTestResult(null); }; return ( -
+
{SIDE_NAV_TYPES[2]} -
+

{BACKTEST_TEXT[0]}

{
setOrderType(type)} - className={`rounded-lg flex-center w-12 h-7 cursor-pointer ${orderType === type ? 'bg-main-1/90 text-white' : 'bg-white border-[1.5px] border-gray-2'}`} + className={`rounded-lg flex-center w-12 h-7 cursor-pointer ${orderType === type ? 'bg-main-1/90 text-white' : 'bg-white border-[1.5px] dark:bg-gray-1 border-gray-2 dark:border-gray-1 '}`} > {type}
@@ -114,13 +131,33 @@ const BackTest = () => {
+ ); +}; + +export default BackTestResult; diff --git a/src/app/components/simulation/side/interest/Interest.tsx b/src/app/components/simulation/side/interest/Interest.tsx index 1068e06d..75107790 100644 --- a/src/app/components/simulation/side/interest/Interest.tsx +++ b/src/app/components/simulation/side/interest/Interest.tsx @@ -1,49 +1,22 @@ import Icons from '@/app/components/common/Icons'; import { interestLike } from '@/app/constants/iconPath'; import { INTEREST_EMPTY, SIDE_NAV_TYPES } from '@/app/constants/simulation'; -import { callDelete, callGet, callPost } from '@/app/utils/callApi'; +import { + useDeleteInterestStock, + useInterestStocks, +} from '@/app/hooks/useInterestStock'; import { isProfit } from '@/app/utils/formatNum'; import { vrssSignColor } from '@/app/utils/qualify'; import Image from 'next/image'; -import { useEffect, useState } from 'react'; import EmptyGuide from '../EmptyGuide'; const Interest = () => { - const [stocks, setStocks] = useState([]); - const [stockPrices, setStockPrices] = useState([]); - const getStockInfo = async () => { - const response = await callGet(`api/stocks/interest`); - const interestDatas = response.result.content; - setStocks(interestDatas); - const prices = await Promise.all( - interestDatas.map(async (item: InterestedStockTypes) => { - const data = await callPost( - `/api/stocks/price/inquire?stockcode=${item.stockcode}`, - ); - const priceData = data.result[0]; - return { - stockCode: item.stockcode, - currentPrice: priceData.stck_prpr, - changeAmount: priceData.prdy_vrss, - changePercent: priceData.prdy_ctrt, - changeSign: priceData.prdy_vrss_sign, - }; - }), - ); - setStockPrices(prices); - }; - - const deleteInterest = async (id: string) => { - const response = await callDelete(`api/stocks/interest?id=${id}`); - getStockInfo(); - }; - - useEffect(() => { - getStockInfo(); - }, []); + const { data } = useInterestStocks(); + const deleteInterestStock = useDeleteInterestStock(); + const { stocks, stockPrices } = data || { stocks: [], stockPrices: [] }; return ( -
+

{SIDE_NAV_TYPES[0]}

{stocks.length === 0 ? ( @@ -71,7 +44,9 @@ const Interest = () => { deleteInterest(stock.interestStockId)} + onClick={() => + deleteInterestStock.mutate(stock.interestStockId) + } />
{ + return ( +
+

{SIDE_NAV_TYPES[1]}

+ +
+
+
+
+
+
+
+
+ ); +}; + +export default LoadingPossesion; diff --git a/src/app/components/simulation/side/posession/Posession.tsx b/src/app/components/simulation/side/posession/Posession.tsx index f99bc040..e6311ddd 100644 --- a/src/app/components/simulation/side/posession/Posession.tsx +++ b/src/app/components/simulation/side/posession/Posession.tsx @@ -1,46 +1,32 @@ import { POSESSION_EMPTY, SIDE_NAV_TYPES } from '@/app/constants/simulation'; +import { useHoldStocks } from '@/app/hooks/usePosession'; import useStockStore from '@/app/store/store'; -import { callGet, callPost } from '@/app/utils/callApi'; import { formatNumberCommas, isProfit } from '@/app/utils/formatNum'; import { valueColor } from '@/app/utils/qualify'; -import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { useState } from 'react'; import HoldStockRecord from '../../trade/HoldStockRecord'; import EmptyGuide from '../EmptyGuide'; +import LoadingPossesion from './\bLoadingPossesion'; const Posession = () => { - const [holdStocks, setHoldStocks] = useState([]); const [isSelected, setIsSelected] = useState(false); - const [stockPrices, setStockPrices] = useState< - { stockCode: string; price: string }[] - >([]); const { setStockCode } = useStockStore(); - const getHoldStocks = async () => { - const response = await callGet( - `/api/stocks/hold?holdStatus=HOLDING&page=1&size=20&property=createdAt&direction=desc`, - ); - const stocks = response.result.content; - setHoldStocks(stocks); - const prices = await Promise.all( - stocks.map(async (item: HoldStockTypes) => { - const data = await callPost( - `/api/stocks/price/inquire?stockcode=${item.stockCode}`, - ); - return { stockCode: item.stockCode, price: data.result[0].stck_prpr }; - }), - ); - setStockPrices(prices); - }; + const { data, isLoading, isError, refetch } = useHoldStocks(); const handleCode = (code: string, name: string) => { setStockCode(code, name); setIsSelected(true); }; + if (isLoading) { + return ; + } - useEffect(() => { - getHoldStocks(); - }, []); - + const { stocks, stockPrices } = data as { + stocks: HoldStockTypes[]; + stockPrices: StockPriceTypes[]; + }; return (
{isSelected ? ( @@ -49,17 +35,24 @@ const Posession = () => {

{SIDE_NAV_TYPES[1]}

- {holdStocks.length === 0 ? ( + {stocks.length === 0 ? ( ) : ( stockPrices.length !== 0 && - holdStocks.map((stock, i) => ( + stocks.map((stock, i) => (
handleCode(stock.stockCode, stock.corpName)} key={stock.holdStockId} >
+ 종목썸네일

{stock.corpName}

diff --git a/src/app/components/simulation/side/trade/Trade.tsx b/src/app/components/simulation/side/trade/Trade.tsx index 586d7afa..2e2f288f 100644 --- a/src/app/components/simulation/side/trade/Trade.tsx +++ b/src/app/components/simulation/side/trade/Trade.tsx @@ -7,39 +7,30 @@ import { TRADE_PLACEHOLDER, TRADETYPE_MAP, } from '@/app/constants/simulation'; -import { callGet } from '@/app/utils/callApi'; +import { useRefreshTrade } from '@/app/hooks/useRefreshTrade'; import { formatNumberCommas } from '@/app/utils/formatNum'; import { tradeTypeColor } from '@/app/utils/qualify'; import { useEffect, useState } from 'react'; import EmptyGuide from '../EmptyGuide'; const Trade = () => { - const [record, setRecord] = useState([]); const [text, setText] = useState(''); - const [filteredRecord, setFilteredRecord] = useState( - [], - ); - - const getTradeRecord = async () => { - const response = await callGet( - `/api/stocks/trade/transactions?page=${1}&size=${20}&property=createdAt&direction=desc`, - ); - setRecord(response.result.content); - }; + const [filteredRecords, setFilteredRecords] = useState< + TransactionDataTypes[] + >([]); - useEffect(() => { - getTradeRecord(); - }, []); + const { data } = useRefreshTrade(); useEffect(() => { - const filtered = record.filter((data) => - data.investment.corpName.includes(text), + const records = data || []; + const filtered = records.filter((item) => + item.investment.corpName.includes(text), ); - setFilteredRecord(filtered); - }, [text, record]); + setFilteredRecords(filtered); + }, [data, text]); return ( -
+

{SIDE_NAV_TYPES[3]}

@@ -51,33 +42,40 @@ const Trade = () => { />
- {filteredRecord.length === 0 ? ( + {filteredRecords.length === 0 ? ( ) : ( - filteredRecord.map((data, i) => ( + filteredRecords.map((filteredData, i) => (
-

{11.12}

+

+ {filteredData?.createdAt && + new Date(filteredData.createdAt).toLocaleDateString()} +

- {data.investment.corpName} + {filteredData.investment.corpName}

-
-
-

{data.investment.quantity}주

-

- {TRADETYPE_MAP[data.investment.investType]} + {filteredData?.investment && ( +

+
+

+ {filteredData.investment.quantity}주 +

+

+ {TRADETYPE_MAP[filteredData.investment.investType]} +

+
+

+ {formatNumberCommas(filteredData.investment.totalPrice)}원

-

- {formatNumberCommas(data.investment.totalPrice)}원 -

-
+ )}
)) )} diff --git a/src/app/components/simulation/trade/AnalyzeBar.tsx b/src/app/components/simulation/trade/AnalyzeBar.tsx index b91dab44..e7dd3f59 100644 --- a/src/app/components/simulation/trade/AnalyzeBar.tsx +++ b/src/app/components/simulation/trade/AnalyzeBar.tsx @@ -7,14 +7,14 @@ import Icons from '../../common/Icons'; const AnalyzeBar = () => { return ( -
+

{ANALYZEBAR_TEXT[0]}

{ANALYZEBAR_TEXT[1]}

{ANALYZEBAR_TEXT[2]} diff --git a/src/app/components/simulation/trade/BuyCalculation.tsx b/src/app/components/simulation/trade/BuyCalculation.tsx index 6571ecaf..8c41a245 100644 --- a/src/app/components/simulation/trade/BuyCalculation.tsx +++ b/src/app/components/simulation/trade/BuyCalculation.tsx @@ -41,7 +41,7 @@ const BuyCalculation = ({ closeModal={closeModal} /> )} -
+

{TRADE_BUY_TEXT[6]}

{assets}

@@ -58,7 +58,11 @@ const BuyCalculation = ({ buttonText={TRADE_BUY_TEXT[9]} isDisabled={!isQualified} type="trade" - className={isQualified ? 'bg-red-1' : 'cursor-not-allowed bg-gray-1'} + className={ + isQualified + ? 'bg-red-1' + : 'cursor-not-allowed bg-gray-1 dark:bg-black-1' + } onClickHandler={openModal} />
diff --git a/src/app/components/simulation/trade/HoldStockRecord.tsx b/src/app/components/simulation/trade/HoldStockRecord.tsx index 2a4da84d..39705a8d 100644 --- a/src/app/components/simulation/trade/HoldStockRecord.tsx +++ b/src/app/components/simulation/trade/HoldStockRecord.tsx @@ -4,11 +4,10 @@ import { SIDE_NAV_TYPES, TRADETYPE_MAP, } from '@/app/constants/simulation'; -import useStockStore from '@/app/store/store'; -import { callGet } from '@/app/utils/callApi'; +import { useHoldStock } from '@/app/hooks/useHoldStock'; import { formatMD } from '@/app/utils/date'; import { formatNumberCommas } from '@/app/utils/formatNum'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { Dispatch, SetStateAction } from 'react'; import Icons from '../../common/Icons'; interface HoldStockRecordProps { @@ -16,23 +15,9 @@ interface HoldStockRecordProps { } const HoldStockRecord = ({ closeDetail }: HoldStockRecordProps) => { - const [record, setRecord] = useState([]); - const [stockInfo, setStockInfo] = useState(null); - const { stockCode } = useStockStore(); - - const getTradeRecord = async () => { - const response = await callGet( - `/api/stocks/trade/investment?code=${stockCode}&page=${1}&size=${20}&property=createdAt&direction=desc`, - ); - setRecord(response.result.content); - - const infoData = await callGet(`/api/stocks/hold/info?code=${stockCode}`); - setStockInfo(infoData.result); - }; - - useEffect(() => { - getTradeRecord(); - }, [stockCode]); + const { data } = useHoldStock(); + const holdStock = data?.holdStock || []; + const stockInfo = data?.stockInfo || null; return (
@@ -59,19 +44,19 @@ const HoldStockRecord = ({ closeDetail }: HoldStockRecordProps) => {
- {record.map((data) => ( + {holdStock.map((stockData) => (

- {formatMD(data.createdAt)} + {formatMD(stockData.createdAt)}

- {TRADETYPE_MAP[data.investType]} {data.quantity}주 + {TRADETYPE_MAP[stockData.investType]} {stockData.quantity}주

- 주당 {formatNumberCommas(data.price)} 원 + 주당 {formatNumberCommas(stockData.price)} 원

))} diff --git a/src/app/components/simulation/trade/SellCalculation.tsx b/src/app/components/simulation/trade/SellCalculation.tsx index 4058d19f..8f3b7074 100644 --- a/src/app/components/simulation/trade/SellCalculation.tsx +++ b/src/app/components/simulation/trade/SellCalculation.tsx @@ -23,7 +23,8 @@ const SellCalculation = ({ }: SellCalculationProps) => { const { isOpen, openModal, closeModal } = useModal(false); const { stockCode, stockName } = useStockStore(); - const isQualified = quantity > 0 && holdStock; + const isQualified = + quantity > 0 && holdStock && holdStock.totalHoldings >= quantity; const reqBody: TradeSellTypes = { holdStockId, diff --git a/src/app/components/simulation/trade/TradeBar.tsx b/src/app/components/simulation/trade/TradeBar.tsx index 42490612..f76fd880 100644 --- a/src/app/components/simulation/trade/TradeBar.tsx +++ b/src/app/components/simulation/trade/TradeBar.tsx @@ -6,6 +6,7 @@ import { TRADE_BUY_TEXT, TRADE_SELL_TEXT, } from '@/app/constants/simulation'; +import { useBalance } from '@/app/hooks/useBalance'; import useStockStore from '@/app/store/store'; import { callGet, callPost } from '@/app/utils/callApi'; import { useEffect, useState } from 'react'; @@ -15,11 +16,11 @@ import SellCalculation from './SellCalculation'; import TradeToggle from './TradeToggle'; const TradeBar = () => { + const { data: balance = 0 } = useBalance(); const [isBuy, setIsBuy] = useState(true); const [tradeCnt, setTradeCnt] = useState(''); const [holdStock, setHoldStock] = useState(null); const [amountType, setAmountType] = useState(null); - const [balance, setBalance] = useState(0); const [stockPrice, setStockPrice] = useState(0); const { stockCode } = useStockStore(); @@ -56,25 +57,19 @@ const TradeBar = () => { } }; - const getBalance = async () => { - const response = await callGet('api/stocks/trade/balance'); - setBalance(response.result.balance); - }; - useEffect(() => { const initCalc = async () => { - const response = await callGet('api/stocks/trade/balance'); const price = await callPost( `/api/stocks/price/inquire?stockcode=${stockCode}`, ); const limitResponse = await callGet( `/api/stocks/hold?holdStatus=HOLDING&page=1&size=20&property=createdAt&direction=desc`, ); + const stocks = limitResponse.result.content; const hold: HoldStockTypes = stocks.find( (item: HoldStockTypes) => item.stockCode === stockCode, ); - setStockPrice(price.result[0].stck_prpr); if (!isBuy && hold) { setStockPrice(Math.floor(hold.avgPrice)); @@ -85,18 +80,17 @@ const TradeBar = () => { } }; stockCode && initCalc(); - getBalance(); }, [stockCode, isBuy]); return ( -
+

{TRADE_BUY_TEXT[0]}

-
+

{isBuy ? TRADE_BUY_TEXT[3] : TRADE_SELL_TEXT[3]}

-
+
{stockPrice}
@@ -111,11 +105,11 @@ const TradeBar = () => { onChange={handleChange} />
-
+
{AMOUNT_TYPES.map((amount, i) => (
selectAmountType(amount, i)} > {amount} @@ -124,7 +118,7 @@ const TradeBar = () => {

{TRADE_BUY_TEXT[5]}

-
+
{Number(tradeCnt) * stockPrice || 0}원
diff --git a/src/app/components/simulation/trade/TradeContainer.tsx b/src/app/components/simulation/trade/TradeContainer.tsx index 2ec30dc9..f9eaeb1b 100644 --- a/src/app/components/simulation/trade/TradeContainer.tsx +++ b/src/app/components/simulation/trade/TradeContainer.tsx @@ -3,7 +3,7 @@ import TradeBar from './TradeBar'; const TradeContainer = () => { return ( -
+
diff --git a/src/app/components/simulation/trade/TradeToggle.tsx b/src/app/components/simulation/trade/TradeToggle.tsx index cc055a58..75606a51 100644 --- a/src/app/components/simulation/trade/TradeToggle.tsx +++ b/src/app/components/simulation/trade/TradeToggle.tsx @@ -8,13 +8,14 @@ interface TradeToggleProps { const TradeToggle = ({ isBuy, chngeTradeType }: TradeToggleProps) => { const textStyles = (type: TradeType) => { - if (isBuy) return type === '매수' ? 'text-red-1' : 'text-gray-1'; - return type === '매도' ? 'text-blue-1' : 'text-gray-1'; + if (isBuy) + return type === '매수' ? 'text-red-1' : 'text-gray-1 dark:text-gray-3'; + return type === '매도' ? 'text-blue-1' : 'text-gray-1 dark:text-gray-3'; }; return ( -
+
{
{type}
diff --git a/src/app/components/simulation/trade/modal/DoubleCheckModal.tsx b/src/app/components/simulation/trade/modal/DoubleCheckModal.tsx index fb034ad9..87611448 100644 --- a/src/app/components/simulation/trade/modal/DoubleCheckModal.tsx +++ b/src/app/components/simulation/trade/modal/DoubleCheckModal.tsx @@ -1,6 +1,10 @@ 'use client'; import Button from '@/app/components/common/Button'; +import { useInvalidateBalance } from '@/app/hooks/useBalance'; +import { useInvalidateHoldStock } from '@/app/hooks/useHoldStock'; +import { useRefreshHoldStocks } from '@/app/hooks/usePosession'; +import { useInvalidateTrade } from '@/app/hooks/useRefreshTrade'; import { callPost } from '@/app/utils/callApi'; import { motion } from 'motion/react'; import { useState } from 'react'; @@ -19,16 +23,27 @@ const DoubleCheckModal = ({ data, }: DoubleCheckModalProps) => { const [isDone, setIsDone] = useState(false); + const invalidateBalance = useInvalidateBalance(); + const invalidateHoldStock = useInvalidateHoldStock(); + const invalidateTrade = useInvalidateTrade(); + const invalidatePosession = useRefreshHoldStocks(); const colorBefore = tradeType === '매수' ? 'bg-red-2' : 'bg-blue-2'; const colorAfter = tradeType === '매수' ? 'hover:bg-red-1' : 'hover:bg-blue-1'; const buyStock = async () => { - const url = - tradeType === '매수' ? 'api/stocks/trade/buy' : 'api/stocks/trade/sell'; - const response = await callPost(url, data); - console.log('tradeType에 따른 응답', '요청데이터', data, response); - setIsDone(true); + try { + const url = + tradeType === '매수' ? 'api/stocks/trade/buy' : 'api/stocks/trade/sell'; + const response = await callPost(url, data); + invalidateBalance.mutate(); + invalidateHoldStock.mutate(); + invalidateTrade.mutate(); + invalidatePosession.mutate(); + setIsDone(true); + } catch (error) { + console.error('매수 실패:', error); + } }; return ( diff --git a/src/app/components/userpage/UserPostCard.tsx b/src/app/components/userpage/UserPostCard.tsx index b631c745..c39122a7 100644 --- a/src/app/components/userpage/UserPostCard.tsx +++ b/src/app/components/userpage/UserPostCard.tsx @@ -25,7 +25,7 @@ const UserPostCard = ({ userpost }: UserPostCardProps) => { src={ userpost.imageUrls.length > 0 ? userpost.imageUrls[0] - : '/images/3c.png' + : '/images/thumbnail/stock3.png' } alt="thumbnail" /> diff --git a/src/app/constants/simulation.ts b/src/app/constants/simulation.ts index 870c862f..7fb768d9 100644 --- a/src/app/constants/simulation.ts +++ b/src/app/constants/simulation.ts @@ -118,14 +118,7 @@ export const STOCK_INFO_TEXT = [ '상장 주식 수', ]; -export const ENT_VALUE_TEXT = [ - 'BPS', - 'PER', - 'PBR', - 'EPS', - '총 배당금', - '주당 배당금', -]; +export const ENT_VALUE_TEXT = ['BPS', 'PER', 'PBR', 'EPS', 'DIV', 'DPS']; export const STOCK_INFO_TOOLTIP = [ '주식이 장 시작 시 기록한 첫 번째 가격을 의미해요', @@ -144,8 +137,8 @@ export const ENT_VALUE_TOOLTIP = [ '현재 주가를 주당 순이익(EPS)으로 나눈 값을 의미해요', '현재 주가를 주당 순자산(BPS)으로 나눈 값을 의미해요', '기업이 일정 기간 동안 벌어들인 순이익을 발행 주식 수로 나눈 값을 의미해요', - '기업이 주주들에게 지급한 총 배당금액을 의미해요', - '기업이 주당 지급하는 배당금을 의미해요', + '현재 종목의 주가 대비 배당금의 비율을 의미해요', + '현재 기업이 주식 1주당 지급하는 원 단위 배당금을 의미해요', ]; export const FINANCIALINFO_TITLE = ['손익계산', '대차대조']; @@ -244,7 +237,12 @@ export const BACKTEST_TEXT = [ export const BACKTEST_BTN_TEXT = ['시작하기', '다시하기']; -export const ORDER_TYPE = ['매일', '매주', '매월', '매년']; +export const ORDER_TYPE: BackTestOrderTypes[] = [ + '매일', + '매주', + '매월', + '매년', +]; export const ORDER_TYPE_MAP: Record = { 매일: 'DAILY', diff --git a/src/app/constants/styles.ts b/src/app/constants/styles.ts index 0688531e..fc319dfd 100644 --- a/src/app/constants/styles.ts +++ b/src/app/constants/styles.ts @@ -29,17 +29,17 @@ export const INPUT_STYLE = { signUp: (className: string) => `w-full pl-4 h-10 rounded border border-gray-2 text-sm outline-none focus:border-main-1 ${className}`, simulation: (className: string) => - `w-80 pl-3 pr-12 h-10 rounded-xl border border-gray-4 font-light text-sm outline-none ${className}`, + `w-80 pl-3 pr-12 h-10 rounded-xl border border-gray-4 font-light text-sm outline-none dark:bg-black-0 dark:border-black-1 dark:text-gray-4 ${className}`, trade: (className: string) => - `w-[140px] h-[33px] px-3 py-2 rounded-md border border-gray-2 font-light text-black-1 text-sm ${className}`, + `w-[140px] h-[33px] px-3 py-2 rounded-md border border-gray-2 font-light text-black-1 dark:text-gray-3 dark:bg-black-0 dark:border-gray-1 text-sm ${className}`, search: (className: string) => `w-[150px] h-[24px] px-3 rounded-md border-none font-light text-black-0 text-base outline-none ${className}`, blogName: (className: string) => `w-[80%] pl-4 h-10 rounded-[10px] border border-gray-2 text-sm outline-none focus:border-main-1 ${className}`, record: (className: string) => - `w-full h-7 pl-2 pr-7 border border-gray-2 text-[11px] rounded-lg outline-none ${className}`, + `w-full h-7 pl-2 pr-7 border border-gray-2 text-[11px] dark:text-gray-1 rounded-lg outline-none ${className}`, calendar: (className: string) => - `w-[105px] h-[28px] px-2 py-2 rounded-md border-[1.5px] border-gray-2 text-black-1 text-[11px] ${className}`, + `w-[105px] h-[28px] px-2 py-2 rounded-md border-[1.5px] dark:bg-black-1 dark:text-gray-3 border-gray-2 text-black-1 text-[11px] ${className}`, orderCnt: (className: string) => - `w-full pr-16 pl-4 h-8 rounded border border-gray-2 text-gray-1 text-sm ${className}`, + `w-full dark:bg-gray-2 dark:border-gray-1 dark:text-black-1 pr-16 pl-4 h-8 rounded border border-gray-2 text-gray-1 text-sm ${className}`, } as const; diff --git a/src/app/data/blogdata.ts b/src/app/data/blogdata.ts index 3e9e5eef..02bdc6d4 100644 --- a/src/app/data/blogdata.ts +++ b/src/app/data/blogdata.ts @@ -1,127 +1,20 @@ -export const dummyPosts: BlogPost[] = [ - { - id: 1, - title: '2024년 주식시장 전망', - content: - '2024년 경제 흐름과 주식시장 분석, 주요 산업의 주식 투자 전략을 제시합니다.', - date: '2024.10.04', - imageUrl: '/images/3c.png', - category: '주식투자,마티니블루', - likes: 100, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 2, - title: '초보자를 위한 주식 투자 가이드', - content: - '주식 투자에 대한 기초 지식을 소개하고 초보자가 피해야 할 실수를 정리합니다.', - date: '2024.10.04', - imageUrl: '/images/3c.png', - category: '주식투자', - likes: 98, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 3, - title: '배당주 투자, 왜 주목해야 할까?', - content: - '배당주의 특징과 전략을 분석합니다. 안정적인 수익을 추구하는 투자자에게 적합한 종목을 추천합니다.', - date: '2024.10.04', - imageUrl: '/images/3c.png', - category: '배당주투자', - likes: 95, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 4, - title: '장기 투자 전략 가이드', - content: '장기적인 주식 투자의 중요성과 전략에 대해 설명합니다.', - date: '2024.10.03', - imageUrl: '/images/3c.png', - category: '장기투자', - likes: 85, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 5, - title: '초보자를 위한 주식 용어 사전', - content: '주식 투자 초보자가 알아야 할 주요 용어들을 정리했습니다.', - date: '2024.10.03', - imageUrl: '/images/3c.png', - category: '주식용어', - likes: 90, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 6, - title: '테크 주식 전망', - content: - '2024년 테크 주식의 전망과 주요 기술 기업들에 대한 분석을 제공합니다.', - date: '2024.10.02', - imageUrl: '/images/3c.png', - category: '테크주', - likes: 120, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 7, - title: '단기 매매 전략', - content: - '단기 주식 거래 전략을 소개하고, 주식 시장에서 빠른 이익을 얻는 방법을 설명합니다.', - date: '2024.10.01', - imageUrl: '/images/3c.png', - category: '단기매매', - likes: 70, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 8, - title: '초보자를 위한 재테크 기초', - content: - '주식 외에도 다양한 재테크 기초 지식과 초보자가 알아야 할 사항들을 정리했습니다.', - date: '2024.09.30', - imageUrl: '/images/3c.png', - category: '재테크', - likes: 110, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, - { - id: 9, - title: '국내외 시장 비교 분석', - content: - '국내 주식 시장과 해외 주식 시장을 비교 분석하여 투자 전략을 세웁니다.', - date: '2024.09.29', - imageUrl: '/images/3c.png', - category: '시장비교', - likes: 80, - author: '주식핑', - authorImageUrl: '/images/4c.png', - }, +export const MockTitle = [ + '2024년 내 재테크 발자취를 돌아보며..', + '과연, 항상 미국주식이 답일까?', + '꽁꽁 얼어붙은 한국장에서 살아남는 법', + '나는 청년적금 대신 주식을 택했다.', ]; -export const filterOptions: FilterOption[] = [ - { - label: '3천 이하', - value: 'below-3000', - }, - { - label: '21 ~ 30', - value: 'age-21-30', - }, - { - label: '해외주식', - value: 'overseas-stock', - }, - { - label: '부동산', - value: 'real-estate', - }, +export const MockTag = [ + '21~30 경제', + '해외주식, 8000이하', + '8000이하, 21~30', + '경제, 8000이하', +]; + +export const MockHashTag = [ + ['회고', '후회', '복기'], + ['나스닥', '엔비디아', '개미', '테슬라'], + ['코스피', '삼전', '영끌'], + ['적금', '재테크', '노후'], ]; diff --git a/src/app/hooks/useBalance.ts b/src/app/hooks/useBalance.ts new file mode 100644 index 00000000..dce21425 --- /dev/null +++ b/src/app/hooks/useBalance.ts @@ -0,0 +1,21 @@ +import { callGet } from '@/app/utils/callApi'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +const fetchBalance = async (): Promise => { + const response = await callGet('api/stocks/trade/balance'); + return response.result.balance; +}; + +export const useBalance = () => { + return useQuery({ queryKey: ['balance'], queryFn: fetchBalance }); +}; + +export const useInvalidateBalance = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: fetchBalance, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['balance'] }); + }, + }); +}; diff --git a/src/app/hooks/useHoldStock.ts b/src/app/hooks/useHoldStock.ts new file mode 100644 index 00000000..69c4bcdd --- /dev/null +++ b/src/app/hooks/useHoldStock.ts @@ -0,0 +1,44 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import useStockStore from '../store/store'; +import { callGet } from '../utils/callApi'; + +const fetchHoldStockAndInfo = async ( + stockCode: string, +): Promise<{ + holdStock: HoldStockRecordTypes[]; + stockInfo: HoldStockInfoTypes; +}> => { + const [holdStockResponse, stockInfoResponse] = await Promise.all([ + callGet( + `/api/stocks/trade/investment?code=${stockCode}&page=1&size=20&property=createdAt&direction=desc`, + ), + callGet(`/api/stocks/hold/info?code=${stockCode}`), + ]); + + return { + holdStock: holdStockResponse.result.content, + stockInfo: stockInfoResponse.result, + }; +}; + +export const useHoldStock = () => { + const { stockCode } = useStockStore(); + + return useQuery({ + queryKey: ['holdStock', stockCode], + queryFn: () => fetchHoldStockAndInfo(stockCode), + enabled: !!stockCode, + }); +}; + +export const useInvalidateHoldStock = () => { + const queryClient = useQueryClient(); + const { stockCode } = useStockStore(); + + return useMutation({ + mutationFn: () => fetchHoldStockAndInfo(stockCode), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['holdStock', stockCode] }); + }, + }); +}; diff --git a/src/app/hooks/useInterestStock.ts b/src/app/hooks/useInterestStock.ts new file mode 100644 index 00000000..fc9c89f4 --- /dev/null +++ b/src/app/hooks/useInterestStock.ts @@ -0,0 +1,54 @@ +import { callDelete, callGet, callPost } from '@/app/utils/callApi'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +export const useInterestStocks = () => { + return useQuery({ + queryKey: ['interestStocks'], + queryFn: async () => { + const response = await callGet(`api/stocks/interest`); + const interestDatas: InterestedStockTypes[] = response.result.content; + + const prices: InterestedPriceTypes[] = await Promise.all( + interestDatas.map(async (item: InterestedStockTypes) => { + const data = await callPost( + `/api/stocks/price/inquire?stockcode=${item.stockcode}`, + ); + const priceData = data.result[0]; + return { + stockcode: item.stockcode, + currentPrice: priceData.stck_prpr, + changeAmount: priceData.prdy_vrss, + changePercent: priceData.prdy_ctrt, + changeSign: priceData.prdy_vrss_sign, + }; + }), + ); + + return { stocks: interestDatas, stockPrices: prices }; + }, + }); +}; + +export const useDeleteInterestStock = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (id: string) => { + return callDelete(`api/stocks/interest?id=${id}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['interestStocks'] }); + }, + }); +}; + +export const useAddInterestStock = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (stockcode: string) => { + return callPost(`api/stocks/interest?code=${stockcode}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['interestStocks'] }); + }, + }); +}; diff --git a/src/app/hooks/usePosession.ts b/src/app/hooks/usePosession.ts new file mode 100644 index 00000000..a9be922b --- /dev/null +++ b/src/app/hooks/usePosession.ts @@ -0,0 +1,39 @@ +import { callGet, callPost } from '@/app/utils/callApi'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +const fetchHoldStocks = async () => { + const response = await callGet( + `/api/stocks/hold?holdStatus=HOLDING&page=1&size=20&property=createdAt&direction=desc`, + ); + const stocks = response.result.content; + const stockPrices = await Promise.all( + stocks.map(async (item: HoldStockTypes) => { + const data = await callPost( + `/api/stocks/price/inquire?stockcode=${item.stockCode}`, + ); + return { stockCode: item.stockCode, price: data.result[0].stck_prpr }; + }), + ); + return { stocks, stockPrices }; +}; + +export const useHoldStocks = () => { + return useQuery< + { stocks: HoldStockTypes[]; stockPrices: StockPriceTypes[] }, + Error + >({ + queryKey: ['holdStocks'], + queryFn: fetchHoldStocks, + }); +}; + +export const useRefreshHoldStocks = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: fetchHoldStocks, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['holdStocks'] }); + }, + }); +}; diff --git a/src/app/hooks/useRefreshTrade.ts b/src/app/hooks/useRefreshTrade.ts new file mode 100644 index 00000000..f2d779e3 --- /dev/null +++ b/src/app/hooks/useRefreshTrade.ts @@ -0,0 +1,26 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +const fetchTradeRecords = async (): Promise => { + const response = await fetch( + `/api/stocks/trade/transactions?page=1&size=30&property=createdAt&direction=desc`, + ); + const data = await response.json(); + return data.result.content as TransactionDataTypes[]; +}; + +export const useRefreshTrade = () => { + return useQuery({ + queryKey: ['tradeRecords'], + queryFn: () => fetchTradeRecords(), + }); +}; + +export const useInvalidateTrade = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: fetchTradeRecords, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['tradeRecords'] }); + }, + }); +}; diff --git a/src/app/hooks/useStockChart.ts b/src/app/hooks/useStockChart.ts new file mode 100644 index 00000000..04cc75bf --- /dev/null +++ b/src/app/hooks/useStockChart.ts @@ -0,0 +1,97 @@ +import { + fetchAdditionalData, + fetchDailyAdditional, + fetchInitialData, + fetchInitialDay, +} from '@/app/utils/fetchStockData'; +import { + queryOptions, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; + +// 쿼리 옵션 생성 함수 +const createStockDataQueryOptions = ( + stockCode: string | null, + timeFrame: number | string, + isDay: boolean, +) => + queryOptions({ + queryKey: isDay + ? ['dailyStockData', stockCode, timeFrame] + : ['minStockData', stockCode], + queryFn: async () => { + if (!stockCode) return []; + return isDay + ? fetchInitialDay(stockCode, timeFrame as string) + : fetchInitialData(stockCode); + }, + enabled: !!stockCode && (isDay ? true : typeof timeFrame === 'number'), + staleTime: 1000 * 60 * 5, // 5분 캐싱 + }); + +export const useStockData = ( + stockCode: string | null, + timeFrame: number | string, +) => { + const isDay = typeof timeFrame === 'string'; + const queryClient = useQueryClient(); + + const { data, isLoading, isError } = useQuery( + createStockDataQueryOptions(stockCode, timeFrame, isDay), + ); + + const additionalDataMutation = useMutation({ + mutationFn: async () => { + if (!stockCode) return []; + + const currentData = + queryClient.getQueryData( + isDay + ? ['dailyStockData', stockCode, timeFrame] + : ['minStockData', stockCode], + ) || []; + + return isDay + ? fetchDailyAdditional( + currentData as DailyPriceTypes[], + stockCode, + timeFrame, + ) + : fetchAdditionalData(currentData as MinPriceTypes[], stockCode); + }, + onSuccess: (newData) => { + queryClient.setQueryData( + isDay + ? ['dailyStockData', stockCode, timeFrame] + : ['minStockData', stockCode], + (oldData: any) => [...(oldData || []), ...newData], + ); + }, + }); + + return { + data: data || [], + isLoading, + isError, + fetchAdditionalData: additionalDataMutation.mutate, + isFetchingAdditionalData: additionalDataMutation.isPending, + additionalDataError: additionalDataMutation.error, + }; +}; + +export const useInvalidateStockData = () => { + const queryClient = useQueryClient(); + + return { + invalidateDailyData: (stockCode: string) => + queryClient.invalidateQueries({ + queryKey: ['dailyStockData', stockCode], + }), + invalidateMinData: (stockCode: string) => + queryClient.invalidateQueries({ + queryKey: ['minStockData', stockCode], + }), + }; +}; diff --git a/src/app/hooks/useWebSocket.ts b/src/app/hooks/useWebSocket.ts index da0960d6..550a94f6 100644 --- a/src/app/hooks/useWebSocket.ts +++ b/src/app/hooks/useWebSocket.ts @@ -63,7 +63,6 @@ const useWebSocket = ({ stockCode, setLiveData }: UseWebSocketProps) => { }; socket.onclose = () => { - console.log('WebSocket 연결 종료'); setIsConnected(false); }; } catch (error) { diff --git a/src/app/service/getRequest.ts b/src/app/service/getRequest.ts index 1219da5a..4e272e4e 100644 --- a/src/app/service/getRequest.ts +++ b/src/app/service/getRequest.ts @@ -101,7 +101,7 @@ export const getLandingPopular = async (req: Request) => { }; export const getLandingRecommend = async (req: Request) => { - const url = '/api/blogs/landings/recommend'; + const url = '/api/blogs/landings/recommend?filter=CREATED_AT&page=1&size=3'; return getRequest(url, req); }; @@ -245,3 +245,13 @@ export const getLandingNews = async (req: Request) => { const url = '/api/news-summary/todaynews'; return getRequest(url, req); }; + +export const getEntInfo = async (req: Request, stockCode: string) => { + const url = `/api/stock-predictions/fundamental-data?stockcode=${stockCode}`; + return getRequest(url, req); +}; + +export const getUserRanking = async (req: Request) => { + const url = '/api/transactions/rankings'; + return getRequest(url, req); +}; diff --git a/src/app/store/store.ts b/src/app/store/store.ts index eaa7e053..56ae2a67 100644 --- a/src/app/store/store.ts +++ b/src/app/store/store.ts @@ -18,10 +18,6 @@ export const useUserStore = create((set) => ({ const data = await callGet('/api/auth/user'); if (data.code === 'JWT_006') { const response = await callPost('/api/auth/refresh'); - console.log(response); - // const { accessToken: accessToken, refreshToken: newRefreshToken } = - // response.result; - // setTokens(accessToken, newRefreshToken, true); } set({ user: data, isLoading: false, error: null }); } catch (err) { @@ -56,3 +52,13 @@ const useStockStore = create((set) => ({ })); export default useStockStore; + +interface LiveStoreState { + liveData: ChartDataTypes | null; + setLiveData: (data: ChartDataTypes | null) => void; +} + +export const useLiveDataStore = create((set) => ({ + liveData: null, + setLiveData: (data) => set({ liveData: data }), +})); diff --git a/src/app/utils/date.ts b/src/app/utils/date.ts index a1aef645..41ecb1b5 100644 --- a/src/app/utils/date.ts +++ b/src/app/utils/date.ts @@ -181,3 +181,13 @@ export const calculateDateFrom = ( throw new Error(`Unsupported timeFrame: ${timeFrame}`); } }; + +export const howManyDays = (startDate: string, endDate: string) => { + const start = new Date(startDate); + const end = new Date(endDate); + + const timeDifference = end.getTime() - start.getTime(); + const daysDifference = timeDifference / (1000 * 3600 * 24); + + return daysDifference; +}; diff --git a/src/app/utils/formatNum.ts b/src/app/utils/formatNum.ts index 3222de1d..5f16e0d1 100644 --- a/src/app/utils/formatNum.ts +++ b/src/app/utils/formatNum.ts @@ -53,3 +53,13 @@ export const isProfit = (num: number | string): string => { ? `+${formatNumberCommas(value)}` : formatNumberCommas(value); }; + +export const formatEntValueUnit = (i: number): string => { + if (i === 0 || i === 3 || i === 5) { + return '원'; + } + if (i === 4) { + return '%'; + } + return '배'; +}; diff --git a/src/app/utils/formatStock.ts b/src/app/utils/formatStock.ts index 17f9d91e..7931cbe2 100644 --- a/src/app/utils/formatStock.ts +++ b/src/app/utils/formatStock.ts @@ -56,7 +56,6 @@ export const extractDateTimeAndPrice = (data: string) => { const parts = data.split('|'); const executionData = parts[3]; const executionParts = executionData.split('^'); - console.log(executionParts, '쪼갠 값들'); const executionTime = executionParts[1]; const close = executionParts[2]; const open = executionParts[7]; @@ -78,3 +77,43 @@ export const extractDateTimeAndPrice = (data: string) => { return realData; }; + +export function formatEntValue(data: EntValueTypes): (string | number)[] { + const { date, stockcode, BPS, PER, PBR, EPS, DIV, DPS } = data; + + return [BPS, PER, PBR, EPS, DIV, DPS]; +} + +export const backTestPurchaseCnt = ( + startDate: string, + endDate: string, + unit: '매일' | '매주' | '매월' | '매년', +): number => { + const start = new Date(startDate); + const end = new Date(endDate); + + let count = 0; + const current = new Date(start); + + while (current <= end) { + const isWeekday = current.getDay() >= 1 && current.getDay() <= 5; + if (isWeekday) { + if (unit === '매일') { + count += 1; + } else if (unit === '매주' && current.getDay() === 1) { + count += 1; + } else if (unit === '매월' && current.getDate() === 1) { + count += 1; + } else if ( + unit === '매년' && + current.getMonth() === 0 && + current.getDate() === 1 + ) { + count += 1; + } + } + current.setDate(current.getDate() + 1); + } + + return count; +}; diff --git a/src/app/utils/qualify.ts b/src/app/utils/qualify.ts index 5f7f0ecd..cfb72842 100644 --- a/src/app/utils/qualify.ts +++ b/src/app/utils/qualify.ts @@ -4,7 +4,9 @@ export const isCorrect = (text: string) => { }; export const valueColor = (value: number) => - value < 0 ? 'text-blue-1' : 'text-red-1'; + value < 0 + ? 'text-blue-1 dark:text-blue-1/80' + : 'text-red-1 dark:text-red-1/80'; export const tradeTypeColor = (tradeType: string) => tradeType === 'BUY' ? 'text-red-1' : 'text-blue-1'; diff --git a/src/app/utils/setToken.ts b/src/app/utils/setToken.ts index bc96dcb8..503b9d18 100644 --- a/src/app/utils/setToken.ts +++ b/src/app/utils/setToken.ts @@ -41,7 +41,6 @@ export const getCookie = (req: Request, name: string) => { export const handleLogout = async () => { const response = await callPost('/api/auth/logout'); - console.log(response); document.cookie = `accessToken=; expires=0; path=/;`; document.cookie = `refreshToken=; expires=0; path=/;`; window.location.href = '/'; // '/sign-in' 경로로 이동 diff --git a/src/instrumentation.ts b/src/instrumentation.ts deleted file mode 100644 index 811df314..00000000 --- a/src/instrumentation.ts +++ /dev/null @@ -1,13 +0,0 @@ -export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - const { tracer } = await import('dd-trace'); - - tracer.init({ - logInjection: true, - env: 'dev', - service: 'nextjs-with-datadog', - }); - - tracer.use('next'); - } -}