웹: useEffect를 useQuery로 리팩토링 (PC/모바일 공개 페이지)

This commit is contained in:
caadiq 2026-01-12 15:46:34 +09:00
parent a89139f056
commit 990d360520
6 changed files with 347 additions and 321 deletions

View file

@ -1,22 +1,18 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getAlbums } from '../../../api/public/albums'; import { getAlbums } from '../../../api/public/albums';
// //
function MobileAlbum() { function MobileAlbum() {
const navigate = useNavigate(); const navigate = useNavigate();
const [albums, setAlbums] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => { // useQuery
getAlbums() const { data: albums = [], isLoading: loading } = useQuery({
.then(data => { queryKey: ['albums'],
setAlbums(data); queryFn: getAlbums,
setLoading(false); });
})
.catch(console.error);
}, []);
if (loading) { if (loading) {
return ( return (

View file

@ -1,6 +1,6 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { ChevronRight, Clock, Tag } from 'lucide-react'; import { ChevronRight, Clock, Tag } from 'lucide-react';
import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getTodayKST } from '../../../utils/date'; import { getTodayKST } from '../../../utils/date';
import { getMembers } from '../../../api/public/members'; import { getMembers } from '../../../api/public/members';
@ -10,27 +10,27 @@ import { getUpcomingSchedules } from '../../../api/public/schedules';
// //
function MobileHome() { function MobileHome() {
const navigate = useNavigate(); const navigate = useNavigate();
const [members, setMembers] = useState([]);
const [albums, setAlbums] = useState([]);
const [schedules, setSchedules] = useState([]);
// // useQuery ( )
useEffect(() => { const { data: members = [] } = useQuery({
// queryKey: ['members'],
getMembers() queryFn: getMembers,
.then(data => setMembers(data.filter(m => !m.is_former))) select: (data) => data.filter(m => !m.is_former),
.catch(console.error); });
// ( 2) // useQuery ( 2)
getAlbums() const { data: albums = [] } = useQuery({
.then(data => setAlbums(data.slice(0, 2))) queryKey: ['albums'],
.catch(console.error); queryFn: getAlbums,
select: (data) => data.slice(0, 2),
});
// useQuery
const { data: schedules = [] } = useQuery({
queryKey: ['upcomingSchedules', 3],
queryFn: () => getUpcomingSchedules(3),
});
//
getUpcomingSchedules(3)
.then(data => setSchedules(data))
.catch(console.error);
}, []);
return ( return (
<div> <div>

View file

@ -1,22 +1,23 @@
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react'; import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Instagram, Calendar, X } from 'lucide-react'; import { Instagram, Calendar, X } from 'lucide-react';
import { getMembers } from '../../../api/public/members'; import { getMembers } from '../../../api/public/members';
// //
function MobileMembers() { function MobileMembers() {
const [members, setMembers] = useState([]);
const [formerMembers, setFormerMembers] = useState([]);
const [selectedMember, setSelectedMember] = useState(null); const [selectedMember, setSelectedMember] = useState(null);
useEffect(() => { // useQuery
getMembers() const { data: allMembers = [] } = useQuery({
.then(data => { queryKey: ['members'],
setMembers(data.filter(m => !m.is_former)); queryFn: getMembers,
setFormerMembers(data.filter(m => m.is_former)); });
})
.catch(console.error); // useMemo /
}, []); const members = useMemo(() => allMembers.filter(m => !m.is_former), [allMembers]);
const formerMembers = useMemo(() => allMembers.filter(m => m.is_former), [allMembers]);
// //
const calculateAge = (birthDate) => { const calculateAge = (birthDate) => {

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Calendar, Music } from 'lucide-react'; import { Calendar, Music } from 'lucide-react';
@ -7,20 +7,13 @@ import { formatDate } from '../../../utils/date';
function Album() { function Album() {
const navigate = useNavigate(); const navigate = useNavigate();
const [albums, setAlbums] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => { // useQuery
getAlbums() const { data: albums = [], isLoading: loading } = useQuery({
.then(data => { queryKey: ['albums'],
setAlbums(data); queryFn: getAlbums,
setLoading(false);
})
.catch(error => {
console.error('앨범 데이터 로드 오류:', error);
setLoading(false);
}); });
}, []);
// //
const getTitleTrack = (tracks) => { const getTitleTrack = (tracks) => {

View file

@ -1,26 +1,24 @@
import { useState, useEffect } from 'react'; import { useState } from "react";
import { motion } from 'framer-motion'; import { useQuery } from "@tanstack/react-query";
import { Link } from 'react-router-dom'; import { motion } from "framer-motion";
import { Calendar, ArrowRight, Clock, Link2, Tag } from 'lucide-react'; import { Link } from "react-router-dom";
import { getTodayKST } from '../../../utils/date'; import { Calendar, ArrowRight, Clock, Link2, Tag } from "lucide-react";
import { getMembers } from '../../../api/public/members'; import { getTodayKST } from "../../../utils/date";
import { getUpcomingSchedules } from '../../../api/public/schedules'; import { getMembers } from "../../../api/public/members";
import { getUpcomingSchedules } from "../../../api/public/schedules";
function Home() { function Home() {
const [members, setMembers] = useState([]); // useQuery
const [upcomingSchedules, setUpcomingSchedules] = useState([]); const { data: members = [] } = useQuery({
queryKey: ["members"],
queryFn: getMembers,
});
useEffect(() => { // useQuery ( 3)
// const { data: upcomingSchedules = [] } = useQuery({
getMembers() queryKey: ["upcomingSchedules", 3],
.then(data => setMembers(data)) queryFn: () => getUpcomingSchedules(3),
.catch(error => console.error('멤버 데이터 로드 오류:', error)); });
// ( 3)
getUpcomingSchedules(3)
.then(data => setUpcomingSchedules(data))
.catch(error => console.error('일정 데이터 로드 오류:', error));
}, []);
return ( return (
<div> <div>
@ -37,8 +35,10 @@ function Home() {
<h1 className="text-6xl font-bold mb-4">fromis_9</h1> <h1 className="text-6xl font-bold mb-4">fromis_9</h1>
<p className="text-2xl font-light mb-2">프로미스나인</p> <p className="text-2xl font-light mb-2">프로미스나인</p>
<p className="text-lg opacity-80 mb-8 leading-relaxed"> <p className="text-lg opacity-80 mb-8 leading-relaxed">
인사드리겠습니다. , !<br /> 인사드리겠습니다. , !
이제는 약속해 소중히 간직해,<br /> <br />
이제는 약속해 소중히 간직해,
<br />
당신의 아이돌로 성장하겠습니다! 당신의 아이돌로 성장하겠습니다!
</p> </p>
<Link <Link
@ -68,20 +68,32 @@ function Home() {
viewport={{ once: true, amount: 0.1 }} viewport={{ once: true, amount: 0.1 }}
variants={{ variants={{
hidden: { opacity: 1 }, hidden: { opacity: 1 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } } visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
}} }}
> >
{[ {[
{ value: "2018.01.24", label: "데뷔일" }, { value: "2018.01.24", label: "데뷔일" },
{ value: `D+${(Math.floor((new Date() - new Date('2018-01-24')) / (1000 * 60 * 60 * 24)) + 1).toLocaleString()}`, label: "D+Day" }, {
value: `D+${(
Math.floor(
(new Date() - new Date("2018-01-24")) /
(1000 * 60 * 60 * 24)
) + 1
).toLocaleString()}`,
label: "D+Day",
},
{ value: "5", label: "멤버 수" }, { value: "5", label: "멤버 수" },
{ value: "flover", label: "팬덤명" } { value: "flover", label: "팬덤명" },
].map((stat, index) => ( ].map((stat, index) => (
<motion.div <motion.div
key={index} key={index}
variants={{ variants={{
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } } 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" className="bg-gradient-to-br from-primary to-primary-dark rounded-2xl p-6 text-white text-center"
> >
@ -98,17 +110,26 @@ function Home() {
<div className="max-w-7xl mx-auto px-6"> <div className="max-w-7xl mx-auto px-6">
<div className="flex justify-between items-center mb-8"> <div className="flex justify-between items-center mb-8">
<h2 className="text-3xl font-bold">멤버</h2> <h2 className="text-3xl font-bold">멤버</h2>
<Link to="/members" className="text-primary hover:underline flex items-center gap-1"> <Link
to="/members"
className="text-primary hover:underline flex items-center gap-1"
>
전체보기 <ArrowRight size={16} /> 전체보기 <ArrowRight size={16} />
</Link> </Link>
</div> </div>
<div className="grid grid-cols-5 gap-6"> <div className="grid grid-cols-5 gap-6">
{members.filter(m => !m.is_former).map((member, index) => ( {members
.filter((m) => !m.is_former)
.map((member, index) => (
<motion.div <motion.div
key={member.id} key={member.id}
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.1, duration: 0.5, ease: "easeOut" }} 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" className="group relative rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300"
> >
{/* 이미지 컨테이너 */} {/* 이미지 컨테이너 */}
@ -125,7 +146,9 @@ function Home() {
{/* 멤버 정보 */} {/* 멤버 정보 */}
<div className="absolute bottom-0 left-0 right-0 p-5 text-white"> <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> <h3 className="font-bold text-xl drop-shadow-lg">
{member.name}
</h3>
</div> </div>
</motion.div> </motion.div>
))} ))}
@ -138,7 +161,10 @@ function Home() {
<div className="max-w-7xl mx-auto px-6"> <div className="max-w-7xl mx-auto px-6">
<div className="flex justify-between items-center mb-8"> <div className="flex justify-between items-center mb-8">
<h2 className="text-3xl font-bold">다가오는 일정</h2> <h2 className="text-3xl font-bold">다가오는 일정</h2>
<Link to="/schedule" className="text-primary hover:underline flex items-center gap-1"> <Link
to="/schedule"
className="text-primary hover:underline flex items-center gap-1"
>
전체보기 <ArrowRight size={16} /> 전체보기 <ArrowRight size={16} />
</Link> </Link>
</div> </div>
@ -155,7 +181,7 @@ function Home() {
viewport={{ once: true, amount: 0.1 }} viewport={{ once: true, amount: 0.1 }}
variants={{ variants={{
hidden: { opacity: 1 }, hidden: { opacity: 1 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } } visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
}} }}
> >
{upcomingSchedules.map((schedule) => { {upcomingSchedules.map((schedule) => {
@ -167,22 +193,30 @@ function Home() {
const scheduleYear = scheduleDate.getFullYear(); const scheduleYear = scheduleDate.getFullYear();
const scheduleMonth = scheduleDate.getMonth(); const scheduleMonth = scheduleDate.getMonth();
const isCurrentYear = scheduleYear === currentYear; const isCurrentYear = scheduleYear === currentYear;
const isCurrentMonth = isCurrentYear && scheduleMonth === currentMonth; const isCurrentMonth =
isCurrentYear && scheduleMonth === currentMonth;
const day = scheduleDate.getDate(); const day = scheduleDate.getDate();
const weekdays = ['일', '월', '화', '수', '목', '금', '토']; const weekdays = ["일", "월", "화", "수", "목", "금", "토"];
const weekday = weekdays[scheduleDate.getDay()]; const weekday = weekdays[scheduleDate.getDay()];
// //
const memberList = schedule.member_names ? schedule.member_names.split(',') : []; const memberList = schedule.member_names
const displayMembers = memberList.length >= 5 ? ['프로미스나인'] : memberList; ? schedule.member_names.split(",")
: [];
const displayMembers =
memberList.length >= 5 ? ["프로미스나인"] : memberList;
return ( return (
<motion.div <motion.div
key={schedule.id} key={schedule.id}
variants={{ variants={{
hidden: { opacity: 0, x: -30 }, hidden: { opacity: 0, x: -30 },
visible: { opacity: 1, x: 0, transition: { duration: 0.4, ease: "easeOut" } } 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" className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden"
> >
@ -201,29 +235,42 @@ function Home() {
</span> </span>
)} )}
<span className="text-3xl font-bold">{day}</span> <span className="text-3xl font-bold">{day}</span>
<span className="text-sm font-medium opacity-80">{weekday}</span> <span className="text-sm font-medium opacity-80">
{weekday}
</span>
</div> </div>
{/* 내용 영역 */} {/* 내용 영역 */}
<div className="flex-1 p-5 flex flex-col justify-center"> <div className="flex-1 p-5 flex flex-col justify-center">
<h3 className="font-bold text-lg text-gray-900 mb-2">{schedule.title}</h3> <h3 className="font-bold text-lg text-gray-900 mb-2">
{schedule.title}
</h3>
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-500"> <div className="flex flex-wrap items-center gap-3 text-sm text-gray-500">
{schedule.time && ( {schedule.time && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Clock size={14} className="text-primary opacity-60" /> <Clock
size={14}
className="text-primary opacity-60"
/>
<span>{schedule.time.slice(0, 5)}</span> <span>{schedule.time.slice(0, 5)}</span>
</div> </div>
)} )}
{schedule.category_name && ( {schedule.category_name && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Tag size={14} className="text-primary opacity-60" /> <Tag
size={14}
className="text-primary opacity-60"
/>
<span>{schedule.category_name}</span> <span>{schedule.category_name}</span>
</div> </div>
)} )}
{schedule.source_name && ( {schedule.source_name && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Link2 size={14} className="text-primary opacity-60" /> <Link2
size={14}
className="text-primary opacity-60"
/>
<span>{schedule.source_name}</span> <span>{schedule.source_name}</span>
</div> </div>
)} )}
@ -248,8 +295,6 @@ function Home() {
})} })}
</motion.div> </motion.div>
)} )}
</div> </div>
</section> </section>
</div> </div>
@ -257,4 +302,3 @@ function Home() {
} }
export default Home; export default Home;

View file

@ -1,24 +1,16 @@
import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Instagram, Calendar } from 'lucide-react'; import { Instagram, Calendar } from 'lucide-react';
import { getMembers } from '../../../api/public/members'; import { getMembers } from '../../../api/public/members';
import { formatDate } from '../../../utils/date'; import { formatDate } from '../../../utils/date';
function Members() { function Members() {
const [members, setMembers] = useState([]); // useQuery
const [loading, setLoading] = useState(true); const { data: members = [], isLoading: loading } = useQuery({
queryKey: ['members'],
useEffect(() => { queryFn: getMembers,
getMembers()
.then(data => {
setMembers(data);
setLoading(false);
})
.catch(error => {
console.error('데이터 로드 오류:', error);
setLoading(false);
}); });
}, []);
if (loading) { if (loading) {
return ( return (