From de2cd907f13abf9b00d664a7eeb1a73823c49896 Mon Sep 17 00:00:00 2001 From: LHT Date: Thu, 12 Feb 2026 13:54:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=20=ED=86=B5=ED=95=A9=20(=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B0=80=EB=93=9C,=20fetchWithAuth,=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - authStore: 메인 프로젝트 세션 쿠키 기반 인증 상태 관리 - fetchWithAuth: 401 응답 시 메인 프로젝트 로그인 페이지 리다이렉트 - SessionGuard: 앱 진입 시 세션 유효성 검증 래퍼 컴포넌트 - 기존 API 모듈 fetch → fetchWithAuth 전환 - 환경변수에 VITE_MAIN_APP_URL, VITE_DEV_SKIP_AUTH 추가 Co-Authored-By: Claude Opus 4.6 --- .env | 3 + .env.dev | 3 + .env.development | 11 ++- .env.qa | 3 + src/App.jsx | 5 +- src/api/commonApi.js | 4 +- src/api/fetchWithAuth.js | 41 ++++++++++++ src/api/satelliteApi.js | 61 ++++++++--------- src/api/trackApi.js | 4 +- src/api/weatherApi.js | 37 ++++++----- src/areaSearch/services/areaSearchApi.js | 4 +- src/areaSearch/services/stsApi.js | 4 +- src/components/auth/SessionGuard.jsx | 36 ++++++++++ src/components/auth/SessionGuard.scss | 9 +++ src/components/layout/Header.jsx | 7 ++ src/stores/authStore.js | 85 ++++++++++++++++++++++++ vite.config.js | 21 ++++-- 17 files changed, 273 insertions(+), 65 deletions(-) create mode 100644 src/api/fetchWithAuth.js create mode 100644 src/components/auth/SessionGuard.jsx create mode 100644 src/components/auth/SessionGuard.scss create mode 100644 src/stores/authStore.js diff --git a/.env b/.env index 0b3484a5..ba05b9ba 100644 --- a/.env +++ b/.env @@ -28,3 +28,6 @@ VITE_TRACKING_WS=wss://mda.kcg.go.kr/ws-tracks/websocket # 선박 데이터 쓰로틀링 (ms, 위성망 대역폭 절약) VITE_SHIP_THROTTLE=30 + +# 메인 프로젝트 URL (세션 만료 시 리다이렉트) +VITE_MAIN_APP_URL=https://mda.kcg.go.kr diff --git a/.env.dev b/.env.dev index 5fb9e2b6..7d751e87 100644 --- a/.env.dev +++ b/.env.dev @@ -27,3 +27,6 @@ VITE_TRACKING_WS=ws://10.26.252.51:8090/ws-tracks/websocket # 선박 데이터 쓰로틀링 (ms) VITE_SHIP_THROTTLE=30 + +# 메인 프로젝트 URL (세션 만료 시 리다이렉트) +VITE_MAIN_APP_URL=http://10.26.252.39:9090 diff --git a/.env.development b/.env.development index 862fb9cb..6f09b3ff 100644 --- a/.env.development +++ b/.env.development @@ -4,8 +4,8 @@ # - 로컬 개발 전용 # ============================================ -# 배포 경로 (로컬 개발은 루트) -VITE_BASE_URL=/ +# 배포 경로 (프록시 모드: localhost:9090/kcgnv/ → localhost:3000/kcgnv/) +VITE_BASE_URL=/kcgnv/ # API 서버 (프록시 타겟) VITE_API_URL=http://10.26.252.39:9090 @@ -27,3 +27,10 @@ VITE_TRACKING_WS=ws://10.26.252.51:8090/ws-tracks/websocket # 선박 데이터 쓰로틀링 (ms, 0=무제한) VITE_SHIP_THROTTLE=0 + +# 메인 프로젝트 URL (세션 만료 시 리다이렉트) +VITE_MAIN_APP_URL=http://localhost:9090 + +# 로컬 개발 인증 우회 (포트가 달라 localStorage 공유 불가) +# true면 세션 없을 때 모의 사용자로 자동 로그인 +VITE_DEV_SKIP_AUTH=false diff --git a/.env.qa b/.env.qa index 12e75f45..1ec19406 100644 --- a/.env.qa +++ b/.env.qa @@ -27,3 +27,6 @@ VITE_TRACKING_WS=ws://10.188.141.123:9090/ws-tracks/websocket # 선박 데이터 쓰로틀링 (ms) VITE_SHIP_THROTTLE=30 + +# 메인 프로젝트 URL (세션 만료 시 리다이렉트) +VITE_MAIN_APP_URL=http://10.188.141.123:9090 diff --git a/src/App.jsx b/src/App.jsx index 91ab597a..d9600238 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,7 @@ import { lazy, Suspense } from 'react'; // 구현 영역 - 레이아웃 import MainLayout from './components/layout/MainLayout'; +import SessionGuard from './components/auth/SessionGuard'; import { ToastContainer } from './components/common/Toast'; import { AlertModalContainer } from './components/common/AlertModal'; @@ -22,7 +23,7 @@ const PublishRouter = import.meta.env.DEV export default function App() { return ( - <> + @@ -48,6 +49,6 @@ export default function App() { {/* />*/} {/*)}*/} - + ); } diff --git a/src/api/commonApi.js b/src/api/commonApi.js index 1bdafaf8..b3629b52 100644 --- a/src/api/commonApi.js +++ b/src/api/commonApi.js @@ -1,6 +1,7 @@ /** * 공통코드 API */ +import { fetchWithAuth } from './fetchWithAuth'; const COMMON_CODE_LIST_ENDPOINT = '/api/cmn/code/list/search'; @@ -12,10 +13,9 @@ const COMMON_CODE_LIST_ENDPOINT = '/api/cmn/code/list/search'; */ export async function fetchCommonCodeList(commonCodeTypeNumber) { try { - const response = await fetch(COMMON_CODE_LIST_ENDPOINT, { + const response = await fetchWithAuth(COMMON_CODE_LIST_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', body: JSON.stringify({ commonCodeTypeNumber }), }); diff --git a/src/api/fetchWithAuth.js b/src/api/fetchWithAuth.js new file mode 100644 index 00000000..59befcb7 --- /dev/null +++ b/src/api/fetchWithAuth.js @@ -0,0 +1,41 @@ +import { useAuthStore } from '../stores/authStore'; +import { SESSION_TIMEOUT_MS } from '../types/constants'; + +/** + * 인증 래퍼 fetch + * - 사전 체크: loginDate 기반 타임아웃 + * - 사후 체크: 4011 응답 감지 (세션 만료) + * - credentials: 'include' 자동 설정 + */ +export async function fetchWithAuth(url, options = {}) { + // 로컬 개발: 세션 타임아웃 체크 우회 + if (import.meta.env.VITE_DEV_SKIP_AUTH !== 'true') { + const loginDate = localStorage.getItem('loginDate'); + if (!loginDate || Date.now() - Number(loginDate) > SESSION_TIMEOUT_MS) { + useAuthStore.getState().handleSessionExpired(); + throw new Error('Session expired'); + } + localStorage.setItem('loginDate', String(Date.now())); + } + + const response = await fetch(url, { ...options, credentials: 'include' }); + + // JSON 응답에서 4011 체크 + if (response.ok) { + const ct = response.headers.get('content-type'); + if (ct && ct.includes('application/json')) { + const cloned = response.clone(); + try { + const data = await cloned.json(); + if (data?.code === 4011) { + useAuthStore.getState().handleSessionExpired(); + throw new Error('Session expired (4011)'); + } + } catch (e) { + if (e.message.includes('Session expired')) throw e; + } + } + } + + return response; +} diff --git a/src/api/satelliteApi.js b/src/api/satelliteApi.js index abe82b4d..60beab89 100644 --- a/src/api/satelliteApi.js +++ b/src/api/satelliteApi.js @@ -1,6 +1,7 @@ /** * 위성 API */ +import { fetchWithAuth } from './fetchWithAuth'; const SATELLITE_VIDEO_SEARCH_ENDPOINT = '/api/gis/satlit/search'; const SATELLITE_CSV_ENDPOINT = '/api/gis/satlit/excelToJson'; @@ -43,10 +44,10 @@ export async function fetchSatelliteVideoList({ satelliteVideoOrigin, }) { try { - const response = await fetch(SATELLITE_VIDEO_SEARCH_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_VIDEO_SEARCH_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ page, startDate, @@ -83,10 +84,10 @@ export async function fetchSatelliteVideoList({ */ export async function fetchSatelliteCsvFeatures(csvFileName) { try { - const response = await fetch(SATELLITE_CSV_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_CSV_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ csvFileName }), }); @@ -116,10 +117,10 @@ export async function fetchSatelliteCsvFeatures(csvFileName) { */ export async function fetchSatelliteVideoDetail(satelliteId) { try { - const response = await fetch(SATELLITE_DETAIL_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_DETAIL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ satelliteId }), }); @@ -154,10 +155,10 @@ export async function fetchSatelliteVideoDetail(satelliteId) { */ export async function updateSatelliteVideo(params) { try { - const response = await fetch(SATELLITE_UPDATE_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_UPDATE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify(params), }); @@ -177,10 +178,10 @@ export async function updateSatelliteVideo(params) { */ export async function fetchSatelliteCompanyList() { try { - const response = await fetch(SATELLITE_COMPANY_LIST_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_COMPANY_LIST_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({}), }); @@ -204,10 +205,10 @@ export async function fetchSatelliteCompanyList() { */ export async function fetchSatelliteManageList(companyNo) { try { - const response = await fetch(SATELLITE_MANAGE_LIST_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_MANAGE_LIST_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ companyNo }), }); @@ -230,9 +231,9 @@ export async function fetchSatelliteManageList(companyNo) { */ export async function saveSatelliteVideo(formData) { try { - const response = await fetch(SATELLITE_SAVE_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_SAVE_ENDPOINT, { method: 'POST', - credentials: 'include', + body: formData, }); @@ -255,10 +256,10 @@ export async function saveSatelliteVideo(formData) { */ export async function searchSatelliteCompany({ companyTypeCode, companyName, page, limit }) { try { - const response = await fetch(SATELLITE_COMPANY_SEARCH_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_COMPANY_SEARCH_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ companyTypeCode, companyName, page, limit }), }); @@ -289,10 +290,10 @@ export async function searchSatelliteCompany({ companyTypeCode, companyName, pag */ export async function saveSatelliteCompany(params) { try { - const response = await fetch(SATELLITE_COMPANY_SAVE_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_COMPANY_SAVE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify(params), }); @@ -313,10 +314,10 @@ export async function saveSatelliteCompany(params) { */ export async function fetchSatelliteCompanyDetail(companyNo) { try { - const response = await fetch(SATELLITE_COMPANY_DETAIL_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_COMPANY_DETAIL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ companyNo }), }); @@ -345,10 +346,10 @@ export async function fetchSatelliteCompanyDetail(companyNo) { */ export async function updateSatelliteCompany(params) { try { - const response = await fetch(SATELLITE_COMPANY_UPDATE_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_COMPANY_UPDATE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify(params), }); @@ -372,10 +373,10 @@ export async function updateSatelliteCompany(params) { */ export async function searchSatelliteManage({ companyNo, satelliteName, sensorType, page, limit }) { try { - const response = await fetch(SATELLITE_MANAGE_SEARCH_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_MANAGE_SEARCH_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ companyNo, satelliteName, sensorType, page, limit }), }); @@ -407,10 +408,10 @@ export async function searchSatelliteManage({ companyNo, satelliteName, sensorTy */ export async function saveSatelliteManage(params) { try { - const response = await fetch(SATELLITE_MANAGE_SAVE_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_MANAGE_SAVE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify(params), }); @@ -431,10 +432,10 @@ export async function saveSatelliteManage(params) { */ export async function fetchSatelliteManageDetail(satelliteManageId) { try { - const response = await fetch(SATELLITE_MANAGE_DETAIL_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_MANAGE_DETAIL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ satelliteManageId }), }); @@ -464,10 +465,10 @@ export async function fetchSatelliteManageDetail(satelliteManageId) { */ export async function updateSatelliteManage(params) { try { - const response = await fetch(SATELLITE_MANAGE_UPDATE_ENDPOINT, { + const response = await fetchWithAuth(SATELLITE_MANAGE_UPDATE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify(params), }); diff --git a/src/api/trackApi.js b/src/api/trackApi.js index 26a77b36..a710d591 100644 --- a/src/api/trackApi.js +++ b/src/api/trackApi.js @@ -8,6 +8,7 @@ * - 응답 데이터 가공 (ProcessedTrack 형태로 변환) */ import useShipStore from '../stores/shipStore'; +import { fetchWithAuth } from './fetchWithAuth'; /** API 엔드포인트 (메인 프로젝트와 동일) */ const API_ENDPOINT = '/api/v2/tracks/vessels'; @@ -31,10 +32,9 @@ export async function fetchTrackQuery({ startTime, endTime, vessels, isIntegrati isIntegration: isIntegration ? '1' : '0', }; - const response = await fetch(API_ENDPOINT, { + const response = await fetchWithAuth(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', body: JSON.stringify(body), }); diff --git a/src/api/weatherApi.js b/src/api/weatherApi.js index 66f2b84f..0c034065 100644 --- a/src/api/weatherApi.js +++ b/src/api/weatherApi.js @@ -1,6 +1,7 @@ /** * 기상해양 API */ +import { fetchWithAuth } from './fetchWithAuth'; const SPECIAL_NEWS_ENDPOINT = '/api/gis/weather/special-news/search'; @@ -16,10 +17,10 @@ const SPECIAL_NEWS_ENDPOINT = '/api/gis/weather/special-news/search'; */ export async function fetchWeatherAlerts({ startPresentationDate, endPresentationDate, page, limit }) { try { - const response = await fetch(SPECIAL_NEWS_ENDPOINT, { + const response = await fetchWithAuth(SPECIAL_NEWS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ startPresentationDate, endPresentationDate, @@ -59,10 +60,10 @@ const TYPHOON_DETAIL_ENDPOINT = '/api/gis/weather/typhoon/search'; */ export async function fetchTyphoonList({ typhoonBeginningYear, typhoonBeginningMonth, page, limit }) { try { - const response = await fetch(TYPHOON_LIST_ENDPOINT, { + const response = await fetchWithAuth(TYPHOON_LIST_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ typhoonBeginningYear, typhoonBeginningMonth, @@ -100,10 +101,10 @@ export async function fetchTyphoonList({ typhoonBeginningYear, typhoonBeginningM */ export async function fetchTyphoonDetail({ typhoonSequence, year }) { try { - const response = await fetch(TYPHOON_DETAIL_ENDPOINT, { + const response = await fetchWithAuth(TYPHOON_DETAIL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ typhoonSequence, year, @@ -135,10 +136,10 @@ const SUNRISE_SUNSET_DETAIL_ENDPOINT = '/api/gis/weather/tide-information/observ */ export async function fetchTideInformation() { try { - const response = await fetch(TIDE_INFORMATION_ENDPOINT, { + const response = await fetchWithAuth(TIDE_INFORMATION_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({}), }); @@ -171,10 +172,10 @@ export async function fetchTideInformation() { */ export async function fetchSunriseSunsetDetail(params) { try { - const response = await fetch(SUNRISE_SUNSET_DETAIL_ENDPOINT, { + const response = await fetchWithAuth(SUNRISE_SUNSET_DETAIL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify(params), }); @@ -201,10 +202,10 @@ const OBSERVATORY_DETAIL_ENDPOINT = '/api/gis/weather/observatory/select/detail/ */ export async function fetchObservatoryList() { try { - const response = await fetch(OBSERVATORY_ENDPOINT, { + const response = await fetchWithAuth(OBSERVATORY_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({}), }); @@ -239,10 +240,10 @@ const AIRPORT_DETAIL_ENDPOINT = '/api/gis/weather/airport/select'; */ export async function fetchAirportList() { try { - const response = await fetch(AIRPORT_ENDPOINT, { + const response = await fetchWithAuth(AIRPORT_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({}), }); @@ -267,10 +268,10 @@ export async function fetchAirportList() { */ export async function fetchAirportDetail({ airportId }) { try { - const response = await fetch(AIRPORT_DETAIL_ENDPOINT, { + const response = await fetchWithAuth(AIRPORT_DETAIL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ airportId }), }); @@ -288,10 +289,10 @@ export async function fetchAirportDetail({ airportId }) { export async function fetchObservatoryDetail({ observatoryId, toDate }) { try { - const response = await fetch(OBSERVATORY_DETAIL_ENDPOINT, { + const response = await fetchWithAuth(OBSERVATORY_DETAIL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + body: JSON.stringify({ observatoryId, toDate }), }); diff --git a/src/areaSearch/services/areaSearchApi.js b/src/areaSearch/services/areaSearchApi.js index 94e393da..16dd25d1 100644 --- a/src/areaSearch/services/areaSearchApi.js +++ b/src/areaSearch/services/areaSearchApi.js @@ -5,6 +5,7 @@ * 응답 변환: trackQueryApi.convertToProcessedTracks() 재사용 */ import { convertToProcessedTracks } from '../../tracking/services/trackQueryApi'; +import { fetchWithAuth } from '../../api/fetchWithAuth'; const API_ENDPOINT = '/api/v2/tracks/area-search'; @@ -62,10 +63,9 @@ export async function fetchAreaSearch(params) { polygons: params.polygons, }; - const response = await fetch(API_ENDPOINT, { + const response = await fetchWithAuth(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', body: JSON.stringify(request), }); diff --git a/src/areaSearch/services/stsApi.js b/src/areaSearch/services/stsApi.js index 5734a6a1..b3f27905 100644 --- a/src/areaSearch/services/stsApi.js +++ b/src/areaSearch/services/stsApi.js @@ -5,6 +5,7 @@ * 응답 변환: trackQueryApi.convertToProcessedTracks() 재사용 */ import { convertToProcessedTracks } from '../../tracking/services/trackQueryApi'; +import { fetchWithAuth } from '../../api/fetchWithAuth'; const API_ENDPOINT = '/api/v2/tracks/vessel-contacts'; @@ -37,10 +38,9 @@ export async function fetchVesselContacts(params) { maxContactDistanceMeters: params.maxContactDistanceMeters, }; - const response = await fetch(API_ENDPOINT, { + const response = await fetchWithAuth(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', body: JSON.stringify(request), }); diff --git a/src/components/auth/SessionGuard.jsx b/src/components/auth/SessionGuard.jsx new file mode 100644 index 00000000..529923d5 --- /dev/null +++ b/src/components/auth/SessionGuard.jsx @@ -0,0 +1,36 @@ +import { useEffect } from 'react'; +import { useAuthStore } from '../../stores/authStore'; +import useShipStore from '../../stores/shipStore'; +import { fetchUserFilter } from '../../api/userSettingApi'; +import './SessionGuard.scss'; + +export default function SessionGuard({ children }) { + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); + const isChecking = useAuthStore((s) => s.isChecking); + + useEffect(() => { + const result = useAuthStore.getState().checkSession(); + if (!result.valid) { + window.location.href = import.meta.env.VITE_MAIN_APP_URL || '/'; + return; + } + // 세션 유효 → 서버에 저장된 필터 설정 로드 + fetchUserFilter() + .then((filterArray) => { + if (filterArray) { + useShipStore.getState().applyFilterSettings(filterArray); + } + }) + .catch((err) => console.warn('[SessionGuard] 필터 로드 실패, 기본값 사용:', err)); + }, []); + + if (isChecking || !isAuthenticated) { + return ( +
+ {!isChecking &&

로그인 페이지로 이동합니다...

} +
+ ); + } + + return children; +} diff --git a/src/components/auth/SessionGuard.scss b/src/components/auth/SessionGuard.scss new file mode 100644 index 00000000..19a698cd --- /dev/null +++ b/src/components/auth/SessionGuard.scss @@ -0,0 +1,9 @@ +.session-guard-loading { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: #1a1a2e; + color: #fff; + font-size: 14px; +} diff --git a/src/components/layout/Header.jsx b/src/components/layout/Header.jsx index b05987b7..ebc56310 100644 --- a/src/components/layout/Header.jsx +++ b/src/components/layout/Header.jsx @@ -1,6 +1,7 @@ import { useRef } from 'react'; import { Link } from 'react-router-dom'; import { showAlert } from '../common/AlertModal'; +import { useAuthStore } from '../../stores/authStore'; const SAMPLE_ALERTS = [ { message: '서버 응답 시간이 초과되었습니다.\n잠시 후 다시 시도해 주세요.', errorCode: 'ERR_TIMEOUT_504' }, @@ -14,6 +15,7 @@ const SAMPLE_ALERTS = [ * - 로고, 알람, 설정(드롭다운), 마이페이지 */ export default function Header() { + const user = useAuthStore((s) => s.user); const alertIndexRef = useRef(0); const handleAlarmClick = (e) => { @@ -33,6 +35,11 @@ export default function Header() {