티스토리 뷰
멤버 로직을 만들었으니, 이젠 게시글 CRUD를 구현해보겠다.
Entity 설계
게시글 Create(생성), Read(읽기), Update(업데이트), Delete(삭제)를 위해 필요한 엔티티는 Article이다.
Article entity
package com.example.MyFreshmanCommunity.entity;
import com.example.MyFreshmanCommunity.dto.ArticleDto;
import com.example.MyFreshmanCommunity.repository.MajorRepository;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.Optional;
@Entity
@Getter
@Setter
@AllArgsConstructor //모든 필드를 매개변수로 갖는 생성자 자동 생성
@NoArgsConstructor //매개변수가 아예 없는 기본 생성자 자동 생성
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private String content;
@Column
private int bookmarkCount;
@Column
private LocalDateTime createDate;
@ManyToOne
@JoinColumn(name = "member_id")
@OnDelete(action = OnDeleteAction.SET_NULL)
private Member member;
@ManyToOne
@JoinColumn(name = "major_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Major major;
public static Article createArticle(ArticleDto articleDto, Member member, Major major){
return new Article(
null,
articleDto.getTitle(),
articleDto.getContent(),
0,
LocalDateTime.now(),
member,
major
);
}
public void patch(ArticleDto articleDto) { //수정할 내용이 있는 경우에만 동작
if(articleDto.getTitle() != null)
this.title = articleDto.getTitle();
if(articleDto.getContent() != null)
this.content = articleDto.getContent();
}
}
게시글에 필요한 필드를 작성해주고, member와 major는 @ManyToOne을 사용하여 1:N 관계를 나타내주었다.
특히 Member 엔티티가 삭제될 경우, 해당 Member에 연결된 Article의 member 필드는 @OnDelete(action = OnDeleteAction.SET_NULL) 어노테이션을 통해 null로 설정된다.
반면, Major 엔티티가 삭제될 경우 @OnDelete(action = OnDeleteAction.CASCADE) 어노테이션에 의해 연결된 Article들도 함께 삭제된다.
createArticle 메서드는 ArticleDto, Member, Major 객체를 매개변수로 받아 새로운 Article 객체를 생성하고 반환한다.
patch 메서드는 밑에 설명할 수정할 내용이 담긴 ArticleDto 객체를 받아 기사를 수정하는 역할을 한다.
DTO
ArticleDto
package com.example.MyFreshmanCommunity.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
//@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
public class ArticleDto {
private String title;
private String content;
}
게시글 생성 및 수정 시 클라이언트에서 서버로 데이터를 전송하는 데 사용되는 dto이다.
제목(title)과 내용(content)을 담고있다.
ArticleResponseDto
package com.example.MyFreshmanCommunity.dto;
import com.example.MyFreshmanCommunity.entity.Article;
import com.example.MyFreshmanCommunity.entity.Major;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
//@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
public class ArticleResponseDto {
private Long id;
private String title;
private String content;
private int bookmarkCount;
private LocalDateTime createDate;
// 유저의 메이저 정보를 포함
private MemberInfoDto memberInfo;
// 게시글의 메이저 정보를 포함 (유저의 메이저와 게시글의 메이저가 다를 수 있으므로)
private Major boardMajor;
public static ArticleResponseDto createArticleDto(Article n) {
MemberInfoDto memberInfo = n.getMember() != null ?
MemberInfoDto.createMemberDto(n.getMember()) : null; //Member가 null인 경우 MemberInfo를 null로 설정
return new ArticleResponseDto(
n.getId(),
n.getTitle(),
n.getContent(),
n.getBookmarkCount(),
n.getCreateDate(),
memberInfo,
n.getMajor()
);
}
}
ArticleResponseDto는 반대로 서버에서 클라이언트로 데이터를 전송하는 데 사용되는 dto이다.
특히, MemberInfoDto는 사용자의 전체 정보를 직접 전달하는 것이 아니라, 사용자의 ID, 이름, 학번, 전공 정보만을 포함하고 있다. 이렇게 별도로 DTO를 정의하는 이유는, 사용자 정보 중 민감한 데이터(비밀번호)를 제외하고 필요한 정보만을 안전하게 전달하기 위함이다.
- 헷갈릴 수 있는데, ArticleResponseDto에선 이 글을 작성한 member의 major와, 이 게시글이 속한 게시판의 major가 모두 필요하다!
ex) 경영학과 학생이 컴퓨터공학과 게시판에 글을 쓸 경우
- java의 삼항 연산자를 이용하여 member가 null일 경우 memberInfo를 null로 설정해주었다. (자세한 건 회원탈퇴 시 오류해결 2 참고)
MemberInfoDto
package com.example.MyFreshmanCommunity.dto;
import com.example.MyFreshmanCommunity.entity.Major;
import com.example.MyFreshmanCommunity.entity.Member;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Data
//@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class MemberInfoDto {
private Long id;
private String memberName;
private String studentId;
private Major major;
public static MemberInfoDto createMemberDto(Member member){
return new MemberInfoDto(
member.getId(),
member.getMemberName(),
member.getStudentId(),
member.getMajor()
);
}
}
Repository
ArticleRepository
package com.example.MyFreshmanCommunity.repository;
import com.example.MyFreshmanCommunity.entity.Article;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ArticleRepository extends JpaRepository<Article, Long> {
//특정 전공의 모든 게시글 조회
@Query(value = "SELECT * FROM article WHERE major_id = :majorId ORDER BY create_date DESC",
nativeQuery = true)
List<Article> findAllByMajorId(Long majorId);
@Query(value = "SELECT * FROM article WHERE major_id = :majorId AND id = :articleId",
nativeQuery = true)
Article findByArticleId(@Param("majorId") Long majorId, @Param("articleId") Long articleId);
}
@Query 어노테이션을 사용하여 특정 전공에 속하는 모든 게시글을 조회하는 findAllByMajorId 메서드와,
특정 전공 내에서 특정 ID를 가진 게시글을 조회하는 findByArticleId 메서드를 만들어주었다.
특히 특정 전공의 모든 게시글을 조회할 때는 ORDER BY create_date DESC 를 사용하여 create_date(게시글 생성날짜)를 기준으로 최신순으로 정렬될 수 있도록 해주었다.
Service
ArticleService
@Service
@Slf4j
@RequiredArgsConstructor
public class ArticleService {
private final ArticleRepository articleRepository;
private final MajorRepository majorRepository;
//게시글 전체 조회
public List<ArticleResponseDto> showAll(Long majorId) {
List<Article> articles = articleRepository.findAllByMajorId(majorId);
List<ArticleResponseDto> dtos = new ArrayList<ArticleResponseDto>();
for (Article a : articles) {
ArticleResponseDto dto = ArticleResponseDto.createArticleDto(a);
dtos.add(dto);
}
return dtos;
}
//게시글 단건 조회
public ArticleResponseDto show(Long majorId, Long articleId) {
Article article = articleRepository.findByArticleId(majorId, articleId);
ArticleResponseDto dto = ArticleResponseDto.createArticleDto(article);
return dto;
}
게시글 조회 로직에서 위에서 만들었던 ArticleResponseDto로 반환을 해주는 모습을 확인할 수 있다.
//게시글 생성
public void create(Long majorId, ArticleDto articleDto, HttpSession session) {
Member member = (Member) session.getAttribute("member");
Major major = majorRepository.findById(majorId)
.orElseThrow(() -> new NotFoundException("존재하지 않는 전공입니다"));
if(member == null) throw new MemberNotFoundException("로그인 하지 않은 상태에서 글을 쓸 수 없습니다.");
Article article = Article.createArticle(articleDto, member, major);
articleRepository.save(article);
}
//게시글 수정
public ArticleResponseDto update(Long majorId, Long articleId, ArticleDto articleDto, HttpSession session) {
Article target = articleRepository.findByArticleId(majorId, articleId);
Member member = (Member) session.getAttribute("member");
if(target == null) {
throw new NotFoundException("대상 게시글이 없습니다.");
}
if(!target.getMember().getId().equals(member.getId())) {
throw new NotPermissionException("다른 사람의 게시물을 수정할 수 없습니다.");
}
target.patch(articleDto);
Article updated = articleRepository.save(target);
return ArticleResponseDto.createArticleDto(updated);
}
생성, 수정 로직에선 위에서 만든 ArticleDto를 사용하고 있음을 볼 수 있다.
근데 지우려는 게시글이 로그인한 유저가 아닌 경우 수정, 삭제 불가 부분인
if(!target.getMember().getId().equals(member.getId())) {
throw new NotPermissionException("다른 사람의 게시물을 수정할 수 없습니다.");
}
이 부분을 작성하는 과정에서 오류가 생겼다.
처음엔 그냥 if(!target.getMember().equals(member)) 이렇게 객체끼리만 비교를 했었는데, 포스트맨으로 확인을 하는 과정에서 분명히 같은 멤버인데도 계속 "다른 사람의 게시물을 수정할 수 없습니다."라는 예외 메시지가 떴다. 그래서 찾아보니, 멤버객체끼리만 비교하면 속성 값이 같아도 서로 다른 메모리 주소에 위치해서 false를 반환한다....!! 따라서 객체끼리의 비교가 아닌 id끼리만 비교하도록 (어차피 고유한 id 이므로) 수정해주었다.
//게시글 삭제
public void delete(Long majorId, Long articleId, HttpSession session) {
Article article = articleRepository.findByArticleId(majorId, articleId);
Member member = (Member) session.getAttribute("member");
if(!article.getMember().getId().equals(member.getId())) {
throw new NotPermissionException("다른 사람의 게시물을 삭제할 수 없습니다.");
}
articleRepository.delete(article);
}
삭제도 마찬가지로 id끼리만 비교할 수 있도록 수정해주었다.
Controller
ArticleApiController
@RestController
@RequiredArgsConstructor
public class ArticleApiController {
private final ArticleService articleService;
//게시글 전체조회
@GetMapping("/article/{majorId}")
public ResponseEntity<List<ArticleResponseDto>> showAll(@PathVariable Long majorId) {
List<ArticleResponseDto> articles = articleService.showAll(majorId);
return ResponseEntity.status(HttpStatus.OK).body(articles);
}
//게시글 단건조회
@GetMapping("/article/{majorId}/{articleId}")
public ResponseEntity<ArticleResponseDto> show(@PathVariable Long majorId, @PathVariable Long articleId) {
ArticleResponseDto article = articleService.show(majorId, articleId);
return ResponseEntity.status(HttpStatus.OK).body(article);
}
//게시글 생성
@PostMapping("/article/{majorId}")
public ResponseEntity<String> create(@PathVariable Long majorId, @RequestBody ArticleDto articleDto, HttpServletRequest request) {
HttpSession session = request.getSession();
articleService.create(majorId, articleDto, session);
return ResponseEntity.status(HttpStatus.OK).body("게시글 생성 완료");
}
//게시글 수정
@PatchMapping("/article/{majorId}/{articleId}")
public ResponseEntity<ArticleResponseDto> update(@PathVariable Long majorId, @PathVariable Long articleId,
@RequestBody ArticleDto articleDto, HttpServletRequest request) {
HttpSession session = request.getSession();
ArticleResponseDto article = articleService.update(majorId, articleId, articleDto, session);
return ResponseEntity.status(HttpStatus.OK).body(article);
}
//게시글 삭제
@DeleteMapping("/article/{majorId}/{articleId}")
public ResponseEntity<String> delete(@PathVariable Long majorId, @PathVariable Long articleId, HttpServletRequest request) {
HttpSession session = request.getSession();
articleService.delete(majorId, articleId, session);
return ResponseEntity.status(HttpStatus.OK).body("게시글 삭제 완료");
}
@PathVariable을 사용하여 URL 경로의 일부를 메서드의 파라미터로 바인딩해주었다. ex)majorId, articleId
게시글 생성, 수정, 삭제 로직에서는 현재 로그인되어있는 멤버의 정보가 필요하므로 HttpServletRequest request, HttpSession session = request.getSession(); 을 통해 세션에 있는 객체를 가져와 articleService에 인자로 전달해주었다.
Postman을 통한 데이터 확인
postman을 통해 데이터가 잘 넘어가는지 확인해주었다.
'Spring' 카테고리의 다른 글
스프링부트 커뮤니티 만들기 #5 - 댓글 CRUD (0) | 2024.03.03 |
---|---|
스프링부트 커뮤니티 만들기 # 회고록 (2) | 2024.02.29 |
스프링부트 커뮤니티 만들기 #3 - 로그아웃, 회원탈퇴 (1) | 2024.02.27 |
스프링부트 커뮤니티 만들기 #2 - 예외처리 (3) | 2024.02.09 |
스프링부트 커뮤니티 만들기 #1 - 회원가입, 로그인 (0) | 2024.02.09 |
- Total
- Today
- Yesterday
- DP
- EnumType.ORDINAL
- 자바 스프링
- 프론트엔드
- 북마크
- JPA
- elasticsearch
- 스프링 커뮤니티
- 스프링부트
- 준영속
- 백준 파이썬
- 로깅
- 스프링 북마크
- SQL
- SQLD
- 영속
- 지연로딩
- 비영속
- 다이나믹 프로그래밍
- 웹MVC
- 자바
- 로그아웃
- 커뮤니티
- 파이썬
- 웹 MVC
- SQL 레벨업
- 인텔리제이
- 스프링
- 백준
- 회원탈퇴
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |