diff --git a/package-lock.json b/package-lock.json index b582139..a4f4cb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "pretendard": "^1.3.9", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", - "react-daum-postcode": "^3.2.0", "react-datepicker": "^8.0.0", "react-daum-postcode": "^3.2.0", "react-dom": "^18.3.1", @@ -6799,14 +6798,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-daum-postcode": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-daum-postcode/-/react-daum-postcode-3.2.0.tgz", - "integrity": "sha512-NHY8TUicZXMqykbKYT8kUo2PEU7xu1DFsdRmyWJrLEUY93Xhd3rEdoJ7vFqrvs+Grl9wIm9Byxh3bI+eZxepMQ==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/react-datepicker": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.0.0.tgz", @@ -8123,7 +8114,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", - "license": "MIT", "engines": { "node": ">=12.20.0" }, diff --git a/package.json b/package.json index e363d50..72b1348 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,16 @@ "@iconify/react": "^5.2.0", "@mui/icons-material": "^6.4.1", "@mui/material": "^6.4.1", - "@types/react-slick": "^0.23.13", "@tanstack/react-query": "^5.66.0", + "@types/react-slick": "^0.23.13", "axios": "^1.7.9", "chart.js": "^4.4.7", "lucide-react": "^0.474.0", "pretendard": "^1.3.9", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", - "react-daum-postcode": "^3.2.0", "react-datepicker": "^8.0.0", + "react-daum-postcode": "^3.2.0", "react-dom": "^18.3.1", "react-router-dom": "^7.1.1", "react-slick": "^0.30.3", diff --git a/src/components/HeaderIconMenu.tsx b/src/components/HeaderIconMenu.tsx deleted file mode 100644 index 1054106..0000000 --- a/src/components/HeaderIconMenu.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import styled from "styled-components"; -import media from "../styles/media"; - -const HeaderIconMenu = () => { - return ( - - - - - - - - ); -}; - -export default HeaderIconMenu; - -const Wrapper = styled.div` - display: flex; - // column-gap: 1rem; - justify-content: space-around; -` - -const IconMenu = styled.div` - background-color: #F4F4F4; - border-radius: 100%; - box-sizing: border-box; - ${media.medium` - width: 84.302px; - height: 86px; - margin: 39px 0 55px 0; - `} - ${media.small` - width: 58.815px; - height: 60px; - margin: 15px 0 22px 0; - `} -` \ No newline at end of file diff --git a/src/components/ResponsiveHeader.tsx b/src/components/ResponsiveHeader.tsx index 2843095..c826435 100644 --- a/src/components/ResponsiveHeader.tsx +++ b/src/components/ResponsiveHeader.tsx @@ -12,17 +12,17 @@ import icMyPage from '../assets/header/icon-mypage.svg'; import icUpload from '../assets/header/icon-upload.svg'; import imgTicket from '../assets/ticket.svg'; import { useNavigate } from "react-router-dom"; -import { ChangeEvent, useEffect, useRef, useState } from "react"; +import { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from "react"; import CategoryMenu from './CategoryMenu'; import { useModalContext } from './Modal/context/ModalContext'; import SplashModal from '../pages/login/components/SplashModal'; import imgVector from '../assets/Vector.png'; import { ReactComponent as IcList } from '../assets/icList.svg'; import icDel from '../assets/icDel.svg'; - -const recentKeywords = ['애플워치','애플워치','애플워치','애플워치', - '애플워치','애플워치','애플워치','애플워치','애플워치','애플워치', -]; +import axiosInstance from '../apis/axiosInstance'; +import { TSearch } from '../types/searchKeywords'; +import { useAuth } from '../context/AuthContext'; +import { useIsSearchCompleted } from '../store/store'; const ResponsiveHeader = () => { const navigate = useNavigate(); @@ -33,6 +33,23 @@ const ResponsiveHeader = () => { const searchRef = useRef(null); const [searchText, setSearchText] = useState(''); const categoryRef = useRef(null); + const [hotKeywords, setHotKeywords] = useState([]); + const [recentKeywords, setRecentKeywords] = useState([]); + const { isAuthenticated, logout } = useAuth(); + const isSearchCompleted = useIsSearchCompleted(v=>v.isSearchCompleted); + + const getSearch = async () => { + const { data }:{data:TSearch} = await axiosInstance.get( + isAuthenticated ? '/api/member/search' + : '/api/permit/search' + ); + + console.log('recentSearch:', data.result.recentSearch); + setHotKeywords(data.result.popularSearch); + setRecentKeywords(data.result.recentSearch); + }; + const delSearch = async (keyword:string) => + await axiosInstance.delete(`/api/member/search?keyword=${keyword}`); const handleCategoryOut = (e:MouseEvent) => { const currentCategoryRef = categoryRef.current; @@ -52,15 +69,36 @@ const ResponsiveHeader = () => { const handleSearchInput = (e: ChangeEvent) => { setSearchText(e.target.value); }; + const handleSearchEnter = (e:KeyboardEvent) => { + if (e.key === "Enter") { + navigate(`/search/${searchText}`); + setIsSearchClicked(false); + }; + }; + + const handleDelKeyword = (keyword:string) => { + // delSearch(): 해당 키워드 서버에서 삭제 + delSearch(keyword).then(_=>getSearch()); + }; const handleOpenModal = () => { openModal(({ onClose }) => ); }; - const onClickLoginBtn = () => { - setIsLoggedIn((prev) => !prev); // 클릭할 때마다 로그인/로그아웃 상태 변경 + if (!isAuthenticated) handleOpenModal(); + else logout(); }; + // 시작하자마자 호출될 API + useEffect(() => { + getSearch(); + }, [isAuthenticated]); + + // 검색할 때마다 최신 검색어 갱신 + useEffect(() => { + getSearch(); + }, [isSearchCompleted]); + useEffect(() => { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleCategoryOut); @@ -74,8 +112,8 @@ const ResponsiveHeader = () => { <> - - {isLoggedIn ? '로그아웃' : '로그인'} + + {isAuthenticated ? '로그아웃' : '로그인'} @@ -104,37 +142,46 @@ const ResponsiveHeader = () => { setIsSearchClicked(true)} value={searchText} onChange={handleSearchInput} + onKeyUp={handleSearchEnter} /> - + {isLoggedIn===false + ? 최근 검색 - {recentKeywords.map((v,_) => ( + {recentKeywords.length!==0 ? + recentKeywords.map((v,_) => ( {v} - + handleDelKeyword(v)} + /> - ))} + )) + : 최근 검색 내역이 없습니다. + } + : <> + } 현재 인기있는 검색어 - {recentKeywords.map((v,_) => ( + {hotKeywords.map((v,_) => ( {v} @@ -172,7 +219,7 @@ export default ResponsiveHeader; const Wrapper = styled.div` display: flex; flex-direction: column; - max-width: 1084px; + width: 1084px; height: 188px; box-sizing: border-box; z-index: 100; @@ -317,15 +364,16 @@ const SearchIcon = styled.img` `; const KeywordContainer = styled.div<{$show:string}>` - // width: 560px; - width: 100%; - height: 386px; + width: 560px; + // width: 100%; + // height: 386px; border-radius: 18px; border: 1px solid #E4E4E4; background-color: #FFF; position: absolute; - left: 0; + left: 50%; top: 120%; + transform: translateX(-50%); padding: 38px 43px 5px 43px; box-sizing: border-box; display: ${props => props.$show==='true' @@ -384,6 +432,14 @@ const RecentKeyword = styled.div` cursor: default; } ` +const KeywordSpan = styled.span` + color: #8F8E94; + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 36.832px; /* 306.932% */ +` const DelImg = styled.img` &:hover { cursor: pointer; diff --git a/src/layout/RootLayout.tsx b/src/layout/RootLayout.tsx index b839a81..75096bb 100644 --- a/src/layout/RootLayout.tsx +++ b/src/layout/RootLayout.tsx @@ -1,14 +1,11 @@ import styled from 'styled-components'; -import Header from '../components/Header'; import { Outlet } from 'react-router-dom'; -import HeaderIconMenu from '../components/HeaderIconMenu'; import ResponsiveHeader from '../components/ResponsiveHeader'; const RootLayout = () => { return ( - {/* */} ); diff --git a/src/pages/homepage/homePage.tsx b/src/pages/homepage/homePage.tsx index ef8b2ce..9654530 100644 --- a/src/pages/homepage/homePage.tsx +++ b/src/pages/homepage/homePage.tsx @@ -41,7 +41,7 @@ const HomePage: React.FC = () => { } }; - fetchHomeData(); + // fetchHomeData(); }, []); if (!homeData) return
Loading...
; diff --git a/src/pages/raffleList/SearchResultPage.tsx b/src/pages/raffleList/SearchResultPage.tsx index 4c97f09..621c084 100644 --- a/src/pages/raffleList/SearchResultPage.tsx +++ b/src/pages/raffleList/SearchResultPage.tsx @@ -1,9 +1,11 @@ import { useState, useEffect, useRef } from 'react'; -import { useParams, useLocation } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import styled from 'styled-components'; import ProductCard from '../../components/ProductCard'; import RaffleProps from '../../components/RaffleProps'; import axiosInstance from '../../apis/axiosInstance'; +import { useAuth } from '../../context/AuthContext'; +import { useIsSearchCompleted } from '../../store/store'; const SearchResultPage: React.FC = () => { const { type } = useParams<{ type?: string }>(); @@ -12,15 +14,21 @@ const SearchResultPage: React.FC = () => { const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [isLoading, setIsLoading] = useState(false); + const { isAuthenticated } = useAuth(); + const setIsCompleted = useIsSearchCompleted(v=>v.setIsSearchCompleted); const fetchMoreProducts = async () => { if (!hasMore || isLoading) return; setIsLoading(true); try { - const { data } = await axiosInstance.get('/api/permit/search/raffles', { + const apirequest = isAuthenticated ? '/api/member/search/raffles' + : '/api/permit/search/raffles' + + const { data } = await axiosInstance.get(apirequest, { params: { keyword: type }, }); + setIsCompleted(true); // Zustand 상태 업데이트 const startIndex = (page - 1) * 16; const endIndex = startIndex + 16; @@ -40,7 +48,8 @@ const SearchResultPage: React.FC = () => { setPage((prev) => prev + 1); } catch (error) { console.error('데이터 불러오기 실패:', error); - } finally { + } + finally { setIsLoading(false); } }; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..51dd32e --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface IIsSearchCompleted { + isSearchCompleted: boolean; + setIsSearchCompleted: (v:boolean) => void; +} + +export const useIsSearchCompleted = create((set) => ({ + isSearchCompleted: false, + setIsSearchCompleted: (v) => set({ isSearchCompleted: v }), +})); \ No newline at end of file diff --git a/src/types/searchKeywords.ts b/src/types/searchKeywords.ts new file mode 100644 index 0000000..aeb3f30 --- /dev/null +++ b/src/types/searchKeywords.ts @@ -0,0 +1,9 @@ +export type TSearch = { + isSuccess: boolean, + code: string, + message: string, + result: { + recentSearch: string[], + popularSearch: string[] + } +}; \ No newline at end of file