최원종의 개발 블로그

V16-3 Board 상세보기 유료/무료 구분 본문

Spring boot 입문/Spring boot 입문 -1

V16-3 Board 상세보기 유료/무료 구분

chl6698 2026. 6. 25. 15:54


BoardController⬇️​

  • 유료 게시글일때 구매여부 포함해서 상세보기 조회
  • 자바코드에서 논리 연산실행 (머스태치는 연산 불가)
더보기
// 게시글 상세보기 화면 요청
  // http://localhost:8080/board/1
  @GetMapping("/board/{id}")
  public String detailPage(@PathVariable(name = "id") Integer id, Model model, HttpSession session) {


      // 게시글 상세보기는 로그인 하지 않은 사용자도 들어올 수 있음
      User sessionUser = (User) session.getAttribute("sessionUser");
      Integer sessionUserId = sessionUser != null ? sessionUser.getId() : null;

      // 추가 로직 - 유료 게시글이라면 구매 여부 포함해서 상세 보기 조회
      BoardResponse.DetailDTO detailDTO = boardService.게시글상세조회(id, sessionUserId);

      // 유료 게시글 경우 - 본인이 작성할 글이라면 보여 주어야 함
      // 소유자 여부
      boolean isOwner = detailDTO.checkIsOwner(sessionUserId);

      // 머스태치 는 AND/OR 같은 논리 연산을 화면에서 못함. 즉, 자바 코드에서 미리 연산해서
      boolean canRead = !detailDTO.getPremium() || detailDTO.getPurchased() || isOwner;
      
      List<ReplyResponse.ListDTO> replyList = replyService.댓글목록조회(id, sessionUserId);

      // view 에 데이터 전달 - canRead -> true, false
      model.addAttribute("board", detailDTO);
      model.addAttribute("canRead", canRead);
      model.addAttribute("checkIsOwner", detailDTO.checkIsOwner(sessionUserId));
      model.addAttribute("replyList", replyList);

      return "board/detail";
  }

BoardResponse 추가⬇️

private Boolean premium; // 유료 게시글 여부
        private Boolean purchased; // 현재 로그인 사용자의 구매 여부
        
        . . . 
        
  public DetailDTO(Board board, Boolean isPurchased) {
            this(board);
            this.purchased = isPurchased;
        }
더보기
package com.tenco.blog.board;

import com.tenco.blog._core.util.MyDateUtil;
import lombok.Data;
import org.springframework.data.domain.Page;

import java.util.ArrayList;
import java.util.List;

/**
 * 게시글 응답 DTO
 * <p>
 * Open Session In View 가 false 일 때
 * 트랜잭션이 종료 되는 시점에 LAZY 로딩이 불가능하다.
 * Service 단에서 필요한 데이터를 모두 조회 또는 일부로 호출(트리거)해서 응답 DTO 변환해서 반환
 * 엔티티를 직접 반환하지 않고(Controller,View) 서비스단에서 DTO 내려줄 예정(결합도 감소)
 */
public class BoardResponse {

    @Data
    public static class DetailDTO {
        private Integer id; // board PK
        private String title;
        private String content;
        private String username;
        private Integer userId; //  user PK
        private boolean isOwner;
        private Boolean premium; // 유료 게시글 여부
        private Boolean purchased; // 현재 로그인 사용자의 구매 여부

        public DetailDTO(Board board) {
            this.id = board.getId();
            this.title = board.getTitle();
            this.content = board.getContent();
            this.premium = board.getPremium();
            if(board.getUser() != null) {
                this.username = board.getUser().getUsername();
                this.userId = board.getUser().getId();
            }
        }
			
        public DetailDTO(Board board, Boolean isPurchased) {
            this(board);
            this.purchased = isPurchased;
        }


        // 소유자 확인
        public boolean checkIsOwner(Integer sessionUserId) {
            if(sessionUserId == null) {
                return false;
            }
            if (sessionUserId.equals(this.userId)) {
                return true;
            } else {
                return false;
            }
        }

    } // end of DetailDTO inner class


} // end of outer class

BoardService 수정 ⬇️​

private final PurchaseService purchaseService;

. . .

// 구매 여부 확인 추가 (로그인 사용자가 있을 때만 의미 있음, 비 로그인 시 null)
        boolean purchased = purchaseService.구매여부확인(sessionUserId, id);

        return new BoardResponse.DetailDTO(boardEntity, purchased);
더보기
   private final PurchaseService purchaseService;
 
 /**
     * 게시글 상세 조회
     * @param id (Board PK)
     * @return DetailDTO 처리 (OSIV 대응)
     */
    public BoardResponse.DetailDTO 게시글상세조회(Integer id, Integer sessionUserId) {

        Board boardEntity = boardRepository.findByIdJoinUser(id).orElseThrow(() -> {
            log.warn("게시글 조회 실패 - ID: {}", id);
            return new Exception404("해당하는 게시글을 찾을 수 없습니다");
        });

        // 구매 여부 확인 추가 (로그인 사용자가 있을 때만 의미 있음, 비 로그인 시 null)
        boolean purchased = purchaseService.구매여부확인(sessionUserId, id);

        return new BoardResponse.DetailDTO(boardEntity, purchased);
    }
    
    
    
    /**
     * 게시글 상세 화면 요청(인가 처리)
     *
     * @param id          (Board PK)
     * @param sessionUser (로그인한 사용자 정보)
     * @return BoardResponse.DetailDTO
     */
    public BoardResponse.DetailDTO 게시글상세화면및인가처리(Integer id, User sessionUser) {
        log.info("게시글 상세 화면 및 인가 확인");
        BoardResponse.DetailDTO detailDTO = 게시글상세조회(id, sessionUser.getId());
        if(!detailDTO.getUserId().equals(sessionUser.getId())) {
            throw new Exception403("권한없음");
        }
        log.info("게시글 수정 조회 완료 - 제목: {}, 작성자: {}",
                detailDTO.getTitle(), detailDTO.getUsername());
        return detailDTO;
    }

detail.mustache 코드

더보기
{{> layout/header}}
<div class="container p-5">

    <!-- 수정삭제버튼 섹션 -->
    {{#checkIsOwner}}
    <div class="d-flex justify-content-end">
        <a href="/board/{{board.id}}/update-form" class="btn btn-warning me-1">수정</a>
        <form action="/board/{{board.id}}/delete" method="post">
            <button class="btn btn-danger">삭제</button>
        </form>
    </div>
    {{/checkIsOwner}}

    <!-- 작성자 정보 표시 -->
    <div class="d-flex justify-content-end">
        <b>작성자</b> : {{board.username}}
    </div>

    <!-- 게시글 내용 섹션 -->
    <div>
        <h2><b>{{board.title}}</b> {{#board.premium}} <span class="badge bg-warning"> 유료 </span>  {{/board.premium}}  </h2>
        <hr />
        <div class="m-4 p-2">
            {{#canRead}}
                {{board.content}}
            {{/canRead}}
            {{^canRead}}
                <!--   로그인 시 - 이미 구매했다면 보여야 함         -->
                {{#sessionUser}}
                    <form action="/board/{{board.id}}/purchase" method="post"
                          onsubmit="return confirm('구매하시겠습니까?');">
                        <button type="submit" class="btn btn-primary">구매하기(500P)</button>
                    </form>
                {{/sessionUser}}
                {{^sessionUser}}
                    <a href="/login-form" class="btn btn-outline-primary">로그인 후 구매가능</a>
                {{/sessionUser}}
                <!--   비 로그인 시 - 로그인 요청          -->
            {{/canRead}}
        </div>
    </div>

    <!-- 댓글 섹션 시작 -->
    <div class="card mt-3">
    {{^sessionUser}}
        <div class="card-body">
            <p class="text-muted text-center p-2">댓글을 작성하려면 <a href="/login-form">로그인</a>이 필요합니다</p>
        </div>
    {{/sessionUser}}
    {{#sessionUser}}
        <div class="card-body">
            <form action="/reply/save" method="post">
                <input type="hidden" name="boardId" value="{{board.id}}" />
                <textarea name="comment" class="form-control"></textarea>
                <div class="d-flex justify-content-end mt-2">
                    <button type="submit" class="btn btn-outline-primary">댓글 등록</button>
                </div>

            </form>
        </div>
    {{/sessionUser}}
    {{^replyList}}
        <p class="text-muted text-center p-2">현재 작성된 댓글이 없습니다</p>
    {{/replyList}}
    {{#replyList}}
        <div class="list-group">
            <div class="list-group-item d-flex justify-content-between align-items-center">
                <div class="d-flex flex-column">
                    <div class="d-flex align-items-center mb-1" >
                        <div class="px-2 py-1 bg-primary text-white rounded"> {{username}} </div>
                        <small class="text-muted">{{createdAt}}</small>
                    </div>
                    <div>{{comment}}</div>
                </div>
                {{#isOwner}}
                    <form action="/reply/{{id}}/delete" method="post">
                        <input type="hidden" name="boardId" value="{{board.id}}" >
                        <button type="submit" class="btn btn-danger btn-sm">🗑️ 삭제</button>
                    </form>
                {{/isOwner}}
            </div>
        </div>
    {{/replyList}}

    </div>
    <!-- 댓글 섹션 종료 -->
</div>

{{> layout/footer}}

update-form.mustache ( 주석 해제)

더보기
   <div class="mb-3">
        <!-- board.premium 컬럼 추가시 주석 해제     -->
        <input type="checkbox" id="premium" name="premium" class="form-check-input"
           {{#board.premium}} checked {{/board.premium}}>
       <label for="premium" class="form-check-label">유료 게시글 등록(열람시 500포인트 차감 됨)</label>
   </div>