diff --git a/FE/package-lock.json b/FE/package-lock.json index 762e3fb..27b337a 100644 --- a/FE/package-lock.json +++ b/FE/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@heroicons/react": "^2.1.5", "@tanstack/react-query": "^4.36.1", + "hangul-js": "^0.2.6", "lottie-react": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2571,6 +2572,12 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/hangul-js": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/hangul-js/-/hangul-js-0.2.6.tgz", + "integrity": "sha512-48axU8LgjCD30FEs66Xc04/8knxMwCMQw0f67l67rlttW7VXT3qRJgQeHmhiuGwWXGvSbk6YM0fhQlcjE1JFQA==", + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/FE/package.json b/FE/package.json index 683aacf..a25e8b5 100644 --- a/FE/package.json +++ b/FE/package.json @@ -12,6 +12,7 @@ "dependencies": { "@heroicons/react": "^2.1.5", "@tanstack/react-query": "^4.36.1", + "hangul-js": "^0.2.6", "lottie-react": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/FE/src/components/Header.tsx b/FE/src/components/Header.tsx index 51058c9..87773e6 100644 --- a/FE/src/components/Header.tsx +++ b/FE/src/components/Header.tsx @@ -6,8 +6,9 @@ import useSearchInputStore from 'store/useSearchInputStore.ts'; import logoPng from 'assets/logo.png'; import logoWebp from 'assets/logo.webp'; import { checkAuth, logout } from 'service/auth.ts'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import Toast from './Toast'; +import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'; export default function Header() { const { toggleModal } = useLoginModalStore(); @@ -16,6 +17,7 @@ export default function Header() { const { searchInput } = useSearchInputStore(); const location = useLocation(); const navigate = useNavigate(); + const [isMenuOpen, setIsMenuOpen] = useState(false); useEffect(() => { const check = async () => { @@ -42,24 +44,24 @@ export default function Header() { return; } navigate(to); + setIsMenuOpen(false); }; return (
-
+
- - {'Logo'} + + Logo -

JuGa

+

+ JuGa +

-
+ {/* Desktop Navigation */} +
); diff --git a/FE/src/components/Loading.tsx b/FE/src/components/Loading.tsx new file mode 100644 index 0000000..1439e0f --- /dev/null +++ b/FE/src/components/Loading.tsx @@ -0,0 +1,26 @@ +export default function Loading() { + return ( +
+ + Loading... +
+ ); +} diff --git a/FE/src/components/Mypage/Order.tsx b/FE/src/components/Mypage/Order.tsx index 296cb6c..839d775 100644 --- a/FE/src/components/Mypage/Order.tsx +++ b/FE/src/components/Mypage/Order.tsx @@ -3,11 +3,17 @@ import useOrderCancelAlertModalStore from 'store/useOrderCancleAlertModalStore'; import CancelAlertModal from './CancelAlertModal.tsx'; import { formatTimestamp } from 'utils/format'; import { useNavigate } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { getOrders } from 'service/orders.ts'; export default function Order() { - const { orderQuery, removeOrder } = useOrders(); + const { removeOrder } = useOrders(); + + const { data } = useQuery(['account', 'order'], () => getOrders(), { + staleTime: 1000, + suspense: true, + }); - const { data } = orderQuery; const { isOpen, open } = useOrderCancelAlertModalStore(); const navigation = useNavigate(); diff --git a/FE/src/components/News/Card.tsx b/FE/src/components/News/Card.tsx index 5b3bb03..5f844c4 100644 --- a/FE/src/components/News/Card.tsx +++ b/FE/src/components/News/Card.tsx @@ -7,26 +7,28 @@ type CardWithImageProps = { export default function Card({ data }: CardWithImageProps) { return (
-
-

+
+

{data.title}

- + {formatDate(data.pubDate)}

-
-

+

+

{data.description}

- + {data.query}
diff --git a/FE/src/components/News/News.tsx b/FE/src/components/News/News.tsx index 2d9de3e..24a7ad4 100644 --- a/FE/src/components/News/News.tsx +++ b/FE/src/components/News/News.tsx @@ -19,7 +19,7 @@ export default function News() {

주요 뉴스

-
+
{data.news .slice(randomNewsIndex, randomNewsIndex + 4) .map((news: NewsDataType, index: number) => ( diff --git a/FE/src/components/Search/KoreanMapping.ts b/FE/src/components/Search/KoreanMapping.ts new file mode 100644 index 0000000..5e0ab2e --- /dev/null +++ b/FE/src/components/Search/KoreanMapping.ts @@ -0,0 +1,50 @@ +type CharacterMap = { + [key: string]: string; +}; + +export class SimpleKoreanConverter { + private charMap: CharacterMap; + + constructor() { + this.charMap = { + r: 'ㄱ', + R: 'ㄲ', + s: 'ㄴ', + e: 'ㄷ', + E: 'ㄸ', + f: 'ㄹ', + a: 'ㅁ', + q: 'ㅂ', + Q: 'ㅃ', + t: 'ㅅ', + T: 'ㅆ', + d: 'ㅇ', + w: 'ㅈ', + W: 'ㅉ', + c: 'ㅊ', + z: 'ㅋ', + x: 'ㅌ', + v: 'ㅍ', + g: 'ㅎ', + + k: 'ㅏ', + o: 'ㅐ', + i: 'ㅑ', + O: 'ㅒ', + j: 'ㅓ', + p: 'ㅔ', + u: 'ㅕ', + P: 'ㅖ', + h: 'ㅗ', + y: 'ㅛ', + n: 'ㅜ', + b: 'ㅠ', + m: 'ㅡ', + l: 'ㅣ', + }; + } + + public convert(input: string): string[] { + return Array.from(input).map((char) => this.charMap[char] || char); + } +} diff --git a/FE/src/components/Search/SearchCard.tsx b/FE/src/components/Search/SearchCard.tsx index 2463352..7c9b91b 100644 --- a/FE/src/components/Search/SearchCard.tsx +++ b/FE/src/components/Search/SearchCard.tsx @@ -25,7 +25,7 @@ export default function SearchCard({ data }: SearchCardProps) { className={ 'h-[52px] w-full rounded-xl hover:cursor-pointer hover:bg-gray-100' } - onClick={handleClick} + onMouseDown={handleClick} >
diff --git a/FE/src/components/Search/index.tsx b/FE/src/components/Search/index.tsx index 21b2390..3069614 100644 --- a/FE/src/components/Search/index.tsx +++ b/FE/src/components/Search/index.tsx @@ -12,6 +12,8 @@ import searchAnimation from 'assets/searchAnimation.json'; import { useSearchHistory } from 'hooks/useSearchHistoryHook.ts'; import { getSearchResults } from 'service/search.ts'; import { formatNoSpecialChar } from 'utils/format.ts'; +import { SimpleKoreanConverter } from './KoreanMapping.ts'; +import * as Hangul from 'hangul-js'; export default function SearchModal() { const { isOpen, toggleSearchModal } = useSearchModalStore(); @@ -19,18 +21,48 @@ export default function SearchModal() { const { searchHistory, addSearchHistory, deleteSearchHistory } = useSearchHistory(); const shouldSearch = searchInput.trim().length >= 2; + const converter = new SimpleKoreanConverter(); const { debounceValue, isDebouncing } = useDebounce( shouldSearch ? searchInput : '', 500, ); - const { data, isLoading, isFetching } = useQuery({ + + const { + data: originalData, + isLoading: isOriginalLoading, + isFetching: isOriginalFetching, + } = useQuery({ queryKey: ['search', debounceValue], queryFn: () => getSearchResults(formatNoSpecialChar(debounceValue)), enabled: !!debounceValue && !isDebouncing, - staleTime: 1000, + staleTime: 10000, cacheTime: 1000 * 60, }); + const convertedSearch = debounceValue + ? Hangul.assemble(converter.convert(debounceValue)) + : ''; + + const { + data: convertedData, + isLoading: isConvertedLoading, + isFetching: isConvertedFetching, + } = useQuery({ + queryKey: ['search', convertedSearch], + queryFn: () => getSearchResults(formatNoSpecialChar(convertedSearch)), + enabled: + !isOriginalLoading && + !isOriginalFetching && + !!convertedSearch && + originalData !== undefined && + originalData.length === 0, + staleTime: 10000, + cacheTime: 1000 * 60, + }); + + const data = originalData?.length ? originalData : convertedData || []; + const isLoading = isOriginalLoading || isConvertedLoading; + const isFetching = isOriginalFetching || isConvertedFetching; useEffect(() => { if (data && data.length > 0 && debounceValue && !isLoading) { @@ -47,7 +79,7 @@ export default function SearchModal() {
toggleSearchModal()} />
- {' '} {!searchInput ? ( +

{name}

{stockIndexValue.curr_value}

diff --git a/FE/src/components/StockIndex/index.tsx b/FE/src/components/StockIndex/index.tsx index 3d8de29..fbb69bc 100644 --- a/FE/src/components/StockIndex/index.tsx +++ b/FE/src/components/StockIndex/index.tsx @@ -3,21 +3,18 @@ import { Card } from './Card.tsx'; import { useQuery } from '@tanstack/react-query'; export default function StockIndex() { - const { data, isLoading, isError } = useQuery({ + const { data } = useQuery({ queryKey: ['StockIndex'], queryFn: () => getStockIndex(), - staleTime: 1000, + staleTime: 10000, cacheTime: 60000, suspense: true, }); - if (isLoading) return
Loading...
; - if (isError) return
Error loading data
; - const { KOSPI, KOSDAQ, KOSPI200, KSQ150 } = data; return ( -
+
diff --git a/FE/src/components/StocksDetail/PriceSection/index.tsx b/FE/src/components/StocksDetail/PriceSection/index.tsx index 6f11d0f..7a48967 100644 --- a/FE/src/components/StocksDetail/PriceSection/index.tsx +++ b/FE/src/components/StocksDetail/PriceSection/index.tsx @@ -8,6 +8,7 @@ import { getTradeHistory } from 'service/tradeHistory.ts'; import { socket } from 'utils/socket.ts'; import { DailyPriceDataType, PriceDataType } from './type.ts'; import { PriceSectionViewType } from 'types.ts'; +import Loading from 'components/Loading.tsx'; export default function PriceSection() { const { id } = useParams(); @@ -109,7 +110,7 @@ export default function PriceSection() { {isLoading ? ( - Loading... + ) : !tradeData ? ( diff --git a/FE/src/components/StocksDetail/TradeSection/BuySection.tsx b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx index 7819307..e6b9754 100644 --- a/FE/src/components/StocksDetail/TradeSection/BuySection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx @@ -14,6 +14,7 @@ import useAuthStore from 'store/useAuthStore.ts'; import { useQuery } from '@tanstack/react-query'; import { getCash } from 'service/assets'; import TradeAlertModal from './TradeAlertModal'; +import Loading from 'components/Loading'; type BuySectionProps = { code: string; @@ -23,11 +24,9 @@ type BuySectionProps = { export default function BuySection({ code, detailInfo }: BuySectionProps) { const { stck_prpr, stck_mxpr, stck_llam, hts_kor_isnm } = detailInfo; - const { data, isLoading, isError } = useQuery( - ['detail', 'cash'], - () => getCash(), - { staleTime: 1000 }, - ); + const { data, isLoading } = useQuery(['detail', 'cash'], () => getCash(), { + staleTime: 1000, + }); const [currPrice, setCurrPrice] = useState(stck_prpr); const { isLogin } = useAuthStore(); @@ -57,9 +56,8 @@ export default function BuySection({ code, detailInfo }: BuySectionProps) { setCount(+s); }, []); - if (isLoading) return
loading
; + if (isLoading) return ; if (!data) return
No data
; - if (isError) return
error
; const handlePriceInputBlur = (e: FocusEvent) => { const n = +e.target.value.replace(/,/g, ''); diff --git a/FE/src/components/StocksDetail/TradeSection/SellSection.tsx b/FE/src/components/StocksDetail/TradeSection/SellSection.tsx index b44c30d..befeb69 100644 --- a/FE/src/components/StocksDetail/TradeSection/SellSection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/SellSection.tsx @@ -16,6 +16,7 @@ import useAuthStore from 'store/useAuthStore.ts'; import useTradeAlertModalStore from 'store/useTradeAlertModalStore'; import { calcYield, isNumericString } from 'utils/common'; import TradeAlertModal from './TradeAlertModal'; +import Loading from 'components/Loading'; type SellSectionProps = { code: string; @@ -25,7 +26,7 @@ type SellSectionProps = { export default function SellSection({ code, detailInfo }: SellSectionProps) { const { stck_prpr, stck_mxpr, stck_llam, hts_kor_isnm } = detailInfo; - const { data, isLoading, isError } = useQuery( + const { data, isLoading } = useQuery( ['detail', 'sellPosiible', code], () => getSellInfo(code), { staleTime: 1000 }, @@ -58,9 +59,8 @@ export default function SellSection({ code, detailInfo }: SellSectionProps) { setCount(+s); }, []); - if (isLoading) return
loading
; + if (isLoading) return ; if (!data) return
No data
; - if (isError) return
error
; const quantity = data.quantity; const avg_price = Math.floor(data.avg_price); diff --git a/FE/src/components/TopFive/index.tsx b/FE/src/components/TopFive/index.tsx index 1ebd80a..cbeb704 100644 --- a/FE/src/components/TopFive/index.tsx +++ b/FE/src/components/TopFive/index.tsx @@ -14,14 +14,16 @@ export default function TopFive() { queryKey: ['topfive', currentMarket], queryFn: () => getTopFiveStocks(stockIndexMap[currentMarket]), keepPreviousData: true, - staleTime: 1000, + staleTime: 10000, cacheTime: 30000, suspense: true, }); return (