feat: 멤버 데이터 API 연동

- 백엔드: MariaDB 연결 설정 (lib/db.js)
- 백엔드: /api/members 라우트 추가 (routes/members.js)
- 프론트엔드: Members 페이지 API 연동
- 프론트엔드: Home 멤버 섹션 API 연동
- 로딩 상태 및 에러 처리 추가
This commit is contained in:
caadiq 2026-01-01 00:26:04 +09:00
parent 91270c2c8b
commit 6ee8e3598a
5 changed files with 102 additions and 7 deletions

15
backend/lib/db.js Normal file
View file

@ -0,0 +1,15 @@
import mysql from "mysql2/promise";
// MariaDB 연결 풀 생성
const pool = mysql.createPool({
host: process.env.DB_HOST || "mariadb",
port: parseInt(process.env.DB_PORT) || 3306,
user: process.env.DB_USER || "fromis9",
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || "fromis9",
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
export default pool;

35
backend/routes/members.js Normal file
View file

@ -0,0 +1,35 @@
import express from "express";
import pool from "../lib/db.js";
const router = express.Router();
// 전체 멤버 조회
router.get("/", async (req, res) => {
try {
const [rows] = await pool.query(
"SELECT id, name, name_en, birth_date, position, image_url, instagram FROM members ORDER BY id"
);
res.json(rows);
} catch (error) {
console.error("멤버 조회 오류:", error);
res.status(500).json({ error: "멤버 정보를 가져오는데 실패했습니다." });
}
});
// 특정 멤버 조회
router.get("/:id", async (req, res) => {
try {
const [rows] = await pool.query("SELECT * FROM members WHERE id = ?", [
req.params.id,
]);
if (rows.length === 0) {
return res.status(404).json({ error: "멤버를 찾을 수 없습니다." });
}
res.json(rows[0]);
} catch (error) {
console.error("멤버 조회 오류:", error);
res.status(500).json({ error: "멤버 정보를 가져오는데 실패했습니다." });
}
});
export default router;

View file

@ -1,6 +1,7 @@
import express from "express"; import express from "express";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import membersRouter from "./routes/members.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -14,11 +15,13 @@ app.use(express.json());
// 정적 파일 서빙 (프론트엔드 빌드 결과물) // 정적 파일 서빙 (프론트엔드 빌드 결과물)
app.use(express.static(path.join(__dirname, "dist"))); app.use(express.static(path.join(__dirname, "dist")));
// API 라우트 (추후 구현) // API 라우트
app.get("/api/health", (req, res) => { app.get("/api/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() }); res.json({ status: "ok", timestamp: new Date().toISOString() });
}); });
app.use("/api/members", membersRouter);
// SPA 폴백 - 모든 요청을 index.html로 // SPA 폴백 - 모든 요청을 index.html로
app.get("*", (req, res) => { app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "dist", "index.html")); res.sendFile(path.join(__dirname, "dist", "index.html"));

View file

@ -1,9 +1,19 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Calendar, Users, Disc3, ArrowRight } from 'lucide-react'; import { Calendar, Users, Disc3, ArrowRight } from 'lucide-react';
import { members, schedules, albums } from '../../data/dummy'; import { schedules, albums } from '../../data/dummy';
function Home() { function Home() {
const [members, setMembers] = useState([]);
useEffect(() => {
fetch('/api/members')
.then(res => res.json())
.then(data => setMembers(data))
.catch(error => console.error('멤버 데이터 로드 오류:', error));
}, []);
return ( return (
<div> <div>
{/* 히어로 섹션 */} {/* 히어로 섹션 */}
@ -92,14 +102,14 @@ function Home() {
> >
<div className="aspect-square bg-gray-100"> <div className="aspect-square bg-gray-100">
<img <img
src={member.imageUrl} src={member.image_url}
alt={member.name} alt={member.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
</div> </div>
<div className="p-4 text-center"> <div className="p-4 text-center">
<h3 className="font-bold text-lg">{member.name}</h3> <h3 className="font-bold text-lg">{member.name}</h3>
<p className="text-sm text-gray-500">{member.position.split(',')[0]}</p> <p className="text-sm text-gray-500">{member.position?.split(',')[0]}</p>
</div> </div>
</motion.div> </motion.div>
))} ))}

View file

@ -1,8 +1,39 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Instagram, Calendar } from 'lucide-react'; import { Instagram, Calendar } from 'lucide-react';
import { members } from '../../data/dummy';
function Members() { function Members() {
const [members, setMembers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/members')
.then(res => res.json())
.then(data => {
setMembers(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')}`;
};
if (loading) {
return (
<div className="py-16 flex justify-center items-center min-h-[60vh]">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
</div>
);
}
return ( return (
<div className="py-16"> <div className="py-16">
<div className="max-w-7xl mx-auto px-6"> <div className="max-w-7xl mx-auto px-6">
@ -39,7 +70,7 @@ function Members() {
{/* 이미지 */} {/* 이미지 */}
<div className="aspect-[3/4] bg-gray-100 overflow-hidden"> <div className="aspect-[3/4] bg-gray-100 overflow-hidden">
<img <img
src={member.imageUrl} src={member.image_url}
alt={member.name} alt={member.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/> />
@ -52,7 +83,7 @@ function Members() {
<div className="flex items-center gap-2 text-sm text-gray-500 mb-4"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-4">
<Calendar size={14} /> <Calendar size={14} />
<span>{member.birthDate}</span> <span>{formatDate(member.birth_date)}</span>
</div> </div>
{/* 인스타그램 링크 */} {/* 인스타그램 링크 */}
@ -106,3 +137,4 @@ function Members() {
} }
export default Members; export default Members;