Repository: kookmin-sw/capstone-2025-24
Pull request #167: [Frontend] feat: 로그아웃 연동 및 모니터링 부분 수정
Author: login: seo0o519
Labels: name: :sparkles: FEAT
description: 새 기능 추가
name: :nail_care: FRONTEND
Created: 2025-05-01T11:36:37Z | Merged: 2025-05-02T15:59:55Z

## Summary 📝
<!-- 간단한 요약내용을 써주세요 -->
로그아웃 api 연동했고, 로그인 상태 관리랑 라우터가이드 등등 추가했습니다.
그리고 cctv 주소 클릭 가능하게 수정했고, 사건 모달에서 cctv로 이동하는 로직 구현했습니다
+ 버셀 배포시에 필요한 vercel.json 파일이 추가되었습니다.

## Issue Number 🔥
<!-- #뒤에 이슈넘버 써주시면 자동으로 이슈페이지 연결이 됩니다!-->

- #157

## To Reviewers 📢
<!-- 리뷰어에게 전달하고 싶은 말을 써주세요 -->



Diff:
diff --git a/frontend/index.html b/frontend/index.html
index f0b9df33..ee250738 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Vite + React + TS</title>
+    <title>누리눈</title>
     <script
       type="text/javascript"
       strategy="beforeInteractive"
diff --git a/frontend/src/apis/LoginApi.ts b/frontend/src/apis/LoginApi.ts
index b5dd840d..758d069f 100644
--- a/frontend/src/apis/LoginApi.ts
+++ b/frontend/src/apis/LoginApi.ts
@@ -1,4 +1,5 @@
 import { axiosInstance } from '@/apis/axiosInstance';
+import { ProfileType } from '@/types/profile';
 
 export const postLogin = async (userId: string, password: string) => {
   try {
@@ -9,5 +10,16 @@ export const postLogin = async (userId: string, password: string) => {
     return response.data;
   } catch (error) {
     console.error('로그인 에러:', error);
+    throw error;
   }
+};
+
+export const postLogout = async () => {
+  const response = await axiosInstance.post('/api/v1/logout');
+  return response.data;
+};
+
+export const getSessionInfo = async (): Promise<ProfileType> => {
+  const response = await axiosInstance.get('/api/v1/session');
+  return response.data;
 };
\ No newline at end of file
diff --git a/frontend/src/components/common/Sidebar/Layout.tsx b/frontend/src/components/Layout/Layout.tsx
similarity index 54%
rename from frontend/src/components/common/Sidebar/Layout.tsx
rename to frontend/src/components/Layout/Layout.tsx
index 3858ddf3..db20bfd2 100644
--- a/frontend/src/components/common/Sidebar/Layout.tsx
+++ b/frontend/src/components/Layout/Layout.tsx
@@ -1,6 +1,7 @@
 import { Outlet } from 'react-router-dom';
-import { Sidebar } from './Sidebar';
-import * as S from './Sidebar.style';
+import { Sidebar } from './Sidebar/Sidebar';
+import * as S from './Sidebar/Sidebar.style';
+import RealTimeAlert from '@/components/RealTimeAlert/RealTimeAlert';
 
 const Layout = () => {
   return (
@@ -9,6 +10,7 @@ const Layout = () => {
       <S.MainContent>
         <Outlet />
       </S.MainContent>
+      <RealTimeAlert/>
     </S.Layout>
   );
 };
diff --git a/frontend/src/components/Layout/Sidebar/ConfirmModal.tsx b/frontend/src/components/Layout/Sidebar/ConfirmModal.tsx
new file mode 100644
index 00000000..4f05f5ee
--- /dev/null
+++ b/frontend/src/components/Layout/Sidebar/ConfirmModal.tsx
@@ -0,0 +1,23 @@
+import * as S from './Sidebar.style';
+
+interface ConfirmModalProps {
+  message: string;
+  onConfirm: () => void;
+  onCancel: () => void;
+}
+
+const ConfirmModal = ({ message, onConfirm, onCancel }: ConfirmModalProps) => {
+  return (
+    <S.Backdrop>
+      <S.ModalBox>
+        <S.Message>{message}</S.Message>
+        <S.ButtonGroup>
+          <S.CancelBtn onClick={onCancel}>취소</S.CancelBtn>
+          <S.ConfirmBtn onClick={onConfirm}>확인</S.ConfirmBtn>
+        </S.ButtonGroup>
+      </S.ModalBox>
+    </S.Backdrop>
+  );
+};
+
+export default ConfirmModal;
diff --git a/frontend/src/components/Layout/Sidebar/LogoutButton.tsx b/frontend/src/components/Layout/Sidebar/LogoutButton.tsx
new file mode 100644
index 00000000..f3d1ba1a
--- /dev/null
+++ b/frontend/src/components/Layout/Sidebar/LogoutButton.tsx
@@ -0,0 +1,38 @@
+import { useAuthStore } from '@/stores/authStore';
+import * as S from './Sidebar.style';
+import { useState } from 'react';
+import ConfirmModal from './ConfirmModal';
+
+const LogoutButton = () => {
+  const { logout } = useAuthStore();
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  const handleLogout = async () => {
+    try {
+      await logout();
+      window.location.href = '/';
+    } catch (error) {
+      console.error('로그아웃 실패:', error);
+      alert('로그아웃 중 문제가 발생했습니다.');
+    }
+  };
+
+  return (
+    <>
+      <S.LogoutBtn onClick={() => setIsModalOpen(true)}>
+        <S.LogoutLogo />
+        로그아웃
+      </S.LogoutBtn>
+
+      {isModalOpen && (
+        <ConfirmModal
+          message="로그아웃 하시겠습니까?"
+          onConfirm={handleLogout}
+          onCancel={() => setIsModalOpen(false)}
+        />
+      )}
+    </>
+  );
+};
+
+export default LogoutButton;
diff --git a/frontend/src/components/common/Sidebar/Profile.tsx b/frontend/src/components/Layout/Sidebar/Profile.tsx
similarity index 100%
rename from frontend/src/components/common/Sidebar/Profile.tsx
rename to frontend/src/components/Layout/Sidebar/Profile.tsx
diff --git a/frontend/src/components/common/Sidebar/Sidebar.style.ts b/frontend/src/components/Layout/Sidebar/Sidebar.style.ts
similarity index 57%
rename from frontend/src/components/common/Sidebar/Sidebar.style.ts
rename to frontend/src/components/Layout/Sidebar/Sidebar.style.ts
index 55252b02..a07e0c93 100644
--- a/frontend/src/components/common/Sidebar/Sidebar.style.ts
+++ b/frontend/src/components/Layout/Sidebar/Sidebar.style.ts
@@ -1,5 +1,6 @@
 import { Avatar, Button } from '@mui/material';
 import styled from 'styled-components';
+import { RiLogoutBoxRLine } from 'react-icons/ri';
 
 // Layout ---------------------------//
 export const Layout = styled.div`
@@ -20,6 +21,9 @@ export const SidebarDiv = styled.div`
   background-color: rgb(9, 29, 109);
   box-shadow: 10px 0px 10px 0px rgba(200, 200, 200, 0.7);
   z-index: 5;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
 `;
 
 export const BtnDiv = styled.div`
@@ -88,10 +92,10 @@ export const SidebarBtn = styled(Button)`
   cursor: default;
 
   gap: 9px;
-  color: var(--primary700);
+  color: white;
   background-color: #00ff0000;
   .IconType {
-    color: var(--primary700);
+    color: white;
   }
 
   &:hover {
@@ -103,3 +107,80 @@ export const SidebarBtn = styled(Button)`
     border: 1px solid #465fc3;
   }
 `;
+
+// LogoutButton.tsx ------------------------------- //
+export const LogoutBtn = styled.button`
+  border: none;
+  display: flex;
+  flex-direction: row;
+  justify-content: start;
+  align-items: center;
+  font-size: 16px;
+  padding: 8px 6px;
+  padding-left: 15px;
+  cursor: pointer;
+  background-color: transparent;
+  color: var(--gray400);
+`;
+
+export const LogoutLogo = styled(RiLogoutBoxRLine)`
+  font-size: 20px;
+  margin: 15px 8px;
+  color: var(--gray400);
+`;
+
+// ConfirmModal.tsx -----------------------//
+export const Backdrop = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 999;
+`;
+
+export const ModalBox = styled.div`
+  background: white;
+  padding: 40px;
+  border-radius: 10px;
+  text-align: center;
+  width: 350px;
+`;
+
+export const Message = styled.p`
+  margin-bottom: 20px;
+  padding: 20px;
+  font-size: 20px;
+  font-weight: 500;
+`;
+
+export const ButtonGroup = styled.div`
+  display: flex;
+  justify-content: center;
+  gap: 10px;
+`;
+
+export const ConfirmBtn = styled.button`
+  background: var(--primary900);
+  color: white;
+  border: none;
+  padding: 10px 20px;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 15px;
+  font-weight: 500;
+`;
+
+export const CancelBtn = styled.button`
+  border: 1px solid #e0e0e0;
+  padding: 10px 20px;
+  border-radius: 5px;
+  cursor: pointer;
+  background-color: transparent;
+  font-size: 15px;
+  font-weight: 500;
+`;
diff --git a/frontend/src/components/common/Sidebar/Sidebar.tsx b/frontend/src/components/Layout/Sidebar/Sidebar.tsx
similarity index 69%
rename from frontend/src/components/common/Sidebar/Sidebar.tsx
rename to frontend/src/components/Layout/Sidebar/Sidebar.tsx
index e9d581e8..e0e2225e 100644
--- a/frontend/src/components/common/Sidebar/Sidebar.tsx
+++ b/frontend/src/components/Layout/Sidebar/Sidebar.tsx
@@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom';
 import useSidebarStore from '@/stores/sidebarStore';
 import { useEffect } from 'react';
 import { useLocation } from 'react-router-dom';
+import LogoutButton from './LogoutButton';
 
 export const Sidebar = () => {
   const location = useLocation();
@@ -46,16 +47,25 @@ export const Sidebar = () => {
 
   return (
     <S.SidebarDiv>
-      <S.LogoDiv>로고</S.LogoDiv>
-      <Profile />
-      <S.BtnDiv>
-        {SIDEBAR_LIST.map((it) => {
-          const Icon = it.icon;
-          return (
-            <SidebarButton key={it.text} text={it.text} icon={Icon} id={it.id} onClick={() => handleNavigate(it.id)} />
-          );
-        })}
-      </S.BtnDiv>
+      <div>
+        <S.LogoDiv>로고</S.LogoDiv>
+        <Profile />
+        <S.BtnDiv>
+          {SIDEBAR_LIST.map((it) => {
+            const Icon = it.icon;
+            return (
+              <SidebarButton
+                key={it.text}
+                text={it.text}
+                icon={Icon}
+                id={it.id}
+                onClick={() => handleNavigate(it.id)}
+              />
+            );
+          })}
+        </S.BtnDiv>
+      </div>
+      <LogoutButton />
     </S.SidebarDiv>
   );
 };
diff --git a/frontend/src/components/common/Sidebar/SidebarButton.tsx b/frontend/src/components/Layout/Sidebar/SidebarButton.tsx
similarity index 100%
rename from frontend/src/components/common/Sidebar/SidebarButton.tsx
rename to frontend/src/components/Layout/Sidebar/SidebarButton.tsx
diff --git a/frontend/src/components/Login/LoginForm.tsx b/frontend/src/components/Login/LoginForm.tsx
index 449d7f1d..82a553d5 100644
--- a/frontend/src/components/Login/LoginForm.tsx
+++ b/frontend/src/components/Login/LoginForm.tsx
@@ -1,21 +1,23 @@
 import * as S from './Login.style.ts';
 import { useState } from 'react';
-import { postLogin } from '@/apis/LoginApi';
 import { useNavigate } from 'react-router-dom';
-import { useProfileStore } from '@/stores/profileStore.ts';
-import { ProfileType } from '@/types/profile';
+import { useAuthStore } from '@/stores/authStore';
+
 const LoginForm = () => {
-  const { setProfile } = useProfileStore();
   const [userId, setUserId] = useState<string>('');
   const [password, setPassword] = useState<string>('');
   const navigate = useNavigate();
+  const { login } = useAuthStore();
 
   const submitLogin = async (e: React.FormEvent) => {
     e.preventDefault();
-    const data: ProfileType = await postLogin(userId, password);
-    setProfile(() => data);
-    console.log('로그인 응답', data);
-    navigate('monitoring');
+    try {
+      await login(userId, password);
+      navigate('/monitoring');
+    } catch (error) {
+      console.error('로그인 실패:', error);
+      alert(error instanceof Error ? error.message : '로그인 중 알 수 없는 오류가 발생했습니다.');
+    }
   };
 
   return (
@@ -37,4 +39,4 @@ const LoginForm = () => {
   );
 };
 
-export default LoginForm;
\ No newline at end of file
+export default LoginForm;
diff --git a/frontend/src/components/Monitoring/AlertList/AlertItem.tsx b/frontend/src/components/Monitoring/AlertList/AlertItem.tsx
index 1501918a..de484147 100644
--- a/frontend/src/components/Monitoring/AlertList/AlertItem.tsx
+++ b/frontend/src/components/Monitoring/AlertList/AlertItem.tsx
@@ -8,7 +8,7 @@ interface AlertItemProps extends AlertProps {
   clicked: boolean;
 }
 
-const AlertItem = ({ id, level, category, date, address, state, clicked }: AlertItemProps) => {
+const AlertItem = ({ id, level, cctvId, category, date, address, state, clicked }: AlertItemProps) => {
   const { isModalOpen, openModal, closeModal } = useIsModalOpen();
   const { updateItemState } = useItemStore();
 
@@ -38,6 +38,7 @@ const AlertItem = ({ id, level, category, date, address, state, clicked }: Alert
               alertItem={{
                 id,
                 level,
+                cctvId,
                 category,
                 date,
                 address,
diff --git a/frontend/src/components/Monitoring/AlertList/AlertList.tsx b/frontend/src/components/Monitoring/AlertList/AlertList.tsx
index 4479cf88..812c02cd 100644
--- a/frontend/src/components/Monitoring/AlertList/AlertList.tsx
+++ b/frontend/src/components/Monitoring/AlertList/AlertList.tsx
@@ -48,6 +48,7 @@ const AlertList = () => {
           alerts.map((alert) => (
             <AlertItem
               key={alert.id}
+              cctvId={alert.cctvId}
               id={alert.id}
               level={alert.level}
               category={alert.category}
diff --git a/frontend/src/components/Monitoring/AlertModal/AlertModal.style.ts b/frontend/src/components/Monitoring/AlertModal/AlertModal.style.ts
index 244e464b..db086db4 100644
--- a/frontend/src/components/Monitoring/AlertModal/AlertModal.style.ts
+++ b/frontend/src/components/Monitoring/AlertModal/AlertModal.style.ts
@@ -1,5 +1,6 @@
 import styled, { css } from 'styled-components';
 import Lottie from 'lottie-react';
+import { BiSolidCctv } from 'react-icons/bi';
 
 // AlertModal.tsx ------------------------------------//
 
@@ -43,6 +44,7 @@ export const ModalContainer = styled.div<{ highlight: boolean }>`
     }
   }
 `;
+// IncidentModal.tsx -------------------------//
 
 export const CloseButton = styled.div`
   position: absolute;
@@ -102,6 +104,36 @@ export const Button = styled.button`
   }
 `;
 
+export const VideoWrapper = styled.div`
+  position: relative;
+  width: 100%;
+  height: 100%;
+`;
+
+export const CctvButton = styled.button`
+  position: absolute;
+  top: 15px;
+  right: 15px;
+
+  background-color: rgb(250, 247, 247);
+  border: none;
+  border-radius: 50px;
+  font-size: 12px;
+  padding: 7px 11px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  z-index: 2;
+`;
+
+export const RecordIcon = styled(BiSolidCctv)`
+  color: var(--primary900);
+  width: 20px;
+  height: 20px;
+  margin: 0 3px;
+`;
+
 // FeedbackModal.tsx ------------------------------------
 
 export const FeedbackLayout = styled.div`
diff --git a/frontend/src/components/Monitoring/AlertModal/AlertModal.tsx b/frontend/src/components/Monitoring/AlertModal/AlertModal.tsx
index fb0a0fed..6a2c4acf 100644
--- a/frontend/src/components/Monitoring/AlertModal/AlertModal.tsx
+++ b/frontend/src/components/Monitoring/AlertModal/AlertModal.tsx
@@ -13,6 +13,7 @@ import { ToastContainer } from 'react-toastify';
 import { toast } from 'react-toastify';
 import 'react-toastify/dist/ReactToastify.css';
 import { FaRegCheckCircle } from 'react-icons/fa';
+import { useSelectedCctvStore } from '@/stores/selectedCctvStore';
 
 interface ModalProps {
   onClose: () => void;
@@ -27,6 +28,7 @@ const AlertModal = ({ onClose, alertItem, highlight }: ModalProps & { highlight:
   const [step, setStep] = useState<ModalStep>('incident');
   const { setHighlight } = useHighlightStore();
   const [isUpdate, setIsUpdate] = useState(false);
+  const { setSelectedIndex } = useSelectedCctvStore();
   const navigate = useNavigate();
 
   const handleOutsideClick = () => {
@@ -81,6 +83,22 @@ const AlertModal = ({ onClose, alertItem, highlight }: ModalProps & { highlight:
     setStep('submit');
   };
 
+  const handleCctvView = () => {
+    if (isUpdate) updateItemState(alertItem.id, '출동');
+    onClose();
+    setSelectedIndex(alertItem.cctvId);
+
+    if (location.pathname !== '/monitoring') {
+      navigate('/monitoring');
+    }
+    setTimeout(() => {
+      const target = document.getElementById('info-section');
+      if (target) {
+        target.scrollIntoView({ behavior: 'smooth' });
+      }
+    }, 150);
+  };
+
   return (
     <S.Overlay onClick={handleOutsideClick}>
       <ToastContainer />
@@ -92,6 +110,7 @@ const AlertModal = ({ onClose, alertItem, highlight }: ModalProps & { highlight:
             onFeedbackClick={handleFeedback}
             onDispatch={handleDispatch}
             isUpdate={isUpdate}
+            onCctvClick={handleCctvView}
           />
         )}
         {step === 'feedback' && (
diff --git a/frontend/src/components/Monitoring/AlertModal/IncidentModal.tsx b/frontend/src/components/Monitoring/AlertModal/IncidentModal.tsx
index 789f0d41..a1b98e9a 100644
--- a/frontend/src/components/Monitoring/AlertModal/IncidentModal.tsx
+++ b/frontend/src/components/Monitoring/AlertModal/IncidentModal.tsx
@@ -14,9 +14,10 @@ interface IncidentModalProps {
   alertItem: AlertProps;
   onDispatch: () => void;
   isUpdate: boolean;
+  onCctvClick: () => void;
 }
 
-const IncidentModal = ({ onClose, alertItem, onFeedbackClick, onDispatch, isUpdate }: IncidentModalProps) => {
+const IncidentModal = ({ onClose, alertItem, onFeedbackClick, onDispatch, isUpdate, onCctvClick }: IncidentModalProps) => {
   const { id, category, date, address, video } = alertItem;
   const [videoUrl, setVideoUrl] = useState('');
   const { updateItemState } = useItemStore();
@@ -26,6 +27,7 @@ const IncidentModal = ({ onClose, alertItem, onFeedbackClick, onDispatch, isUpda
     setVideoUrl(response.video);
   };
 
+
   useEffect(() => {
     if (video != undefined) {
       setVideoUrl(video);
@@ -55,7 +57,13 @@ const IncidentModal = ({ onClose, alertItem, onFeedbackClick, onDispatch, isUpda
           {date}
         </S.InfoDiv>
       </S.InfoContainer>
-      <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+      <S.VideoWrapper>
+        <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+        <S.CctvButton onClick={onCctvClick}>
+          <S.RecordIcon />
+          CCTV 보러 가기
+        </S.CctvButton>
+      </S.VideoWrapper>
       <S.ButtonContainer>
         <S.Button className="no" onClick={onFeedbackClick}>
           미출동
diff --git a/frontend/src/components/Monitoring/InfoSection/InfoBox.tsx b/frontend/src/components/Monitoring/InfoSection/InfoBox.tsx
index 9665c9ff..344c7bfe 100644
--- a/frontend/src/components/Monitoring/InfoSection/InfoBox.tsx
+++ b/frontend/src/components/Monitoring/InfoSection/InfoBox.tsx
@@ -2,13 +2,14 @@ import * as S from './InfoSection.style';
 import cctvIcon from '@/assets/icons/cctvIcon.svg';
 import cctvIconBlue from '@/assets/icons/cctvBlueIcon.svg';
 import { CctvInfo } from '@/types/cctv';
+import { useSelectedCctvStore } from '@/stores/selectedCctvStore';
 
 interface InfoBoxProps {
-  selectedIndex: number | null;
-  Locations: CctvInfo[];
+  locations: CctvInfo[];
 }
 
-const InfoBox = ({ selectedIndex, Locations }: InfoBoxProps) => {
+const InfoBox = ({ locations }: InfoBoxProps) => {
+  const { selectedIndex, setSelectedIndex } = useSelectedCctvStore();
   return (
     <S.InfoBoxLayout>
       <S.UpperDiv>
@@ -20,9 +21,13 @@ const InfoBox = ({ selectedIndex, Locations }: InfoBoxProps) => {
       </S.UpperDiv>
       <S.Line />
       <S.LocationDiv>
-        {Locations.map((location, index) => (
-          <S.Location key={index} selected={selectedIndex === index}>
-            <img src={selectedIndex === index ? cctvIconBlue : cctvIcon} alt="cctv-icon" />
+        {locations.map((location, index) => (
+          <S.Location
+            key={index}
+            selected={selectedIndex === location.id}
+            onClick={() => setSelectedIndex(location.id)}
+          >
+            <img src={selectedIndex === location.id ? cctvIconBlue : cctvIcon} alt="cctv-icon" />
             {location.address}
           </S.Location>
         ))}
diff --git a/frontend/src/components/Monitoring/InfoSection/InfoSection.style.ts b/frontend/src/components/Monitoring/InfoSection/InfoSection.style.ts
index 31da90f2..c8e7e0cb 100644
--- a/frontend/src/components/Monitoring/InfoSection/InfoSection.style.ts
+++ b/frontend/src/components/Monitoring/InfoSection/InfoSection.style.ts
@@ -87,6 +87,7 @@ export const LocationDiv = styled.div`
   padding: 0 7px;
   max-height: 157px;
   overflow-y: auto;
+  cursor: pointer;
 
   img {
     width: 22px;
diff --git a/frontend/src/components/Monitoring/InfoSection/InfoSection.tsx b/frontend/src/components/Monitoring/InfoSection/InfoSection.tsx
index aa1618bd..ee216a94 100644
--- a/frontend/src/components/Monitoring/InfoSection/InfoSection.tsx
+++ b/frontend/src/components/Monitoring/InfoSection/InfoSection.tsx
@@ -6,26 +6,31 @@ import VideoPlayer from './VideoPlayer';
 import { useEffect } from 'react';
 import { CctvInfo } from '@/types/cctv';
 import { getCctvInfo } from '@/apis/CctvApi';
+import { useSelectedCctvStore } from '@/stores/selectedCctvStore';
 
 const InfoSection = () => {
-  const [selectedIndex, setSelectedIndex] = useState<number | null>(0);
   const [cctv, setCctv] = useState<CctvInfo[]>([]);
+  const { setSelectedIndex } = useSelectedCctvStore();
 
   useEffect(() => {
     const fetchCctv = async () => {
       const data = await getCctvInfo();
       setCctv(data);
+      if (data.length > 0) {
+        setSelectedIndex(data[0].id);
+      }
     };
-
+  
     fetchCctv();
   }, []);
+  
   return (
-    <S.InfoLayout>
+    <S.InfoLayout id="info-section">
       <S.InfoContent>
-        <KakaoMap selectedIndex={selectedIndex} setSelectedIndex={setSelectedIndex} Locations={cctv} />
-        <InfoBox selectedIndex={selectedIndex} Locations={cctv} />
+        <KakaoMap locations={cctv} />
+        <InfoBox locations={cctv} />
       </S.InfoContent>
-      <VideoPlayer selectedIndex={selectedIndex} Locations={cctv} />
+      <VideoPlayer locations={cctv} />
     </S.InfoLayout>
   );
 };
diff --git a/frontend/src/components/Monitoring/InfoSection/KakaoMap.tsx b/frontend/src/components/Monitoring/InfoSection/KakaoMap.tsx
index 35774061..ab523c12 100644
--- a/frontend/src/components/Monitoring/InfoSection/KakaoMap.tsx
+++ b/frontend/src/components/Monitoring/InfoSection/KakaoMap.tsx
@@ -3,17 +3,17 @@ import { MapMarker } from 'react-kakao-maps-sdk';
 import cctvIcon from '@/assets/icons/cctvIcon.svg';
 import cctvIconBlue from '@/assets/icons/cctvBlueIcon.svg';
 import { CctvInfo } from '@/types/cctv';
+import { useSelectedCctvStore } from '@/stores/selectedCctvStore';
 
 interface KakaoMapProps {
-  selectedIndex: number | null;
-  setSelectedIndex: (index: number | null) => void;
-  Locations: CctvInfo[];
+  locations: CctvInfo[];
 }
 
-const KakaoMap = ({ selectedIndex, setSelectedIndex, Locations }: KakaoMapProps) => {
+const KakaoMap = ({ locations }: KakaoMapProps) => {
+  const { selectedIndex, setSelectedIndex } = useSelectedCctvStore();
   return (
-    <S.Maps id="map" center={{ lat: 37.611, lng: 126.9949 }} level={4} zoomable={false} >
-      {Locations.map((location, index) => (
+    <S.Maps id="map" center={{ lat: 37.6105, lng: 126.9978 }} level={4} zoomable={false}>
+      {locations.map((location, index) => (
         <MapMarker
           key={index}
           position={{
@@ -21,13 +21,13 @@ const KakaoMap = ({ selectedIndex, setSelectedIndex, Locations }: KakaoMapProps)
             lng: location.longitude,
           }}
           image={{
-            src: selectedIndex === index ? cctvIconBlue : cctvIcon,
+            src: selectedIndex === location.id ? cctvIconBlue : cctvIcon,
             size: {
               width: 22,
               height: 22,
             },
           }}
-          onClick={() => setSelectedIndex(index)}
+          onClick={() => setSelectedIndex(location.id)}
         />
       ))}
     </S.Maps>
diff --git a/frontend/src/components/Monitoring/InfoSection/VideoPlayer.tsx b/frontend/src/components/Monitoring/InfoSection/VideoPlayer.tsx
index f0a6d5e3..7c04834a 100644
--- a/frontend/src/components/Monitoring/InfoSection/VideoPlayer.tsx
+++ b/frontend/src/components/Monitoring/InfoSection/VideoPlayer.tsx
@@ -2,32 +2,33 @@ import { useEffect, useRef } from 'react';
 import Hls from 'hls.js';
 import * as S from './InfoSection.style';
 import { CctvInfo } from '@/types/cctv';
+import { useSelectedCctvStore } from '@/stores/selectedCctvStore';
 
 interface VideoPlayerProps {
-  selectedIndex: number | null;
-  Locations: CctvInfo[];
+  locations: CctvInfo[];
 }
 
-const VideoPlayer = ({ selectedIndex, Locations }: VideoPlayerProps) => {
+const VideoPlayer = ({ locations }: VideoPlayerProps) => {
+  const { selectedIndex } = useSelectedCctvStore();
   const videoRef = useRef<HTMLVideoElement | null>(null);
 
   useEffect(() => {
     if (selectedIndex === null) return;
-    const index = selectedIndex;
 
-    if (!Locations[index]) return;
+    const target = locations.find((loc) => loc.id === selectedIndex);
+    if (!target) return;
 
     const video = videoRef.current;
     const hls = new Hls();
 
     if (video && Hls.isSupported()) {
-      hls.loadSource(Locations[index].liveUrl);
+      hls.loadSource(target.liveUrl);
       hls.attachMedia(video);
       hls.on(Hls.Events.MANIFEST_PARSED, () => {
         video.play();
       });
     } else if (video?.canPlayType('application/vnd.apple.mpegurl')) {
-      video.src = Locations[index].liveUrl;
+      video.src = target.liveUrl;
       video.addEventListener('loadedmetadata', () => {
         video.play();
       });
@@ -36,7 +37,7 @@ const VideoPlayer = ({ selectedIndex, Locations }: VideoPlayerProps) => {
     return () => {
       hls.destroy();
     };
-  }, [selectedIndex, Locations]);
+  }, [selectedIndex, locations]);
 
   return <S.VideoPlayer ref={videoRef} muted autoPlay playsInline />;
 };
diff --git a/frontend/src/components/RealTimeAlert/RealTimeAlert.tsx b/frontend/src/components/RealTimeAlert/RealTimeAlert.tsx
index 0f5abac4..42f2905d 100644
--- a/frontend/src/components/RealTimeAlert/RealTimeAlert.tsx
+++ b/frontend/src/components/RealTimeAlert/RealTimeAlert.tsx
@@ -21,14 +21,14 @@ const RealTimeAlert = () => {
 
   if (!alertData) return null;
 
-  const { id, level, category, date, address, state, video } = alertData;
+  const { id, level, cctvId, category, date, address, state, video } = alertData;
 
   return (
     <div>
       {level === 2 && isModalOpen && (
         <AlertModal
           onClose={handleModalClose}
-          alertItem={{ id, level, category, date, address, state, video }}
+          alertItem={{ id, level, cctvId, category, date, address, state, video }}
           highlight={!!highlight}
         />
       )}
diff --git a/frontend/src/routes/PrivateRoute.tsx b/frontend/src/routes/PrivateRoute.tsx
new file mode 100644
index 00000000..de958813
--- /dev/null
+++ b/frontend/src/routes/PrivateRoute.tsx
@@ -0,0 +1,41 @@
+import { useEffect, useState } from 'react';
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuthStore } from '@/stores/authStore';
+import { useProfileStore } from '@/stores/profileStore';
+import { getSessionInfo } from '@/apis/LoginApi';
+
+const PrivateRoute = () => {
+  const { isLoggedIn, clearAuth } = useAuthStore();
+  const { setProfile } = useProfileStore();
+  const [checking, setChecking] = useState(true);
+
+  useEffect(() => {
+    const verifySession = async () => {
+      try {
+        const userInfo = await getSessionInfo();
+        setProfile(() => userInfo);
+      } catch (error) {
+        console.log(error)
+        alert('로그인 후 이용하세요');
+        clearAuth();
+
+      } finally {
+        setChecking(false);
+      }
+    };
+
+    verifySession();
+  }, []);
+
+  if (checking) {
+    return null;
+  }
+
+  if (!isLoggedIn) {
+    return <Navigate to="/" replace />;
+  }
+
+  return <Outlet />;
+};
+
+export default PrivateRoute;
diff --git a/frontend/src/routes/PublicRoute.tsx b/frontend/src/routes/PublicRoute.tsx
new file mode 100644
index 00000000..d0bb012e
--- /dev/null
+++ b/frontend/src/routes/PublicRoute.tsx
@@ -0,0 +1,14 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuthStore } from '@/stores/authStore';
+
+const PublicRoute = () => {
+  const { isLoggedIn } = useAuthStore();
+
+  if (isLoggedIn) {
+    return <Navigate to="/monitoring" replace />;
+  }
+
+  return <Outlet />;
+};
+
+export default PublicRoute;
diff --git a/frontend/src/routes/Router.tsx b/frontend/src/routes/Router.tsx
index e9985af8..12d63f00 100644
--- a/frontend/src/routes/Router.tsx
+++ b/frontend/src/routes/Router.tsx
@@ -3,20 +3,23 @@ import ChartPage from '../pages/ChartPage/ChartPage';
 import MonitoringPage from '../pages/MonitoringPage/MonitoringPage';
 import IncidentHistoryPage from '../pages/IncidentHistoryPage/IncidentHistoryPage';
 import LoginPage from '../pages/LoginPage/LoginPage';
-import Layout from '../components/common/Sidebar/Layout';
-import RealTimeAlert from '@/components/RealTimeAlert/RealTimeAlert.tsx';
+import Layout from '../components/Layout/Layout';
+import PrivateRoute from './PrivateRoute';
+import PublicRoute from './PublicRoute';
 
 const Router = () => {
   return (
     <BrowserRouter>
-      <RealTimeAlert />
       <Routes>
-        <Route path="/" element={<LoginPage />} />
-        <Route element={<Layout />}>
-          <Route path="/monitoring" element={<MonitoringPage />} />
-          <Route path="/history" element={<IncidentHistoryPage />} />
-          <Route path="/chart" element={<ChartPage />} />
-          <Route element={<RealTimeAlert />} />
+        <Route element={<PublicRoute />}>
+          <Route path="/" element={<LoginPage />} />
+        </Route>
+        <Route element={<PrivateRoute />}>
+          <Route element={<Layout />}>
+            <Route path="/monitoring" element={<MonitoringPage />} />
+            <Route path="/history" element={<IncidentHistoryPage />} />
+            <Route path="/chart" element={<ChartPage />} />
+          </Route>
         </Route>
       </Routes>
     </BrowserRouter>
diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts
new file mode 100644
index 00000000..8d5f69f6
--- /dev/null
+++ b/frontend/src/stores/authStore.ts
@@ -0,0 +1,44 @@
+import { create } from 'zustand';
+import { postLogin, postLogout } from '@/apis/LoginApi';
+import { useProfileStore } from './profileStore';
+import { persist } from 'zustand/middleware';
+
+interface AuthState {
+  isLoggedIn: boolean;
+  login: (userId: string, password: string) => Promise<void>;
+  logout: () => Promise<void>;
+  clearAuth: () => void;
+}
+
+export const useAuthStore = create<AuthState>()(
+  persist(
+    (set, get) => ({
+      isLoggedIn: false,
+
+      login: async (userId, password) => {
+        const profile = await postLogin(userId, password);
+        useProfileStore.getState().setProfile(() => profile);
+        set({ isLoggedIn: true });
+      },
+
+      logout: async () => {
+        await postLogout();
+        get().clearAuth();
+      },
+
+      clearAuth: () => {
+        useProfileStore.getState().setProfile(() => ({
+          officeId: 0,
+          name: '',
+          officeName: '',
+          profileUrl: '',
+          rank: '',
+        }));
+        set({ isLoggedIn: false });
+      },
+    }),
+    {
+      name: 'auth-storage',
+    }
+  )
+);
diff --git a/frontend/src/stores/selectedCctvStore.ts b/frontend/src/stores/selectedCctvStore.ts
new file mode 100644
index 00000000..11e86e05
--- /dev/null
+++ b/frontend/src/stores/selectedCctvStore.ts
@@ -0,0 +1,11 @@
+import { create } from 'zustand';
+
+interface SelectedCctvStore {
+  selectedIndex: number | null;
+  setSelectedIndex: (index: number | null) => void;
+}
+
+export const useSelectedCctvStore = create<SelectedCctvStore>((set) => ({
+  selectedIndex: 1,
+  setSelectedIndex: (index) => set({ selectedIndex: index }),
+}));
\ No newline at end of file
diff --git a/frontend/src/types/alert.ts b/frontend/src/types/alert.ts
index dcd7d7e3..3bd39f68 100644
--- a/frontend/src/types/alert.ts
+++ b/frontend/src/types/alert.ts
@@ -2,6 +2,7 @@ export interface AlertProps {
   id: number;
   level: number;
   category: string | null;
+  cctvId: number;
   address: string;
   date: string;
   state: '미확인' | '확인' | '미출동' | '출동' | '완료';
diff --git a/frontend/src/types/cctv.ts b/frontend/src/types/cctv.ts
index b48f9cb5..2882d2ee 100644
--- a/frontend/src/types/cctv.ts
+++ b/frontend/src/types/cctv.ts
@@ -1,5 +1,5 @@
 export interface CctvInfo{
-  id: string;
+  id: number;
   latitude: number;
   longitude: number;
   address: string;
diff --git a/frontend/vercel.json b/frontend/vercel.json
new file mode 100644
index 00000000..6eb126da
--- /dev/null
+++ b/frontend/vercel.json
@@ -0,0 +1,5 @@
+{
+    "rewrites":  [
+      {"source": "/(.*)", "destination": "/"}
+    ]
+  }
\ No newline at end of file


Reviews:
user: login: HeeNamgoong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T12:50:42Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:21:13Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:26:24Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:30:45Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:38:11Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T14:15:06Z
user: login: hummingbbird
body: 수고하셨어요! 👍 그런데 저도(6번 계정) `InfoBox`에 선택 안 된 상태로 뜨네요...! 확인 한 번 부탁드려용~
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T00:15:51Z
user: login: HeeNamgoong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T05:27:02Z
user: login: HeeNamgoong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:05:02Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:10:05Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:10:22Z
user: login: seo0o519
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:28:50Z
user: login: hummingbbird
body: 반영된 거 확인 완~ 수고했어요 😇 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-02T15:32:17Z

Review comments:
diff_hunk: @@ -55,7 +59,20 @@ const IncidentModal = ({ onClose, alertItem, onFeedbackClick, onDispatch, isUpda
           {date}
         </S.InfoDiv>
       </S.InfoContainer>
-      <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+      <S.VideoWrapper>
+        <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+        <S.CctvButton
+          onClick={async () => {
+            if (isUpdate) updateItemState(alertItem.id, '출동');
path: frontend/src/components/Monitoring/AlertModal/IncidentModal.tsx
user: login: HeeNamgoong
body: 이거 넣어야하는지 어떻게 아셨어요 !! 굳잡 ^_^
created_at: 2025-05-01T12:22:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,10 +21,10 @@ const InfoSection = () => {
   return (
     <S.InfoLayout>
       <S.InfoContent>
-        <KakaoMap selectedIndex={selectedIndex} setSelectedIndex={setSelectedIndex} Locations={cctv} />
-        <InfoBox selectedIndex={selectedIndex} Locations={cctv} />
+        <KakaoMap Locations={cctv} />
path: frontend/src/components/Monitoring/InfoSection/InfoSection.tsx
user: login: HeeNamgoong
body: p3) 우리 변수명 카멜케이스로 하기로 했으니까 Locations를 다 locations로 수정하면 좋을 거 같습니당 첫글자가 대문자이면 파스칼케이스니까! props의 프로퍼티 변수명만 다 바꿔주면 좋을 거 같아요
(+ 처음에 이 부분 만들었을 때 제가 Locations라고 한 거 같은데 ㅋㅋ 번거롭게 해 죄송해요🥲)
created_at: 2025-05-01T12:32:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,10 +21,10 @@ const InfoSection = () => {
   return (
     <S.InfoLayout>
       <S.InfoContent>
-        <KakaoMap selectedIndex={selectedIndex} setSelectedIndex={setSelectedIndex} Locations={cctv} />
-        <InfoBox selectedIndex={selectedIndex} Locations={cctv} />
+        <KakaoMap Locations={cctv} />
path: frontend/src/components/Monitoring/InfoSection/InfoSection.tsx
user: login: HeeNamgoong
body: 일일이 바꾸지 말고 vs의 검색에서 Locations: CctvInfo[]; 일케 치고
locations: CctvInfo[]; 바꿀 내용으로 이렇게 한번에 바꾸면 될 거예요 ㅎㅎ

그럼 변수명 오류로 파일마다 빨간색 뜰테니까 그 파일들 다 따라가면 되고.. 바꿀 게 넘 많은 거 같아서 ㅜㅜㅜ
created_at: 2025-05-01T12:34:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,3 +107,26 @@ export const SidebarBtn = styled(Button)`
     border: 1px solid #465fc3;
   }
 `;
+
+// LogoutButton.tsx ------------------------------- //
+export const LogoutBtn = styled.button`
+  border: none;
+  display: flex;
+  flex-direction: row;
+  justify-content: start;
+  align-items: center;
+  width: 90%;
path: frontend/src/components/common/Sidebar/Sidebar.style.ts
user: login: HeeNamgoong
body: p5) 아무런 효과가 없어서 없애도 될 거 같습니다ㅏ (width: 90%)
created_at: 2025-05-01T12:40:35Z
author_association: CONTRIBUTOR
path: frontend/src/routes/Router.tsx
user: login: HeeNamgoong
body: p2) RealTimeAlert PrivateRoute 안에 넣어주세요! 19번째 줄 아래 넣으면 될 거 같슴다
created_at: 2025-05-01T12:42:49Z
author_association: CONTRIBUTOR
path: frontend/vercel.json
user: login: HeeNamgoong
body: 이건 뭔가요?
created_at: 2025-05-01T12:44:45Z
author_association: CONTRIBUTOR
path: frontend/src/routes/PrivateRoute.tsx
user: login: HeeNamgoong
body: getLogin대신에 getSessionInfo로 요청보내고 프로필 이미지 저장하는 이유가 세션이 끝나고도 안 지워져서 인가요?
created_at: 2025-05-01T12:50:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +59,20 @@ const IncidentModal = ({ onClose, alertItem, onFeedbackClick, onDispatch, isUpda
           {date}
         </S.InfoDiv>
       </S.InfoContainer>
-      <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+      <S.VideoWrapper>
+        <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+        <S.CctvButton
+          onClick={async () => {
+            if (isUpdate) updateItemState(alertItem.id, '출동');
path: frontend/src/components/Monitoring/AlertModal/IncidentModal.tsx
user: login: seo0o519
body: ㅋㅋㅋ클로즈버튼 보고 따라햇죱
created_at: 2025-05-01T13:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,10 +21,10 @@ const InfoSection = () => {
   return (
     <S.InfoLayout>
       <S.InfoContent>
-        <KakaoMap selectedIndex={selectedIndex} setSelectedIndex={setSelectedIndex} Locations={cctv} />
-        <InfoBox selectedIndex={selectedIndex} Locations={cctv} />
+        <KakaoMap Locations={cctv} />
path: frontend/src/components/Monitoring/InfoSection/InfoSection.tsx
user: login: seo0o519
body: 넵 수정했습니다~
created_at: 2025-05-01T13:26:24Z
author_association: CONTRIBUTOR
path: frontend/src/routes/Router.tsx
user: login: seo0o519
body: 호 저렇게 하니까 에러뜨네여? 좀 알아봐야될듯,,
created_at: 2025-05-01T13:30:45Z
author_association: CONTRIBUTOR
path: frontend/vercel.json
user: login: seo0o519
body: 이거 버셀 배포할 때 필요한 파일이에요 없으면 404에러 뜸,,
created_at: 2025-05-01T13:38:11Z
author_association: CONTRIBUTOR
path: frontend/src/routes/PrivateRoute.tsx
user: login: seo0o519
body: 이게 세션 유효한지 확인하는 코드인데여,,
getSessionInfo로 세션이 유지되고 있는지 확인 요청을 보내요, 만약에 세션이 유효한 상태면 백에서 사용자 정보를 리스폰스로 주거든요? 그래서 이걸 사용자 프로필에 반영해요. 근데 이건 로그인 할 때도 하는거라 굳이 없어도 되는거긴 한데.. 일단 백에서 정보를 주니까 반영하도록 해놨어용
created_at: 2025-05-01T14:15:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,13 +1,29 @@
 import { axiosInstance } from '@/apis/axiosInstance';
+import { ProfileType } from '@/types/profile';
 
 export const postLogin = async (userId: string, password: string) => {
   try {
     const response = await axiosInstance.post('api/v1/auth', {
       userId,
       password,
     });
+
+    if (response.data?.message === '잘못된 아이디 혹은 비밀번호입니다.') {
path: frontend/src/apis/LoginApi.ts
user: login: hummingbbird
body: p2) 로그인 실패했을 때 에러아니고 `message` return  하는 걸로 바뀌어서 여기 코드는 없어도 될 거 같아요! 제가 로그인 처리하면서 이 부분 처리했습니다~(feature/164 브랜치)
created_at: 2025-05-01T23:58:23Z
author_association: MEMBER
diff_hunk: @@ -1,21 +1,23 @@
 import * as S from './Login.style.ts';
 import { useState } from 'react';
-import { postLogin } from '@/apis/LoginApi';
 import { useNavigate } from 'react-router-dom';
-import { useProfileStore } from '@/stores/profileStore.ts';
-import { ProfileType } from '@/types/profile';
+import { useAuthStore } from '@/stores/authStore';
+
 const LoginForm = () => {
-  const { setProfile } = useProfileStore();
   const [userId, setUserId] = useState<string>('');
   const [password, setPassword] = useState<string>('');
   const navigate = useNavigate();
+  const { login } = useAuthStore();
 
   const submitLogin = async (e: React.FormEvent) => {
     e.preventDefault();
-    const data: ProfileType = await postLogin(userId, password);
-    setProfile(() => data);
-    console.log('로그인 응답', data);
-    navigate('monitoring');
+    try {
+      await login(userId, password);
+      navigate('/monitoring');
+    } catch (error) {
+      console.error('로그인 실패:', error);
+      alert(error instanceof Error ? error.message : '로그인 중 알 수 없는 오류가 발생했습니다.');
path: frontend/src/components/Login/LoginForm.tsx
user: login: hummingbbird
body: p3) 여기도 제가 처리했습니다! 일단 머지하고 제 브랜치 pr 날릴 때 충돌난 거 해결해두 되니까 그냥 둬도 됩니다~
created_at: 2025-05-02T00:01:44Z
author_association: MEMBER
path: frontend/src/components/Monitoring/AlertModal/AlertModal.style.ts
user: login: hummingbbird
body: p5) 이 파일 컨에스 한 번만 눌러주세요~
created_at: 2025-05-02T00:03:23Z
author_association: MEMBER
diff_hunk: @@ -55,7 +59,20 @@ const IncidentModal = ({ onClose, alertItem, onFeedbackClick, onDispatch, isUpda
           {date}
         </S.InfoDiv>
       </S.InfoContainer>
-      <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+      <S.VideoWrapper>
+        <VideoComponent w="100%" h="100%" radius={10} video={videoUrl} />
+        <S.CctvButton
+          onClick={async () => {
path: frontend/src/components/Monitoring/AlertModal/IncidentModal.tsx
user: login: hummingbbird
body: p3) 여기 onClick 함수 분리해서 처리해주세요! 가독성이나 성능 측면에서 inline 함수는 가능한 지양하는 것이 좋습니다!
created_at: 2025-05-02T00:08:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,30 @@
+import { useAuthStore } from '@/stores/authStore';
+import * as S from './Sidebar.style';
+
+const LogoutButton = () => {
+  const { logout } = useAuthStore();
+
+  const handleLogout = async () => {
+    const confirmLogout = window.confirm('로그아웃 하시겠습니까?');
path: frontend/src/components/common/Sidebar/LogoutButton.tsx
user: login: hummingbbird
body: p4) window.confirm으로 처리하는 방식 말고 모달 하나 만들어서 하는 건 어때요? 
created_at: 2025-05-02T00:10:35Z
author_association: MEMBER
path: frontend/vercel.json
user: login: HeeNamgoong
body: 아하 이 브랜치에서 배포 관련 태스크도 있군요 그럼 pr summary 쪽에 추가 해주시면 감사하겠습니다
created_at: 2025-05-02T05:27:02Z
author_association: CONTRIBUTOR
path: frontend/src/routes/Router.tsx
user: login: HeeNamgoong
body: Layout 컴포넌트 안에 넣어할 거 같습니다. 그런데 ㅠㅠ Layout 안에 넣어버리면 티가 안나서 존재하는지 안 보여서 최선일진 잘 모르겠어요..
created_at: 2025-05-02T06:05:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,13 +1,29 @@
 import { axiosInstance } from '@/apis/axiosInstance';
+import { ProfileType } from '@/types/profile';
 
 export const postLogin = async (userId: string, password: string) => {
   try {
     const response = await axiosInstance.post('api/v1/auth', {
       userId,
       password,
     });
+
+    if (response.data?.message === '잘못된 아이디 혹은 비밀번호입니다.') {
path: frontend/src/apis/LoginApi.ts
user: login: seo0o519
body: 넵 지웠습니당
created_at: 2025-05-02T06:10:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,21 +1,23 @@
 import * as S from './Login.style.ts';
 import { useState } from 'react';
-import { postLogin } from '@/apis/LoginApi';
 import { useNavigate } from 'react-router-dom';
-import { useProfileStore } from '@/stores/profileStore.ts';
-import { ProfileType } from '@/types/profile';
+import { useAuthStore } from '@/stores/authStore';
+
 const LoginForm = () => {
-  const { setProfile } = useProfileStore();
   const [userId, setUserId] = useState<string>('');
   const [password, setPassword] = useState<string>('');
   const navigate = useNavigate();
+  const { login } = useAuthStore();
 
   const submitLogin = async (e: React.FormEvent) => {
     e.preventDefault();
-    const data: ProfileType = await postLogin(userId, password);
-    setProfile(() => data);
-    console.log('로그인 응답', data);
-    navigate('monitoring');
+    try {
+      await login(userId, password);
+      navigate('/monitoring');
+    } catch (error) {
+      console.error('로그인 실패:', error);
+      alert(error instanceof Error ? error.message : '로그인 중 알 수 없는 오류가 발생했습니다.');
path: frontend/src/components/Login/LoginForm.tsx
user: login: seo0o519
body: 일단 이부분은 남겨둘게요!
created_at: 2025-05-02T06:10:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+import { useAuthStore } from '@/stores/authStore';
+import * as S from './Sidebar.style';
+
+const LogoutButton = () => {
+  const { logout } = useAuthStore();
+
+  const handleLogout = async () => {
+    const confirmLogout = window.confirm('로그아웃 하시겠습니까?');
path: frontend/src/components/common/Sidebar/LogoutButton.tsx
user: login: seo0o519
body: 좋아요 얘도 그럼 디자인을 해보아요~
created_at: 2025-05-02T06:28:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: HeeNamgoong
created_at: 2025-05-01T12:56:41Z
body: 1번 계정에서는 InfoBox에서 default index값이 제일 처음 주소로 잘 보이는데 제 5번 계정에서는 아무것도 선택 안 된 상태이거든요? 왜 그럴까요..?
author_association: CONTRIBUTOR
user: login: seo0o519
created_at: 2025-05-01T14:01:39Z
body: > 1번 계정에서는 InfoBox에서 default index값이 제일 처음 주소로 잘 보이는데 제 5번 계정에서는 아무것도 선택 안 된 상태이거든요? 왜 그럴까요..?

아 지금은 디폴트 인덱스 값을 1로 해놨는데 생각해보니까 그럼 다른 계정에서는 안되겠네요ㅎㅣ;
cctv 목록중에 맨 첫번째가 디폴트이도록 수정했습니다~
author_association: CONTRIBUTOR