From c4571ab9fd99fd98ddffcd11cdd5aa5b562ce93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9D=EC=B0=AC=20=EC=9C=A4?= <ysc0731@ajou.ac.kr> Date: Fri, 6 Dec 2024 02:38:44 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[#8]=20=EC=8A=A4=EC=BC=80=EC=A4=84=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/App.js | 4 +- src/api/schedule.js | 157 ++++++++++++++ src/pages/Schedule.jsx | 7 - src/pages/SchedulePage.jsx | 411 ++++++++++++++++++++++++++++++++++++ src/pages/TimeTablePage.jsx | 7 - 6 files changed, 571 insertions(+), 16 deletions(-) create mode 100644 src/api/schedule.js delete mode 100644 src/pages/Schedule.jsx create mode 100644 src/pages/SchedulePage.jsx delete mode 100644 src/pages/TimeTablePage.jsx diff --git a/.gitignore b/.gitignore index 4d29575..8692cf6 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 689e5d3..43a9c0d 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 0000000..bb36ad4 --- /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 980b0e5..0000000 --- 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 0000000..5fa5f28 --- /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 059584d..0000000 --- a/src/pages/TimeTablePage.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const TimeTablePage = () => { - return <></>; -}; - -export default TimeTablePage; -- GitLab From 6a641de707e6dd9f81c3236ef1901e56afac9218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9D=EC=B0=AC=20=EC=9C=A4?= <ysc0731@ajou.ac.kr> Date: Sat, 7 Dec 2024 16:37:55 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[#10]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 31 ++++++++- package.json | 3 +- src/App.js | 2 - src/api/auth.js | 54 +++++++++++++++ src/api/schedule.js | 2 +- src/components/icons/GoogleLogoIcon.jsx | 28 ++++++++ src/components/layout/HeaderLogoBar.jsx | 14 +++- src/components/layout/HeaderNav.jsx | 42 +++++++----- src/pages/LoginPage.jsx | 89 ++++++++++++++++++++++++- src/pages/SignUpPage.jsx | 7 -- src/store/authStore.js | 30 +++++++++ 11 files changed, 270 insertions(+), 32 deletions(-) create mode 100644 src/api/auth.js create mode 100644 src/components/icons/GoogleLogoIcon.jsx delete mode 100644 src/pages/SignUpPage.jsx create mode 100644 src/store/authStore.js diff --git a/package-lock.json b/package-lock.json index d9e2f70..671b138 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,8 @@ "workbox-range-requests": "^6.6.0", "workbox-routing": "^6.6.0", "workbox-strategies": "^6.6.0", - "workbox-streams": "^6.6.0" + "workbox-streams": "^6.6.0", + "zustand": "^5.0.2" }, "devDependencies": { "tailwindcss": "^3.4.15" @@ -16441,6 +16442,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", + "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 032f68f..a8aeba3 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "workbox-range-requests": "^6.6.0", "workbox-routing": "^6.6.0", "workbox-strategies": "^6.6.0", - "workbox-streams": "^6.6.0" + "workbox-streams": "^6.6.0", + "zustand": "^5.0.2" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.js b/src/App.js index 43a9c0d..af5e2cb 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,6 @@ import React from "react"; import "./styles/globals.css"; 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 ChattingListPage from "./pages/Chatting/ChattingListPage"; import MyPage from "./pages/Mypage"; @@ -25,7 +24,6 @@ const App = () => { <Route path="/chattinglist" element={<ChattingListPage />} /> <Route path="/mypage" element={<MyPage />} /> <Route path="/login" element={<LoginPage />} /> - <Route path="/signup" element={<SignupPage />} /> </Routes> </BodyLayout> <Footer /> diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 0000000..df4042f --- /dev/null +++ b/src/api/auth.js @@ -0,0 +1,54 @@ +// src/api/auth.js + +/** + * Google 로그인 URL 반환 + * @returns {string} 로그인 엔드포인트 URL + */ +export const getLoginUrl = () => { + return `${process.env.REACT_APP_BASE_URL}/api/auth/login`; +}; + +/** + * 로그아웃 API 호출 + * @returns {Promise<void>} + */ +export const logout = async () => { + try { + const response = await fetch( + `${process.env.REACT_APP_BASE_URL}/api/auth/logout`, + { + method: "GET", + credentials: "include", // 세션 쿠키 포함 + } + ); + if (!response.ok) { + throw new Error("Failed to logout"); + } + } catch (error) { + console.error("Error during logout:", error); + throw error; + } +}; + +/** + * 세션 정보 확인 API 호출 + * @returns {Promise<Object|null>} 세션이 있으면 사용자 정보 반환, 없으면 null 반환 + */ +export const getSessionInfo = async () => { + try { + const response = await fetch( + `${process.env.REACT_APP_BASE_URL}/api/session/info`, + { + method: "GET", + credentials: "include", // 세션 쿠키 포함 + } + ); + if (response.ok) { + return await response.json(); // 사용자 정보 반환 + } + return null; // 세션 없음 + } catch (error) { + console.error("Error checking session info:", error); + throw error; + } +}; diff --git a/src/api/schedule.js b/src/api/schedule.js index bb36ad4..c59a852 100644 --- a/src/api/schedule.js +++ b/src/api/schedule.js @@ -1,5 +1,5 @@ // api.js -const baseURL = process.env.REACT_APP_BACKEND_BASE_URL; +const baseURL = process.env.REACT_APP_BASE_URL; // Fetch all schedules export const fetchAllSchedules = async () => { diff --git a/src/components/icons/GoogleLogoIcon.jsx b/src/components/icons/GoogleLogoIcon.jsx new file mode 100644 index 0000000..a28ef03 --- /dev/null +++ b/src/components/icons/GoogleLogoIcon.jsx @@ -0,0 +1,28 @@ +export default function GoogleLogo({ className, ...props }) { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + > + <path + d="M16.5883 17.58C18.951 15.3748 20.0011 11.6995 19.371 8.18164H9.97266V12.067H15.3281C15.1181 13.3271 14.3831 14.3772 13.333 15.0598L16.5883 17.58Z" + fill="#4285F4" + /> + <path + d="M1.04688 14.4824C1.73758 15.843 2.72813 17.0291 3.94387 17.9512C5.15962 18.8733 6.56882 19.5074 8.06526 19.8056C9.5617 20.1039 11.1063 20.0586 12.5827 19.6731C14.0591 19.2876 15.4287 18.572 16.5883 17.5802L13.333 15.06C10.5502 16.8976 5.92982 16.2151 4.35467 11.9097L1.04688 14.4824Z" + fill="#34A853" + /> + <path + d="M4.35584 11.9095C3.93581 10.5969 3.93581 9.38926 4.35584 8.07664L1.04804 5.50391C-0.159565 7.91912 -0.527099 11.3319 1.04804 14.4822L4.35584 11.9095Z" + fill="#FBBC02" + /> + <path + d="M4.35467 8.07674C5.50978 4.45391 10.4452 2.35372 13.753 5.4515L16.6408 2.61624C12.5454 -1.32161 4.56469 -1.1641 1.04688 5.50401L4.35467 8.07674Z" + fill="#EA4335" + /> + </svg> + ); +} diff --git a/src/components/layout/HeaderLogoBar.jsx b/src/components/layout/HeaderLogoBar.jsx index 0b8aef8..4e08f2d 100644 --- a/src/components/layout/HeaderLogoBar.jsx +++ b/src/components/layout/HeaderLogoBar.jsx @@ -1,12 +1,24 @@ +import React from "react"; import LogoIcon from "../icons/LogoIcon"; +import useAuthStore from "../../store/authStore"; const HeaderLogoBar = () => { + const { user } = useAuthStore(); // Zustand에서 user 상태 가져오기 + return ( - <div className="flex items-center justify-start w-full h-16 px-4 bg-white"> + <div className="flex items-center justify-between w-full h-16 px-4 bg-white"> + {/* 왼쪽: 로고와 앱 이름 */} <div className="flex items-center"> <LogoIcon width={32} height={32} /> <span className="title-1">YANAWA</span> </div> + + {/* 오른쪽: 사용자 이름 */} + <div className="flex items-center"> + <span className="text-gray-600 label-1"> + {user ? `${user.name}` : "guest"} 님 + </span> + </div> </div> ); }; diff --git a/src/components/layout/HeaderNav.jsx b/src/components/layout/HeaderNav.jsx index e1d5d5d..2dba5ad 100644 --- a/src/components/layout/HeaderNav.jsx +++ b/src/components/layout/HeaderNav.jsx @@ -2,10 +2,12 @@ import React, { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import Button from "../Button"; import LogoIcon from "../icons/LogoIcon"; +import useAuthStore from "../../store/authStore"; export default function HeaderNav() { const navigate = useNavigate(); const [isMobile, setIsMobile] = useState(false); + const { user } = useAuthStore(); // Zustand에서 user 상태 가져오기 useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth <= 768); @@ -18,6 +20,7 @@ export default function HeaderNav() { const navigateToHome = () => navigate("/"); const navigateToChattingList = () => navigate("/chattinglist"); const navigateToLogin = () => navigate("/login"); + const navigateToMyPage = () => navigate("/mypage"); return ( <header className="bg-white shadow-md"> @@ -47,20 +50,14 @@ export default function HeaderNav() { size="icon" theme="black" icon={<LogoIcon fillColor="#ffffff" />} - onClick={navigateToLogin} + onClick={user ? navigateToMyPage : navigateToLogin} // 조건부 이동 /> </> ) : ( <> - <Button - size="icon" - theme="pink" - icon={<LogoIcon fillColor="#ffffff" />} - onClick={navigateToHome} - /> <Button size="lg" - theme="purple" + theme="pink" icon={<LogoIcon fillColor="#ffffff" />} onClick={navigateToHome} > @@ -68,7 +65,7 @@ export default function HeaderNav() { </Button> <Button size="lg" - theme="indigo" + theme="purple" icon={<LogoIcon fillColor="#ffffff" />} onClick={navigateToTimeTable} > @@ -82,14 +79,25 @@ export default function HeaderNav() { > 번개채팅방 </Button> - <Button - size="lg" - theme="black" - icon={<LogoIcon fillColor="#ffffff" />} - onClick={navigateToLogin} - > - 로그인 - </Button> + {user ? ( + <Button + size="lg" + theme="black" + icon={<LogoIcon fillColor="#ffffff" />} + onClick={navigateToMyPage} + > + 마이페이지 + </Button> + ) : ( + <Button + size="lg" + theme="black" + icon={<LogoIcon fillColor="#ffffff" />} + onClick={navigateToLogin} + > + 로그인 + </Button> + )} </> )} </div> diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index f93bbc0..00aa883 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,7 +1,92 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import GoogleLogo from "../components/icons/GoogleLogoIcon"; +import Button from "../components/Button"; +import { getLoginUrl } from "../api/auth"; +import useAuthStore from "../store/authStore"; const LoginPage = () => { - return <></>; + const { user, fetchSession, logoutUser } = useAuthStore(); + const [loading, setLoading] = useState(true); + + // 페이지 로드 시 세션 확인 + useEffect(() => { + const fetchSessionInfo = async () => { + setLoading(true); + try { + await fetchSession(); // 세션 정보 가져오기 + } catch (error) { + console.error("Failed to fetch session info:", error); + } finally { + setLoading(false); + } + }; + + fetchSessionInfo(); + }, [fetchSession]); + + // Google 로그인 처리 + const handleGoogleLogin = () => { + const loginUrl = getLoginUrl(); // 로그인 URL 가져오기 + window.location.href = loginUrl; // 리다이렉트 + }; + + // 로그아웃 처리 + const handleLogout = async () => { + try { + setLoading(true); + await logoutUser(); + } catch (error) { + console.error("Failed to logout:", error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( + <div className="flex items-center justify-center min-h-screen"> + <p>Loading...</p> + </div> + ); + } + + if (user) { + return ( + <div className="flex items-center justify-center min-h-screen p-4 bg-gray-100"> + <div className="flex flex-col items-center justify-center min-w-[260px] w-1/2 max-w-md p-8 space-y-4 bg-white rounded-lg shadow-lg"> + <div> + <h2 className="text-center heading-2">환영합니다!</h2> + <h2 className="text-center heading-1"> + <span className="text-primary-500">{user.name}</span>님 + </h2> + </div> + <p className="text-center text-gray-700"> + <span className="text-primary-500 title-1">번개모임</span>을 생성해 + 보세요! + </p> + <Button size="md" theme="black" onClick={handleLogout}> + 로그아웃 + </Button> + </div> + </div> + ); + } + + return ( + <div className="flex items-center justify-center min-h-screen p-4 bg-gray-100"> + <div className="flex flex-col items-center min-w-[260px] justify-center w-1/2 max-w-md p-8 space-y-6 bg-white rounded-lg shadow-lg"> + <h2 className="text-2xl font-bold text-center text-gray-900">로그인</h2> + <Button + size="md" + theme="white" + icon={<GoogleLogo />} + onClick={handleGoogleLogin} + > + 구글로 로그인 + </Button> + </div> + </div> + ); }; export default LoginPage; diff --git a/src/pages/SignUpPage.jsx b/src/pages/SignUpPage.jsx deleted file mode 100644 index 4a4bbb5..0000000 --- a/src/pages/SignUpPage.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const SignupPage = () => { - return <></>; -}; - -export default SignupPage; diff --git a/src/store/authStore.js b/src/store/authStore.js new file mode 100644 index 0000000..1d1223b --- /dev/null +++ b/src/store/authStore.js @@ -0,0 +1,30 @@ +import { create } from "zustand"; +import { getSessionInfo, logout } from "../api/auth"; + +const useAuthStore = create((set) => ({ + user: null, // 사용자 정보 + // user: { name: "윤석찬", email: "ysc0731@ajou.ac.kr" }, // 사용자 정보 + + // 세션 정보 가져오기 + fetchSession: async () => { + try { + const userInfo = await getSessionInfo(); + set({ user: userInfo }); + } catch (error) { + console.error("Failed to fetch session info:", error); + set({ user: null }); + } + }, + + // 로그아웃 처리 + logoutUser: async () => { + try { + await logout(); + set({ user: null }); + } catch (error) { + console.error("Failed to logout:", error); + } + }, +})); + +export default useAuthStore; -- GitLab From a4a9e3e0a66490a16e287f7845f87db15f611741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9D=EC=B0=AC=20=EC=9C=A4?= <ysc0731@ajou.ac.kr> Date: Sun, 8 Dec 2024 02:31:41 +0900 Subject: [PATCH 3/6] =?UTF-8?q?hotfix:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9E=84=EC=8A=AC?= =?UTF-8?q?=EB=A1=AF=20=EC=84=A0=ED=83=9D=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SchedulePage.jsx | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/pages/SchedulePage.jsx b/src/pages/SchedulePage.jsx index 5fa5f28..2f41569 100644 --- a/src/pages/SchedulePage.jsx +++ b/src/pages/SchedulePage.jsx @@ -74,34 +74,34 @@ const SchedulePage = () => { 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); - } + // 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) - // ); + const slotInSchedule = schedules.find((s) => + s.time_indices.includes(timeIdx) + ); - // if (slotInSchedule) { - // if (selectedSlots.length === 0) { - // setSelectedSchedule(slotInSchedule); - // } - // return; - // } + 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]); - // } + if (selectedSlots.includes(timeIdx)) { + setSelectedSlots((prev) => prev.filter((idx) => idx !== timeIdx)); + } else { + setSelectedSlots((prev) => [...prev, timeIdx]); + } }; const handleCancelSchedule = () => { -- GitLab From 443cc7ee623da58ad97c645368f552b05e8449e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9D=EC=B0=AC=20=EC=9C=A4?= <ysc0731@ajou.ac.kr> Date: Sun, 8 Dec 2024 02:32:07 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=ED=99=95=EC=9D=B8=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/HeaderLogoBar.jsx | 31 ++++++++++++--- src/pages/HomePage.jsx | 27 ++++++++++++- src/pages/LoginPage.jsx | 50 +++++++++---------------- src/store/authStore.js | 5 ++- 4 files changed, 72 insertions(+), 41 deletions(-) diff --git a/src/components/layout/HeaderLogoBar.jsx b/src/components/layout/HeaderLogoBar.jsx index 4e08f2d..5772d41 100644 --- a/src/components/layout/HeaderLogoBar.jsx +++ b/src/components/layout/HeaderLogoBar.jsx @@ -1,23 +1,42 @@ -import React from "react"; +import React, { useState } from "react"; import LogoIcon from "../icons/LogoIcon"; import useAuthStore from "../../store/authStore"; +import Button from "../Button"; const HeaderLogoBar = () => { - const { user } = useAuthStore(); // Zustand에서 user 상태 가져오기 + const { user, logoutUser } = useAuthStore(); // Zustand에서 상태 및 메서드 가져오기 + const [loading, setLoading] = useState(false); // 로딩 상태 관리 + + // 로그아웃 처리 + const handleLogout = async () => { + try { + setLoading(true); // 로딩 상태 활성화 + await logoutUser(); // 로그아웃 실행 + } catch (error) { + console.error("Failed to logout:", error); + } finally { + setLoading(false); // 로딩 상태 비활성화 + } + }; return ( - <div className="flex items-center justify-between w-full h-16 px-4 bg-white"> + <div className="flex items-center justify-between w-full h-16 px-4 bg-white shadow-md"> {/* 왼쪽: 로고와 앱 이름 */} <div className="flex items-center"> <LogoIcon width={32} height={32} /> - <span className="title-1">YANAWA</span> + <span className="ml-2 text-lg font-bold text-gray-900">YANAWA</span> </div> - {/* 오른쪽: 사용자 이름 */} + {/* 오른쪽: 사용자 정보 및 로그아웃 */} <div className="flex items-center"> - <span className="text-gray-600 label-1"> + <span className="mr-3 text-gray-600 label-1"> {user ? `${user.name}` : "guest"} 님 </span> + {user && ( + <Button size="sm" onClick={handleLogout} disabled={loading}> + {loading ? "로그아웃 중..." : "로그아웃"} + </Button> + )} </div> </div> ); diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index 68fc933..01e46fe 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -1,9 +1,32 @@ -import React from "react"; +import React, { useEffect } from "react"; +import useAuthStore from "../store/authStore"; const HomePage = () => { + const { user, fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기 + + useEffect(() => { + const fetchUserSession = async () => { + try { + await fetchSession(); // 세션 정보 가져오기 + } catch (error) { + console.error("Failed to fetch session:", error); + } + }; + + fetchUserSession(); + }, [fetchSession]); // 페이지 마운트 시 실행 + return ( - <div className="w-full h-screen flex items-center justify-center"> + <div className="flex flex-col items-center justify-center w-full h-screen space-y-4"> <h1 className="heading-1">야나와 홈페이지</h1> + {user ? ( + <p className="text-lg text-gray-700"> + 안녕하세요,{" "} + <span className="font-bold text-primary-500">{user.name}</span> 님! + </p> + ) : ( + <p className="text-lg text-gray-700">로그인이 필요합니다.</p> + )} </div> ); }; diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 00aa883..f2ad8ff 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,27 +1,15 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import GoogleLogo from "../components/icons/GoogleLogoIcon"; import Button from "../components/Button"; import { getLoginUrl } from "../api/auth"; import useAuthStore from "../store/authStore"; const LoginPage = () => { - const { user, fetchSession, logoutUser } = useAuthStore(); - const [loading, setLoading] = useState(true); + const { user, fetchSession, logoutUser, loading } = useAuthStore(); // Zustand 상태 가져오기 // 페이지 로드 시 세션 확인 useEffect(() => { - const fetchSessionInfo = async () => { - setLoading(true); - try { - await fetchSession(); // 세션 정보 가져오기 - } catch (error) { - console.error("Failed to fetch session info:", error); - } finally { - setLoading(false); - } - }; - - fetchSessionInfo(); + fetchSession(); // 세션 정보 가져오기 }, [fetchSession]); // Google 로그인 처리 @@ -32,37 +20,34 @@ const LoginPage = () => { // 로그아웃 처리 const handleLogout = async () => { - try { - setLoading(true); - await logoutUser(); - } catch (error) { - console.error("Failed to logout:", error); - } finally { - setLoading(false); - } + await logoutUser(); // 로그아웃 실행 }; + // 로딩 상태 처리 if (loading) { return ( - <div className="flex items-center justify-center min-h-screen"> - <p>Loading...</p> + <div className="flex items-center justify-center min-h-screen bg-gray-100"> + <p className="text-lg text-gray-600">로딩 중...</p> </div> ); } + // 로그인된 상태 if (user) { return ( <div className="flex items-center justify-center min-h-screen p-4 bg-gray-100"> - <div className="flex flex-col items-center justify-center min-w-[260px] w-1/2 max-w-md p-8 space-y-4 bg-white rounded-lg shadow-lg"> + <div className="flex flex-col items-center min-w-[260px] w-1/2 max-w-md p-8 space-y-4 bg-white rounded-lg shadow-lg"> <div> - <h2 className="text-center heading-2">환영합니다!</h2> - <h2 className="text-center heading-1"> - <span className="text-primary-500">{user.name}</span>님 + <h2 className="text-2xl font-semibold text-center text-gray-900"> + 환영합니다! + </h2> + <h2 className="text-xl font-bold text-center text-primary-500"> + {user.name}님 </h2> </div> - <p className="text-center text-gray-700"> - <span className="text-primary-500 title-1">번개모임</span>을 생성해 - 보세요! + <p className="text-center text-gray-600"> + <span className="font-bold text-primary-500">번개모임</span>을 + 생성하거나 참여해 보세요! </p> <Button size="md" theme="black" onClick={handleLogout}> 로그아웃 @@ -72,6 +57,7 @@ const LoginPage = () => { ); } + // 비로그인 상태 return ( <div className="flex items-center justify-center min-h-screen p-4 bg-gray-100"> <div className="flex flex-col items-center min-w-[260px] justify-center w-1/2 max-w-md p-8 space-y-6 bg-white rounded-lg shadow-lg"> diff --git a/src/store/authStore.js b/src/store/authStore.js index 1d1223b..05da3b6 100644 --- a/src/store/authStore.js +++ b/src/store/authStore.js @@ -3,16 +3,19 @@ import { getSessionInfo, logout } from "../api/auth"; const useAuthStore = create((set) => ({ user: null, // 사용자 정보 - // user: { name: "윤석찬", email: "ysc0731@ajou.ac.kr" }, // 사용자 정보 + loading: true, // 로딩 상태 // 세션 정보 가져오기 fetchSession: async () => { + set({ loading: true }); try { const userInfo = await getSessionInfo(); set({ user: userInfo }); } catch (error) { console.error("Failed to fetch session info:", error); set({ user: null }); + } finally { + set({ loading: false }); } }, -- GitLab From e6e575293c6a8fcfd730aabb14eb9c945939470e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9D=EC=B0=AC=20=EC=9C=A4?= <ysc0731@ajou.ac.kr> Date: Sun, 8 Dec 2024 02:34:30 +0900 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20merge=20request=20template=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../merge_request_templates.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .gitlab/{issue_templates => merge_request_templates}/merge_request_templates.md (100%) diff --git a/.gitlab/issue_templates/merge_request_templates.md b/.gitlab/merge_request_templates/merge_request_templates.md similarity index 100% rename from .gitlab/issue_templates/merge_request_templates.md rename to .gitlab/merge_request_templates/merge_request_templates.md -- GitLab From 3e7965be8806098281a30da8e4dbc1c1b7fd6b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9D=EC=B0=AC=20=EC=9C=A4?= <ysc0731@ajou.ac.kr> Date: Sun, 8 Dec 2024 02:52:54 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EA=B0=9C=EB=B3=84=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A5=B4=20=EC=A1=B0=ED=9A=8C=20api=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SchedulePage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SchedulePage.jsx b/src/pages/SchedulePage.jsx index 2f41569..6dcb5c7 100644 --- a/src/pages/SchedulePage.jsx +++ b/src/pages/SchedulePage.jsx @@ -3,7 +3,7 @@ import Label from "../components/Label"; import { createSchedule, deleteSchedule, - fetchScheduleByTimeIndex, + // fetchScheduleByTimeIndex, fetchAllSchedules, updateSchedule, } from "../api/schedule"; -- GitLab