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 = () => {