setting >> plugins

비지니스 파악
CommService
package com.example.aboutme.comm;
import com.example.aboutme._core.error.exception.Exception404;
import com.example.aboutme.comm.ResourceNotFoundException.ResourceNotFoundException;
import com.example.aboutme.reply.Reply;
import com.example.aboutme.reply.ReplyRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class CommService {
private final CommRepository commRepository;
private final ReplyRepository replyRepository;
public List<CommResponse.CommAndReplyDTO> findAllCommsWithReply() {
return commRepository.findAllCommsWithReply();
}
@Transactional
public CommResponse.CommDetailDTO getCommDetail(int id) {
// 주어진 ID로 게시글을 가져옵니다.
Comm comm = commRepository.findById(id)
.orElseThrow(() -> new Exception404("게시물을 찾을 수 없습니다"));
// 주어진 게시글의 댓글을 가져옵니다.
List<Reply> replies = replyRepository.findByCommId(comm.getId());
// 같은 카테고리의 다른 글들과 해당 글들의 댓글을 가져옵니다.
List<Comm> relatedComms = commRepository.findByCategoryWithRepliesAndExcludeId(comm.getCategory(), comm.getId());
return new CommResponse.CommDetailDTO(comm, replies, relatedComms);
}
public Comm findById(Integer id) {
Optional<Comm> commOptional = commRepository.findById(id);
return commOptional.orElse(null); // orElse(null)을 사용하여 엔티티가 없을 경우 null 반환
}
}
쿼리 관련, 전부 다 test해보기
ctrl + shift + t
package com.example.aboutme.comm;
import com.example.aboutme.comm.enums.CommCategory;
import com.example.aboutme.reply.Reply;
import com.example.aboutme.reply.ReplyRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
@DataJpaTest
class CommServiceTest {
@Autowired
private CommRepository commRepository;
@Autowired
private ReplyRepository replyRepository;
@Test
void findByCatagory() {
CommCategory category = CommCategory.GENERAL_CONCERNS;
List<Comm> comms = commRepository.findByCategory(category);
comms.forEach(comm -> System.out.println("comm = " + comm));
}
@Test
void getCommDetail() {
int id = 1;
CommCategory category = CommCategory.GENERAL_CONCERNS;
List<Comm> comms = commRepository.findByCategoryWithRepliesAndExcludeId(category, id);
comms.forEach(comm -> {
System.out.println("comm = " + comm);
if (comm.getReplies() != null) {
comm.getReplies().forEach(reply -> System.out.println("reply = " + reply));
} else {
System.out.println("No replies found for comm id " + comm.getId());
}
});
}
@BeforeEach
void setUp() {
}
@Test
void findById() {
int id = 1;
Comm comm = commRepository.findById(id).get();
System.out.println("comm = " + comm);
}
@Test
void 댓글조회하기() {
int id = 1;
List<Reply> replys = replyRepository.findByCommId(id);
for (Reply reply : replys) {
System.out.println("reply = " + reply);
}
}
}
레파지토리 쿼리
package com.example.aboutme.comm;
import com.example.aboutme.comm.enums.CommCategory;
import com.example.aboutme.user.UserResponse;
import jdk.jfr.Category;
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 CommRepository extends JpaRepository<Comm, Integer> {
// 현재 게시글 ID를 제외하고 같은 카테고리의 다른 게시글을 가져오는 쿼리
List<Comm> findByCategoryAndIdNot(CommCategory category, Long id);
List<Comm> findByCategory(CommCategory category);
@Query("SELECT c FROM Comm c LEFT JOIN FETCH c.replies r WHERE c.category = :category AND c.id <> :id")
List<Comm> findByCategoryWithRepliesAndExcludeId(@Param("category") CommCategory category, @Param("id") Integer id);
// 메인 커뮤니티 리스트
@Query("""
SELECT new com.example.aboutme.user.UserResponse$ClientMainDTO$CommDTO(
c.id,
c.title,
c.content,
c.category,
c.user.profileImage,
c.user.name,
r.user.profileImage,
r.user.name
)
FROM Comm c
JOIN c.replies r
WHERE r.user.userRole = com.example.aboutme.user.enums.UserRole.EXPERT
""")
List<UserResponse.ClientMainDTO.CommDTO> findCommsWithReply();
// /comm 출력하려고 뽑은 쿼리
@Query("""
SELECT new com.example.aboutme.comm.CommResponse$CommAndReplyDTO(
c.id,
c.title,
c.content,
c.category,
c.user.profileImage,
c.user.name,
r.user.userRole,
r.user.profileImage,
r.user.name,
r.solution
)
FROM Comm c
JOIN c.replies r
""")
List<CommResponse.CommAndReplyDTO> findAllCommsWithReply();
}
CommResponse
package com.example.aboutme.comm;
import com.example.aboutme.comm.enums.CommCategory;
import com.example.aboutme.reply.Reply;
import com.example.aboutme.user.enums.UserRole;
import lombok.Data;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import static java.util.stream.Collectors.toList;
public class CommResponse {
@Data
public static class ClientMainCommListDTO {
private Integer communityId;
private String title;
private String content;
private String category;
private String writerImage;
private String writerName;
private String expertImage;
private String expertName;
public ClientMainCommListDTO(Integer communityId, String title, String content, CommCategory category,
String writerImage, String writerName, String expertImage, String expertName) {
this.communityId = communityId;
this.title = title;
this.content = content;
this.category = category.getKorean();
this.writerImage = writerImage;
this.writerName = writerName;
this.expertImage = expertImage;
this.expertName = expertName;
}
}
@Data
public static class CommAndReplyDTO {
private Integer id;
private String title;
private String content;
private String category;
private String userProfileImage;
private String writerName;
private boolean userRole;
private String replyProfileImage;
private String expertName;
private String solution;
// 생성자 잘 확인해야함. EXPERT면 true 나와서 출력할 수 있도록.
public CommAndReplyDTO(Integer id, String title, String content, CommCategory category, String userProfileImage, String writerName,
UserRole userRole, String replyProfileImage, String expertName, String solution) {
this.id = id;
this.title = title;
this.content = content;
this.category = category.getKorean();
this.userProfileImage = userProfileImage;
this.writerName = writerName;
this.userRole = userRole == UserRole.EXPERT ? true : false;
this.replyProfileImage = replyProfileImage;
this.expertName = expertName;
this.solution = solution;
}
}
// @Data
// public static class CommDetailDTO {
//
// private List<String> replyContents;
// private List<String> replyContents;
// private Map<String, List<CommDTO>> commsByCategory;
//
// public CommDetailDTO(Integer id, String clientImage, String name, String content, String title, String category, Timestamp createdAt, List<String> replyContents, Map<String, List<CommDTO>> commsByCategory) {
// this.id = id;
// this.clientImage = clientImage;
// this.name = name;
// this.content = content;
// this.title = title;
// this.category = category;
// this.createdAt = createdAt;
// this.replyContents = replyContents;
// this.commsByCategory = commsByCategory;
// }
// }
//
// @Data
// public static class CommDTO {
// private Integer id;
// private String content;
// private String title;
// private String category;
// private Timestamp createdAt;
//
// public CommDTO(Integer id, String content, String title, String category, Timestamp createdAt) {
// this.id = id;
// this.content = content;
// this.title = title;
// this.category = category;
// this.createdAt = createdAt;
// }
// }
@Data
public static class CommDetailDTO {
private CommDTO commDTO;
private List<ReplyDTO> replies = new ArrayList<>();
private List<RelatedCommWithRepliesDTO> relatedComms = new ArrayList<>();
public CommDetailDTO(Comm comm, List<Reply> replies, List<Comm> relatedComms) {
this.commDTO = new CommDTO(comm);
this.replies = replies.stream()
.map(ReplyDTO::new)
.toList();
this.relatedComms = relatedComms.stream()
.map(RelatedCommWithRepliesDTO::new)
.toList();
}
@Data
public static class CommDTO {
private Integer id;
private String title;
private String content;
private String category;
private String writerName;
private String writerProfileImage;
private Timestamp createdAt;
// private int likeCount;
private int replyCount;
public CommDTO(Comm comm) {
this.id = comm.getId();
this.title = comm.getTitle();
this.content = comm.getContent();
this.category = comm.getCategory().getKorean();
this.writerName = comm.getUser().getName();
this.writerProfileImage = comm.getUser().getProfileImage();
this.createdAt = comm.getCreatedAt();
// this.likeCount = comm.getLikes().size();
this.replyCount = comm.getReplies().size();
}
}
@Data
public static class ReplyDTO {
private Integer id;
private String content;
private String solution;
private Timestamp createdAt;
public ReplyDTO(Reply reply) {
this.id = reply.getId();
this.content = reply.getContent();
this.content = reply.getSolution();
this.createdAt = reply.getCreatedAt();
}
}
@Data
public static class RelatedCommWithRepliesDTO {
private Integer id;
private String content;
private String title;
private String category;
private Timestamp createdAt;
private int replies;
private List<ExpertReplyDTO> expertReplies = new ArrayList<>();
public RelatedCommWithRepliesDTO(Comm comm) {
this.id = comm.getId();
this.content = comm.getContent();
this.title = comm.getTitle();
this.category = comm.getCategory().getKorean();
this.createdAt = comm.getCreatedAt();
this.replies = (int) comm.getReplies().stream()
.filter(reply -> !reply.getUser().getUserRole().equals(UserRole.CLIENT))
.count();
this.expertReplies = comm.getReplies().stream()
.filter(reply -> reply.getUser().getUserRole().equals(UserRole.EXPERT))
.map(ExpertReplyDTO::new)
.collect(toList());
}
@Data
public static class ExpertReplyDTO {
private Integer id;
private String solution;
public ExpertReplyDTO(Reply reply) {
this.id = reply.getId();
this.solution = reply.getSolution();
}
}
}
}
}
data.sql
-- reply_tb
INSERT INTO reply_tb (user_id, comm_id, summary, cause_analysis, solution, created_at)
VALUES
-- 글 1의 댓글
(1, 1, '집에 가고 싶은 이유는...', '스트레스가 원인입니다', '휴식을 취하세요', NOW()),
(21, 1, '상담사 의견', '생활 속 스트레스가 주요 원인입니다.', '정기적인 휴식과 상담을 추천합니다.', NOW()),
-- 글 2의 댓글
(2, 2, '회사에서 스트레스 받는 이유는...', '업무 부담이 원인입니다', '업무 분담을 요청해보세요', NOW()),
-- 글 3의 댓글
(3, 3, '저녁 메뉴 고민', '다양한 옵션이 많아 결정이 어렵습니다', '가볍게 샐러드나 요거트를 추천합니다', NOW()),
(22, 3, '상담사 의견', '건강한 다이어트를 위해 균형 잡힌 식사를 추천합니다.', '영양소가 균형 잡힌 식단을 고려해보세요.', NOW()),
-- 글 4의 댓글
(4, 4, '친구와의 갈등 원인', '의사소통 부족이 원인입니다', '서로 솔직하게 대화해보세요', NOW()),
(23, 4, '상담사 의견', '갈등 해결을 위해 열린 마음으로 대화하세요.', '서로의 입장을 이해해보세요.', NOW()),
-- 글 5의 댓글
(5, 5, '수업시간 졸음 원인', '수면 부족이 원인입니다', '충분한 수면을 취하세요', NOW()),
-- 글 6의 댓글
(6, 6, '시험 공부 힘든 이유', '과도한 학습량이 원인입니다', '적절한 학습 계획을 세우세요', NOW()),
(24, 6, '상담사 의견', '효과적인 학습 방법을 찾는 것이 중요합니다.', '짧은 휴식과 함께 계획적인 학습을 추천합니다.', NOW()),
-- 글 7의 댓글
(7, 7, '프로젝트 어려운 이유', '프로젝트 범위가 큽니다', '작게 나눠서 진행하세요', NOW()),
(25, 7, '상담사 의견', '프로젝트 관리가 중요합니다.', '체계적인 계획을 세워보세요.', NOW()),
-- 글 8의 댓글
(8, 8, '취업 준비가 막막해요', '준비할 것이 많아서', '계획적으로 준비하세요', NOW()),
(26, 8, '상담사 의견', '취업 준비는 체계적으로.', '리스트를 만들어보세요.', NOW()),
-- 글 9의 댓글
(9, 9, '연애가 어려운 이유는...', '서로의 이해가 부족합니다', '서로에게 솔직해지세요', NOW()),
(27, 9, '상담사 의견', '서로의 감정을 잘 이해하세요.', '솔직한 대화를 나누세요.', NOW()),
-- 글 10의 댓글
(10, 10, '취미를 찾고 싶어요', '다양한 시도를 해보세요', '관심사를 탐색하세요', NOW()),
(28, 10, '상담사 의견', '다양한 취미를 시도해보세요.', '여러 가지를 경험해보세요.', NOW()),
-- 글 11의 댓글
(11, 11, '스트레스 해소 방법', '운동과 휴식이 중요합니다', '규칙적인 운동을 해보세요', NOW()),
(29, 11, '상담사 의견', '운동과 휴식의 균형을 맞추세요.', '일정한 운동 시간을 가져보세요.', NOW()),
-- 글 12의 댓글
(12, 12, '혼자 밥 먹기 싫은 이유', '외로움이 원인입니다', '가벼운 외출을 해보세요', NOW()),
(30, 12, '상담사 의견', '혼자 있는 시간도 소중하게 생각하세요.', '작은 취미를 가져보세요.', NOW()),
-- 일반 유저들의 추가 댓글
-- 글 2의 추가 댓글
(3, 2, '회사에서 받은 스트레스는...', '동료와의 관계도 원인일 수 있습니다', '개인 시간을 가져보세요', NOW()),
(4, 2, '스트레스를 줄이는 방법은...', '업무 외 활동이 중요합니다', '취미 활동을 시작해보세요', NOW()),
-- 글 5의 추가 댓글
(6, 5, '수업시간에 집중하려면...', '충분한 수면과 휴식이 필요합니다', '낮잠을 잠깐 자보세요', NOW()),
(7, 5, '졸음을 이기기 위해...', '커피나 차를 마시는 것도 도움이 됩니다', '잠시 산책을 해보세요', NOW()),
-- 글 6의 추가 댓글
(8, 6, '시험 공부가 힘든 이유는...', '과도한 스트레스와 불안감이 원인입니다', '리뷰나 정리를 해보세요', NOW()),
(9, 6, '공부에 집중하기 위해...', '짧은 휴식을 자주 가지세요', '체계적인 공부 계획을 세우세요', NOW()),
-- 글 9의 추가 댓글
(10, 9, '연애가 어려운 이유는...', '서로의 감정을 잘 이해하는 것이 중요합니다', '대화를 많이 나누세요', NOW()),
(11, 9, '연애에서 중요한 것은...', '서로의 차이를 인정하는 것입니다', '타협점을 찾아보세요', NOW()),
-- 글 12의 추가 댓글
(12, 12, '혼자 밥 먹기 싫다면...', '외로움을 달래기 위한 활동이 필요합니다', '다양한 활동을 시도해보세요', NOW()),
(13, 12, '혼밥의 장점은...', '자유롭게 시간을 쓸 수 있습니다', '혼밥의 즐거움을 찾아보세요', NOW());
근데 여기서 solution이 2개여서 잘 못되었음. 1개로 되어야 함.
더미 수정 후
erd 볼 때도 비지니스 흐름으로 봐야 한다. 같은 엔티티라도 다른 조에는 다르게 표현되어 있을 수 있다. 그러니 여기서 중요한 것은 1:다 관계가 아니라 비지니스 흐름이 중요하다.

실습1
CommService로 가서 커뮤니티 상세보기를 구현한다.
//커뮤니티 상세보기
@Transactional
public CommResponse.CommDetailDTO getCommDetail(int id) {
//주어진 commID로 게시글(Comm_tb)을 가져옵니다.
Comm comm = commRepository.findById(id)
.orElseThrow(() -> new Exception404("게시물을 찾을 수 없습니다."));
Spring Data JPA 기본 메서드
Spring Data JPA에서 제공하는 몇 가지 기본 메서드는 다음과 같습니다:
findById(ID id)
: 주어진 ID로 엔티티를 조회합니다. 반환 타입은Optional<T>
입니다.
save(S entity)
: 엔티티를 저장하거나 업데이트합니다.
deleteById(ID id)
: 주어진 ID로 엔티티를 삭제합니다.
findAll()
: 모든 엔티티를 조회합니다.
test로 가서 시험해본다.

여기서 왜 Comm의
@OneToMany(mappedBy = "comm", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Reply> replies;
이 부분은 안 나올까?
⇒
@OneToMany
관계에서 fetch = FetchType.LAZY
를 사용하면 기본적으로 해당 컬렉션은 명시적으로 접근할 때까지 로드되지 않습니다. 따라서 테스트 코드에서 findById
를 호출한 후 replies
를 접근하려고 하면, replies
컬렉션이 비어있거나 null
로 보일 수 있습니다.
Share article