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 |
Tags
- While
- java변수
- 메서드
- Thread
- for문
- JAVA객체지향
- 인텔리제이 기초 설정
- 자바 멀티스레딩
- break문
- 반복문
- 접근제어지시자
- 상수
- multi-threading
- 생성자
- this예약어
- continue문
- 형 변환
- 인텔리제이 한글 깨짐 해결법
- 시스템 환경 변수 편집
- Java데이터 타입
- 포함관계
- 컴파일
- 집합관계
- function
- 연관관계
- OPP개념
- 메서드 오버로딩
- IntelliJ IDEA
- JAVA기초
- Java
Archives
- Today
- Total
최원종의 개발 블로그
V7-3 댓글 조회 기능 추가 본문
샘플 데이터 입력⬇️
더보기
-- User 테이블 데이터 (5명의 사용자)
INSERT INTO user_tb (username, password, email, created_at) VALUES
('admin', '1234', 'admin@blog.com', NOW()),
('ssar', '1234', 'ssar@nate.com', NOW()),
('cos', '1234', 'cos@gmail.com', NOW()),
('hong', '1234', 'hong@naver.com', NOW()),
('kim', '1234', 'kim@daum.net', NOW());
-- 2단계: Board 테이블 데이터 (10개의 게시글)
-- 주의: user_id는 위에서 생성된 사용자의 id를 참조
-- admin 사용자가 작성한 게시글 (3개)
INSERT INTO board_tb (title, content, user_id, created_at) VALUES
('블로그 개설을 환영합니다!', '안녕하세요! 새로운 블로그가 오픈했습니다. 많은 관심과 참여 부탁드립니다.', 1, NOW()),
('공지사항: 이용수칙 안내', '블로그 이용 시 지켜야 할 기본적인 수칙들을 안내드립니다. 건전한 소통 문화를 만들어가요.', 1, NOW()),
('업데이트 소식', '새로운 기능들이 추가되었습니다. 댓글 기능과 좋아요 기능을 곧 만나보실 수 있습니다.', 1, NOW());
-- ssar 사용자가 작성한 게시글 (3개)
INSERT INTO board_tb (title, content, user_id, created_at) VALUES
('Spring Boot 학습 후기', 'Spring Boot를 처음 배우면서 느낀 점들을 공유합니다. JPA가 정말 편리하네요!', 2, NOW()),
('JPA 연관관계 정리노트', '오늘 배운 @ManyToOne, @OneToMany 연관관계에 대해 정리해봤습니다. 헷갈리는 부분이 많아요.', 2, NOW()),
('코딩테스트 문제 추천', '백준과 프로그래머스에서 풀어볼 만한 문제들을 추천드립니다. 알고리즘 공부 화이팅!', 2, NOW());
-- cos 사용자가 작성한 게시글 (2개)
INSERT INTO board_tb (title, content, user_id, created_at) VALUES
('React vs Vue 비교', '프론트엔드 프레임워크 선택에 고민이 많았는데, 각각의 장단점을 비교해봤습니다.', 3, NOW()),
('개발자 취업 팁 공유', '신입 개발자로 취업하면서 도움이 되었던 팁들을 공유합니다. 포트폴리오가 중요해요!', 3, NOW());
-- hong 사용자가 작성한 게시글 (1개)
INSERT INTO board_tb (title, content, user_id, created_at) VALUES
('첫 번째 게시글입니다', '안녕하세요! 블로그에 처음 글을 올려봅니다. 앞으로 자주 소통해요~', 4, NOW());
-- kim 사용자가 작성한 게시글 (1개)
INSERT INTO board_tb (title, content, user_id, created_at) VALUES
('맛집 추천 - 강남역 근처', '강남역 근처에서 점심 먹기 좋은 맛집들을 추천드립니다. 가성비도 좋아요!', 5, NOW());
-- 댓글 테이블 데이터 (각 게시글에 댓글들을 추가)
-- 1번 게시글 (admin의 '블로그 개설을 환영합니다!')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('축하드립니다! 새로운 블로그 기대되네요.', 1, 2, NOW()),
('관리자님 수고 많으셨습니다. 좋은 컨텐츠 부탁드려요!', 1, 3, NOW()),
('드디어 오픈했군요. 자주 방문하겠습니다.', 1, 4, NOW());
-- 2번 게시글 (admin의 '공지사항: 이용수칙 안내')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('이용수칙 잘 읽어보겠습니다.', 2, 2, NOW()),
('건전한 소통 문화 만들기에 동참하겠습니다!', 2, 5, NOW());
-- 3번 게시글 (admin의 '업데이트 소식')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('댓글 기능 추가 감사합니다!', 3, 2, NOW()),
('좋아요 기능도 빨리 나왔으면 좋겠어요.', 3, 3, NOW()),
('업데이트 소식 감사합니다. 잘 사용하겠습니다.', 3, 4, NOW()),
('새로운 기능들이 기대됩니다.', 3, 5, NOW());
-- 4번 게시글 (ssar의 'Spring Boot 학습 후기')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('저도 Spring Boot 공부 중인데 많은 도움이 되었습니다!', 4, 1, NOW()),
('JPA 정말 편리하죠. 처음엔 어려웠지만 익숙해지면 좋더라구요.', 4, 3, NOW()),
('학습 후기 공유해주셔서 감사합니다.', 4, 5, NOW());
-- 5번 게시글 (ssar의 'JPA 연관관계 정리노트')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('연관관계 정말 헷갈리죠 ㅠㅠ 정리 잘 해주셨네요!', 5, 1, NOW()),
('@ManyToOne 부분이 특히 어려웠는데 덕분에 이해했습니다.', 5, 3, NOW()),
('저도 공부하면서 참고하겠습니다.', 5, 4, NOW()),
('양방향 매핑 부분도 추가로 설명해주시면 좋을 것 같아요.', 5, 5, NOW());
-- 6번 게시글 (ssar의 '코딩테스트 문제 추천')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('문제 추천 감사합니다! 바로 풀어보겠습니다.', 6, 1, NOW()),
('백준 문제 중에서 어떤 걸 먼저 풀어보면 좋을까요?', 6, 4, NOW());
-- 7번 게시글 (cos의 'React vs Vue 비교')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('React 쪽이 더 인기가 많은 것 같긴 하네요.', 7, 1, NOW()),
('Vue가 더 배우기 쉽다고 들었는데 실제로는 어떤가요?', 7, 2, NOW()),
('둘 다 써봤는데 각각 장단점이 있는 것 같아요.', 7, 4, NOW()),
('프로젝트 성격에 따라 선택하면 될 것 같습니다.', 7, 5, NOW());
-- 8번 게시글 (cos의 '개발자 취업 팁 공유')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('취업 준비 중인데 정말 유용한 정보네요!', 8, 2, NOW()),
('포트폴리오 작성 방법도 자세히 알려주세요.', 8, 4, NOW()),
('면접 준비는 어떻게 하셨나요?', 8, 5, NOW());
-- 9번 게시글 (hong의 '첫 번째 게시글입니다')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('첫 게시글 축하드려요! 환영합니다.', 9, 1, NOW()),
('앞으로 자주 소통해요~', 9, 2, NOW()),
('좋은 게시글 기대하겠습니다!', 9, 3, NOW());
-- 10번 게시글 (kim의 '맛집 추천 - 강남역 근처')에 대한 댓글
INSERT INTO reply_tb (comment, board_id, user_id, created_at) VALUES
('강남역 자주 가는데 맛집 정보 감사해요!', 10, 1, NOW()),
('가성비 좋은 곳 추천해주셔서 고마워요.', 10, 2, NOW()),
('저도 가봐야겠네요. 위치 정보도 알려주세요.', 10, 3, NOW()),
('점심 메뉴 추천도 해주시면 좋을 것 같아요.', 10, 4, NOW());
ReplyRepository - 댓글 조회 기능 ( JOIN FETCH User, Board 한 번에 가져 옴 )
package com.tenco.blog.reply;
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;
// @Repository - 부모의 클래스에 정의 되어 있음()
public interface ReplyRepository extends JpaRepository<Reply, Integer> {
// 기본적인 CRUD 자동 완성 및 추가 편의 기능 자동 생성
// select r.*, b.*, u.*
// from reply_tb r
// inner join board_tb b on r.board_id = b.id
// inner join user_tb u on r.user_id = u.id
// where r.board_id = 1
// order by r.created_at asc;
// JPQL 문법으로 변환
// 게시글 ID로 댓글 목록 조회(한번에 댓글 작성자 정보 포함 - JOIN FETCH 사용)
@Query("""
SELECT r FROM Reply r
JOIN FETCH r.user
JOIN FETCH r.board
WHERE r.board.id = :boardId
ORDER BY r.createdAt ASC
""")
List<Reply> findByBoardIdWithUser(@Param("board_id") Integer boardId);
}
ReplyService
댓글 조회시 List<Reply> 데이터 타입을 List<ReplyResponse.ListDTO> 로 변환 ( stream api 활용)
package com.tenco.blog.reply;
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;
import java.util.ArrayList;
import java.util.List;
// Service 계층에서는 여러 Repository를 조합해서 비즈니스 규칙을 완성한다
// 즉, 서비스 계층이 필요한 이유 중 하나이다.
@Service // IoC
@RequiredArgsConstructor // DI 처리
public class ReplyService {
private final ReplyRepository replyRepository;
private final BoardRepository boardRepository;
private final UserRepository userRepository;
// 댓글 목록 조회
public List<ReplyResponse.ListDTO> 댓글목록조회(Integer boardId, Integer sessionUserId) {
List<Reply> replyList = replyRepository.findByBoardIdWithUser(boardId);
// [방법 3]
// List<ReplyResponse.ListDTO> listDtoList = new ArrayList<>();
// for(int i =0; i < replyList.size(); i++) {
// Reply tempReply = replyList.get(i);
// // 객체 생성을 위해 필요한 데이터
// ReplyResponse.ListDTO listDTO = new ReplyResponse.ListDTO(tempReply, sessionUserId);
// listDtoList.add(listDTO);
// }
// Stream API 활용 [방법 2] <-- 사용
return replyList.stream()
.map(reply -> new ReplyResponse.ListDTO(reply, sessionUserId))
.toList();
}
@Transactional
public Reply 댓글작성(ReplyRequest.SaveDTO saveDTO, Integer id) {
// 게시글 조회
Board boardEntity = boardRepository.findById(saveDTO.getBoardId()).orElseThrow(
() -> new Exception404("해당 게시글을 찾을 수 없습니다"));
// id 로 사용자 조회
User userEntity = userRepository.findById(id).orElseThrow(
() -> new Exception404("사용자를 찾을 수 없습니다")
);
Reply reply = saveDTO.toEntity(userEntity, boardEntity);
replyRepository.save(reply);
return reply;
}
}
BoardController ( 게시글 상세보기 조회 시 BoardController에서 시작함)
// 게시글 상세보기 화면 요청
// http://localhost:8080/board/1
@GetMapping("/board/{id}")
public String detailPage(@PathVariable(name = "id") Integer id, Model model) {
BoardResponse.DetailDTO detailDTO = boardService.게시글상세조회(id);
// 대글 목록 조회 기능 필요 - TODO
model.addAttribute("board", detailDTO);
return "board/detail";
}
GlobalExceptionHandler ( 제약 조건 위반 오류)
package com.tenco.blog._core.errors;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
// 모든 컨트롤러에서 발생하는 예외를 이 클래스에서 처리 하겠다.
// RuntimeException 이 발생되면 해당 이 파일로 예외 처리가 오게 됨.
@Slf4j
@ControllerAdvice // IoC -> 에러 페이지 찾아 가능 녀석
// @RestControllerAdvice // 에러를 데이터로 반환할 때 사용
public class GlobalExceptionHandler {
@ExceptionHandler(Exception400.class)
public String ex400(Exception400 e, HttpServletRequest request) {
log.warn("=== 400 Bad Request 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", e.getMessage());
return "err/400";
}
// @ExceptionHandler(Exception401.class)
// public String ex401(Exception401 e, HttpServletRequest request) {
// log.warn("=== 401 Unauthorized 에러 발생 ===");
// log.warn("요청 URL: {}", request.getRequestURL());
// log.warn("에러메시지: {}", e.getMessage());
//
// request.setAttribute("msg", e.getMessage());
// return "err/401";
// }
@ExceptionHandler(Exception401.class)
@ResponseBody
public String ex401(Exception401 e, HttpServletRequest request) {
String script = """
<script>
alert('%s');
location.href='/login-form';
</script>
""".formatted(e.getMessage());
return script;
}
// @ExceptionHandler(Exception403.class)
// public String ex403(Exception403 e, HttpServletRequest request) {
// log.warn("=== 403 Forbidden 에러 발생 ===");
// log.warn("요청 URL: {}", request.getRequestURL());
// log.warn("에러메시지: {}", e.getMessage());
//
// request.setAttribute("msg", e.getMessage());
// return "err/403";
// }
@ExceptionHandler(Exception403.class)
@ResponseBody // 파일 찾지 말고 데이터 반환
public String ex403(Exception403 e, HttpServletRequest request) {
// String script = "<script>alert(' " + e.getMessage() + " ');" +
// "history.back();" +
// "</script>";
String script = """
<script>
alert('%s');
history.back();
</script>
""".formatted(e.getMessage());
return script;
}
@ExceptionHandler(Exception404.class)
public String ex404(Exception404 e, HttpServletRequest request) {
log.warn("=== 404 Not Found 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", e.getMessage());
return "err/404";
}
@ExceptionHandler(Exception500.class)
public String ex500(Exception500 e, HttpServletRequest request) {
log.warn("=== 500 Internal Server Error 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", e.getMessage());
return "err/500";
}
// 기타 모든 RuntimeException 처리 (최후의 보루)
@ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.warn("=== 예상치 못한 런타임 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", "시스템 오류가 발생했습니다. 관리자에게 문의해주세요");
return "err/500";
}
// 데이터베이스 관련 및 제약조건 위반 오류 처리
@ExceptionHandler(DataIntegrityViolationException.class)
public String handleDataIntegrityViolationException(DataIntegrityViolationException e,
HttpServletRequest request) {
log.warn("=== 데이터 베이스 제약 조건 위반 오류 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
String errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("FOREIGN KEY")) {
request.setAttribute("msg", "관련된 데이터가 있어 삭제할 수 없습니다");
} else {
// 실제로는 다른 내용으로 에러페이지에 내려 줘야 함.
request.setAttribute("msg", "데이터베이스 제약조건 위반:" + e.getMessage());
}
return "err/500";
}
}
board/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></h2>
<hr />
<div class="m-4 p-2">
{{board.content}}
</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">
<button type="submit" class="btn btn-danger btn-sm">🗑️ 삭제</button>
</form>
{{/isOwner}}
</div>
</div>
{{/replyList}}
</div>
<!-- 댓글 섹션 종료 -->
</div>
{{> layout/footer}}
'Spring boot 입문' 카테고리의 다른 글
| V8 게시글 페이징 처리 (0) | 2026.05.21 |
|---|---|
| V7-4 댓글 삭제 기능 추가 (0) | 2026.05.21 |
| V7-2 댓글 작성하기 기능 추가 (0) | 2026.05.20 |
| 스프링부트 V7 댓글 기능 추가 (User 도메인 리팩토링 ) (0) | 2026.05.20 |
| V6-2 expose-session-attributes 의 false 사용 (0) | 2026.05.20 |
