diff --git a/apps/web/src/entities/schedule/api/getMemberScheduleList.ts b/apps/web/src/entities/schedule/api/getMemberScheduleList.ts new file mode 100644 index 00000000..467f263a --- /dev/null +++ b/apps/web/src/entities/schedule/api/getMemberScheduleList.ts @@ -0,0 +1,11 @@ +import { auth } from '@/shared/api/apiClient'; +import type { ApiResponse } from '@/shared/api/baseTypes'; +import { END_POINT } from '@/shared/constants/endpoint'; + +import type { CrewScheduleListResponse } from '../model/schedule.model'; + +export const getMemberScheduleList = () => { + return auth.get>( + END_POINT.SCHEDULE.JOINED_LIST + ); +}; diff --git a/apps/web/src/entities/schedule/api/getScheduleCalendar.ts b/apps/web/src/entities/schedule/api/getScheduleCalendar.ts index c5d25d0e..cfeb690c 100644 --- a/apps/web/src/entities/schedule/api/getScheduleCalendar.ts +++ b/apps/web/src/entities/schedule/api/getScheduleCalendar.ts @@ -2,10 +2,17 @@ import { auth } from '@/shared/api/apiClient'; import type { ApiResponse } from '@/shared/api/baseTypes'; import { END_POINT } from '@/shared/constants/endpoint'; -import type { CrewScheduleCalendarResponse } from '../model/schedule.model'; +import type { + CrewScheduleCalendarRequest, + CrewScheduleCalendarResponse, +} from '../model/schedule.model'; -export const getScheduleCalendar = (crewId: number) => { - return auth.get>( - END_POINT.SCHEDULE.CALENDAR(crewId) +export const getScheduleCalendar = ( + crewId: number, + request?: CrewScheduleCalendarRequest +) => { + return auth.get>( + END_POINT.SCHEDULE.CALENDAR(crewId), + request ? { searchParams: request } : undefined ); }; diff --git a/apps/web/src/entities/schedule/model/schedule.types.ts b/apps/web/src/entities/schedule/model/schedule.types.ts index c52f5d16..06409655 100644 --- a/apps/web/src/entities/schedule/model/schedule.types.ts +++ b/apps/web/src/entities/schedule/model/schedule.types.ts @@ -1,5 +1,11 @@ -import type { CrewScheduleListResponse } from './schedule.model'; +import type { + CrewScheduleCalendarResponse, + CrewScheduleListResponse, +} from './schedule.model'; export type ScheduleListItem = CrewScheduleListResponse; export type ScheduleList = ScheduleListItem[]; export type RunType = ScheduleListItem['runType']; + +export type ScheduleCalendarItem = CrewScheduleCalendarResponse; +export type ScheduleCalendarList = ScheduleCalendarItem[]; diff --git a/apps/web/src/entities/schedule/styles/ScheduleList.css.ts b/apps/web/src/entities/schedule/styles/ScheduleList.css.ts index 2fae6d7c..583ef285 100644 --- a/apps/web/src/entities/schedule/styles/ScheduleList.css.ts +++ b/apps/web/src/entities/schedule/styles/ScheduleList.css.ts @@ -6,7 +6,6 @@ export const listContainer = style({ flexDirection: 'column', gap: '10px', width: '100%', - height: '100%', }); export const emptyContainer = style({ @@ -15,8 +14,8 @@ export const emptyContainer = style({ alignItems: 'center', justifyContent: 'center', width: '100%', - height: '100px', - flex: 1, + height: '220px', + gap: '16px', }); export const emptyText = style([ @@ -25,3 +24,8 @@ export const emptyText = style([ color: vars.colors.gray60, }, ]); + +export const addScheduleButton = style({ + width: 'fit-content', + padding: '0 30px', +}); diff --git a/apps/web/src/entities/schedule/ui/ScheduleList.tsx b/apps/web/src/entities/schedule/ui/ScheduleList.tsx index ca4a7fe2..8a15e4fe 100644 --- a/apps/web/src/entities/schedule/ui/ScheduleList.tsx +++ b/apps/web/src/entities/schedule/ui/ScheduleList.tsx @@ -1,3 +1,6 @@ +import { Button } from '@azit/design-system/button'; +import { CalendarIcon } from '@azit/design-system/icon'; + import { useFlow } from '@/app/routes/stackflow'; import type { ScheduleListItem as ScheduleListItemType } from '@/entities/schedule/model/schedule.types'; @@ -6,9 +9,10 @@ import { ScheduleListItem } from '@/entities/schedule/ui/ScheduleListItem'; interface ScheduleListProps { items: ScheduleListItemType[]; + isHomePage?: boolean; } -export function ScheduleList({ items }: ScheduleListProps) { +export function ScheduleList({ items, isHomePage = false }: ScheduleListProps) { const { push } = useFlow(); const handleClickItem = (item: ScheduleListItemType) => { @@ -17,6 +21,22 @@ export function ScheduleList({ items }: ScheduleListProps) { const renderItem = () => { if (items.length === 0) { + if (isHomePage) { + return ( +
+ +

일정 탭에서 일정을 추가해보세요!

+ +
+ ); + } return (

등록된 일정이 없어요

diff --git a/apps/web/src/entities/schedule/ui/ScheduleListItem.tsx b/apps/web/src/entities/schedule/ui/ScheduleListItem.tsx index 6eed6fa7..bfb0ec01 100644 --- a/apps/web/src/entities/schedule/ui/ScheduleListItem.tsx +++ b/apps/web/src/entities/schedule/ui/ScheduleListItem.tsx @@ -4,7 +4,7 @@ import { ClockIcon, MarkerPinIcon, UsersIcon } from '@azit/design-system/icon'; import type { ScheduleListItem as ScheduleListItemType } from '@/entities/schedule/model/schedule.types'; import * as styles from '@/entities/schedule/styles/ScheduleListItem.css.ts'; -function formatMeetingAt(meetingAt: string | undefined) { +const formatMeetingAt = (meetingAt: string | undefined) => { if (!meetingAt) return { month: '', day: '', time: '' }; try { const date = new Date(meetingAt); @@ -20,30 +20,30 @@ function formatMeetingAt(meetingAt: string | undefined) { } catch { return { month: '', day: '', time: '' }; } -} +}; -function buildTags( +const buildTags = ( item: ScheduleListItemType -): { label: string; type: 'primary' | 'secondary' }[] { - const tags: { label: string; type: 'primary' | 'secondary' }[] = []; +): { label: string; type: 'primary' | 'secondary' | 'gray' }[] => { + const tags: { label: string; type: 'primary' | 'secondary' | 'gray' }[] = []; if (item.runType) { tags.push({ label: item.runType === 'REGULAR' ? '정기런' : '번개런', - type: 'primary', + type: item.runType === 'REGULAR' ? 'primary' : 'secondary', }); } if (item.distance != null) - tags.push({ label: `${item.distance}km`, type: 'secondary' }); + tags.push({ label: `${item.distance}km`, type: 'gray' }); if (item.pace != null) { const min = Math.floor(item.pace); const sec = Math.round((item.pace - min) * 60); tags.push({ label: `${min}'${sec.toString().padStart(2, '0')}"/km`, - type: 'secondary', + type: 'gray', }); } return tags; -} +}; interface ScheduleListItemProps { item: ScheduleListItemType; @@ -77,10 +77,7 @@ export function ScheduleListItem({ item, handleClick }: ScheduleListItemProps) {
{tags.map((tag, index) => ( - + {tag.label} ))} diff --git a/apps/web/src/pages/home/ui/HomePage.tsx b/apps/web/src/pages/home/ui/HomePage.tsx index b23817aa..77a4ff85 100644 --- a/apps/web/src/pages/home/ui/HomePage.tsx +++ b/apps/web/src/pages/home/ui/HomePage.tsx @@ -1,13 +1,17 @@ import { Header } from '@azit/design-system/header'; import { BellIcon } from '@azit/design-system/icon'; import { AppScreen } from '@stackflow/plugin-basic-ui'; +import { useQuery } from '@tanstack/react-query'; import { useFlow } from '@/app/routes/stackflow'; import { ScheduleAttendanceSection } from '@/widgets/schedule-attendance/ui'; import { ScheduleSectionLayout } from '@/widgets/schedule-section-layout/ui'; +import { ScheduleListSkeleton } from '@/widgets/skeleton/ui'; -import { mockActivityActivation, mockScheduleList } from '@/shared/mock/home'; +import { mockActivityActivation } from '@/shared/mock/home'; +import { memberQueries } from '@/shared/queries'; +import { scheduleQueries } from '@/shared/queries/schedule'; import { scrollContainer } from '@/shared/styles/container.css'; import { logo } from '@/shared/styles/logo.css'; import { AppLayout } from '@/shared/ui/layout'; @@ -22,6 +26,14 @@ export function HomePage() { push('AlertPage', {}); }; + const { data: myInfoData } = useQuery(memberQueries.myInfoQuery()); + const crewId = myInfoData?.ok ? myInfoData.data.result.crewId : 0; + + const { data: scheduleList = [], isLoading } = useQuery({ + ...scheduleQueries.getMemberScheduleListQuery(), + enabled: crewId > 0, + }); + return ( @@ -40,7 +52,13 @@ export function HomePage() { } scheduleTitle="내 일정" - scheduleContent={} + scheduleContent={ + isLoading ? ( + + ) : ( + + ) + } />
diff --git a/apps/web/src/pages/mypage/ui/MyAttendancePage.tsx b/apps/web/src/pages/mypage/ui/MyAttendancePage.tsx index 1ce2fd0d..9592adc5 100644 --- a/apps/web/src/pages/mypage/ui/MyAttendancePage.tsx +++ b/apps/web/src/pages/mypage/ui/MyAttendancePage.tsx @@ -32,12 +32,7 @@ export function MyAttendancePage() { /> {}} - activeStartDate={new Date()} - onActiveStartDateChange={() => {}} - /> + {}} /> } scheduleContent={ <> diff --git a/apps/web/src/pages/schedule/ui/SchedulePage.tsx b/apps/web/src/pages/schedule/ui/SchedulePage.tsx index dd9e3329..987ae83c 100644 --- a/apps/web/src/pages/schedule/ui/SchedulePage.tsx +++ b/apps/web/src/pages/schedule/ui/SchedulePage.tsx @@ -5,10 +5,13 @@ import { useQuery } from '@tanstack/react-query'; import dayjs from 'dayjs'; import { useState } from 'react'; -import { ScheduleWeekCalendar } from '@/widgets/schedule-calendar/ui/ScheduleWeekCalendar'; +import { ScheduleCalendar } from '@/widgets/schedule-calendar/ui/ScheduleCalendar'; +// import { ScheduleWeekCalendar } from '@/widgets/schedule-calendar/ui/ScheduleWeekCalendar'; import { ScheduleFilterTab } from '@/widgets/schedule-filter-tab/ui'; import { ScheduleSectionLayout } from '@/widgets/schedule-section-layout/ui'; +import { ScheduleListSkeleton } from '@/widgets/skeleton/ui'; +import { formatDate } from '@/shared/lib/formatters'; import { memberQueries } from '@/shared/queries/member'; import { scheduleQueries } from '@/shared/queries/schedule'; import { scrollContainer } from '@/shared/styles/container.css'; @@ -21,13 +24,26 @@ import { ScheduleList } from '@/entities/schedule/ui'; export function SchedulePage() { const [activeFilter, setActiveFilter] = useState(undefined); const [selectedDate, setSelectedDate] = useState(new Date()); + const [date, setDate] = useState(undefined); + + const handleDateChange = (date: Date) => { + setSelectedDate(date); + setDate(formatDate(date, 'YYYY-MM-DD')); + }; const { data: myInfoData } = useQuery(memberQueries.myInfoQuery()); const crewId = myInfoData?.ok ? myInfoData.data.result.crewId : 0; const { data: scheduleList = [], isLoading } = useQuery({ ...scheduleQueries.getScheduleListQuery(crewId, { runType: activeFilter, - date: dayjs(selectedDate).format('YYYY-MM-DD'), + date, + }), + enabled: crewId > 0, + }); + + const { data: scheduleCalendarList = [] } = useQuery({ + ...scheduleQueries.getScheduleCalendarQuery(crewId, { + yearMonth: formatDate(selectedDate, 'YYYY-MM'), }), enabled: crewId > 0, }); @@ -43,10 +59,15 @@ export function SchedulePage() {
+ // } scheduleContent={ <> @@ -54,7 +75,15 @@ export function SchedulePage() { activeFilter={activeFilter} onFilterChange={setActiveFilter} /> - {!isLoading && } + {isLoading ? ( + + ) : ( + + dayjs(item.meetingAt).isAfter(dayjs(selectedDate)) + )} + /> + )} } /> diff --git a/apps/web/src/shared/constants/endpoint.ts b/apps/web/src/shared/constants/endpoint.ts index 56e782ba..0479ad4c 100644 --- a/apps/web/src/shared/constants/endpoint.ts +++ b/apps/web/src/shared/constants/endpoint.ts @@ -51,5 +51,6 @@ export const END_POINT = { SCHEDULE: { LIST: (crewId: number) => `crews/${crewId}/schedules`, CALENDAR: (crewId: number) => `crews/${crewId}/schedules/calendar`, + JOINED_LIST: 'members/me/schedules', }, } as const; diff --git a/apps/web/src/shared/lib/formatters.ts b/apps/web/src/shared/lib/formatters.ts index 3e667126..e3f3d50d 100644 --- a/apps/web/src/shared/lib/formatters.ts +++ b/apps/web/src/shared/lib/formatters.ts @@ -1,3 +1,5 @@ +import dayjs from 'dayjs'; + const WEEKDAYS = ['일', '월', '화', '수', '목', '금', '토'] as const; export const formatOrderDate = (dateString: string | undefined) => { @@ -42,3 +44,7 @@ export const formatExpectedShippingDate = (dateStr: string) => { const weekday = date.toLocaleDateString('ko-KR', { weekday: 'short' }); return `${month}월 ${day}일 (${weekday}) 이내 판매자 발송 예정`; }; + +export const formatDate = (date: Date, format: string) => { + return dayjs(date).format(format); +}; diff --git a/apps/web/src/shared/queries/schedule.ts b/apps/web/src/shared/queries/schedule.ts index c36c19cb..ef2d2c3a 100644 --- a/apps/web/src/shared/queries/schedule.ts +++ b/apps/web/src/shared/queries/schedule.ts @@ -1,23 +1,47 @@ import { queryOptions } from '@tanstack/react-query'; +import { getMemberScheduleList } from '@/entities/schedule/api/getMemberScheduleList'; import { getScheduleCalendar } from '@/entities/schedule/api/getScheduleCalendar'; import { getScheduleList } from '@/entities/schedule/api/getScheduleList'; -import type { CrewScheduleListRequest } from '@/entities/schedule/model/schedule.model'; +import type { + CrewScheduleCalendarRequest, + CrewScheduleListRequest, +} from '@/entities/schedule/model/schedule.model'; export const scheduleQueries = { all: ['schedule'] as const, + listKey: (crewId: number, request?: CrewScheduleListRequest) => + [...scheduleQueries.all, 'getScheduleList', crewId, request] as const, + memberListKey: () => + [...scheduleQueries.all, 'getMemberScheduleList'] as const, getScheduleListQuery: (crewId: number, request?: CrewScheduleListRequest) => queryOptions({ - queryKey: [...scheduleQueries.all, 'getScheduleList', crewId, request], + queryKey: scheduleQueries.listKey(crewId, request), queryFn: async () => { const res = await getScheduleList(crewId, request); if (!res.ok) return []; return res.data.result ?? []; }, }), - getScheduleCalendarQuery: (crewId: number) => + getMemberScheduleListQuery: () => + queryOptions({ + queryKey: scheduleQueries.memberListKey(), + queryFn: async () => { + const res = await getMemberScheduleList(); + if (!res.ok) return []; + return res.data.result ?? []; + }, + }), + getScheduleCalendarQuery: ( + crewId: number, + request?: CrewScheduleCalendarRequest + ) => queryOptions({ queryKey: [...scheduleQueries.all, 'getScheduleCalendar', crewId], - queryFn: () => getScheduleCalendar(crewId), + queryFn: async () => { + const res = await getScheduleCalendar(crewId, request); + if (!res.ok) return []; + return res.data.result ?? []; + }, }), }; diff --git a/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendar.css.ts b/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendar.css.ts index 8b2243d6..4804a974 100644 --- a/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendar.css.ts +++ b/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendar.css.ts @@ -20,11 +20,18 @@ export const calendarHeaderSection = style({ display: 'flex', flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', + justifyContent: 'space-between', gap: '32px', marginBottom: '16px', }); +export const calendarButtonWrapper = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '10px', +}); + export const calendarHeaderButton = style({ display: 'flex', alignItems: 'center', @@ -34,3 +41,26 @@ export const calendarHeaderButton = style({ background: 'transparent', cursor: 'pointer', }); + +export const lightningTile = style({ + width: '6px', + height: '6px', + backgroundColor: vars.colors.secondary, + borderRadius: '100%', +}); + +export const regularTile = style({ + width: '6px', + height: '6px', + backgroundColor: vars.colors.blue60, + borderRadius: '100%', +}); + +export const tileContainer = style({ + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '2px', +}); diff --git a/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendarBase.css.ts b/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendarBase.css.ts index abb7a9b6..681ca0d4 100644 --- a/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendarBase.css.ts +++ b/apps/web/src/widgets/schedule-calendar/style/ScheduleCalendarBase.css.ts @@ -30,7 +30,7 @@ globalStyle('.react-calendar__navigation', { }); globalStyle( - '.react-calendar__month-view__days__day--neighboringMonth, .react-calendar__decade-view__years__year--neighboringDecade, .react-calendar__century-view__decades__decade--neighboringCentury', + '.react-calendar__decade-view__years__year--neighboringDecade, .react-calendar__century-view__decades__decade--neighboringCentury', { display: 'none', } @@ -92,7 +92,8 @@ globalStyle('.react-calendar__tile:disabled', { }); globalStyle('.react-calendar__month-view__days__day--neighboringMonth', { - display: 'none', + visibility: 'hidden', + pointerEvents: 'none', }); globalStyle('.react-calendar__tile--hasActive', { diff --git a/apps/web/src/widgets/schedule-calendar/ui/ScheduleCalendar.tsx b/apps/web/src/widgets/schedule-calendar/ui/ScheduleCalendar.tsx index 94e2cf9a..49646653 100644 --- a/apps/web/src/widgets/schedule-calendar/ui/ScheduleCalendar.tsx +++ b/apps/web/src/widgets/schedule-calendar/ui/ScheduleCalendar.tsx @@ -5,50 +5,76 @@ import '@/widgets/schedule-calendar/style/ScheduleCalendarBase.css.ts'; import * as styles from '@/widgets/schedule-calendar/style/ScheduleCalendar.css.ts'; +import { formatDate } from '@/shared/lib/formatters'; + +import type { ScheduleCalendarList } from '@/entities/schedule/model/schedule.types'; + interface ScheduleCalendarProps { value: Date; onChange: (date: Date) => void; - activeStartDate: Date; - onActiveStartDateChange: (date: Date) => void; + scheduleData?: ScheduleCalendarList; } export function ScheduleCalendar({ value, onChange, - activeStartDate, - onActiveStartDateChange, + scheduleData, }: ScheduleCalendarProps) { + const handlePreviousMonth = () => { + onChange(dayjs(value).subtract(1, 'month').toDate()); + }; + const handleNextMonth = () => { + onChange(dayjs(value).add(1, 'month').toDate()); + }; + + const activeStartDate = dayjs(value).startOf('month').toDate(); + return (
- - {dayjs(activeStartDate).format('YYYY년 M월')} + {formatDate(value, 'YYYY년 M월')} - +
+ + +
{ + if (nextStart) onChange(nextStart); + }} onChange={(value) => { - if (value instanceof Date) { - onChange(value); - } + if (value instanceof Date) onChange(value); }} - activeStartDate={activeStartDate} - onActiveStartDateChange={({ activeStartDate }) => { - if (activeStartDate) { - onActiveStartDateChange(activeStartDate); - } + formatShortWeekday={(_, date) => formatDate(date, 'ddd').toUpperCase()} + formatDay={(_, date) => formatDate(date, 'D')} + tileContent={({ date }: { date: Date }) => { + const schedule = scheduleData?.find((item) => + dayjs(item.date).isSame(date, 'day') + ); + const hasLightning = schedule?.hasLightning; + const hasRegular = schedule?.hasRegular; + return ( +
+ {hasRegular &&
} + {hasLightning &&
} +
+ ); }} - formatShortWeekday={(_, date) => - dayjs(date).format('ddd').toUpperCase() - } - formatDay={(_, date) => dayjs(date).format('D')} />
); diff --git a/apps/web/src/widgets/schedule-calendar/ui/ScheduleWeekCalendar.tsx b/apps/web/src/widgets/schedule-calendar/ui/ScheduleWeekCalendar.tsx index cba34aa4..8dcef07d 100644 --- a/apps/web/src/widgets/schedule-calendar/ui/ScheduleWeekCalendar.tsx +++ b/apps/web/src/widgets/schedule-calendar/ui/ScheduleWeekCalendar.tsx @@ -4,6 +4,8 @@ import Calendar from 'react-calendar'; import '@/widgets/schedule-calendar/style/ScheduleCalendarBase.css.ts'; import * as styles from '@/widgets/schedule-calendar/style/ScheduleWeekCalendar.css.ts'; +import { formatDate } from '@/shared/lib/formatters'; + interface ScheduleWeekCalendarProps { value: Date; onChange: (date: Date) => void; @@ -21,7 +23,7 @@ export function ScheduleWeekCalendar({
- {dayjs(activeStartDate).format('YYYY년 M월')} + {formatDate(activeStartDate, 'YYYY년 M월')}
@@ -34,10 +36,8 @@ export function ScheduleWeekCalendar({ onActiveStartDateChange={({ activeStartDate: nextStart }) => { if (nextStart) onChange(nextStart); }} - formatShortWeekday={(_, date) => - dayjs(date).format('ddd').toUpperCase() - } - formatDay={(_, date) => dayjs(date).format('D')} + formatShortWeekday={(_, date) => formatDate(date, 'ddd').toUpperCase()} + formatDay={(_, date) => formatDate(date, 'D')} tileDisabled={({ date }) => date < startOfWeek || date > endOfWeek} />
diff --git a/apps/web/src/widgets/schedule-filter-tab/ui/ScheduleFilterTab.tsx b/apps/web/src/widgets/schedule-filter-tab/ui/ScheduleFilterTab.tsx index 8b469859..59586261 100644 --- a/apps/web/src/widgets/schedule-filter-tab/ui/ScheduleFilterTab.tsx +++ b/apps/web/src/widgets/schedule-filter-tab/ui/ScheduleFilterTab.tsx @@ -23,7 +23,7 @@ export function ScheduleFilterTab({
{FILTERS.map((filter) => (