Compare commits
2 Commits
d7e7ccdb16
...
1d78b2c430
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d78b2c430 | |||
| bdb055cb32 |
9
package-lock.json
generated
9
package-lock.json
generated
@@ -12,7 +12,8 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-router-dom": "^6.30.3"
|
||||
"react-router-dom": "^6.30.3",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -2921,6 +2922,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-router-dom": "^6.30.3"
|
||||
"react-router-dom": "^6.30.3",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
@@ -26,11 +26,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.suspend-loading {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -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() {
|
||||
<div className="app-shell">
|
||||
<Navbar />
|
||||
<main className="site-main">
|
||||
<React.Suspense fallback={<div className="suspend-loading"><Loading /></div>}>
|
||||
<Outlet />
|
||||
</React.Suspense>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
21
src/api.js
21
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 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 += '/';
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = new URL(baseForJoin, window.location.origin);
|
||||
return new URL(pathForJoin, baseUrl).toString();
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
58
src/components/Loading.css
Normal file
58
src/components/Loading.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
23
src/components/Loading.jsx
Normal file
23
src/components/Loading.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import './Loading.css';
|
||||
|
||||
const Loading = ({ type = 'spinner', message = '로딩 중...' }) => {
|
||||
if (type === 'skeleton') {
|
||||
return (
|
||||
<div className="loading-skeleton">
|
||||
<div className="loading-skeleton__line" style={{ width: '60%' }}></div>
|
||||
<div className="loading-skeleton__line" style={{ width: '80%' }}></div>
|
||||
<div className="loading-skeleton__line" style={{ width: '40%' }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="loading-spinner">
|
||||
<div className="loading-spinner__circle"></div>
|
||||
{message && <p className="loading-spinner__text">{message}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
59
src/pages/effect-lab/EffectLab.css
Normal file
59
src/pages/effect-lab/EffectLab.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.effect-lab {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 80px);
|
||||
/* Adjust based on navbar height */
|
||||
overflow: hidden;
|
||||
background-color: #050505;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--line);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.effect-lab canvas {
|
||||
display: block;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.effect-lab-overlay {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.effect-lab-overlay h2 {
|
||||
margin: 0 0 8px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 28px;
|
||||
color: var(--text);
|
||||
text-shadow: 0 0 20px rgba(68, 170, 221, 0.5);
|
||||
}
|
||||
|
||||
.active-mode {
|
||||
display: inline-block;
|
||||
background: rgba(68, 170, 221, 0.1);
|
||||
border: 1px solid rgba(68, 170, 221, 0.3);
|
||||
padding: 6px 12px;
|
||||
border-radius: 99px;
|
||||
font-size: 12px;
|
||||
color: #44aadd;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.active-mode span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.effect-lab-overlay p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
max-width: 400px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
218
src/pages/effect-lab/EffectLab.jsx
Normal file
218
src/pages/effect-lab/EffectLab.jsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import './EffectLab.css';
|
||||
|
||||
const EffectLab = () => {
|
||||
const containerRef = useRef(null);
|
||||
const requestRef = useRef();
|
||||
const [mode, setMode] = useState('HOVER'); // HOVER, ATTACK, ORBIT
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// --- Configuration ---
|
||||
const COUNT = 1500;
|
||||
const SWORD_COLOR = 0x44aadd;
|
||||
const SWORD_EMISSIVE = 0x112244;
|
||||
|
||||
// --- Helper: Random Range ---
|
||||
const rand = (min, max) => Math.random() * (max - min) + min;
|
||||
|
||||
// --- Setup Scene ---
|
||||
const width = containerRef.current.clientWidth;
|
||||
const height = containerRef.current.clientHeight;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.fog = new THREE.FogExp2(0x050505, 0.002);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||
camera.position.z = 80;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
// Tone mapping for better glow look
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
containerRef.current.appendChild(renderer.domElement);
|
||||
|
||||
// --- Lighting ---
|
||||
const ambientLight = new THREE.AmbientLight(0x404040);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const pointLight = new THREE.PointLight(SWORD_COLOR, 2, 100);
|
||||
scene.add(pointLight);
|
||||
|
||||
// --- Geometry & Material ---
|
||||
// Sword shape: Cone stretched
|
||||
const geometry = new THREE.CylinderGeometry(0.1, 0.4, 4, 8);
|
||||
geometry.rotateX(Math.PI / 2); // Point towards Z
|
||||
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: SWORD_COLOR,
|
||||
emissive: SWORD_EMISSIVE,
|
||||
shininess: 100,
|
||||
flatShading: true,
|
||||
});
|
||||
|
||||
const mesh = new THREE.InstancedMesh(geometry, material, COUNT);
|
||||
scene.add(mesh);
|
||||
|
||||
// --- Particle Data ---
|
||||
const dummy = new THREE.Object3D();
|
||||
const particles = [];
|
||||
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
particles.push({
|
||||
pos: new THREE.Vector3(rand(-100, 100), rand(-100, 100), rand(-50, 50)),
|
||||
vel: new THREE.Vector3(),
|
||||
acc: new THREE.Vector3(),
|
||||
// Orbit parameters
|
||||
angle: rand(0, Math.PI * 2),
|
||||
radius: rand(15, 30),
|
||||
speed: rand(0.02, 0.05),
|
||||
// Offset for natural movement
|
||||
offset: new THREE.Vector3(rand(-5, 5), rand(-5, 5), rand(-5, 5))
|
||||
});
|
||||
}
|
||||
|
||||
// --- Mouse & Interaction State ---
|
||||
const mouse = new THREE.Vector3();
|
||||
const target = new THREE.Vector3();
|
||||
const mousePlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
|
||||
const raycaster = new THREE.Raycaster();
|
||||
|
||||
let isMouseDown = false;
|
||||
let time = 0;
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
|
||||
raycaster.ray.intersectPlane(mousePlane, mouse);
|
||||
|
||||
// Allow light to follow mouse
|
||||
pointLight.position.copy(mouse);
|
||||
pointLight.position.z = 20;
|
||||
};
|
||||
|
||||
const handleMouseDown = () => { isMouseDown = true; setMode('ORBIT'); };
|
||||
const handleMouseUp = () => { isMouseDown = false; setMode('HOVER'); };
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mousedown', handleMouseDown);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
// --- Animation Loop ---
|
||||
const animate = () => {
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
time += 0.01;
|
||||
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
const p = particles[i];
|
||||
|
||||
// --- Behavior Logic ---
|
||||
if (isMouseDown) {
|
||||
// 1. ORBIT MODE: Rotate around mouse
|
||||
p.angle += p.speed + 0.02; // Spin faster
|
||||
|
||||
const orbitX = mouse.x + Math.cos(p.angle + time) * p.radius;
|
||||
const orbitY = mouse.y + Math.sin(p.angle + time) * p.radius;
|
||||
// Spiraling Z for depth
|
||||
const orbitZ = Math.sin(p.angle * 2 + time) * 10;
|
||||
|
||||
target.set(orbitX, orbitY, orbitZ);
|
||||
|
||||
// Strong pull to orbit positions
|
||||
p.acc.subVectors(target, p.pos).multiplyScalar(0.08);
|
||||
|
||||
} else {
|
||||
// 2. HOVER/FOLLOW MODE: Follow mouse with flocking feel
|
||||
|
||||
// Add noise/wandering
|
||||
const noiseX = Math.sin(time + i * 0.1) * 5;
|
||||
const noiseY = Math.cos(time + i * 0.1) * 5;
|
||||
|
||||
target.copy(mouse).add(p.offset).add(new THREE.Vector3(noiseX, noiseY, 0));
|
||||
|
||||
// Gentle pull
|
||||
p.acc.subVectors(target, p.pos).multiplyScalar(0.008);
|
||||
}
|
||||
|
||||
// Physics update
|
||||
p.vel.add(p.acc);
|
||||
p.vel.multiplyScalar(isMouseDown ? 0.90 : 0.94); // Drag
|
||||
p.pos.add(p.vel);
|
||||
|
||||
// Update Matrix
|
||||
dummy.position.copy(p.pos);
|
||||
|
||||
// Rotation: Look at velocity direction (dynamic) or mouse (focused)
|
||||
// Blending lookAt target for smoother rotation
|
||||
const lookPos = new THREE.Vector3().copy(p.pos).add(p.vel.clone().multiplyScalar(5));
|
||||
|
||||
// If moving very slowly, keep previous rotation to avoid jitter
|
||||
if (p.vel.lengthSq() > 0.01) {
|
||||
dummy.lookAt(lookPos);
|
||||
}
|
||||
|
||||
// Scale effect based on speed (stretch when fast)
|
||||
const speedScale = 1 + Math.min(p.vel.length(), 2) * 0.5;
|
||||
dummy.scale.set(1, 1, speedScale);
|
||||
|
||||
dummy.updateMatrix();
|
||||
mesh.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
// --- Resize ---
|
||||
const handleResize = () => {
|
||||
if (!containerRef.current) return;
|
||||
const newWidth = containerRef.current.clientWidth;
|
||||
const newHeight = containerRef.current.clientHeight;
|
||||
|
||||
camera.aspect = newWidth / newHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(newWidth, newHeight);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mousedown', handleMouseDown);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
if (containerRef.current && renderer.domElement) {
|
||||
containerRef.current.removeChild(renderer.domElement);
|
||||
}
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
renderer.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="effect-lab" ref={containerRef}>
|
||||
<div className="effect-lab-overlay">
|
||||
<h2>Sword Stream</h2>
|
||||
<div className="active-mode">
|
||||
MODE: <span>{mode}</span>
|
||||
</div>
|
||||
<p>
|
||||
<strong>Move</strong> to Guide |
|
||||
<strong>Click & Hold</strong> to Orbit & Charge
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EffectLab;
|
||||
@@ -1,6 +1,8 @@
|
||||
.stock {
|
||||
display: grid;
|
||||
gap: 28px;
|
||||
/* Prevent overflow on small screens */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stock-header {
|
||||
@@ -536,8 +538,32 @@
|
||||
}
|
||||
|
||||
@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 {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
<div className="stock-panel__actions">
|
||||
{indicesLoading ? (
|
||||
<span className="stock-chip">불러오는 중</span>
|
||||
<Loading type="spinner" message="" />
|
||||
) : null}
|
||||
<button
|
||||
className="button ghost small"
|
||||
@@ -237,8 +238,7 @@ const Stock = () => {
|
||||
return (
|
||||
<div
|
||||
key={item.name}
|
||||
className={`stock-snapshot__card ${
|
||||
highlighted.has(item.name)
|
||||
className={`stock-snapshot__card ${highlighted.has(item.name)
|
||||
? 'is-highlight'
|
||||
: ''
|
||||
}`}
|
||||
@@ -246,8 +246,7 @@ const Stock = () => {
|
||||
<p>{item.name}</p>
|
||||
<strong>{item.value ?? '--'}</strong>
|
||||
<span
|
||||
className={`stock-snapshot__change ${
|
||||
direction === 'up'
|
||||
className={`stock-snapshot__change ${direction === 'up'
|
||||
? 'is-up'
|
||||
: direction === 'down'
|
||||
? 'is-down'
|
||||
@@ -332,9 +331,7 @@ const Stock = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="stock-panel__actions">
|
||||
{loading ? (
|
||||
<span className="stock-chip">불러오는 중</span>
|
||||
) : null}
|
||||
{loading && <span className="stock-chip">Updating...</span>}
|
||||
<span className="stock-chip">
|
||||
국내 {newsDomestic.length} / 해외{' '}
|
||||
{newsOverseas.length}
|
||||
@@ -342,8 +339,8 @@ const Stock = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="stock-empty">뉴스를 불러오는 중...</p>
|
||||
{loading && combinedNews.length === 0 ? (
|
||||
<Loading type="skeleton" />
|
||||
) : newsError ? (
|
||||
<p className="stock-empty">{newsError}</p>
|
||||
) : combinedNews.length === 0 ? (
|
||||
@@ -353,8 +350,7 @@ const Stock = () => {
|
||||
<div className="stock-tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`stock-tab ${
|
||||
newsCategory === 'domestic'
|
||||
className={`stock-tab ${newsCategory === 'domestic'
|
||||
? 'is-active'
|
||||
: ''
|
||||
}`}
|
||||
@@ -364,8 +360,7 @@ const Stock = () => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`stock-tab ${
|
||||
newsCategory === 'overseas'
|
||||
className={`stock-tab ${newsCategory === 'overseas'
|
||||
? 'is-active'
|
||||
: ''
|
||||
}`}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import Home from './pages/home/Home';
|
||||
import Blog from './pages/blog/Blog';
|
||||
import Lotto from './pages/lotto/Lotto';
|
||||
import Travel from './pages/travel/Travel';
|
||||
import Stock from './pages/stock/Stock';
|
||||
import StockTrade from './pages/stock/StockTrade';
|
||||
import React, { lazy } from 'react';
|
||||
|
||||
const Home = lazy(() => import('./pages/home/Home'));
|
||||
const Blog = lazy(() => import('./pages/blog/Blog'));
|
||||
const Lotto = lazy(() => import('./pages/lotto/Lotto'));
|
||||
const Travel = lazy(() => import('./pages/travel/Travel'));
|
||||
const Stock = lazy(() => import('./pages/stock/Stock'));
|
||||
const StockTrade = lazy(() => import('./pages/stock/StockTrade'));
|
||||
const EffectLab = lazy(() => import('./pages/effect-lab/EffectLab'));
|
||||
|
||||
export const navLinks = [
|
||||
{
|
||||
@@ -37,6 +39,12 @@ export const navLinks = [
|
||||
path: '/travel',
|
||||
description: '여행에서 담은 색과 장면을 전시하는 갤러리',
|
||||
},
|
||||
{
|
||||
id: 'lab',
|
||||
label: 'Lab',
|
||||
path: '/lab',
|
||||
description: '실험적인 UI/UX 효과를 테스트하는 공간',
|
||||
},
|
||||
];
|
||||
|
||||
export const appRoutes = [
|
||||
@@ -64,4 +72,8 @@ export const appRoutes = [
|
||||
path: 'travel',
|
||||
element: <Travel />,
|
||||
},
|
||||
{
|
||||
path: 'lab',
|
||||
element: <EffectLab />,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user