diff --git a/.gitignore b/.gitignore index 4d29575de80483b005c29bfcac5061cd2f45313e..8692cf66d2ebc048ca0baeb86f799b1b5907daac 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/src/App.js b/src/App.js index 689e5d3f3195325f0db5445bac6ddb09b0dd5817..43a9c0d0a112c54c6efe32f76601e253004ffd16 100644 --- a/src/App.js +++ b/src/App.js @@ -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 />} /> diff --git a/src/api/schedule.js b/src/api/schedule.js new file mode 100644 index 0000000000000000000000000000000000000000..bb36ad460177c8fe5e8e0aaab245e4dcfd21a78e --- /dev/null +++ b/src/api/schedule.js @@ -0,0 +1,157 @@ +// 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; + } +}; diff --git a/src/pages/Schedule.jsx b/src/pages/Schedule.jsx deleted file mode 100644 index 980b0e5bb47b8cef01e7094d47a3cdd2c503380f..0000000000000000000000000000000000000000 --- a/src/pages/Schedule.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const Schedule = () => { - return <></>; -}; - -export default Schedule; diff --git a/src/pages/SchedulePage.jsx b/src/pages/SchedulePage.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5fa5f28ce29ab4f1a38ee3e07ac4dda555eb0563 --- /dev/null +++ b/src/pages/SchedulePage.jsx @@ -0,0 +1,411 @@ +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; diff --git a/src/pages/TimeTablePage.jsx b/src/pages/TimeTablePage.jsx deleted file mode 100644 index 059584d3d5e907fba65568f3cc000cb8bb4ee053..0000000000000000000000000000000000000000 --- a/src/pages/TimeTablePage.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const TimeTablePage = () => { - return <></>; -}; - -export default TimeTablePage;