0922. 댓글처리 Ajax, REST API
🌱서론
자바스크립트는 멀티스레드를 지원하지 않기 때문에 비동기로만 할 수 있다.
<요청 보내거나 받을 때 쓰는 인코딩>
지금까지는 데이터를 form 태그를 사용한 1, 2번 방식으로 보냈다.
<form enctype=" ">
1. 이름 = 값&이름=값& ... => default 인코딩 : 제일 많이 씀
2. header 띄우고 body ...=> multipart 인코딩 : 바이너리파일
이제 나오는 건 자바스크립트로 표현할 때 쓰는 Ajax 형식이다.
{
"키" : 값,
"키" : 값 ... }
=> application/json 인코딩 : Ajax
📍Ajax
Asynchronous Javascript and XML (비동기 자바스크립트와 XML)
페이지의 이동(깜빡임) 없이 자바스크립트로 서버와 통신하는 기법이다.
최근 데이터 표현 : xml < Json
*전송과 응답 시 데이터모드를 .application/json 인코딩으로 한다.
브라우저에서 XMLHttpRequest 객체를 제공하지만 타이핑 양이 많아서 사용이 어렵고,
이 객체를 사용할 수 있는 간소화된 라이브러리를 사용한다.
<라이브러리 종류>
- $.ajax : jquery 에서 제공하는 ajax 메서드, 예전에 주름잡음
- ⭐ fetch() : js 기본함수로 제공, 요즘 많이 씀 => react나 순수 자바스크립트에서 많이 씀
- axios : 최근에 jquery 안 쓰면 쓰는 또다른 후보 => 프론트에서 많이 씀
📍REST API
Representational State Transfer "상태를 표현"
함수호출과 다를바 없으므로 일종의 라이브러리이다.
- URI와 메서드 : 함수명
- {bno} 관련게시글 : 변수
기능적 매커니즘은 함수호출이고 다른 컴퓨터의 서버에서 함수호출이 제공된다는 차이점만 있다.
=> "RPC(Remote Procedure Call) 다른 컴퓨터의 함수를 호출하는 매커니즘"
[장점]
1. 클라이언트 파트가 브라우저일 필요없이 일반 어플리케이션도 웹서버의 자원을 사용할 수 있다.(자원=정보)
2. 통신만 http로 하지 주고 받는 데이터에 html 처리를 할 필요가 없으니, 클라이언트 종류에 제한이 생기지 않는다.
예시로 휴대폰을 사용할 때,
JSON으로 데이터를 호출, 응답하고 있으니 (REST API는 아닐 수 있지만!)
휴대폰은 브라우저가 아닌데 웹서버 정보 사용할 수 있는 이유이다.
인터넷에서 웹서버는 자원을 관리한다.
자원이라함은, 정보인데 게시글 1개나 여행지정보 1개 등을 예로 들 수 있고 이 정보를 식별하는 게 자원관리이다.
이 정보는 해당 웹서버에 존재하는데 이를 URL(정확히는 URI)로 나타낼 수 있다.
=> URI로 정보를 식별
이전에 쓰던 URL 개념은 "페이지를 식별"하는 용도였다.
REST API를 배울 때는 "정보를 식별"하자
🖤즉, REST API란 URI 정보(자원)와 메서드(행위)로 대상과 어떤 일을 할지 표현하는 라이브러리이다.
표현방식으론 일반적으로 JSON을 사용한다.
✨REST API의 메서드
<예전>
read : GET
insert,update,delete : POST
<현재>
- read : GET
- create : POST
- modify : PUT
- remove : DELETE
* board 뒤에 식별번호={bno} : 그 글 한 개를 나타내겠다.
- 1 -
차이점은 board 뒤에 오는 url에서 나타난다.
- 2 -
추가된 정보 {bno} 부분은 key가 되고 이 key 에 대응하는 정보 하나로 해석한다.
표현을 볼 때는 uri 와 메서드까지 다 봐야한다.
수정과 삭제는 대상 지칭이 필요하니 key(={bno})가 들어가는 것이다.
참고로 수정은 board의 해당 id가 게시글 수정을 하는 것이니 body와 함께 no를 컨트롤러에서 변수로 받음.
나머지도 uri가 같을 땐, 메서드로 구분을 해야 한다.
이제 서버는 해당 정보를 보고 할 일 한다.
- 3 -
GET(추출) 요청일 때는 전달할 게 없으니 요청이 끝이지만(=DELETE는 body 말고 대상만 필요)
POST(생성)는 body의 내용이 전달되어야 한다. PUT(수정)도 마찬가지.
- 4 -
여지껏 어떤 페이지를 보고 싶을 때 Spring에서 파라미터를 받는 방식으로
쿼리스트링인 @RequestParam(Get방식 통신)을 써서 ?bno=10 으로 했다면,
rest 통신을 하려면 @PathVariable(RESTful방식 통신)을 사용하여 {bno}를 사용하면 된다.
delete를 @PathVariable을 쓴 예로 표현식은 이렇게 된다.
>> @DeleteMapping('/api/board/{bno}')
- 5 -
POST와 PUT에서 body구성은 Json으로 한다.
요청과 응답 둘다 Json.
(지금까지 응답은 Html로 받았다면 Ajax에서는 Json으로 받는다.)
🌏 중간 참고하면 좋은 글
https://velog.io/@dmchoi224/Rest-API-RequestParam-%EA%B3%BC-PathVariable
Rest API , @RequestParam 과 @PathVariable
REST API REST API 란 REST API 에서 REST는 Representational State Transfer 의 약자로 소프트웨어 프로그램 아키텍처의 한 형식. 즉, 자원을 이름 (자원의 표현) 으로 구분하여 해당 자원의 상태 (정보)를 주고 받
velog.io
🌱댓글처리 REST API
다른 uri와 구분하기 위해 (웹브라우저가 아님을 표현) api를 앞에 붙인다.
구분하지 않으면 보안 설정 시 까다로워진다.
관련게시글 번호 : 변수 파트 {bno}
댓글 id : Primary Key {no}
view를 사용하지 않아서 html응답이 아니다.
view를 통하지 않고 직접 응답하는 Json이다. (ex: avatar -> 이미지로 직접 응답함)
직접 응답하려면 아바타처럼 responsebody를 직접 다 붙여줘야하는데 귀찮으니까 특수 컨트롤러를 만들자.
JSON으로 응답하는 특수 컨트롤러
@RestController
처리결과를 뷰로 보내는 게 아닌 직접 응답하는 컨트롤러
모든 메서드에 @ResponseBody 가 자동으로 추가되고 처리결과는 객체로 바로 리턴이 가능하다.
❗단, Gson(구글), Jackson같은 라이브러리가 등록되어있어야 한다.
에러가 항상 200으로 나가서(=해당 객체를 json문자열로 변경해서 클라이언트에 응답) 처리에 대한 조작이 필요하다.
* ResponseEntity<T>를 리턴하면 응답코드, 응답 헤더, 내용에 대한 직접 지정이 가능하다.
⭐POST, PUT 요청에서 Body를 Json 인코딩(application/json)해서 보냈으므로 이 json을 디코딩해서 @ModelAttribute에 보내야한다.
❗모델객체 앞에 @RequestBody를 추가해야 값이 들어간다. (안 붙이면 아무 값도 안 들어감)
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.4</version>
</dependency>
📍서버 측 설계
1. tbl_comment 댓글 테이블 생성
drop table if exists tbl_comment;
create table tbl_comment (
no integer auto_increment primary key,
bno integer not null,
content varchar(2000) not null,
writer varchar(50) not null,
reg_date datetime default now(),
update_date datetime default now(),
constraint fk_comment_board foreign key(bno) references tbl_board(bno),
constraint fk_comment_member foreign key(writer)
references tbl_member(username)
);
select * from tbl_comment;
2. VO 객체 생성
CommentVO
package org.galapagos.domain;
import java.util.Date;
import lombok.Data;
@Data
public class CommentVO {
private Long no;
private Long bno;
private String writer;
private String content;
private Date regDate;
private Date updateDate;
}
3. 인터페이스 Mapping
CommentMapper 인터페이스 생성
RestAPI에 대응하도록
- 목록보기(readAll) : 글번호(bno) 필요 - foreign key(Long bno)
- 하나 얻기-보기(get) : no (댓글 id - pk) >> (Long no)
- 생성(create) , 수정(update) : Body 전달 필요 (CommentVO vo)
- 삭제(delete) : comment의 id 필요 (Long no) *댓글삭제니까 댓글 id : pk
package org.galapagos.mapper;
import java.util.List;
import org.galapagos.domain.CommentVO;
public interface CommentMapper {
List<CommentVO> readAll(Long bno);
CommentVO get(Long no);
void create(CommentVO vo);
void update(CommentVO vo);
void delete(Long no);
}
4. XML
CommentMapper.xml 생성
insert하면 pk는 auto_increment 라서 코멘트의 값에 어떤 값이(CommentVO no xxx) 배정됐는지 모른다.
이걸 확인하는 게 selectKey(VO객체의 keyproperty, keycolumn 확인)
selectKey 설정 안 하면 무슨 값인지 모르니까 넣어줘야 한다.⭐
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.galapagos.mapper.CommentMapper">
<select id="readAll" resultType="CommentVO">
select * from tbl_comment
where bno = #{bno} <!-- Foreign Key : 글번호 bno -->
</select>
<select id="get" resultType="CommentVO">
select * from tbl_comment
where no = #{no} <!-- 댓글 id - pk -->
</select>
<insert id="create">
insert into tbl_comment (bno, writer, content)
values(#{bno}, #{writer}, #{content})
<!-- insert로 인해 property랑 column에 auto_increment 된 값이 무엇인지 확인용 -->
<selectKey resultType="Long" keyProperty="no" keyColumn="no"
order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
</insert>
<update id="update">
update tbl_comment
set
content = #{content},
update_date = now()
where no = #{no}
</update>
<delete id="delete">
delete from tbl_comment
where no = #{no}
</delete>
</mapper>
서비스 안 만들고 컨트롤러로 바로 연결하기
5. Controller 설정
CommentController 생성
*댓글 조회이니 게시글번호 필요없으면 빼도 된다.
29열 @Pathvariable Long bno <<
🖤@메서드명Mapping("")로 할 경우 공통 url로 들어갈 부분을 @RequestMapping으로 설정한다.
package org.galapagos.controller;
import java.util.List;
import org.galapagos.domain.CommentVO;
import org.galapagos.mapper.CommentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/board/{bno}/comment") //("")로 할 경우 공통 url로 들어갈 부분
public class CommentController {
@Autowired
CommentMapper mapper;
@GetMapping("")
public List<CommentVO> readComments(@PathVariable Long bno) { // 글번호(게시글 보여줌)
return mapper.readAll(bno); //경로변수에 있던 글번호
}
@GetMapping("/{no}") //자체적인 pathvariable List<>로 생성된 배열[] 뒤에 붙여라.
public CommentVO readComment(@PathVariable Long bno, @PathVariable Long no) { //글번호(게시글) + 댓글 id(달린 댓글들)
return mapper.get(no); //댓글 보는 거니 글번호 bno는 필요없으면 빼도 된다.
}
@PostMapping("")
public CommentVO create(@RequestBody CommentVO vo) { // body 전달 (생성할 댓글내용)
mapper.create(vo);
return mapper.get(vo.getNo()); //생성한 댓글 다시 꺼내서 리턴(댓글 id = no)
}
@PutMapping("/{no}")
public CommentVO update(@PathVariable Long no, @RequestBody CommentVO vo) { //댓글 id + body(수정할 댓글내용)
System.out.println("==> " + vo); //수정한 댓글의 내용 body
mapper.update(vo);
return mapper.get(vo.getNo()); //올바른 방법
//return vo; //그냥 받은 대로 바로 리턴
}
@DeleteMapping("/{no}")
public String delete(@PathVariable Long no) { //댓글 id
System.out.println("delete ==>" + no); // 댓글삭제 (댓글 id를 삭제했다고 알림)
mapper.delete(no);
return "OK";
}
}
get 요청으로 댓글 있는지만 확인 가능한 단계이니 해 보자
>> localhost:8080/api/board/존재하는 게시글 번호 중 내가 아는 거/comment
빈 배열의 댓글 등장
❗클라이언트 측 설계를 하기 전에 REST 테스트가 필요하다.
PostMan은 가입이 번거로우니 크롬 확장 프로그램인 Talend API Tester를 설치하자.
설치한 화면
댓글 조회
아까 사용한 댓글 요청 url 복사해서 입력 후 [Send] (alt+enter 해도 send 된다.)
응답 화면
메서드 고를 수 있다. (밑에 3개는 지금 안 씀)
댓글 등록
POST 메서드에서 우측 body에 json 문자열을 만들면 된다.
❗해당 게시글의 작성자가 아닌 다른 작성자로 등록해야 한다.
(우리 설정이 본인글에는 대댓글만 되지 댓글은 안 되는 걸로 설정할 거라서;;)
key는 항상 문자열 등록하고 >> "bno" : 36
댓글 없는 상태에서 create하면 selectKey no 배정이 처리될 예정.
updateDate, regDate의 값은 안 넘겼으므로 default now가 들어가서 여전히 null이다.
=> null이면 create(vo)가 바로 안 넘어가고 no가 배정된다고 했다.
=> get이 필요하므로 DB에서 꺼내서 리턴해야 한다.
그러면 >>return mapper.get(vo.getNo()); 에서는 값이 들어가 있다.
send해 보면 csrf 토큰 때문에 403 에러 뜬다.
REST API에 대해서는 csrf 토큰을 안 쓰겠다고 설정해 줘야 한다...
SecurityConfig에서 csrf 필터 아래에 코드 추가
ignoreingAntMatchers의 패턴에 맞으면 csrf 체크를 안 하겠다는 뜻 >>/api/**
http.csrf().ignoringAntMatchers("/api/**");
다시 send 누르면 찰나의 시간동안 [abort]가 카운트와 함께 노란색 버튼으로 지나간 후 다시 send로 돌아간다.
abort는 중단버튼으로 찰나의 0.n초 안에 누르면 중단되나 보다.
regDate와 updateDate에 값이 부여되었다.
sql에서 확인
이제 GET 요청으로 읽어보자
url 뒤에 슬래시하고 댓글 no 입력하면 해당 댓글만 나온다.
>>http://localhost:8080/api/board/36/comment/1
댓글 수정
${content}와 ${no}를 꼭 넘겨야 한다.
>>http://localhost:8080/api/board/36/comment/1
PUT
다른 부분 다 비워지고 보낸 데이터만 담김
updateDate 바뀜
DB 한 번 읽어서 리턴하는 게 올바른 방법이다.
댓글 삭제
+) 없는 댓글을 삭제 요청했을 때
404를 기대했는데 서버 내부에서 에러가 발생해서 500에러가 아니면 정상처리로 간주해서 별 에러 없다.
에러로 취급하고 싶으면 @ResponseEntity<T> 를 써서 응답코드,헤더, 내용 직접 지정해서 대응 가능하다.
📍클라이언트 측 설계
Ajax
* fetch() 함수
매개변수 기본 2개를 받는다.
첫 번째 인자 : URL (주로 REST url 사용)
ex) http://localhost:8080/api/board/36/comment/6
두 번째 인자 : 옵션 객체 => content type, body 등
리턴 : Promise 타입 객체 (비동기 함수가 리턴하는 타입이다.)
비동기 함수는 함수가 끝났을 때 처리할 로직이 담긴 callback함수를 필수로 가지고 있다.(click, ready event처리 등)
그런데 이러면 콜백 안에 콜백 안에 콜백이 된다. 콜백지옥 => 해석불가
콜백지옥의 개선안으로 나온 게 Promise 객체이다.
커피 시키고 진동벨 받으면 울릴 때까지 다른 일 하는 것 : 비동기 (다른 일 못 하고 기다리면 동기)
진동벨 역할 : promise 객체
🖤promise 객체의 메서드 (fetch의 리턴값으로 호출된다.)
.then((response) => console.log("response:", response)) 작업 끝나면 실행하겠다
.catch((error) => console.log("error:", error)); 실행하다가 예외가 발생하면 작동
*function 키워드없이 괄호로만 하는 람다식(=화살표함수)
괄호 안 : 매개변수 / 화살표 우측 : 함수 body
함수가 한 줄 코드이면 중괄호{ }와 리턴이 생략되어있다.
"function(response){ } return console.~이하생략" 의 축약형태가 위(🖤)의 함수식이다.
[표준안]
fetch(url).then((response) => response.json()).then((data) => console.log(data));
[새 버전]
비동기 매커니즘이지만 표기는 동기스타일로 지원해 주는 형식
✨async
- await : 기다리겠다. "비동기"(빼먹으면 undefined 에러)
- let res 에서 res : response를 말함
- json 도 비동기라 기다림
- 예외는 catch가 처리
모양은 동기지만 실제 진행은 주루루 진행. promise의 then 절로 다 들어가버림.
async function rest_get(url) {
try {
let res = await fetch(url);
return await res.json();
} catch(e) {
console.log(e);
}
}
await
1. Promise를 리턴하는 비동기 함수 앞에 붙일 수 있고
2. async 키워드가 붙은 함수에서만 쓸 수 있다.
3. await를 쓸지 안 쓸지는 선택사항이다.(빼도 문법 에러 아님)
4. await를 생략하면 실제 리턴값이 아니라 Promise 객체로 작업한 코드가 되어 의도치 않은 결과가 나온다.
(에러 날 수도, 이상한 데이터 들어가서 에러 안 날 수도)
<적용>
board의 get.jsp
1. 일단은 화살표 안 쓰고 익명함수 function을 사용해서 해 보자
fetch('/api/board/36/comment') //경로만 가져오기, 두 번째 매개변수 없으니 GET요청
.then(function (res) {//익명함수 function
console.log(res);
}); //callback 1개 등록
게시글 하나 선택해서 개발자 모드로 콘솔을 찍어보면..
콘솔 찍었던 부분을 return res.json(); 으로 바꾸면 promise 객체가 리턴 된다.
fetch('/api/board/36/comment') //경로만 가져오기, 두 번째 매개변수 없으니 GET요청
.then(function(res) {//익명함수 function, 리턴값의 메서드 then을 호출 -> response 객체
return res.json(); //Json 문자열을 실제 객체(promise)로 바꿔라 "역직렬화"
}) //callback 1개 등록
.then(function(data) {
console.log(data); //data라는 댓글 배열
});
// res=>console.log(res) 와 같음
});
비동기 함수를 동기처럼 나타낸 최종
fetch('/api/board/36/comment') //경로만 가져오기, 두 번째 매개변수 없으니 GET요청
.then(function(res) {//익명함수 function, 리턴값의 메서드 then을 호출 -> response 객체
console.log('응답 수신');
return res.json(); //Json 문자열을 실제 객체(promise)로 바꿔라 "역직렬화"
}) //callback 1개 등록
.then(function(data) {
console.log('Json 데이터 변환완료');
console.log(data); //data라는 댓글 배열
});
console.log('fetch 호출 완료');
// res=>console.log(res) 와 같음
});
[순서]
비동기이므로 출력순서가 코드 순서 그대로 나오지 않는다.
(1) 하드코딩한 경로로 fetch 호출(GET 요청) 💁"커피 주문할게요"
➡️ (2) response 반응 🤹♀️"네, 기다리세요"
➡️ (3) data 정보 보냄 💁 ☕🤹♀️ "커피 나왔습니다"
*모양
매개변수 => 함수 본체(body)
자 이제 고치자~! (길다)
🤹♀️더 정리할 수 있다.
리소스의 js 폴더에 rest.js 로 자바스크립트 코드 정리하기
예외도 이 코드가 처리해 줄 것이다!
rest.js
async function rest_get(url) {
try {
let res = await fetch(url);
return await res.json();
} catch(e) {
console.log(e);
}
}
/*이클립스의 자바스크립트 분석기가 await가 뭔지 몰라서 나는 에러이므로 브라우저에서는 괜찮다.*/
get.jsp에서 스크립트 태그로 정리
<script src="/resources/js/rest.js"></script>
기존 fetch 함수 지우고 상수로 url 정의한 rest_get 요청 코드 삽입
const url = '/api/board/36/comment';
let data = await rest_get(url);
console.log(data);
출력!
url 뒤에 댓글id 입력하면 역시 단일 데이터 리턴
⭐하드코딩 변수처리하기
- bno el로 얻기(board에 제시되어 있으니까 사용 가능)
//철기
//bno 하드코딩 변수 처리
const bno = ${board.bno}
const url = '/api/board/' + bno + '/comment';
console.log(url);
let data = await rest_get(url);
console.log(data);
진짜진짜 최종 get.jsp
❗괄호처리가 중간에 마무리되니 주의
❗❗주석 너무 많음 주의
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@include file="../layouts/header.jsp"%>
<script src="/resources/js/rest.js"></script>
<script>
$(document).ready(async function() {
$('.remove').click(function(){
if(!confirm('정말 삭제할까요?')) return;
document.forms.removeForm.submit();
});
//철기
//bno 하드코딩 변수 처리
const bno = ${board.bno}
const url = '/api/board/' + bno + '/comment';
console.log(url);
let data = await rest_get(url);
console.log(data);
//청동기
//rest.js로 뺀 후 스크립트 처리
/*const url = '/api/board/36/comment/1';
let data = await rest_get(url);
console.log(data); */
//신석기
// async로 가기 위한 준비
/* let res = await fetch('/api/board/36/comment'); //GET 요청
let data = await res.json());
console.log(data); */
});
//구석기
//callback .then 사용
/* fetch('/api/board/36/comment') //경로만 가져오기, 두 번째 매개변수 없으니 GET요청
.then((res) {//익명함수 function, 리턴값의 메서드 then을 호출 -> response 객체
console.log('응답 수신');
// 위에 두 줄이 res=>console.log(res) 와 같음
return res.json(); //Json 문자열을 실제 객체(promise)로 바꿔라 "역직렬화"
}) //callback 1개 등록
.then(function(data) {
console.log('Json 데이터 변환완료');
console.log(data); //data라는 댓글 배열
});
console.log('fetch 호출 완료');
});
*/
</script>
<h1 class="page-header"><i class="far fa-file-alt"></i> ${board.title}</h1>
<div class="d-flex justify-content-between">
<div><i class="fas fa-user"></i> ${board.writer}</div>
<div>
<i class="fas fa-clock"></i>
<fmt:formatDate pattern="yyyy-MM-dd" value="${board.regDate}"/>
</div>
</div>
<hr>
<div>
${board.content}
</div>
<div class="mt-4">
<a href="${cri.getLink('list')}" class="btn btn-primary list">
<i class="fas fa-list"></i> 목록</a>
<c:if test="${username == board.writer}">
<a href="${cri.getLinkWithBno('modify', board.bno) }" class="btn btn-primary modify">
<i class="far fa-edit"></i> 수정</a>
<a href="#" class="btn btn-danger remove">
<i class="fas fa-trash-alt"></i> 삭제</a>
</c:if>
</div>
<%--
<form id="listForm" action="/board/list" method="get" >
<input type="hidden" name="pageNum" value="${cri.pageNum}"/>
<input type="hidden" name="amount" value="${cri.amount}"/>
<input type="hidden" name="type" value="${cri.type}"/>
<input type="hidden" name="keyword" value="${cri.keyword}"/>
</form>
<form id="modifyForm" action="/board/modify" method="get" >
<input type="hidden" id="bno" name="bno" value="${board.bno}"/>
<input type="hidden" name="pageNum" value="${cri.pageNum}"/>
<input type="hidden" name="amount" value="${cri.amount}"/>
<input type="hidden" name="type" value="${cri.type}"/>
<input type="hidden" name="keyword" value="${cri.keyword}"/>
</form>
--%>
<form action="remove" method="post" name="removeForm">
<!-- csrf 토큰 안 넣으면 404 에러 -->
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token }" />
<input type="hidden" name="bno" value="${board.bno}"/>
<input type="hidden" name="pageNum" value="${cri.pageNum}"/>
<input type="hidden" name="amount" value="${cri.amount}"/>
<input type="hidden" name="type" value="${cri.type}"/>
<input type="hidden" name="keyword" value="${cri.keyword}"/>
</form>
<%@include file="../layouts/footer.jsp"%>