Skip to content

Commit 17b5afb

Browse files
authored
Merge pull request #71 from wafflestudio/haram
소셜로그인 URL 수정, 시간표 기능 추가 및 모바일 뷰 개발
2 parents 5429b7b + 6cd478d commit 17b5afb

9 files changed

Lines changed: 545 additions & 19 deletions

File tree

src/pages/auth/Home.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ import { useNavigate } from "react-router-dom";
33
import logo from "/assets/logo.png";
44
import styles from "@styles/Home.module.css";
55

6-
const API_URL = import.meta.env.VITE_API_URL || "";
7-
86
const SOCIAL_LOGIN_ENTRY = {
9-
google: `${API_URL}/auth/login/google`,
10-
kakao: `${API_URL}/auth/login/kakao`,
11-
naver: `${API_URL}/auth/login/naver`,
7+
google: `/oauth2/authorization/google`,
8+
kakao: `/oauth2/authorization/kakao`,
9+
naver: `/oauth2/authorization/naver`,
1210
} as const;
1311

1412
export default function Home() {

src/pages/timetable/TimeTableToolbar.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ interface TimeTableToolbarProps {
1111
SEMESTER_LABEL: { id: Semester; label: string }[];
1212
onYearChange: (year: number) => void;
1313
onSemesterChange: (semester: Semester) => void;
14+
isToggleOn: boolean;
15+
onToggleChange: (isToggleOn: boolean) => void;
1416
years?: number[];
1517
}
1618

@@ -21,6 +23,8 @@ const TimeTableToolbar = ({
2123
SEMESTER_LABEL,
2224
onYearChange,
2325
onSemesterChange,
26+
isToggleOn,
27+
onToggleChange,
2428
years,
2529
}: TimeTableToolbarProps) => {
2630
const { user } = useAuth();
@@ -62,6 +66,17 @@ const TimeTableToolbar = ({
6266
</select>
6367
</span>
6468
<p className={styles.dateTitle}>{timetableName}</p>
69+
<button
70+
type="button"
71+
className={`${styles.timetableToggle} ${
72+
isToggleOn ? styles.timetableToggleOn : ""
73+
}`}
74+
aria-label="시간표 토글"
75+
aria-pressed={isToggleOn}
76+
onClick={() => onToggleChange(!isToggleOn)}
77+
>
78+
<span className={styles.timetableToggleThumb} />
79+
</button>
6580
</div>
6681

6782
<div className={styles.profileRow}>

src/pages/timetable/TimetableGrid.tsx

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
import { useMemo } from "react";
1+
import { useEffect, useMemo, useState } from "react";
22
import {
33
DAY_LABELS_KO,
44
type Day,
55
type GetCoursesResponse,
66
type Course,
7+
type CalendarEvent,
8+
type Event,
79
} from "../../util/types";
810
import type {
911
GridConfig,
1012
TimetableGridBlock,
13+
LayoutedBlock,
14+
} from "../../util/weekly_timetable/layout";
15+
import {
16+
flattenEventsToBlocks,
17+
layoutDayBlocksLane,
1118
} from "../../util/weekly_timetable/layout";
1219
import { formatAmPmFromMinutes } from "../../util/weekly_timetable/time";
20+
import { CATEGORY_COLORS } from "../../util/constants";
1321
import { MdCancel } from "react-icons/md";
1422
import styles from "@styles/Timetable.module.css";
1523

@@ -24,6 +32,9 @@ export type TimetableProps = {
2432
onSelectBlock?: (id: number, item: Course) => void;
2533
onAddBlock?: (id: number, item: Course) => void;
2634
onRemoveBlock?: (timetableId: number, enrollId: number) => Promise<void>;
35+
isSimplified?: boolean;
36+
weekEvents?: CalendarEvent[];
37+
onSelectEvent?: (event: Event) => void;
2738
dayLabels?: Record<Day, string>;
2839
};
2940

@@ -35,23 +46,40 @@ export function TimetableGrid({
3546
config,
3647
toBlocks,
3748
onRemoveBlock,
49+
isSimplified = false,
50+
weekEvents = [],
51+
onSelectEvent,
3852
dayLabels = DAY_LABELS_KO,
3953
}: TimetableProps) {
54+
const [isMobile, setIsMobile] = useState(false);
4055
const blocks = useMemo(
4156
() => toBlocks(items, config),
4257
[items, config, toBlocks],
4358
);
44-
const totalHeight = config.endHour * 60 * config.ppm;
59+
const totalHeight = (config.endHour - config.startHour) * 60 * config.ppm;
60+
61+
useEffect(() => {
62+
const checkIsMobile = () => {
63+
setIsMobile(window.innerWidth <= 576);
64+
};
65+
66+
checkIsMobile();
67+
window.addEventListener("resize", checkIsMobile);
68+
69+
return () => window.removeEventListener("resize", checkIsMobile);
70+
}, []);
4571

4672
const hourMarks = useMemo(() => {
4773
const list: { hour: number; top: number; label: string }[] = [];
4874
for (let h = config.startHour; h <= config.endHour; h++) {
4975
const top = (h * 60 - config.startHour * 60) * config.ppm;
50-
const labelHour = formatAmPmFromMinutes(h * 60);
76+
const labelHour = isMobile
77+
? String(h % 12 || 12)
78+
: formatAmPmFromMinutes(h * 60);
5179
list.push({ hour: h, top, label: labelHour });
5280
}
5381
return list;
54-
}, [config]);
82+
}, [config, isMobile]);
5583

5684
const blocksByDay = useMemo(() => {
5785
const map: Record<Day, TimetableGridBlock<Course>[]> = {
@@ -67,6 +95,28 @@ export function TimetableGrid({
6795
return map;
6896
}, [blocks]);
6997

98+
const eventBlocksByDay = useMemo(() => {
99+
const eventBlocks = flattenEventsToBlocks(
100+
weekEvents.filter((event) => event.allDay !== true),
101+
config,
102+
);
103+
const map: Record<Day, LayoutedBlock[]> = {
104+
0: [],
105+
1: [],
106+
2: [],
107+
3: [],
108+
4: [],
109+
5: [],
110+
6: [],
111+
};
112+
113+
for (const day of Days) {
114+
map[day] = layoutDayBlocksLane(eventBlocks.filter((b) => b.day === day));
115+
}
116+
117+
return map;
118+
}, [weekEvents, config]);
119+
70120
return (
71121
<div className={styles.gridWrap}>
72122
<div className={styles.headerRow}>
@@ -101,6 +151,9 @@ export function TimetableGrid({
101151
config={config}
102152
// onSelectBlock={onSelectBlock}
103153
onRemoveBlock={onRemoveBlock}
154+
isSimplified={isSimplified}
155+
eventBlocks={isSimplified ? eventBlocksByDay[d] : []}
156+
onSelectEvent={onSelectEvent}
104157
/>
105158
))}
106159
</div>
@@ -116,13 +169,19 @@ function DayColumn<T>({
116169
config,
117170
onSelectBlock,
118171
onRemoveBlock,
172+
isSimplified,
173+
eventBlocks,
174+
onSelectEvent,
119175
}: {
120176
timetableId: number;
121177
height: number;
122178
blocks: TimetableGridBlock<T>[];
123179
config: GridConfig;
124180
onSelectBlock?: (id: number, item: T) => void;
125181
onRemoveBlock?: (timetableId: number, enrollId: number) => Promise<void>;
182+
isSimplified: boolean;
183+
eventBlocks: LayoutedBlock[];
184+
onSelectEvent?: (event: Event) => void;
126185
}) {
127186
return (
128187
<div className={styles.dayCol} style={{ height }}>
@@ -131,7 +190,9 @@ function DayColumn<T>({
131190
{blocks.map((b) => (
132191
<button
133192
key={b.id}
134-
className={styles.block}
193+
className={`${styles.block} ${
194+
isSimplified ? styles.simplifiedBlock : ""
195+
}`}
135196
style={{
136197
top: b.top,
137198
height: b.height,
@@ -158,6 +219,35 @@ function DayColumn<T>({
158219
</div>
159220
</button>
160221
))}
222+
223+
{eventBlocks.map((b) => {
224+
const { event } = b.raw.resource;
225+
const color = CATEGORY_COLORS[event.eventTypeId] || CATEGORY_COLORS[6];
226+
227+
return (
228+
<button
229+
key={`event-${b.blockId}-${b.sourceId}`}
230+
className={styles.eventBlock}
231+
style={{
232+
top: b.top,
233+
height: b.height,
234+
left: `${b.leftPct}%`,
235+
width: `${b.widthPct}%`,
236+
backgroundColor: color,
237+
opacity: b.opacity,
238+
zIndex: b.zIndex,
239+
}}
240+
onClick={() => onSelectEvent?.(event)}
241+
type="button"
242+
>
243+
<div className={styles.eventBlockTitle}>{b.title}</div>
244+
<div className={styles.eventBlockTime}>
245+
{formatAmPmFromMinutes(b.startMin)} -{" "}
246+
{formatAmPmFromMinutes(b.endMin)}
247+
</div>
248+
</button>
249+
);
250+
})}
161251
</div>
162252
);
163253
}

src/pages/timetable/TimetablePage.tsx

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { useEffect, useState, useMemo } from "react";
2+
import { Views } from "react-big-calendar";
23
import { useTimetable } from "../../contexts/TimetableContext";
3-
import type { Semester } from "../../util/types";
4+
import { useEvents } from "../../contexts/EventContext";
5+
import { useFilter } from "../../contexts/FilterContext";
6+
import type {
7+
CalendarEvent,
8+
Event,
9+
FetchWeekEventArgs,
10+
Semester,
11+
} from "../../util/types";
412
import { AddClassPanel } from "./AddClassPanel";
513
import {
614
flattenCoursesToBlocks,
715
config,
816
} from "../../util/weekly_timetable/layout";
17+
import calendarEventMapper from "../../util/Calendar/calendarEventMapper";
18+
import { formatDateToYYYYMMDD } from "../../util/Calendar/dateFormatter";
919
import { TimetableGrid } from "./TimetableGrid";
1020
import styles from "@styles/Timetable.module.css";
1121
import { SlArrowLeft } from "react-icons/sl";
@@ -38,12 +48,15 @@ export default function TimetablePage() {
3848
// updateCustomCourse,
3949
deleteCourse,
4050
} = useTimetable();
51+
const { weekViewData, fetchWeekEvents } = useEvents();
52+
const { globalCategory, globalOrg, globalStatus } = useFilter();
4153

4254
const [year, setYear] = useState<number>(now.getFullYear());
4355
const [semester, setSemester] = useState<Semester>("SPRING");
4456
const [tableName, setTableName] = useState<string>("");
4557
const [isAddClassPanelOpen, setIsAddClassPanelOpen] = useState(false);
4658
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
59+
const [isTimetableSimplified, setIsTimetableSimplified] = useState(false);
4760

4861
useEffect(() => {
4962
loadTimetable(year, semester);
@@ -72,12 +85,87 @@ export default function TimetablePage() {
7285
[visibleCourses],
7386
);
7487

88+
useEffect(() => {
89+
const today = new Date();
90+
const from = new Date(today);
91+
from.setDate(from.getDate() - from.getDay());
92+
93+
const to = new Date(from);
94+
to.setDate(to.getDate() + 6);
95+
96+
const params: FetchWeekEventArgs = {
97+
from: formatDateToYYYYMMDD(from),
98+
to: formatDateToYYYYMMDD(to),
99+
};
100+
101+
if (globalCategory) params.eventTypeId = globalCategory.map((g) => g.id);
102+
if (globalOrg) params.orgId = globalOrg.map((g) => g.id);
103+
if (globalStatus) params.statusId = globalStatus.map((g) => g.id);
104+
105+
void fetchWeekEvents(params);
106+
}, [fetchWeekEvents, globalCategory, globalOrg, globalStatus]);
107+
108+
const weekCalendarEvents = useMemo(() => {
109+
const weekStart = new Date();
110+
weekStart.setHours(0, 0, 0, 0);
111+
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
112+
113+
const weekEnd = new Date(weekStart);
114+
weekEnd.setDate(weekEnd.getDate() + 6);
115+
weekEnd.setHours(23, 59, 59, 999);
116+
117+
const rawWeekEvents = Object.values(weekViewData?.byDate || {}).flatMap(
118+
(bucket) => bucket.events,
119+
);
120+
const uniqueWeekEvents = Array.from(
121+
new Map(rawWeekEvents.map((event: Event) => [event.id, event])).values(),
122+
);
123+
124+
return uniqueWeekEvents
125+
.map((event) => calendarEventMapper(event, Views.WEEK) as CalendarEvent)
126+
.filter(
127+
(calendarEvent) =>
128+
calendarEvent.start >= weekStart && calendarEvent.end <= weekEnd,
129+
)
130+
.map((calendarEvent) => {
131+
const sameMinute =
132+
Math.floor(calendarEvent.start.getTime() / 60000) ===
133+
Math.floor(calendarEvent.end.getTime() / 60000);
134+
const startDay = new Date(
135+
calendarEvent.start.getFullYear(),
136+
calendarEvent.start.getMonth(),
137+
calendarEvent.start.getDate(),
138+
);
139+
const endDay = new Date(
140+
calendarEvent.end.getFullYear(),
141+
calendarEvent.end.getMonth(),
142+
calendarEvent.end.getDate(),
143+
);
144+
const differentDate =
145+
startDay.getFullYear() !== endDay.getFullYear() ||
146+
startDay.getMonth() !== endDay.getMonth() ||
147+
startDay.getDate() !== endDay.getDate();
148+
149+
return {
150+
...calendarEvent,
151+
allDay: Boolean(calendarEvent.allDay) || differentDate || sameMinute,
152+
};
153+
})
154+
.filter(
155+
(calendarEvent) =>
156+
calendarEvent.resource.isPeriodEvent === false &&
157+
calendarEvent.allDay === false,
158+
);
159+
}, [weekViewData]);
160+
75161
if (isLoading) return <div>로딩 중...</div>;
76162

77163
return (
78164
<div
79165
className={`${styles.page} ${
80-
isAddClassPanelOpen ? styles.AddClassPanelOpen : styles.AddClassPanelClosed
166+
isAddClassPanelOpen
167+
? styles.AddClassPanelOpen
168+
: styles.AddClassPanelClosed
81169
} ${isSidebarOpen ? styles.sidebarOpen : styles.sidebarClosed}`}
82170
>
83171
<TimeTableSidebar
@@ -104,6 +192,8 @@ export default function TimetablePage() {
104192
SEMESTER_LABEL={semesters}
105193
onSemesterChange={setSemester}
106194
onYearChange={setYear}
195+
isToggleOn={isTimetableSimplified}
196+
onToggleChange={setIsTimetableSimplified}
107197
years={years}
108198
/>
109199

@@ -123,6 +213,8 @@ export default function TimetablePage() {
123213
config={config}
124214
toBlocks={flattenCoursesToBlocks}
125215
onRemoveBlock={deleteCourse}
216+
isSimplified={isTimetableSimplified}
217+
weekEvents={isTimetableSimplified ? weekCalendarEvents : []}
126218
/>
127219
)}
128220
</main>

src/router/AppRoutes.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ export default function AppRoutes() {
2222
<Route path="/auth/complete" element={<CompleteSignUp />} />
2323

2424
{/* OAuth Redirect */}
25-
<Route path="/login/callback/kakao" element={<LoginHandler />} />
26-
<Route path="/login/callback/google" element={<LoginHandler />} />
27-
<Route path="/login/callback/naver" element={<LoginHandler />} />
25+
<Route path="/auth/callback" element={<LoginHandler />} />
2826

2927
{/* Main Feature page */}
3028
<Route path="/main" element={<CalendarView />} />

0 commit comments

Comments
 (0)