Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
Tags
- IntelliJ IDEA
- 형 변환
- this예약어
- While
- 인텔리제이 기초 설정
- 접근제어지시자
- 집합관계
- 연관관계
- Java
- JAVA객체지향
- break문
- multi-threading
- 메서드 오버로딩
- 시스템 환경 변수 편집
- 컴파일
- 자바 멀티스레딩
- Java데이터 타입
- for문
- 상수
- java변수
- function
- continue문
- 인텔리제이 한글 깨짐 해결법
- 포함관계
- Thread
- 생성자
- JAVA기초
- OPP개념
- 메서드
- 반복문
Archives
- Today
- Total
최원종의 개발 블로그
V16-2 포인트 시스템의 개념과 유료 게시글 본문
Purchase 도메인 추가
Purchase 엔티티 설계⬇️
더보기
package com.tenco.blog.purchase;
import com.tenco.blog.board.Board;
import com.tenco.blog.user.User;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
/**
* 구매 내역 엔티티
*
* User 와 Board 의 구매 이력 관계를 표현 함
*
* 한 사람에 사용자는 여러 게시글을 구매할 수 있다. (o)
* 한 게시글을 여러 사용자에게 구매 될 수 있다. (o)
* User : Board - 다대다 관계로 표현이 되기 때문게 중간 테이블 (Purchase) 생성이 되어야 한다
* Purchase : User --> @ManyToOne --> join column 이름 지정
* Purchase : Board --> @ManyToOne --> join column 이름 지정
* 복합키 설정 방법도 확인 (테이블 기준으로 어노테이션 설정)
*/
@Data
@NoArgsConstructor
@Entity
@Table(name = "purchase_tb",
uniqueConstraints = {
@UniqueConstraint(name = "uk_user_board", columnNames = {"user_id", "board_id"})
})
public class Purchase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// 단방향 관계 " Purchase -> User "
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
// 단방향 관계 " Purchase -> Board "
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
// 구매시 지불한 포인트
private Integer price;
@CreationTimestamp // pc -> db 자동 주입
private Timestamp createdAt;
@Builder
public Purchase(User user, Board board, Integer price) {
this.user = user;
this.board = board;
this.price = price;
}
}
PurchaseRepository 생성⬇️
더보기
package com.tenco.blog.purchase;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository // 가독성 때문에 명시하기도 함
public interface PurchaseRepository extends JpaRepository<Purchase, Integer> {
// 기본적인 CRUDE 생성 및 추가 기능 자동 생성 됨...
// 사용자(상세보기)와 게시글의 구매 내역 조회
@Query("""
SELECT p FROM Purchase p WHERE p.user.id = :userId AND p.board.id = :boardId
""")
Optional<Purchase> findByUserIdAndBoardId(@Param("userId") Integer userId,
@Param("boardId") Integer boardId);
// 사용자(상세보기)와 게시글의 구매 여부 확인
@Query("""
SELECT COUNT(p) > 0 FROM Purchase p WHERE p.user.id = :userId AND p.board.id = :boardId
""")
boolean existsByUserIdAndBoardId(@Param("userId") Integer userId,
@Param("boardId") Integer boardId);
}
PurchaseService⬇️
더보기
package com.tenco.blog.purchase;
import com.tenco.blog._core.errors.Exception400;
import com.tenco.blog._core.errors.Exception404;
import com.tenco.blog.board.Board;
import com.tenco.blog.board.BoardRepository;
import com.tenco.blog.user.User;
import com.tenco.blog.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PurchaseService {
private final PurchaseRepository purchaseRepository;
private final BoardRepository boardRepository;
private final UserRepository userRepository;
// 유료 게시글 기본 가격 설정
private static final Integer PREMIUM_BOARD_PRICE = 500;
/**
* 유료 게시글 구매 로직
* <p>
* 트랜 잭션 처리 : 포인트 차감과 구매 내역(저장)을 하나의 트랜잭션으로 처리 해야 한다.
*
*/
@Transactional
public User 구매하기(Integer userId, Integer boardId) {
// 1. 게시글 조회
Board boardEntity = boardRepository.findById(boardId)
.orElseThrow(() -> new Exception404("게시글을 찾을 수 없습니다"));
// 2. 유료 게시글 여부 확인
// board <-- 컬럼 추가 예정 (premium) , 유료, 무료 확인
// 3. 작성자가 자신의 게시글을 구매 하려는 경우는 방지
if (boardEntity.getUser() != null && boardEntity.getUser().getId().equals(userId)) {
throw new Exception400("자신이 작성한 게시글을 구매할 수 없습니다");
}
// 4. 이미 구매한 게시글인지 확인
if (purchaseRepository.existsByUserIdAndBoardId(userId, boardId)) {
throw new Exception400("이미 구매한 게시글입니다");
}
// 5. 사용자 조회 (포인트 차감 처리)
User userEntity = userRepository.findById(userId)
.orElseThrow(() -> new Exception404("사용자를 찾을 수 없습니다"));
// 6. 포인트 차감
userEntity.deductPoint(PREMIUM_BOARD_PRICE);
// 구매 내역 저장
Purchase purchase = Purchase
.builder()
.user(userEntity)
.board(boardEntity)
.build();
// 구매 이력 insert
purchaseRepository.save(purchase);
// 사용자 포인트 차감된 상태를 update 처리
// Controller 단에서 차감된 포인트 상태를 위해 세션 동기화 (반환)
return userRepository.save(userEntity);
}
// 상세보기 진입시 (유료 게시글 이라면)
// 구매 여부 확인
public boolean 구매여부확인(Integer userId, Integer boardId) {
if (userId == null) {
return false;
}
return purchaseRepository.existsByUserIdAndBoardId(userId, boardId);
}
}
유료 게시글 기능 추가 (글 저장 시)

save-form.mustache⬇️
더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
<div class="card">
<div class="card-header"><b>글쓰기 화면입니다</b></div>
<div class="card-body">
<form action="/board/save" method="post">
<div class="mb-3">
<input type="text" name="title" class="form-control" placeholder="enter title">
</div>
<div class="mb-3">
<textarea type="text" name="content" rows="5" class="form-control" placeholder="enter content"></textarea>
</div>
<div class="mb-3">
<input type="checkbox" id="premium" name="premium" value="true" class="form-check-input">
<label for="premium" class="form-check-label">유료 게시글 등록(열람시 500포인트 차감 됨)</label>
</div>
<button class="btn btn-primary">글쓰기 완료</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
update-from.mustache⬇️
더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
<div class="card">
<div class="card-header"><b>글수정 화면입니다</b></div>
<div class="card-body">
<!-- 익명 게시글 작성 -->
<form action="/board/{{id}}/update" method="post">
<div class="mb-3">
<input type="text" name="username" class="form-control"
placeholder="enter username" value="{{board.username}}" readonly>
</div>
<div class="mb-3">
<input type="text" name="title" class="form-control"
placeholder="enter title" value="{{board.title}}">
</div>
<div class="mb-3">
<textarea type="text" name="content" rows="5" class="form-control" placeholder="enter title">
{{board.content}}
</textarea>
</div>
<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>
<button class="btn btn-primary">글수정 완료</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
BoardRequest⬇️
- 유료 게시글 여부(체크박스 추가)
더보기
package com.tenco.blog.board;
import com.tenco.blog.user.User;
import lombok.Builder;
import lombok.Data;
// 요청 데이터를 담는 DTO 클래스
// 컨트롤러.. 비즈니스 .. 데이터 계층 사이에서 데이터 전송 역할 객체
public class BoardRequest {
@Data
@Builder
public static class SaveDTO {
private String title;
private String content;
private Boolean premium; // 유료 게시글 여부 (체크 박스)
// 편의 기능 설계 가능
// DTO 에서 Entity로 변환해주는 편의 메서드
public Board toEntity(User user) {
return Board.builder()
.title(title)
.user(user)
.content(content)
.premium(premium != null ? premium : false)
.build();
}
public void validate() {
if(title == null || title.trim().isEmpty()) {
throw new IllegalArgumentException("제목은 필수입니다");
}
if(content == null || content.length() < 3) {
throw new IllegalArgumentException("내용은 3글자 이상 작성해야 합니다.");
}
}
}
// 내부 정적 클래스 게시글 수정 DTO 설계
@Data
public static class UpdateDTO {
private String title;
private String content;
private Boolean premium; // 수정시 유료 게시글 여부 (체크 박스)
// 게시글 수정시 유효성 검사 편의 메서드
public void validate() {
if(title == null || title.trim().isEmpty()) {
throw new IllegalArgumentException("제목은 필수입니다");
}
if(content== null || content.length() < 3) {
throw new IllegalArgumentException("내용은 3글자 이상 작성해야 합니다.");
}
}
}
}
Board 코드 추가⬇️
@ColumnDefault("false")
@Builder.Default // 이게 없으면 다른 파일에서 Build 객체 생성시 null 값이 셋팅 됨
private Boolean premium = false;
. . .
// 유료 여부도 함께 수정
this.premium = (updateDTO.getPremium() != null ? updateDTO.getPremium() : false);
더보기
package com.tenco.blog.board;
import com.tenco.blog._core.errors.Exception403;
import com.tenco.blog._core.util.MyDateUtil;
import com.tenco.blog.user.User;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@Data
@Entity
@Table(name = "board_tb")
@NoArgsConstructor
@AllArgsConstructor // 전체 멤벼 번수를 넣을 수 있는 생성자.
@Builder
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
private String content;
@ColumnDefault("false")
@Builder.Default // 이게 없으면 다른 파일에서 Build 객체 생성시 null 값이 셋팅 됨
private Boolean premium = false;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") // 외래키 컬럼명 표시 됨
private User user;
@CreationTimestamp
private Timestamp createdAt;
public String getTime() {
return MyDateUtil.timestampFormat(createdAt);
}
public void update(BoardRequest.UpdateDTO updateDTO) {
this.title = updateDTO.getTitle();
this.content = updateDTO.getContent();
// 유료 여부도 함께 수정
this.premium = (updateDTO.getPremium() != null ? updateDTO.getPremium() : false);
}
public boolean isOwner(Integer sessionUserId) {
if(!this.user.getId().equals(sessionUserId)) {
throw new Exception403("본인이 작성한 게시글이 아닙니다");
}
return true;
}
}
BoardController( 수정사항 x)⬇️
더보기
/**
* 게시글 작성 기능 요청
* @return 페이지 반환
* 주소설계 : http://localhost:8080/board/save-form
*/
@PostMapping("/board/save")
public String saveProc(BoardRequest.SaveDTO saveDTO, HttpSession session) {
User sessionUser = (User) session.getAttribute(Define.SESSION_USER);
saveDTO.validate();
boardService.게시글작성(saveDTO, sessionUser);
return "redirect:/";
}
BoardService (수정 사항 x)⬇️
더보기
/**
* 게시글 작성
* @param saveDTO
* @param sessionUser (세션에서 가져온 사용자 정보)
*/
@Transactional
public void 게시글작성(BoardRequest.SaveDTO saveDTO, User sessionUser) {
Board board = saveDTO.toEntity(sessionUser);
boardRepository.save(board);
}
'Spring boot 입문 > Spring boot 입문 -1' 카테고리의 다른 글
| V16-4 유료 게시글 구매하기 (0) | 2026.06.25 |
|---|---|
| V16-3 Board 상세보기 유료/무료 구분 (0) | 2026.06.25 |
| V16-1 포인트 시스템(유료 게시글 기능 추가) (0) | 2026.06.23 |
| V15-4 이메일 인증 후 회원가입 로직 변경 (0) | 2026.06.23 |
| V15-3 인증 이메일 발송 API 테스트 (0) | 2026.06.18 |
