검색 페이지 | 검색결과 페이지 |
---|---|
검색 페이지의 레이아웃은 위와 같이 짰다.
.
.
//검색바 컴포넌트
import SearchBox from './SearchComponents/SearchBox';
//최근검색어 컴포넌트
import KeywordsList from './SearchComponents/KeywordsList';
.
.
interface StateProps {
productName: string;
productId: number;
category: string;
}
const Search = () => {
// 최근 본 상품
const [recentProduct, setRecentProduct] = useState<StateProps | undefined>();
useEffect(() => {
const getSeverRecentProductData = async () => {
const json = await getRecentProduct();
setRecentProduct(json.slice(-1)[0]);
};
getSeverRecentProductData();
}, []);
return (
<Container>
<SearchBox />
{recentProduct && (
<RecentProducts>
<h4>최근 본 상품</h4>
<DetailLink to={`/detail/${recentProduct.category.toLowerCase()}/${recentProduct.productId}`}>
<div>
<span className="history">
<BiHistory />
</span>
{recentProduct.productName}
</div>
<div className="info">
상세정보
<span className="move">
<IoChevronForwardOutline />
</span>
</div>
</DetailLink>
</RecentProducts>
)}
<RecentKeywords>
<KeywordsList />
</RecentKeywords>
</Container>
);
};
검색바 - <SearchBox />
, 최근 본 상품
, 최근 검색어 - <KeywordsList />
세가지 컴포넌트가 순서대로 오게 된다.DetailLink
은 styled-components의 styled(Link)
로 Link를 받아, to={이동하려는 위치}
를 통해 상품 상세페이지로 이동할 수 있도록 하였다.const KeywordsList = () => {
const dispatch = useDispatch();
const [data, setData] = useState<Array<StateObject>>([]);
const [toast, setToast] = useState(false);
const [deletedCount, setDeletedCount] = useState('');
const location = useLocation();
const findResultsPage = location.pathname.slice(0, 8) === '/search/';
const isToggleTrue = useSelector<ReducerType>((state) => state.autosave.isToggleTrue);
//서버에서 키워드 목록 가져오기
useEffect(() => {
const getSeverSearchKeywordsData = async () => {
const json = await getSearchKeywords();
setData(json);
};
getSeverSearchKeywordsData();
}, []);
//키워드 단일 삭제
const handleDeleteKeyword = (id: number) => {
const deletedData = data.filter((element) => element.searchId !== id);
setData(deletedData);
deleteSearchKeywordsSingle(id);
};
//키워드 전체 삭제
const handleDeleteKeywordAll = async () => {
if (!confirm('최근 검색어를 모두 삭제하시겠습니까?')) return;
setData([]);
const res = await deleteSearchKeywordsAll();
// 00개 삭제 완료 토스트 띄우기
res.status === 'success' && setToast(true);
setDeletedCount(res.deletedNum);
};
//자동저장 상태를 store로 전달
const toggleAutosaveHandler = () => {
dispatch(toggle());
};
//자동저장
const handleAutoSave = () => {
confirm(
isToggleTrue ? '최근 검색어 저장 기능을\n사용 중지하시겠습니까?' : '최근 검색어 저장 기능을\n사용 하시겠습니까?'
) && toggleAutosaveHandler();
};
return (
//검색페에지와 검색결과페이지 스타일 다르게
<Container className={findResultsPage ? 'resultPage' : ''}>
<div>
<h4>최근에 찾아봤던</h4>
<button className="autoSave" onClick={handleAutoSave}>
자동저장 {isToggleTrue ? '끄기' : '켜기'}
</button>
</div>
{isToggleTrue ? (
data && data.length !== 0 ? (
<>
<ol>
{data
.sort((a, b) => {
return +new Date(b.createdAt) - +new Date(a.createdAt);
})
.map((list) => (
<List key={list.searchId}>
<SearchLink to={`/search/${list.searchContent}`}>{list.searchContent}</SearchLink>
<button onClick={() => handleDeleteKeyword(list.searchId)}>
<TfiClose />
</button>
</List>
))}
</ol>
<button className="deleteAll" onClick={handleDeleteKeywordAll}>
모두 지우기
</button>
</>
) : (
<Info>최근 찾아봤던 내역이 없습니다.</Info>
)
) : (
<Info>검색어 저장 기능이 꺼져있습니다.</Info>
)}
<Toast isTrue={toast} message={`${deletedCount}개가 삭제됐어요`} />
</Container>
);
};
최근검색어
useEffect
의 의존성 배열을 비워서, 컴포넌트가 처음으로 렌더링될 때 서버에서 검색 키워드 데이터를 가져와서 상태로 설정한다.sort
메서드를 사용해 최신순으로 정렬하고, 정렬한 값을 보여준다. link로 묶어 클릭시 바로 검색결과 페이지로 넘어가도록 한다.filter
메서드를 사용해 클릭한 것 이외의 검색어들만 남아있도록 처리한다. 서버로 삭제 api도 요청한다.자동저장
useDispatch
를 사용하여 dispatch
함수를 가져오고, dispatch
함수를 호출하여 toggle() 액션
을 디스패치한다. 액션 디스패치를 통해 redux의 상태를 업데이트 한다. redux의 자동저장 상태는 useSelector
를 통해 조회한다.자동저장 켜기
또는 자동저장 끄기
로 버튼을 보여줄 수 있다. 버튼을 눌렀을 때는 confirm 창을 띄워주고, 창에서 누른 버튼이 true일 경우에만 액션 디스패치로 redux의 상태를 원래 상태와 반대로 업데이트 한다.const SearchBox = () => {
const isToggleTrue = useSelector<ReducerType>((state) => state.autosave.isToggleTrue);
const location = useLocation();
const params = useParams();
const navigate = useNavigate();
const [inputValue, setInputValue] = useState('');
const findResultsPage = location.pathname.slice(0, 8) === '/search/';
//검색결과 페이지에서 검색바에 키워드 보여주기
useEffect(() => {
params.keywords !== undefined && setInputValue(params.keywords);
}, []);
//검색결과 페이지에서만 뒤로가기 버튼 보여줌. 검색페이지로 이동하는 버튼.
const handleBack = () => {
navigate('/search');
};
//검색 submit 되면, 페이지 이동 및 자동저장 여부에 따라 키워드 저장 api 호출
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const movepageAndAddkeywordAndCallApi = () => {
navigate(`/search/${inputValue}`);
isToggleTrue && addSearchKeywords(inputValue);
};
inputValue !== '' ? movepageAndAddkeywordAndCallApi() : alert('상품명을 입력해주세요.');
};
//키워드가 있을 때만, 키워드삭제 버튼 보여주려고 사용
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
console.log(event.target.value);
};
//키워드 삭제 버튼
const handleDeleteBtn = () => {
setInputValue('');
};
return (
<Container onSubmit={handleSubmit} className={findResultsPage ? 'results' : ''}>
{/* 검색결과에서만 뒤로가기 버튼 보이기 */}
{findResultsPage ? (
<IoChevronBackOutline size="22" color="#353D4A" onClick={handleBack} style={{ marginLeft: '-8px' }} />
) : null}
<div>
<span className="search">
<BiSearch />
</span>
<input type="text" placeholder="필요한 상품을 찾아보세요" value={inputValue} onChange={handleInputChange} />
{/* 검색바에 글자 있을 때만 삭제버튼 노출 */}
{inputValue !== '' ? (
<button type="button" className="delete" onClick={handleDeleteBtn}>
<TiDelete />
</button>
) : null}
</div>
</Container>
);
};
useLocation
을 사용해 pathname으로 조건을 설정하여, 검색결과 페이지면 뒤로가기 아이콘을 보여주고 아니라면 아이콘을 보여주지 않는다.onChange
이벤트를 걸어 값이 변화할 때마다 setState 하여 값의 상태를 관리한다. 이를 통해 값이 있으면 키워드 삭제 버튼을 보여주고, 값이 없으면 보여주지 않는다. 삭제 버튼을 클릭하면 input 의 value를 ''
으로 변경한다.useParams
를 사용해 라우터에서 :keyword
부분이 있으면, 검색바에 그 값을 출력하도록 했다. 검색결과 페이지의 라우터는 /search/:keyword
로 설정했으므로, 검색결과 페이지에서만 검색어가 보인다.