Skip to content
Snippets Groups Projects
Commit c4571ab9 authored by 석찬 윤's avatar 석찬 윤
Browse files

[#8] 스케줄 페이지 개발

parent a019b0ce
No related branches found
No related tags found
5 merge requests!13[#7] 채팅방 목록, 채팅방 상세 페이지 디자인, 컴포넌트 구현,!11[Hotfix] 스케줄 타임슬롯 선택 오류 해결 및 로그인 시 UI 개선,!9[#10] 로그인 페이지 배포,!7SchedulePage API Test merge,!6[#8] 스케줄 페이지 개발
......@@ -13,6 +13,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
......
......@@ -4,13 +4,13 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import LoginPage from "./pages/LoginPage";
import SignupPage from "./pages/SignUpPage";
import HomePage from "./pages/HomePage";
import TimeTablePage from "./pages/TimeTablePage";
import ChattingListPage from "./pages/Chatting/ChattingListPage";
import MyPage from "./pages/Mypage";
import HeaderNav from "./components/layout/HeaderNav";
import Footer from "./components/layout/Footer";
import BodyLayout from "./components/layout/BodyLayout";
import HeaderLogoBar from "./components/layout/HeaderLogoBar";
import SchedulePage from "./pages/SchedulePage";
const App = () => {
return (
......@@ -21,7 +21,7 @@ const App = () => {
<BodyLayout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/timetable" element={<TimeTablePage />} />
<Route path="/timetable" element={<SchedulePage />} />
<Route path="/chattinglist" element={<ChattingListPage />} />
<Route path="/mypage" element={<MyPage />} />
<Route path="/login" element={<LoginPage />} />
......
// api.js
const baseURL = process.env.REACT_APP_BACKEND_BASE_URL;
// Fetch all schedules
export const fetchAllSchedules = async () => {
try {
const response = await fetch(`${baseURL}/api/schedule/all`, {
method: "GET",
credentials: "include", // Include credentials for session-based authentication
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error("Failed to fetch schedules.");
}
return result.data.schedules;
} catch (error) {
console.error("Error fetching schedules:", error);
throw error;
}
};
// Fetch schedule by time index
export const fetchScheduleByTimeIndex = async (timeIdx) => {
try {
const response = await fetch(`${baseURL}/api/schedule/${timeIdx}`, {
method: "GET",
credentials: "include", // Include credentials for session-based authentication
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
if (response.status === 404) {
const error = await response.json();
throw new Error(error.error.message || "Schedule not found.");
}
throw new Error(`Error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error("Failed to fetch schedule.");
}
return result.data.schedule;
} catch (error) {
console.error(`Error fetching schedule with timeIdx ${timeIdx}:`, error);
throw error;
}
};
// Create a new schedule
export const createSchedule = async (scheduleData) => {
try {
const response = await fetch(`${baseURL}/api/schedule`, {
method: "POST",
credentials: "include", // Include credentials for session-based authentication
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(scheduleData),
});
if (!response.ok) {
if (response.status === 400) {
const error = await response.json();
throw new Error(error.error.message || "Failed to create schedule.");
}
throw new Error(`Error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error("Failed to create schedule.");
}
return result.data.schedule;
} catch (error) {
console.error("Error creating schedule:", error);
throw error;
}
};
// Update an existing schedule
export const updateSchedule = async (scheduleData) => {
try {
const response = await fetch(`${baseURL}/api/schedule`, {
method: "PUT",
credentials: "include", // Include credentials for session-based authentication
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(scheduleData),
});
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error("Failed to update schedule.");
}
return result.data.schedule;
} catch (error) {
console.error("Error updating schedule:", error);
throw error;
}
};
// Delete a schedule
export const deleteSchedule = async (title) => {
try {
const response = await fetch(`${baseURL}/api/schedule`, {
method: "DELETE",
credentials: "include", // Include credentials for session-based authentication
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title }),
});
if (!response.ok) {
if (response.status === 404) {
const error = await response.json();
throw new Error(error.error.message || "Schedule not found.");
}
throw new Error(`Error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error("Failed to delete schedule.");
}
return result.data;
} catch (error) {
console.error("Error deleting schedule:", error);
throw error;
}
};
import React from "react";
const Schedule = () => {
return <></>;
};
export default Schedule;
import React, { useEffect, useState } from "react";
import Label from "../components/Label";
import {
createSchedule,
deleteSchedule,
fetchScheduleByTimeIndex,
fetchAllSchedules,
updateSchedule,
} from "../api/schedule";
const generateTimeSlots = () => {
const timeSlots = [];
for (let hour = 0; hour < 24; hour++) {
for (let min = 0; min < 60; min += 15) {
timeSlots.push(
`${hour.toString().padStart(2, "0")}:${min.toString().padStart(2, "0")}`
);
}
}
return timeSlots;
};
const days = ["", "", "", "", "", "", ""];
// const dummySchedules = [
// {
// id: 1,
// user_id: 1,
// title: "알고리즘 스터디",
// is_fixed: true,
// time_indices: [36, 37, 38, 39],
// createdAt: "2024-12-02T09:52:00.000Z",
// updatedAt: "2024-12-02T09:52:00.000Z",
// },
// {
// id: 5,
// user_id: 1,
// title: "웹시설 팀플",
// is_fixed: true,
// time_indices: [165, 166, 167, 255, 256, 257],
// createdAt: "2024-12-02T09:54:53.000Z",
// updatedAt: "2024-12-02T09:54:53.000Z",
// },
// ];
const SchedulePage = () => {
const timeSlots = generateTimeSlots();
const [schedules, setSchedules] = useState([]);
const [isEditMode, setIsEditMode] = useState(false);
const [isUpdateMode, setIsUpdateMode] = useState(false);
const [selectedSchedule, setSelectedSchedule] = useState(null);
const [selectedSlots, setSelectedSlots] = useState([]);
const [newTitle, setNewTitle] = useState("");
const [isFixed, setIsFixed] = useState(true);
useEffect(() => {
// API
const initializeSchedules = async () => {
try {
const data = await fetchAllSchedules();
setSchedules(data);
} catch (error) {
console.error("Failed to load schedules", error);
}
};
initializeSchedules();
// 임시 코드
// setSchedules(dummySchedules);
}, []);
const handleSlotClick = async (timeIdx) => {
if (!isEditMode) return;
// API
try {
const response = await fetchScheduleByTimeIndex(timeIdx);
if (response && response.data && response.data.schedule) {
setSelectedSchedule(response.data.schedule); // API로 가져온 스케줄 설정
} else {
console.error("No schedule found for time index:", timeIdx);
}
} catch (error) {
console.error("Failed to fetch schedule for time index:", timeIdx, error);
}
// 임시 코드
// const slotInSchedule = schedules.find((s) =>
// s.time_indices.includes(timeIdx)
// );
// if (slotInSchedule) {
// if (selectedSlots.length === 0) {
// setSelectedSchedule(slotInSchedule);
// }
// return;
// }
// if (selectedSlots.includes(timeIdx)) {
// setSelectedSlots((prev) => prev.filter((idx) => idx !== timeIdx));
// } else {
// setSelectedSlots((prev) => [...prev, timeIdx]);
// }
};
const handleCancelSchedule = () => {
setSelectedSlots([]);
setNewTitle("");
setIsFixed(true);
setSelectedSchedule(null);
};
const handleEditSchedule = () => {
setIsUpdateMode(true);
setSchedules((prev) =>
prev.filter((s) => s.title !== selectedSchedule.title)
);
setSelectedSlots(selectedSchedule.time_indices);
setNewTitle(selectedSchedule.title);
setIsFixed(selectedSchedule.is_fixed);
};
const handleUpdateSchedule = async () => {
try {
const scheduleData = {
originalTitle: selectedSchedule.title,
title: newTitle,
is_fixed: isFixed,
time_indices: selectedSlots,
};
// API
const newSchedule = await updateSchedule(scheduleData);
// 임시 코드
// const newSchedule = {
// ...scheduleData,
// id: Date.now(),
// createdAt: new Date().toISOString(),
// updatedAt: new Date().toISOString(),
// };
setSchedules((prev) => [...prev, newSchedule]);
setSelectedSchedule(newSchedule);
setSelectedSlots([]);
setNewTitle("");
setIsFixed(true);
alert("스케줄을 수정했습니다.!");
} catch (error) {
console.error("스케줄 수정에 실패했습니다.:", error);
alert("스케줄 수정에 실패했습니다.");
} finally {
setIsUpdateMode(false);
}
};
const handleCreateSchedule = async () => {
try {
const scheduleData = {
title: newTitle,
is_fixed: isFixed,
time_indices: selectedSlots,
};
// API
const newSchedule = await createSchedule(scheduleData);
// 임시코드
// const newSchedule = {
// ...scheduleData,
// id: Date.now(),
// createdAt: new Date().toISOString(),
// updatedAt: new Date().toISOString(),
// };
setSchedules((prev) => [...prev, newSchedule]);
setSelectedSlots([]);
setNewTitle("");
setIsFixed(true);
alert("스케줄이 추가되었습니다!");
} catch (error) {
console.error("스케줄 삭제에 실패했습니다:", error);
alert("스케줄 추가에 실패했습니다.");
}
};
const handleDeleteSchedule = async () => {
if (!selectedSchedule) return;
try {
const body = { title: selectedSchedule.title };
// API 호출 준비가 되었을 때 사용:
await deleteSchedule(body);
const updatedSchedules = await fetchAllSchedules();
setSchedules(updatedSchedules);
// 임시코드
// setSchedules((prev) =>
// prev.filter((s) => s.title !== selectedSchedule.title)
// );
setSelectedSchedule(null);
alert("스케줄이 삭제되었습니다.");
} catch (error) {
console.error("스케줄 삭제에 실패했습니다:", error);
}
};
return (
<div className="min-h-screen bg-grayscale-50">
{/* Toggle View/Edit Mode */}
<div className="flex items-center justify-between p-4 bg-white shadow">
<h1 className="heading-1">Schedule</h1>
<label className="flex items-center space-x-3 cursor-pointer">
<span className="title-1 text-primary-500">Edit Mode</span>
<div
className={`relative w-12 h-6 rounded-full transition-colors ${
isEditMode ? "bg-primary-500" : "bg-grayscale-300"
}`}
onClick={() => {
setIsEditMode((prev) => !prev);
setSelectedSlots([]);
setSelectedSchedule(null);
}}
>
<div
className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform ${
isEditMode ? "translate-x-6" : ""
}`}
></div>
</div>
</label>
</div>
{/* Sticky Container in Edit Mode */}
{isEditMode && (
<div className="fixed bottom-0 right-0 flex items-center justify-center w-full ">
<div
className={`transform transition-transform w-full max-w-[768px] tablet:rounded-2xl bg-primary-100 p-6 text-center shadow-lg`}
>
{selectedSlots.length === 0 && selectedSchedule ? (
<div className="flex flex-col items-center justify-center w-full">
<h3 className="mb-2 heading-2 text-primary-500">스케줄 정보</h3>
<div className="flex flex-col items-start w-1/2">
<p className="mb-1 body-1">
<strong>제목:</strong> {selectedSchedule.title}
</p>
<p className="mb-1 body-1">
<strong>스케줄 타입:</strong>{" "}
{selectedSchedule.is_fixed ? "고정" : "유동"}
</p>
<div className="mb-4 body-1">
<strong>선택된 시간:</strong>{" "}
{selectedSchedule.time_indices.map((time_idx) => (
<Label key={time_idx} theme="indigo" size="sm">
{time_idx}
</Label>
))}
</div>
</div>
<div className="flex justify-center mt-4 space-x-4">
<button
className="px-4 py-2 font-bold text-white rounded bg-gradient-purple"
onClick={handleEditSchedule}
>
수정하기
</button>
<button
className="px-4 py-2 font-bold text-white rounded bg-gradient-pink"
onClick={handleDeleteSchedule}
>
삭제하기
</button>
</div>
</div>
) : (
<>
{isUpdateMode ? (
<h3 className="mb-4 heading-2 text-primary-500">
스케줄 수정하기
</h3>
) : (
<h3 className="mb-4 heading-2 text-primary-500">
새 스케줄 만들기
</h3>
)}
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Enter title"
className="w-full p-2 mb-4 border rounded shadow-input-box"
/>
<div className="flex items-center justify-center mb-4 space-x-4">
<label className="flex items-center space-x-2">
<input
type="radio"
name="is_fixed"
value={true}
checked={isFixed === true}
onChange={() => setIsFixed(true)}
/>
<span className="body-1">고정 스케줄</span>
</label>
<label className="flex items-center space-x-2">
<input
type="radio"
name="is_fixed"
value={false}
checked={isFixed === false}
onChange={() => setIsFixed(false)}
/>
<span className="body-1">유동 스케줄</span>
</label>
</div>
<div className="mb-4 body-1">
<span>선택된 시간:</span>
{selectedSlots.map((time_idx) => (
<Label key={time_idx} theme="indigo" size="sm">
{time_idx}
</Label>
))}
</div>
{isUpdateMode ? (
<button
className="px-4 py-2 font-bold text-white rounded bg-gradient-pink"
onClick={() => handleUpdateSchedule()}
>
수정 완료
</button>
) : (
<div className="flex justify-center mt-4 space-x-4">
<button
className="px-4 py-2 font-bold text-white rounded bg-tertiary-900"
onClick={() => handleCancelSchedule()}
>
취소
</button>
<button
className="px-4 py-2 font-bold text-white rounded bg-gradient-pink"
onClick={() => handleCreateSchedule()}
>
추가
</button>
</div>
)}
</>
)}
</div>
</div>
)}
{/* Schedule Grid */}
<div className="p-4">
<div className="overflow-auto scrollbar-hide">
<div className="w-[100vw] tablet:w-[960px] grid grid-cols-[64px,repeat(7,1fr)] gap-2">
{/* Header */}
<div className="min-w-[54px] p-2 font-bold text-center bg-grayscale-200 select-none">
Time
</div>
{days.map((day) => (
<div
key={day}
className="p-2 font-bold text-center select-none bg-grayscale-200"
>
{day}
</div>
))}
{/* Time Slots */}
{timeSlots.map((time, rowIndex) => (
<React.Fragment key={rowIndex}>
{/* Time Column */}
<div className="min-w-[54px] p-2 font-bold text-center bg-grayscale-100 select-none">
{time}
</div>
{days.map((_, colIndex) => {
const slotIndex = colIndex * timeSlots.length + rowIndex;
const isSelected = selectedSlots.includes(slotIndex);
const schedule = schedules.find((s) =>
s.time_indices.includes(slotIndex)
);
return (
<div
key={slotIndex}
className={`p-2 border rounded ${
schedule
? "bg-primary-300 text-white cursor-not-allowed"
: isSelected
? "bg-primary-100 border-primary-300"
: "bg-grayscale-50 cursor-pointer"
}`}
onClick={() => handleSlotClick(slotIndex)}
>
{schedule ? schedule.title : ""}
</div>
);
})}
</React.Fragment>
))}
</div>
</div>
</div>
</div>
);
};
export default SchedulePage;
import React from "react";
const TimeTablePage = () => {
return <></>;
};
export default TimeTablePage;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment