From bdb055cb329895f3be2e8010c734ea6521ae25ab Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 9 Feb 2026 00:13:40 +0900 Subject: [PATCH] =?UTF-8?q?1.=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=202.=20API=20=ED=98=B8=EC=B6=9C=20=EB=B3=91?= =?UTF-8?q?=EB=A0=AC=20=EC=B2=98=EB=A6=AC=203.=20UI=EA=B0=9C=EC=84=A0=20-?= =?UTF-8?q?=20=EB=A1=9C=EB=94=A9=20=EA=B2=BD=ED=97=98=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=204.=20=EB=B0=98=EC=9D=91=ED=98=95=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=205.=20API=20=ED=86=B5=EC=8B=A0=20=ED=8A=B9=EC=9D=B4?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20-=20URL=20=EA=B5=AC=EC=84=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=98=20=EC=9E=A0=EC=9E=AC=EC=A0=81=20=EC=9C=84?= =?UTF-8?q?=ED=97=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 11 ++++++-- src/App.jsx | 5 +++- src/api.js | 23 ++++++++------- src/components/Loading.css | 58 ++++++++++++++++++++++++++++++++++++++ src/components/Loading.jsx | 23 +++++++++++++++ src/pages/stock/Stock.css | 42 +++++++++++++++++++++++---- src/pages/stock/Stock.jsx | 51 +++++++++++++++------------------ src/routes.jsx | 15 +++++----- 8 files changed, 173 insertions(+), 55 deletions(-) create mode 100644 src/components/Loading.css create mode 100644 src/components/Loading.jsx diff --git a/src/App.css b/src/App.css index f1b0b1b..57c0ade 100644 --- a/src/App.css +++ b/src/App.css @@ -26,18 +26,25 @@ } } +.suspend-loading { + display: grid; + place-items: center; + min-height: 50vh; +} + @keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } } -.site-main > * { +.site-main>* { animation: fadeUp 0.6s ease both; } @@ -67,4 +74,4 @@ .button.ghost { background: transparent; -} +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 1befa3f..49b766f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { Outlet } from 'react-router-dom'; import Navbar from './components/Navbar'; +import Loading from './components/Loading'; import './App.css'; function App() { @@ -8,7 +9,9 @@ function App() {
- +
}> + + ); diff --git a/src/api.js b/src/api.js index a080079..3c79907 100644 --- a/src/api.js +++ b/src/api.js @@ -4,20 +4,19 @@ const API_BASE = import.meta.env.VITE_API_BASE || ""; const toApiUrl = (path) => { if (!API_BASE) return path; - const baseClean = API_BASE.replace(/\/+$/, ""); - const baseForJoin = `${baseClean}/`; - const normalizedPath = path.startsWith("/") ? path.slice(1) : path; - let pathForJoin = normalizedPath; - - if (baseClean.endsWith("/api") && normalizedPath.startsWith("api/")) { - pathForJoin = normalizedPath.slice(4); - } - try { - const baseUrl = new URL(baseForJoin, window.location.origin); - return new URL(pathForJoin, baseUrl).toString(); + const base = new URL(API_BASE, window.location.origin); + // Ensure base pathname ends with '/' if it's not the root or if likely intended as a directory + if (!base.pathname.endsWith('/')) { + base.pathname += '/'; + } + + // Remove leading slash from path to avoid double slashes when joining + const cleanPath = path.startsWith('/') ? path.slice(1) : path; + + return new URL(cleanPath, base).toString(); } catch (error) { - console.warn("Invalid VITE_API_BASE, falling back to relative URL.", error); + console.error("Invalid VITE_API_BASE configuration:", error); return path; } }; diff --git a/src/components/Loading.css b/src/components/Loading.css new file mode 100644 index 0000000..0fa3ae3 --- /dev/null +++ b/src/components/Loading.css @@ -0,0 +1,58 @@ +.loading-spinner { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + gap: 12px; +} + +.loading-spinner__circle { + width: 32px; + height: 32px; + border: 3px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; + border-top-color: var(--accent, #f7a8a5); + animation: spin 0.8s linear infinite; +} + +.loading-spinner__text { + font-size: 13px; + color: var(--muted, #b6b1a9); + margin: 0; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-skeleton { + display: grid; + gap: 12px; + padding: 16px; + width: 100%; +} + +.loading-skeleton__line { + height: 16px; + border-radius: 4px; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.05) 25%, + rgba(255, 255, 255, 0.1) 50%, + rgba(255, 255, 255, 0.05) 75% + ); + background-size: 200% 100%; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} diff --git a/src/components/Loading.jsx b/src/components/Loading.jsx new file mode 100644 index 0000000..917f910 --- /dev/null +++ b/src/components/Loading.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './Loading.css'; + +const Loading = ({ type = 'spinner', message = '로딩 중...' }) => { + if (type === 'skeleton') { + return ( +
+
+
+
+
+ ); + } + + return ( +
+
+ {message &&

{message}

} +
+ ); +}; + +export default Loading; diff --git a/src/pages/stock/Stock.css b/src/pages/stock/Stock.css index d2fc6eb..c439647 100644 --- a/src/pages/stock/Stock.css +++ b/src/pages/stock/Stock.css @@ -1,6 +1,8 @@ .stock { display: grid; gap: 28px; + /* Prevent overflow on small screens */ + width: 100%; } .stock-header { @@ -66,7 +68,7 @@ font-size: 13px; } -.stock-status > div { +.stock-status>div { display: flex; justify-content: space-between; gap: 12px; @@ -536,15 +538,39 @@ } @media (max-width: 768px) { + .stock { + gap: 20px; + } + .stock-panel { padding: 16px; + gap: 12px; + } + + .stock-filter-row { + gap: 12px; + grid-template-columns: 1fr; + } + + .stock-header h1 { + font-size: 28px; + } + + .stock-actions { + width: 100%; + } + + .stock-actions .button { + flex: 1; + text-align: center; + justify-content: center; } .stock-card { padding: 16px; } - .stock-status > div { + .stock-status>div { gap: 8px; } @@ -568,13 +594,19 @@ } .stock-holdings__metric { - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + grid-template-columns: repeat(2, 1fr); align-items: center; justify-items: start; + gap: 8px 16px; + } + + /* Make the last item span full width if it's odd */ + .stock-holdings__metric>*:last-child:nth-child(odd) { + grid-column: 1 / -1; } .stock-holdings__metric span { - font-size: 12px; + font-size: 11px; } .stock-holdings__metric strong { @@ -591,4 +623,4 @@ .stock-holdings__metric strong { font-size: 14px; } -} +} \ No newline at end of file diff --git a/src/pages/stock/Stock.jsx b/src/pages/stock/Stock.jsx index 2749326..d410c36 100644 --- a/src/pages/stock/Stock.jsx +++ b/src/pages/stock/Stock.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { getStockIndices, getStockNews } from '../../api'; +import Loading from '../../components/Loading'; import './Stock.css'; const formatDate = (value) => { @@ -206,7 +207,7 @@ const Stock = () => {
{indicesLoading ? ( - 불러오는 중 + ) : null}
- {loading ? ( -

뉴스를 불러오는 중...

+ {loading && combinedNews.length === 0 ? ( + ) : newsError ? (

{newsError}

) : combinedNews.length === 0 ? ( @@ -353,22 +350,20 @@ const Stock = () => {