{값 && <></>}> true && false
false
> false && "kim"
false
> true && "kim"
'kim'
> null && "kim"
null
> 0 && "kim"
0
> undefined && "kim"
undefined
> "" && "kim"
''
update 메소드 생성multipart/form-data 요청이기 때문에 @RequestBody 어노테이션을 붙이지 않는다 (파일 업로드 처리)@PatchMapping("/user")
public ResponseEntity<Void> update(UserDto dto){
// userName 은 전송이 안되기 때문에 spring security 로 부터 얻어내기
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
dto.setUserName(userName);
userService.updateUser(dto);
return ResponseEntity.noContent().build();
}
➜ UserDto 에서 필드명과 input 속성의 name 요소가 같아야 한다
private MultipartFile profileFile;
<input type="file" name="profileFile"/>
handleSubmit 에 alert & navigate 코드 추가const navigate = useNavigate();
alert("가입정보 수정 완료");
navigate("/user")
# 업로드 파일의 최대 크기
spring.servlet.multipart.max-file-size=50MB
# 업로드 요청의 최대 크기 (파일의 크기 + 폼 전송되는 문자열)
spring.servlet.multipart.max-request-size=60MB
<Nav.Link as={NavLink} to="/board">Board</Nav.Link>
spring08 의 BoardDto 와 BoardListResponse , CommentDto, BoardMapper, BoardDaom, BoardDaoImpl, CommentDao, CommentDaoImpl, BoardService, BoardServiceImpl, CommentService, CommentServiceImpl 복사해서 Spring10 에 붙여넣기
@RequestMapping("/v1")
@RestController
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
private final CommentService commentService;
}
list 메소드 생성@GetMapping("/board")
public BoardListResponse list(@RequestParam(defaultValue = "1") int pageNum, BoardDto dto) {
return boardService.getBoardList(pageNum, dto);
}
➜ public BoardListResponse : json 문자열로 변환 되어서 응답한다
{
"list" : [{}, {}, {}, ...],
"startPageNum":1,
"endPageNum":5,
...
}
securityFilterChain 메소드에 config 안에 코드 추가.requestMatchers(HttpMethod.GET, "/v1/board").permitAll()
createAt 의 데이터 타입을 LocalDateTime 으로 변경 & @JsonFormat 어노테이션 추가@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd 일 HH:mm")
private LocalDateTime createdAt;
function Board() {
return <>
</>
}
Board 라우트 경로 추가{path:"/board", element:<Board/>}
useState 코드 추가const [pageInfo, setPageInfo] = useState({
list:[]
});
useEffect 코드 추가useEffect(() => {
api.get("/v1/board")
.then(res => {
setPageInfo(res.data)
})
.catch(err => {
console.log(err);
});
}, []);
<table className="table table-bordered">
<thead>
<tr>
<th>글번호</th>
<th>작성자</th>
<th>제목</th>
<th>조회수</th>
<th>작성일</th>
</tr>
</thead>
<tbody>
{pageInfo.list.map(item =>
<tr key={item.num}>
<td>{item.num}</td>
<td>{item.writer}</td>
<td>{item.title}</td>
<td>{item.viewCount}</td>
<td>{item.createdAt}</td>
</tr>
)}
</tbody>
</table>
npm install @toast-ui/editor
value , height , onChange 라는 props 를 전달할 수 있다value : 초기값height : 높이 (기번 400px)onChange : change 이벤트가 일어났을 때 실행할 함수<ToastEditor value={초기값} height="500px" onChange={() => {}}/>import '@toast-ui/editor/dist/toastui-editor.css';
import Editor from '@toast-ui/editor';
import { useEffect, useRef } from 'react';
export default function ToastEditor({ value = '', height='400px', onChange }) {
const elRef = useRef(null);
const instRef = useRef(null);
useEffect(() => {
if (!elRef.current) return;
instRef.current = new Editor({
el: elRef.current,
height:height,
initialEditType: 'wysiwyg',
previewStyle: 'vertical',
initialValue: value,
events: {
onChange?.(instRef.current.getHTML()),
},
});
return () => instRef.current?.destroy();
}, []);
return <div ref={elRef} />;
}
➜ onChange?.(instRef.current.getHTML()), 해당 코드를 한 줄 코딩한다면?
// 만일 props 로 전달된 함수가 있으면
if(onChange){
// 해당 함수 호출하면서 현재까지 작성한 내용을 얻어와서 전달한다
onChange(instRef.current.getHTML());
}

function BoardForm() {
return (
<div>
</div>
);
}
BoardForm 라우트 경로 추가{path:"/board/new", element:<ProtectedRoute><BoardForm/></ProtectedRoute>}
<NavLink to="/board/new">새글 작성</NavLink>
ToastUI 작성<h1>게시글 작성 양식</h1>
<form action="/v1/board" method="post">
<div className="mb-2">
<label className="form-label" htmlFor="title">제목</label>
<input className="form-control" type="text" name="title" id="title"/>
</div>
<div className="mb-2">
<label className="form-label" htmlFor="editor">내용</label>
<ToastEditor/>
</div>
<button className="btn btn-success btn-sm" type="submit">저장</button>
</form>
useState 추가const [state, setState] = useState({
title:"",
content:""
});
handleTitleChange 함수 추가const handleTitleChange = (e) => {
// 함수형 setState 를 이용해서 상태값을 변경
setState(prev => ({
...prev,
title:e.target.value
}));
};
handleContentChange 함수 추가const handleContentChange = (content) => {
setState(prev => ({
...prev,
content
}));
};
➜ 함수형 setState : 무조건 동작
객체형 setState : 상황에 따라 동작을 하거나 동작하지 않는다setState({
...state,
title:e.target.value
});
⚠️ 만일 객체형 setState 를 사용한다면?
let name = "kim";
function printName(){
console.log(name);
}
useFunc(printName);
function test(){
name = "park";
}
function useFunc(f){
f();
}
handleSubmit 함수 추가const handleSubmit = async (e) => {
e.preventDefault();
try{
await api.post("/v1/board", state)
} catch(err){
console.log(err);
}
};
onSubmit & input 에 onChange & ToastEditor 에 onChange 이벤트 추가<form onSubmit={handleSubmit} action="/v1/board" method="post">
<input onChange={handleTitleChange} className="form-control" type="text" name="title" id="title"/>
<ToastEditor onChange={handleContentChange}/>
useNavigate 함수 추가const navigate = useNavigate();
handleSubmit 함수 안에 try-catch 문 & async, await 코드 추가const handleSubmit = async (e) => {
e.preventDefault();
try{
const res = await api.post("/v1/board", state);
alert("글 저장 완료");
// 글 자세히 보기로 이동
navigate(`/board/${res.num}`);
} catch(err){
console.log(err);
}
};
save 메소드 생성@PostMapping("/board")
public BoardDto save(@RequestBody BoardDto dto) {
// 글 작성자
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
dto.setWriter(userName);
// 서비스를 이용해서 저장
boardService.createContent(dto);
return dto;
}
range 함수 추가function range(start, end) {
const result = [];
for (let i = start; i <= end; i++) {
result.push(i);
}
return result;
}
pageArray 코드 추가const pageArray = range(pageInfo.startPageNum, pageInfo.endPageNum);
useNavigate 함수 추가const navigate = useNavigate();
useSearchParams 함수 추가const [params] = useSearchParams();
pageMove 함수 추가const pageMove = (num) => {
// 현재 URLSearchParams 를 복사하고
const qs = new URLSearchParams(params);
// pageNum 만 교체 한다
qs.set("pageNum", num);
navigate(`/board?${qs.toString()}`);
};
Pagination 코드 추가<Pagination>
<Pagination.Item
onClick={() => pageMove(pageInfo.startPageNum - 1)}
disabled={pageInfo.startPageNum === 1}
>
Prev
</Pagination.Item>
{
pageArray.map(num =>
<Pagination.Item
onClick={() => pageMove(num)}
active={pageInfo.pageNum === num}
>
{num}
</Pagination.Item>
)
}
<Pagination.Item
onClick={() => pageMove(pageInfo.endPageNum + 1)}
disabled={pageInfo.endPageNum === pageInfo.totalPageCount}
>
Next
</Pagination.Item>
</Pagination>
useEffect 함수 안에 추가// params 정보를 읽어온다
const pageNum = params.get("pageNum"); // null 일 수 있다
const search = params.get("search"); // null 일 수 있다
const keyword = params.get("keyword"); // null 일 수 있다
// api 서버에 요청할 query 문자열 구성
const qs = new URLSearchParams(); // 객체 생성 후 하나씩 setting
if (pageNum) {
qs.set("pageNum", pageNum);
}
if (keyword) {
qs.set("search", search);
qs.set("keyword", keyword);
}
console.log(qs.toString());
api.get(`/v1/board?${qs.toString()}`)
.then(res => {
setPageInfo(res.data)
})
.catch(err => {
console.log(err);
})
}, [params]);

<div className="row my-3">
<div className="col-md-6 ms-auto">
<div className="input-group">
<select value="" name="search" className="form-select">
<option value="title_content">제목+내용</option>
<option value="title">제목</option>
<option value="writer">작성자</option>
</select>
<input value="" type="text" name="keyword" className="form-control" placeholder="검색어 입력..." />
<button type="submit" className="btn btn-outline-secondary">
<i className="bi bi-search"></i>
<span className="visually-hidden">검색</span>
</button>
<button className="btn btn-outline-danger">
<i className="bi bi-arrow-clockwise"></i>
<span className="visually-hidden">새로고침</span>
</button>
</div>
</div>
</div>
<p>
<strong></strong> 에 대한 검색 결과
<strong></strong> 개
</p>
useState 함수 추가// 검색 조건과 검색 키워드를 상태값으로 관리
const [search, setSearch] = useState({
search:"title_content", // 제목 + 내용 검색이 초기값
keyword:""
});
handleSearchChange 함수 추가const handleSearchChange = (e) =>{
setSearch({
...search,
[e.target.name]:e.target.value
});
};
handleSearchClick 함수 추가const handleSearchClick = () => {
// search 상태값 object 에 저장된 내용을 query 문자열로 변경한다
// {search:"name_addr" , keyword:"kim"} => search=title_content&keyword=kim
const query = new URLSearchParams(search).toString();
// "/board?search=title_content&keyword=kim" 이런 형식으로 주소창이 변경된다
navigate(`/board?${query}`);
};
onChange 이벤트 & value 속성 추가<select onChange={handleSearchChange} value={search.keyword} name="search" className="form-select">
onChange 이벤트 & value 속성 추가<input onChange={handleSearchChange} value={search.keyword} type="text" name="keyword" className="form-control" placeholder="검색어 입력..."/>
onClick 이벤트 추가<button onClick={handleSearchClick} className="btn btn-outline-secondary">
handleRefreshClick 함수 추가const handleRefreshClick = () => {
// 검색 상태값을 초기 상태로 변경하고
setSearch({
search:"title_content",
keyword:""
});
// 1 page 로 이동
navigate("/board");
};
onClick 이벤트 추가<button onClick={handleRefreshClick} className="btn btn-outline-danger">
{pageInfo.keyword && <p className="alert alert-success">
<strong>{pageInfo.keyword} </strong> 에 대한 검색 결과
<strong>{pageInfo.totalRow}</strong> 개
</p>}