package com.twelve.challengeapp.entity;
import jakarta.persistence.Entity;
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.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Table(name = "post") // 이 Entity가 매핑되는 DB 테이블의 이름 지정
@Entity // 이 클래스가 JPA Entity임을 나타냄
@Getter // Lombok 사용해 모든 필드에 대해 getter 메서드 자동 생성
@NoArgsConstructor // Lombok 사용해 매개변수 없는 기본 생성자 자동 생성
public class Post extends Timestamped {
@Id // Entity의 기본 키임을 나타냄
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키의 값을 DB가 자동으로 생성하도록 지정
private Long id; // 게시물 고유 ID
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY) // 다대일 관계, 지연 로딩
@JoinColumn(name = "user_id") // 외래 키 컬럼 이름 user_id
private User user; // 게시글 작성한 사용자 나타내는 필드, User Entity와 연관 관계
@Builder // 생성자 통해 빌더 패턴 사용
public Post(String title, String content) { // title, content 필드 초기화
this.title = title;
this.content = content;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
void setUser(User user) { // 게시글 작성자 설정 메서드 (같은 패키지 내에서만 접근 가능)
this.user = user;
}
}
클라이언트에서 서버로 요청 데이터 전송 (게시물 생성, 업데이트에 사용)
package com.twelve.challengeapp.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostRequestDto {
private String title;
private String content;
@Builder
public PostRequestDto(String title, String content) { // 매개변수 필드 초기화 (객체가 유효한 상태로 생성됨을 보장)
this.title = title; // 매개변수로 받은 title을 클래스의 title 필드에 할당
this.content = content;
}
}
// userID, createdAt, updatedAt 은 서버에서 관리되는 필드이기 때문에 클라이언트가 직접 설정 X
서버에서 클라이언트로 응답 데이터 전송 (게시물 조회 시 상세 정보 반환에 사용)
package com.twelve.challengeapp.dto;
import com.twelve.challengeapp.entity.Post;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Getter
@NoArgsConstructor // 기본 생성자 자동 생성 (아무런 인자도 받지 않는다)
public class PostResponseDto {
private Long id;
private Long username;
private String title;
private String content;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Post Entity 객체 받아서, 해당 Entity의 데이터를 DTO 필드에 복사
public PostResponseDto(Post post) {
this.id = post.getId(); // Post Entity의 id 필드를 PostResponseDto의 id 필드에 복사
this.username = post.getUser().getUsername();
this.title = post.getTitle();
this.content = post.getContent();
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
}
}
JPA 사용해 Post Entity에 대한 DB 작업 수행
package com.twelve.challengeapp.repository;
import com.twelve.challengeapp.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {
}
/*JpaRepository 인터페이스 상속받음으로써 Post Entity에 대해 기본적인 CRUD 작업 수행*/
클라이언트 요청 처리, 요청에 따라 서비스 계층 호출해 비즈니스 로직 수행 후, 응답 생성해 반환
package com.twelve.challengeapp.controller;
import com.twelve.challengeapp.dto.PostRequestDto;
import com.twelve.challengeapp.dto.PostResponseDto;
import com.twelve.challengeapp.jwt.UserDetailsImpl;
import com.twelve.challengeapp.service.PostService;
import com.twelve.challengeapp.util.SuccessResponseFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController // JSON 또는 XML 형식으로 응답 반환
@RequestMapping("/api/posts") // 클래스의 모든 메서드가 이 url 경로에 매핑
@RequiredArgsConstructor // final이 붙은 필드들을 매개변수로 갖는 생성자 자동 생성 (의존성 주입 처리 간편)
public class PostController {
private final PostService postService; // 필드 주입 위한 생성자 생성
// 게시글 등록
@PostMapping
public ResponseEntity<?> createPost(@RequestBody PostRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
Long userId = userDetails.getUserId(); // 인증된 사용자 ID 가져온다
PostResponseDto responseDto = postService.createPost(requestDto, userId);
return SuccessResponseFactory.ok(responseDto); // 200 ok 상태와 함께 반환
}
/*
@RequestBody PostRequestDto requestDto: 클라이언트가 보낸 JSON 데이터를 postRequestDto 객체로 변환해 매개변수로 받는다
@AuthenticationPrincipal UserDetailsImpl userDetails: Spring Security 통해 인증된 사용자 정보 매개변수로 받는다
*/
// 선택 게시글 조회
@GetMapping("/{postId}")
public ResponseEntity<?> getPost(@PathVariable Long postId) { // 경로에서 추출한 postId 파라미터로 받는다
PostResponseDto responseDto = postService.getPost(postId); // getPost 호출해 postId에 해당하는 게시글 조회 후 PostResponseDto로 반환
return SuccessResponseFactory.ok(responseDto); // 생성된 게시물 정보 포함한 성공 응답 반환
}
// 전체 게시글 조회
@GetMapping
// 기본 페이지 크기 5, createdAt 기준 내림차순 정렬
public ResponseEntity<?> getPosts(@PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<PostResponseDto> posts = postService.getPosts(pageable); // getPosts 메서드 호출해 Page<PostResponseDto> 반환
return SuccessResponseFactory.ok(posts); // 게시글 목록 posts 반환
}
// 선택 게시글 수정
@PutMapping("/{postId}")
public ResponseEntity<?> updatePost(@PathVariable Long postId, @RequestBody PostRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
Long userId = userDetails.getUserId();
PostResponseDto responseDto = postService.updatePost(postId, requestDto, userId); // postId 에 해당하는 게시글 업데이트하고, 업데이트 된 PostResponseDto 반환
return SuccessResponseFactory.ok(responseDto);
}
// 선택 게시글 삭제
@DeleteMapping("/{postId}")
public ResponseEntity<?> deletePost(@PathVariable Long postId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
Long userId = userDetails.getUserId();
postService.deletePost(postId, userId);
return SuccessResponseFactory.noContent(); // 삭제 작업 성공적
}
}
CRUD 핵심 기능 정의, 각 메서드는 DTO를 통해 데이터 주고받는다
package com.twelve.challengeapp.service;
import com.twelve.challengeapp.dto.PostRequestDto;
import com.twelve.challengeapp.dto.PostResponseDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface PostService {
PostResponseDto createPost(PostRequestDto requestDto, Long userId);
Page<PostResponseDto> getPosts(Pageable pageable); // 페이징 정보 담은 Pageable 객체
PostResponseDto getPost(Long id);
PostResponseDto updatePost(Long id, PostRequestDto requestDto, Long userId);
void deletePost(Long id, Long userId);
}
PostService 인터페이스 구현해 CRUD 기능 제공
package com.twelve.challengeapp.service;
import com.twelve.challengeapp.dto.PostRequestDto;
import com.twelve.challengeapp.dto.PostResponseDto;
import com.twelve.challengeapp.entity.Post;
import com.twelve.challengeapp.entity.User;
import com.twelve.challengeapp.exception.PostNotFoundException;
import com.twelve.challengeapp.exception.UserNotFoundException;
import com.twelve.challengeapp.repository.PostRepository;
import com.twelve.challengeapp.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service // Spring이 이 클래스를 서비스 빈으로 인식하고 관리
@RequiredArgsConstructor // final 필드를 매개변수로 받는 생성자 자동 생성
public class PostServiceImpl implements PostService {
private final PostRepository postRepository; // 게시글 관련 DB 작업
private final UserRepository userRepository; // 사용자 관련 DB 작업
@Override
@Transactional // 메서드 내에서 수행되는 작업들이 하나의 트랜잭션으로 처리됨을 보장
public PostResponseDto createPost(PostRequestDto requestDto, Long userId) {
User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException("Not found user.")); // userId로 사용자 조회
Post post = Post.builder().title(requestDto.getTitle()).content(requestDto.getContent()).build(); // 게시글 Entity 생성
user.addPost(post); // 사용자의 게시글 리스트에 새 게시글 추가
Post savedPost = postRepository.save(post); // 게시글 DB에 저장
return new PostResponseDto(savedPost); // 저장된 게시글 정보 반환
}
@Override
public Page<PostResponseDto> getPosts(Pageable pageable) { // 페이징 정보 기반으로 모든 게시글 조회
return postRepository.findAll(pageable).map(PostResponseDto::new); // 매핑해 반환
}
@Override
public PostResponseDto getPost(Long id) {
Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException("Post not found"));
return new PostResponseDto(post);
}
@Override
@Transactional
public PostResponseDto updatePost(Long id, PostRequestDto requestDto, Long userId) {
Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException("Post not found"));
if (!(post.getUser().getId() == userId)) {
throw new SecurityException("You are not authorized to update this post");
} // 게시글 작성자와 요청 사용자가 동일한지 확인, 다르면 예외처리
post.update(requestDto.getTitle(), requestDto.getContent());
return new PostResponseDto(post); // 수정된 게시글 정보 반환
}
@Override
@Transactional
public void deletePost(Long id, Long userId) {
Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException("Post not found")); // 주어진 id로 게시글 조회
if (!(post.getUser().getId() == userId)) {
throw new SecurityException("You are not authorized to delete this post");
}
postRepository.delete(post);
}
}