몇일동안 고생해서 멍 한 상태로 작성 중이며
혼자 개인적으로 볼 목적으로 만들었고 혹시 디태일한 설명은 영상을 보면서 이해하시면
좋을거같습니다.
인텔리제이에서 작성을 했으며 다 인텔리제이에서 코드를 복붙했는대 글이 좀 지저분하게 나옵니다.. 죄송합니다.
DB는 MYSQL 로 사용하고있습니다.
MYSQL Workbench 를 사용 하고 있습니다.
id 'java'
id 'org.springframework.boot' version '2.7.8'
id 'io.spring.dependency-management' version '1.1.0'
group = 'com.cho'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
자바 버전을 원래 17을 쓰다가 11로 다운그레이드 했습니다 JPA를 사용해 .yml 을 쓰면 좀 간편하게 DB를 생성할수있는대
17버전은 yml을 사용했을때 오류가 걸리고 옛날 자료들이랑 비교를 하면서 공부를 해도 계속 막히는 부분이 있어서
낮췃습니다
JPA hiernate를 사용했습니다 엔티티,DTO를 사용하면 자동으로 DB테이블을 생성해주고 관리를 해주는 편리한 녀석입니다 사실 이거 때문에 자바버전을 낮췃습니다
.ajax
댓글 출력은 .ajax를 사용했습니다. 근대 대충 원리만 알고 그냥 프론트 부분은 복붙하면서 한거같습니다
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.mybatis:mybatis:3.5.7'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
그래들을 사용했으며 메이븐을 사용해도 상관은 없을겁니다
JDBC, 데이터 JPA, thymeleaf, spring web ,lombok,mysql컨트롤러 ,마이베티스는 그냥 넣어봤는대 안썻습니다.
기본적인 큰 흐름은 이렇습니다
1. DB 셋팅(jpa를 사용하더라도 스키마는 생성 하고 하셔야합니다.)
여기서 제일 중요한건 기존 부모 테이블과 자식테이블의 관계를 맺어야하는대 부모 테이블과 자식 테이블의 관계를
맺을시 무조건 1:n 의 관계가 되어야 합니다.
즉 뭐 하나라도 부모테이블과 자식 테이블이 연결할때 공통된 키가 있어야 한다는 뜻입니다.
참조받는 컬럼 은 반드시 고유한 값을 가지는 컬럼이어야죠
컬럼에는 고유한 값만 들어오 PRIMARY KEY 또는 NOT NULL 인 UNIQUE 제약을 걸어야 합니다.
이것들은 중복이 될수 없으니 키를 연결할때 좋죠 이것이 핵심입니다.
2.댓글 입력 출력할 html+javascript 작성
게시글 상세페이지에서 댓글을 출력할 프론트문을 작성합니다.
3. 이제 패키지를 만들고 클래스를 만들어야 합니다. 기존 게시판에서 추가되는 클래스는 저는 편하게 comment (댓글) 이름 지을려고합니다 그리고 저는 DTO 를 추가 해서 사용하니 참고 바랍니다.
CommentController
CommentDTO
CommentEntity
CommentRepository
CommentService
기본은 이렇게 추가 되었습니다.
전체적인 흐름은 대충 느낌적으로 이렇게 흘러가는거 같습니다
Entity작성-> DTO 작성 -> Controller 작성 (추가하는동안 메소드 생성필요)-> Service(컨트롤러에 필요한 메소드를 서비스에서 작성) ->Repository(서비스에서 필요한걸 레포지토리에서 추가)-> (부모클래스들 연결) ->Service->Entity ->DTO->Controller ->Service ->Entity->Service->Controller->Service-> 프론트
application.yml
server:
port: 8080
# database 설정
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_codingrecipe?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: ??
password: ??
thymeleaf:
cache: false
# spring data jpa 설정
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
open-in-view: false
show-sql: true
hibernate:
ddl-auto: update
저 hibernate가 스키마만 지정해주면 알아서 엔티티랑 DTO를 읽고 테이블을 만들어주는 아주 편리한 녀석입니다.
근대 자바 17버전은 쓰기가 더럽게 힘들길래 그냥 맘편히 11로 낮췃습니다.
detail.html (게시판 상세페이지)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>detail</title>
<!-- jquery cdn -->
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
</head>
<body>
<table>
<tr>
<th>id</th>
<td th:text="${board.id}"></td>
</tr>
<tr>
<th>title</th>
<td th:text="${board.boardTitle}"></td>
</tr>
<tr>
<th>writer</th>
<td th:text="${board.boardWriter}"></td>
</tr>
<tr>
<th>date</th>
<td th:text="${board.boardCreatedTime}"></td>
</tr>
<tr>
<th>hits</th>
<td th:text="${board.boardHits}"></td>
</tr>
<tr>
<th>contents</th>
<td th:text="${board.boardContents}"></td>
</tr>
<tr th:if="${board.fileAttached == 1}">
<th>image</th>
<td td th:each="fileName: ${board.storedFileName}">
<img th:src="@{|/static/${board.storedFileName}|}" alt="" width="200" height="200">
</td>
</tr>
</table>
<button onclick="listReq()">목록</button>
<button onclick="updateReq()">수정</button>
<button onclick="deleteReq()">삭제</button>
<!-- 댓글 작성 부분 -->
<div id="comment-write">
<input type="text" id="commentWriter" placeholder="작성자">
<input type="text" id="commentContents" placeholder="내용">
<button id="comment-write-btn" onclick="commentWrite()">댓글작성</button>
</div>
댓글 달 부분을 먼저 만듭니다
CommentRepository
package com.cho.board.repository;
import com.cho.board.entity.BoardEntity;
import com.cho.board.entity.CommentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CommentRepository extends JpaRepository <CommentEntity, Long>{
}
CommentService
public class CommentService {
private final CommentRepository commentRepository;
private final BoardRepository boardRepository;
public Long save(CommentDTO commentDTO) {
서비스를 간단하게 일단 만들고 (다른 패키지 다만들고 private 걸어주십쇼 ,부모 클래스도 마찬가지)
CommentController
@Controller
@RequiredArgsConstructor
@RequestMapping("/comment")
public class CommentController {
private final CommentService commentService;
(다른 패키지 다만들고 private 걸어주십쇼)
보드 컨트롤 패키지에 같이 만들어줍니다
CommentEntity
package com.cho.board.entity;
import com.cho.board.dto.CommentDTO;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@Table(name = "comment_table")
public class CommentEntity extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 20, nullable = false)
private String commentWriter;
@Column
private String commentContents;
/*Board Comment = 1:N */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private BoardEntity boardEntity;//코맨트 엔티티랑 보드 엔티티를 연결하고 1:n 을 지킨다 파일 업로드의 구조랑 유사하다
테이블을 작성하기 위해 만들어줍시다 그리고 밑에 @JoinColumn 이걸 해주셔야 부모 테이블과 연결이됩니다.
CommentDTO
package com.cho.board.dto;
import com.cho.board.entity.CommentEntity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.domain.Page;
import java.time.LocalDateTime;
@Setter
@Getter
@ToString
public class CommentDTO {
private Long id;
private String commentWriter;
private String commentContents;
private Long boardId;
private LocalDateTime commentCreatedTime;
엔티티를 지저분하게 만들수 없기에 DTO를 만들죠
기본 클래스들은 다 만들었고 이제 CommentController 로 가서 댓글 작성 부분을 구현 해야합니다.
CommentController
@PostMapping("/save")
public @ResponseBody String save(@ModelAttribute CommentDTO commentDTO) {
System.out.println("commentDTO =" + commentDTO);
return "요청성공"
}
추가합시다
실행후 댓글 작성을 해보시면
보이면 성공입니다.
기존 게시판에 쓰시던 엔티티에 코멘트 엔티티를 상속 시켜야합니다. 저는 클래스 이름이 BoardEntity입니다
BoardEntity
@OneToMany(mappedBy = "boardEntity", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY)
private List<CommentEntity> commentEntityList = new ArrayList<>();
기존 엔티티에서 추가할 내용입니다 그후 실행을 해보시면
나오면 성공입니다. 실행 화면은 다시 코드 치기 귀찮아서 강의 화면을 캡처했습니다 ㅋㅋ 미안합니다.
그후 다시 CommentController 에서 한줄 코드를 추가합니다
CommentController
@PostMapping("/save")
public ResponseEntity save(@ModelAttribute CommentDTO commentDTO) {
System.out.println("commentDTO =" + commentDTO);
commentService.save(commentDTO);
return "요청 성공"
}
commentService. save를 추가하시면 save 오류가 걸리실 겁니다
서비스 패키지로 가셔서 save 메소드를 추가합시다.
CommentService
public Long save(CommentDTO commentDTO) {
CommentEntity commentEntity = CommentEntity.toSaveEntity(commentDTO);
}
}
toSaveEntity오류도 걸릴겁니다.
코멘트 엔티티로 가서 메소드 다시 만듭시다
CommentEntity
ublic static CommentEntity toSaveEntity(CommentDTO commentDTO, BoardEntity boardEntity) {
CommentEntity commentEntity = new CommentEntity();//클래스 내부는 이렇게 써도 상관없다
commentEntity.setCommentWriter(commentDTO.getCommentWriter());
commentEntity.setCommentContents(commentDTO.getCommentContents());
commentEntity.setBoardEntity(boardEntity);
return commentEntity;
}
다 씁니다 나중에 밖에서 컨트롤러,서비스에서 빼서 쓰기 편하게 하고 보안상 때문에 엔티티에서 정의하고 꺼내씁니다
DTO도 마찬가지 입니다.
최종적으로 CommentEntity 작성입니다
package com.cho.board.entity;
import com.cho.board.dto.CommentDTO;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@Table(name = "comment_table")
public class CommentEntity extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 20, nullable = false)
private String commentWriter;
@Column
private String commentContents;
/*Board Comment = 1:N */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private BoardEntity boardEntity;//코맨트 엔티티랑 보드 엔티티를 연결하고 1:n 을 지킨다 파일 업로드의 구조랑 유사하다
public static CommentEntity toSaveEntity(CommentDTO commentDTO, BoardEntity boardEntity) {
CommentEntity commentEntity = new CommentEntity();//클래스 내부는 이렇게 써도 상관없다
commentEntity.setCommentWriter(commentDTO.getCommentWriter());
commentEntity.setCommentContents(commentDTO.getCommentContents());
commentEntity.setBoardEntity(boardEntity);
return commentEntity;
}
}
CommentService
public Long save(CommentDTO commentDTO) {
/*부모 (BoardEntity)엔티티 조회*/
Optional<BoardEntity> optionalBoardEntity= boardRepository.findById(commentDTO.getBoardId());
if(optionalBoardEntity.isPresent()){
BoardEntity boardEntity = optionalBoardEntity.get();
CommentEntity commentEntity =CommentEntity.toSaveEntity(commentDTO,boardEntity);
return commentRepository.save(commentEntity).getId();
}else {
return null;//부모 엔티티가 없으면 null 발동
}
부모엔티티가 조회가 가능하면 댓글저장 처리가 될거고 아니면 null처리가 되는 코드입니다
CommentController
@PostMapping("/save")
public ResponseEntity save(@ModelAttribute CommentDTO commentDTO) {
System.out.println("commentDTO =" + commentDTO);
Long saveResult = commentService.save(commentDTO);
if (saveResult != null) {//작성 성공하고 끝나면 기존 댓글이 화면에 새로 추가된 댓글이 목록에 다시 화면에 추가된걸 보여줘야한다
//작성 성공 하면 댓글 목록을 가져와 리턴한다.
//댓글 목록: 해당게시글 댓글 전체
List<CommentDTO> commentDTOList = commentService.findAll(commentDTO.getBoardId()); //댓글 목록을 가저올땐 게시글 번호 기준이된다.
} else {
return"작성실패"
}
}
}
findAll(commentDTO.getBoardId()); 이게 댓글 목록을 가져올 때는 게시글 번호기준이 된다는걸 입력하는겁니다
이제 CommentService 에서 findAll 메소드를 만들러 가야합니다.
일단 다시 CommentRepository 로 돌아갑시다
CommentRepository
package com.cho.board.repository;
import com.cho.board.entity.BoardEntity;
import com.cho.board.entity.CommentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CommentRepository extends JpaRepository <CommentEntity, Long>{
// select * from comment_table where board_id=? order by id desc;
List<CommentEntity> findAllByBoardEntityOrderByIdDesc(BoardEntity boardEntity); //대소문자 정말 중요함 안지키면 쿼리가 제대로 생성이 안됨
}
내림차순으로 보드 id 값을 받고 id값을 받을 땐 엔티티로 받아야합니다
findAll 메소드를 만들러갑시다
CommentService
public List<CommentDTO> findAll(Long boardId) {
BoardEntity boardEntity = boardRepository.findById(boardId).get();
List<CommentEntity> commentEntityList = commentRepository.findAllByBoardEntityOrderByIdDesc(boardEntity);
/* EntityList -> DTOList */
List<CommentDTO> commentDTOList = new ArrayList<>();
for (CommentEntity commentEntity: commentEntityList) {
CommentDTO commentDTO = CommentDTO.toCommentDTO(commentEntity, boardId);
commentDTOList.add(commentDTO);
}
return commentDTOList;
}
조건에 부모 엔티티가 들어가야하기 때문에 부모 엔티티를 넣어야합니다 (boardEntity) 그러기 위해서 부모 엔티티를 조회 하는것도 필요합니다
BoardEntity boardEntity = boardRepository.findById(boardId).get();
2번째줄에 저거죠 엔티티 리스트를 가져오고 그리고 DTO 리스트로 변환 하기 위해서 for문을 돌렷습니다
변환 하면서
List<CommentDTO> commentDTOList = new ArrayList<>();
요기 코드에 하나하나 씩 옮겨 담습니다.
이제 toCommentDTO 메소드를 만들어야 합니다
CommentDTO
public static CommentDTO toCommentDTO(CommentEntity commentEntity, Long boardId) {
CommentDTO commentDTO = new CommentDTO();
commentDTO.setId(commentEntity.getId());
commentDTO.setCommentWriter(commentEntity.getCommentWriter());
commentDTO.setCommentContents(commentEntity.getCommentContents());
commentDTO.setCommentCreatedTime(commentEntity.getCreatedTime());
//commentDTO.setBoardId(commentEntity.getBoardEntity().getId()); // Service 메소드에 @Transactional
commentDTO.setBoardId(boardId);
return commentDTO;
}
DTO의 정보를 다 Entity로 반환하는겁니다
최종적으로
CommentDTO
package com.cho.board.dto;
import com.cho.board.entity.CommentEntity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.domain.Page;
import java.time.LocalDateTime;
@Setter
@Getter
@ToString
public class CommentDTO {
private Long id;
private String commentWriter;
private String commentContents;
private Long boardId;
private LocalDateTime commentCreatedTime;
public static CommentDTO toCommentDTO(CommentEntity commentEntity, Long boardId) {
CommentDTO commentDTO = new CommentDTO();
commentDTO.setId(commentEntity.getId());
commentDTO.setCommentWriter(commentEntity.getCommentWriter());
commentDTO.setCommentContents(commentEntity.getCommentContents());
commentDTO.setCommentCreatedTime(commentEntity.getCreatedTime());
//commentDTO.setBoardId(commentEntity.getBoardEntity().getId()); // Service 메소드에 @Transactional
commentDTO.setBoardId(boardId);
return commentDTO;
}
}
DTO 클래스가 완성이 되고 이제 컨트롤러만 남았습니다
CommentController
package com.cho.board.controller;
import com.cho.board.dto.CommentDTO;
import com.cho.board.service.CommentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
@RequiredArgsConstructor
@RequestMapping("/comment")
public class CommentController {
private final CommentService commentService;
@PostMapping("/save")
public ResponseEntity save(@ModelAttribute CommentDTO commentDTO) {
System.out.println("commentDTO =" + commentDTO);
Long saveResult = commentService.save(commentDTO);
if (saveResult != null) {//작성 성공하고 끝나면 기존 댓글이 화면에 새로 추가된 댓글이 목록에 다시 화면에 추가된걸 보여줘야한다
//작성 성공 하면 댓글 목록을 가져와 리턴한다.
//댓글 목록: 해당게시글 댓글 전체
List<CommentDTO> commentDTOList = commentService.findAll(commentDTO.getBoardId()); //댓글 목록을 가저올땐 게시글 번호 기준이된다.
return new ResponseEntity<>(commentDTOList, HttpStatus.OK);
} else {
return new ResponseEntity<>("해당 게시글이 존재하지 않습니다.",HttpStatus.NOT_FOUND);
}
}
}
NOT_FOUND 로 리턴하면서 자연스럽게 ajax에서 에러부분이 동작하도록 할것이다
detail.html (게시판 상세페이지)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>detail</title>
<!-- jquery cdn -->
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
</head>
<body>
<table>
<tr>
<th>id</th>
<td th:text="${board.id}"></td>
</tr>
<tr>
<th>title</th>
<td th:text="${board.boardTitle}"></td>
</tr>
<tr>
<th>writer</th>
<td th:text="${board.boardWriter}"></td>
</tr>
<tr>
<th>date</th>
<td th:text="${board.boardCreatedTime}"></td>
</tr>
<tr>
<th>hits</th>
<td th:text="${board.boardHits}"></td>
</tr>
<tr>
<th>contents</th>
<td th:text="${board.boardContents}"></td>
</tr>
<tr th:if="${board.fileAttached == 1}">
<th>image</th>
<td td th:each="fileName: ${board.storedFileName}">
<img th:src="@{|/static/${board.storedFileName}|}" alt="" width="200" height="200">
</td>
</tr>
</table>
<button onclick="listReq()">목록</button>
<button onclick="updateReq()">수정</button>
<button onclick="deleteReq()">삭제</button>
<!-- 댓글 작성 부분 -->
<div id="comment-write">
<input type="text" id="commentWriter" placeholder="작성자">
<input type="text" id="commentContents" placeholder="내용">
<button id="comment-write-btn" onclick="commentWrite()">댓글작성</button>
</div>
</body>
<script th:inline="javascript">
const commentWrite = () => {
const writer = document.getElementById("commentWriter").value;
const contents = document.getElementById("commentContents").value;
console.log("작성자: ", writer);
console.log("내용: ", contents);
const id = [[${board.id}]]; //게시글번호
$.ajax({
// 요청방식: post, 요청주소: /comment/save, 요청데이터: 작성자, 작성내용, 게시글번호
type: "post",
url: "/comment/save",
data: {
"commentWriter": writer,
"commentContents": contents,
"boardId": id
},
success: function (res) {
console.log("요청성공", res);
let output = "<table>";
output += "<tr><th>댓글번호</th>";
output += "<th>작성자</th>";
output += "<th>내용</th>";
output += "<th>작성시간</th></tr>";
for (let i in res) {
output += "<tr>";
output += "<td>" + res[i].id + "</td>";
output += "<td>" + res[i].commentWriter + "</td>";
output += "<td>" + res[i].commentContents + "</td>";
output += "<td>" + res[i].commentCreatedTime + "</td>";
output += "</tr>";
}
output += "</table>";
document.getElementById('comment-list').innerHTML = output;
document.getElementById('commentWriter').value = '';
document.getElementById('commentContents').value = '';
},
error: function (err) {
console.log("요청실패", err);
}
});
}
const listReq = () => {
console.log("목록 요청");
const page = [[${page}]];
location.href = "/board/paging?page="+page;
}
const updateReq = () => {
console.log("수정 요청");
const id = [[${board.id}]];
location.href = "/board/update/" + id;
}
const deleteReq = () => {
console.log("삭제 요청");
const id = [[${board.id}]];
location.href = "/board/delete/" + id;
}
</script>
</html>
그리고이제 목록 데이터가 잘 넘겨 오는지 확인을 해야합니다.
크롬 F12를 누르고 콘솔을 클릭해서 보면 이렇게 넘어온게 보입니다.
2개를 입력 하시면
그리고 댓글 출력 하는 화면을 다시구현을 다시하면
detail.html + ajax(게시판 상세페이지 최종)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>detail</title>
<!-- jquery cdn -->
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
</head>
<body>
<table>
<tr>
<th>id</th>
<td th:text="${board.id}"></td>
</tr>
<tr>
<th>title</th>
<td th:text="${board.boardTitle}"></td>
</tr>
<tr>
<th>writer</th>
<td th:text="${board.boardWriter}"></td>
</tr>
<tr>
<th>date</th>
<td th:text="${board.boardCreatedTime}"></td>
</tr>
<tr>
<th>hits</th>
<td th:text="${board.boardHits}"></td>
</tr>
<tr>
<th>contents</th>
<td th:text="${board.boardContents}"></td>
</tr>
<tr th:if="${board.fileAttached == 1}">
<th>image</th>
<td td th:each="fileName: ${board.storedFileName}">
<img th:src="@{|/static/${board.storedFileName}|}" alt="" width="200" height="200">
</td>
</tr>
</table>
<button onclick="listReq()">목록</button>
<button onclick="updateReq()">수정</button>
<button onclick="deleteReq()">삭제</button>
<!-- 댓글 작성 부분 -->
<div id="comment-write">
<input type="text" id="commentWriter" placeholder="작성자">
<input type="text" id="commentContents" placeholder="내용">
<button id="comment-write-btn" onclick="commentWrite()">댓글작성</button>
</div>
<!-- 댓글 출력 부분 -->
<div id="comment-list">
<table>
<tr>
<th>댓글번호</th>
<th>작성자</th>
<th>내용</th>
<th>작성시간</th>
</tr>
<tr th:each="comment: ${commentList}">
<td th:text="${comment.id}"></td>
<td th:text="${comment.commentWriter}"></td>
<td th:text="${comment.commentContents}"></td>
<td th:text="${comment.commentCreatedTime}"></td>
</tr>
</table>
</div>
</body>
<script th:inline="javascript">
const commentWrite = () => {
const writer = document.getElementById("commentWriter").value;
const contents = document.getElementById("commentContents").value;
console.log("작성자: ", writer);
console.log("내용: ", contents);
const id = [[${board.id}]]; //게시글번호
$.ajax({
// 요청방식: post, 요청주소: /comment/save, 요청데이터: 작성자, 작성내용, 게시글번호
type: "post",
url: "/comment/save",
data: {
"commentWriter": writer,
"commentContents": contents,
"boardId": id
},
success: function (res) {
console.log("요청성공", res);
let output = "<table>";
output += "<tr><th>댓글번호</th>";
output += "<th>작성자</th>";
output += "<th>내용</th>";
output += "<th>작성시간</th></tr>";
for (let i in res) {
output += "<tr>";
output += "<td>" + res[i].id + "</td>";
output += "<td>" + res[i].commentWriter + "</td>";
output += "<td>" + res[i].commentContents + "</td>";
output += "<td>" + res[i].commentCreatedTime + "</td>";
output += "</tr>";
}
output += "</table>";
document.getElementById('comment-list').innerHTML = output;
document.getElementById('commentWriter').value = '';
document.getElementById('commentContents').value = '';
},
error: function (err) {
console.log("요청실패", err);
}
});
}
const listReq = () => {
console.log("목록 요청");
const page = [[${page}]];
location.href = "/board/paging?page="+page;
}
const updateReq = () => {
console.log("수정 요청");
const id = [[${board.id}]];
location.href = "/board/update/" + id;
}
const deleteReq = () => {
console.log("삭제 요청");
const id = [[${board.id}]];
location.href = "/board/delete/" + id;
}
</script>
</html>
다시 실행하고 댓글을 작성하면
아 참고로 재목은 파일첨부 공부하다가 저렇게 쓴겁니다.
최종 코드 모음집입니다
detail.html (게시판 상세페이지 최종)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>detail</title>
<!-- jquery cdn -->
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
</head>
<body>
<table>
<tr>
<th>id</th>
<td th:text="${board.id}"></td>
</tr>
<tr>
<th>title</th>
<td th:text="${board.boardTitle}"></td>
</tr>
<tr>
<th>writer</th>
<td th:text="${board.boardWriter}"></td>
</tr>
<tr>
<th>date</th>
<td th:text="${board.boardCreatedTime}"></td>
</tr>
<tr>
<th>hits</th>
<td th:text="${board.boardHits}"></td>
</tr>
<tr>
<th>contents</th>
<td th:text="${board.boardContents}"></td>
</tr>
<tr th:if="${board.fileAttached == 1}">
<th>image</th>
<td td th:each="fileName: ${board.storedFileName}">
<img th:src="@{|/static/${board.storedFileName}|}" alt="" width="200" height="200">
</td>
</tr>
</table>
<button onclick="listReq()">목록</button>
<button onclick="updateReq()">수정</button>
<button onclick="deleteReq()">삭제</button>
<!-- 댓글 작성 부분 -->
<div id="comment-write">
<input type="text" id="commentWriter" placeholder="작성자">
<input type="text" id="commentContents" placeholder="내용">
<button id="comment-write-btn" onclick="commentWrite()">댓글작성</button>
</div>
<!-- 댓글 출력 부분 -->
<div id="comment-list">
<table>
<tr>
<th>댓글번호</th>
<th>작성자</th>
<th>내용</th>
<th>작성시간</th>
</tr>
<tr th:each="comment: ${commentList}">
<td th:text="${comment.id}"></td>
<td th:text="${comment.commentWriter}"></td>
<td th:text="${comment.commentContents}"></td>
<td th:text="${comment.commentCreatedTime}"></td>
</tr>
</table>
</div>
</body>
<script th:inline="javascript">
const commentWrite = () => {
const writer = document.getElementById("commentWriter").value;
const contents = document.getElementById("commentContents").value;
console.log("작성자: ", writer);
console.log("내용: ", contents);
const id = [[${board.id}]]; //게시글번호
$.ajax({
// 요청방식: post, 요청주소: /comment/save, 요청데이터: 작성자, 작성내용, 게시글번호
type: "post",
url: "/comment/save",
data: {
"commentWriter": writer,
"commentContents": contents,
"boardId": id
},
success: function (res) {
console.log("요청성공", res);
let output = "<table>";
output += "<tr><th>댓글번호</th>";
output += "<th>작성자</th>";
output += "<th>내용</th>";
output += "<th>작성시간</th></tr>";
for (let i in res) {
output += "<tr>";
output += "<td>" + res[i].id + "</td>";
output += "<td>" + res[i].commentWriter + "</td>";
output += "<td>" + res[i].commentContents + "</td>";
output += "<td>" + res[i].commentCreatedTime + "</td>";
output += "</tr>";
}
output += "</table>";
document.getElementById('comment-list').innerHTML = output;
document.getElementById('commentWriter').value = '';
document.getElementById('commentContents').value = '';
},
error: function (err) {
console.log("요청실패", err);
}
});
}
const listReq = () => {
console.log("목록 요청");
const page = [[${page}]];
location.href = "/board/paging?page="+page;
}
const updateReq = () => {
console.log("수정 요청");
const id = [[${board.id}]];
location.href = "/board/update/" + id;
}
const deleteReq = () => {
console.log("삭제 요청");
const id = [[${board.id}]];
location.href = "/board/delete/" + id;
}
</script>
</html>
CommentController
package com.cho.board.controller;
import com.cho.board.dto.CommentDTO;
import com.cho.board.service.CommentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
@RequiredArgsConstructor
@RequestMapping("/comment")
public class CommentController {
private final CommentService commentService;
@PostMapping("/save")
public ResponseEntity save(@ModelAttribute CommentDTO commentDTO) {
System.out.println("commentDTO =" + commentDTO);
Long saveResult = commentService.save(commentDTO);
if (saveResult != null) {//작성 성공하고 끝나면 기존 댓글이 화면에 새로 추가된 댓글이 목록에 다시 화면에 추가된걸 보여줘야한다
//작성 성공 하면 댓글 목록을 가져와 리턴한다.
//댓글 목록: 해당게시글 댓글 전체
List<CommentDTO> commentDTOList = commentService.findAll(commentDTO.getBoardId()); //댓글 목록을 가저올땐 게시글 번호 기준이된다.
return new ResponseEntity<>(commentDTOList, HttpStatus.OK);
} else {
return new ResponseEntity<>("해당 게시글이 존재하지 않습니다.",HttpStatus.NOT_FOUND);
}
}
}
CommentDTO
package com.cho.board.dto;
import com.cho.board.entity.CommentEntity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.domain.Page;
import java.time.LocalDateTime;
@Setter
@Getter
@ToString
public class CommentDTO {
private Long id;
private String commentWriter;
private String commentContents;
private Long boardId;
private LocalDateTime commentCreatedTime;
public static CommentDTO toCommentDTO(CommentEntity commentEntity, Long boardId) {
CommentDTO commentDTO = new CommentDTO();
commentDTO.setId(commentEntity.getId());
commentDTO.setCommentWriter(commentEntity.getCommentWriter());
commentDTO.setCommentContents(commentEntity.getCommentContents());
commentDTO.setCommentCreatedTime(commentEntity.getCreatedTime());
//commentDTO.setBoardId(commentEntity.getBoardEntity().getId()); // Service 메소드에 @Transactional
commentDTO.setBoardId(boardId);
return commentDTO;
}
}
CommentEntity
package com.cho.board.entity;
import com.cho.board.dto.CommentDTO;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@Table(name = "comment_table")
public class CommentEntity extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 20, nullable = false)
private String commentWriter;
@Column
private String commentContents;
/*Board Comment = 1:N */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private BoardEntity boardEntity;//코맨트 엔티티랑 보드 엔티티를 연결하고 1:n 을 지킨다 파일 업로드의 구조랑 유사하다
public static CommentEntity toSaveEntity(CommentDTO commentDTO, BoardEntity boardEntity) {
CommentEntity commentEntity = new CommentEntity();//클래스 내부는 이렇게 써도 상관없다
commentEntity.setCommentWriter(commentDTO.getCommentWriter());
commentEntity.setCommentContents(commentDTO.getCommentContents());
commentEntity.setBoardEntity(boardEntity);
return commentEntity;
}
}
CommentRepository
package com.cho.board.repository;
import com.cho.board.entity.BoardEntity;
import com.cho.board.entity.CommentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CommentRepository extends JpaRepository <CommentEntity, Long>{
// select * from comment_table where board_id=? order by id desc;
List<CommentEntity> findAllByBoardEntityOrderByIdDesc(BoardEntity boardEntity); //대소문자 정말 중요함 안지키면 쿼리가 제대로 생성이 안됨
}
CommentService
package com.cho.board.service;
import com.cho.board.dto.CommentDTO;
import com.cho.board.entity.BoardEntity;
import com.cho.board.entity.CommentEntity;
import com.cho.board.repository.BoardRepository;
import com.cho.board.repository.CommentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.events.CommentEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final BoardRepository boardRepository;
public Long save(CommentDTO commentDTO) {
/*부모 (BoardEntity)엔티티 조회*/
Optional<BoardEntity> optionalBoardEntity= boardRepository.findById(commentDTO.getBoardId());
if(optionalBoardEntity.isPresent()){
BoardEntity boardEntity = optionalBoardEntity.get();
CommentEntity commentEntity =CommentEntity.toSaveEntity(commentDTO,boardEntity);
return commentRepository.save(commentEntity).getId();
}else {
return null;//부모 엔티티가 없으면 null 발동
}
}
public List<CommentDTO> findAll(Long boardId) {
BoardEntity boardEntity = boardRepository.findById(boardId).get();
List<CommentEntity> commentEntityList = commentRepository.findAllByBoardEntityOrderByIdDesc(boardEntity);
/* EntityList -> DTOList */
List<CommentDTO> commentDTOList = new ArrayList<>();
for (CommentEntity commentEntity: commentEntityList) {
CommentDTO commentDTO = CommentDTO.toCommentDTO(commentEntity, boardId);
commentDTOList.add(commentDTO);
}
return commentDTOList;
}
}
혹시 밑 Entity,Service,DTO에서 메소드를 적으실때 불편하실까봐 추가로 기존 게시판에 있던 메소드들 좀 올려놓습니다.
BoardEntity
public static BoardEntity toSaveEntity(BoardDTO boardDTO) {
BoardEntity boardEntity = new BoardEntity();
boardEntity.setBoardWriter(boardDTO.getBoardWriter());
boardEntity.setBoardPass(boardDTO.getBoardPass());
boardEntity.setBoardTitle(boardDTO.getBoardTitle());
boardEntity.setBoardContents(boardDTO.getBoardContents());
boardEntity.setBoardHits(0);
boardEntity.setFileAttached(0); // 파일 없음.
return boardEntity;
}
//세이브 엔티티랑은 약간의 차이가 있음 setBoardHits 이부분 이랑 setId 가 추가
public static BoardEntity toUpdateEntity(BoardDTO boardDTO) {
BoardEntity boardEntity = new BoardEntity();
boardEntity.setId(boardDTO.getId());
boardEntity.setBoardWriter(boardDTO.getBoardWriter());
boardEntity.setBoardPass(boardDTO.getBoardPass());
boardEntity.setBoardTitle(boardDTO.getBoardTitle());
boardEntity.setBoardContents(boardDTO.getBoardContents());
boardEntity.setBoardHits(boardDTO.getBoardHits());
return boardEntity;
}
//파일 업로드 관련 엔티티
public static BoardEntity toSaveFileEntity(BoardDTO boardDTO) {
BoardEntity boardEntity = new BoardEntity();
boardEntity.setBoardWriter(boardDTO.getBoardWriter());
boardEntity.setBoardPass(boardDTO.getBoardPass());
boardEntity.setBoardTitle(boardDTO.getBoardTitle());
boardEntity.setBoardContents(boardDTO.getBoardContents());
boardEntity.setBoardHits(0);
boardEntity.setFileAttached(1); // 파일 없음.
return boardEntity;
}
}
BoardDTO
//위에있는거 DTO 를 엔티티로 넘기는 작업이다 복잡한거 없다.
public static BoardDTO toBoardDTO(BoardEntity boardEntity) {
BoardDTO boardDTO = new BoardDTO();
boardDTO.setId(boardEntity.getId());
boardDTO.setBoardWriter(boardEntity.getBoardWriter());
boardDTO.setBoardPass(boardEntity.getBoardPass());
boardDTO.setBoardTitle(boardEntity.getBoardTitle());
boardDTO.setBoardContents(boardEntity.getBoardContents());
boardDTO.setBoardHits(boardEntity.getBoardHits());
boardDTO.setBoardCreatedTime(boardEntity.getCreatedTime());
boardDTO.setBoardUpdatedTime(boardEntity.getUpdatedTime());
//파일이 없다면
if(boardEntity.getFileAttached()== 0){
boardDTO.setFileAttached(boardEntity.getFileAttached()); //0
//파일이 있는경우
}else{
boardDTO.setFileAttached(boardEntity.getFileAttached()); // 1
List<String> originalFileNameList = new ArrayList<>();
List<String> storedFileNameList = new ArrayList<>();
for (BoardFileEntity boardFileEntity: boardEntity.getBoardFileEntityList()) {
originalFileNameList.add(boardFileEntity.getOriginalFileName());
storedFileNameList.add(boardFileEntity.getStoredFileName());
// BoardDTO의 originalFileName이 List이기 때문에 add()를 이용하여
// boardFileEntity에 있는 originalFileName을 옮겨 담음.
}
boardDTO.setOriginalFileName(originalFileNameList);
boardDTO.setStoredFileName(storedFileNameList);
}
return boardDTO;
BoardService
@Transactional
public List<BoardDTO> findAll() {//파인드 올은 대부분 엔티티 한태서 온다.
List<BoardEntity> boardEntityList = boardRepository.findAll();
List<BoardDTO> boardDTOList = new ArrayList<>();
for (BoardEntity boardEntity: boardEntityList) {
boardDTOList.add(BoardDTO.toBoardDTO(boardEntity));
}
@Transactional
public BoardDTO findById(Long id) {
Optional<BoardEntity> optionalBoardEntity = boardRepository.findById(id);
if (optionalBoardEntity.isPresent()) {
BoardEntity boardEntity = optionalBoardEntity.get();
BoardDTO boardDTO = BoardDTO.toBoardDTO(boardEntity);
return boardDTO;
} else {
return null;
'Spring > 게시판 CRUD + 추가기능들' 카테고리의 다른 글
게시글 수정 (0) | 2023.03.29 |
---|---|
게시글 상세조회 (0) | 2023.03.28 |
게시글 목록 (0) | 2023.03.28 |
게시글 작성 (0) | 2023.03.27 |
mysql db id 연결 (0) | 2023.02.14 |