feat: 메인 페이지 일정 API 및 디자인 개선
- 백엔드에 startDate 파라미터 지원 추가 (다가오는 일정 조회) - handleUpcomingSchedules 함수 구현 - 메인 페이지 일정 카드 디자인을 일정 페이지와 동일하게 변경 - 멤버 표시 로직 API 응답에 맞게 수정 (PC/모바일) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8e15cd6d2c
commit
f780e91f14
3 changed files with 99 additions and 51 deletions
|
|
@ -42,22 +42,28 @@ export default async function schedulesRoutes(fastify) {
|
||||||
search: { type: 'string', description: '검색어' },
|
search: { type: 'string', description: '검색어' },
|
||||||
year: { type: 'integer', description: '년도' },
|
year: { type: 'integer', description: '년도' },
|
||||||
month: { type: 'integer', minimum: 1, maximum: 12, description: '월' },
|
month: { type: 'integer', minimum: 1, maximum: 12, description: '월' },
|
||||||
|
startDate: { type: 'string', description: '시작 날짜 (YYYY-MM-DD)' },
|
||||||
offset: { type: 'integer', default: 0, description: '페이지 오프셋' },
|
offset: { type: 'integer', default: 0, description: '페이지 오프셋' },
|
||||||
limit: { type: 'integer', default: 100, description: '결과 개수' },
|
limit: { type: 'integer', default: 100, description: '결과 개수' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
const { search, year, month, offset = 0, limit = 100 } = request.query;
|
const { search, year, month, startDate, offset = 0, limit = 100 } = request.query;
|
||||||
|
|
||||||
// 검색 모드
|
// 검색 모드
|
||||||
if (search && search.trim()) {
|
if (search && search.trim()) {
|
||||||
return await handleSearch(fastify, search.trim(), parseInt(offset), parseInt(limit));
|
return await handleSearch(fastify, search.trim(), parseInt(offset), parseInt(limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 다가오는 일정 조회 (startDate부터)
|
||||||
|
if (startDate) {
|
||||||
|
return await handleUpcomingSchedules(db, startDate, parseInt(limit));
|
||||||
|
}
|
||||||
|
|
||||||
// 월별 조회 모드
|
// 월별 조회 모드
|
||||||
if (!year || !month) {
|
if (!year || !month) {
|
||||||
return reply.code(400).send({ error: 'search 또는 year/month는 필수입니다.' });
|
return reply.code(400).send({ error: 'search, startDate, 또는 year/month는 필수입니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await handleMonthlySchedules(db, parseInt(year), parseInt(month));
|
return await handleMonthlySchedules(db, parseInt(year), parseInt(month));
|
||||||
|
|
@ -423,3 +429,65 @@ async function handleMonthlySchedules(db, year, month) {
|
||||||
|
|
||||||
return grouped;
|
return grouped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 다가오는 일정 조회 (startDate부터 limit개)
|
||||||
|
*/
|
||||||
|
async function handleUpcomingSchedules(db, startDate, limit) {
|
||||||
|
// 일정 조회
|
||||||
|
const [schedules] = await db.query(`
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.title,
|
||||||
|
s.date,
|
||||||
|
s.time,
|
||||||
|
s.category_id,
|
||||||
|
c.name as category_name,
|
||||||
|
c.color as category_color
|
||||||
|
FROM schedules s
|
||||||
|
LEFT JOIN schedule_categories c ON s.category_id = c.id
|
||||||
|
WHERE s.date >= ?
|
||||||
|
ORDER BY s.date ASC, s.time ASC
|
||||||
|
LIMIT ?
|
||||||
|
`, [startDate, limit]);
|
||||||
|
|
||||||
|
// 멤버 정보 조회
|
||||||
|
const scheduleIds = schedules.map(s => s.id);
|
||||||
|
let memberMap = {};
|
||||||
|
|
||||||
|
if (scheduleIds.length > 0) {
|
||||||
|
const [scheduleMembers] = await db.query(`
|
||||||
|
SELECT sm.schedule_id, m.name
|
||||||
|
FROM schedule_members sm
|
||||||
|
JOIN members m ON sm.member_id = m.id
|
||||||
|
WHERE sm.schedule_id IN (?)
|
||||||
|
ORDER BY m.id
|
||||||
|
`, [scheduleIds]);
|
||||||
|
|
||||||
|
for (const sm of scheduleMembers) {
|
||||||
|
if (!memberMap[sm.schedule_id]) {
|
||||||
|
memberMap[sm.schedule_id] = [];
|
||||||
|
}
|
||||||
|
memberMap[sm.schedule_id].push({ name: sm.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결과 포맷팅
|
||||||
|
return schedules.map(s => {
|
||||||
|
const scheduleMembers = memberMap[s.id] || [];
|
||||||
|
const members = scheduleMembers.length >= 5
|
||||||
|
? [{ name: '프로미스나인' }]
|
||||||
|
: scheduleMembers;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
date: s.date,
|
||||||
|
time: s.time,
|
||||||
|
category_id: s.category_id,
|
||||||
|
category_name: s.category_name,
|
||||||
|
category_color: s.category_color,
|
||||||
|
members,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,8 +176,10 @@ function MobileHome() {
|
||||||
const isCurrentYear = scheduleYear === currentYear;
|
const isCurrentYear = scheduleYear === currentYear;
|
||||||
const isCurrentMonth = isCurrentYear && scheduleMonth === currentMonth;
|
const isCurrentMonth = isCurrentYear && scheduleMonth === currentMonth;
|
||||||
|
|
||||||
// 멤버 처리 (5명 이상이면 프로미스나인)
|
// 멤버 처리
|
||||||
const memberList = schedule.member_names ? schedule.member_names.split(',').map(n => n.trim()).filter(Boolean) : [];
|
const memberList = schedule.member_names
|
||||||
|
? schedule.member_names.split(',').map(n => n.trim()).filter(Boolean)
|
||||||
|
: schedule.members?.map(m => m.name) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -237,7 +239,7 @@ function MobileHome() {
|
||||||
{/* 멤버 */}
|
{/* 멤버 */}
|
||||||
{memberList.length > 0 && (
|
{memberList.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
{(memberList.length >= 5 ? ['프로미스나인'] : memberList).map((name, i) => (
|
{memberList.map((name, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className="px-2 py-0.5 bg-primary/10 text-primary text-[10px] rounded-full font-medium"
|
className="px-2 py-0.5 bg-primary/10 text-primary text-[10px] rounded-full font-medium"
|
||||||
|
|
|
||||||
|
|
@ -254,9 +254,10 @@ function Home() {
|
||||||
// 멤버 처리
|
// 멤버 처리
|
||||||
const memberList = schedule.member_names
|
const memberList = schedule.member_names
|
||||||
? schedule.member_names.split(",")
|
? schedule.member_names.split(",")
|
||||||
: [];
|
: schedule.members?.map(m => m.name) || [];
|
||||||
const displayMembers =
|
const displayMembers = memberList;
|
||||||
memberList.length >= 5 ? ["프로미스나인"] : memberList;
|
|
||||||
|
const categoryColor = schedule.category_color || '#6366f1';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -269,71 +270,48 @@ function Home() {
|
||||||
transition: { duration: 0.4, ease: "easeOut" },
|
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 cursor-pointer"
|
||||||
>
|
>
|
||||||
{/* 날짜 영역 - primary 색상 고정 */}
|
{/* 날짜 영역 - 카테고리 색상 */}
|
||||||
<div className="w-20 flex flex-col items-center justify-center text-white py-5 bg-primary">
|
<div
|
||||||
{/* 현재 년도가 아니면 년.월 표시 */}
|
className="w-24 flex flex-col items-center justify-center text-white py-6"
|
||||||
|
style={{ backgroundColor: categoryColor }}
|
||||||
|
>
|
||||||
{!isCurrentYear && (
|
{!isCurrentYear && (
|
||||||
<span className="text-xs font-medium opacity-70">
|
<span className="text-xs font-medium opacity-70">
|
||||||
{scheduleYear}.{scheduleMonth + 1}
|
{scheduleYear}.{scheduleMonth + 1}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* 현재 달이 아니면 월 표시 (현재 년도일 때) */}
|
|
||||||
{isCurrentYear && !isCurrentMonth && (
|
{isCurrentYear && !isCurrentMonth && (
|
||||||
<span className="text-xs font-medium opacity-70">
|
<span className="text-xs font-medium opacity-70">
|
||||||
{scheduleMonth + 1}월
|
{scheduleMonth + 1}월
|
||||||
</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">
|
<span className="text-sm font-medium opacity-80">{weekday}</span>
|
||||||
{weekday}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 내용 영역 */}
|
{/* 내용 영역 */}
|
||||||
<div className="flex-1 p-5 flex flex-col justify-center">
|
<div className="flex-1 p-6 flex flex-col justify-center">
|
||||||
<h3 className="font-bold text-lg text-gray-900 mb-2">
|
<h3 className="font-bold text-lg mb-2">{schedule.title}</h3>
|
||||||
{schedule.title}
|
<div className="flex flex-wrap gap-3 text-base text-gray-500">
|
||||||
</h3>
|
|
||||||
|
|
||||||
<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">
|
<span className="flex items-center gap-1">
|
||||||
<Clock
|
<Clock size={16} className="opacity-60" />
|
||||||
size={14}
|
{schedule.time.slice(0, 5)}
|
||||||
className="text-primary opacity-60"
|
</span>
|
||||||
/>
|
|
||||||
<span>{schedule.time.slice(0, 5)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{schedule.category_name && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Tag
|
|
||||||
size={14}
|
|
||||||
className="text-primary opacity-60"
|
|
||||||
/>
|
|
||||||
<span>{schedule.category_name}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{schedule.source?.name && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Link2
|
|
||||||
size={14}
|
|
||||||
className="text-primary opacity-60"
|
|
||||||
/>
|
|
||||||
<span>{schedule.source?.name}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Tag size={16} className="opacity-60" />
|
||||||
|
{schedule.category_name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 멤버 태그 */}
|
|
||||||
{displayMembers.length > 0 && (
|
{displayMembers.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1.5 mt-3">
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||||
{displayMembers.map((name, i) => (
|
{displayMembers.map((name, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className="px-2.5 py-1 bg-primary/10 text-primary text-xs font-medium rounded-full"
|
className="px-2 py-0.5 bg-primary/10 text-primary text-sm font-medium rounded-full"
|
||||||
>
|
>
|
||||||
{name.trim()}
|
{name.trim()}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue