diff --git a/apps/nowait-user/src/assets/icon/cancel.svg b/apps/nowait-user/src/assets/icon/cancel.svg index b3dfb7df..2767b326 100644 --- a/apps/nowait-user/src/assets/icon/cancel.svg +++ b/apps/nowait-user/src/assets/icon/cancel.svg @@ -1,4 +1,4 @@ - - + + diff --git a/apps/nowait-user/src/assets/icon/search.svg b/apps/nowait-user/src/assets/icon/search.svg index adcd7125..0cae67f4 100644 --- a/apps/nowait-user/src/assets/icon/search.svg +++ b/apps/nowait-user/src/assets/icon/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/apps/nowait-user/src/components/Header.tsx b/apps/nowait-user/src/components/Header.tsx index 1e9016d2..f05eb8b2 100644 --- a/apps/nowait-user/src/components/Header.tsx +++ b/apps/nowait-user/src/components/Header.tsx @@ -3,12 +3,14 @@ import { AnimatePresence, motion } from "framer-motion"; import { useNavigate } from "react-router-dom"; import Logo from "../assets/logo.svg?react"; import Menu from "../assets/icon/menu.svg?react"; -import Search from "../assets/icon/search_black.svg?react"; +import Search from "../assets/icon/search.svg?react"; import Cancel from "../assets/icon/cancel.svg?react"; import Portal from "./common/modal/Portal"; +import SearchModal from "./common/modal/SearchModal"; const HomeHeader = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isSearchOpen, setIsSearchOpen] = useState(false); const navigate = useNavigate(); const toggleMenu = () => { @@ -19,6 +21,14 @@ const HomeHeader = () => { setIsMenuOpen(false); }; + const openSearch = () => { + setIsSearchOpen(true); + }; + + const closeSearch = () => { + setIsSearchOpen(false); + }; + const handleBookmarkClick = () => { closeMenu(); navigate("/bookmark"); @@ -34,7 +44,7 @@ const HomeHeader = () => {
- + + + {/* 검색 내용 영역 */} + + {searchQuery.trim() ? ( + // 검색 결과 표시 +
+
+ 검색 결과{" "} + {searchResults.length > 0 && `${searchResults.length}`} +
+ {isSearching ? ( +
+
+ 검색 중... +
+
+ ) : searchResults.length > 0 ? ( +
+ {searchResults.map((store) => ( + + ))} +
+ ) : ( +
+
+ 검색 결과가 없습니다 +
+
+ 다른 키워드로 검색해보세요 +
+
+ )} +
+ ) : ( + // 최근 검색 표시 +
+
+ 최근 검색 +
+ {recentSearches.length > 0 ? ( + recentSearches.map((searchTerm, index) => ( +
+ +
+ +
+
+ )) + ) : ( +
+ 최근 검색어가 없습니다 +
+ )} +
+ )} +
+
+ + )} + + + ); +}; + +export default SearchModal; diff --git a/apps/nowait-user/src/hooks/useInfiniteStores.ts b/apps/nowait-user/src/hooks/useInfiniteStores.ts index 5a095ed1..4eb7ef2d 100644 --- a/apps/nowait-user/src/hooks/useInfiniteStores.ts +++ b/apps/nowait-user/src/hooks/useInfiniteStores.ts @@ -33,7 +33,7 @@ const fetchStores = async ({ try { // UserApi 사용으로 헤더 설정 자동화 (인터셉터에서 최신 토큰 처리) const response = await UserApi.get( - "/v1/stores/all-stores/infinite-scroll", + "/v1/stores/all-stores", { params: { page: pageParam, diff --git a/apps/nowait-user/src/types/search.ts b/apps/nowait-user/src/types/search.ts new file mode 100644 index 00000000..a05151a8 --- /dev/null +++ b/apps/nowait-user/src/types/search.ts @@ -0,0 +1,18 @@ +export interface SearchStore { + storeId: number; + departmentId: number; + departmentName: string; + name: string; + location: string; + description: string; + profileImage: string | null; + bannerImages: any[]; + isActive: boolean; + deleted: boolean; + createdAt: string; +} + +export interface SearchResponse { + success: boolean; + response: SearchStore[]; +} diff --git a/apps/nowait-user/src/utils/UserApi.tsx b/apps/nowait-user/src/utils/UserApi.tsx index 9e955f3e..cbef028a 100644 --- a/apps/nowait-user/src/utils/UserApi.tsx +++ b/apps/nowait-user/src/utils/UserApi.tsx @@ -63,6 +63,7 @@ const refreshToken = async (): Promise => { } localStorage.removeItem("accessToken"); + localStorage.removeItem("recentSearches"); return null; } }; @@ -89,14 +90,36 @@ UserApi.interceptors.response.use( async (error) => { const originalRequest = error.config; - // 토큰 만료 에러 체크 (401 상태 코드 또는 "access token expired" 메시지) - if ( - error.response?.status === 401 || - (error.response?.data && - (error.response.data.includes?.("access token expired") || - error.response.data.message?.includes?.("access token expired") || - error.response.data === "access token expired")) - ) { + // 토큰 갱신 조건: access token 만료이지만 refresh token은 정상일 때만 + const isAccessTokenExpired = + error.response?.data && + (error.response.data.includes?.("expired access token") || + error.response.data.message?.includes?.("expired access token") || + error.response.data === "expired access token"); + + // refresh token 문제가 있는 경우 토큰 갱신하지 않음 + const isRefreshTokenInvalid = + error.response?.data && + (error.response.data.includes?.("expired refresh token") || + error.response.data.message?.includes?.("expired refresh token") || + error.response.data.includes?.("invalid refresh token") || + error.response.data.message?.includes?.("invalid refresh token") || + error.response.data.includes?.("Invalid or expired refresh token") || + error.response.data.message?.includes?.( + "Invalid or expired refresh token" + )); + + // refresh token에 문제가 있으면 바로 로그인 페이지로 이동 + if (isRefreshTokenInvalid) { + console.log("Refresh token 문제 감지 - 재로그인 필요"); + localStorage.removeItem("accessToken"); + localStorage.removeItem("recentSearches"); + window.location.href = "/login"; + return Promise.reject(error); + } + + // access token만 만료된 경우에만 토큰 갱신 시도 + if (isAccessTokenExpired) { // 이미 재시도한 요청이면 더 이상 재시도하지 않음 if (originalRequest._retry) { console.log("토큰 갱신 재시도 실패, 로그인 페이지로 이동"); @@ -123,6 +146,7 @@ UserApi.interceptors.response.use( } catch (refreshError) { console.log("토큰 갱신 중 오류 발생:", refreshError); localStorage.removeItem("accessToken"); + localStorage.removeItem("recentSearches"); window.location.href = "/login"; return Promise.reject(error); }