SpringBoot 프로젝트

Spring Boot - 리뷰 CRUD기능 구현하기 (3)

orin602 2024. 11. 19. 14:41

리뷰 상세보기 및 수정

// 리뷰 상세 화면
@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}]] &nbsp;&nbsp;&nbsp;
			추천수 : [[${review.recoCount}]] &nbsp;&nbsp;&nbsp;
			즐겨찾기수 : [[${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>&nbsp;
			<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>&nbsp;
				<span>좋아요 수 : <span th:text="${reply.likes}"></span></span>&nbsp;
				<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}">&nbsp;&nbsp;
        <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>&nbsp;&nbsp;&nbsp;
		<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로 리다이렉트하여 리뷰 목록 페이지로 이동합니다.

 

 

수정된 게시글

 

수정된 DB