feat(frontend): Phase 7 - 홈 페이지 및 App.jsx 구현

- PC/Mobile 홈 페이지 분리 구현
- App.jsx: react-device-detect (BrowserView/MobileView) 사용
- PCWrapper: body.is-pc 클래스 추가
- 커스텀 훅 사용 (useMembers, useAlbums, useUpcomingSchedules)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 20:18:15 +09:00
parent 86bf2359f2
commit edde52b06e
5 changed files with 659 additions and 132 deletions

View file

@ -1,144 +1,83 @@
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { useEffect } from 'react';
import { cn, getTodayKST, formatFullDate } from "@/utils"; import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { useAuthStore, useUIStore } from "@/stores"; import { BrowserView, MobileView } from 'react-device-detect';
import { useIsMobile, useCategories, useCalendar } from "@/hooks";
import { memberApi } from "@/api"; //
import { useQuery } from "@tanstack/react-query"; import { ScrollToTop } from '@/components/common';
// PC
import { Layout as PCLayout } from '@/components/pc';
// Mobile
import { Layout as MobileLayout } from '@/components/mobile';
//
import { PCHome, MobileHome } from '@/pages/home';
/**
* PC 환경에서 body에 클래스 추가하는 래퍼
*/
function PCWrapper({ children }) {
useEffect(() => {
document.body.classList.add('is-pc');
return () => document.body.classList.remove('is-pc');
}, []);
return children;
}
/** /**
* 프로미스나인 팬사이트 메인 * 프로미스나인 팬사이트 메인
* * react-device-detect를 사용한 PC/Mobile 분리
* Phase 5: 커스텀 완료
* - useMediaQuery, useIsMobile, useIsDesktop
* - useScheduleData, useScheduleDetail, useCategories
* - useScheduleSearch (무한 스크롤)
* - useScheduleFiltering, useCategoryCounts
* - useCalendar
* - useAdminAuth
*/ */
function App() { function App() {
const today = getTodayKST();
const isMobile = useIsMobile();
const { isAuthenticated } = useAuthStore();
const { showSuccess, toasts } = useUIStore();
//
const { data: categories, isLoading: categoriesLoading } = useCategories();
const calendar = useCalendar();
// ( )
const { data: members, isLoading: membersLoading } = useQuery({
queryKey: ["members"],
queryFn: memberApi.getMembers,
});
return ( return (
<BrowserRouter> <BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ScrollToTop />
{/* PC 뷰 */}
<BrowserView>
<PCWrapper>
<Routes>
{/* 관리자 페이지 (레이아웃 없음) - 추후 추가 */}
{/* <Route path="/admin" element={<AdminLogin />} /> */}
{/* 일반 페이지 (레이아웃 포함) */}
<Route
path="/*"
element={
<PCLayout>
<Routes>
<Route path="/" element={<PCHome />} />
{/* 추가 페이지는 Phase 8-11에서 구현 */}
{/* <Route path="/members" element={<PCMembers />} /> */}
{/* <Route path="/album" element={<PCAlbum />} /> */}
{/* <Route path="/schedule" element={<PCSchedule />} /> */}
{/* <Route path="*" element={<PCNotFound />} /> */}
</Routes>
</PCLayout>
}
/>
</Routes>
</PCWrapper>
</BrowserView>
{/* Mobile 뷰 */}
<MobileView>
<Routes> <Routes>
<Route <Route
path="/" path="/"
element={ element={
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-4"> <MobileLayout>
<div className="text-center space-y-4 max-w-md w-full"> <MobileHome />
<h1 className="text-2xl font-bold text-primary mb-2"> </MobileLayout>
fromis_9 Frontend Refactoring
</h1>
<p className="text-gray-600">Phase 5 완료 - 커스텀 </p>
<p className={cn("text-sm", isMobile ? "text-blue-500" : "text-green-500")}>
디바이스: {isMobile ? "모바일" : "PC"} (useIsMobile )
</p>
<div className="mt-6 p-4 bg-white rounded-lg shadow text-left text-sm space-y-3">
<p><strong>오늘:</strong> {formatFullDate(today)}</p>
<p><strong>인증:</strong> {isAuthenticated ? "✅" : "❌"}</p>
<div className="border-t pt-3">
<p className="font-semibold mb-2">useCategories </p>
<p>
{categoriesLoading ? "로딩 중..." : `${categories?.length || 0}개 카테고리`}
</p>
{categories && (
<div className="flex flex-wrap gap-1 mt-1">
{categories.map((c) => (
<span
key={c.id}
className="px-2 py-0.5 rounded text-xs text-white"
style={{ backgroundColor: c.color }}
>
{c.name}
</span>
))}
</div>
)}
</div>
<div className="border-t pt-3">
<p className="font-semibold mb-2">useCalendar </p>
<p><strong>현재:</strong> {calendar.year} {calendar.monthName}</p>
<p><strong>선택:</strong> {calendar.selectedDate}</p>
<div className="flex gap-2 mt-2">
<button
onClick={calendar.goToPrevMonth}
disabled={!calendar.canGoPrevMonth}
className={cn(
"px-3 py-1 rounded text-xs",
calendar.canGoPrevMonth ? "bg-primary text-white" : "bg-gray-200 text-gray-400"
)}
>
이전
</button>
<button
onClick={calendar.goToToday}
className="px-3 py-1 bg-gray-500 text-white rounded text-xs"
>
오늘
</button>
<button
onClick={calendar.goToNextMonth}
className="px-3 py-1 bg-primary text-white rounded text-xs"
>
다음
</button>
</div>
</div>
<div className="border-t pt-3">
<p className="font-semibold mb-2">멤버 데이터</p>
<p>{membersLoading ? "로딩 중..." : `${members?.length || 0}`}</p>
{members && (
<div className="flex flex-wrap gap-1 mt-1">
{members.map((m) => (
<span key={m.id} className="px-2 py-0.5 bg-gray-100 rounded text-xs">
{m.name}
</span>
))}
</div>
)}
</div>
</div>
</div>
{/* 토스트 표시 */}
<div className="fixed bottom-4 right-4 space-y-2">
{toasts.map((toast) => (
<div
key={toast.id}
className={cn(
"px-4 py-2 rounded shadow-lg text-white text-sm",
toast.type === "success" && "bg-green-500",
toast.type === "error" && "bg-red-500",
toast.type === "warning" && "bg-yellow-500",
toast.type === "info" && "bg-blue-500"
)}
>
{toast.message}
</div>
))}
</div>
</div>
} }
/> />
{/* 추가 페이지는 Phase 8-11에서 구현 */}
{/* <Route path="/members" element={<MobileLayout pageTitle="멤버"><MobileMembers /></MobileLayout>} /> */}
{/* <Route path="/album" element={<MobileLayout pageTitle="앨범"><MobileAlbum /></MobileLayout>} /> */}
{/* <Route path="/schedule" element={<MobileLayout useCustomLayout><MobileSchedule /></MobileLayout>} /> */}
</Routes> </Routes>
</MobileView>
</BrowserRouter> </BrowserRouter>
); );
} }

View file

@ -0,0 +1,2 @@
export { default as PCHome } from './pc/Home';
export { default as MobileHome } from './mobile/Home';

View file

@ -0,0 +1,266 @@
import { motion } from 'framer-motion';
import { ChevronRight, Clock, Tag } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
/**
* Mobile 페이지
*/
function MobileHome() {
const navigate = useNavigate();
// ( )
const { data: allMembers = [] } = useMembers();
const members = allMembers.filter((m) => !m.is_former);
// ( 2)
const { data: allAlbums = [] } = useAlbums();
const albums = allAlbums.slice(0, 2);
// (3)
const { data: schedules = [] } = useUpcomingSchedules(3);
return (
<div>
{/* 히어로 섹션 */}
<motion.section
className="relative bg-gradient-to-br from-primary to-primary-dark py-12 px-4 overflow-hidden"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<div className="absolute inset-0 bg-black/10" />
<motion.div
className="relative text-center text-white"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.5 }}
>
<h1 className="text-3xl font-bold mb-1">fromis_9</h1>
<p className="text-lg font-light mb-3">프로미스나인</p>
<p className="text-sm opacity-80 leading-relaxed">
인사드리겠습니다. , !
<br />
이제는 약속해 소중히 간직해,
<br />
당신의 아이돌로 성장하겠습니다!
</p>
</motion.div>
{/* 장식 */}
<div className="absolute right-0 top-0 w-32 h-32 rounded-full bg-white/10 -translate-y-1/2 translate-x-1/2" />
<div className="absolute left-0 bottom-0 w-24 h-24 rounded-full bg-white/5 translate-y-1/2 -translate-x-1/2" />
</motion.section>
{/* 멤버 섹션 */}
<motion.section
className="px-4 py-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.5 }}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">멤버</h2>
<button
onClick={() => navigate('/members')}
className="text-primary text-sm flex items-center gap-1"
>
전체보기 <ChevronRight size={16} />
</button>
</div>
<div className="grid grid-cols-5 gap-2">
{members.map((member, index) => (
<motion.div
key={member.id}
className="text-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4 + index * 0.05, duration: 0.3 }}
whileTap={{ scale: 0.95 }}
>
<div className="aspect-square rounded-full overflow-hidden bg-gray-200 mb-1">
{member.image_url && (
<img
src={member.image_url}
alt={member.name}
className="w-full h-full object-cover"
/>
)}
</div>
<p className="text-xs font-medium truncate">{member.name}</p>
</motion.div>
))}
</div>
</motion.section>
{/* 앨범 섹션 */}
<motion.section
className="px-4 py-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5, duration: 0.5 }}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">앨범</h2>
<button
onClick={() => navigate('/album')}
className="text-primary text-sm flex items-center gap-1"
>
전체보기 <ChevronRight size={16} />
</button>
</div>
<div className="grid grid-cols-2 gap-3">
{albums.map((album, index) => (
<motion.div
key={album.id}
onClick={() => navigate(`/album/${album.folder_name}`)}
className="bg-white rounded-xl overflow-hidden shadow-md"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 + index * 0.1, duration: 0.3 }}
whileTap={{ scale: 0.98 }}
>
<div className="aspect-square bg-gray-200">
{album.cover_thumb_url && (
<img
src={album.cover_thumb_url}
alt={album.title}
className="w-full h-full object-cover"
/>
)}
</div>
<div className="p-3">
<p className="font-medium text-sm truncate">{album.title}</p>
<p className="text-xs text-gray-400">
{album.release_date?.slice(0, 4)}
</p>
</div>
</motion.div>
))}
</div>
</motion.section>
{/* 일정 섹션 */}
<motion.section
className="px-4 py-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7, duration: 0.5 }}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">다가오는 일정</h2>
<button
onClick={() => navigate('/schedule')}
className="text-primary text-sm flex items-center gap-1"
>
전체보기 <ChevronRight size={16} />
</button>
</div>
{schedules.length > 0 ? (
<div className="space-y-3">
{schedules.map((schedule, index) => {
const scheduleDate = new Date(schedule.date);
const today = new Date();
const currentYear = today.getFullYear();
const currentMonth = today.getMonth();
const scheduleYear = scheduleDate.getFullYear();
const scheduleMonth = scheduleDate.getMonth();
const isCurrentYear = scheduleYear === currentYear;
const isCurrentMonth =
isCurrentYear && scheduleMonth === currentMonth;
//
const memberList = schedule.member_names
? schedule.member_names
.split(',')
.map((n) => n.trim())
.filter(Boolean)
: schedule.members?.map((m) => m.name) || [];
return (
<motion.div
key={schedule.id}
className="flex gap-4 bg-white p-4 rounded-xl shadow-sm border border-gray-100 overflow-hidden"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8 + index * 0.1, duration: 0.3 }}
whileTap={{ scale: 0.98 }}
onClick={() => navigate('/schedule')}
>
{/* 날짜 영역 */}
<div className="flex flex-col items-center justify-center min-w-[50px]">
{!isCurrentYear && (
<span className="text-[10px] text-gray-400 font-medium">
{scheduleYear}.{scheduleMonth + 1}
</span>
)}
{isCurrentYear && !isCurrentMonth && (
<span className="text-[10px] text-gray-400 font-medium">
{scheduleMonth + 1}
</span>
)}
<span className="text-2xl font-bold text-primary">
{scheduleDate.getDate()}
</span>
<span className="text-xs text-gray-400 font-medium">
{
['일', '월', '화', '수', '목', '금', '토'][
scheduleDate.getDay()
]
}
</span>
</div>
{/* 세로 구분선 */}
<div className="w-px bg-gray-100" />
{/* 내용 영역 */}
<div className="flex-1 min-w-0">
<p className="font-semibold text-sm text-gray-800 line-clamp-2 leading-snug">
{schedule.title}
</p>
{/* 시간 + 카테고리 */}
<div className="flex items-center gap-3 mt-2 text-xs text-gray-400">
{schedule.time && (
<span className="flex items-center gap-1">
<Clock size={12} />
{schedule.time.slice(0, 5)}
</span>
)}
{schedule.category_name && (
<span className="flex items-center gap-1">
<Tag size={12} />
{schedule.category_name}
{schedule.source?.name && ` · ${schedule.source.name}`}
</span>
)}
</div>
{/* 멤버 */}
{memberList.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{memberList.map((name, i) => (
<span
key={i}
className="px-2 py-0.5 bg-primary/10 text-primary text-[10px] rounded-full font-medium"
>
{name.trim()}
</span>
))}
</div>
)}
</div>
</motion.div>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-400">
<p>다가오는 일정이 없습니다</p>
</div>
)}
</motion.section>
</div>
);
}
export default MobileHome;

View file

@ -0,0 +1,318 @@
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { Calendar, ArrowRight, Clock, Tag, Music } from 'lucide-react';
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
/**
* PC 페이지
*/
function Home() {
//
const { data: members = [] } = useMembers();
// ( 4)
const { data: allAlbums = [] } = useAlbums();
const albums = allAlbums.slice(0, 4);
// (3)
const { data: upcomingSchedules = [] } = useUpcomingSchedules(3);
// D+Day
const debutDate = new Date('2018-01-24');
const today = new Date();
const dDay = Math.floor((today - debutDate) / (1000 * 60 * 60 * 24)) + 1;
return (
<div>
{/* 히어로 섹션 */}
<section className="relative h-[600px] bg-gradient-to-br from-primary to-primary-dark overflow-hidden">
<div className="absolute inset-0 bg-black/20" />
<div className="relative max-w-7xl mx-auto px-6 h-full flex items-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-white"
>
<h1 className="text-6xl font-bold mb-4">fromis_9</h1>
<p className="text-2xl font-light mb-2">프로미스나인</p>
<p className="text-lg opacity-80 leading-relaxed">
인사드리겠습니다. , !
<br />
이제는 약속해 소중히 간직해,
<br />
당신의 아이돌로 성장하겠습니다!
</p>
</motion.div>
</div>
{/* 장식 */}
<div className="absolute right-0 bottom-0 w-1/2 h-full opacity-10">
<div className="absolute right-10 top-20 w-64 h-64 rounded-full bg-white/30" />
<div className="absolute right-40 bottom-20 w-48 h-48 rounded-full bg-white/20" />
</div>
</section>
{/* 그룹 통계 */}
<section className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6">
<motion.div
className="grid grid-cols-4 gap-6"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
variants={{
hidden: { opacity: 1 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
}}
>
{[
{ value: '2018.01.24', label: '데뷔일' },
{ value: `D+${dDay.toLocaleString()}`, label: 'D+Day' },
{ value: '5', label: '멤버 수' },
{ value: 'flover', label: '팬덤명' },
].map((stat, index) => (
<motion.div
key={index}
variants={{
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: 'easeOut' },
},
}}
className="bg-gradient-to-br from-primary to-primary-dark rounded-2xl p-6 text-white text-center"
>
<p className="text-3xl font-bold mb-1">{stat.value}</p>
<p className="text-white/70 text-sm">{stat.label}</p>
</motion.div>
))}
</motion.div>
</div>
</section>
{/* 멤버 미리보기 */}
<section className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6">
<div className="flex justify-between items-center mb-8">
<h2 className="text-3xl font-bold">멤버</h2>
<Link
to="/members"
className="text-primary hover:underline flex items-center gap-1"
>
전체보기 <ArrowRight size={16} />
</Link>
</div>
<div className="grid grid-cols-5 gap-6">
{members
.filter((m) => !m.is_former)
.map((member, index) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3 + index * 0.1,
duration: 0.5,
ease: 'easeOut',
}}
className="group relative rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300"
>
<div className="aspect-[3/4] overflow-hidden">
<img
src={member.image_url}
alt={member.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-60 group-hover:opacity-90 transition-opacity duration-300" />
<div className="absolute bottom-0 left-0 right-0 p-5 text-white">
<h3 className="font-bold text-xl drop-shadow-lg">
{member.name}
</h3>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* 앨범 미리보기 */}
<section className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6">
<div className="flex justify-between items-center mb-8">
<h2 className="text-3xl font-bold">앨범</h2>
<Link
to="/album"
className="text-primary hover:underline flex items-center gap-1"
>
전체보기 <ArrowRight size={16} />
</Link>
</div>
<div className="grid grid-cols-4 gap-6">
{albums.map((album, index) => (
<motion.div
key={album.id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3 + index * 0.1,
duration: 0.5,
ease: 'easeOut',
}}
className="group cursor-pointer"
onClick={() =>
(window.location.href = `/album/${encodeURIComponent(album.title)}`)
}
>
<div className="relative aspect-square rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-300">
<img
src={album.cover_medium_url || album.cover_original_url}
alt={album.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<div className="text-center text-white">
<Music size={32} className="mx-auto mb-2" />
<p className="text-sm">{album.tracks?.length || 0} 수록</p>
</div>
</div>
</div>
<div className="mt-3">
<h3 className="font-bold text-lg truncate">{album.title}</h3>
<p className="text-gray-500 text-sm">
{album.release_date?.slice(0, 4)}
</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* 일정 미리보기 */}
<section className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6">
<div className="flex justify-between items-center mb-8">
<h2 className="text-3xl font-bold">다가오는 일정</h2>
<Link
to="/schedule"
className="text-primary hover:underline flex items-center gap-1"
>
전체보기 <ArrowRight size={16} />
</Link>
</div>
{upcomingSchedules.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<Calendar size={48} className="mx-auto mb-4 opacity-30" />
<p>예정된 일정이 없습니다</p>
</div>
) : (
<motion.div
className="space-y-4"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
variants={{
hidden: { opacity: 1 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
}}
>
{upcomingSchedules.map((schedule) => {
const scheduleDate = new Date(schedule.date);
const todayDate = new Date();
const currentYear = todayDate.getFullYear();
const currentMonth = todayDate.getMonth();
const scheduleYear = scheduleDate.getFullYear();
const scheduleMonth = scheduleDate.getMonth();
const isCurrentYear = scheduleYear === currentYear;
const isCurrentMonth =
isCurrentYear && scheduleMonth === currentMonth;
const day = scheduleDate.getDate();
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
const weekday = weekdays[scheduleDate.getDay()];
//
const memberList = schedule.member_names
? schedule.member_names.split(',')
: schedule.members?.map((m) => m.name) || [];
const categoryColor = schedule.category_color || '#6366f1';
return (
<motion.div
key={schedule.id}
variants={{
hidden: { opacity: 0, x: -30 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.4, ease: 'easeOut' },
},
}}
className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer"
>
{/* 날짜 영역 */}
<div
className="w-24 flex flex-col items-center justify-center text-white py-6"
style={{ backgroundColor: categoryColor }}
>
{!isCurrentYear && (
<span className="text-xs font-medium opacity-70">
{scheduleYear}.{scheduleMonth + 1}
</span>
)}
{isCurrentYear && !isCurrentMonth && (
<span className="text-xs font-medium opacity-70">
{scheduleMonth + 1}
</span>
)}
<span className="text-3xl font-bold">{day}</span>
<span className="text-sm font-medium opacity-80">
{weekday}
</span>
</div>
{/* 내용 영역 */}
<div className="flex-1 p-6 flex flex-col justify-center">
<h3 className="font-bold text-lg mb-2">{schedule.title}</h3>
<div className="flex flex-wrap gap-3 text-base text-gray-500">
{schedule.time && (
<span className="flex items-center gap-1">
<Clock size={16} className="opacity-60" />
{schedule.time.slice(0, 5)}
</span>
)}
<span className="flex items-center gap-1">
<Tag size={16} className="opacity-60" />
{schedule.category_name}
{schedule.source?.name && ` · ${schedule.source.name}`}
</span>
</div>
{memberList.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{memberList.map((name, i) => (
<span
key={i}
className="px-2 py-0.5 bg-primary/10 text-primary text-sm font-medium rounded-full"
>
{name.trim()}
</span>
))}
</div>
)}
</div>
</motion.div>
);
})}
</motion.div>
)}
</div>
</section>
</div>
);
}
export default Home;

View file

@ -0,0 +1,2 @@
// 홈 페이지
export * from './home';