diff --git a/FE/index.html b/FE/index.html index 48dcbde9..81421944 100644 --- a/FE/index.html +++ b/FE/index.html @@ -2,7 +2,7 @@ - + = 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3486,6 +3495,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", + "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/FE/package.json b/FE/package.json index 4d667eee..dd0e90c4 100644 --- a/FE/package.json +++ b/FE/package.json @@ -17,6 +17,7 @@ "react-dom": "^18.3.1", "react-error-boundary": "^4.1.2", "react-router-dom": "^6.27.0", + "react-toastify": "^10.0.6", "socket.io-client": "^4.8.1", "vite-tsconfig-paths": "^5.0.1", "zustand": "^5.0.1" diff --git a/FE/src/App.tsx b/FE/src/App.tsx index 7b88da0b..9939f1f4 100644 --- a/FE/src/App.tsx +++ b/FE/src/App.tsx @@ -12,6 +12,8 @@ import Login from 'components/Login'; import SearchModal from './components/Search'; import MyPage from 'page/MyPage'; import Rank from 'page/Rank.tsx'; +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; function App() { return ( @@ -39,6 +41,7 @@ function Layout() { + ); } diff --git a/FE/src/assets/favicon.ico b/FE/src/assets/favicon.ico new file mode 100644 index 00000000..8efdf207 Binary files /dev/null and b/FE/src/assets/favicon.ico differ diff --git a/FE/src/assets/Logo.png b/FE/src/assets/logo.png similarity index 100% rename from FE/src/assets/Logo.png rename to FE/src/assets/logo.png diff --git a/FE/src/assets/logo.webp b/FE/src/assets/logo.webp new file mode 100644 index 00000000..c237350f Binary files /dev/null and b/FE/src/assets/logo.webp differ diff --git a/FE/src/components/Header.tsx b/FE/src/components/Header.tsx index 3381cb00..e3a4d6e7 100644 --- a/FE/src/components/Header.tsx +++ b/FE/src/components/Header.tsx @@ -3,7 +3,8 @@ import useAuthStore from 'store/authStore'; import useLoginModalStore from 'store/useLoginModalStore'; import useSearchModalStore from '../store/useSearchModalStore.ts'; import useSearchInputStore from '../store/useSearchInputStore.ts'; -import logo from 'assets/Logo.png'; +import logoPng from 'assets/logo.png'; +import logoWebp from 'assets/logo.webp'; import { checkAuth, logout } from 'service/auth.ts'; import { useEffect } from 'react'; @@ -42,7 +43,14 @@ export default function Header() {
- + + + +

JuGa

diff --git a/FE/src/components/Mypage/Account.tsx b/FE/src/components/Mypage/Account.tsx index b403e7ef..456abb9a 100644 --- a/FE/src/components/Mypage/Account.tsx +++ b/FE/src/components/Mypage/Account.tsx @@ -20,8 +20,6 @@ export default function Account() { const { asset, stocks } = data; - console.log(asset, stocks); - return (
diff --git a/FE/src/components/Mypage/BookMark.tsx b/FE/src/components/Mypage/BookMark.tsx new file mode 100644 index 00000000..7731bf1b --- /dev/null +++ b/FE/src/components/Mypage/BookMark.tsx @@ -0,0 +1,61 @@ +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { getBookmarkedStocks } from 'service/bookmark'; + +export default function BookMark() { + const navigation = useNavigate(); + + const handleClick = (code: string) => { + navigation(`/stocks/${code}`); + }; + + const { data, isLoading, isError } = useQuery( + ['bookmark', 'stock'], + () => getBookmarkedStocks(), + { + staleTime: 1000, + }, + ); + + if (isLoading) return
loading
; + if (!data) return
No data
; + if (isError) return
error
; + + return ( +
+
+

종목

+

현재가

+

등락률

+
+ +
    + {data.map((stock) => { + const { code, name, stck_prpr, prdy_ctrt, prdy_vrss_sign } = stock; + + return ( +
  • handleClick(code)} + > +
    +

    {name}

    +

    {code}

    +
    +

    + {(+stck_prpr).toLocaleString()}원 +

    +

    3 ? 'text-juga-blue-50' : 'text-juga-red-60'}`} + > + {+prdy_vrss_sign < 3 && '+'} + {prdy_ctrt}% +

    +
  • + ); + })} +
+
+ ); +} diff --git a/FE/src/components/Mypage/MyInfo.tsx b/FE/src/components/Mypage/MyInfo.tsx index d1c9d44b..ee0ef344 100644 --- a/FE/src/components/Mypage/MyInfo.tsx +++ b/FE/src/components/Mypage/MyInfo.tsx @@ -1,34 +1,88 @@ import { PencilSquareIcon } from '@heroicons/react/16/solid'; -import { useQuery } from '@tanstack/react-query'; -import { getMyProfile } from 'service/user'; +import Toast from 'components/Toast'; + +import useUser from 'hooks/useUser'; +import { useEffect, useState } from 'react'; export default function MyInfo() { - const { data, isLoading, isError } = useQuery( - ['myInfo', 'profile'], - () => getMyProfile(), - { staleTime: 1000 }, - ); + const [isEditMode, setIsEditMode] = useState(false); + const { userQuery, updateNickame } = useUser(); + + const { data, isLoading, isError } = userQuery; + + const [nickname, setNickname] = useState(''); + + useEffect(() => { + if (!data) return; + setNickname(data.name); + }, [data]); if (isLoading) return
loading
; if (!data) return
No data
; if (isError) return
error
; - const { name } = data; + const handeleEditBtnClick = () => { + if (nickname === data.name) { + setIsEditMode(false); + return; + } + + updateNickame.mutate(nickname, { + onSuccess: (res) => { + if (res.statusCode === 400) { + Toast({ message: res.message, type: 'error' }); + return; + } + setIsEditMode(false); + }, + }); + }; return (
-
+
-

- username +

+ 닉네임

-

- {name} -

- + {isEditMode ? ( + <> + setNickname(e.target.value)} + className='w-24 min-w-[60px] flex-1 text-right font-semibold text-juga-grayscale-500 sm:w-auto sm:min-w-[80px]' + autoFocus + /> + + + + ) : ( + <> +

+ {nickname} +

+
+ +
+ + )}
diff --git a/FE/src/components/Mypage/Nav.tsx b/FE/src/components/Mypage/Nav.tsx index a50c272b..1c94434c 100644 --- a/FE/src/components/Mypage/Nav.tsx +++ b/FE/src/components/Mypage/Nav.tsx @@ -4,9 +4,10 @@ import { MypageSectionType } from 'types'; const mapping = { account: '보유 자산 현황', order: '주문 요청 현황', + bookmark: '즐겨찾기', info: '내 정보', }; -const sections: MypageSectionType[] = ['account', 'order', 'info']; +const sections: MypageSectionType[] = ['account', 'order', 'bookmark', 'info']; export default function Nav() { const [searchParams, setSearchParams] = useSearchParams(); diff --git a/FE/src/components/Search/index.tsx b/FE/src/components/Search/index.tsx index d18e3e35..e9a543c4 100644 --- a/FE/src/components/Search/index.tsx +++ b/FE/src/components/Search/index.tsx @@ -47,7 +47,7 @@ export default function SearchModal() { <> toggleSearchModal()} />
{isSearching ? ( -
- +
+ +

+ 두 글자 이상의 검색어를 입력해주세요. +

) : ( showSearchResults && diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index 49007520..84b26203 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -15,6 +15,7 @@ import { drawUpperYAxis } from 'utils/chart/drawUpperYAxis.ts'; import { drawLowerYAxis } from 'utils/chart/drawLowerYAxis.ts'; import { drawChartGrid } from 'utils/chart/drawChartGrid.ts'; import { drawMouseGrid } from 'utils/chart/drawMouseGrid.ts'; +import { EyeIcon, EyeSlashIcon } from '@heroicons/react/16/solid'; const categories: { label: string; value: TiemCategory }[] = [ { label: '일', value: 'D' }, @@ -48,6 +49,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { const chartX = useRef(null); const rafRef = useRef(); const [timeCategory, setTimeCategory] = useState('D'); + const [moveAverageToggle, setMoveAverageToggle] = useState(true); const [charSizeConfig, setChartSizeConfig] = useState({ upperHeight: 0.5, lowerHeight: 0.4, @@ -62,6 +64,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { x: 0, y: 0, }); + const [mouseIndex, setMouseIndex] = useState(null); const { data, isLoading } = useQuery( ['stocksChartData', code, timeCategory], @@ -198,16 +201,18 @@ export default function Chart({ code }: StocksDeatailChartProps) { padding, ); - drawLineChart( - UpperChartCtx, - chartData, - 0, - 0, - upperChartCanvas.width - padding.left - padding.right, - upperChartCanvas.height - padding.top - padding.bottom, - padding, - 0.1, - ); + if (moveAverageToggle) { + drawLineChart( + UpperChartCtx, + chartData, + 0, + 0, + upperChartCanvas.width - padding.left - padding.right, + upperChartCanvas.height - padding.top - padding.bottom, + padding, + 0.1, + ); + } drawCandleChart( UpperChartCtx, @@ -262,6 +267,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { padding, mousePosition, upperChartCanvas.height + lowerChartCanvas.height, + setMouseIndex, ); if ( @@ -293,6 +299,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { drawUpperYAxis, drawLowerYAxis, drawXAxis, + moveAverageToggle, ], ); @@ -355,7 +362,6 @@ export default function Chart({ code }: StocksDeatailChartProps) { charSizeConfig, mousePosition, ]); - return (
@@ -381,6 +387,59 @@ export default function Chart({ code }: StocksDeatailChartProps) {
+
+ {mouseIndex !== null && data ? ( +
+ + 시작 {Number(data[mouseIndex].stck_oprc).toLocaleString()}원 + + + 고가 {Number(data[mouseIndex].stck_hgpr).toLocaleString()}원 + + + 저가 {Number(data[mouseIndex].stck_lwpr).toLocaleString()}원 + + + 종가 {Number(data[mouseIndex].stck_clpr).toLocaleString()}원 + +
+ ) : null} +
+ +
이동평균선
+
+ 5 + {mouseIndex !== null && data ? ( + + {Math.floor( + Number(data[mouseIndex].mov_avg_5), + ).toLocaleString()} + 원 + + ) : null} + 20 + {mouseIndex !== null && data ? ( + + {Math.floor( + Number(data[mouseIndex].mov_avg_20), + ).toLocaleString()} + 원 + + ) : null} +
+
+
{ + // if (isInitialMount.current) { + // isInitialMount.current = false; + // return; + // } + + // if (debounceValue) { + // bookmark(code); + // } else { + // unbookmark(code); + // } + // }, [code, debounceValue]); useEffect(() => { const handleSocketData = (data: { @@ -65,7 +91,7 @@ export default function Header({ code, data }: StocksDetailHeaderProps) { currPrdyVrssSign === '3' ? '' : currPrdyVrssSign < '3' ? '+' : '-'; return ( -
+

{hts_kor_isnm}

@@ -81,13 +107,32 @@ export default function Header({ code, data }: StocksDetailHeaderProps) {

-
+
{stockInfo.map((e, idx) => (

{e.label}

{e.value}

))} +
); diff --git a/FE/src/components/StocksDetail/TradeSection/BuySection.tsx b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx index 26836169..b1660702 100644 --- a/FE/src/components/StocksDetail/TradeSection/BuySection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx @@ -1,4 +1,11 @@ -import { ChangeEvent, FocusEvent, FormEvent, useRef, useState } from 'react'; +import { + ChangeEvent, + FocusEvent, + FormEvent, + useEffect, + useRef, + useState, +} from 'react'; import useTradeAlertModalStore from 'store/tradeAlertModalStore'; import { StockDetailType } from 'types'; import { isNumericString } from 'utils/common'; @@ -24,6 +31,10 @@ export default function BuySection({ code, detailInfo }: BuySectionProps) { const [currPrice, setCurrPrice] = useState(stck_prpr); const { isLogin } = useAuthStore(); + useEffect(() => { + setCurrPrice(stck_prpr); + }, [stck_prpr]); + const { isOpen, toggleModal } = useTradeAlertModalStore(); const [count, setCount] = useState(0); diff --git a/FE/src/components/StocksDetail/TradeSection/SellSection.tsx b/FE/src/components/StocksDetail/TradeSection/SellSection.tsx index 9eaec4c3..167cb403 100644 --- a/FE/src/components/StocksDetail/TradeSection/SellSection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/SellSection.tsx @@ -2,7 +2,14 @@ import Lottie from 'lottie-react'; import emptyAnimation from 'assets/emptyAnimation.json'; import { useQuery } from '@tanstack/react-query'; import { getSellInfo } from 'service/assets'; -import { ChangeEvent, FocusEvent, FormEvent, useRef, useState } from 'react'; +import { + ChangeEvent, + FocusEvent, + FormEvent, + useEffect, + useRef, + useState, +} from 'react'; import { StockDetailType } from 'types'; import useAuthStore from 'store/authStore'; import useTradeAlertModalStore from 'store/tradeAlertModalStore'; @@ -34,6 +41,10 @@ export default function SellSection({ code, detailInfo }: SellSectionProps) { const { isOpen, toggleModal } = useTradeAlertModalStore(); + useEffect(() => { + setCurrPrice(stck_prpr); + }, [stck_prpr]); + if (isLoading) return
loading
; if (!data) return
No data
; if (isError) return
error
; diff --git a/FE/src/components/Toast.tsx b/FE/src/components/Toast.tsx new file mode 100644 index 00000000..aeb0c281 --- /dev/null +++ b/FE/src/components/Toast.tsx @@ -0,0 +1,23 @@ +import { toast } from 'react-toastify'; + +type ToastType = 'success' | 'error' | 'warning' | 'info'; + +type ToastProps = { + message: string; + type: ToastType; +}; + +export default function Toast({ message, type }: ToastProps) { + switch (type) { + case 'success': + return toast.success(message, { position: 'top-right', autoClose: 1000 }); + case 'error': + return toast.error(message, { position: 'top-right', autoClose: 1000 }); + case 'warning': + return toast.warning(message, { position: 'top-right', autoClose: 1000 }); + case 'info': + return toast.info(message, { position: 'top-right', autoClose: 1000 }); + default: + return null; + } +} diff --git a/FE/src/components/TopFive/Card.tsx b/FE/src/components/TopFive/Card.tsx index 819efcf0..de7a31ce 100644 --- a/FE/src/components/TopFive/Card.tsx +++ b/FE/src/components/TopFive/Card.tsx @@ -6,6 +6,7 @@ type CardProps = { price: string; changePercentage: string; changePrice: string; + flag: string; index: number; }; @@ -15,14 +16,14 @@ export default function Card({ price, changePercentage, changePrice, + flag, index, }: CardProps) { - const changeValue = - typeof changePercentage === 'string' - ? Number(changePercentage) - : changePercentage; - const changeColor = - changeValue > 0 ? 'text-juga-red-60' : 'text-juga-blue-50'; + const color = + flag === '3' ? '' : flag < '3' ? 'text-juga-red-60' : 'text-juga-blue-40'; + const percentAbsolute = Math.abs(Number(changePercentage)).toFixed(2); + + const plusOrMinus = flag === '3' ? '' : flag < '3' ? '+' : '-'; const navigation = useNavigate(); @@ -46,11 +47,11 @@ export default function Card({ {Number(price).toLocaleString()}

-
+

- {changeValue > 0 - ? `${Number(changePrice).toLocaleString()}(${changeValue}%)` - : `${Number(changePrice).toLocaleString()}(${Math.abs(changeValue)}%)`} + {plusOrMinus} + {Math.abs(Number(changePrice))}({percentAbsolute} + %)

diff --git a/FE/src/components/TopFive/List.tsx b/FE/src/components/TopFive/List.tsx index 49a8f6f3..5e05004a 100644 --- a/FE/src/components/TopFive/List.tsx +++ b/FE/src/components/TopFive/List.tsx @@ -34,6 +34,7 @@ export default function List({ listTitle, data, isLoading }: ListProps) { price={stock.stck_prpr} changePercentage={stock.prdy_ctrt} changePrice={stock.prdy_vrss} + flag={stock.prdy_vrss_sign} index={index} /> diff --git a/FE/src/hooks/useUser.ts b/FE/src/hooks/useUser.ts new file mode 100644 index 00000000..77513a4b --- /dev/null +++ b/FE/src/hooks/useUser.ts @@ -0,0 +1,17 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { getMyProfile, rename } from 'service/user'; + +export default function useUser() { + const queryClient = useQueryClient(); + + const userQuery = useQuery(['myInfo', 'profile'], () => getMyProfile(), { + staleTime: 1000, + }); + + const updateNickame = useMutation((nickname: string) => rename(nickname), { + onSuccess: () => queryClient.invalidateQueries(['myInfo', 'profile']), + }); + + return { userQuery, updateNickame }; +} diff --git a/FE/src/page/MyPage.tsx b/FE/src/page/MyPage.tsx index 99e00687..a5e35263 100644 --- a/FE/src/page/MyPage.tsx +++ b/FE/src/page/MyPage.tsx @@ -1,4 +1,5 @@ import Account from 'components/Mypage/Account'; +import BookMark from 'components/Mypage/BookMark'; import MyInfo from 'components/Mypage/MyInfo'; import Nav from 'components/Mypage/Nav'; import Order from 'components/Mypage/Order'; @@ -16,6 +17,7 @@ export default function MyPage() { { account: , order: , + bookmark: , info: , }[currentPage] } diff --git a/FE/src/service/bookmark.ts b/FE/src/service/bookmark.ts new file mode 100644 index 00000000..2cab9b39 --- /dev/null +++ b/FE/src/service/bookmark.ts @@ -0,0 +1,42 @@ +import { BookmakredStock } from 'types'; + +export async function bookmark(code: string) { + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/stocks/bookmark/${code}` + : `/api/stocks/bookmark/${code}`; + + return fetch(url, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +export async function unbookmark(code: string) { + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/stocks/bookmark/${code}` + : `/api/stocks/bookmark/${code}`; + + return fetch(url, { + method: 'DELETE', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +export async function getBookmarkedStocks(): Promise { + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/stocks/bookmark` + : '/api/stocks/bookmark'; + + return fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()); +} diff --git a/FE/src/service/stocks.ts b/FE/src/service/stocks.ts index e461b550..040e7c78 100644 --- a/FE/src/service/stocks.ts +++ b/FE/src/service/stocks.ts @@ -1,24 +1,29 @@ import { StockChartUnit, StockDetailType, TiemCategory } from 'types'; export async function getStocksByCode(code: string): Promise { - return fetch(`${import.meta.env.VITE_API_URL}/stocks/detail/${code}`).then( - (res) => res.json(), - ); + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/stocks/detail/${code}` + : `/api/stocks/detail/${code}`; + + return fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()); } export async function getStocksChartDataByCode( code: string, peroid: TiemCategory = 'D', - start: string = '', - end: string = '', + count: number = 50, ): Promise { return fetch(`${import.meta.env.VITE_API_URL}/stocks/detail/${code}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - fid_input_date_1: start, - fid_input_date_2: end, fid_period_div_code: peroid, + count: count, }), }).then((res) => res.json()); } diff --git a/FE/src/service/user.ts b/FE/src/service/user.ts index 5aa9caaf..4c204993 100644 --- a/FE/src/service/user.ts +++ b/FE/src/service/user.ts @@ -10,3 +10,18 @@ export async function getMyProfile(): Promise { headers: { 'Content-Type': 'application/json' }, }).then((res) => res.json()); } + +export async function rename(nickname: string) { + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/user/rename` + : '/api/user/rename'; + + return fetch(url, { + method: 'PATCH', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + nickname, + }), + }).then((res) => res.json()); +} diff --git a/FE/src/types.ts b/FE/src/types.ts index 53ba70c5..e175d827 100644 --- a/FE/src/types.ts +++ b/FE/src/types.ts @@ -28,6 +28,7 @@ export type StockDetailType = { per: string; stck_mxpr: string; stck_llam: string; + is_bookmarked: boolean; }; export type StockChartUnit = { @@ -38,11 +39,11 @@ export type StockChartUnit = { stck_lwpr: string; acml_vol: string; prdy_vrss_sign: string; - mov_avg_5: string; + mov_avg_5?: string; mov_avg_20?: string; }; -export type MypageSectionType = 'account' | 'order' | 'info'; +export type MypageSectionType = 'account' | 'order' | 'bookmark' | 'info'; export type Asset = { cash_balance: string; @@ -91,3 +92,12 @@ export type Profile = { name: string; email: string; }; + +export type BookmakredStock = { + name: string; + code: string; + stck_prpr: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; +}; diff --git a/FE/src/utils/chart/drawCandleChart.ts b/FE/src/utils/chart/drawCandleChart.ts index 8f0b83a1..ea5c3ff4 100644 --- a/FE/src/utils/chart/drawCandleChart.ts +++ b/FE/src/utils/chart/drawCandleChart.ts @@ -14,7 +14,7 @@ export function drawCandleChart( const values = data .map((d) => { - if (d.mov_avg_20) { + if (d.mov_avg_20 && d.mov_avg_5) { return [ +d.stck_hgpr, +d.stck_lwpr, @@ -23,7 +23,7 @@ export function drawCandleChart( Math.floor(+d.mov_avg_5), Math.floor(+d.mov_avg_20), ]; - } else { + } else if (d.mov_avg_5) { return [ +d.stck_hgpr, +d.stck_lwpr, @@ -31,6 +31,16 @@ export function drawCandleChart( +d.stck_oprc, Math.floor(+d.mov_avg_5), ]; + } else if (d.mov_avg_20) { + return [ + +d.stck_hgpr, + +d.stck_lwpr, + +d.stck_clpr, + +d.stck_oprc, + Math.floor(+d.mov_avg_20), + ]; + } else { + return [+d.stck_hgpr, +d.stck_lwpr, +d.stck_clpr, +d.stck_oprc]; } }) .flat(); diff --git a/FE/src/utils/chart/drawLineChart.ts b/FE/src/utils/chart/drawLineChart.ts index 05e9da0a..b44abccf 100644 --- a/FE/src/utils/chart/drawLineChart.ts +++ b/FE/src/utils/chart/drawLineChart.ts @@ -20,7 +20,7 @@ export function drawLineChart( const values = data .map((d) => { - if (d.mov_avg_20) { + if (d.mov_avg_20 && d.mov_avg_5) { return [ +d.stck_hgpr, +d.stck_lwpr, @@ -29,7 +29,7 @@ export function drawLineChart( Math.floor(+d.mov_avg_5), Math.floor(+d.mov_avg_20), ]; - } else { + } else if (d.mov_avg_5) { return [ +d.stck_hgpr, +d.stck_lwpr, @@ -37,6 +37,16 @@ export function drawLineChart( +d.stck_oprc, Math.floor(+d.mov_avg_5), ]; + } else if (d.mov_avg_20) { + return [ + +d.stck_hgpr, + +d.stck_lwpr, + +d.stck_clpr, + +d.stck_oprc, + Math.floor(+d.mov_avg_20), + ]; + } else { + return [+d.stck_hgpr, +d.stck_lwpr, +d.stck_clpr, +d.stck_oprc]; } }) .flat(); @@ -44,20 +54,22 @@ export function drawLineChart( const yMin = Math.round(Math.min(...values) * (1 - weight)); data.forEach((e, i) => { - const cx = x + padding.left + (width * i) / (n - 1) + gap / 2; - const cy = - y + - padding.top + - height - - (height * (+e.mov_avg_5 - yMin)) / (yMax - yMin); + if (e.mov_avg_5) { + const cx = x + padding.left + (width * i) / (n - 1) + gap / 2; + const cy = + y + + padding.top + + height - + (height * (+e.mov_avg_5 - yMin)) / (yMax - yMin); - if (i === 0) { - ctx.moveTo(cx, cy); - } else { - ctx.lineTo(cx, cy); + if (i === 0) { + ctx.moveTo(cx, cy); + } else { + ctx.lineTo(cx, cy); + } } }); - ctx.strokeStyle = '#000'; + ctx.strokeStyle = 'rgb(249 115 22)'; ctx.lineWidth = lineWidth; ctx.stroke(); @@ -78,7 +90,7 @@ export function drawLineChart( } } }); - ctx.strokeStyle = '#199123'; + ctx.strokeStyle = 'rgb(22 163 74)'; ctx.lineWidth = lineWidth; ctx.stroke(); } diff --git a/FE/src/utils/chart/drawUpperYAxis.ts b/FE/src/utils/chart/drawUpperYAxis.ts index b89103b1..769a7e0f 100644 --- a/FE/src/utils/chart/drawUpperYAxis.ts +++ b/FE/src/utils/chart/drawUpperYAxis.ts @@ -16,7 +16,8 @@ export const drawUpperYAxis = ( ) => { const values = data .map((d) => { - if (d.mov_avg_20) { + + if (d.mov_avg_20 && d.mov_avg_5) { return [ +d.stck_hgpr, +d.stck_lwpr, @@ -25,7 +26,8 @@ export const drawUpperYAxis = ( Math.floor(+d.mov_avg_5), Math.floor(+d.mov_avg_20), ]; - } else { + + } else if (d.mov_avg_5) { return [ +d.stck_hgpr, +d.stck_lwpr, @@ -33,6 +35,17 @@ export const drawUpperYAxis = ( +d.stck_oprc, Math.floor(+d.mov_avg_5), ]; + + } else if (d.mov_avg_20) { + return [ + +d.stck_hgpr, + +d.stck_lwpr, + +d.stck_clpr, + +d.stck_oprc, + Math.floor(+d.mov_avg_20), + ]; + } else { + return [+d.stck_hgpr, +d.stck_lwpr, +d.stck_clpr, +d.stck_oprc]; } }) .flat(); diff --git a/FE/src/utils/chart/drawXAxis.ts b/FE/src/utils/chart/drawXAxis.ts index 1ec02bf9..fca31589 100644 --- a/FE/src/utils/chart/drawXAxis.ts +++ b/FE/src/utils/chart/drawXAxis.ts @@ -11,6 +11,9 @@ export const drawXAxis = ( padding: Padding, mousePosition: MousePositionType, parentHeight: number, + setMouseIndex: ( + value: ((prevState: number | null) => number | null) | number | null, + ) => void, ) => { const labels = makeXLabels(data); @@ -44,8 +47,8 @@ export const drawXAxis = ( ) { const mouseX = mousePosition.x - padding.left; const dataIndex = Math.floor((mouseX / width) * (data.length - 1)); - if (dataIndex >= 0 && dataIndex < data.length) { + setMouseIndex(dataIndex); const boxPadding = 10; const boxHeight = 30; const mouseDate = data[dataIndex].stck_bsop_date; @@ -71,5 +74,5 @@ export const drawXAxis = ( boxY + boxHeight / 2 + 8, ); } - } + } else setMouseIndex(null); }; diff --git a/FE/src/utils/chart/makeLabels.ts b/FE/src/utils/chart/makeLabels.ts index 652a5f90..df49a2c0 100644 --- a/FE/src/utils/chart/makeLabels.ts +++ b/FE/src/utils/chart/makeLabels.ts @@ -21,20 +21,36 @@ export const makeYLabels = ( export const makeXLabels = (data: StockChartUnit[]) => { const totalData = data.length; - // 데이터 양에 따른 표시 간격 결정 - let interval: number; - if (totalData <= 10) { - interval = 1; - } else if (totalData <= 20) { - interval = 2; - } else if (totalData <= 30) { - interval = 5; + let desiredLabelCount: number; + if (totalData < 10) { + desiredLabelCount = 1; + } else if (totalData < 20) { + desiredLabelCount = 2; + } else if (totalData < 30) { + desiredLabelCount = 3; } else { - interval = 6; + desiredLabelCount = 5; } - // 선택된 날짜만 라벨로 반환 - return data - .filter((_, index) => index % interval === 0) - .map((item) => item.stck_bsop_date); + if (totalData === 1) { + return [data[0].stck_bsop_date]; + } + + const interval = Math.max( + 1, + Math.floor((totalData - 1) / (desiredLabelCount - 1)), + ); + const labels: string[] = []; + + labels.push(data[0].stck_bsop_date); + + for (let i = interval; i < totalData - interval; i += interval) { + labels.push(data[i].stck_bsop_date); + } + + if (totalData > 1) { + labels.push(data[totalData - 1].stck_bsop_date); + } + + return labels; }; diff --git a/FE/src/utils/useDebounce.ts b/FE/src/utils/useDebounce.ts index 3cd1d5ec..64598fbb 100644 --- a/FE/src/utils/useDebounce.ts +++ b/FE/src/utils/useDebounce.ts @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; -import { formatNoSpecialChar } from './formatNoSpecialChar.ts'; -export const useDebounce = (value: string, delay: number) => { +export const useDebounce = (value: T, delay: number) => { const [debounceValue, setDebounceValue] = useState(value); const [isDebouncing, setIsDebouncing] = useState(false); @@ -9,7 +8,7 @@ export const useDebounce = (value: string, delay: number) => { setIsDebouncing(true); const handler = setTimeout(() => { - setDebounceValue(formatNoSpecialChar(value)); + setDebounceValue(value); setIsDebouncing(false); }, delay);