From 1ca5640a67f32a31228eafe844402cf6157a8ca1 Mon Sep 17 00:00:00 2001 From: caadiq Date: Thu, 1 Jan 2026 09:32:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=95=A8=EB=B2=94=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백엔드: /api/albums 라우트 추가 (routes/albums.js) - 앨범별 트랙 정보 포함 조회 - 프론트엔드: Discography 페이지 API 연동 - 앨범 타입별 통계 동적 계산 - 타이틀곡 자동 표시 --- backend/routes/albums.js | 57 +++++++++++++++++++++++ backend/routes/stats.js | 28 ++++++++++++ backend/server.js | 4 ++ frontend/src/pages/pc/Discography.jsx | 66 +++++++++++++++++++++++---- frontend/src/pages/pc/Members.jsx | 37 ++++++++------- 5 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 backend/routes/albums.js create mode 100644 backend/routes/stats.js diff --git a/backend/routes/albums.js b/backend/routes/albums.js new file mode 100644 index 0000000..b5ea92c --- /dev/null +++ b/backend/routes/albums.js @@ -0,0 +1,57 @@ +import express from "express"; +import pool from "../lib/db.js"; + +const router = express.Router(); + +// 전체 앨범 조회 (트랙 포함) +router.get("/", async (req, res) => { + try { + // 앨범 목록 조회 + const [albums] = await pool.query( + "SELECT id, title, album_type, release_date, cover_url FROM albums ORDER BY release_date DESC" + ); + + // 각 앨범에 트랙 정보 추가 + for (const album of albums) { + const [tracks] = await pool.query( + "SELECT id, track_number, title, is_title_track, duration, lyricist, composer, arranger FROM tracks WHERE album_id = ? ORDER BY track_number", + [album.id] + ); + album.tracks = tracks; + } + + res.json(albums); + } catch (error) { + console.error("앨범 조회 오류:", error); + res.status(500).json({ error: "앨범 정보를 가져오는데 실패했습니다." }); + } +}); + +// 특정 앨범 조회 (트랙 및 상세 정보 포함) +router.get("/:id", async (req, res) => { + try { + const [albums] = await pool.query("SELECT * FROM albums WHERE id = ?", [ + req.params.id, + ]); + + if (albums.length === 0) { + return res.status(404).json({ error: "앨범을 찾을 수 없습니다." }); + } + + const album = albums[0]; + + // 트랙 정보 조회 (가사 포함) + const [tracks] = await pool.query( + "SELECT * FROM tracks WHERE album_id = ? ORDER BY track_number", + [album.id] + ); + album.tracks = tracks; + + res.json(album); + } catch (error) { + console.error("앨범 조회 오류:", error); + res.status(500).json({ error: "앨범 정보를 가져오는데 실패했습니다." }); + } +}); + +export default router; diff --git a/backend/routes/stats.js b/backend/routes/stats.js new file mode 100644 index 0000000..30af305 --- /dev/null +++ b/backend/routes/stats.js @@ -0,0 +1,28 @@ +import express from "express"; +import pool from "../lib/db.js"; + +const router = express.Router(); + +// 통계 조회 (멤버 수, 앨범 수) +router.get("/", async (req, res) => { + try { + const [memberCount] = await pool.query( + "SELECT COUNT(*) as count FROM members" + ); + const [albumCount] = await pool.query( + "SELECT COUNT(*) as count FROM albums" + ); + + res.json({ + memberCount: memberCount[0].count, + albumCount: albumCount[0].count, + debutYear: 2018, + fandomName: "flover", + }); + } catch (error) { + console.error("통계 조회 오류:", error); + res.status(500).json({ error: "통계 정보를 가져오는데 실패했습니다." }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 8ac2ea1..b2490a9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,6 +2,8 @@ import express from "express"; import path from "path"; import { fileURLToPath } from "url"; import membersRouter from "./routes/members.js"; +import albumsRouter from "./routes/albums.js"; +import statsRouter from "./routes/stats.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -21,6 +23,8 @@ app.get("/api/health", (req, res) => { }); app.use("/api/members", membersRouter); +app.use("/api/albums", albumsRouter); +app.use("/api/stats", statsRouter); // SPA 폴백 - 모든 요청을 index.html로 app.get("*", (req, res) => { diff --git a/frontend/src/pages/pc/Discography.jsx b/frontend/src/pages/pc/Discography.jsx index 0ef4276..003ca5f 100644 --- a/frontend/src/pages/pc/Discography.jsx +++ b/frontend/src/pages/pc/Discography.jsx @@ -1,8 +1,54 @@ +import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { Calendar, Music } from 'lucide-react'; -import { albums } from '../../data/dummy'; function Discography() { + const [albums, setAlbums] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch('/api/albums') + .then(res => res.json()) + .then(data => { + setAlbums(data); + setLoading(false); + }) + .catch(error => { + console.error('앨범 데이터 로드 오류:', error); + setLoading(false); + }); + }, []); + + // 날짜 포맷팅 + const formatDate = (dateStr) => { + if (!dateStr) return ''; + const date = new Date(dateStr); + return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`; + }; + + // 타이틀곡 찾기 + const getTitleTrack = (tracks) => { + if (!tracks || tracks.length === 0) return ''; + const titleTrack = tracks.find(t => t.is_title_track); + return titleTrack ? titleTrack.title : tracks[0].title; + }; + + // 앨범 타입별 개수 계산 + const albumStats = { + 정규: albums.filter(a => a.album_type === '정규').length, + 미니: albums.filter(a => a.album_type === '미니').length, + 싱글: albums.filter(a => a.album_type === '싱글').length, + 총: albums.length + }; + + if (loading) { + return ( +
+
+
+ ); + } + return (
@@ -38,21 +84,21 @@ function Discography() { {/* 앨범 커버 */}
{album.title} {/* 앨범 타입 배지 */} - {album.albumType} + {album.album_type} {/* 호버 오버레이 */}
-

앨범 상세보기

+

{album.tracks?.length || 0}곡 수록

@@ -61,11 +107,11 @@ function Discography() {

{album.title}

- {album.titleTrack} + {getTitleTrack(album.tracks)}

- {album.releaseDate} + {formatDate(album.release_date)}
@@ -80,19 +126,19 @@ function Discography() { className="mt-16 grid grid-cols-4 gap-6" >
-

1

+

{albumStats.정규}

정규 앨범

-

2

+

{albumStats.미니}

미니 앨범

-

1

+

{albumStats.싱글}

싱글 앨범

-

4+

+

{albumStats.총}

총 앨범

diff --git a/frontend/src/pages/pc/Members.jsx b/frontend/src/pages/pc/Members.jsx index 8571a63..55e8bd7 100644 --- a/frontend/src/pages/pc/Members.jsx +++ b/frontend/src/pages/pc/Members.jsx @@ -4,17 +4,21 @@ import { Instagram, Calendar } from 'lucide-react'; function Members() { const [members, setMembers] = useState([]); + const [stats, setStats] = useState({ memberCount: 0, albumCount: 0, debutYear: 2018, fandomName: 'flover' }); const [loading, setLoading] = useState(true); useEffect(() => { - fetch('/api/members') - .then(res => res.json()) - .then(data => { - setMembers(data); + Promise.all([ + fetch('/api/members').then(res => res.json()), + fetch('/api/stats').then(res => res.json()) + ]) + .then(([membersData, statsData]) => { + setMembers(membersData); + setStats(statsData); setLoading(false); }) .catch(error => { - console.error('멤버 데이터 로드 오류:', error); + console.error('데이터 로드 오류:', error); setLoading(false); }); }, []); @@ -52,7 +56,7 @@ function Members() { transition={{ delay: 0.2 }} className="text-gray-500" > - 프로미스나인의 5명의 멤버를 소개합니다 + 프로미스나인의 {stats.memberCount}명의 멤버를 소개합니다
@@ -64,11 +68,11 @@ function Members() { initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1 }} - className="group" + className="group h-full" > -
+
{/* 이미지 */} -
+
{member.name} {/* 정보 */} -
+

{member.name}

-

{member.position}

+

{member.position || '\u00A0'}

@@ -91,7 +95,7 @@ function Members() { href={member.instagram} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-2 text-sm text-gray-500 hover:text-pink-500 transition-colors" + className="inline-flex items-center gap-2 text-sm text-gray-500 hover:text-pink-500 transition-colors mt-auto" > Instagram @@ -114,19 +118,19 @@ function Members() { >
-

2018

+

{stats.debutYear}

데뷔 연도

-

5

+

{stats.memberCount}

멤버 수

-

7+

+

{stats.albumCount}

앨범 수

-

flover

+

{stats.fandomName}

팬덤명

@@ -137,4 +141,3 @@ function Members() { } export default Members; -