2026-01-07 10:10:12 +09:00
|
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
import { ChevronRight, Clock, Tag } from 'lucide-react';
|
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
|
|
|
|
|
// 모바일 홈 페이지
|
|
|
|
|
function MobileHome() {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const [members, setMembers] = useState([]);
|
|
|
|
|
const [albums, setAlbums] = useState([]);
|
|
|
|
|
const [schedules, setSchedules] = useState([]);
|
|
|
|
|
|
|
|
|
|
// 데이터 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// 멤버 로드
|
|
|
|
|
fetch('/api/members')
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
.then(data => setMembers(data.filter(m => !m.is_former)))
|
|
|
|
|
.catch(console.error);
|
|
|
|
|
|
|
|
|
|
// 앨범 로드 (최신 4개)
|
|
|
|
|
fetch('/api/albums')
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
.then(data => setAlbums(data.slice(0, 2)))
|
|
|
|
|
.catch(console.error);
|
|
|
|
|
|
|
|
|
|
// 다가오는 일정 로드 (startDate + limit 방식)
|
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
|
fetch(`/api/schedules?startDate=${today}&limit=3`)
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
.then(data => setSchedules(data))
|
|
|
|
|
.catch(console.error);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-09 09:26:51 +09:00
|
|
|
<div>
|
2026-01-07 10:10:12 +09:00
|
|
|
{/* 히어로 섹션 */}
|
|
|
|
|
<section className="relative bg-gradient-to-br from-primary to-primary-dark py-12 px-4 overflow-hidden">
|
|
|
|
|
<div className="absolute inset-0 bg-black/10" />
|
|
|
|
|
<div className="relative text-center text-white">
|
|
|
|
|
<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>
|
|
|
|
|
</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" />
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* 멤버 섹션 */}
|
|
|
|
|
<section className="px-4 py-6">
|
|
|
|
|
<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) => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={member.id}
|
|
|
|
|
className="text-center"
|
|
|
|
|
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>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* 앨범 섹션 */}
|
|
|
|
|
<section className="px-4 py-6 bg-gray-50">
|
|
|
|
|
<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) => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={album.id}
|
|
|
|
|
onClick={() => navigate(`/album/${album.folder_name}`)}
|
|
|
|
|
className="bg-white rounded-xl overflow-hidden shadow-sm"
|
|
|
|
|
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>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* 일정 섹션 */}
|
2026-01-09 09:26:51 +09:00
|
|
|
<section className="px-4 py-4">
|
2026-01-07 10:10:12 +09:00
|
|
|
<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) => {
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 멤버 처리 (5명 이상이면 프로미스나인)
|
|
|
|
|
const memberList = schedule.member_names ? schedule.member_names.split(',').map(n => n.trim()).filter(Boolean) : [];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={schedule.id}
|
|
|
|
|
className="flex gap-4 bg-white p-4 rounded-xl shadow-sm border border-gray-100 overflow-hidden"
|
|
|
|
|
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>
|
|
|
|
|
{/* 시간 + 카테고리 (PC버전 스타일) */}
|
|
|
|
|
<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}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{/* 멤버 */}
|
|
|
|
|
{memberList.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
|
|
|
|
{(memberList.length >= 5 ? ['프로미스나인'] : 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>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MobileHome;
|