77일차 내용 정리

채공부·2025년 9월 10일

페이징 처리

  • Spring09JpaApplication 에 반복문을 이용한 샘플 데이터 100개 저장
for(int i=0; i<100; i++) {
	Member tmp = Member.builder().name("이름"+i).addr("주소"+i).build();
	em.persist(tmp);
}         
  • MemberPageResponse 클래스 생성
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class MemberPageResponse {
	// 회원 목록
	private List<MemberDto> list;
	private int startPageNum;
	private int endPageNum;
	private int totalPageCount;
	private int pageNum;
}
{
	"list":[{], {}, {}, ...]
	"startPageNum":1,
	"endPageNum":10,
	"totalPageNum" : 11,
	"pageNum" : 3
}
  • MemberService 인터페이스에 getPage 메소드 생성
public MemberPageResponse getPage(int pageNum);
  • MemberServiceImpl 에 상수값 추가 및 추상 메소드 오버라이드
// 한 페이지에 몇 개의 row 를 출력할 것인지에 대한 값
final int PAGE_ROW_COUNT = 10;
// 페이징 처리 UI 에 페이지 번호를 몇 개씩 출력할 지에 대한 값
final int PAGE_DISPLAY_COUNT = 5;
@Override
public MemberPageResponse getPage(int pageNum) {
	// num 에 대해서 내림차순 정렬하겠다는 Sort 객체
	Sort sort = Sort.by(Sort.Direction.DESC, "num");
		
	// pageNum 과 page row count 와 정렬 객체를 전달해서 원하는 PageRequest 를 만들어내고
    // 몇 페이지 내용이 필요한지, 페이지 사이즈, 정렬 방법
	PageRequest pageRequest = PageRequest.of(pageNum-1, PAGE_ROW_COUNT, sort);
		
	// drg.springframework.data.domain 패키지의 Page type 을 import 해야 한다
	Page<Member> page = memberRepo.findAll(pageRequest);
		
	// Page 객체를 stream 으로 만들어서 dto 의 list 를 얻어낸다
	List<MemberDto> list = page.stream().map(MemberDto :: toDto).toList();
		
	return MemberPageResponse.builder()
			.list(list)
			.pageNum(pageNum)
			.totalPageNum(page.getTotalPages())
			.build();
}
  • RestMemberController2 클래스 생성
@RequiredArgsConstructor
@RequestMapping("/v2")
@RestController
public class RestMemberController2 {
	
	private final MemberService memberService;
	
	@GetMapping("/members")
	public MemberPageResponse list(@RequestParam(defaultValue = "1") int pageNum) {
		// @RequestParam(defaultValue="1") int pageNum
		// 만일 pageNum 이 넘어오지 않는다면 기본값 1 을 설정한다
		
		return memberService.getPage(pageNum);
	}
}
  • Member.jsx 에 useState 기본값 설정
 const [state, setState] = useState({
	list:[],
	pageNum:0,
	startPageNum:0,
	endPageNum:0,
	totalPageCount:0
});
  • Member.jsx 에 fetch 경로 수정
useEffect(()=>{
	fetch("/api/v2/members")
	.then(res=>res.json())
	.then(data=>{
	 // data 는 {list:[], pageNum:1, totalPageCount:x} 형식의 object 이다
		setState(data);
	});
}, []);
  • tbodymap 함수 수정
<tbody>
	{state.list.map(item=>
		<tr>
			<td>{item.num}</td>
			<td>{item.name}</td>
			<td>
				<NavLink to={`/members/${item.num}`}>보기</NavLink>
			</td>
		</tr>
	)}
</tbody>
  • MemberServiceImpl 에 getPage 코드 추가
public MemberPageResponse getPage(int pageNum) {
	// num 에 대해서 내림차순 정렬하겠다는 Sort 객체 
	Sort sort=Sort.by(Sort.Direction.DESC, "num");
		
	// pageNum 과 page row count 와 정렬 객체를 전달해서 원하는 PageRequest 를 만들어내고 
	PageRequest pageRequest = PageRequest.of(pageNum-1, PAGE_ROW_COUNT, sort);
		
	// org.springframework.data.domain 페키지의 Page type 을 import 해야 한다.
	Page<Member> page=memberRepo.findAll(pageRequest);
		
	// Page 객체를 stream 으로 만들어서 dto 의 List 를 얻어낸다.
	List<MemberDto> list=page.stream().map(MemberDto::toDto).toList();
		
	// 하단 시작 페이지 번호 
	int startPageNum = 1 + ((pageNum-1)/PAGE_DISPLAY_COUNT)*PAGE_DISPLAY_COUNT;
		
	// 하단 끝 페이지 번호
	int endPageNum=startPageNum+PAGE_DISPLAY_COUNT-1;
		
	// 전체 페이지의 갯수 구하기 (Page 객체에 이미 계산되어서 들어 있다)
	int totalPageCount = page.getTotalPages();
	
	// 끝 페이지 번호가 이미 전체 페이지 갯수보다 크게 계산되었다면 잘못된 값이다.
	if(endPageNum > totalPageCount){
		endPageNum = totalPageCount; //보정해 준다. 
	}
		
	return MemberPageResponse.builder()
			.list(list)
			.pageNum(pageNum)
			.totalPageCount(totalPageCount)
			.startPageNum(startPageNum)
			.endPageNum(endPageNum)
			.build();
}
  • Member.jsx 에 Pagination 페이징 처리 코드 추가
<Pagination>
	<Pagination.Item>Prev</Pagination.Item>
	{
		state.list.map(item => <Pagination.Item></Pagination.Item>)
	}
	<Pagination.Item>Next</Pagination.Item>
</Pagination>
  • Member.jsx 에 페이징 UI 를 만들 때 사용할 배열을 리턴해주는 range 함수 추가
function range(start, end) {
	const result = [];
	for (let i = start; i <= end; i++) {
		result.push(i);
	}
	return result;
}
  • 페이지 번호를 출력할 때 사용하는 숫자를 배열에 미리 준비
const pageArray = range(state.startPageNum, state.endPageNum);
  • pageArraymap 코드 수정
<Pagination>
	<Pagination.Item>Prev</Pagination.Item>
	{
		pageArray.map(num => <Pagination.Item>{num}</Pagination.Item>)
	}
	<Pagination.Item>Next</Pagination.Item>
</Pagination>

페이지 이동

  • 페이지를 이동하는 pageMove 함수 추가
const pageMove = (num) => {

};
  • 페이지 번호 클릭 시 이동하는 onClick 이벤트 추가
<Pagination>
	<Pagination.Item>Prev</Pagination.Item>
	{
		pageArray.map(num =>
			<Pagination.Item onClick={()=>pageMove(num)}>
				{num}
			</Pagination.Item>
		)
	}
	<Pagination.Item>Next</Pagination.Item>
</Pagination>

navigate 함수를 이용해 파라미터 값을 읽어와서 해당 컴포넌트에서 페이지 번호로 읽어오게 코드를 수정

/members?pageNum=1
/members?pageNum=2
/members?pageNum=3
  • 이동을 하기 위한 hook navigate 추가
const navigate = useNavigate();
  • 페이지를 이동하는 pageMove 함수에 navigate 사용
const pageMove = (num) => {
	navigate(`/members?pageNum=${num}`);
};
  • useSearchParams 함수 추가
    • "/members?pageNum=x" 에서 pageNum 을 추출하기 위해 사용
const [params] = useSearchParams();
  • useEffect 함수에 query 파라미터 & url 구성 & params 전달
    • 컴포넌트가 활성화 되거나 params 가 변경될 때 회원 목록을 받아온다
useEffect(()=>{
	// query 파라미터값을 읽어와 본다
	const pageNum = params.get("pageNum");
	// pageNum 이 있으면 해당 페이지 정보를 요청하는 url 을 구성하고 없으면 페이지 번호 없이 구성
	const url = pageNum ? `/api/v2/members?pageNum=${pageNum}` : "/api/v2/members"
	fetch(url)
	.then(res=>res.json())
	.then(data=>{
		setState(data);
	});
}, [params]);
  • active 처리
<Pagination>
	<Pagination.Item>Prev</Pagination.Item>
	{
		pageArray.map(num =>
			<Pagination.Item 
				onClick={()=>pageMove(num)}
				active={state.pageNum === num}
			>
				{num}
			</Pagination.Item>
		)
	}
	<Pagination.Item>Next</Pagination.Item>
</Pagination>
  • Prev 에 onClick 이벤트 추가
<Pagination.Item
	onClick={()=>pageMove(state.startPageNum-1)}
	disabled={state.startPageNum === 1}
>
	Prev
</Pagination.Item>
<Pagination.Item
	onClick={()=>pageMove(state.endPageNum+1)}
	disabled={state.endPageNum === state.totalPageCount}
>
	Next
</Pagination.Item>

검색 기능

  • Member.jsx 에 select 옵션 & 검색 박스 & 버튼 추가
<label htmlFor="condition">검색 조건</label>
<select name="condition" id="condition">
	<option value="name_addr">이름 + 주소</option>
	<option value="name">이름</option>
	<option value="addr">주소</option>
</select>
<input type="text" name="keyword" placeholder="검색어 입력" />
<button>검색</button>
  • 검색 조건과 검색 키워드를 상태값으로 관리
const [search, setSearch] = useState({
	condition:"name_addr",
	keyword:""
});
  • 검색 조건에 변화가 생겼을 때 호출할 handleSearchChange 함수 추가
    • e.target.name : condition or keyword
 const handleSearchChange = (e) =>{
	setSearch({
		...search,
		[e.target.name]:e.target.value
	});
};
  • select 와 input 요소에 onChange 이벤트 & value 속성 추가
<select onChange={handleSearchChange} value={search.condition} name="condition" id="condition">
<input onChange={handleSearchChange} value={search.keyword} type="text" name="keyword" placeholder="검색어 입력" />
  • 검색 버튼을 눌렀을 때 호출할 handleSearchClick 함수 추가
const handleSearchClick = () => {
	// search 상태값 object 에 저장된 내용을 query 문자열로 변경한다
	// {condition:"name_addr" , keyword:"kim"} => condition=name_addr&keyword=kim
	const query = new URLSearchParams(search).toString();
  
	// "/members?condition=name_addr&keyword=kim" 이런 형식으로 주소창이 변경된다
	navigate(`/members?${query}`);
};
  • 버튼에 onClick 이벤트 추가
<button onClick={handleSearchClick}>검색</button>
http://localhost:5173/#/members?condition=name_addr&keyword=%EA%B0%95%EB%82%A8
  • useEffect 함수 전체 수정
// 컴포넌트가 활성화 될 때 회원 목록 받아오기 + params 가 변경되었을 때도 다시 받아오기
useEffect(() => {
    // params 정보를 읽어온다
    const pageNum = params.get("pageNum"); // null 일 수 있다
    const condition = params.get("condition"); // null 일 수 있다
    const keyword = params.get("keyword"); // null 일 수 있다

    // api 서버에 요청할 query 문자열 구성
    const qs = new URLSearchParams(); // 객체 생성 후 하나씩 setting 
    if (pageNum) {
        qs.set("pageNum", pageNum);
    }
    if (keyword) {
        qs.set("condition", condition);
        qs.set("keyword", keyword);
    }

    console.log(qs.toString());

    fetch(`/api/v2/members?${qs.toString()}`)
        .then(res => res.json())
        .then(data => {
            // data 는 {list:[], pageNum:1, totalPageCount:x} 형식의 object 이다
            setState(data);
        });
}, [params]);
  • MemberListRequest 클래스 생성
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class MemberListRequest {
	// 페이지 번호 (default 값 1)
	private int pageNum = 1;
	// 검색 조건
	private String condition;
	// 검색 키워드
	private String keyword;
}
  • RestMemberController2 에 list 메소드 수정
@GetMapping("/members")
public MemberPageResponse list(MemberListRequest request) {
		
	return memberService.getPage(request);
}
  • MemberService 인터페이스에 getPage 메소드 수정
public MemberPageResponse getPage(MemberListRequest request);
  • MemberServiceImpl 에 getPage 메소드 오버라이드 수정
public MemberPageResponse getPage(MemberListRequest request) {
	// 페이지 번호
	int pageNum = request.getPageNum();
		
	// num 에 대해서 내림차순 정렬하겠다는 Sort 객체 
	Sort sort=Sort.by(Sort.Direction.DESC, "num");
		
	// pageNum 과 page row count 와 정렬 객체를 전달해서 원하는 PageRequest 를 만들어내고 
	PageRequest pageRequest = PageRequest.of(pageNum-1, PAGE_ROW_COUNT, sort);
		
	// org.springframework.data.domain 페키지의 Page type 을 import 해야 한다.
	Page<Member> page=memberRepo.findAll(pageRequest);
		
	// Page 객체를 stream 으로 만들어서 dto 의 List 를 얻어낸다.
	List<MemberDto> list=page.stream().map(MemberDto::toDto).toList();
		
	// 하단 시작 페이지 번호 
	int startPageNum = 1 + ((pageNum-1)/PAGE_DISPLAY_COUNT)*PAGE_DISPLAY_COUNT;
		
	// 하단 끝 페이지 번호
	int endPageNum=startPageNum+PAGE_DISPLAY_COUNT-1;
	
	// 전체 페이지의 갯수 구하기 (Page 객체에 이미 계산되어서 들어 있다)
	int totalPageCount = page.getTotalPages();
		
	// 끝 페이지 번호가 이미 전체 페이지 갯수보다 크게 계산되었다면 잘못된 값이다.
	if(endPageNum > totalPageCount){
		endPageNum = totalPageCount; //보정해 준다. 
	}
		
	return MemberPageResponse.builder()
			.list(list)
			.pageNum(pageNum)
			.totalPageCount(totalPageCount)
			.startPageNum(startPageNum)
			.endPageNum(endPageNum)
			.build();
}
  • MemberRepository 에 검색 메소드 생성
  1. findByNameContaining
  • name 검색 메소드
public Page<Member> findByNameContaining(String keyword, Pageable pageable);
  1. findByAddrContaining
  • addr 검색 메소드
public Page<Member> findByAddrContaining(String keyword, Pageable pageable);
  1. findByNameContainingAddrContaining
  • name or addr 검색 메소드
public Page<Member> findByNameContainingOrAddrContaining(String keyword, String keyword2, Pageable pageable);
  • MemberServiceImpl 에 getPage 에 코드 추가
// 검색 키워드
String keyword = request.getKeyword();		
		
Page<Member> page = null;
		
// 만일 키워드가 비었으면 모든 회원 정보 중에서 원하는 페이지의 결과 얻어내기
if(keyword == null || keyword.isEmpty()) {
	page = memberRepo.findAll(pageRequest);
} else { // 키워드가 있으면 키워드에 해당하는 결과 얻어내기
	// 검색 조건으로 분기한다
	switch(request.getCondition()) {
		case "name":
			page = memberRepo.findByNameContaining(keyword, pageRequest);
			break;
		case "addr":
			page = memberRepo.findByAddrContaining(keyword, pageRequest);
			break;
		case "name_addr":
			page = memberRepo.findByNameContainingOrAddrContaining(keyword, keyword, pageRequest);
			break;
	}
}
  • Member.jsx 에 pageMove 함수 코드 수정
const pageMove = (num) => {       
	// 현재 URLSearchParams 를 복사하고
	const qs = new URLSearchParams(params);
	// pageNum 만 교체 한다
    qs.set("pageNum", num);

    navigate(`/members?${qs.toString()}`);
};

Axios

src 폴더명을 Step02SPA 로 변경
src_spa 폴더를 복사해 복붙한 폴더명을 src 로 변경

  • Home.jsx 에 링크 추가
<NavLink to="/members">회원 목록</NavLink>
  • Member.jsx 생성
function Member() {
    
    return <>
        <h1>회원 목록</h1>
        <table className="table table-striped">
            <thead>
                <tr>
                    <th>번호</th>
                    <th>이름</th>
                    <th>주소</th>
                </tr>
            </thead>
            <tbody>

            </tbody>
        </table>
    </>
}
  • index.jsx 에 Member 라우트 경로 추가
{paht:"members", element:<Member/>}
  • Member.jsx 에 useState & useEffect 추가
// 회원 목록을 상태값으로 관리
const [members, setMembers] = useState([]);

useEffect(()=>{
	// npm install axios 로 설치한 axios 를 이용해서 api 요청해보기
	// get방식 : axios.get  post방식 :axios.post
	axios.get("/api/v1/members")
		.then(res => {
        console.log(res);
        // res.data 에 서버가 응답한 data 가 들어 있다
        setMembers(res.data);
	})
    .catch(err => console.log(err));
},[]);
  • tbody 에 목록 띄우기
<tbody>
	{members.map(item=> 
		<tr key={item.num}>
			<td>{item.num}</td>
            <td>{item.name}</td>
            <td>{item.addr}</td>
		</tr>
	)}
</tbody>
  • 회원 추가 링크 추가
<NavLink to="/members/new">회원 추가</NavLink>
  • MemberForm.jsx 생성
function MemberForm() {
    // javascript 로 라우팅할 수 있는 함수 얻어내기
    const navigate = useNavigate(); // 여기서 navigate 함수를 이용해서 이동할 수 있다

    //form 에 submit 이벤트가 발생했을때 실행할 함수
    const handleSubmit = (e)=>{
        // 기본동작 막기 (form 제출 막기)
        e.preventDefault();
        // 이벤트가 발생한 요소의 참조값 (form 요소의 참조값)
        const form=e.target;    
        // 폼에 입력한 내용을 담고 있는 FormData 객체를 얻어낸다
        const formData=new FormData(form);
        // 폼에 입력한 내용을 object 로 변환
        const obj=Object.fromEntries(formData);
    };

    return<>
        <h1>회원 추가 양식</h1>  
        <form action="/api/v1/members" method="post" onSubmit={handleSubmit}>
            <div className="mb-3">
                <label className="form-label" htmlFor="name">이름</label>
                <input className="form-control" type="text" name="name" id="name"/>
            </div>
            <div className="mb-3">
                <label className="form-label" htmlFor="addr">주소</label>
                <input className="form-control" type="text" name="addr" id="addr"/>
            </div>
            <button className="btn btn-success" type="submit">저장</button>
        </form>
    </>
}
  • index.jsx 에 MemberForm 라우트 경로 추가
{path:"/members/new", element:<MemberForm/>}
  • MemberForm.jsx 에 axios 추가
    • axios 를 이용해서 post 방식 요청하기
axios.post("/api/v2/members", obj)
.then(res=>{
	console.log(res.data);       
})
.catch(err=>{
	// err 는 에러 정보를 담고 있는 object 이다
	console.log(err);
})

에러 메세지

  • MemberDto 에 @size 어노테이션 추가
@Size(max=20, message="이름은 최대 20글자 까지 가능합니다")
  • RestMemberController2 에 create 메소드 생성
    • @Valid 어노테이션을 추가해서 검증도 수행한다
@PostMapping("/members")
public MemberDto create(@Valid @RequestBody MemberDto dto) {
	return memberService.addMember(dto);
}

step09.exception 패키지 생성

  • ExceptionController 클래스 생성하고 handleValidation 메소드 생성
@RestControllerAdvice
public class ExceptionController {

	// @Valid 어노테이션을 이용해서 검증을 하다가 검증을 통과하지 못하면 여기가 실행된다
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidation(MethodArgumentNotValidException ex) {
    		// dto 에 필드 검증에 작성한 메세지 정보가 에러 정보에 담겨서 응답되도록 한다
        var errors = ex.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(
                        FieldError::getField,
                        DefaultMessageSourceResolvable::getDefaultMessage,
                        (a, b) -> a,
                        LinkedHashMap::new
                ));
        Map<String, Object> body = Map.of(
                "code", "VALIDATION_ERROR",
                "errors", errors
        );
        return ResponseEntity.badRequest().body(body);
    }
}
  • MemberDto 에 검증 조건 추가
@NotBlank(message = "이름은 반드시 입력해야 합니다")
@Size(max=20, message="이름은 최대 20글자 까지 가능합니다")
private String name;
	
@NotBlank(message = "주소는 반드시 입력해야 합니다")
private String addr;

  • MemberForm 에 useState 추가
    • 필드 에러 정보를 상태값으로 관리
const [errors, setError] = useState({});
  • catch 문 안에 추가
    • 필드 에러 객체를 상태값에 넣어준다
setError(err.response.data.errors);
  • 필드 에러 객체에 에러 정보 존재시 에러 정보 출력하는 코드 추가
    • {name:"최대 20자 가능"}
    • {addr:"반드시 입력해야 한다", name:"최대 20자 가능"}
<div className="mb-3">
	<label className="form-label" htmlFor="name">이름</label>
	<input className="form-control" type="text" name="name" id="name"/>
	{errors.name && <p className="form-text text-danger">{errors.name}</p>}
</div>
<div className="mb-3">
	<label className="form-label" htmlFor="addr">주소</label>
	<input className="form-control" type="text" name="addr" id="addr"/>
	{errors.addr && <p className="form-text text-danger">{errors.addr}</p>}
</div>

profile
학원 공부 내용 정리

0개의 댓글