2024. 4. 29. 12:44ㆍ개발일지
이번 포스트는 게시판 및 댓글 구현 방법을 서술하려고 한다.
게시판은 총 3개이며,
고객센터, 구매후기, 공지사항 게시판으로 만들었다.
게시판 구현
공지사항 게시판은 관리자만 작성하고 수정, 삭제 할 수 있게 하였고, 읽기는 모두 가능하게 권한을 주었다.
고객센터, 구매후기는 회원과 관리자만 삽입, 수정, 삭제 가능하며, 읽기는 모두 가능하게 하였으며,
고객센터에 추가 사항으로는 비밀글 쓰기를 추가하여 비밀번호를 확인하여 비밀글을 읽을 수 있게 하였다.
공지사항, 구매후기 게시판에는 조회수 업데이트 하는 쿼리를 적용하였고, 해당 글을 상세보기할 때만 조회수 증가할 수 있게 작성하였다.
키워드로 각 필드 별로 게시글을 조회할 수 있는 쿼리를 작성하였다.
댓글 구현
고객센터와 구매후기 게시판에는 댓글을 삽입하고 삭제 할 수 있게 구현하였다.
하나의 게시글에는 댓글이 여러개 달릴 수 있기에 @ManyToOne 설정하였음.
추가사항
프로젝트에는 게시판이 회원과 연관 관계 없이 개별적으로 존재하였었으나,
추후 회원테이블과 게시판 테이블, 댓글 테이블을 join하여 관계를 설정하였다.
회원테이블과 게시판테이블 및 댓글테이블은 @ManyToOne 관계이다.
마이페이지가 너무 썰렁해서 내가 쓴 글을 추가하기 위해 설정해보았다.'-'
BoardDTO
/*
작성자 : 정아름
작성일 : 24.02.19
작성내용 : 고객센터(1:1)게시판 구현
확인사항 : 테스트 해야함
*/
package com.example.basic.DTO;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BoardDTO {
//고객센터 게시판 번호
private Integer boardId;
//고객센터 게시판 제목
@NotBlank(message = "제목은 필수입니다.")
private String boardSubject;
//고객센터 게시판 내용
private String boardContent;
//고객센터 게시판 이미지
private String boardImage;
//고객센터 게시판 작성자
private String boardWriter;
//추가사항
//회원 번호
private Integer memberId;
//추가사항
//비밀글 체크 여부
private boolean secret;
//추가사항
//비밀글 비밀번호
private String boardPassword;
//등록일
private LocalDateTime regDate;
//수정일
private LocalDateTime modDate;
}
BoardEntity
package com.example.basic.Entity;
/*
작성자 : 정아름
작성일 : 24.02.21
수정사항 : 테이블 생성되고 테스트까지 완료했는데 왜 오류가 떴지?
*/
import jakarta.persistence.*;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Builder
@Table(name = "board")
@SequenceGenerator(name = "board_SEQ", sequenceName = "board_SEQ", initialValue = 1,
allocationSize = 1)
public class BoardEntity extends BaseEntity{
//고객센터 게시판 번호
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "board_SEQ")
private Integer boardId;
//고객센터 제목
@Column(nullable = false, length = 20)
private String boardSubject;
//고객센터 내용
@Column(length = 100)
private String boardContent;
//고객센터 이미지
private String boardImage;
//고객센터 작성자
private String boardWriter;
//추가사항
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private MemberEntity memberEntity;
//추가사항
//비밀글 체크 여부
private boolean secret;
//추가사항
//비밀글 비밀번호
private String boardPassword;
}
BoardcmtDTO
/*
작성자 : 정아름
작성일 : 24.02.19
작성내용 : 고객센터(1:1)게시판 댓글 구현
확인사항 : 테스트 완료
*/
package com.example.basic.DTO;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BoardcmtDTO {
//고객센터 댓글 번호
private Integer boardcmtId;
//고객센터 댓글 내용
@NotBlank(message = "내용은 필수입니다.")
private String boardcmtBody;
//고객센터 댓글 작성자
private String boardcmtWriter;
//추가사항
//회원 번호
private Integer memberId;
//등록일
private LocalDateTime regDate;
//수정일
private LocalDateTime modDate;
//고객센터 게시판 번호
private Integer boardId;
}
BoardcmtEntity
package com.example.basic.Entity;
/*
작성자 : 정아름
작성일 : 24.02.21
수정사항 : 테이블 생성되고 테스트까지 완료했는데 왜 오류 떠있는지 모르겠음
*/
import jakarta.persistence.*;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Builder
@Table(name = "boardcmt")
@SequenceGenerator(name = "boardcmt_SEQ", sequenceName = "boardcmt_SEQ", initialValue = 1,
allocationSize = 1)
public class BoardcmtEntity extends BaseEntity{
//고객센터 댓글 번호
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "boardcmt_SEQ")
private Integer boardcmtId;
//고객센터 댓글 내용
@Column(nullable = false, length = 100)
private String boardcmtBody;
//고객센터 댓글 작성자
private String boardcmtWriter;
//고객센터 게시판 번호
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private BoardEntity boardEntity;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private MemberEntity memberEntity;
}
BoardRepository
package com.example.basic.Repository;
import com.example.basic.Entity.BoardEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface BoardRepository extends JpaRepository<BoardEntity, Integer> {
//제목 조회
Page<BoardEntity> findByBoardSubjectContaining(String keyword, Pageable pageable);
//내용 조회
Page<BoardEntity> findByBoardContentContaining(String keyword, Pageable pageable);
//작성자조회
Page<BoardEntity> findByBoardWriterContaining(String keyword, Pageable pageable);
//제목+내용 조회
@Query("SELECT s FROM BoardEntity s WHERE s.boardSubject LIKE %:keyword%" +
" or s.boardContent LIKE %:keyword%")
Page<BoardEntity> findByBoardSCContaining(String keyword, Pageable pageable);
@Query("select w from BoardEntity w where w.memberEntity.memberId = :memberId")
List<BoardEntity> findByMemberId(Integer memberId);
}
Page<BoardEntity> findByBoardSubjectContaining(String keyword, Pageable pageable);
페이지 정보를 가지고, Board테이블의 제목 필드에 포함된 키워드를 조회하는 메소드이다.
@Query("SELECT s FROM BoardEntity s WHERE s.boardSubject LIKE %:keyword%" +
" or s.boardContent LIKE %:keyword%")
Board테이블에서 제목 필드와 내용 필드에서 keyword가 포함된 문자열을 검색한다는 쿼리이다.
BoardService
package com.example.basic.Service;
import com.example.basic.DTO.BoardDTO;
import com.example.basic.Entity.BoardEntity;
import com.example.basic.Entity.BoardcmtEntity;
import com.example.basic.Entity.MemberEntity;
import com.example.basic.Repository.BoardRepository;
import com.example.basic.Repository.BoardcmtRepository;
import com.example.basic.Repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
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 java.util.Arrays;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
private final BoardcmtRepository boardcmtRepository;
private final MemberRepository memberRepository;
private final ModelMapper modelMapper;
//삽입
public void boardInsert(BoardDTO boardDTO, Integer memberId) {
MemberEntity memberEntity = memberRepository.findById(memberId).orElseThrow();
BoardEntity boardEntity = modelMapper.map(boardDTO, BoardEntity.class);
boardEntity.setMemberEntity(memberEntity);
boardRepository.save(boardEntity);
}
//수정
public void boardUpdate(BoardDTO boardDTO, Integer memberId) {
Integer id = boardDTO.getBoardId();
MemberEntity member = memberRepository.findById(memberId).orElseThrow();
BoardEntity board = boardRepository.findById(id).orElseThrow();
if (board.getMemberEntity().getMemberId().equals(memberId)) {
BoardEntity boardEntity = modelMapper.map(boardDTO, BoardEntity.class);
boardEntity.setMemberEntity(member);
boardRepository.save(boardEntity);
}
}
//삭제
public void boardDelete(Integer boardId) {
List<BoardcmtEntity> boardcmtEntity = boardcmtRepository.findByBoardId(boardId);
for (BoardcmtEntity boardcmt : boardcmtEntity) {
boardcmtRepository.deleteById(boardcmt.getBoardcmtId());
}
boardRepository.deleteById(boardId);
}
//전체 조회
public Page<BoardDTO> boardlist(String type, String keyword, Pageable pageable) {
int cntPage = pageable.getPageNumber() - 1; //현재페이지
int pageLimit = 10;
Pageable page = PageRequest.of(cntPage, pageLimit,
Sort.by(Sort.Direction.DESC, "boardId"));
Page<BoardEntity> boardEntities;
if (type.equals("s") && keyword != null) {
//분류가 제목에 검색어가 존재하면
boardEntities = boardRepository.findByBoardSubjectContaining(keyword, page);
} else if (type.equals("c") && keyword != null) {
//분류가 내용에 검색어가 존재하면
boardEntities = boardRepository.findByBoardContentContaining(keyword, page);
} else if (type.equals("w") && keyword != null) {
//분류가 작성자에 검색어가 존재하면
boardEntities = boardRepository.findByBoardWriterContaining(keyword, page);
} else if (type.equals("sc") && keyword != null) {
//분류가 제목과 내용에 검색어가 존재하면
boardEntities = boardRepository.findByBoardSCContaining(keyword, page);
} else {
//분류 및 검색어가 없는 경우
boardEntities = boardRepository.findAll(page);
}
Page<BoardDTO> boardDTOS = boardEntities.map(
data -> modelMapper.map(data, BoardDTO.class));
return boardDTOS;
}
//개별 조회
public BoardDTO boardDetail(Integer boardId, Integer memberId) {
BoardEntity boardEntity = boardRepository.findById(boardId).orElseThrow();
BoardDTO boardDTO = null;
if (boardEntity.getMemberEntity().getMemberId().equals(memberId)) {
boardDTO = modelMapper.map(boardEntity, BoardDTO.class);
}
return boardDTO;
}
//회원의 게시글 조회
public List<BoardDTO> memberBoard(Integer memberId) {
List<BoardEntity> boardEntities = boardRepository.findByMemberId(memberId);
List<BoardDTO> boardDTOS = Arrays.asList(modelMapper.map(boardEntities, BoardDTO[].class));
return boardDTOS;
}
}
orElseThrow()
orElseThrow는 Optional의 인자가 null일 경우 예외처리를 시킨다.
조회한 값이 null일 경우 예외를 발생시키며,
null이 아닐 경우 내부적으로 Optional의 value를 가져오도록 구현되어있다.
Delete
Board 테이블은 Boardcmt 테이블과 join 되어 있다.
따라서,
Board 테이블 삭제 시 boardId(외래키)로 걸려있는 Boardcmt 테이블을 조회하여, 값이 있으면 댓글도 모두 삭제해야 게시글을 삭제 할 수 있다.
BoardcmtService
/*
작성자 : 정아름
작성일 : 24.02.21
수정사항 : 리뷰 댓글이랑 게시판 댓글 repository & service 확인완료
*/
package com.example.basic.Service;
import com.example.basic.DTO.BoardcmtDTO;
import com.example.basic.Entity.BoardEntity;
import com.example.basic.Entity.BoardcmtEntity;
import com.example.basic.Entity.MemberEntity;
import com.example.basic.Repository.BoardRepository;
import com.example.basic.Repository.BoardcmtRepository;
import com.example.basic.Repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BoardcmtService {
private final BoardcmtRepository boardcmtRepository;
private final BoardRepository boardRepository;
private final MemberRepository memberRepository;
private final ModelMapper modelMapper;
//삽입
public void boardcmtInsert(BoardcmtDTO boardcmtDTO, Integer boardId, Integer memberId) {
BoardEntity boardEntity = boardRepository.findById(boardId).orElseThrow();
MemberEntity memberEntity = memberRepository.findById(memberId).orElseThrow();
BoardcmtEntity boardcmtEntity = modelMapper.map(boardcmtDTO, BoardcmtEntity.class);
boardcmtEntity.setBoardEntity(boardEntity);
boardcmtEntity.setMemberEntity(memberEntity);
boardcmtRepository.save(boardcmtEntity);
}
//수정
public void boardcmtUpdate(BoardcmtDTO boardcmtDTO) {
MemberEntity member = memberRepository.findById(boardcmtDTO.getMemberId()).orElseThrow();
BoardEntity boardDTO = boardRepository.findById(boardcmtDTO.getBoardId()).orElseThrow();
BoardcmtEntity boardcmtEntity = boardcmtRepository.findById(boardcmtDTO.getBoardcmtId()).orElseThrow();
if (boardcmtEntity != null) {
BoardcmtEntity boardcmt = modelMapper.map(boardcmtDTO, BoardcmtEntity.class);
boardcmt.setMemberEntity(member);
boardcmt.setBoardEntity(boardDTO);
boardcmtRepository.save(boardcmt);
}
}
//삭제
public void boardcmtDelete(Integer boardcmtId) {
boardcmtRepository.deleteById(boardcmtId);
}
//전체 조회
public List<BoardcmtDTO> boardcmtlist(Integer boardId) {
List<BoardcmtEntity> boardcmtEntities = boardcmtRepository.findByBoardId(boardId);
List<BoardcmtDTO> boardcmtDTOS = Arrays.asList(modelMapper.
map(boardcmtEntities, BoardcmtDTO[].class));
return boardcmtDTOS;
}
//개별조회
public BoardcmtDTO boardcmtDetail(Integer boardcmtId, Integer boardId, Integer memberId) {
BoardcmtEntity boardcmtEntity = boardcmtRepository.findById(boardcmtId).orElseThrow();
BoardcmtDTO boardcmtDTO = null;
if (boardcmtEntity.getMemberEntity().getMemberId().equals(memberId) &&
boardcmtEntity.getBoardEntity().getBoardId().equals(boardId)) {
boardcmtDTO = modelMapper.map(boardcmtEntity, BoardcmtDTO.class);
}
return boardcmtDTO;
}
}
BoardController
/*
설명 : 고객센터 게시판의 목록, 수정, 삭제, 조회로 이동하는 페이지 영역
입력값 : /board/list, /board/insert, /board/update, /board/delete, /board/detail
출력값 : board/list, board/insert, board/update, board/detail
작성일 : 24.02.21
작성자 : 정아름
수정사항 : 고객센터 게시판의 전체 목록 페이지는 page 처리 하기로 함
*/
package com.example.basic.Controller;
import com.example.basic.DTO.BoardDTO;
import com.example.basic.DTO.BoardcmtDTO;
import com.example.basic.DTO.MemberDTO;
import com.example.basic.Service.BoardService;
import com.example.basic.Service.BoardcmtService;
import com.example.basic.Service.MemberService;
import com.example.basic.Util.PaginationUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
import java.util.Map;
@Controller
@RequiredArgsConstructor
public class BoardController {
//주입
private final BoardService boardService;
private final BoardcmtService boardcmtService;
private final MemberService memberService;
//전체 조회
@GetMapping("/board/list")
public String listForm(@RequestParam(value = "type", defaultValue = "") String type,
@RequestParam(value = "keyword", defaultValue = "") String keyword,
@PageableDefault(page = 1) Pageable pageable,
Model model) {
//검색조회, 페이지처리
Page<BoardDTO> list = boardService.boardlist(type, keyword, pageable);
Map<String, Integer> page = PaginationUtil.Pagination(list);
model.addAllAttributes(page);
model.addAttribute("list", list);
return "board/list";
}
//삽입
@GetMapping("/board/insert")
public String insertForm(@AuthenticationPrincipal User user, Model model) {
MemberDTO memberDTO = memberService.detail(user.getUsername());
model.addAttribute("member", memberDTO);
return "board/insert";
}
@PostMapping("/board/insert")
public String insertProc(BoardDTO boardDTO, @AuthenticationPrincipal User user,
RedirectAttributes redirectAttributes) {
MemberDTO memberDTO = memberService.detail(user.getUsername());
boardService.boardInsert(boardDTO, memberDTO.getMemberId());
redirectAttributes.addFlashAttribute("successMessage",
"게시글이 등록되었습니다.");
System.out.println(memberDTO);
System.out.println(boardDTO);
return "redirect:/board/list";
}
//수정
@GetMapping("/board/update")
public String updateForm(Integer id, @AuthenticationPrincipal User user,
Model model, RedirectAttributes redirectAttributes) {
MemberDTO memberDTO = memberService.detail(user.getUsername());
BoardDTO boardDTO = boardService.boardDetail(id, memberDTO.getMemberId());
if (!boardDTO.getMemberId().equals(memberDTO.getMemberId())) {
redirectAttributes.addFlashAttribute("successMessage",
"권한이 없습니다.");
return "redirect:/board/list";
}
System.out.println(memberDTO);
System.out.println(boardDTO);
model.addAttribute("data", boardDTO);
return "board/update";
}
@PostMapping("/board/update")
public String updateProc(BoardDTO boardDTO, @AuthenticationPrincipal User user,
RedirectAttributes redirectAttributes) {
MemberDTO memberDTO = memberService.detail(user.getUsername());
BoardDTO boardDTO1 = boardService.boardDetail(boardDTO.getBoardId(), memberDTO.getMemberId());
if (boardDTO1 != null) {
boardService.boardUpdate(boardDTO, memberDTO.getMemberId());
}
redirectAttributes.addFlashAttribute("successMessage",
"게시글이 수정되었습니다.");
System.out.println(memberDTO);
System.out.println(boardDTO1);
return "redirect:/board/list";
}
//삭제
@GetMapping("/board/delete")
public String deleteProc(Integer id, @AuthenticationPrincipal User user,
RedirectAttributes redirectAttributes) {
MemberDTO memberDTO = memberService.detail(user.getUsername());
BoardDTO boardDTO = boardService.boardDetail(id, memberDTO.getMemberId());
if (boardDTO.getMemberId().equals(memberDTO.getMemberId())) {
boardService.boardDelete(id);
}
redirectAttributes.addFlashAttribute("successMessage",
"게시글이 삭제되었습니다.");
System.out.println(memberDTO);
System.out.println(boardDTO);
return "redirect:/board/list";
}
//개별 조회
@GetMapping("/board/detail")
public String detailProc(Integer id, Model model, String password,
@AuthenticationPrincipal User user) {
MemberDTO memberDTO = memberService.detail(user.getUsername());
BoardDTO boardDTO = boardService.boardDetail(id, memberDTO.getMemberId());
List<BoardcmtDTO> boardcmtDTOS = boardcmtService.boardcmtlist(id);
//게시글 조회
//비밀글 일 때
if (boardDTO.isSecret()) {
//입력받은 비밀번호와 저장된 비밀번호를 확인
//비밀번호가 맞지 않을 때
if (!password.equals(boardDTO.getBoardPassword())) {
return "redirect:/board/list";
} else {
//비밀번호가 일치할 때. 비밀번호를 저장하고 페이지 이동
boardDTO.setBoardPassword(password);
}
}
System.out.println(memberDTO);
System.out.println(boardDTO);
System.out.println(boardcmtDTOS);
model.addAttribute("member", memberDTO);
model.addAttribute("data", boardDTO);
model.addAttribute("list", boardcmtDTOS);
return "board/detail";
}
}
View
- 게시판에 게시글 작성 시 꾸밀 수 있게 summernote를 적용하였다.
- 게시글 작성 시 비밀글 설정 여부를 선택하여 비밀글을 선택했을 때만 비밀번호를 입력받을 수 있도록 설계하였다.
board/detail
<!--
파일명 : board/detail
작성자 : 정아름
작성일 : 24.02.22
수정사항 : 글씨체 적용함
틀 수정, 배경 및 이미지, 아이콘 등 확인!
-->
<!DOCTYPE html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<th:block layout:fragment="head">
<meta charset="UTF-8">
<!-- bootstrap -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap" rel="stylesheet">
<!-- google icon -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"/>
<!-- summernote (jQuery) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<!-- summernote (css/js) -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<style>
body {
font-family: Verdana, sans-serif;
font-size: 15px;
}
h2 {
font-family: "Nanum Pen Script", cursive;
font-weight: 400;
font-size: 50px;
font-style: normal;
}
</style>
</th:block>
</head>
<body>
<div layout:fragment="content">
<div class="row">
<!-- 여백 -->
<div class="col-sm-3"></div>
<!-- 고객센터 게시글 상세보기 -->
<div class="col-sm-6">
<div class="container mt-5 mb-5">
<h2>고객센터 게시글 상세보기</h2><br>
<div class="card">
<div class="card-body">
<p>
번호 : <span th:text="${data.boardId}"></span>
</p>
<p>
제목 : <span th:text="${data.boardSubject}"></span>
</p>
<p>
내용 :
</p>
<div id="summernoteContent"
th:text="${data.boardContent}" readonly></div>
<p>
사진파일 : <span th:text="${data.boardImage}"></span>
</p>
<p>
작성자 : <span th:text="${data.boardWriter}"></span>
</p>
<p>
수정일 : <span th:text="${#temporals.format(data.modDate, 'yyyy-MM-dd')}"></span></p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-outline-warning"
th:onclick="|location.href='@{/board/update(id=${data.boardId})}'|"
sec:authorize="hasAnyRole('USER', 'ADMIN')">
게시글 수정
</button>
<button type="button" class="btn btn-outline-warning float-end"
th:onclick="|location.href='@{/board/delete(id=${data.boardId})}'|"
sec:authorize="hasAnyRole('USER', 'ADMIN')">
게시글 삭제
</button>
</div>
</div>
<!--댓글 입력-->
<form th:action="@{/boardcmt/insert(id=${data.boardId})}" method="post">
<input type="hidden" name="id" th:value="${data.boardId}">
<div class="card">
<div class="card-body">
<div class="mt-3 mb-3">
<textarea class="form-control" rows="5" name="boardcmtBody"></textarea>
<label for="boardcmtWriter"></label>
<input type="text" name="boardcmtWriter" id="boardcmtWriter" th:value="${member.memberName}" readonly>
</div>
<button type="submit" class="btn btn-outline-warning"
sec:authorize="hasAnyRole('USER', 'ADMIN')">
댓글등록
</button>
</div>
</div>
</form>
<!--댓글 끝-->
<div th:each="boardcmt:${list}">
<div class="card">
<div class="card-footer">
<p th:text="${boardcmt.boardcmtBody}">댓글 ▶</p>
<p th:text="${boardcmt.boardcmtWriter}">작성자</p>
<p th:text="${#temporals.format(data.modDate, 'yyyy-MM-dd')}">등록일</p>
<a th:href="@{/boardcmt/update(id=${data.boardId}, cid=${boardcmt.boardcmtId})}"
sec:authorize="hasAnyRole('USER', 'ADMIN')">
수정</a>
<a th:href="@{/boardcmt/delete(id=${data.boardId}, cid=${boardcmt.boardcmtId})}"
sec:authorize="hasAnyRole('USER', 'ADMIN')">
삭제</a>
</div>
</div>
</div>
</div>
</div>
<!-- 여백 -->
<div class="col-sm-3"></div>
</div>
</div>
</div>
<th:block layout:fragment="script">
<!-- summernote -->
<script>
$(document).ready(function () {
// 빈 div 요소에 Summernote를 초기화합니다.
$('#summernoteContent').summernote({
height: 300, // 에디터 높이
minHeight: null, // 최소 높이
maxHeight: null, // 최대 높이
focus: true, // 에디터 로딩후 포커스를 맞출지 여부
lang: "ko-KR", // 한글 설정
disableResizeEditor: true, // 더 나은 외관을 위해 크기 조정 비활성화
toolbar: [] // 툴바를 비워서 숨깁니다.
});
//$('#summernoteContent').summernote('disable');
$("#summernoteContent").summernote("destroy");
});
</script>
<script th:inline="javascript">
/* 작업 성공했을 때 성공메세지 창을 출력 */
var successMessage = /*[[ ${successMessage} ]]*/ null;
if (successMessage) {
alert(successMessage);
}
</script>
</th:block>
</body>
</html>
board/list
<!--
파일명 : board/list
작성자 : 정아름
작성일 : 24.02.22
수정사항 :
-->
<!DOCTYPE html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>SalPick</title>
<!-- bootstrap -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap" rel="stylesheet">
<!-- google icon -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"/>
<style>
body {
font-family: Verdana, sans-serif;
font-size: 15px;
}
h2 {
font-family: "Nanum Pen Script", cursive;
font-weight: 400;
font-size: 50px;
font-style: normal;
}
</style>
</head>
<body>
<div layout:fragment="content">
<div class="row">
<!-- 여백 -->
<div class="col-sm-3"></div>
<!-- 고객센터 게시글 목록 -->
<div class="col-sm-6">
<div class="container mt-5 mb-5">
<h2>고객센터 게시판</h2>
<button type="button" class="btn btn-outline-success float-end"
th:onclick="|location.href='@{/board/insert}'|">
게시글 등록
</button>
<table class="table table-hover">
<thead>
<tr>
<th>No</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>작업</th>
</tr>
</thead>
<tbody>
<tr th:each="data:${list}">
<td th:text="${data.boardId}">boardId</td>
<td>
<a th:text="${data.boardSubject}"
th:href="@{/board/detail(id=${data.boardId})}" th:unless="${data.isSecret()}">
boardSubject
</a>
<a th:text="${data.boardSubject}"
th:href="@{/board/detail(id=${data.boardId})}" th:if="${data.isSecret()}"
data-bs-toggle="modal" data-bs-target="#myModal">
boardSubject
</a>
</td>
<td th:text="${data.boardWriter}">boardWriter</td>
<td th:text="${#temporals.format(data.modDate, 'yyyy-MM-dd')}">
modDate
</td>
<td>
<button type="button" class="btn btn-light btn-sm"
th:onclick="|location.href='@{/board/update(id=${data.boardId})}'|"
sec:authorize="hasAnyRole('USER', 'ADMIN')">
수정
</button>
<button type="button" class="btn btn-light btn-sm"
th:onclick="|location.href='@{/board/delete(id=${data.boardId})}'|"
sec:authorize="hasAnyRole('USER', 'ADMIN')">
삭제
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!--페이지 번호-->
<ul class="pagination pagination-sm justify-content-center">
<li class="page-item" th:unless="${startPage==1}">
<a class="page-link"
th:href="@{/board/list(page=1, type=${type}, keyword=${keyword})}">
<span class="material-symbols-outlined">chevron_left</span>
</a>
</li>
<li class="page-item" th:unless="${currentPage==1}">
<a class="page-link"
th:href="@{/board/list(page=${prevPage}, type=${type}, keyword=${keyword})}">
<span class="material-symbols-outlined">arrow_back_ios</span>
</a>
</li>
<span th:each="page:${#numbers.sequence(startPage, endPage, 1)}">
<li class="page-item active" th:class="${page==currentPage} ? 'active':''">
<a class="page-link" th:href="@{/list(page=${page}, type=${type}, keyword=${keyword})}"
th:text="${page}"> 1 </a>
</li>
</span>
<li class="page-item" th:unless="${endPage==currentPage}">
<a class="page-link"
th:href="@{/board/list(page=${nextPage}, type=${type}, keyword=${keyword})}">
<span class="material-symbols-outlined">chevron_right</span>
</a>
</li>
<li class="page-item" th:unless="${endPage==lastPage}">
<a class="page-link"
th:href="@{/board/list(page=${lastPage}, type=${type}, keyword=${keyword})}">
<span class="material-symbols-outlined">keyboard_double_arrow_right</span>
</a>
</li>
</ul>
<!--게시글 검색-->
<div class="container mt-3">
<form th:action="@{/board/list}" method="get">
<div class="container input-group mb-3 mt-3">
<label class="form-label"></label>
<select class="form-select-sm" name="type">
<option value="" th:selected="${type==null}"></option>
<option value="s" th:selected="${type=='s'}">제목</option>
<option value="c" th:selected="${type=='c'}">내용</option>
<option value="w" th:selected="${type=='w'}">작성자</option>
<option value="sc" th:selected="${type=='sc'}">제목+내용</option>
</select>
<input type="text" class="form-control" name="keyword"
th:value="${keyword}">
<button type="submit" class="btn">
<img src="/image/search.png" alt="search">
</button>
</div>
</form>
</div>
</div>
<!-- 여백 -->
<div class="col-sm-3"></div>
</div>
</div>
<script th:inline="javascript">
/* 작업 성공했을 때 성공메세지 창을 출력 */
var successMessage = /*[[ ${successMessage} ]]*/ null;
if (successMessage) {
alert(successMessage);
}
</script>
<div class="modal" id="myModal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">비밀번호 확인</h4>
</div>
<!-- Modal body -->
<div class="modal-body">
<input type="password" class="form-control" name="boardPassword">
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">확인</button>
</div>
</div>
</div>
</div>
</body>
</html>
board/insert
<!--
파일명 : board/insert
작성자 : 정아름
작성일 : 24.03.05
수정사항 : 작성자 확인
-->
<!DOCTYPE html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<th:block layout:fragment="head">
<meta charset="UTF-8">
<!-- bootstrap -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap" rel="stylesheet">
<!-- google icon -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"/>
<!-- summernote (jQuery) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<!-- summernote (css/js) -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<style>
body {
font-family: Verdana, sans-serif;
font-size: 15px;
}
h2 {
font-family: "Nanum Pen Script", cursive;
font-weight: 400;
font-size: 50px;
font-style: normal;
}
</style>
</th:block>
</head>
<body>
<div layout:fragment="content">
<div class="row">
<!-- 여백 -->
<div class="col-sm-3"></div>
<!-- 고객센터 게시글 등록 -->
<div class="col-sm-6">
<div class="container mt-5 mb-5">
<h2>고객센터 문의하기</h2>
<form th:action="@{/board/insert}" method="post">
<div class="card">
<div class="card-body">
<div class="mb-3 mt-3">
<label class="form-label">제목 : </label>
<input type="text" class="form-control" name="boardSubject">
</div>
<div class="mb-3">
<label for="summernote">문의 내용 : </label>
<textarea id="summernote" name="boardContent">
</textarea>
</div>
<div class="mb-3">
<label class="form-label">사진 파일 : </label>
<input type="file" class="form-control" name="boardImage">
</div>
<div class="mb-3">
<label class="form-label">작성자 : </label>
<input type="text" class="form-control" name="boardWriter"
th:value="${member.memberName}" readonly>
</div>
<div class="input-group mb-3">
<div class="form-check form-switch">
<label for="secret" class="form-check-label">비밀글 선택</label>
<input class="form-check-input" type="checkbox" id="secret" name="secret"
value="true">
</div>
<div>
<label for="boardPassword" class="form-label"></label>
<input type="password" class="form-control" name="boardPassword" id="boardPassword"
placeholder="비밀번호를 입력하세요" readonly>
</div>
</div>
<button type="submit" class="btn btn-outline-warning float-end">
등록
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 여백 -->
<div class="col-sm-3"></div>
</div>
</div>
<th:block layout:fragment="script">
<!-- summernote -->
<script>
$(document).ready(function () {
$('#summernote').summernote({
height: 300, // 에디터 높이
minHeight: null, // 최소 높이
maxHeight: null, // 최대 높이
focus: true, // 에디터 로딩후 포커스를 맞출지 여부
lang: "ko-KR", // 한글 설정
placeholder: '최대 2048자까지 쓸 수 있습니다', //placeholder 설정
toolbar: [
['style', ['style']], // 글자 스타일 설정 옵션
['fontsize', ['fontsize']], // 글꼴 크기 설정 옵션
['font', ['bold', 'underline', 'clear']], // 글자 굵게, 밑줄, 포맷 제거 옵션
['color', ['color']], // 글자 색상 설정 옵션
['table', ['table']], // 테이블 삽입 옵션
['para', ['ul', 'ol', 'paragraph']], // 문단 스타일, 순서 없는 목록, 순서 있는 목록 옵션
['height', ['height']], // 에디터 높이 조절 옵션
['insert', ['picture', 'link', 'video']], // 이미지 삽입, 링크 삽입, 동영상 삽입 옵션
['view', ['codeview', 'fullscreen', 'help']], // 코드 보기, 전체 화면, 도움말 옵션
],
fontNames: [
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', '맑은 고딕',
'궁서', '굴림체', '굴림', '돋움체', '바탕체'
],
fontSizes: [
'8', '9', '10', '11', '12', '14', '16', '18',
'20', '22', '24', '28', '30', '36', '50', '72',
] // 글꼴 크기 옵션
});
});
</script>
<script th:inline="javascript">
/* 작업 성공했을 때 성공메세지 창을 출력 */
var successMessage = /*[[ ${successMessage} ]]*/ null;
if (successMessage) {
alert(successMessage);
}
</script>
<script th:inline="javascript">
/*<![CDATA[*/
$(document).ready(function () {
// 체크박스 클릭 이벤트 처리
$('#secret').click(function () {
if ($(this).is(':checked')) {
// 체크되었을 때 비밀번호 입력란을 읽기/쓰기 가능으로 설정
$('#boardPassword').prop('readonly', false);
} else {
// 체크 해제되었을 때 비밀번호 입력란을 읽기 전용으로 설정
$('#boardPassword').prop('readonly', true).val('');
}
});
});
/*]]>*/
</script>
</th:block>
</body>
</html>
board/update
<!--
파일명 : board/update
작성자 : 정아름
작성일 : 24.02.22
수정사항 : 글씨체 적용함
틀 수정, 배경 및 이미지, 아이콘 등 확인!
-->
<!DOCTYPE html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<th:block layout:fragment="head">
<meta charset="UTF-8">
<!-- bootstrap -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap" rel="stylesheet">
<!-- google icon -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"/>
<!-- summernote (jQuery) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<!-- summernote (css/js) -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<style>
body {
font-family: Verdana, sans-serif;
font-size: 15px;
}
h2 {
font-family: "Nanum Pen Script", cursive;
font-weight: 400;
font-size: 50px;
font-style: normal;
}
</style>
</th:block>
</head>
<body>
<div layout:fragment="content">
<div class="container mt-5 mb-5">
<div class="row">
<!-- 여백 -->
<div class="col-sm-3"></div>
<!-- 고객센터 게시글 수정 -->
<div class="col-sm-6">
<h2>고객센터 게시글 수정</h2>
<form th:action="@{/board/update}" method="post">
<input type="hidden" name="boardId" id="id" th:value="${data.boardId}">
<div class="mb-3 mt-3">
<label for="boardSubject" class="form-label">제목 : </label>
<input type="text" class="form-control" name="boardSubject"
id="boardSubject" th:value="${data.boardSubject}">
</div>
<div class="mb-3">
<label for="summernote" class="form-label">내용 : </label>
<textarea id="summernote" class="form-control" name="boardContent"
th:text="${data.boardContent}">
</textarea>
</div>
<div class="mb-3">
<label for="boardImage" class="form-label">사진 파일 : </label>
<input type="file" class="form-control" name="boardImage"
id="boardImage" th:value="${data.boardImage}">
</div>
<div class="mb-3">
<label for="boardWriter" class="form-label">작성자 : </label>
<input type="text" class="form-control" name="boardWriter"
id="boardWriter" th:value="${data.boardWriter}" readonly>
</div>
<div class="mb-3">
<label for="modDate" class="form-label">최종 수정일 : </label>
<input type="text" class="form-control" name="modDate"
id="modDate" th:value="${data.modDate}" readonly>
</div>
<button type="submit" class="btn btn-outline-warning float-end"
sec:authorize="hasRole('USER')">
수정
</button>
</form>
</div>
<!-- 여백 -->
<div class="col-sm-3"></div>
</div>
</div>
</div>
<th:block layout:fragment="script">
<!-- summernote -->
<script>
$(document).ready(function () {
$('#summernote').summernote({
height: 300, // 에디터 높이
minHeight: null, // 최소 높이
maxHeight: null, // 최대 높이
focus: true, // 에디터 로딩후 포커스를 맞출지 여부
lang: "ko-KR", // 한글 설정
placeholder: '최대 2048자까지 쓸 수 있습니다', //placeholder 설정
toolbar: [
['style', ['style']], // 글자 스타일 설정 옵션
['fontsize', ['fontsize']], // 글꼴 크기 설정 옵션
['font', ['bold', 'underline', 'clear']], // 글자 굵게, 밑줄, 포맷 제거 옵션
['color', ['color']], // 글자 색상 설정 옵션
['table', ['table']], // 테이블 삽입 옵션
['para', ['ul', 'ol', 'paragraph']], // 문단 스타일, 순서 없는 목록, 순서 있는 목록 옵션
['height', ['height']], // 에디터 높이 조절 옵션
['insert', ['picture', 'link', 'video']], // 이미지 삽입, 링크 삽입, 동영상 삽입 옵션
['view', ['codeview', 'fullscreen', 'help']], // 코드 보기, 전체 화면, 도움말 옵션
],
fontNames: [
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', '맑은 고딕',
'궁서', '굴림체', '굴림', '돋움체', '바탕체'
],
fontSizes: [
'8', '9', '10', '11', '12', '14', '16', '18',
'20', '22', '24', '28', '30', '36', '50', '72',
] // 글꼴 크기 옵션
});
});
</script>
<script th:inline="javascript">
/* 작업 성공했을 때 성공메세지 창을 출력 */
var successMessage = /*[[ ${successMessage} ]]*/ null;
if (successMessage) {
alert(successMessage);
}
</script>
</th:block>
</body>
</html>
화면 캡쳐
'개발일지' 카테고리의 다른 글
Team project - 쇼핑몰 구현(4) (0) | 2024.04.29 |
---|---|
Team project - 쇼핑몰 구현(3) (0) | 2024.04.29 |
Team project - 쇼핑몰 구현(1) (2) | 2024.04.26 |
Team project - 쇼핑몰 구현 (0) | 2024.04.22 |
Spring Security - 회원가입 강화 (0) | 2024.04.16 |