From 001e7b73303d1fc990de2a1530430c27d0b4f50d 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 16:51:27 +0900
Subject: [PATCH 01/13] =?UTF-8?q?[#12]=20=EB=A9=94=EC=9D=B8=ED=8E=98?=
 =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package-lock.json                         |  10 +
 package.json                              |   1 +
 src/App.js                                |   5 +-
 src/components/DailyModal.jsx             |  64 +++++
 src/components/icons/CalendarIcon.jsx     |  22 ++
 src/components/icons/ChatIcon.jsx         |  22 ++
 src/components/icons/GoogleLogoIcon.jsx   |   2 +-
 src/components/icons/MiniScheduleIcon.jsx |  57 ++++
 src/components/layout/HeaderNav.jsx       |  34 ++-
 src/pages/HomePage.jsx                    | 302 +++++++++++++++++++++-
 src/pages/LoginPage.jsx                   |   4 +-
 11 files changed, 498 insertions(+), 25 deletions(-)
 create mode 100644 src/components/DailyModal.jsx
 create mode 100644 src/components/icons/CalendarIcon.jsx
 create mode 100644 src/components/icons/ChatIcon.jsx
 create mode 100644 src/components/icons/MiniScheduleIcon.jsx

diff --git a/package-lock.json b/package-lock.json
index 802c233..c147d1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
         "react": "^18.3.1",
         "react-dom": "^18.3.1",
         "react-icons": "^5.4.0",
+        "react-ios-pwa-prompt": "^2.0.6",
         "react-modal": "^3.16.1",
         "react-router-dom": "^6.28.0",
         "react-scripts": "5.0.1",
@@ -13991,6 +13992,15 @@
         "react": "*"
       }
     },
+    "node_modules/react-ios-pwa-prompt": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/react-ios-pwa-prompt/-/react-ios-pwa-prompt-2.0.6.tgz",
+      "integrity": "sha512-slRvFlcYsOL01D8gbJW4agOQ64pk7V+0EBk+d8z8wQ40vtcaoSw/JJkh+fJQrWSfWFVvZESu3FHP0+un4YUjJg==",
+      "peerDependencies": {
+        "react": ">=16.8.0",
+        "react-dom": ">=16.8.0"
+      }
+    },
     "node_modules/react-is": {
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
diff --git a/package.json b/package.json
index 9794b7a..a3bcaf5 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-icons": "^5.4.0",
+    "react-ios-pwa-prompt": "^2.0.6",
     "react-modal": "^3.16.1",
     "react-router-dom": "^6.28.0",
     "react-scripts": "5.0.1",
diff --git a/src/App.js b/src/App.js
index ef2b121..5cc4e04 100644
--- a/src/App.js
+++ b/src/App.js
@@ -23,7 +23,10 @@ const App = () => {
             <Route path="/" element={<HomePage />} />
             <Route path="/timetable" element={<SchedulePage />} />
             <Route path="/chattinglist" element={<ChattingListPage />} />
-            <Route path="/chat/chatRoom/:chatRoomId" element={<ChattingDetailPage />} />
+            <Route
+              path="/chat/chatRoom/:chatRoomId"
+              element={<ChattingDetailPage />}
+            />
             <Route path="/mypage" element={<MyPage />} />
             <Route path="/login" element={<LoginPage />} />
           </Routes>
diff --git a/src/components/DailyModal.jsx b/src/components/DailyModal.jsx
new file mode 100644
index 0000000..e8cc4df
--- /dev/null
+++ b/src/components/DailyModal.jsx
@@ -0,0 +1,64 @@
+import React, { useState } from "react";
+import { Link } from "react-router-dom";
+
+const DailyModal = ({ show, onClose }) => {
+  const [doNotShowToday, setDoNotShowToday] = useState(false);
+
+  const handleDoNotShowTodayChange = (e) => {
+    setDoNotShowToday(e.target.checked);
+  };
+
+  const handleClose = () => {
+    if (doNotShowToday) {
+      const tomorrow = new Date();
+      tomorrow.setDate(tomorrow.getDate() + 1);
+      localStorage.setItem("modalDismissedUntil", tomorrow.toISOString());
+    }
+    onClose();
+  };
+
+  if (!show) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
+      <div className="w-11/12 max-w-lg p-6 bg-white shadow-lg rounded-2xl">
+        <div className="flex flex-col items-center gap-4 text-center">
+          <img
+            src={`${process.env.PUBLIC_URL}/logo196.png`}
+            alt="Modal"
+            className="w-1/2"
+          />
+          <p className="text-gray-700">
+            홈화면에 앱 추가하고 <br />
+            공지사항, 이벤트 알림을 받아보세요.
+          </p>
+        </div>
+        <Link
+          to="/guide"
+          className="block px-6 py-2 mt-4 font-bold text-center text-white bg-blue-700 rounded-full hover:bg-blue-800"
+        >
+          설치없이 앱으로 열기
+        </Link>
+        <div className="flex items-center justify-between mt-6">
+          <label className="flex items-center text-sm text-gray-600">
+            <input
+              type="checkbox"
+              checked={doNotShowToday}
+              onChange={handleDoNotShowTodayChange}
+              className="mr-2"
+            />
+            오늘은 보지 않기
+          </label>
+          <button
+            onClick={handleClose}
+            className="px-4 py-2 text-sm border rounded-lg hover:bg-gray-100"
+          >
+            닫기
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default DailyModal;
diff --git a/src/components/icons/CalendarIcon.jsx b/src/components/icons/CalendarIcon.jsx
new file mode 100644
index 0000000..ed843ea
--- /dev/null
+++ b/src/components/icons/CalendarIcon.jsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+const CalendarIcon = () => {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      fill="none"
+      viewBox="0 0 24 24"
+      strokeWidth={2}
+      stroke="white"
+      className="w-8 h-8"
+    >
+      <path
+        strokeLinecap="round"
+        strokeLinejoin="round"
+        d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
+      />
+    </svg>
+  );
+};
+
+export default CalendarIcon;
diff --git a/src/components/icons/ChatIcon.jsx b/src/components/icons/ChatIcon.jsx
new file mode 100644
index 0000000..47fbdff
--- /dev/null
+++ b/src/components/icons/ChatIcon.jsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+const ChatIcon = () => {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      fill="none"
+      viewBox="0 0 24 24"
+      strokeWidth={2}
+      stroke="white"
+      className="w-8 h-8"
+    >
+      <path
+        strokeLinecap="round"
+        strokeLinejoin="round"
+        d="M8 10h8m-4 4h4m-9.293 5.707a1 1 0 01-1.414-1.414l1.293-1.293A1 1 0 005 16.586V8a3 3 0 013-3h8a3 3 0 013 3v8a3 3 0 01-3 3H9.414l-1.293 1.293z"
+      />
+    </svg>
+  );
+};
+
+export default ChatIcon;
diff --git a/src/components/icons/GoogleLogoIcon.jsx b/src/components/icons/GoogleLogoIcon.jsx
index a28ef03..b4a61be 100644
--- a/src/components/icons/GoogleLogoIcon.jsx
+++ b/src/components/icons/GoogleLogoIcon.jsx
@@ -1,4 +1,4 @@
-export default function GoogleLogo({ className, ...props }) {
+export default function GoogleLogoIcon({ className, ...props }) {
   return (
     <svg
       xmlns="http://www.w3.org/2000/svg"
diff --git a/src/components/icons/MiniScheduleIcon.jsx b/src/components/icons/MiniScheduleIcon.jsx
new file mode 100644
index 0000000..9d2662d
--- /dev/null
+++ b/src/components/icons/MiniScheduleIcon.jsx
@@ -0,0 +1,57 @@
+import React from "react";
+
+const days = ["월", "화", "수", "목", "금", "토", "일"];
+
+// 더미 데이터: 요일별 일정 상태 (고정 또는 유동)
+const dummyScheduleStatus = [
+  { day: "월", type: "fixed" },
+  { day: "화", type: "flexible" },
+  { day: "수", type: "empty" },
+  { day: "목", type: "fixed" },
+  { day: "금", type: "flexible" },
+  { day: "토", type: "empty" },
+  { day: "일", type: "flexible" },
+];
+
+const MiniScheduleIcon = ({ scheduleStatus = dummyScheduleStatus }) => {
+  return (
+    <div className="flex flex-col items-center justify-center h-40 p-4 bg-white rounded-lg shadow-lg w-60">
+      {/* 헤더 */}
+      <div className="mb-2 text-gray-300 label-1">
+        <span className="text-secondary-500">고정</span> /{" "}
+        <span className="text-primary-500">유동</span>
+      </div>
+
+      {/* 요일별 상태 */}
+      <div className="grid w-full grid-cols-7 gap-1">
+        {days.map((day, index) => {
+          const status =
+            scheduleStatus.find((item) => item.day === day)?.type || "empty";
+          return (
+            <div
+              key={index}
+              className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold ${
+                status === "fixed"
+                  ? "bg-primary-300 text-white"
+                  : status === "flexible"
+                  ? "bg-secondary-300 text-white"
+                  : "bg-gray-200 text-gray-500"
+              }`}
+              title={`${day} ${
+                status === "fixed"
+                  ? "고정 일정 있음"
+                  : status === "flexible"
+                  ? "유동 일정 있음"
+                  : "일정 없음"
+              }`}
+            >
+              {day}
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+};
+
+export default MiniScheduleIcon;
diff --git a/src/components/layout/HeaderNav.jsx b/src/components/layout/HeaderNav.jsx
index 2dba5ad..f274349 100644
--- a/src/components/layout/HeaderNav.jsx
+++ b/src/components/layout/HeaderNav.jsx
@@ -3,6 +3,9 @@ import { useNavigate } from "react-router-dom";
 import Button from "../Button";
 import LogoIcon from "../icons/LogoIcon";
 import useAuthStore from "../../store/authStore";
+import ChatIcon from "../icons/ChatIcon";
+import GoogleLogoIcon from "../icons/GoogleLogoIcon";
+import CalendarIcon from "../icons/CalendarIcon";
 
 export default function HeaderNav() {
   const navigate = useNavigate();
@@ -37,21 +40,30 @@ export default function HeaderNav() {
               <Button
                 size="icon"
                 theme="purple"
-                icon={<LogoIcon fillColor="#ffffff" />}
+                icon={<CalendarIcon />}
                 onClick={navigateToTimeTable}
               />
               <Button
                 size="icon"
                 theme="mix"
-                icon={<LogoIcon fillColor="#ffffff" />}
+                icon={<ChatIcon />}
                 onClick={navigateToChattingList}
               />
-              <Button
-                size="icon"
-                theme="black"
-                icon={<LogoIcon fillColor="#ffffff" />}
-                onClick={user ? navigateToMyPage : navigateToLogin} // 조건부 이동
-              />
+              {user ? (
+                <Button
+                  size="icon"
+                  theme="black"
+                  icon={<LogoIcon fillColor="#ffffff" />}
+                  onClick={navigateToMyPage}
+                />
+              ) : (
+                <Button
+                  size="icon"
+                  theme="black"
+                  icon={<GoogleLogoIcon />}
+                  onClick={navigateToLogin}
+                />
+              )}
             </>
           ) : (
             <>
@@ -66,7 +78,7 @@ export default function HeaderNav() {
               <Button
                 size="lg"
                 theme="purple"
-                icon={<LogoIcon fillColor="#ffffff" />}
+                icon={<CalendarIcon />}
                 onClick={navigateToTimeTable}
               >
                 캘린더
@@ -74,7 +86,7 @@ export default function HeaderNav() {
               <Button
                 size="lg"
                 theme="mix"
-                icon={<LogoIcon fillColor="#ffffff" />}
+                icon={<ChatIcon />}
                 onClick={navigateToChattingList}
               >
                 번개채팅방
@@ -92,7 +104,7 @@ export default function HeaderNav() {
                 <Button
                   size="lg"
                   theme="black"
-                  icon={<LogoIcon fillColor="#ffffff" />}
+                  icon={<GoogleLogoIcon />}
                   onClick={navigateToLogin}
                 >
                   로그인
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index 01e46fe..877f32a 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -1,8 +1,91 @@
-import React, { useEffect } from "react";
+import React, { useState, useEffect } from "react";
 import useAuthStore from "../store/authStore";
+import GetUserPermission from "../fcm/GetUserPermission";
+import DailyModal from "../components/DailyModal";
+import PWAPrompt from "react-ios-pwa-prompt";
+import { useNavigate } from "react-router-dom";
+import Button from "../components/Button";
+import GoogleLogoIcon from "../components/icons/GoogleLogoIcon";
+import MiniScheduleIcon from "../components/icons/MiniScheduleIcon";
+import LogoIcon from "../components/icons/LogoIcon";
+import ChatIcon from "../components/icons/ChatIcon";
 
 const HomePage = () => {
   const { user, fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기
+  const [isLoading, setIsLoading] = useState(false);
+  const [showModal, setShowModal] = useState(false);
+  const [isPWAInstalled, setIsPWAInstalled] = useState(false);
+  const [deferredPrompt, setDeferredPrompt] = useState(null);
+  const [showInstallPrompt, setShowInstallPrompt] = useState(false);
+  const [isIOS, setIsIOS] = useState(false);
+  const [shouldShowPWAPrompt, setShouldShowPWAPrompt] = useState(false);
+  const [showPushNotificationPrompt, setShowPushNotificationPrompt] =
+    useState(false);
+
+  const features = [
+    {
+      title: "Google OAuth 간편 로그인",
+      description: "Google 계정을 통해 빠르고 간편하게 로그인하세요.",
+      component: (
+        <Button size="md" theme="white" icon={<GoogleLogoIcon />}>
+          구글로 로그인
+        </Button>
+      ),
+    },
+    {
+      title: "고정 및 유동적인 시간표 관리",
+      description:
+        "고정 시간표와 유동 시간표를 쉽게 관리하고, 일정을 효율적으로 조율하세요.",
+      component: <MiniScheduleIcon />, // 예시 컴포넌트
+    },
+    {
+      title: "친구 초대 및 번개 모임 관리",
+      description:
+        "친구를 초대하고 번개 모임을 생성하여 쉽고 빠르게 약속을 잡으세요.",
+      component: (
+        <div className="flex gap-2">
+          <Button
+            size="icon"
+            theme="purple"
+            icon={<LogoIcon fillColor="#ffffff" />}
+          />
+          <Button
+            size="icon"
+            theme="indigo"
+            icon={<LogoIcon fillColor="#ffffff" />}
+          />
+          <Button
+            size="icon"
+            theme="mix"
+            icon={<LogoIcon fillColor="#ffffff" />}
+          />
+        </div>
+      ),
+    },
+    {
+      title: "실시간 채팅 및 푸시 알림",
+      description:
+        "모임 중 실시간으로 소통하고, 알림을 통해 중요한 정보를 놓치지 마세요.",
+      component: (
+        <div className="flex flex-col items-center justify-center w-40 h-40 p-4 rounded-lg shadow-lg bg-primary-100">
+          <div className="flex items-center justify-center w-16 h-16 rounded-full shadow-md bg-primary-500">
+            <ChatIcon />
+          </div>
+          <p className="mt-4 text-sm font-bold text-primary-500">채팅</p>
+        </div>
+      ),
+    },
+  ];
+
+  const navigate = useNavigate();
+
+  const handleStartNow = () => {
+    if (user) {
+      navigate("/chat-room"); // 번개 채팅방으로 리다이렉션
+    } else {
+      navigate("/login"); // 로그인 페이지로 리다이렉션
+    }
+  };
 
   useEffect(() => {
     const fetchUserSession = async () => {
@@ -16,16 +99,215 @@ const HomePage = () => {
     fetchUserSession();
   }, [fetchSession]); // 페이지 마운트 시 실행
 
+  //PWA 권한 허용 및 FCM 토큰 받아오기
+  useEffect(() => {
+    GetUserPermission(setIsLoading);
+  }, []);
+
+  useEffect(() => {
+    const isDeviceIOS =
+      /iPad|iPhone|iPod/.test(window.navigator.userAgent) && !window.MSStream;
+    setIsIOS(isDeviceIOS);
+
+    if (window.matchMedia("(display-mode: standalone)").matches) {
+      setIsPWAInstalled(true);
+      const isFirstTimeOpen = localStorage.getItem("isFirstTimeOpen");
+      if (!isFirstTimeOpen) {
+        setShowPushNotificationPrompt(true);
+        localStorage.setItem("isFirstTimeOpen", "false");
+      }
+      return;
+    }
+
+    const dismissedUntil = localStorage.getItem("modalDismissedUntil");
+    if (dismissedUntil) {
+      const now = new Date();
+      if (new Date(dismissedUntil) > now) {
+        return;
+      }
+    }
+
+    const handleBeforeInstallPrompt = (e) => {
+      e.preventDefault();
+      console.log("PWA 설치여부", isPWAInstalled);
+      if (!isPWAInstalled) {
+        setDeferredPrompt(e);
+        setShowInstallPrompt(true);
+      }
+    };
+
+    window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
+
+    return () => {
+      window.removeEventListener(
+        "beforeinstallprompt",
+        handleBeforeInstallPrompt
+      );
+    };
+  }, [isPWAInstalled]);
+
+  useEffect(() => {
+    if (isIOS) {
+      setShouldShowPWAPrompt(true);
+    }
+  }, [isIOS]);
+
+  const handleInstallClick = () => {
+    if (deferredPrompt) {
+      deferredPrompt.prompt();
+      deferredPrompt.userChoice.then((choiceResult) => {
+        if (choiceResult.outcome === "accepted") {
+          console.log("User accepted the install prompt");
+        } else {
+          console.log("User dismissed the install prompt");
+        }
+        setDeferredPrompt(null);
+        setShowInstallPrompt(false);
+      });
+    }
+  };
+
+  const handleCloseModal = () => {
+    setShowModal(false);
+    setShowInstallPrompt(false);
+  };
+
+  const handleAllowNotifications = () => {
+    GetUserPermission(setIsLoading);
+    setShowPushNotificationPrompt(false);
+  };
+
   return (
-    <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 className="flex flex-col items-center justify-center w-full min-h-screen space-y-4 bg-grayscale-50">
+      <div className="min-h-screen bg-grayscale-50">
+        {/* 헤더 */}
+        <header className="px-4 py-6 text-center text-white rounded-t-xl bg-gradient-pink">
+          <h1 className="font-bold heading-1">번개 모임 관리 플랫폼</h1>
+          <p className="mt-2 body-1">
+            친구들과의 약속을 쉽고 빠르게 관리하세요!
+          </p>
+        </header>
+
+        <section className="grid max-w-5xl grid-cols-1 gap-8 px-6 py-12 mx-auto tablet:grid-cols-2">
+          {features.map((feature, index) => (
+            <div
+              key={index}
+              className="flex flex-col items-center p-6 bg-white rounded-lg shadow-lg"
+            >
+              {/* 컴포넌트 렌더링 */}
+              <div className="flex items-center justify-center w-full h-48 mb-4">
+                {feature.component}
+              </div>
+              <h2 className="mb-2 heading-2 text-primary-500">
+                {feature.title}
+              </h2>
+              <p className="text-center text-gray-600 body-1">
+                {feature.description}
+              </p>
+            </div>
+          ))}
+        </section>
+
+        {/* 번개 모임 예시 섹션 */}
+        <section className="py-12 bg-primary-50">
+          <div className="max-w-5xl mx-auto text-center">
+            <h2 className="mb-4 heading-2 text-primary-500">번개 모임 예시</h2>
+            <p className="px-4 mb-8 text-gray-600 body-1">
+              실시간 번개 모임으로 친구들과 약속을 쉽게 잡아보세요!
+            </p>
+            {/* 기존 Card 컴포넌트 활용 */}
+            <div className="grid grid-cols-1 gap-6 px-6 tablet:grid-cols-2">
+              <div className="p-4 bg-white rounded shadow">
+                <h3 className="mb-2 heading-3">🎉 번개 회식</h3>
+                <p className="text-gray-600 body-2">
+                  오늘 저녁 7시, 강남역 모임
+                </p>
+              </div>
+              <div className="p-4 bg-white rounded shadow">
+                <h3 className="mb-2 heading-3">🍻 번개 술 약속</h3>
+                <p className="text-gray-600 body-2">내일 오후 9시, 홍대입구</p>
+              </div>
+            </div>
+          </div>
+        </section>
+
+        {/* 시작하기 버튼 섹션 */}
+        <section className="flex flex-col items-center justify-center py-12 text-center bg-white">
+          <h2 className="mb-4 heading-2">지금 바로 시작해보세요!</h2>
+          <Button onClick={handleStartNow} size="xl" theme="mix">
+            YANAWA 시작하기
+          </Button>
+        </section>
+      </div>
+
+      {showInstallPrompt && (
+        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
+          <div className="w-11/12 max-w-lg p-6 text-center bg-white rounded-xl">
+            <img
+              className="w-1/2 mx-auto"
+              src={`${process.env.PUBLIC_URL}/logo196.png`}
+              alt="Modal"
+            />
+            <h2 className="mt-4 text-lg font-bold">
+              YHANAWA를 설치하고 <br /> 번개모임을 만들어보세요!
+            </h2>
+            <p className="my-4 text-sm text-gray-600">
+              앱에서 푸시 알림을 받을 수 있어요.
+            </p>
+            <button
+              className="px-6 py-2 font-bold text-white bg-blue-700 rounded-full"
+              onClick={handleInstallClick}
+            >
+              설치
+            </button>
+            <button
+              className="mt-4 text-sm text-gray-500"
+              onClick={handleCloseModal}
+            >
+              나중에 설치
+            </button>
+          </div>
+        </div>
+      )}
+
+      {showPushNotificationPrompt && (
+        <div className="fixed inset-0 flex flex-col items-center justify-center bg-white">
+          <img
+            className="w-24 h-24 mb-4"
+            alt="알람"
+            src={`${process.env.PUBLIC_URL}/icons/notiOn.svg`}
+          />
+          <h1 className="text-xl font-bold">푸시 알림 받기</h1>
+          <p className="my-2 text-center text-gray-600">
+            푸시 알림을 설정하고 각종 공지사항, 키워드 알림을 받아보세요!
+          </p>
+          <button
+            className="px-6 py-2 font-bold text-white bg-blue-700 rounded-full"
+            onClick={handleAllowNotifications}
+          >
+            알림 받기
+          </button>
+          <button
+            className="mt-4 text-sm text-gray-500"
+            onClick={() => setShowPushNotificationPrompt(false)}
+          >
+            나중에 받을게요
+          </button>
+        </div>
+      )}
+      {showModal && <DailyModal onClose={handleCloseModal} />}
+      {isIOS && (
+        <PWAPrompt
+          promptOnVisit={1}
+          timesToShow={1}
+          copyTitle="야나와 앱 설치하기 - 아이폰"
+          copySubtitle="홈 화면에 앱을 추가하고 번개모임 생성 알림을 받아보세요."
+          copyDescription="야나와는 앱설치 없이 홈화면에 추가를 통해 사용할 수 있습니다."
+          copyShareStep="하단 메뉴에서 '공유' 아이콘을 눌러주세요."
+          copyAddToHomeScreenStep="아래의 '홈 화면에 추가' 버튼을 눌러주세요."
+          appIconPath="%PUBLIC_URL%/ios/192.png"
+          isShown={shouldShowPWAPrompt}
+        />
       )}
     </div>
   );
diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx
index f2ad8ff..c5061e4 100644
--- a/src/pages/LoginPage.jsx
+++ b/src/pages/LoginPage.jsx
@@ -1,5 +1,5 @@
 import React, { useEffect } from "react";
-import GoogleLogo from "../components/icons/GoogleLogoIcon";
+import GoogleLogoIcon from "../components/icons/GoogleLogoIcon";
 import Button from "../components/Button";
 import { getLoginUrl } from "../api/auth";
 import useAuthStore from "../store/authStore";
@@ -65,7 +65,7 @@ const LoginPage = () => {
         <Button
           size="md"
           theme="white"
-          icon={<GoogleLogo />}
+          icon={<GoogleLogoIcon />}
           onClick={handleGoogleLogin}
         >
           구글로 로그인
-- 
GitLab


From dafb4e64f0afb36eb6380893b2e7419410252c7b 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 16:58:16 +0900
Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?=
 =?UTF-8?q?=ED=95=9C=20useState=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/fcm/GetUserPermission.jsx | 24 +-----------------------
 src/pages/HomePage.jsx        |  5 ++---
 2 files changed, 3 insertions(+), 26 deletions(-)

diff --git a/src/fcm/GetUserPermission.jsx b/src/fcm/GetUserPermission.jsx
index 621f1a7..c00186c 100644
--- a/src/fcm/GetUserPermission.jsx
+++ b/src/fcm/GetUserPermission.jsx
@@ -1,5 +1,4 @@
 import GetFCMToken from "./GetFCMToken";
-import { registerServiceWorker } from "../serviceWorkerRegistration";
 import Swal from "sweetalert2";
 
 const Toast = Swal.mixin({
@@ -14,18 +13,8 @@ const Toast = Swal.mixin({
   },
 });
 
-const GetUserPermission = async (setIsLoading) => {
+const GetUserPermission = async () => {
   try {
-    //서비스워커 추가
-    // await navigator.serviceWorker.register("firebase-messaging-sw.js");
-
-    // const registrations = await navigator.serviceWorker.getRegistrations();
-    // if (registrations.length === 0) {
-    //   setIsLoading(true);
-    //   await registerServiceWorker();
-    //   setIsLoading(false);
-    // }
-
     if (!("Notification" in window)) {
       alert(
         "알림 서비스를 원활하게 사용하시려면 바탕화면에 바로가기 추가 후, 홈페이지에 종모양아이콘 클릭하여 알림 허용을 해주세요."
@@ -39,14 +28,10 @@ const GetUserPermission = async (setIsLoading) => {
     if (permission === "granted") {
       try {
         console.log("Notification permission granted. Ready to send token...");
-        setIsLoading(true);
         await GetFCMToken();
-        setIsLoading(false);
         let isFCMToken = localStorage.getItem("fcmToken");
         if (!isFCMToken) {
-          setIsLoading(true);
           await GetFCMToken();
-          setIsLoading(false);
           Toast.fire({
             icon: "error",
             title: `알림 토큰 저장 실패`,
@@ -61,16 +46,10 @@ const GetUserPermission = async (setIsLoading) => {
         });
       }
     } else if (permission === "denied") {
-      // alert("알림권한이 허용되어 있지않습니다. 권한을 허용해 주십시오.");
       console.log(
         "Notification permission not granted. Requesting permission..."
       );
     } else {
-      // Toast.fire({
-      //   icon: "warning",
-      //   title: `알림 설정 안함`,
-      //   text: "알림 설정 요청을 원하시면 종아이콘을 클릭해주세요.",
-      // });
     }
   } catch (error) {
     Toast.fire({
@@ -78,7 +57,6 @@ const GetUserPermission = async (setIsLoading) => {
       title: `알림 설정 요청 실패`,
     });
     console.error("Failed to check or request notification permission:", error);
-    setIsLoading(false);
   }
 };
 
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index 877f32a..574a157 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -12,7 +12,6 @@ import ChatIcon from "../components/icons/ChatIcon";
 
 const HomePage = () => {
   const { user, fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기
-  const [isLoading, setIsLoading] = useState(false);
   const [showModal, setShowModal] = useState(false);
   const [isPWAInstalled, setIsPWAInstalled] = useState(false);
   const [deferredPrompt, setDeferredPrompt] = useState(null);
@@ -101,7 +100,7 @@ const HomePage = () => {
 
   //PWA 권한 허용 및 FCM 토큰 받아오기
   useEffect(() => {
-    GetUserPermission(setIsLoading);
+    GetUserPermission();
   }, []);
 
   useEffect(() => {
@@ -173,7 +172,7 @@ const HomePage = () => {
   };
 
   const handleAllowNotifications = () => {
-    GetUserPermission(setIsLoading);
+    GetUserPermission();
     setShowPushNotificationPrompt(false);
   };
 
-- 
GitLab


From 04a477c3f56cbb562f91b5d23623f96e903e68d8 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 17:13:28 +0900
Subject: [PATCH 03/13] =?UTF-8?q?hotfix:=20PWA=20=EB=AA=A8=EB=8B=AC=20?=
 =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EA=B2=BD=EB=A1=9C=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/pages/HomePage.jsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index 574a157..50afbd9 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -244,7 +244,7 @@ const HomePage = () => {
           <div className="w-11/12 max-w-lg p-6 text-center bg-white rounded-xl">
             <img
               className="w-1/2 mx-auto"
-              src={`${process.env.PUBLIC_URL}/logo196.png`}
+              src={`${process.env.PUBLIC_URL}/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png`}
               alt="Modal"
             />
             <h2 className="mt-4 text-lg font-bold">
@@ -274,7 +274,7 @@ const HomePage = () => {
           <img
             className="w-24 h-24 mb-4"
             alt="알람"
-            src={`${process.env.PUBLIC_URL}/icons/notiOn.svg`}
+            src={`${process.env.PUBLIC_URL}/android/android-launchericon-96-96.png`}
           />
           <h1 className="text-xl font-bold">푸시 알림 받기</h1>
           <p className="my-2 text-center text-gray-600">
@@ -304,7 +304,7 @@ const HomePage = () => {
           copyDescription="야나와는 앱설치 없이 홈화면에 추가를 통해 사용할 수 있습니다."
           copyShareStep="하단 메뉴에서 '공유' 아이콘을 눌러주세요."
           copyAddToHomeScreenStep="아래의 '홈 화면에 추가' 버튼을 눌러주세요."
-          appIconPath="%PUBLIC_URL%/ios/192.png"
+          appIconPath={`${process.env.PUBLIC_URL}/ios/192.png`}
           isShown={shouldShowPWAPrompt}
         />
       )}
-- 
GitLab


From 50325995e98eb12a3aef32243cb3a89b4edbaa48 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 17:25:11 +0900
Subject: [PATCH 04/13] =?UTF-8?q?design:=20PWA=20modal=20=EB=94=94?=
 =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/pages/HomePage.jsx | 52 +++++++++++++++++++-----------------------
 1 file changed, 23 insertions(+), 29 deletions(-)

diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index 50afbd9..b61cc10 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -238,14 +238,13 @@ const HomePage = () => {
           </Button>
         </section>
       </div>
-
       {showInstallPrompt && (
         <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
-          <div className="w-11/12 max-w-lg p-6 text-center bg-white rounded-xl">
+          <div className="flex flex-col items-center justify-center w-11/12 max-w-lg p-6 text-center bg-white rounded-xl">
             <img
-              className="w-1/2 mx-auto"
+              className="w-1/4 mx-auto"
               src={`${process.env.PUBLIC_URL}/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png`}
-              alt="Modal"
+              alt="YANAWA"
             />
             <h2 className="mt-4 text-lg font-bold">
               YHANAWA를 설치하고 <br /> 번개모임을 만들어보세요!
@@ -253,22 +252,17 @@ const HomePage = () => {
             <p className="my-4 text-sm text-gray-600">
               앱에서 푸시 알림을 받을 수 있어요.
             </p>
-            <button
-              className="px-6 py-2 font-bold text-white bg-blue-700 rounded-full"
-              onClick={handleInstallClick}
-            >
-              설치
-            </button>
-            <button
-              className="mt-4 text-sm text-gray-500"
-              onClick={handleCloseModal}
-            >
-              나중에 설치
-            </button>
+            <div className="flex gap-2">
+              <Button size="md" onClick={handleInstallClick}>
+                설치
+              </Button>
+              <Button size="md" theme="white" onClick={handleCloseModal}>
+                나중에 설치
+              </Button>
+            </div>
           </div>
         </div>
       )}
-
       {showPushNotificationPrompt && (
         <div className="fixed inset-0 flex flex-col items-center justify-center bg-white">
           <img
@@ -280,18 +274,18 @@ const HomePage = () => {
           <p className="my-2 text-center text-gray-600">
             푸시 알림을 설정하고 각종 공지사항, 키워드 알림을 받아보세요!
           </p>
-          <button
-            className="px-6 py-2 font-bold text-white bg-blue-700 rounded-full"
-            onClick={handleAllowNotifications}
-          >
-            알림 받기
-          </button>
-          <button
-            className="mt-4 text-sm text-gray-500"
-            onClick={() => setShowPushNotificationPrompt(false)}
-          >
-            나중에 받을게요
-          </button>
+          <div className="flex gap-2 mt-4">
+            <Button size="md" onClick={handleAllowNotifications}>
+              알림 받기
+            </Button>
+            <Button
+              size="md"
+              theme="white"
+              onClick={() => setShowPushNotificationPrompt(false)}
+            >
+              나중에 받을게요
+            </Button>
+          </div>
         </div>
       )}
       {showModal && <DailyModal onClose={handleCloseModal} />}
-- 
GitLab


From 08650b167041816b8fb72822e15bc00f8fd06e7b 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 19:17:35 +0900
Subject: [PATCH 05/13] =?UTF-8?q?[#11]=20=EB=A7=88=EC=9D=B4=ED=8E=98?=
 =?UTF-8?q?=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

---
 .../merge_request_templates.md                |   0
 src/api/friend.js                             | 126 +++++++
 src/api/meeting.js                            |  35 ++
 src/components/Card.jsx                       |  59 ++--
 src/components/Label.jsx                      |   2 +-
 src/components/layout/HeaderNav.jsx           |   2 +-
 src/pages/Mypage.jsx                          | 325 +++++++++++++++++-
 src/store/authStore.js                        |   2 +
 8 files changed, 524 insertions(+), 27 deletions(-)
 create mode 100644 .gitlab/issue_templates/merge_request_templates.md
 create mode 100644 src/api/friend.js
 create mode 100644 src/api/meeting.js

diff --git a/.gitlab/issue_templates/merge_request_templates.md b/.gitlab/issue_templates/merge_request_templates.md
new file mode 100644
index 0000000..e69de29
diff --git a/src/api/friend.js b/src/api/friend.js
new file mode 100644
index 0000000..7a5a7c7
--- /dev/null
+++ b/src/api/friend.js
@@ -0,0 +1,126 @@
+// 기본 API URL
+const BASE_URL = process.env.REACT_APP_BASE_URL;
+
+/**
+ * 친구 요청 보내기
+ * @param {Object} requestData - 요청 데이터 (userId, email)
+ * @returns {Promise<Object>} - 생성된 친구 요청 데이터
+ */
+export const sendFriendRequest = async (requestData) => {
+  const response = await fetch(`${BASE_URL}/api/friend/request`, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify(requestData),
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to send friend request");
+  }
+
+  return await response.json();
+};
+
+/**
+ * 받은 친구 요청 조회
+ * @returns {Promise<Object[]>} - 받은 친구 요청 리스트
+ */
+export const getReceivedFriendRequests = async () => {
+  const response = await fetch(`${BASE_URL}/api/friend/requests/received`);
+
+  if (!response.ok) {
+    throw new Error("Failed to fetch received friend requests");
+  }
+
+  return (await response.json()).data;
+};
+
+/**
+ * 보낸 친구 요청 조회
+ * @returns {Promise<Object[]>} - 보낸 친구 요청 리스트
+ */
+export const getSentFriendRequests = async () => {
+  const response = await fetch(`${BASE_URL}/api/friend/requests/sent`);
+
+  if (!response.ok) {
+    throw new Error("Failed to fetch sent friend requests");
+  }
+
+  return (await response.json()).data;
+};
+
+/**
+ * 친구 요청 수락
+ * @param {number} requestId - 친구 요청 ID
+ * @returns {Promise<Object>} - 수락된 친구 요청 데이터
+ */
+export const acceptFriendRequest = async (requestId) => {
+  const response = await fetch(
+    `${BASE_URL}/api/friend/request/${requestId}/accept`,
+    {
+      method: "POST",
+    }
+  );
+
+  if (!response.ok) {
+    throw new Error("Failed to accept friend request");
+  }
+
+  return (await response.json()).data;
+};
+
+/**
+ * 친구 요청 거절
+ * @param {number} requestId - 친구 요청 ID
+ * @returns {Promise<Object>} - 거절된 친구 요청 데이터
+ */
+export const rejectFriendRequest = async (requestId) => {
+  const response = await fetch(
+    `${BASE_URL}/api/friend/request/${requestId}/reject`,
+    {
+      method: "POST",
+    }
+  );
+
+  if (!response.ok) {
+    throw new Error("Failed to reject friend request");
+  }
+
+  return (await response.json()).data;
+};
+
+/**
+ * 친구 목록 조회
+ * @param {number} page - 페이지 번호
+ * @param {number} size - 페이지 크기
+ * @returns {Promise<Object>} - 친구 목록 데이터
+ */
+export const getAllFriends = async (page = 0, size = 10) => {
+  const response = await fetch(
+    `${BASE_URL}/api/friend/all?page=${page}&size=${size}`
+  );
+
+  if (!response.ok) {
+    throw new Error("Failed to fetch friends list");
+  }
+
+  return (await response.json()).data.content;
+};
+
+/**
+ * 친구 삭제
+ * @param {number} friendId - 삭제할 친구 ID
+ * @returns {Promise<Object>} - 삭제 결과 데이터
+ */
+export const deleteFriend = async (friendId) => {
+  const response = await fetch(`${BASE_URL}/api/friends/${friendId}`, {
+    method: "DELETE",
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to delete friend");
+  }
+
+  return await response.json();
+};
diff --git a/src/api/meeting.js b/src/api/meeting.js
new file mode 100644
index 0000000..e4f7611
--- /dev/null
+++ b/src/api/meeting.js
@@ -0,0 +1,35 @@
+// src/api/meeting.js
+
+// 기본 API URL
+const BASE_URL = process.env.REACT_APP_BASE_URL;
+
+// 내가 참여하고 있는 채팅방 가져오기
+export const fetchMyMeetings = async (page = 0, size = 20) => {
+  try {
+    const response = await fetch(
+      `${BASE_URL}/api/meeting/my?page=${page}&size=${size}`,
+      {
+        method: "GET",
+        credentials: "include", // 세션 기반 인증을 위해 필요
+        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 meetings.");
+    }
+
+    return result.data; // 서버에서 제공된 데이터 반환
+  } catch (error) {
+    console.error("Error fetching my meetings:", error);
+    throw error;
+  }
+};
diff --git a/src/components/Card.jsx b/src/components/Card.jsx
index 5dfcc0e..b721751 100644
--- a/src/components/Card.jsx
+++ b/src/components/Card.jsx
@@ -1,8 +1,10 @@
 import { cn } from "../libs/index";
 import { cva } from "class-variance-authority";
 import React from "react";
+import Label from "./Label";
 
-const cardVariants = cva("w-[20rem] rounded-xl shadow-lg p-4 overflow-hidden", {
+// Card 스타일 변수를 정의합니다.
+const cardVariants = cva("w-full rounded-xl shadow-lg p-4 overflow-hidden", {
   variants: {
     theme: {
       black: "bg-black text-white",
@@ -11,35 +13,46 @@ const cardVariants = cva("w-[20rem] rounded-xl shadow-lg p-4 overflow-hidden", {
       purple: "bg-gradient-purple text-white",
       indigo: "bg-gradient-indigo text-white",
       mix: "bg-gradient-mix text-white",
+      gray: "bg-gray-300 text-gray-500",
     },
   },
 });
 
-export default function Card({
-  image,
-  title,
-  content,
-  time,
-  headCount,
-  theme = "black",
-  onClick,
-}) {
-  const variantClass = cardVariants({ theme });
+export default function Card({ meeting, theme = "black", onClick }) {
+  const {
+    title,
+    timeIdxStart,
+    timeIdxEnd,
+    location,
+    creatorName,
+    time_idx_deadline,
+    type,
+  } = meeting;
+
+  const variantClass = cardVariants({
+    theme: type === "CLOSE" ? "gray" : theme,
+  });
 
   return (
     <div className={cn(variantClass)} onClick={onClick}>
-      {image && (
-        <img
-          src={image}
-          alt={title}
-          className="object-contain w-full h-48 rounded-xl"
-        />
-      )}
-      <div className="p-3">
-        <h3 className="mb-2 text-xl font-bold">{title}</h3>
-        <p className="text-base">{content}</p>
-        <p className="text-base">{time}</p>
-        <p className="mt-5 text-right">{headCount}</p>
+      <h3 className="mb-2 text-xl font-bold">{title}</h3>
+      <div className="flex gap-2 mb-2">
+        <Label size="sm" theme="indigo">
+          {location}
+        </Label>
+        <Label size="sm" theme="indigo">
+          시간: {timeIdxStart} ~ {timeIdxEnd}
+        </Label>
+      </div>
+
+      <Label size="sm" theme="indigo">
+        마감 시간: {time_idx_deadline}
+      </Label>
+      <div className="flex justify-between mt-2">
+        <span className="text-sm text-white">작성자: {creatorName}</span>
+        <span className="text-sm text-right">
+          {type === "OPEN" ? "참여 가능" : "참여 마감"}
+        </span>
       </div>
     </div>
   );
diff --git a/src/components/Label.jsx b/src/components/Label.jsx
index 6fae129..8a2edf8 100644
--- a/src/components/Label.jsx
+++ b/src/components/Label.jsx
@@ -9,7 +9,7 @@ const labelVariants = cva(
       theme: {
         indigo: "bg-secondary-900 text-white",
         solid: "bg-primary-600 text-white",
-        lightsolid: "bg-primary-100 text-primary-600",
+        lightsolid: "bg-primary-600 text-primary-100 border border-primary-100",
         graysolid: "bg-grayscale-100 text-grayscale-900",
         ghost: "border border-grayscale-500 text-grayscale-900",
       },
diff --git a/src/components/layout/HeaderNav.jsx b/src/components/layout/HeaderNav.jsx
index f274349..426f712 100644
--- a/src/components/layout/HeaderNav.jsx
+++ b/src/components/layout/HeaderNav.jsx
@@ -89,7 +89,7 @@ export default function HeaderNav() {
                 icon={<ChatIcon />}
                 onClick={navigateToChattingList}
               >
-                번개채팅방
+                번개모임
               </Button>
               {user ? (
                 <Button
diff --git a/src/pages/Mypage.jsx b/src/pages/Mypage.jsx
index 7ec5065..2c8c013 100644
--- a/src/pages/Mypage.jsx
+++ b/src/pages/Mypage.jsx
@@ -1,7 +1,328 @@
-import React from "react";
+import React, { useState, useEffect } from "react";
+import useAuthStore from "../store/authStore";
+import {
+  getReceivedFriendRequests,
+  sendFriendRequest,
+  getAllFriends,
+  acceptFriendRequest,
+  rejectFriendRequest,
+  deleteFriend,
+} from "../api/friend";
+import Button from "../components/Button";
+import LogoIcon from "../components/icons/LogoIcon";
+import { fetchMyMeetings } from "../api/meeting";
+import Card from "../components/Card";
+import { useNavigate } from "react-router-dom";
 
 const MyPage = () => {
-  return <></>;
+  const { user, fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기
+  const [activeTab, setActiveTab] = useState("lightning"); // 현재 활성화된 탭
+  const [receivedRequests, setReceivedRequests] = useState([]);
+  const [sentRequests, setSentRequests] = useState([]);
+  const [friends, setFriends] = useState([]);
+  const [email, setEmail] = useState(""); // 친구 요청할 이메일
+  const [page, setPage] = useState(0); // 친구 목록 페이지
+  const [hasNext, setHasNext] = useState(true); // 페이지네이션 상태
+  const [isLoading, setIsLoading] = useState(false);
+
+  const [meetings, setMeetings] = useState([]);
+  const [meetingPage, setMeetingPage] = useState(0);
+  const [meetingHasNext, setMeetingHasNext] = useState(true);
+  const [meetingIsLoading, setMeetingIsLoading] = useState(false);
+
+  const navigate = useNavigate();
+
+  // 탭 변경 함수
+  const switchTab = (tab) => setActiveTab(tab);
+
+  useEffect(() => {
+    const fetchUserSession = async () => {
+      try {
+        const userInfo = localStorage.getItem("user");
+        if (!userInfo) {
+          alert("로그인이 필요한 페이지입니다.");
+          navigate("/login");
+        }
+        await fetchSession(); // 세션 정보 가져오기
+      } catch (error) {
+        console.error("Failed to fetch session:", error);
+      }
+    };
+
+    fetchUserSession();
+  }, [fetchSession, navigate]); // 페이지 마운트 시 실행
+
+  // 번개 모임 가져오기
+  useEffect(() => {
+    const fetchMeetings = async () => {
+      if (!meetingHasNext || meetingIsLoading) return;
+
+      try {
+        setMeetingIsLoading(true);
+        const data = await fetchMyMeetings(meetingPage, 20);
+        setMeetings((prev) => [...prev, ...data.content]);
+        setMeetingHasNext(data.meetingHasNext);
+        setMeetingPage((prev) => prev + 1);
+      } catch (error) {
+        console.error("Failed to fetch meetings:", error);
+      } finally {
+        setIsLoading(false);
+      }
+    };
+
+    if (activeTab === "lightning") fetchMeetings();
+  }, [activeTab, meetingPage, meetingHasNext, meetingIsLoading]);
+
+  // 받은 친구 요청 조회
+  useEffect(() => {
+    const fetchReceivedRequests = async () => {
+      try {
+        const data = await getReceivedFriendRequests();
+        setReceivedRequests(data);
+      } catch (error) {
+        console.error("Failed to fetch received requests:", error);
+      }
+    };
+    if (activeTab === "friends") fetchReceivedRequests();
+  }, [activeTab]);
+
+  // 친구 목록 무한스크롤 처리
+  useEffect(() => {
+    const fetchFriends = async () => {
+      if (!hasNext || isLoading) return;
+      try {
+        setIsLoading(true);
+        const data = await getAllFriends(page, 10);
+        setFriends((prev) => [...prev, ...data.content]);
+        setHasNext(data.hasNext);
+        setPage((prev) => prev + 1);
+      } catch (error) {
+        console.error("Failed to fetch friends:", error);
+      } finally {
+        setIsLoading(false);
+      }
+    };
+    if (activeTab === "friends") fetchFriends();
+  }, [page, hasNext, activeTab, isLoading]);
+
+  // 친구 요청 보내기
+  const handleSendRequest = async () => {
+    try {
+      const requestData = { email };
+      const response = await sendFriendRequest(requestData);
+      setSentRequests((prev) => [...prev, response.data]);
+      setEmail(""); // 입력 필드 초기화
+    } catch (error) {
+      console.error("Failed to send friend request:", error);
+    }
+  };
+
+  // 친구 요청 수락
+  const handleAcceptRequest = async (requestId) => {
+    try {
+      const response = await acceptFriendRequest(requestId);
+      setReceivedRequests((prev) =>
+        prev.filter((request) => request.id !== requestId)
+      );
+      setFriends((prev) => [response, ...prev]); // 친구 목록에 추가
+    } catch (error) {
+      console.error("Failed to accept request:", error);
+    }
+  };
+
+  // 친구 요청 거절
+  const handleRejectRequest = async (requestId) => {
+    try {
+      await rejectFriendRequest(requestId);
+      setReceivedRequests((prev) =>
+        prev.filter((request) => request.id !== requestId)
+      );
+    } catch (error) {
+      console.error("Failed to reject request:", error);
+    }
+  };
+
+  // 친구 삭제
+  const handleDeleteFriend = async (friendId) => {
+    try {
+      await deleteFriend(friendId);
+      setFriends((prev) => prev.filter((friend) => friend.id !== friendId));
+    } catch (error) {
+      console.error("Failed to delete friend:", error);
+    }
+  };
+
+  return (
+    <div className="w-full max-w-screen-lg min-h-screen mx-auto bg-white">
+      {/* 프로필 영역 */}
+      <div className="flex items-center px-6 py-4 border-b">
+        <div className="flex-shrink-0">
+          <LogoIcon className="w-20 h-20 rounded-full" />
+        </div>
+        <div className="flex-grow ml-6">
+          <h1 className="text-xl font-bold">{user?.name || "Guest"}</h1>
+          <p className="text-gray-600">{user?.email || "guest@example.com"}</p>
+        </div>
+      </div>
+
+      {/* 탭 네비게이션 */}
+      <div className="flex justify-center py-2 space-x-6 border-b">
+        <button
+          className={`px-4 py-2 text-sm font-medium ${
+            activeTab === "lightning"
+              ? "text-secondary-500 border-b-2 border-secondary-500"
+              : "text-gray-600"
+          }`}
+          onClick={() => switchTab("lightning")}
+        >
+          번개 모임
+        </button>
+        <button
+          className={`px-4 py-2 text-sm font-medium ${
+            activeTab === "friends"
+              ? "text-tertiary-500 border-b-2 border-tertiary-500"
+              : "text-gray-600"
+          }`}
+          onClick={() => switchTab("friends")}
+        >
+          친구
+        </button>
+      </div>
+      {/* 번개 모임 탭 */}
+      {activeTab === "lightning" && (
+        <div className="p-4">
+          {meetings.length === 0 && !isLoading && (
+            <p className="text-center">참여 중인 번개 모임이 없습니다.</p>
+          )}
+          <div className="grid grid-cols-1 gap-4 tablet:grid-cols-2 desktop:grid-cols-3">
+            {meetings.map((meeting) => (
+              <Card
+                key={meeting.id}
+                meeting={meeting}
+                theme="purple"
+                onClick={() => console.log("Clicked meeting:", meeting.id)}
+              />
+            ))}
+          </div>
+          {isLoading && <p className="text-center">로딩 중...</p>}
+          {!hasNext && meetings.length > 0 && (
+            <p className="text-sm text-center text-gray-500">
+              더 이상 불러올 번개 모임이 없습니다.
+            </p>
+          )}
+        </div>
+      )}
+
+      {/* 친구 탭 */}
+      {activeTab === "friends" && (
+        <div className="p-4 space-y-8">
+          {/* 친구 요청하기 */}
+          <div>
+            <h2 className="text-lg font-bold">친구 요청하기</h2>
+            <div className="flex items-center my-4 space-x-4">
+              <input
+                type="email"
+                className="flex-1 min-w-0 p-3 border rounded-full"
+                placeholder="친구 이메일 입력"
+                value={email}
+                onChange={(e) => setEmail(e.target.value)}
+              />
+              <Button
+                size="md"
+                theme="indigo"
+                className="min-w-[50px]"
+                onClick={handleSendRequest}
+              >
+                요청
+              </Button>
+            </div>
+            <div className="mt-4 space-y-4">
+              {sentRequests.map((request) => (
+                <div key={request.id} className="p-2 border rounded-lg">
+                  <p className="text-sm">
+                    {request.receiver.name} ({request.receiver.email})
+                  </p>
+                  <p className="text-xs text-gray-500">{request.status}</p>
+                </div>
+              ))}
+            </div>
+          </div>
+          <hr />
+          {/* 받은 친구 요청 */}
+          <div>
+            <h2 className="mb-2 text-lg font-bold">받은 친구 요청</h2>
+            <div className="space-y-4">
+              {receivedRequests.map((request) => (
+                <div
+                  key={request.id}
+                  className="flex items-center justify-between p-4 border rounded-lg"
+                >
+                  <div>
+                    <h3 className="font-semibold text-md">
+                      {request.requester.name}
+                    </h3>
+                    <p className="text-[10px] tablet:text-sm text-gray-600">
+                      {request.requester.email}
+                    </p>
+                  </div>
+                  <div className="flex gap-2">
+                    <Button
+                      size="sm"
+                      theme="indigo"
+                      onClick={() => handleAcceptRequest(request.id)}
+                    >
+                      수락
+                    </Button>
+                    <Button
+                      size="sm"
+                      theme="pink"
+                      onClick={() => handleRejectRequest(request.id)}
+                    >
+                      거절
+                    </Button>
+                  </div>
+                </div>
+              ))}
+            </div>
+          </div>
+          <hr />
+
+          {/* 친구 목록 */}
+          <div>
+            <h2 className="mb-2 text-lg font-bold">친구 목록</h2>
+            <div className="space-y-4">
+              {friends.map((friend) => (
+                <div
+                  key={friend.id}
+                  className="flex items-center justify-between p-4 border rounded-lg"
+                >
+                  <div>
+                    <h3 className="font-semibold text-md">
+                      {friend.friendInfo.name}
+                    </h3>
+                    <p className="text-[10px] tablet:text-sm text-gray-600">
+                      {friend.friendInfo.email}
+                    </p>
+                  </div>
+                  <Button
+                    size="sm"
+                    theme="black"
+                    onClick={() => handleDeleteFriend(friend.id)}
+                  >
+                    삭제
+                  </Button>
+                </div>
+              ))}
+              {isLoading && <p>로딩 중...</p>}
+              {!hasNext && (
+                <p className="text-center label-2">더 이상 친구가 없습니다.</p>
+              )}
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
 };
 
 export default MyPage;
diff --git a/src/store/authStore.js b/src/store/authStore.js
index 05da3b6..5715ec0 100644
--- a/src/store/authStore.js
+++ b/src/store/authStore.js
@@ -11,6 +11,7 @@ const useAuthStore = create((set) => ({
     try {
       const userInfo = await getSessionInfo();
       set({ user: userInfo });
+      localStorage.setItem("user", { user: userInfo });
     } catch (error) {
       console.error("Failed to fetch session info:", error);
       set({ user: null });
@@ -24,6 +25,7 @@ const useAuthStore = create((set) => ({
     try {
       await logout();
       set({ user: null });
+      localStorage.removeItem("user");
     } catch (error) {
       console.error("Failed to logout:", error);
     }
-- 
GitLab


From 2ec5e31b1f010d6caa20901b6ec0895d3f5b2ed4 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 19:38:32 +0900
Subject: [PATCH 06/13] =?UTF-8?q?hotfix:=20=EB=A1=9C=EA=B7=B8=EC=95=84?=
 =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=ED=99=88=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?=
 =?UTF-8?q?=EB=A0=89=EC=85=98=20=EB=B0=8F=20=ED=99=88=ED=8E=98=EC=9D=B4?=
 =?UTF-8?q?=EC=A7=80=20=EB=B2=84=ED=8A=BC=20=EB=A1=9C=EC=A7=81=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/layout/HeaderLogoBar.jsx | 3 +++
 src/pages/HomePage.jsx                  | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/layout/HeaderLogoBar.jsx b/src/components/layout/HeaderLogoBar.jsx
index 5772d41..6483cc9 100644
--- a/src/components/layout/HeaderLogoBar.jsx
+++ b/src/components/layout/HeaderLogoBar.jsx
@@ -2,16 +2,19 @@ import React, { useState } from "react";
 import LogoIcon from "../icons/LogoIcon";
 import useAuthStore from "../../store/authStore";
 import Button from "../Button";
+import { useNavigate } from "react-router-dom";
 
 const HeaderLogoBar = () => {
   const { user, logoutUser } = useAuthStore(); // Zustand에서 상태 및 메서드 가져오기
   const [loading, setLoading] = useState(false); // 로딩 상태 관리
+  const navigate = useNavigate();
 
   // 로그아웃 처리
   const handleLogout = async () => {
     try {
       setLoading(true); // 로딩 상태 활성화
       await logoutUser(); // 로그아웃 실행
+      navigate("/");
     } catch (error) {
       console.error("Failed to logout:", error);
     } finally {
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index b61cc10..e6252be 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -80,7 +80,7 @@ const HomePage = () => {
 
   const handleStartNow = () => {
     if (user) {
-      navigate("/chat-room"); // 번개 채팅방으로 리다이렉션
+      navigate("/chattinglist"); // 번개 채팅방으로 리다이렉션
     } else {
       navigate("/login"); // 로그인 페이지로 리다이렉션
     }
-- 
GitLab


From 7d7172b29f75ad8bf909bce5d2bb12dccd583bf9 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 20:06:39 +0900
Subject: [PATCH 07/13] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20stor?=
 =?UTF-8?q?e=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/store/authStore.js | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/store/authStore.js b/src/store/authStore.js
index 5715ec0..e521680 100644
--- a/src/store/authStore.js
+++ b/src/store/authStore.js
@@ -11,7 +11,13 @@ const useAuthStore = create((set) => ({
     try {
       const userInfo = await getSessionInfo();
       set({ user: userInfo });
-      localStorage.setItem("user", { user: userInfo });
+      localStorage.setItem("user", userInfo);
+      const nickname = userInfo.name || "Unknown";
+      localStorage.setItem("nickname", nickname);
+      console.log("반환값 userInfo: " + userInfo);
+      const localuser = localStorage.getItem("user");
+      console.log("user: " + localuser);
+      console.log("nickname: " + nickname);
     } catch (error) {
       console.error("Failed to fetch session info:", error);
       set({ user: null });
@@ -26,6 +32,7 @@ const useAuthStore = create((set) => ({
       await logout();
       set({ user: null });
       localStorage.removeItem("user");
+      localStorage.removeItem("nickname");
     } catch (error) {
       console.error("Failed to logout:", error);
     }
-- 
GitLab


From b7ba7fb01be97a56c4f65370d76ea494b0f7019f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=8B=AC=EC=9E=AC=EC=97=BD?= <jysim0326@ajou.ac.kr>
Date: Sun, 8 Dec 2024 20:21:22 +0900
Subject: [PATCH 08/13] =?UTF-8?q?fix:=20nickname=20=EB=A1=9C=EC=BB=AC?=
 =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20=EC=A0=80=EC=9E=A5=20?=
 =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/store/authStore.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/store/authStore.js b/src/store/authStore.js
index e521680..a944cdc 100644
--- a/src/store/authStore.js
+++ b/src/store/authStore.js
@@ -12,7 +12,7 @@ const useAuthStore = create((set) => ({
       const userInfo = await getSessionInfo();
       set({ user: userInfo });
       localStorage.setItem("user", userInfo);
-      const nickname = userInfo.name || "Unknown";
+      const nickname = userInfo.user.name || "Unknown";
       localStorage.setItem("nickname", nickname);
       console.log("반환값 userInfo: " + userInfo);
       const localuser = localStorage.getItem("user");
-- 
GitLab


From 01ff468272179f56a73cb61c0c732fea860bdff0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=8B=AC=EC=9E=AC=EC=97=BD?= <jysim0326@ajou.ac.kr>
Date: Sun, 8 Dec 2024 20:21:47 +0900
Subject: [PATCH 09/13] =?UTF-8?q?fix:=20api=20=EA=B2=BD=EB=A1=9C=20"/"=20?=
 =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/ChattingDetail.jsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/ChattingDetail.jsx b/src/components/ChattingDetail.jsx
index ef2a6ae..a163c99 100644
--- a/src/components/ChattingDetail.jsx
+++ b/src/components/ChattingDetail.jsx
@@ -529,7 +529,7 @@ function ChattingDetail() {
 
   const updateLastReadAt = async () => {
     try {
-      const response = await fetch(`${process.env.REACT_APP_BASE_URL}api/chat/update-read-status`, {
+      const response = await fetch(`${process.env.REACT_APP_BASE_URL}/api/chat/update-read-status`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
-- 
GitLab


From 02a59654bcf53523b3c13c2f50a897f9cbae5219 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=8B=AC=EC=9E=AC=EC=97=BD?= <jysim0326@ajou.ac.kr>
Date: Sun, 8 Dec 2024 20:22:20 +0900
Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20=EB=8B=89=EB=84=A4=EC=9E=84?=
 =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=20=EB=B6=80=EB=B6=84=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/ChattingList.jsx | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/components/ChattingList.jsx b/src/components/ChattingList.jsx
index 4c079e9..ff4e5d1 100644
--- a/src/components/ChattingList.jsx
+++ b/src/components/ChattingList.jsx
@@ -116,10 +116,6 @@ function ChattingList() {
   }, [nickname]);
 
   const joinRoom = (chatRoomId, chatRoomName) => {
-    if (!nickname.trim()) {
-      alert("닉네임을 입력하세요.");
-      return;
-    }
     setUnreadCounts((prevUnreadCounts) => ({
       ...prevUnreadCounts,
       [chatRoomId]: 0,
-- 
GitLab


From c6c79b594c8dde1fc8fe7b8bfa8b2eeefd02cb5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=A1=B0=EB=8C=80=ED=9D=AC?= <joedaehui@ajou.ac.kr>
Date: Sun, 8 Dec 2024 20:44:41 +0900
Subject: [PATCH 11/13] =?UTF-8?q?[#13]=20=EC=8A=A4=EC=BC=80=EC=A4=84=20?=
 =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?=
 =?UTF-8?q?=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/pages/SchedulePage.jsx | 333 ++++++++++++++++++++++++++-----------
 1 file changed, 239 insertions(+), 94 deletions(-)

diff --git a/src/pages/SchedulePage.jsx b/src/pages/SchedulePage.jsx
index 35a53ba..5febb0c 100644
--- a/src/pages/SchedulePage.jsx
+++ b/src/pages/SchedulePage.jsx
@@ -7,6 +7,7 @@ import {
   fetchAllSchedules,
   updateSchedule,
 } from "../api/schedule";
+import Button from "../components/Button";
 
 const generateTimeSlots = () => {
   const timeSlots = [];
@@ -41,8 +42,56 @@ const days = ["월", "화", "수", "목", "금", "토", "일"];
 //     createdAt: "2024-12-02T09:54:53.000Z",
 //     updatedAt: "2024-12-02T09:54:53.000Z",
 //   },
+//   {
+//     id: 11,
+//     user_id: 1,
+//     title: "점심약속",
+//     is_fixed: false,
+//     time_indices: [240, 241, 242],
+//     createdAt: "2024-12-02T09:54:53.000Z",
+//     updatedAt: "2024-12-02T09:54:53.000Z",
+//   },
+//   {
+//     id: 14,
+//     user_id: 1,
+//     title: "롤 5:5",
+//     is_fixed: true,
+//     time_indices: [302, 303, 304, 305, 306, 307],
+//     createdAt: "2024-12-02T09:54:53.000Z",
+//     updatedAt: "2024-12-02T09:54:53.000Z",
+//   },
+//   {
+//     id: 20,
+//     user_id: 1,
+//     title: "토트넘 vs 첼시 경기",
+//     is_fixed: true,
+//     time_indices: [13, 14, 15, 16, 17, 18],
+//     createdAt: "2024-12-02T09:54:53.000Z",
+//     updatedAt: "2024-12-02T09:54:53.000Z",
+//   },
+//   {
+//     id: 26,
+//     user_id: 1,
+//     title: "아침 구보",
+//     is_fixed: true,
+//     time_indices: [34, 35, 130, 131, 226, 227, 322, 323, 418, 419, 514, 515, 610, 611],
+//     createdAt: "2024-12-02T09:54:53.000Z",
+//     updatedAt: "2024-12-02T09:54:53.000Z",
+//   },
 // ];
 
+const colorClasses = [
+  "bg-indigo-300 hover:bg-indigo-400",
+  "bg-purple-300 hover:bg-purple-400",
+  "bg-pink-300 hover:bg-pink-400",
+  "bg-blue-300 hover:bg-blue-400",
+  "bg-green-300 hover:bg-green-400",
+  "bg-yellow-300 hover:bg-yellow-400",
+  "bg-red-300 hover:bg-red-400",
+  "bg-orange-300 hover:bg-orange-400",
+  "bg-teal-300 hover:bg-teal-400",
+  "bg-cyan-300 hover:bg-cyan-400",
+];
 const SchedulePage = () => {
   const timeSlots = generateTimeSlots();
   const [schedules, setSchedules] = useState([]);
@@ -52,13 +101,22 @@ const SchedulePage = () => {
   const [selectedSlots, setSelectedSlots] = useState([]);
   const [newTitle, setNewTitle] = useState("");
   const [isFixed, setIsFixed] = useState(true);
+  const [titleColorMap, setTitleColorMap] = useState(new Map());
+  const [showAllTimeSlot, setShowAllTimeSlot] = useState(false);
 
   useEffect(() => {
     // API
     const initializeSchedules = async () => {
       try {
         const data = await fetchAllSchedules();
-        setSchedules(data);
+
+        // 스케줄 병합을 위해서 사용
+        const sortedSchedules = [...data].sort((a, b) => {
+          const aMin = Math.min(...a.time_indices);
+          const bMin = Math.min(...b.time_indices);
+          return aMin - bMin;
+        });
+        setSchedules(sortedSchedules);
       } catch (error) {
         console.error("Failed to load schedules", error);
       }
@@ -70,6 +128,19 @@ const SchedulePage = () => {
     // setSchedules(dummySchedules);
   }, []);
 
+  useEffect(() => {
+    const newColorMap = new Map();
+    schedules.forEach((schedule) => {
+      if (!newColorMap.has(schedule.title)) {
+        newColorMap.set(
+          schedule.title,
+          colorClasses[newColorMap.size % colorClasses.length]
+        );
+      }
+    });
+    setTitleColorMap(newColorMap);
+  }, [schedules]);
+
   const handleSlotClick = async (timeIdx) => {
     if (!isEditMode) return;
 
@@ -206,13 +277,74 @@ const SchedulePage = () => {
     }
   };
 
+  const getColorForTitle = (title) => {
+    if (!titleColorMap.has(title)) {
+      const newColor = colorClasses[titleColorMap.size % colorClasses.length];
+      setTitleColorMap((prev) => new Map(prev).set(title, newColor));
+      return newColor;
+    }
+    return titleColorMap.get(title);
+  };
+
+  const convertIndexToTime = (timeIndex) => {
+    const dayIndex = Math.floor(timeIndex / 96);
+    const timeSlotIndex = timeIndex % 96;
+    const hour = Math.floor(timeSlotIndex / 4);
+    const minute = (timeSlotIndex % 4) * 15;
+    const day = days[dayIndex];
+    const time = `${hour.toString().padStart(2, "0")}:${minute
+      .toString()
+      .padStart(2, "0")}`;
+    return `${day} ${time}`;
+  };
+
+  // 스케줄 통합해서 보여주기
+  const renderTimeSlot = (slotIndex, rowIndex, colIndex) => {
+    const schedule = schedules.find((s) => s.time_indices.includes(slotIndex));
+    const isSelected = selectedSlots.includes(slotIndex);
+
+    const isFirstSlot =
+      schedule &&
+      !schedule.time_indices.includes(slotIndex - 1) &&
+      Math.floor((slotIndex - 1) / 96) === Math.floor(slotIndex / 96);
+
+    const isLastSlot =
+      schedule &&
+      !schedule.time_indices.includes(slotIndex + 1) &&
+      Math.floor((slotIndex + 1) / 96) === Math.floor(slotIndex / 96);
+
+    return (
+      <div
+        key={slotIndex}
+        className={`p-2 border ${
+          schedule
+            ? `${getColorForTitle(schedule.title)} text-white
+               ${!isFirstSlot && !isLastSlot ? "border-t-0 border-b-0" : ""}
+               ${!isFirstSlot ? "border-t-0" : ""}
+               ${!isLastSlot ? "border-b-0" : ""}`
+            : isSelected
+            ? "bg-primary-100 border-primary-300"
+            : "bg-grayscale-50"
+        } cursor-pointer`}
+        onClick={() => handleSlotClick(slotIndex)}
+      >
+        {isFirstSlot ? schedule?.title : ""}
+      </div>
+    );
+  };
+
+  const filterTimeSlots = (time) => {
+    const hour = parseInt(time.split(":")[0]);
+    return showAllTimeSlot || (hour >= 8 && hour <= 18);
+  };
+
   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>
+        <h1 className="heading-2">내 시간표</h1>
         <label className="flex items-center space-x-3 cursor-pointer">
-          <span className="title-1 text-primary-500">Edit Mode</span>
+          <span className="title-1 text-primary-500">수정 모드</span>
           <div
             className={`relative w-12 h-6 rounded-full transition-colors ${
               isEditMode ? "bg-primary-500" : "bg-grayscale-300"
@@ -232,12 +364,59 @@ const SchedulePage = () => {
         </label>
       </div>
 
+      {/* 더보기 버튼 */}
+      <div className="flex justify-center mt-4">
+        <Button
+          theme="white"
+          size="sm"
+          className="w-full mx-4 hover:bg-grayscale-50"
+          onClick={() => setShowAllTimeSlot(!showAllTimeSlot)}
+        >
+          {showAllTimeSlot ? "시간 접기" : "전체 시간 보기"}
+        </Button>
+      </div>
+
+      {/* Schedule Grid */}
+      <div className="p-4 pb-[210px]">
+        <div className="overflow-auto scrollbar-hide">
+          <div className="w-[100vw] tablet:w-[960px] grid grid-cols-[64px,repeat(7,1fr)] gap-0">
+            {/* 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) => {
+              if (!filterTimeSlots(time)) return null;
+
+              return (
+                <React.Fragment key={rowIndex}>
+                  <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;
+                    return renderTimeSlot(slotIndex, rowIndex, colIndex);
+                  })}
+                </React.Fragment>
+              );
+            })}
+          </div>
+        </div>
+      </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`}
-          >
+        <div className="fixed bottom-0 right-0 z-10 flex items-center justify-center w-full">
+          <div className="transform transition-transform w-full max-w-[768px] tablet:rounded-2xl bg-primary-100/90 backdrop-blur-sm 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>
@@ -253,7 +432,7 @@ const SchedulePage = () => {
                     <strong>선택된 시간:</strong>{" "}
                     {selectedSchedule.time_indices.map((time_idx) => (
                       <Label key={time_idx} theme="indigo" size="sm">
-                        {time_idx}
+                        {convertIndexToTime(time_idx)}
                       </Label>
                     ))}
                   </div>
@@ -288,36 +467,51 @@ const SchedulePage = () => {
                   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"
+                  placeholder="스케줄 제목"
+                  className="w-full p-2 px-4 mb-4 border rounded-full 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 className="flex items-center space-x-4">
+                    <label className="flex items-center cursor-pointer">
+                      <input
+                        type="radio"
+                        name="is_fixed"
+                        value={true}
+                        checked={isFixed === true}
+                        onChange={() => setIsFixed(true)}
+                        className="hidden peer"
+                      />
+                      <div className="flex items-center justify-center w-5 h-5 border-2 border-gray-400 rounded-full peer-checked:border-tertiary-500 peer-checked:bg-tertiary-500">
+                        <div className="w-2.5 h-2.5 bg-white rounded-full peer-checked:bg-white"></div>
+                      </div>
+                      <span className="ml-2 text-sm font-medium peer-checked:text-tertiary-500">
+                        고정 스케줄
+                      </span>
+                    </label>
+
+                    <label className="flex items-center cursor-pointer">
+                      <input
+                        type="radio"
+                        name="is_fixed"
+                        value={false}
+                        checked={isFixed === false}
+                        onChange={() => setIsFixed(false)}
+                        className="hidden peer"
+                      />
+                      <div className="flex items-center justify-center w-5 h-5 border-2 border-gray-400 rounded-full peer-checked:border-primary-500 peer-checked:bg-primary-500">
+                        <div className="w-2.5 h-2.5 bg-white rounded-full peer-checked:bg-white"></div>
+                      </div>
+                      <span className="ml-2 text-sm font-medium peer-checked:text-primary-500">
+                        유동 스케줄
+                      </span>
+                    </label>
+                  </div>
                 </div>
-                <div className="mb-4 body-1">
-                  <span>선택된 시간:</span>
+                <span className="heading-4">선택된 시간</span>
+                <div className="flex flex-wrap gap-1 p-2 m-2 border rounded-lg body-1 border-primary-500">
                   {selectedSlots.map((time_idx) => (
-                    <Label key={time_idx} theme="indigo" size="sm">
-                      {time_idx}
+                    <Label key={time_idx} theme="solid" size="sm">
+                      {convertIndexToTime(time_idx)}
                     </Label>
                   ))}
                 </div>
@@ -329,19 +523,23 @@ const SchedulePage = () => {
                     수정 완료
                   </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"
+                  <div className="flex justify-center w-full mt-4 space-x-2">
+                    <Button
+                      theme="indigo"
+                      size="md"
+                      className="flex-1"
                       onClick={() => handleCancelSchedule()}
                     >
                       취소
-                    </button>
-                    <button
-                      className="px-4 py-2 font-bold text-white rounded bg-gradient-pink"
+                    </Button>
+                    <Button
+                      theme="pink"
+                      size="md"
+                      className="flex-1"
                       onClick={() => handleCreateSchedule()}
                     >
                       추가
-                    </button>
+                    </Button>
                   </div>
                 )}
               </>
@@ -349,59 +547,6 @@ const SchedulePage = () => {
           </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>
   );
 };
-- 
GitLab


From 10c5b0d8ae9ec00c8c0660118a04a7524479629b 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 21:21:43 +0900
Subject: [PATCH 12/13] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?=
 =?UTF-8?q?=EB=A1=9C=EC=A7=81=20fcm=20=ED=86=A0=ED=81=B0=20=ED=8F=AC?=
 =?UTF-8?q?=ED=95=A8=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95=20(#14)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/auth.js | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/api/auth.js b/src/api/auth.js
index df4042f..fe57b12 100644
--- a/src/api/auth.js
+++ b/src/api/auth.js
@@ -5,7 +5,16 @@
  * @returns {string} 로그인 엔드포인트 URL
  */
 export const getLoginUrl = () => {
-  return `${process.env.REACT_APP_BASE_URL}/api/auth/login`;
+  const baseUrl = process.env.REACT_APP_BASE_URL;
+  const fcmToken = localStorage.getItem("fcmToken");
+
+  // fcmToken이 있을 경우 파라미터에 추가
+  const params = new URLSearchParams();
+  if (fcmToken) {
+    params.append("fcmToken", fcmToken);
+  }
+
+  return `${baseUrl}/api/auth/login?${params.toString()}`;
 };
 
 /**
-- 
GitLab


From 51bcb25ef0c4f5c517c1887dd24d225f63412f09 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 21:22:19 +0900
Subject: [PATCH 13/13] =?UTF-8?q?feat:=20404NotFoundPage=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80=20=EA=B0=9C=EB=B0=9C=20(#14)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/App.js                 |  2 ++
 src/pages/NotFoundPage.jsx | 28 ++++++++++++++++++++++++++++
 2 files changed, 30 insertions(+)
 create mode 100644 src/pages/NotFoundPage.jsx

diff --git a/src/App.js b/src/App.js
index 5cc4e04..ced6082 100644
--- a/src/App.js
+++ b/src/App.js
@@ -11,6 +11,7 @@ import Footer from "./components/layout/Footer";
 import BodyLayout from "./components/layout/BodyLayout";
 import HeaderLogoBar from "./components/layout/HeaderLogoBar";
 import SchedulePage from "./pages/SchedulePage";
+import NotFoundPage from "./pages/NotFoundPage";
 
 const App = () => {
   return (
@@ -29,6 +30,7 @@ const App = () => {
             />
             <Route path="/mypage" element={<MyPage />} />
             <Route path="/login" element={<LoginPage />} />
+            <Route path="*" element={<NotFoundPage />} />
           </Routes>
         </BodyLayout>
         <Footer />
diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx
new file mode 100644
index 0000000..1612743
--- /dev/null
+++ b/src/pages/NotFoundPage.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import Button from "../components/Button"; // 기존 디자인 시스템의 버튼 컴포넌트
+
+const NotFoundPage = () => {
+  const navigate = useNavigate();
+
+  const handleGoHome = () => {
+    navigate("/");
+  };
+
+  return (
+    <div className="flex flex-col items-center justify-center min-h-screen text-center bg-gray-50">
+      <h1 className="text-6xl font-bold text-primary-500">404</h1>
+      <p className="mt-4 text-lg text-gray-700">
+        페이지를 찾을 수 없습니다. 잘못된 URL을 입력했거나 페이지가 삭제되었을
+        수 있습니다.
+      </p>
+      <div className="mt-6">
+        <Button size="lg" theme="indigo" onClick={handleGoHome}>
+          홈으로 이동
+        </Button>
+      </div>
+    </div>
+  );
+};
+
+export default NotFoundPage;
-- 
GitLab