헬프
뮤테이션은 버튼 누르면 작동하는 게 아니고,
get 요청 말고 데이터를 수정하는 create, update, delete 할 때 쓰이는 것임.
검색은 서버에 있는 데이터가 수정되는 게 아니니까 뮤테이션을 쓰면 안 되고 쿼리스트링에 있는 검색어를 캐시키 배열에 추가해야 함.
올바른 URL 설계 : 1) Query string과 Path Variable 이해하기
board/qna
board/qna?page=1
<List>
<Item>
<SLink
to={{
pathname: '/board/qna',
state: { type: 'qna', sort: 'id' },
search: '?page=1',
}}
>
<Icon>
<AiOutlineQuestion />
</Icon>
<Text>Q&A</Text>
</SLink>
</Item>
<Item>
select에 value를 적지 않아서 동기화가 되지 않음. input의 값을 바꿨을 때는 상태가 바뀌었지만 상태를 바꿨을 때는 input의 value가 바뀌지 않았음.
<SearchSelect
value={searchModeInput}
onChange={(e) => setSearchModeInput(e.target.value)}
>
<option value="title" defaultValue>
제목
</option>
<option value="content">내용</option>
<option value="all">제목+내용</option>
</SearchSelect>
useQuery에 조건문을 사용할 수 있었다..! 유즈쿼리 안이 복잡해지지 않게 하려면 함수를 빼는 것도 좋음.
const { isLoading, data } = useQuery( [boardType + 'Board', curPage, sort, searchInfo], () =>{
if (searchInfo === ''){
return boardApi.getPosts(curPage, sort, boardType).then((res) => res.data)
} else {
return boardApi .searchPosts(keyword, curPage, searchMode, boardType) .then((res) => res.data),
}
});
돔에서 가져온 innerText는 문자열이다~
useState,,, 상태 렌더링
어제 코드
import React, { useEffect, useState } from 'react';
import { Link, useLocation, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { AiOutlineSearch } from 'react-icons/ai';
import { boardApi } from '../api';
import { useUser } from '../context';
import { useQuery } from 'react-query';
import Post from '../components/Post';
import PageList from '../components/PageList';
const Board = () => {
const user = useUser();
const location = useLocation();
const boardType = location.pathname.split('/')[2] || location.state?.type;
const boardTitle =
(boardType === 'qna' && 'Q&A') ||
(boardType === 'tech' && 'Tech') ||
(boardType === 'free' && '자유게시판');
// const sort = location.pathname.split('=')[1] || location.state?.sort || null;
const history = useHistory();
const [sort, setSort] = useState('createdDate');
const [curPage, setCurPage] = useState(1);
const [pageList, setPageList] = useState([1, 2, 3, 4, 5]);
const [searchModeInput, setSearchModeInput] = useState('title'); // searchModeInput
const [keywordInput, setKeywordInput] = useState('');
const [searchInfo, setSearchInfo] = useState({
keyword: '',
searchMode: '',
});
const { keyword, searchMode } = searchInfo;
// 검색 결과 이후에 cancle 버튼을 누르거나, 최신순 조회순 등 탭, 다른 boardType을 클릭해도
// http://localhost:3000/board/qna?searchMode=undefined&keyword=undefined
// url이 저렇게 뜸. useEffect를 어떻게 섞어쓸 수 있을까
const { isLoading, data } = useQuery(
[`${boardType}Board`, curPage, sort, searchInfo],
() => {
if (!keyword || !searchMode) {
return boardApi
.getPosts(curPage, sort, boardType)
.then((res) => res.data);
} else {
return boardApi
.searchPosts(keyword, curPage, searchMode, boardType)
.then((res) => res.data);
}
}
);
console.log('Board data', data, curPage, sort);
const handleSearch = (e) => {
e.preventDefault();
console.log('handleSearch');
if (keywordInput.trim().length < 2) {
alert('검색어는 2글자 이상 입력해주세요.');
return;
}
setSearchInfo({
...searchInfo,
keyword: keywordInput,
searchMode: searchModeInput,
});
};
useEffect(() => {
if (!keyword || !searchMode) return;
const searchParams = new URLSearchParams();
searchParams.set('searchMode', searchMode);
searchParams.set('keyword', keyword);
console.log('searchParams', searchParams.toString());
setSort('createdDate');
history.push(`/board/${boardType}?` + searchParams.toString());
}, [boardType, history, keyword, searchMode]);
// 1. initSearchInfo Constant 만들기
// 2. 생각이 기계적이다.
const handleOrderListClick = (e) => {
// 자의적으로 변하는 화면, 이벤트, ui에 의존하는 코드는 좋지 않음.
// 돔에 직접 접근하는 것은 좋지 않음! 바뀔 수 있음
// 기획자와 카피라이터
const sort = e.target.dataset.name;
// 함수로 쪼개기
// 중복이 있어서 쪼개는 게 아니라 -> 비슷한 역할을 가진 것들을 묶기
// 서로 다른 로직이 섞이지 않게
// Board를 만드는데 if문이 3개임 .필요한 로직중에 하나지만
// inntertText에 따라서 sort를 뽑아주는 로직.
// 언어에따라
setCurPage(1);
setSort(sort);
setSearchInfo({
keyword: '',
searchMode: '',
});
history.push(`/board/${boardType}`);
};
const handleCancelSearch = (e) => {
e.preventDefault();
setKeywordInput('');
setSearchInfo({
keyword: '',
searchMode: '',
});
history.push(`/board/${boardType}`);
// http://localhost:3000/board/qna?searchMode=undefined&keyword=undefined
};
return (
<Container>
<Header>
<Title>{boardTitle}</Title>
{user && (
<Button>
<Link
className="link"
to={{ pathname: '/write', state: { type: boardType } }}
>
새 글 쓰기
</Link>
</Button>
)}
</Header>
<FilterContainer>
<OrderList onClick={handleOrderListClick}>
{[
['최신순', 'createdDate'],
['조회순', 'views'],
['댓글순', 'commentSize'],
['좋아요순', 'likes'],
].map(([text, name]) => (
<OrderItem key={name} data-name={name} active={sort === name}>
{text}
</OrderItem>
))}
</OrderList>
<SearchForm onSubmit={handleSearch}>
<SearchSelect onChange={(e) => setSearchModeInput(e.target.value)}>
<option value="title" defaultValue>
제목
</option>
<option value="content">내용</option>
<option value="all">제목+내용</option>
</SearchSelect>
<SearchInput
value={keywordInput}
onChange={(e) => setKeywordInput(e.target.value)}
minLength="2"
maxLength="20"
/>
{/* 폼안에 버튼 만들 때 조심하기 */}
<button type="button" className="dbtn" onClick={handleCancelSearch}>
x
</button>
<SearchButton type="submit">
<AiOutlineSearch className="sbtn" />
</SearchButton>
</SearchForm>
</FilterContainer>
{!isLoading ? (
<>
{data?.contents?.map((post) => (
<Post
key={post.id}
post={post}
type={boardType}
fci={data?.contents[0].id}
/>
))}
</>
) : (
<div>loading..</div>
)}
</Container>
);
};
export default Board;
// 오늘코드
import React, { useEffect, useState } from 'react';
import { Link, useLocation, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { AiOutlineSearch } from 'react-icons/ai';
import { MdCancel } from 'react-icons/md';
import { boardApi } from '../api';
import { useUser } from '../context';
import { useQuery } from 'react-query';
import Post from '../components/Post';
import PageList from '../components/PageList';
const Board = () => {
const user = useUser();
const location = useLocation();
const boardType = location.pathname.split('/')[2] || location.state?.type;
const boardTitle =
(boardType === 'qna' && 'Q&A') ||
(boardType === 'tech' && 'Tech') ||
(boardType === 'free' && '자유게시판');
const history = useHistory();
const [sort, setSort] = useState('createdDate');
const search = new URLSearchParams(location.search);
const curPage = Number(search.get('page')) || 1;
const [pageList, setPageList] = useState([1, 2, 3, 4, 5]);
const [searchModeInput, setSearchModeInput] = useState('title'); // searchModeInput
const [keywordInput, setKeywordInput] = useState('');
const initSearchInfo = {
searchMode: 'title',
keyword: '',
};
const [searchInfo, setSearchInfo] = useState(initSearchInfo);
const { keyword, searchMode } = searchInfo;
// 얘가 setCurrentPage
const fetchBoards = (page = 1) => {
// 현재 url의 searchParams
const searchParams = new URLSearchParams(location.search);
searchParams.set('page', page);
console.log('searchParams', searchParams.toString());
history.push(`/board/${boardType}?` + searchParams.toString());
};
// 상태랑 아무 상관도 없음.
const { isLoading, data } = useQuery(
[`${boardType}Board`, curPage, sort, searchInfo],
async () => {
if (!keyword) {
return boardApi
.getPosts(curPage, sort, boardType)
.then((res) => res.data);
} else {
return boardApi
.searchPosts(keyword, curPage, searchMode, boardType)
.then((res) => res.data);
}
}
);
const handleSearch = (e) => {
e.preventDefault();
fetchBoards(1);
if (keywordInput.trim().length < 2) {
alert('검색어는 2글자 이상 입력해주세요.');
return;
}
setSearchInfo({
searchMode: searchModeInput,
keyword: keywordInput,
});
};
useEffect(() => {
if (!keyword || !searchMode) return;
const searchParams = new URLSearchParams(location.search);
searchParams.set('searchMode', searchMode);
searchParams.set('keyword', keyword);
console.log('searchParams', searchParams.toString());
setSort('createdDate');
history.push(`/board/${boardType}?` + searchParams.toString());
}, [boardType, history, keyword, searchMode]);
const handleOrderListClick = (e) => {
fetchBoards(1);
const sort = e.target.dataset.name;
setSort(sort);
setSearchModeInput('title'); // 작동x
setKeywordInput('');
setSearchInfo(initSearchInfo);
history.push(`/board/${boardType}`);
};
const handleCancelSearch = () => {
fetchBoards(1);
setSearchModeInput('title'); // 작동x
setKeywordInput('');
setSearchInfo(initSearchInfo);
history.push(`/board/${boardType}`);
};
return (
<Container>
<Header>
<Title>{boardTitle}</Title>
{user && (
<Button>
<Link
className="link"
to={{ pathname: '/write', state: { type: boardType } }}
>
새 글 쓰기
</Link>
</Button>
)}
</Header>
<FilterContainer>
<OrderList onClick={handleOrderListClick}>
{[
['최신순', 'createdDate'],
['조회순', 'views'],
['댓글순', 'commentSize'],
['좋아요순', 'likes'],
].map(([text, name]) => (
<OrderItem key={name} data-name={name} active={sort === name}>
{text}
</OrderItem>
))}
</OrderList>
<SearchForm onSubmit={handleSearch}>
<SearchSelect
value={searchModeInput}
onChange={(e) => setSearchModeInput(e.target.value)}
>
<option value="title" defaultValue>
제목
</option>
<option value="content">내용</option>
<option value="all">제목+내용</option>
</SearchSelect>
<SearchInput
value={keywordInput}
onChange={(e) => setKeywordInput(e.target.value)}
minLength="2"
maxLength="20"
/>
<SearchButton type="submit">
<AiOutlineSearch className="sbtn" />
</SearchButton>
{/* 폼안에 버튼 만들 때 조심하기 */}
{keyword !== '' && (
<CancelButton type="button" onClick={handleCancelSearch}>
<div className="cbtn">
<MdCancel /> clear
</div>
</CancelButton>
)}
</SearchForm>
</FilterContainer>
{!isLoading ? (
<>
{data?.contents?.map((post) => (
<Post
key={post.id}
post={post}
type={boardType}
fci={data?.contents[0].id}
/>
))}
{data?.totalElements > 0 ? (
<PageList
page={pageList}
setPage={setPageList}
fetchContents={fetchBoards}
totalPages={data?.totalPages}
// typeof curPage - numbers
currentPage={data?.currentPage}
sort={sort}
/>
) : (
<div style={{ marginTop: 150, textAlign: 'center' }}>
아직 게시물이 없습니다.
</div>
)}
</>
) : (
<div>loading..</div>
)}
</Container>
);
};
export default Board;