리뷰 상세보기 및 수정
// 리뷰 상세 화면
@GetMapping("/review_detail")
public String reviewDetailView(HttpSession session, Model model, @RequestParam("review_seq") int review_seq) throws Exception {
Member loginUser = (Member)session.getAttribute("loginUser");
Review review = reviewService.getReviewBySeq(review_seq);
if(loginUser != null) {
model.addAttribute("loginUser", loginUser);
}
// 어보드한 이미지 파일 URL 리스트 추가
List<String> uploadImages = review.getUploadedImages();
model.addAttribute("uploadImages", uploadImages);
// 댓글
List<Reply> reply = replyService.getReplyByReview(review_seq);
model.addAttribute("reply", reply);
model.addAttribute("review", review);
return "review/reviewDetail";
}
- URL 매핑: 2편의 리뷰 메인 html에서 <a th:href="@{/review_detail(review_seq=${review.review_seq})}"> /review_detail - 리뷰의 review_seq 값을 통해 특정 리뷰의 상세 정보를 조회합니다.
- 로그인 상태 확인: 세션에서 로그인 유저 정보를 가져와 로그인 상태일 경우, loginUser 정보를 모델에 추가합니다.
- 리뷰 조회: reviewService.getReviewBySeq(review_seq)를 호출하여 특정 리뷰 데이터를 가져오고, 이를 review라는 이름으로 모델에 추가합니다.
- 이미지 URL 리스트 추가: review.getUploadedImages()를 호출하여 업로드된 이미지 리스트를 가져와 uploadImages로 모델에 추가합니다.
- 댓글 목록 조회: 댓글 서비스 replyService.getReplyByReview(review_seq)로 리뷰에 달린 댓글 목록을 조회하고 reply로 모델에 추가합니다.
- 뷰 반환: 모델에 추가된 데이터와 함께 review/reviewDetail 화면으로 이동합니다.
리뷰 상세 페이지 html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Review Detail</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.css">
<link rel="stylesheet" th:href="@{/css/review.css}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.js"></script>
<script th:src="@{/js/review.js}"></script>
<script th:inline="javascript">
var writer = /*[[${review.member.id}]]*/ '';
var viewer = /*[[${loginUser.id}]]*/ '';
var review_seq = /*[[${review.review_seq}]]*/ 0;
function checkAndEdit() {
if(writer == viewer) {
window.location.href = '/edit?review_seq=' + review_seq;
} else {
swal.fire({
title: '리뷰 수정 실패',
text: '작성자만 수정 가능합니다.',
icon: 'warning',
confirmButtonText: '확인'
});
}
}
</script>
</head>
<body>
<th:block th:insert="~{include/header}"></th:block>
<div class="review-detail-container">
<h1>리뷰 상세 보기</h1>
<div class="review-btn">
<h4>
조회수 : [[${review.viewCount}]]
추천수 : [[${review.recoCount}]]
즐겨찾기수 : [[${review.checkCount}]]</h4>
<div class="count-btn">
<button type="button" id="recommend-button" th:attr="data-review-seq=${review.review_seq}"
class="detail-recoment-btn" onclick="reviewReco(this)">추천하기</button>
<button type="button" id="bookmark-button" th:attr="data-review-seq=${review.review_seq}"
class="detail-bookmark-btn" onclick="reviewCheck(this)">즐겨찾기</button>
</div>
</div>
<div class="review-detail-content">
<th:block th:if="${#lists.isEmpty(uploadImages)}">
<img th:src="@{/images/no_img.jpg}" alt="이미지 없음" style="width:200px; height:200px;">
</th:block>
<th:block th:unless="${#lists.isEmpty(uploadImages)}">
<div th:each="image : ${uploadImages}">
<img th:src="@{${image}}" th:alt="${review.title}" style="width:200px; height:200px;">
</div>
</th:block>
<h4>작성자 : <span th:text="${review.member.id}"></span></h4>
<h4>제목 : <span th:text="${review.title}"></span></h4>
<h4>리뷰 내용 : <span th:text="${review.content}"></span></h4>
<h4>작성 날짜 : <span th:text="${#dates.format(review.review_date, 'yyyy-MM-dd HH:mm')}"></span></h4>
</div>
<div class="review-reply">
<h2>댓글</h2>
<div class="reply-box" th:each="reply : ${reply}">
<div class="reply-content">
<h5><span th:text="${reply.member.name}"></span>
<span th:text="${reply.content}"></span>
<span th:text="${#dates.format(reply.reply_date, 'yyyy-MM-dd HH:mm')}"></span></h5>
<button type="button" th:attr="data-reply-seq=${reply.replySeq}"
class="detail-replylike-btn" onclick="replyLike(this)">좋아요</button>
<span>좋아요 수 : <span th:text="${reply.likes}"></span></span>
<button type="button" th:attr="data-reply-seq=${reply.replySeq}"
class="detail-replydelete-btn" onclick="replyDelete(this)">댓글삭제</button>
<!-- th:attr은 속성 이름과 값을 동적으로 설정할 수 있게 한다. (JavaScript와 상호작용할 때 유용) -->
</div>
</div>
<div class="reply-content" th:if="${loginUser != null}">
<form class="reply-write-form" id="reply-write-form" method="post">
<input type="hidden" name="review_seq" th:value="${review.review_seq}">
<input type="hidden" name="member_id" th:value="${loginUser.id}">
<input type="text" id="content" name="content" placeholder="댓글을 작성하세요">
</form>
<button class="detail-reply-btn"type="button" onclick="reply_write()">댓글 작성</button>
</div>
</div>
<div class="review_btn">
<button class="detail-list-btn" type="button" onclick="window.location.href='/review'">목록으로</button>
<th:block th:if="${loginUser != null}">
<button class="detail-edit-btn" type="button" onclick="checkAndEdit()">수정하기</button>
</th:block>
</div>
</div>
</body>
</html>
- 상세 정보 섹션:
- 리뷰의 제목, 작성자, 내용, 작성 날짜 등을 표시합니다.
- uploadImages가 비어있으면 기본 이미지를 보여주고, 그렇지 않으면 업로드된 이미지 리스트를 보여줍니다.
- 버튼 섹션:
- 추천하기, 즐겨찾기 버튼: 각 버튼 클릭 시 추천 또는 즐겨찾기 기능이 실행됩니다.
- 수정하기 버튼: 로그인한 사용자와 작성자가 일치할 경우에만 보이도록 조건부로 표시됩니다.
- 목록으로 버튼: 리뷰 목록 페이지로 이동합니다.
- JavaScript:
- 작성자가 본인인지 확인하는 checkAndEdit() 함수는 로그인한 유저와 작성자가 같을 때에만 수정 화면으로 이동할 수 있도록 합니다.
- 댓글 목록 표시:
- reply 리스트를 반복하여 각 댓글의 작성자, 내용, 작성 날짜, 좋아요 수 등을 표시합니다.
- 각 댓글에 좋아요와 댓글삭제 버튼을 제공합니다.
- 댓글 작성 폼:
- 로그인한 경우에만 댓글 작성 폼이 보이며, 댓글 입력 후 댓글 작성 버튼을 통해 reply_write() 함수가 호출되어 댓글이 추가됩니다.
<button class="detail-edit-btn" type="button" onclick="checkAndEdit()">수정하기</button> 를 클릭하면
var writer = /*[[${review.member.id}]]*/ '';
var viewer = /*[[${loginUser.id}]]*/ '';
var review_seq = /*[[${review.review_seq}]]*/ 0;
function checkAndEdit() {
if(writer == viewer) {
window.location.href = '/edit?review_seq=' + review_seq;
} else {
swal.fire({
title: '리뷰 수정 실패',
text: '작성자만 수정 가능합니다.',
icon: 'warning',
confirmButtonText: '확인'
});
}
}
- if(writer == viewer): 리뷰 작성자(writer)와 현재 로그인한 사용자(viewer)의 ID를 비교합니다.
- 작성자와 로그인 사용자가 일치하는 경우:
- window.location.href = '/edit?review_seq=' + review_seq;를 통해 리뷰 수정 페이지로 이동합니다.
- URL에 review_seq를 쿼리 매개변수로 전달하여, 해당 리뷰에 대한 수정 작업을 진행할 수 있게 합니다.
- 작성자와 로그인 사용자가 일치하지 않는 경우:
- Swal.fire를 사용해 경고 메시지를 표시합니다.
- 메시지 내용은 "리뷰 수정 실패"로, "작성자만 수정 가능합니다."라는 경고와 함께 warning 아이콘을 통해 알림을 제공합니다.
수정 화면 Controller
// 리뷰 수정 화면
@GetMapping("/edit")
public String reviewEditView(HttpSession session, Model model, @RequestParam("review_seq") int review_seq) {
Member loginUser = (Member)session.getAttribute("loginUser");
Review review = reviewService.getReviewBySeq(review_seq);
if(loginUser == null) { // 비로그인 상태
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "리뷰 수정을 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "login/login";
}
if(!review.getMember().getId().equals(loginUser.getId())) { // 작성자와 로그인유저 비교
model.addAttribute("message", "수정 불가");
model.addAttribute("text", "작성자만 리뷰를 수정할 수 있습니다.");
model.addAttribute("messageType", "error");
return "redirect:/review";
}
model.addAttribute("review", review);
return "review/reviewEdit";
}
- @RequestParam("review_seq") int review_seq 를 통해 review_seq 값을 받아 리뷰를 식별하고, reviewService.getReviewBySeq(review_seq) 로 해당 리뷰를 가져옵니다.
- 로그인 여부 및 작성자 확인: 로그인 여부를 확인한 뒤 작성자가 맞는지 검증합니다. 작성자가 아니면 수정할 수 없도록 제한하고, 메시지를 설정해 리뷰 목록으로 리다이렉트합니다.
- 리뷰 정보 전달: 검증을 통과하면 review 객체를 모델에 추가하여 reviewEdit 페이지에서 해당 리뷰 데이터를 사용할 수 있게 합니다.
수정화면 html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Review Edit</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.css">
<link rel="stylesheet" th:href="@{/css/review.css}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.js"></script>
<script th:src="@{/js/review.js}"></script>
</head>
<body>
<th:block th:insert="~{include/header}"></th:block>
<div class="review-edit-container">
<h1>리뷰 수정</h1>
<form class="review-edit-form" id="review-edit-form" method="post" enctype="multipart/form-data">
<input type="hidden" name="review_seq" th:value="${review.review_seq}" />
<!-- 이미 업로드된 이미지 표시 -->
<div class="uploaded-images">
<th:block th:if="${#lists.isEmpty(review.uploadedImages)}">
<div class="uploaded-image">
<img th:src="@{/images/no_img.jpg}" alt="이미지 없음" style="width:200px; height:200px;">
</div>
</th:block>
<th:block th:unless="${#lists.isEmpty(review.uploadedImages)}">
<th:block th:each="image, iterStat : ${review.uploadedImages}">
<div class="uploaded-image">
<img th:src="@{${image}}" alt="이미지 없음" style="width:200px; height:200px;">
<button type="button" th:data-review-seq="${review.review_seq}"
th:data-index="${iterStat.index}" class="edit-imgdelete-btn"
onclick="deleteImage(this)">삭제
</button>
</div>
</th:block>
</th:block>
</div>
<label>제목</label>
<input type="text" name="title" id="title" th:value="${review.title}" />
<label>내용</label>
<textarea name="content" id="content" th:text="${review.content}"></textarea>
<input type="file" name="uploadFile" id="uploadFile" multiple>
</form>
<div class="edit-bottom">
<button class="edit-btn" type="button" onclick="window.location.href='/review'">목록으로</button>
<button class="edit-btn" type="button" onclick="update_review()">리뷰 수정</button>
</div>
</div>
</body>
</html>
<button class="edit-btn" type="button" onclick="update_review()">리뷰 수정</button> 로 JavaScript 함수 호출
// 리뷰 수정
function update_review() {
if ($("#title").val() == "") {
swal.fire({
title: '제목을 입력해주세요.',
icon: 'warning'
});
$("#title").focus();
return false;
} else if ($("#content").val() == "") {
swal.fire({
title: '내용을 입력해주세요.',
icon: 'warning'
});
$("#content").focus();
return false;
} else {
swal.fire({
title: '리뷰 수정 성공!',
text: '리뷰 목록 페이지로 이동합니다.',
icon: 'success',
confirmButtonText: '확인'
}).then((result) => {
if (result.isConfirmed) {
$("#review-edit-form").attr("action", "/update-review").submit();
}
});
}
}
- 제목과 내용 검증: 사용자가 제목과 내용을 입력했는지 확인합니다.
- 서브미션 처리: 입력이 모두 완료되었을 경우 성공 메시지를 보여주고, 사용자가 확인 버튼을 누르면 form의 action을 /update-review로 설정하여 submit 합니다.
- 폼 전송: 서브미트된 폼은 POST 요청으로 Spring 컨트롤러의 /update-review URL로 전달됩니다.
수정 처리 Controller
// 리뷰 수정 처리
@PostMapping("/update-review")
public String reviewUpdate(HttpSession session, Model model, Review vo,
@RequestParam("uploadFile") MultipartFile[] uploadFile,
@RequestParam("review_seq") int review_seq) {
Member loginUser = (Member)session.getAttribute("loginUser");
Review review = reviewService.getReviewBySeq(review_seq);
if(loginUser == null) { // 비로그인 상태
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "리뷰 수정을 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "login/login";
}
if(!review.getMember().getId().equals(loginUser.getId())) { // 작성자와 로그인유저 비교
model.addAttribute("message", "수정 불가");
model.addAttribute("text", "작성자만 리뷰를 수정할 수 있습니다.");
model.addAttribute("messageType", "error");
return "redirect:/review";
}
// 제목, 내용 수정
review.setTitle(vo.getTitle());
review.setContent(vo.getContent());
// 기존 이미지 리스트 유지
List<String> existingImages = review.getUploadedImages();
if (existingImages == null) {
existingImages = new ArrayList<>();
}
// 새로운 업로드된 이미지 추가
if (vo.getUploadedImages() != null) {
existingImages.addAll(vo.getUploadedImages());
}
// 수정된 이미지 리스트를 Review 객체에 설정
review.setUploadedImages(existingImages);
// 파일 업로드 처리
if (uploadFile.length > 0 || !review.getUploadedImages().isEmpty()) {
List<String> fileUrls = new ArrayList<>();
if (!review.getUploadedImages().isEmpty()) {
fileUrls.addAll(review.getUploadedImages());
}
for (MultipartFile file : uploadFile) {
if (!file.isEmpty()) {
// 파일 경로
String uploadDir = "C:/ThisIsJava/SpringBootWorkspace/Book/uploads/";
// 파일 이름 수정
String originalName = file.getOriginalFilename();
String fileExtension = originalName.substring(originalName.lastIndexOf("."));
String uuid = UUID.randomUUID().toString();
String fileName = uuid + fileExtension;
try {
// 파일 저장
FileUploadUtil.saveFile(uploadDir, fileName, file);
// URL 생성
String fileUrl = "/uploads/" + fileName;
fileUrls.add(fileUrl);
} catch (IOException e) {
e.printStackTrace();
model.addAttribute("message", "파일 업로드 중 오류 발생");
model.addAttribute("messageType", "error");
return "review/reviewEdit";
}
}
}
review.setUploadedImages(fileUrls); // 저장된 파일 경로를 Review 객체에 설정
}
reviewService.updateReview(review);
return "redirect:/review";
}
review.setTitle(vo.getTitle());
review.setContent(vo.getContent());
리뷰 제목과 내용 수정: 폼에서 전달받은 title과 content 값으로 기존 review 객체의 제목과 내용을 업데이트합니다.
// 기존 이미지 리스트 유지
List<String> existingImages = review.getUploadedImages();
if (existingImages == null) {
existingImages = new ArrayList<>();
}
기존 이미지 유지: 리뷰의 기존 이미지 리스트를 유지합니다. 만약 기존 이미지가 없다면 새로운 리스트를 생성합니다.
if (vo.getUploadedImages() != null) {
existingImages.addAll(vo.getUploadedImages());
}
// 수정된 이미지 리스트를 Review 객체에 설정
review.setUploadedImages(existingImages);
추가된 이미지 처리: 수정 시 새로운 이미지가 업로드되었을 경우, 기존 이미지 리스트에 추가합니다.
if (uploadFile.length > 0 || !review.getUploadedImages().isEmpty()) {
List<String> fileUrls = new ArrayList<>();
if (!review.getUploadedImages().isEmpty()) {
fileUrls.addAll(review.getUploadedImages());
}
- 파일 업로드 처리: uploadFile 배열에 업로드된 파일이 있거나 기존에 업로드된 이미지가 있는 경우 새로운 파일을 서버에 저장합니다.
- 파일 저장 경로 설정: 서버의 특정 경로에 파일을 저장하고, 저장된 파일 URL을 리스트에 추가합니다
for (MultipartFile file : uploadFile) {
if (!file.isEmpty()) {
String uploadDir = "C:/ThisIsJava/SpringBootWorkspace/Book/uploads/";
String originalName = file.getOriginalFilename();
String fileExtension = originalName.substring(originalName.lastIndexOf("."));
String uuid = UUID.randomUUID().toString();
String fileName = uuid + fileExtension;
try {
FileUploadUtil.saveFile(uploadDir, fileName, file);
String fileUrl = "/uploads/" + fileName;
fileUrls.add(fileUrl);
} catch (IOException e) {
e.printStackTrace();
model.addAttribute("message", "파일 업로드 중 오류 발생");
model.addAttribute("messageType", "error");
return "review/reviewEdit";
}
}
}
review.setUploadedImages(fileUrls);
}
reviewService.updateReview(review);
return "redirect:/review";
}
- 각 파일을 서버의 uploadDir 경로에 저장하며, 파일명에는 고유한 UUID를 붙여 충돌을 방지합니다.
- 저장에 성공한 경우 해당 파일의 URL을 fileUrls 리스트에 추가하고, review 객체의 uploadedImages 속성에 설정합니다.
- 파일 업로드 중 에러가 발생할 경우, 에러 메시지를 설정하고 reviewEdit 화면으로 다시 돌아갑니다.
- DB 업데이트: 모든 데이터 수정이 완료되면 reviewService.updateReview(review);를 호출하여 데이터베이스에 변경 사항을 반영합니다.
- 리다이렉트: 수정이 완료되면 /review URL로 리다이렉트하여 리뷰 목록 페이지로 이동합니다.
'SpringBoot 프로젝트' 카테고리의 다른 글
Spring Boot - 이미지 업로드 및 삭제 (0) | 2024.11.21 |
---|---|
Spring Boot - 리뷰 CRUD기능 구현하기 (4) (0) | 2024.11.19 |
Spring Boot - 리뷰 CRUD기능 구현하기 (2) (0) | 2024.11.19 |
Spring Boot - 리뷰 CRUD기능 구현하기 (1) (4) | 2024.11.18 |
Spring Boot로 간단한 ID와 비밀번호 찾기 기능 구현하기 (0) | 2024.11.18 |