1) SpringBoot
1-1) 복습 예제
1-1-1) 글 읽기 기능 업데이트 / 글 수정 및 삭제 / 댓글 작성 및 삭제 기능 구현
1) SpringBoot
1-1) 복습 예제
1-1-1) 글 읽기 기능 업데이트 / 글 수정 및 삭제 / 댓글 작성 및 삭제 기능 구현
(1) [src/main/resources] - [templates] 안에 home.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>web5</title>
</head>
<body>
<h1>[ web5 ]</h1>
<p sec:authorize="isAuthenticated()">
<span th:text="${#authentication.name}"></span>
님 환영합니다~~!
</p>
<div sec:authorize="not isAuthenticated()">
<p>
<a th:href="@{/member/joinForm}">회원가입</a>
</p>
<p>
<a th:href="@{/member/loginForm}">로그인</a>
</p>
</div>
<div sec:authorize="isAuthenticated()">
<p>
<a th:href="@{/member/logout}">로그아웃</a>
</p>
<p>
<a th:href="@{/member/info}">개인정보 수정</a>
</p>
</div>
<p>
<a th:href="@{/board/list}">게시판</a>
</p>
</body>
</html>
(2) DB(MySQL)에 web5_reply 테이블(댓글) 아래와 같이 신규 생성
-- DB 구조
-- 게시판 글(테이블명 : web5_reply)
create table web5_reply(
-- 컬럼명 자료형 제약조건 설명
reply_num int auto_increment primary key, -- 리플 일련번호
board_num int, -- 게시글 번호(외래키 : web5_board 테이블 참조)
member_id varchar(30), -- 작성자 ID(외래키 : web5_member 테이블 참조)
contents varchar(2000) not null, -- 리플 내용
create_date timestamp default current_timestamp, -- 작성 시간
constraint foreign key (board_num)
references web5_board(board_num) on delete cascade,
constraint foreign key (member_id)
references web5_member(member_id) on delete set null
);
select * from web5_reply;
insert into web5_reply(board_num, member_id, contents) values(51, 'fghj1234', '리플 내용');
insert into web5_reply(board_num, member_id, contents) values(51, 'hjkl1234', '리플 내용2');
insert into web5_reply(board_num, member_id, contents) values(51, 'fghj1234', '리플 내용3');
insert into web5_reply(board_num, member_id, contents) values(51, 'fghj1234', '리플 내용4');
-- delete from web5_member where member_name = '김종국';
drop table web5_reply;
commit;
(3) [src/main/java] - [net.datasa.web5.domain.entity] 안에 ReplyEntity.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.domain.entity;
import java.time.LocalDateTime;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
* 리플 entity
* */
@Builder
// @Data
@Getter
@Setter
@ToString(exclude = "board")
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="web5_reply")
@EntityListeners(AuditingEntityListener.class)
public class ReplyEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="reply_num")
private Integer replyNum;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="board_num", referencedColumnName = "board_num")
BoardEntity board;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="member_id", referencedColumnName = "member_id")
MemberEntity member;
// 리플 내용
@Column(name="contents", nullable = false, length = 2000)
private String contents;
// 작성 시간
@CreatedDate
@Column(name="create_date", columnDefinition = "timestamp default current_timestamp")
private LocalDateTime createDate;
}
(4) [src/main/java] - [net.datasa.web5.domain.entity] 안에 BoardEntity.java 파일 아래와 같이 수정
package net.datasa.web5.domain.entity;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="web5_board")
@EntityListeners(AuditingEntityListener.class)
public class BoardEntity {
/*
board_num int auto_increment primary key, -- 게시글 일련번호
member_id varchar(30), -- 작성자 ID(외래키 : web5_member 테이블 참조)
title varchar(1000) not null, -- 글제목
contents text not null, -- 글내용
view_count int default 0, -- 조회수
like_count int default 0, -- 추천수
original_name varchar(300), -- 첨부파일 원래 이름
file_name varchar(100), -- 첨부파일 저장된 이름
create_date timestamp default current_timestamp, -- 작성 시간
update_date timestamp default current_timestamp
on update current_timestamp, -- 수정 시간
constraint foreign key (member_id)
references web5_member(member_id) on delete set null
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="board_num")
private Integer boardNum;
// 작성자 아이디
// @Column(name="member_id", length = 30)
// private String memberId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="member_id", referencedColumnName = "member_id")
MemberEntity member;
// 글 제목
@Column(name="title", nullable = false, length = 1000)
private String title;
// 글 내용
@Column(name="contents", nullable = false, columnDefinition = "text")
private String contents;
// 조회수
@Column(name="view_count", columnDefinition = "int default 0")
private Integer viewCount;
// 추천수
@Column(name="like_count", columnDefinition = "int default 0")
private Integer likeCount;
// 첨부파일 원래 이름
@Column(name="original_name", length = 300)
private String originalName;
// 첨부파일 저장된 이름
@Column(name="file_name", length = 100)
private String fileName;
// 작성 시간
@CreatedDate
@Column(name="create_date", columnDefinition = "timestamp default current_timestamp")
private LocalDateTime createDate;
// 수정 시간
@LastModifiedDate
@Column(name="update_date", columnDefinition = "timestamp default current_timestamp on update current_timestamp")
private LocalDateTime updateDate;
// 해당 글에 달린 리플들 목록
// 여기서 'mappedBy = "board"'는 ReplyEntity의 "board(BoardEntity 타입의 객체) 변수"를 가리킴
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ReplyEntity> replyList;
}
(5) [src/main/java] - [net.datasa.web5.domain.dto] 안에 BoardDTO.java 파일 아래와 같이 수정
package net.datasa.web5.domain.dto;
import java.time.LocalDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 게시글 정보 DTO
* */
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BoardDTO {
// Wrapper Class(Integer 등) : 기본 자료형을 포장해서 객체로 만들어 줌
private Integer boardNum; // 게시글 일련번호
private String memberId; // 작성자 아이디(외래키)
private String memberName; // 작성자 이름(외래키를 통해 가지고 온 내용 중 이름)
private String title; // 글 제목
private String contents; // 글 내용
private Integer viewCount; // 조회수
private Integer likeCount; // 추천수
private String originalName; // 첨부파일 원래 이름
private String fileName; // 첨부파일 저장된 이름
private LocalDateTime createDate ; // 작성 시간
private LocalDateTime updateDate; // 수정 시간
// 리플목록
private List<ReplyDTO> replyList;
}
(6) [src/main/java] - [net.datasa.web5.domain.dto] 안에 ReplyDTO.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.domain.dto;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReplyDTO {
private Integer replyNum; // 리플 일련번호
private Integer boardNum; // 게시글 일련번호(외래키)
private String memberId; // 작성자 아이디(외래키)
private String memberName; // 작성자 이름(외래키)
private String contents; // 리플 내용
private LocalDateTime createDate ; // 작성 시간
}
(7) [src/main/java] - [net.datasa.web5.repository] 안에 ReplyRepository.java 파일 생성(반드시 Interface로 생성할 것!!) 후 아래와 같이 작성
package net.datasa.web5.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import net.datasa.web5.domain.entity.ReplyEntity;
/**
* 리플 관련 repository
* */
@Repository
public interface ReplyRepository extends JpaRepository<ReplyEntity, Integer> {
}
(8) [src/main/java] - [net.datasa.web5.controller] 안에 BoardController.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.BoardDTO;
import net.datasa.web5.domain.dto.ReplyDTO;
import net.datasa.web5.security.AuthenticatedUser;
import net.datasa.web5.service.BoardService;
@Slf4j
@RequestMapping("board")
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
@Value("${board.pageSize}")
int pageSize;
@Value("${board.linkSize}")
int linkSize;
@Value("${board.uploadPath}")
String uploadPath;
// 메인화면에서 "board/list" 경로를 클릭했을 때 처리하는 메소드
// templates/boardView/list.html 파일로 포워딩
// 로그인 안 한 상태에서 해당 페이지의 "게시판"이라는 제목이 보여야 함
/**
* 글 목록 보기
* @return 글 목록 출력 HTML 파일
* */
@GetMapping("list")
public String list(Model model,
@RequestParam(name="page", defaultValue = "1") int page,
@RequestParam(name="searchType", defaultValue = "") String searchType,
@RequestParam(name="searchWord", defaultValue = "") String searchWord) {
log.debug("properties의 값 : pageSize={}, linkSize={}, uploadPath={}",
pageSize, linkSize, uploadPath);
log.debug("요청 파라미터 : page={}, searchType={}, searchWord={}",
page, searchType, searchWord);
// 서비스로 (페이지, 페이지당 글 수, 검색조건, 검색어) 전달하여 결과를 Page 객체로 받음
Page<BoardDTO> boardPage = boardService.getList(page, pageSize, searchType, searchWord);
// Page 객체 내의 정보들
log.debug("목록정보 getContent() : {}", boardPage.getContent());
log.debug("현재페이지 getNumber() : {}", boardPage.getNumber());
log.debug("전체 개수 getTotalElements() : {}", boardPage.getTotalElements());
log.debug("전체 페이지수 getTotalPages() : {}", boardPage.getTotalPages());
log.debug("한페이지당 글 수 getSize() : {}", boardPage.getSize());
log.debug("이전페이지 존재 여부 hasPrevious() : {}", boardPage.hasPrevious());
log.debug("다음페이지 존재 여부 hasNext() : {}", boardPage.hasNext());
// HTML로 포워딩하기 전에 모델에 필요 정보들을 저장
model.addAttribute("boardPage", boardPage);
model.addAttribute("page", page);
model.addAttribute("linkSize", linkSize);
model.addAttribute("searchType", searchType);
model.addAttribute("searchWord", searchWord);
// HTML로 포워딩
return "boardView/list";
/*
// 서비스에서 전체 글 목록 전달받음
List<BoardDTO> boardList = boardService.getList(searchWord);
log.debug("전달된 글 목록 : {}", boardList);
// 글 목록을 모델에 저장하고 HTML로 포워딩하여 출력
model.addAttribute("boardList", boardList);
return "boardView/list";
*/
}
/**
* 글쓰기 폼으로 이동
* @return 글쓰기 폼을 출력하는 HTML 파일
* */
@GetMapping("write")
public String write() {
return "boardView/writeForm";
}
@PostMapping("write")
public String write(@AuthenticationPrincipal AuthenticatedUser user,
@ModelAttribute BoardDTO boardDTO) {
log.debug("전달된 글정보 : {}", boardDTO);
boardDTO.setMemberId(user.getUsername());
log.debug("로그인한 아이디 추가한 글정보 : {}", boardDTO);
// 서비스로 전달하여 저장
boardService.saveWrite(boardDTO);
return "redirect:list";
}
@GetMapping("read")
public String read(@RequestParam("boardNum") Integer boardNum,
Model model) {
try {
BoardDTO dto = boardService.getBoard(boardNum);
log.debug("선택한 글 정보 : {}", dto);
// 해당 게시글을 모델에 저장하고 HTML로 포워딩하여 출력
model.addAttribute("boardDTO", dto);
return "boardView/readForm";
} catch (Exception e) {
// e.printStackTrace() : 예외가 발생할 시 Console에 에러를 출력해주는 역할을 함
e.printStackTrace();
return "redirect:list";
}
}
/**
* 본인 게시글 삭제
* @param boardNum 삭제할 글 번호
* @param user 로그인 정보
* @return 게시판 글 목록 보기 경로
* */
@GetMapping("delete")
public String delete(@RequestParam("boardNum") Integer boardNum,
@AuthenticationPrincipal AuthenticatedUser user) {
// 삭제할 글 번호와 로그인한 아이디를 서비스로 전달하여 본인 글인 경우에만 삭제
boardService.delete(boardNum, user.getUsername());
return "redirect:list";
}
// 글 수정
@GetMapping("update")
public String update(@RequestParam("boardNum") Integer boardNum,
Model model) {
BoardDTO dto = boardService.getBoard(boardNum);
log.debug("수정 선택한 글 정보 : {}", dto);
model.addAttribute("board", dto);
return "boardView/updateForm";
}
// update form에서 글 내용 수정하고 "수정" 버튼 누르면 받아서 수정된 글 저장
@PostMapping("update")
public String update(@ModelAttribute BoardDTO boardDTO,
@AuthenticationPrincipal AuthenticatedUser user) {
log.debug("수정한 boardDTO : {}", boardDTO);
boardService.update(boardDTO, user.getUsername());
return "redirect:list";
}
// read form에서 리플 입력란에 내용 입력 후 "확인" 버튼 누르면 새로 입력한 리플 저장
@PostMapping("rippleWrite")
public String rippleWrite(@ModelAttribute ReplyDTO replyDTO,
@AuthenticationPrincipal AuthenticatedUser user) {
replyDTO.setMemberId(user.getUsername());
boardService.rippleWrite(replyDTO);
log.debug("새로 입력한 replyDTO : {}", replyDTO);
return "redirect:list";
}
// 리플 삭제
@GetMapping("rippleDelete")
public String rippleDelete(@RequestParam("replyNum") Integer replyNum,
@AuthenticationPrincipal AuthenticatedUser user) {
// 삭제할 리플 번호와 로그인한 아이디를 서비스로 전달하여 본인 글인 경우에만 삭제
boardService.rippleDelete(replyNum, user.getUsername());
return "redirect:list";
}
}
(9) [src/main/java] - [net.datasa.web5.service] 안에 BoardService.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import jakarta.persistence.EntityNotFoundException;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.BoardDTO;
import net.datasa.web5.domain.dto.ReplyDTO;
import net.datasa.web5.domain.entity.BoardEntity;
import net.datasa.web5.domain.entity.MemberEntity;
import net.datasa.web5.domain.entity.ReplyEntity;
import net.datasa.web5.repository.BoardRepository;
import net.datasa.web5.repository.MemberRepository;
import net.datasa.web5.repository.ReplyRepository;
/**
* 게시판 관련 서비스
* */
@Slf4j
@RequiredArgsConstructor
@Service
// @Transactional : Commit, Rollback을 감지하는 역할을 하는 Annotation
@Transactional
public class BoardService {
private final BoardRepository boardRepository;
private final MemberRepository memberRepository;
private final ReplyRepository replyRepository;
/**
* 게시판 글 저장
* @param boardDTO 게시판 글 정보
* */
public void saveWrite(BoardDTO boardDTO) {
MemberEntity memberEntity = memberRepository.findById(boardDTO.getMemberId())
.orElseThrow(() -> new EntityNotFoundException("아이디가 없습니다."));
BoardEntity entity = BoardEntity.builder()
// DTO로 전달받은 값들을 Entity에 세팅
// .memberId(boardDTO.getMemberId())
.member(memberEntity)
.title(boardDTO.getTitle())
.contents(boardDTO.getContents())
// 기타 추가 데이터를 Entity에 세팅
.viewCount(0)
.likeCount(0)
.build();
log.debug("저장되는 entity : {}", entity);
// DB에 저장
boardRepository.save(entity);
}
/**
* 검색 결과 글 목록을 지정한 한 페이지 분량의 Page 객체로 리턴
* @param page 현재 페이지
* @param pageSize 페이지당 글 수
* @param searchType 검색조건(제목 검색: title, 본문 검색: contents, 작성자 검색: id)
* @param searchWord 검색어
* @return 게시글 목록 정보
* */
public Page<BoardDTO> getList(int page, int pageSize, String searchType, String searchWord) {
// 조회 조건을 담은 Pageable 객체 생성
// page의 경우, 배열의 인덱스처럼 0부터 시작하기에 1페이지(인덱스로 0페이지에 해당)를 가져오기 위해 "page - 1"로 작성함
Pageable p = PageRequest.of(page - 1, pageSize, Sort.Direction.DESC, "boardNum");
Page<BoardEntity> entityPage;
// repository의 메소드로 Pageable 전달하여 조회. Page 리턴받음.
if (searchType.equals("title")) {
entityPage = boardRepository.findByTitleContaining(searchWord, p);
} else if (searchType.equals("contents")) {
entityPage = boardRepository.findByContentsContaining(searchWord, p);
} else if (searchType.equals("id")) {
entityPage = boardRepository.findByMember_MemberId(searchWord, p);
} else {
entityPage = boardRepository.findAll(p);
}
log.debug("조회된 결과 Entity 페이지 : {}", entityPage.getContent());
// entityPage 객체 내의 Entity들을 DTO 객체로 변환하여 새로운 Page 객체 생성
Page<BoardDTO> dtoPage = entityPage.map(this::convertToDTO);
return dtoPage;
}
private BoardDTO convertToDTO(BoardEntity entity) {
return BoardDTO.builder()
.boardNum(entity.getBoardNum())
.memberId(entity.getMember().getMemberId())
.memberName(entity.getMember().getMemberName())
.title(entity.getTitle())
.contents(entity.getContents())
.viewCount(entity.getViewCount())
.likeCount(entity.getLikeCount())
.originalName(entity.getOriginalName())
.fileName(entity.getFileName())
.createDate(entity.getCreateDate())
.updateDate(entity.getUpdateDate())
.build();
}
// localhost:8888/board/read?boardNum=1
public BoardDTO getBoard(Integer boardNum) {
BoardEntity entity = boardRepository.findById(boardNum)
.orElseThrow(() -> new EntityNotFoundException("글이 없습니다."));
log.debug("조회한 Entity : {}", entity);
BoardDTO dto = convertToDTO(entity);
// Entity 내의 replyList를 ArrayList<ReplyDTO>로 변환해서 dto에 추가
List<ReplyDTO> replyList = new ArrayList<>();
for (ReplyEntity replyEntity : entity.getReplyList()) {
ReplyDTO replyDTO = ReplyDTO.builder()
.replyNum(replyEntity.getReplyNum())
.boardNum(replyEntity.getBoard().getBoardNum())
.memberId(replyEntity.getMember().getMemberId())
.memberName(replyEntity.getMember().getMemberName())
.contents(replyEntity.getContents())
.createDate(replyEntity.getCreateDate())
.build();
replyList.add(replyDTO);
}
dto.setReplyList(replyList);
return dto;
}
public void delete(Integer boardNum, String username) {
BoardEntity entity = boardRepository.findById(boardNum)
.orElseThrow(() -> new EntityNotFoundException("글이 없습니다."));
if (!username.equals(entity.getMember().getMemberId())) {
throw new RuntimeException("본인이 작성한 글이 아닙니다.");
}
log.debug("삭제할 글정보 : {}", entity);
boardRepository.delete(entity);
}
public void update(BoardDTO dto, String username) {
BoardEntity entity = boardRepository.findById(dto.getBoardNum())
.orElseThrow(() -> new EntityNotFoundException("글이 없습니다."));
if (!username.equals(entity.getMember().getMemberId())) {
throw new RuntimeException("본인이 작성한 글이 아닙니다.");
}
// 값 수정
entity.setTitle(dto.getTitle());
entity.setContents(dto.getContents());
entity.setFileName(dto.getFileName());
log.debug("수정한 Entity : {}", entity);
// 저장
boardRepository.save(entity);
}
public void rippleWrite(ReplyDTO replyDTO) {
MemberEntity memberEntity = memberRepository.findById(replyDTO.getMemberId())
.orElseThrow(() -> new EntityNotFoundException("아이디가 없습니다."));
BoardEntity boardEntity = boardRepository.findById(replyDTO.getBoardNum())
.orElseThrow(() -> new EntityNotFoundException("글이 없습니다."));
ReplyEntity entity = ReplyEntity.builder()
// DTO로 전달받은 값들을 Entity에 세팅
.member(memberEntity)
.board(boardEntity)
.contents(replyDTO.getContents())
.build();
log.debug("저장되는 entity : {}", entity);
// DB에 저장
replyRepository.save(entity);
}
public void rippleDelete(Integer replyNum, String username) {
ReplyEntity entity = replyRepository.findById(replyNum)
.orElseThrow(() -> new EntityNotFoundException("리플이 없습니다."));
if (!username.equals(entity.getMember().getMemberId())) {
throw new RuntimeException("본인이 작성한 글이 아닙니다.");
}
log.debug("삭제할 글정보 : {}", entity);
replyRepository.delete(entity);
}
}
(10) [src/main/resources] - [templates.boardView] 안에 list.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>게시판</title>
<style>
#listArea {
width: 1000px;
margin: 0 auto;
text-align: center;
}
a {
text-decoration-line: none;
color: black;
}
h1 {
text-align: center;
}
table, th, tr, td {
border: 2px solid black;
border-collapse: collapse;
padding: 10px;
text-align: center;
}
.white {
border: none;
}
.head {
border: none;
}
th {
background-color: grey;
color: white;
}
td {
width: 100px;
}
.title {
width: 300px;
}
.date {
width: 130px;
}
#pageArea {
margin: 30px 0;
}
</style>
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<!-- 페이지 이동 스크립트 -->
<script>
function pagingFormSubmit(currentPage) {
$('#page').val(currentPage);
$('#pagingForm').submit();
}
</script>
</head>
<body>
<div id="listArea">
<h1>
<a th:href="@{/board/list}">[ 게시판 ]</a>
</h1>
<!--/*
<div>[[${boardPage.getContent()}]]</div>
<div>[[${boardPage.getTotalElements()}]]</div>
<div>[[${boardPage.getTotalPages()}]]</div>
<div>[[${boardPage.getNumber()}]]</div>
<div>[[${boardPage.getSize()}]]</div>
<div>[[${boardPage.hasPrevious()}]]</div>
<div>[[${boardPage.hasNext()}]]</div>
*/-->
<!-- 글목록 출력 영역 -->
<div id="list">
<table style="border: none;">
<tr class="white">
<td class="white">
전체 <span th:text="${boardPage.totalElements}"></span>
</td>
<td class="white">페이지 <span th:text="${page}"></span> of <span th:text="${boardPage.getTotalPages()}"></span></td>
<td class="white" colspan="4"></td>
<td class="head">
<a sec:authorize="isAuthenticated()" th:href="@{/board/write}">글쓰기</a>
<a th:href="@{/}">HOME</a>
</td>
</tr>
<tr>
<th>번호</th>
<th class="title">제목</th>
<th>작성자 ID</th>
<th>작성자명</th>
<th>조회수</th>
<th class="date">작성일</th>
<th class="date">수정일</th>
</tr>
<tr th:each="board, n : ${boardPage}">
<td th:text="${board.boardNum}"></td>
<td>
<!-- /board/read?boardNum=1 -->
<!--/* <a th:text="${board.title}" th:href="@{/board/read(boardNum=${board.boardNum})}" class="title"></a> */-->
<a th:text="${board.title}" th:href="|/board/read?boardNum=${board.boardNum}|" class="title"></a>
</td>
<td th:text="${board.memberId}"></td>
<td th:text="${board.memberName}"></td>
<td th:text="${board.viewCount}"></td>
<!-- LocalDateTime 타입으로 가지고 온 값은 아래와 같이 "temporals.format()"을 써서 형식을 지정해야 함 -->
<td th:text="${#temporals.format(board.createDate, 'yy.MM.dd HH:mm')}" class="date"></td>
<td th:text="${#temporals.format(board.updateDate, 'yy.MM.dd HH:mm')}" class="date"></td>
</tr>
</table>
</div>
<div id="pageArea">
<a
th:if="${boardPage.hasPrevious() and boardPage.getNumber() > 4}"
th:href="|javascript:pagingFormSubmit(${page - 5})|">◁◁</a>
<a
th:if="${boardPage.hasPrevious()}"
th:href="|javascript:pagingFormSubmit(${page - 1})|">◀</a>
<!-- 페이지 이동 링크 -->
<span
th:if="${boardPage.getTotalPages() > 0}"
th:each="counter : ${#numbers.sequence((page - linkSize < 1 ? 1 : page - linkSize), (page + linkSize > boardPage.getTotalPages() ? boardPage.getTotalPages() : page + linkSize))}">
<th:block th:if="${counter == page}"><b></th:block>
<a th:text="${counter}" th:href="|javascript:pagingFormSubmit(${counter})|"></a>
<th:block th:if="${counter == page}"></b></th:block>
</span>
<a
th:if="${boardPage.hasNext()}"
th:href="|javascript:pagingFormSubmit(${page + 1})|">▶</a>
<a
th:if="${boardPage.hasNext() and boardPage.getNumber() < boardPage.getTotalPages() - 5}"
th:href="|javascript:pagingFormSubmit(${page + 5})|">▷▷</a>
</div>
<!-- 검색폼 -->
<div id="searchArea">
<form id="pagingForm" method="get" th:action="@{/board/list}">
<input type="hidden" name="page" id="page" />
<select id="type" name="searchType">
<option value="title" th:selected="${searchType == 'title'}">제목</option>
<option value="contents" th:selected="${searchType == 'contents'}">본문</option>
<option value="id" th:selected="${searchType == 'id'}">작성자ID</option>
</select>
<input type="text" name="searchWord" th:value="${searchWord}">
<input type="button" onclick="pagingFormSubmit(1)" value="검색">
</form>
</div>
</div>
</body>
</html>
(11) [src/main/resources] - [templates.boardView] 안에 readForm.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>게시글 읽기</title>
<style>
a {
text-decoration-line: none;
color: black;
font-weight: bold;
}
#readArea {
width: 900px;
margin: 0 auto;
text-align: center;
}
#readForm {
display: flex;
flex-direction: column;
justify-content: center;
}
#formArea {
display: flex;
justify-content: center;
margin-bottom: 50px;
}
#boardRead {
width: 800px;
}
#boardRead, #boardRead tr, #boardRead td {
border: 2px solid black;
border-collapse: collapse;
padding: 10px;
margin: 20px 0 15px 0;
}
th {
width: 100px;
height: 40px;
background-color: grey;
color: white;
}
#boardRead td {
width: 500px;
height: 40px;
text-align: left;
}
#rippleWrite {
margin: 50px 0;
}
#rippleList {
width: 800px;
margin: 0 auto;
border-left: 2px solid black;
border-collapse: collapse;
}
#rippleList .visible {
border: 2px solid black;
border-collapse: collapse;
}
#rippleList th {
width: 120px;
}
#rippleContents {
width: 470px;
}
#rippleDate {
width: 150px;
}
#rippleDel {
border: none;
}
</style>
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script></script>
</head>
<body>
<div id="readArea">
<h1>[ 게시글 읽기 ]</h1>
<div id="readForm">
<div id="formArea">
<table id="boardRead">
<tr>
<th>작성자</th>
<td th:text="${boardDTO.memberId}"></td>
</tr>
<tr>
<th>작성일</th>
<td th:text="${#temporals.format(boardDTO.createDate, 'yyyy.MM.dd HH:mm:ss')}"></td>
</tr>
<tr>
<th>조회수</th>
<td th:text="${boardDTO.viewCount}"></td>
</tr>
<tr>
<th>제목</th>
<td th:text="${boardDTO.title}"></td>
</tr>
<tr>
<th>내용</th>
<td th:text="${boardDTO.contents}"></td>
</tr>
<tr>
<th>파일첨부</th>
<td th:text="${boardDTO.fileName}"></td>
</tr>
</table>
</div>
<!-- 글 수정/삭제(본인글일 때만 보임) -->
<div id="btnArea" th:if="${#authentication.name == boardDTO.memberId}">
<a th:href="|/board/update?boardNum=${boardDTO.boardNum}|" id="updateBtn">
<input type="button" id="updateBtn" value="수정">
</a>
<a th:href="|/board/delete?boardNum=${boardDTO.boardNum}|" id="deleteBtn">
<input type="button" id="deleteBtn" value="삭제">
</a>
</div>
<!-- 리플 작성 폼(로그인했을 때만 보임) -->
<div id="rippleArea" sec:authorize="isAuthenticated()">
<form th:action="@{/board/rippleWrite}" method="post" id="rippleWrite">
<span>리플내용 </span>
<input type="text" name="contents" id="contents">
<!-- controller의 rippleWrite 메소드(PostMapping)에 글 번호를 넘겨주기 위해 추가한 내용이다. -->
<input type="hidden" name="boardNum" id="boardNum" th:value="${boardDTO.boardNum}" />
<!-- <button>저장</button> -->
<input type="submit" value="저장" />
</form>
</div>
<!-- 리플 목록 출력(다 보임) -->
<table id="rippleList">
<tr th:each="reply : ${boardDTO.replyList}">
<th th:text="${reply.memberName}" class="visible"></th>
<td th:text="${reply.contents}" id="rippleContents" class="visible"></td>
<td th:text="${#temporals.format(reply.createDate, 'yy.MM.dd HH:mm')}" id="rippleDate" class="visible"></td>
<td th:if="${#authentication.name == reply.memberId}" sec:authorize="isAuthenticated()" id="rippleDel">
<a th:href="|/board/rippleDelete?replyNum=${reply.replyNum}|" id="rippleDelBtn">X</a>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
(12) [src/main/resources] - [templates.boardView] 안에 updateForm.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>글수정</title>
<style>
#updateArea {
width: 900px;
margin: 0 auto;
text-align: center;
}
#updateForm {
display: flex;
flex-direction: column;
justify-content: center;
}
#formArea {
display: flex;
justify-content: center;
}
table, tr, td {
border: 2px solid black;
border-collapse: collapse;
padding: 10px;
margin: 20px 0 15px 0;
}
th {
width: 100px;
background-color: grey;
color: white;
}
td {
width: 300px;
text-align: left;
}
td > .writeContent {
width: 300px;
}
td > textarea {
height: 250px;
}
</style>
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script>
$(document).ready(function() {
$("#writeForm").submit(check);
if ("[[${msg}]]") {
alert("[[${msg}]]");
}
});
function check() {
let title = $("#title").val();
let contents = $("#contents").val();
if (title.length < 7) {
alert("제목은 최소 7자 이상으로 반드시 입력해주세요!!");
$("#title").focus();
$("#title").val('');
return false;
}
if (contents.length < 20) {
alert("내용은 20자 이상으로 반드시 입력해주세요!!");
$("#contents").focus();
$("#contents").val('');
return false;
}
return true;
}
</script>
</head>
<body>
<div id="updateArea">
<h1>[ 글수정 ]</h1>
<form th:action="@{/board/update}" method="post" id="updateForm">
<div id="formArea">
<!-- controller의 update 메소드(PostMapping)에 글 번호, 작성자 ID를 넘겨주기 위해 추가한 내용이다. -->
<input type="hidden" name="boardNum" id="boardNum" th:value="${board.boardNum}" />
<input type="hidden" name="memberId" id="memberId" th:value="${board.memberId}" />
<table>
<tr>
<th>
<label for="title">제목</label>
</th>
<td>
<input type="text" name="title" id="title" th:value="${board.title}" class="writeContent" />
</td>
</tr>
<tr>
<th>
<label for="contents">내용</label>
</th>
<td>
<textarea name="contents" id="contents" class="writeContent">[[${board.contents}]]</textarea>
</td>
</tr>
<tr>
<th>파일첨부</th>
<td>
<input type="file" name="file" id="file" th:value="${board.fileName}" class="writeContent" />
</td>
</tr>
</table>
</div>
<div id="btnArea">
<input type="submit" value="수정" />
</div>
</form>
</div>
</body>
</html>
(13) 결과 화면
첫 접속 화면 : 메인 페이지, 게시판 글 목록 페이지는 로그인 안해도 볼 수 있음
각각의 글 정보 화면 : 글 수정 및 삭제, 댓글 작성 및 삭제 가능함
- 글 수정 및 삭제, 댓글 작성 및 삭제를 하려면 로그인해야 함
- 본인 글만 수정 및 삭제를 할 수 있으며, 댓글 역시 본인 댓글만 삭제할 수 있음
글 수정 및 삭제(53번 글)
53번 글 삭제
댓글 작성 및 삭제(51번 글)
"리플 내용4" 댓글 삭제
'SpringBoot' 카테고리의 다른 글
SpringBoot(30) - 복습 예제(글 작성 및 수정 파일 첨부 기능 추가) (0) | 2024.08.06 |
---|---|
SpringBoot(29) - 복습 예제(글 읽기 / 글 수정 및 삭제 / 댓글 작성 및 삭제 기능 업데이트) (0) | 2024.08.05 |
SpringBoot(27) - 복습 예제(게시판 글 목록 페이징 / 검색 선택 / 글 읽기) (0) | 2024.08.01 |
SpringBoot(26) - 복습 예제(게시판 글 목록 보기) (0) | 2024.07.31 |
SpringBoot(25) - 복습 예제(글쓰기) (0) | 2024.07.30 |