Spring

0922. 댓글처리 Ajax, REST API

삼월은마치 2023. 9. 22. 17:51

 🌱서론

자바스크립트는 멀티스레드를 지원하지 않기 때문에 비동기로만 할 수 있다.
 
<요청 보내거나 받을 때 쓰는 인코딩>
 지금까지는 데이터를 form 태그를 사용한 1, 2번 방식으로 보냈다.


<form enctype=" ">
1. 이름 = 값&이름=값& ... => default 인코딩 : 제일 많이 씀
2. header 띄우고 body ...=> multipart 인코딩 : 바이너리파일 
 
이제 나오는 건 자바스크립트로 표현할 때 쓰는 Ajax 형식이다.

 "키" : 값,
 "키" : 값 ... }
=> application/json 인코딩 : Ajax

(예시) .application/json 빼먹는 실수 많이 한다.


📍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} : 그 글 한 개를 나타내겠다.

REST API에서 자주 쓰는 5가지 메서드 표현

- 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를 앞에 붙인다.
구분하지 않으면 보안 설정 시 까다로워진다.
 

rest 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를 추가해야 값이 들어간다. (안 붙이면 아무 값도 안 들어감)

Jackson json dependency

<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);

}

*다시 참고 REST API

 
 

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으로 설정한다.

RESTfull 메서드명 + Mapping

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를 설치하자.

 
설치한 화면

Talend API Tester

댓글 조회

아까 사용한 댓글 요청 url 복사해서 입력 후 [Send] (alt+enter 해도 send 된다.)

 

 
응답 화면 

메서드 고를 수 있다. (밑에 3개는 지금 안 씀)

 

댓글 등록

 
POST 메서드에서 우측 body에 json 문자열을 만들면 된다.
❗해당 게시글의 작성자가 아닌 다른 작성자로 등록해야 한다.
(우리 설정이 본인글에는 대댓글만 되지 댓글은 안 되는 걸로 설정할 거라서;;)
key는 항상 문자열 등록하고 >> "bno" : 36
 

컨트롤러의 postmapping create가 작동할 예정

댓글 없는 상태에서 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

bno는 url에 제시했으니 no랑 content만

다른 부분 다 비워지고 보낸 데이터만 담김

updateDate 바뀜

 
DB 한 번 읽어서 리턴하는 게 올바른 방법이다.

결과는 같지만 아무튼 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);
} 
}

fetch와 async 비교

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 정보 보냄 💁  ☕🤹‍♀️ "커피 나왔습니다"

callback 호출 순서

*모양 

매개변수 => 함수 본체(body)

 
자 이제 고치자~! (길다)

원래 형태
화살표 추가했으니 res 감싼 괄호 제거
한 줄이니 대괄호 제거
한 줄이니 리턴 제거
아랫줄 then도 똑같이 정리.


이제 콜백지옥으로부터 자유로워지게 async 형식으로 만들자

 

⭐ready의 function앞에 async 추가
let res = ⭐await 앞에 붙였으니 .then 정리

 

let data = await 추가 했으니 나머지 .then 정리

 

최종 형태

 

🤹‍♀️더 정리할 수 있다.

 
리소스의 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"%>