여행 탭 구성 추가

- CORS 문제로 Proxy 서버 구축하여 api 키로 JSON 목록 불러옴
 - 여행 탭 UI 수정
This commit is contained in:
2026-01-18 15:34:05 +09:00
parent e8d3e65b69
commit 837408423e
2 changed files with 184 additions and 15 deletions

View File

@@ -52,6 +52,62 @@
gap: 18px; 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 { .travel-card {
position: relative; position: relative;
border-radius: 20px; border-radius: 20px;
@@ -106,4 +162,9 @@
.travel-card.is-wide { .travel-card.is-wide {
grid-column: span 1; grid-column: span 1;
} }
.travel-album__cover {
width: 100%;
height: 160px;
}
} }

View File

@@ -1,8 +1,85 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { travelGallery } from '../../data/travel';
import './Travel.css'; 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 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 ( return (
<div className="travel"> <div className="travel">
<header className="travel-header"> <header className="travel-header">
@@ -21,20 +98,51 @@ const Travel = () => {
</div> </div>
</header> </header>
<section className="travel-grid"> <section className="travel-albums">
{travelGallery.map((photo, index) => ( {loading ? <p className="travel-state">앨범을 불러오는 ...</p> : null}
<article {error ? <p className="travel-error">{error}</p> : null}
key={photo.id} {!loading && !error && albums.length === 0 ? (
className={`travel-card ${index % 3 === 0 ? 'is-wide' : ''}`} <p className="travel-state">표시할 앨범이 없습니다.</p>
> ) : null}
<img src={photo.image} alt={photo.title} loading="lazy" />
<div className="travel-card__overlay"> {albums.map((album) => (
<p className="travel-card__title">{photo.title}</p> <div key={album.name} className="travel-album">
<p className="travel-card__meta"> <div className="travel-album__head">
{photo.location} · {photo.month} <div>
<p className="travel-album__eyebrow">Album</p>
<h2>{album.name}</h2>
<p className="travel-album__meta">
{album.count} photos
</p> </p>
</div> </div>
{album.cover ? (
<img
className="travel-album__cover"
src={album.cover}
alt={`${album.name} cover`}
loading="lazy"
/>
) : null}
</div>
<div className="travel-grid">
{album.photos.map((photo, index) => {
const label = photo.title || getPhotoLabel(photo.src);
return (
<article
key={`${album.name}-${photo.src}`}
className={`travel-card ${
index % 6 === 0 ? 'is-wide' : ''
}`}
>
<img src={photo.src} alt={label} loading="lazy" />
<div className="travel-card__overlay">
<p className="travel-card__title">{label}</p>
</div>
</article> </article>
);
})}
</div>
</div>
))} ))}
</section> </section>
</div> </div>