diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index 36c83fe..58d0bf0 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -42,22 +42,28 @@ export default async function schedulesRoutes(fastify) { search: { type: 'string', description: '검색어' }, year: { type: 'integer', description: '년도' }, month: { type: 'integer', minimum: 1, maximum: 12, description: '월' }, + startDate: { type: 'string', description: '시작 날짜 (YYYY-MM-DD)' }, offset: { type: 'integer', default: 0, description: '페이지 오프셋' }, limit: { type: 'integer', default: 100, description: '결과 개수' }, }, }, }, }, 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()) { return await handleSearch(fastify, search.trim(), parseInt(offset), parseInt(limit)); } + // 다가오는 일정 조회 (startDate부터) + if (startDate) { + return await handleUpcomingSchedules(db, startDate, parseInt(limit)); + } + // 월별 조회 모드 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)); @@ -423,3 +429,65 @@ async function handleMonthlySchedules(db, year, month) { 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, + }; + }); +} diff --git a/frontend/src/pages/mobile/public/Home.jsx b/frontend/src/pages/mobile/public/Home.jsx index e8b6a2a..d4454f4 100644 --- a/frontend/src/pages/mobile/public/Home.jsx +++ b/frontend/src/pages/mobile/public/Home.jsx @@ -176,8 +176,10 @@ function MobileHome() { 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) : []; + // 멤버 처리 + const memberList = schedule.member_names + ? schedule.member_names.split(',').map(n => n.trim()).filter(Boolean) + : schedule.members?.map(m => m.name) || []; return ( 0 && (
- {(memberList.length >= 5 ? ['프로미스나인'] : memberList).map((name, i) => ( + {memberList.map((name, i) => ( = 5 ? ["프로미스나인"] : memberList; + : schedule.members?.map(m => m.name) || []; + const displayMembers = memberList; + + const categoryColor = schedule.category_color || '#6366f1'; return ( - {/* 날짜 영역 - primary 색상 고정 */} -
- {/* 현재 년도가 아니면 년.월 표시 */} + {/* 날짜 영역 - 카테고리 색상 */} +
{!isCurrentYear && ( {scheduleYear}.{scheduleMonth + 1} )} - {/* 현재 달이 아니면 월 표시 (현재 년도일 때) */} {isCurrentYear && !isCurrentMonth && ( {scheduleMonth + 1}월 )} {day} - - {weekday} - + {weekday}
{/* 내용 영역 */} -
-

- {schedule.title} -

- -
+
+

{schedule.title}

+
{schedule.time && ( -
- - {schedule.time.slice(0, 5)} -
- )} - {schedule.category_name && ( -
- - {schedule.category_name} -
- )} - {schedule.source?.name && ( -
- - {schedule.source?.name} -
+ + + {schedule.time.slice(0, 5)} + )} + + + {schedule.category_name} +
- - {/* 멤버 태그 */} {displayMembers.length > 0 && ( -
+
{displayMembers.map((name, i) => ( {name.trim()}