티스토리 뷰

1. html 부분

<!DOCTYPE html>
<html lang="en" xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script th:src="@{/base/jq/jquery-3.7.1.min.js}"></script>
    <link rel="stylesheet" href="../base/css/read.css">
</head>
<body>
    <div class="container">
    <h1 style="text-align: center">[ 글 읽기 ]</h1>
        <!-- 글 내용 출력 부분 -->
        <div class="post-info">
            <div>
                <label>글번호: </label>
                <span th:text="${board.boardNum}"></span>
            </div>
            <div>
                <label>작성자: </label>
                <span th:text="${board.memberId}"></span>
            </div>
            <div>
                <label>작성일: </label>
                <span th:text="${#temporals.format(board.createDate, 'yyyy-MM-dd')}"></span>
            </div>
            <div>
                <label>수정일: </label>
                <span th:text="${#temporals.format(board.updateDate, 'yyyy-MM-dd')}"></span>
            </div>
            <div>
                <label>조회수: </label>
                <span th:text="${board.viewCount}"></span>
            </div>
            <div>
                <label>좋아요: </label>
                <span th:text="${board.likeCount}"></span>
            </div>
            <div>
                <label>싫어요: </label>
                <span th:text="${board.dislikeCount}"></span>
            </div>
        </div>
        <hr>
        <div class="content">
            <p th:text="${board.contents}"></p>
        </div>
        <div class="attachments">
            <label>파일첨부:</label>
            <a th:text="${board.originalName}"></a>
        </div>
        <hr>

        <!-- 리플 작성 폼(로그인한 사람한테만 보임) -->
        <div sec:authorize="isAuthenticated()" class="comment">
            <form>
                <label>리플 달기: </label>
                <input style="width: 500px" type="text">
                <button>저장</button>
            </form>

        <!-- 리플 출력 폼(아무한테나 다 보임) -->
            <form>
                <p th:each="reply : ${board.replyDTOList}">
                <!-- 리플목록 전체를 리스트형태로 담은 다음에 DTO 변환 및 리턴 형태를 거친 후 HTML 출력 -->
                    <span th:text="${reply.memberName}"></span>
                    <span th:text="${reply.contents}"></span>
                    <span th:text="${#temporals.format(reply.createTime, 'yyyy-MM-dd')}"></span>
                </p>
            </form>
        </div>
    </div>
</body>
</html>

2. Controller 부분

package net.datasa.web5.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.BoardDTO;
import net.datasa.web5.domain.dto.MemberDTO;
import net.datasa.web5.security.AuthenticatedUser;
import net.datasa.web5.service.BoardService;
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.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@Controller
@Slf4j
@RequestMapping("board")
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    //  8. 1(木)
    /*  게시물을 10개씩 보여주기
        2번이상 쓰는 상수는 선언해놓는게 좋다
     */
    //  int pageSize = 10; → 타임리프와 @Value 안쓰는 경우
    @Value("${board.pageSize}") // 타임리프를 통하여 괄호안에 한 페이지당 게시글 갯수가 정해짐. 위에 변수를 직접 선언한 것과 같은 효과
            int pageSize;

    @Value("${board.linkSize}") // 페이지 이동 링크 수
    int linkSize;

    @Value("${board.uploadPath}") // 첨부파일 저장 경로
    String uploadPath;

    /**
     * 글 목록 보기
     *
     * @param model
     * @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<BoardDTO> boardPage = boardService.getList(page, pageSize, searchType, searchWord);

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

        // 검색부분 보여줄 것들
        model.addAttribute("page", page);               // 현재 페이지
        model.addAttribute("linkSize", linkSize);       // 페이지 이동링크 수
        model.addAttribute("searchType", searchType);   // 검색기준
        model.addAttribute("searchWord", searchWord);   // 검색어
        // 글 목록부분 보여줄 것
        model.addAttribute("boardPage", boardPage);     // 출력할 글정보
        return "boardView/list";
    }

    /**
     * 글쓰기 폼으로 이동
     *
     * @return 글쓰기폼을 출력하는 HTML파일
     */
    @GetMapping("write")
    public String write() {
        return "boardView/writeForm";
    }

    /**
     * 게시글 저장
     */
    @PostMapping("write")
    public String writesave(@ModelAttribute BoardDTO boardDTO, @AuthenticationPrincipal AuthenticatedUser user) {
        boardDTO.setMemberId(user.getId()); // 작성자 ID 설정
        log.debug("저장할 정보 {}", boardDTO); // 저장할 정보 로그 출력
        boardService.write(boardDTO);   // 게시글 저장
        return "redirect:/";    // 홈으로 리다이렉트
    }

    /**
     * 선택한 게시글 보기
     *
     * @param model    Model 객체로 뷰에 데이터를 전달
     * @param boardNum 게시글 번호
     * @return 게시글을 출력하는 HTML 파일 이름
     */

//  선생님 풀이
  @GetMapping("read")
  public String read(Model model, @RequestParam("boardNum") Integer boardNum) {
      try {
          BoardDTO boardDTO = boardService.getBoard(boardNum);
          model.addAttribute("board", boardDTO);  // 모델에 저장
          return "boardView/read";
      } catch (Exception e) {
          e.printStackTrace(); // 문제가 생겼을 때 로그를 확인하기 위해서 남기는 것
          return "redirect:list"; // 해당 URL로 리다이렉트
      }
  }

3. Service부분

package net.datasa.web5.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 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.ArrayList;
import java.util.List;
import java.util.Optional;

@Service  // Spring에서 서비스 계층을 나타내는 어노테이션
@Transactional  // 클래스 내의 모든 메서드가 트랜잭셔널(트랜잭션 범위 내에서 실행)하게 함
@RequiredArgsConstructor  // Lombok 어노테이션으로, final로 선언된 모든 필드를 파라미터로 갖는 생성자를 자동으로 생성
@Slf4j  // Lombok 어노테이션으로, 로깅을 위한 로거를 자동으로 생성

public class BoardService {

    private final BoardRepository boardRepository;  // 게시판 관련 DB 작업을 처리하는 Repository
    private final MemberRepository memberRepository;  // 회원 관련 DB 작업을 처리하는 Repository

    /**
     * 게시판 글 저장
     *
     * @param dto 저장할 글 정보
     */
    public void write(BoardDTO dto) {
        log.info("Writing a new board entry: {}", dto);  // 글 작성 로그 출력
        // 글 작성자 정보 조회
        MemberEntity memberEntity = memberRepository.findById(dto.getMemberId())
                .orElseThrow(() -> new EntityNotFoundException("Member not found"));  // 작성자 정보가 없을 시 예외 처리

        BoardEntity entity = BoardEntity.builder()
                .member(memberEntity)  // 글 작성자 정보 설정
                .title(dto.getTitle())  // 글 제목 설정
                .contents(dto.getContents())  // 글 내용 설정
                .originalName(dto.getOriginalName())  // 첨부파일 원본 이름 설정
                .fileName(dto.getFileName())  // 첨부파일 이름 설정
                .build();

        boardRepository.save(entity);  // 게시글 저장
        log.debug("저장되는 엔티티 : {}", entity);  // 저장되는 엔티티 로그 출력
        // TODO: 첨부파일 처리할 것
    }

    /**
     * 게시글 목록 조회
     *
     * @param page       현재 페이지
     * @param pageSize   한 페이지당 글 수
     * @param searchType 검색 대상 (제목 : title, 내용 : contents, 작성자 : memberName)
     * @param searchWord 검색어
     * @return 글정보가 한 페이지 분량 저장된 Page객체
     */
    public Page<BoardDTO> getList(int page, int pageSize, String searchType, String searchWord) {
        // Pageable : 한페이지를 끊어오기 위한 정보
        // Sort.Direction.DESC, "boardNum" : 정렬 객체
        // Spring에서는 첫 페이지가 '0'이므로, 가독성을 위해 page-1해줘야 '1' 페이지부터 시작
        // p : 한 페이지당 pageSize개씩 boardNum기준으로 내림차순하여 페이지 정보 생성
        Pageable p = PageRequest.of(page - 1, pageSize, Sort.Direction.DESC, "boardNum");
        // 검색 타입에 따라 적절한 메서드 호출
        Page<BoardEntity> entityPage = null;
        if (searchType.equals("title")) {
            entityPage = boardRepository.findByTitleContaining(searchWord, p);
        } else if (searchType.equals("contents")) {
            entityPage = boardRepository.findByContentsContaining(searchWord, p);
        } else if (searchType.equals("memberName")) {
            entityPage = boardRepository.findByMember_MemberName(searchWord, p);
        } else {
            entityPage = boardRepository.findAll(p);
        }

        // entityPage를 DTO로 변환하여 반환
        Page<BoardDTO> dtoPage = entityPage.map(this::convertToDTO);
        return dtoPage;
    }

    /**
     * BoardEntity 객체를 전달받아 BoardDTO객체로 변환하여 리턴
     * @param entity DB에서 읽은 정보를 담은 엔티티 객체
     * @return       출력용 정보를 담은 DTO객체
     */
    private BoardDTO convertToDTO(BoardEntity entity) {
        return BoardDTO.builder()  // Builder 패턴을 사용하여 BoardDTO 객체 생성
                .boardNum(entity.getBoardNum())
                .memberId(entity.getMember().getMemberId())
                .memberName(entity.getMember().getMemberName())
                .title(entity.getTitle())
                .contents(entity.getContents())
                .viewCount(entity.getViewCount())
                .likeCount(entity.getLikeCount())
                .dislikeCount(entity.getDislikeCount())
                .originalName(entity.getOriginalName())
                .fileName(entity.getFileName())
                .createDate(entity.getCreateDate())
                .updateDate(entity.getUpdateDate())
                .build();
    }

    /**
     * 게시글 클릭시 해당 게시글 띄우기
     * @Param boardNum 글번호
     * @return          글 정보 DTO
     */

//  선생님 풀이
    /**
     * 글 1개 조회
     * @param boardNum 글 번호
     * @return         글 정보 DTO
     */
  public BoardDTO getBoard(Integer boardNum) {
      // 글 번호로 BoardEntity 조회 없으면 예외 처리 있으면 BoardDTO로 변환하여 리턴
      BoardEntity boardEntity = boardRepository.findById(boardNum).orElseThrow(()
              -> new EntityNotFoundException("글이 없습니다."));

      // 조회결과 출력 (순환참조)
      log.debug("조회된 게시글 정보 : {}", boardEntity);

      // 조회된 엔티티(글 정보)를 DTO로 변환하여 반환
      BoardDTO boardDTO = convertToDTO(boardEntity);

      // 리플 목록을 DTO로 변환하여 추가
      List<ReplyDTO> replyDTOList = new ArrayList<>();
      for (ReplyEntity replyEntity : boardEntity.getReplyEntityList()) {
          ReplyDTO replyDTO = ReplyDTO.builder()
                  .replyNum(replyEntity.getReplyNum())
                  .boardNum(replyEntity.getBoard().getBoardNum())
                  .memberId(replyEntity.getMember().getMemberId())
                  .memberName(replyEntity.getMember().getMemberName())
                  .contents(replyEntity.getContents())
                  .createTime(replyEntity.getCreateDate())
                  .build();
          replyDTOList.add(replyDTO);
      }
      boardDTO.setReplyDTOList(replyDTOList); // boardDTO에 리플 리스트를 담음
      return boardDTO;
  }

4. Entity 부분

package net.datasa.web5.domain.entity;

import jakarta.persistence.*; // JPA 관련 어노테이션을 가져옵니다.
import lombok.*; // Lombok 어노테이션을 가져옵니다.
import org.springframework.data.annotation.CreatedDate; // 생성 일자를 나타내기 위한 어노테이션을 가져옵니다.
import org.springframework.data.jpa.domain.support.AuditingEntityListener; // JPA 엔티티 리스너를 가져옵니다.
import java.time.LocalDateTime; // 날짜와 시간을 나타내기 위한 클래스를 가져옵니다.

@Data // Lombok 어노테이션으로, getter, setter, toString, equals, hashCode 메서드를 자동으로 생성
@ToString(exclude = "board") // @Data안에 있는 ToString 기능도 있으나, 여기서는 BoardEntity정보를 다시 불러오는 것을 금하기 위해 사용(순환참조에러 해결)
@Builder // Lombok 어노테이션으로, 빌더 패턴을 사용할 수 있게 합니다.
@NoArgsConstructor // Lombok 어노테이션으로, 매개변수가 없는 생성자를 자동으로 생성
@AllArgsConstructor // Lombok 어노테이션으로, 모든 필드를 매개변수로 가지는 생성자를 자동으로 생성
@Entity // JPA 어노테이션으로, 이 클래스가 엔티티임을 나타냄
@Table(name = "web5_reply") // JPA 어노테이션으로, 엔티티와 매핑되는 테이블의 이름을 지정
@EntityListeners(AuditingEntityListener.class) // JPA 어노테이션으로, 엔티티 리스너를 지정하여 생성 및 수정 일자를 자동으로 관리

public class ReplyEntity {
    @Id // JPA 어노테이션으로, 기본 키(primary key) 필드를 나타냅니다.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // JPA 어노테이션으로, 기본 키의 생성 전략을 지정합니다.
    @Column(name = "reply_num") // JPA 어노테이션으로, 컬럼의 속성을 지정합니다.
    private Integer replyNum; // 리플 번호

    // 게시글 정보 (외래키로 참조)
    @ManyToOne(fetch = FetchType.LAZY) // Reply입장에서 다대일 관계 // (fetch~) : 필요할 때만 가져와 달라는 뜻
    @JoinColumn(name = "board_num") // web5_board 테이블 조인
    private BoardEntity board; // 리플이 달린 게시글과 게시글의 제목까지도 알 수 있음

    // 작성자 정보 (외래키로 참조)
    @ManyToOne(fetch = FetchType.LAZY) // Board입장에서 다대일 관계 // (fetch~) : 필요할 때만 가져와 달라는 뜻
    @JoinColumn(name = "member_id", referencedColumnName = "member_id") // web5_Member 테이블 조인
    private MemberEntity member;

    // 리플 내용
    @Column(name = "contents", nullable = false, length = 2000, columnDefinition = "text") // JPA 어노테이션으로, 컬럼의 속성을 지정합니다.
    private String contents; // 리플 내용

    // 리플 작성 시간
    @CreatedDate // 스프링 데이터 어노테이션으로, 엔티티가 생성될 때의 일자를 자동으로 설정합니다.
    @Column(name = "create_date", columnDefinition = "timestamp default current_timestamp")     // JPA 어노테이션으로, 컬럼의 속성을 지정합니다.
    private LocalDateTime createDate; // 생성 일자
}

5. Repository부분

package net.datasa.web5.repository;

import net.datasa.web5.domain.entity.ReplyEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/*
    리플 관련 repository
 */
@Repository // 안붙이면 bin 오류 발생
public interface ReplyRepository extends JpaRepository<ReplyEntity, Integer> {
    // 어떤 게시글 하나에 달린 리플 목록 전체 조회(리플번호 순으로 정렬)
    // select * from web5_reply where board_num = 게시글 번호 order by reply_num;
    List<ReplyEntity> findByBoard_BoardNumOrderByReplyNum(Integer boradNum);
//  = List<ReplyEntity> findByBoard_BoardNum(Integer boradNum ,Sort sort);
}
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함