diff --git a/src/pages/travel/Travel.css b/src/pages/travel/Travel.css
index edd8ec6..8e350ee 100644
--- a/src/pages/travel/Travel.css
+++ b/src/pages/travel/Travel.css
@@ -52,6 +52,62 @@
gap: 18px;
}
+.travel-albums {
+ display: grid;
+ gap: 24px;
+}
+
+.travel-album {
+ border: 1px solid var(--line);
+ border-radius: 24px;
+ padding: 20px;
+ background: rgba(9, 10, 16, 0.5);
+ display: grid;
+ gap: 18px;
+}
+
+.travel-album__head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.travel-album__eyebrow {
+ margin: 0 0 6px;
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.22em;
+ color: var(--accent);
+}
+
+.travel-album__meta {
+ margin: 6px 0 0;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.travel-album__cover {
+ width: 120px;
+ height: 120px;
+ border-radius: 16px;
+ object-fit: cover;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+}
+
+.travel-state {
+ color: var(--muted);
+}
+
+.travel-error {
+ color: #f9b6b1;
+ border: 1px solid rgba(249, 182, 177, 0.4);
+ border-radius: 14px;
+ padding: 12px;
+ background: rgba(249, 182, 177, 0.1);
+}
+
.travel-card {
position: relative;
border-radius: 20px;
@@ -106,4 +162,9 @@
.travel-card.is-wide {
grid-column: span 1;
}
+
+ .travel-album__cover {
+ width: 100%;
+ height: 160px;
+ }
}
diff --git a/src/pages/travel/Travel.jsx b/src/pages/travel/Travel.jsx
index 4cd950e..ca30d05 100644
--- a/src/pages/travel/Travel.jsx
+++ b/src/pages/travel/Travel.jsx
@@ -1,8 +1,85 @@
-import React from 'react';
-import { travelGallery } from '../../data/travel';
+import React, { useEffect, useState } from 'react';
import './Travel.css';
+const normalizePhotos = (items = []) =>
+ items
+ .map((item) => {
+ if (typeof item === 'string') return { src: item, title: '' };
+ if (!item) return null;
+ return {
+ src: item.url || item.path || item.src || '',
+ title: item.title || item.name || '',
+ };
+ })
+ .filter((item) => item && item.src);
+
+const getPhotoLabel = (src) => {
+ if (!src) return '';
+ const parts = src.split('/');
+ return parts[parts.length - 1];
+};
+
const Travel = () => {
+ const [albums, setAlbums] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadAlbums = async () => {
+ setLoading(true);
+ setError('');
+
+ try {
+ const albumRes = await fetch('/api/travel/albums');
+ if (!albumRes.ok) {
+ throw new Error(`앨범 목록 로딩 실패 (${albumRes.status})`);
+ }
+ const albumJson = await albumRes.json();
+ const items = albumJson.items ?? [];
+
+ const hydrated = await Promise.all(
+ items.map(async (item) => {
+ const name = item.album || item.name || '';
+ if (!name) return null;
+ const photoRes = await fetch(
+ `/api/travel/albums/${encodeURIComponent(name)}`
+ );
+ if (!photoRes.ok) {
+ throw new Error(`앨범 로딩 실패: ${name}`);
+ }
+ const photoJson = await photoRes.json();
+ const photos = normalizePhotos(photoJson.items ?? []);
+ return {
+ name,
+ count: item.count ?? photos.length,
+ cover: item.cover || photos[0]?.src || '',
+ photos,
+ };
+ })
+ );
+
+ if (!cancelled) {
+ setAlbums(hydrated.filter(Boolean));
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(err?.message ?? String(err));
+ }
+ } finally {
+ if (!cancelled) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadAlbums();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
return (
@@ -21,20 +98,51 @@ const Travel = () => {
-
- {travelGallery.map((photo, index) => (
-
-
-
-
{photo.title}
-
- {photo.location} · {photo.month}
-
+
+ {loading ? 앨범을 불러오는 중...
: null}
+ {error ? {error}
: null}
+ {!loading && !error && albums.length === 0 ? (
+ 표시할 앨범이 없습니다.
+ ) : null}
+
+ {albums.map((album) => (
+
+
+
+
Album
+
{album.name}
+
+ {album.count} photos
+
+
+ {album.cover ? (
+

+ ) : null}
-
+
+ {album.photos.map((photo, index) => {
+ const label = photo.title || getPhotoLabel(photo.src);
+ return (
+
+
+
+
+ );
+ })}
+
+
))}