diff --git a/src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts b/src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts index 0bf32b1..6137acc 100644 --- a/src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts +++ b/src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts @@ -7,7 +7,7 @@ export async function getCalendarEventsByWorkspaceId(workspaceId: string): Promi const supabase = getSupabaseBrowserClient(); const { data, error } = await supabase .from('calendar_events') - .select('id, workspace_id, title, starts_at, description') + .select('id, workspace_id, title, starts_at, description, event_type') .eq('workspace_id', workspaceId) .order('starts_at'); diff --git a/src/entities/calendar-event/model/calendar-event.mapper.ts b/src/entities/calendar-event/model/calendar-event.mapper.ts index 93823e1..27600be 100644 --- a/src/entities/calendar-event/model/calendar-event.mapper.ts +++ b/src/entities/calendar-event/model/calendar-event.mapper.ts @@ -6,6 +6,7 @@ export interface CalendarEventRow { title: string; starts_at: string; description: string | null; + event_type: 'meeting' | 'deadline'; } interface CalendarEventMetadata { @@ -119,6 +120,7 @@ export function toCalendarEvent(row: CalendarEventRow): CalendarEvent { date: formatIsoDateInKst(row.starts_at), time: normalizeCalendarEventTime(metadata.time), color: isCalendarEventColor(metadata.color) ? metadata.color : defaultColor, + eventType: row.event_type, }; } diff --git a/src/entities/calendar-event/model/calendar-event.types.ts b/src/entities/calendar-event/model/calendar-event.types.ts index 9a4014e..9399df2 100644 --- a/src/entities/calendar-event/model/calendar-event.types.ts +++ b/src/entities/calendar-event/model/calendar-event.types.ts @@ -8,6 +8,7 @@ export interface CalendarEvent { date: string; time: string | null; color: CalendarEventColor; + eventType: 'meeting' | 'deadline'; } export interface CalendarEventFormValues { diff --git a/src/entities/side-project/schedule-event/index.ts b/src/entities/side-project/schedule-event/index.ts index c9977ed..2457386 100644 --- a/src/entities/side-project/schedule-event/index.ts +++ b/src/entities/side-project/schedule-event/index.ts @@ -5,4 +5,7 @@ export { type ScheduleEventType, type CalendarMonth, } from './model/schedule-event.types'; -export { mockTodaySchedule, mockCalendar } from './model/schedule-event.mock'; +export { + buildCalendarMonthFromEvents, + selectTodayScheduleEvents, +} from './model/schedule-event.selectors'; diff --git a/src/entities/side-project/schedule-event/model/schedule-event.mock.ts b/src/entities/side-project/schedule-event/model/schedule-event.mock.ts deleted file mode 100644 index 2cec3ca..0000000 --- a/src/entities/side-project/schedule-event/model/schedule-event.mock.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 오늘 일정·월간 캘린더 목데이터 -import type { CalendarMonth, ScheduleEvent } from './schedule-event.types'; - -export const mockTodaySchedule: ScheduleEvent[] = [ - { id: 'event-1', title: '디자인 리뷰 회의', time: '11:00', type: 'meeting' }, - { id: 'event-2', title: '팀 점심', time: '12:30', type: 'meeting' }, - { id: 'event-3', title: '스프린트 데일리', time: '14:00', type: 'meeting' }, - { id: 'event-4', title: '코드 리뷰', time: '15:00', type: 'meeting' }, - { id: 'event-5', title: 'API 명세 마감', time: '16:00', type: 'deadline' }, - { id: 'event-6', title: '회고 준비', time: '17:00', type: 'meeting' }, -]; - -export const mockCalendar: CalendarMonth = { - year: 2025, - month: 7, - today: 30, - eventDays: [2, 5, 8, 10, 14, 22, 28], -}; diff --git a/src/entities/side-project/schedule-event/model/schedule-event.selectors.ts b/src/entities/side-project/schedule-event/model/schedule-event.selectors.ts new file mode 100644 index 0000000..ee7662b --- /dev/null +++ b/src/entities/side-project/schedule-event/model/schedule-event.selectors.ts @@ -0,0 +1,70 @@ +import type { CalendarEvent } from '@/entities/calendar-event'; +import { formatCalendarEventTimeLabel } from '@/entities/calendar-event'; + +import type { CalendarMonth, ScheduleEvent } from './schedule-event.types'; + +function formatIsoDate(date: Date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +function sortByTime(left: CalendarEvent, right: CalendarEvent) { + if (left.time === right.time) { + return left.title.localeCompare(right.title, 'ko'); + } + + if (!left.time) { + return 1; + } + + if (!right.time) { + return -1; + } + + return left.time.localeCompare(right.time); +} + +export function selectTodayScheduleEvents( + calendarEvents: CalendarEvent[], + currentDate = new Date(), +): ScheduleEvent[] { + const todayIsoDate = formatIsoDate(currentDate); + + return calendarEvents + .filter((event) => event.date === todayIsoDate) + .sort(sortByTime) + .map((event) => ({ + id: event.id, + title: event.title, + time: formatCalendarEventTimeLabel(event.time), + type: event.eventType, + })); +} + +export function buildCalendarMonthFromEvents( + calendarEvents: CalendarEvent[], + currentDate = new Date(), +): CalendarMonth { + const year = currentDate.getFullYear(); + const month = currentDate.getMonth() + 1; + const currentMonthPrefix = `${year}-${String(month).padStart(2, '0')}-`; + + const eventDays = Array.from( + new Set( + calendarEvents + .filter((event) => event.date.startsWith(currentMonthPrefix)) + .map((event) => Number(event.date.slice(-2))) + .filter((day) => Number.isInteger(day) && day > 0), + ), + ).sort((left, right) => left - right); + + return { + year, + month, + today: currentDate.getDate(), + eventDays, + }; +} diff --git a/src/features/manage-progress-chart/ui/ProgressChartView.tsx b/src/features/manage-progress-chart/ui/ProgressChartView.tsx index b90cfaf..c0f30c4 100644 --- a/src/features/manage-progress-chart/ui/ProgressChartView.tsx +++ b/src/features/manage-progress-chart/ui/ProgressChartView.tsx @@ -71,7 +71,9 @@ function OverallProgressCard({ function AssigneeBarChartCard({ items }: { items: ProgressChartAssigneeItem[] }) { const maxValue = Math.max(...items.map((item) => item.count), 0); - const gridValues = [0, 2, 4, 6, 8]; + const gridStep = maxValue <= 4 ? 1 : Math.ceil(maxValue / 4); + const gridMaxValue = Math.max(gridStep * 4, 4); + const gridValues = Array.from({ length: 5 }, (_, index) => index * gridStep); return (
@@ -106,7 +108,7 @@ function AssigneeBarChartCard({ items }: { items: ProgressChartAssigneeItem[] })
diff --git a/src/views/dashboard/config/widget-catalog.tsx b/src/views/dashboard/config/widget-catalog.tsx index 0165bae..1256efe 100644 --- a/src/views/dashboard/config/widget-catalog.tsx +++ b/src/views/dashboard/config/widget-catalog.tsx @@ -72,7 +72,7 @@ export const WIDGET_CATALOG = { 'today-schedule': { layout: { i: 'today-schedule', x: 6, y: 9, w: 6, h: 4, minW: 2, minH: 3 }, title: '오늘 일정', - render: (size) => , + render: (size, { workspaceId }) => , }, 'overall-progress': { layout: { i: 'overall-progress', x: 9, y: 10, w: 3, h: 5, minW: 3, minH: 4 }, @@ -87,7 +87,7 @@ export const WIDGET_CATALOG = { calendar: { layout: { i: 'calendar', x: 6, y: 13, w: 6, h: 8, minW: 4, minH: 6 }, title: '캘린더', - render: (size) => , + render: (size, { workspaceId }) => , }, } satisfies Record; diff --git a/src/views/progress-chart/ui/ProgressChartPage.tsx b/src/views/progress-chart/ui/ProgressChartPage.tsx index daea48e..8161376 100644 --- a/src/views/progress-chart/ui/ProgressChartPage.tsx +++ b/src/views/progress-chart/ui/ProgressChartPage.tsx @@ -1,7 +1,3 @@ -// side-project 진행률만 실 DB 연동 완료. -// 그 외 purpose는 manage-progress-chart 뷰(다른 담당자, mock 기반) — 실 API 연동 전이라 -// workspaceId 'test' 하드코딩 상태. 실 연동은 해당 담당자 작업으로 남김. -// TODO(담당자): ProgressChartView 실 API 연동 + workspaceId={workspaceId} 전달 import { ProgressChartView } from '@/features/manage-progress-chart'; import { ProgressChartView as SideProjectProgressChartView } from '@/views/side-project/progress-chart'; import { plusJakartaSans } from '@/shared/lib/fonts'; diff --git a/src/widgets/side-project/dashboard-calendar/ui/Calendar.tsx b/src/widgets/side-project/dashboard-calendar/ui/Calendar.tsx index 56805ee..e8f329e 100644 --- a/src/widgets/side-project/dashboard-calendar/ui/Calendar.tsx +++ b/src/widgets/side-project/dashboard-calendar/ui/Calendar.tsx @@ -1,9 +1,13 @@ +'use client'; + // 캘린더 위젯 — 월간 달력. 오늘 날짜 강조 + 이벤트 점 표시. // · sm: 오늘 날짜 + 이벤트 건수 요약 // · md/lg: 월간 그리드(요일 헤더 + 날짜 셀) -import { mockCalendar } from '@/entities/side-project/schedule-event'; +import { useCalendarEventsByWorkspaceId } from '@/entities/calendar-event'; +import { buildCalendarMonthFromEvents } from '@/entities/side-project/schedule-event'; import { cn } from '@/shared/lib/utils'; import type { WidgetSize } from '@/shared/dashboard/lib/widget-size'; +import { WidgetStateMessage } from '@/shared/dashboard/ui/widget-state-message'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; const WEEKDAYS = ['일', '월', '화', '수', '목', '금', '토']; @@ -18,9 +22,17 @@ function buildMonthCells(year: number, month: number): (number | null)[] { return cells; } -export default function Calendar({ size = 'md' }: { size?: WidgetSize }) { - const { year, month, today, eventDays } = mockCalendar; - +export default function Calendar({ + workspaceId, + size = 'md', +}: { + workspaceId: string; + size?: WidgetSize; +}) { + const calendarEventsQuery = useCalendarEventsByWorkspaceId(workspaceId); + const { year, month, today, eventDays } = buildCalendarMonthFromEvents( + calendarEventsQuery.data ?? [], + ); const header = ( ); + if (calendarEventsQuery.isPending) { + return ; + } + + if (calendarEventsQuery.isError) { + return ; + } + if (size === 'sm') { return ( diff --git a/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx index 7cc81be..0422c45 100644 --- a/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx +++ b/src/widgets/side-project/dashboard-today-schedule/ui/TodaySchedule.tsx @@ -1,18 +1,46 @@ +'use client'; + // 오늘 일정 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 // · sm: 다음 일정 1건(액센트 바 + 제목 + 시간 + 외 N건) // · md: 3건 리스트 // · lg: 전체 리스트 (일정 유형별 액센트 바 색상 — 마감=빨강) -import { mockTodaySchedule, SCHEDULE_TYPE_COLOR } from '@/entities/side-project/schedule-event'; +import { useCalendarEventsByWorkspaceId } from '@/entities/calendar-event'; +import { + selectTodayScheduleEvents, + SCHEDULE_TYPE_COLOR, +} from '@/entities/side-project/schedule-event'; import type { WidgetSize } from '@/shared/dashboard/lib/widget-size'; +import { WidgetStateMessage } from '@/shared/dashboard/ui/widget-state-message'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; const header = ( 전체 보기} /> ); -export default function TodaySchedule({ size = 'md' }: { size?: WidgetSize }) { +export default function TodaySchedule({ + workspaceId, + size = 'md', +}: { + workspaceId: string; + size?: WidgetSize; +}) { + const calendarEventsQuery = useCalendarEventsByWorkspaceId(workspaceId); + const todaySchedule = selectTodayScheduleEvents(calendarEventsQuery.data ?? []); + + if (calendarEventsQuery.isPending) { + return ; + } + + if (calendarEventsQuery.isError) { + return ; + } + + if (todaySchedule.length === 0) { + return ; + } + if (size === 'sm') { - const [next, ...rest] = mockTodaySchedule; + const [next, ...rest] = todaySchedule; return ( {header} @@ -35,7 +63,7 @@ export default function TodaySchedule({ size = 'md' }: { size?: WidgetSize }) { } // md: 3건, lg: 전체 - const events = size === 'md' ? mockTodaySchedule.slice(0, 3) : mockTodaySchedule; + const events = size === 'md' ? todaySchedule.slice(0, 3) : todaySchedule; return ( {header}