후기작성 기능 구현

기여·2024년 8월 18일
0

소소한 개발팁

목록 보기
74/103
post-thumbnail

기획의도:
동일 제품을 2회 이상 주문 가능하기에 후기는 '제품' 상세보기에서 출력되지만 '주문' 기준으로 생성된다.

조건:

  • 주문상태가 '도착'이고 + '후기 없음'이면 후기 작성 가능
  • 주문건마다 후기 1회만 작성 가능

DB
기본키도 R001, R002 식으로 자동 증가하도록 한다.

CREATE TABLE `review` (
    id INT PRIMARY KEY AUTO_INCREMENT,
    rid VARCHAR(10) UNIQUE,
    oid VARCHAR(10),
    product_code varchar(100) null,
    uid varchar(50) default 'user',
    rcontent longtext,
    rdate date DEFAULT curdate(),
CONSTRAINT `fk_review_uid` FOREIGN KEY (`uid`) REFERENCES `users` (`uid`),
CONSTRAINT `fk_review_pid` FOREIGN KEY (`product_code`) REFERENCES `products` (`product_code`),
CONSTRAINT ‘fk_review_oid’ FOREIGN KEY (‘oid’) REFERENCES ord (‘oid’)
);

DELIMITER //
CREATE TRIGGER trg_before_insert_review
BEFORE INSERT ON review
FOR EACH ROW
BEGIN
    DECLARE last_id INT;

    SELECT AUTO_INCREMENT
    INTO last_id
    FROM information_schema.TABLES
    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'review';

    SET NEW.rid = CONCAT('R', LPAD(last_id, 3, '0'));
END//
DELIMITER ;

Vo

@Data
public class ReviewVo {
	
	private String rid; //고유키
	private String rcontent;
	private String rdate;	
	private int review_count; //제품별 후기 건수. vo에만 있음
	
	//참조키
	private String oid;
	private String product_code;
	private String uid;
	
	//users와 join
	private String uname;	
	
	//products와 join
	private String pname;
	private String pimgStr;
}

+) 해당 주문에 후기 있는지 구분차 OrdVo에는 beReviewed(int) 필드 추가
기본값이 0이고 후기 생성되면 1로 업데이트됨

Mapper
Mapper 나누는 기준:
(1)어떤 테이블의 데이터 조회하는지,
(2)어떤 Vo에 값을 반환할지에 따라 역할 분담..
이게 최선인지 계속 고민됨.

A, OrdMapper
먼저, OrdMapper에서 후기 작성 가능한 주문(제품)을 호출한다.

//후기등록 위해 등록 가능한 (도착 + 후기 없는) 주문 보기
		@Select("select oid, pname, pimgStr, p.product_code, odate "
				+ "from ord o join products p "
				+ "on o.product_code = p.product_code "
				+ "where uid=#{uid} "
				+ "and ostatus = '도착' "
				+ "and beReviewed=0 "
				+ "order by oid desc")		
		List<OrdVo> reviewable(OrdVo vo);

그 중에서 후기 작성할 주문건 선택. 위의 ostatus = '도착' and beReviewed=0 조건에 충족된 건들에서 뽑는 것으로 밑에서는 생략.

//후기 등록할 oid 기준 상품정보 호출
		@Select("select oid, pname, pimgStr, p.product_code, odate "
				+ "from ord o join products p "
				+ "on o.product_code = p.product_code "
				+ "and oid=#{oid} ")		
		OrdVo reviewByOid (OrdVo vo);

B, ReviewMapper
후기 등록 및 조회는 ReviewMapper에서 처리한다.

// 후기 등록--------------------------------------
		//1, 수행
		@Insert("INSERT INTO review "
				+ "(oid, product_code, uid, rcontent) "
				+ "VALUES "
				+ "(#{oid}, #{product_code}, #{uid}, #{rcontent})")
		void addReview(ReviewVo vo);
		
		//2, 해당 oid 상태를 '후기 있음'으로 변경
		@Update("update ord set beReviewed=1 "
				+ "where oid=#{oid}")		
		void oidReviewed(@Param("oid") String oid);
		
		//3, oid에 해당하는 beReviewed 값 조회 - 필수x
		@Select("SELECT beReviewed FROM ord WHERE oid = #{oid}")
		int getBeReviewedByOid(@Param("oid") String oid);		
	// 후기 등록 끝-----------------------------------

본인이 등록한 후기를 마이페이지에서 확인:

//mypage에서 (uid별) 등록된 후기 보기
	@Select("SELECT o.oid, rdate, rcontent, pname, pimgStr, p.product_code "
			+ "FROM review r join products p on r.product_code = p.product_code "
			+ "join ord o on o.oid = r.oid "
			+ "WHERE r.uid=#{uid} "
			+ "and beReviewed=1 "
			+ "order by rid desc")	
	List<ReviewVo> myReview(ReviewVo vo);

제품별 모든 후기 조회:

	//제품별 후기 list 보기
	@Select("SELECT uname, rdate, rcontent "
			+ "FROM review r join users u on r.uid = u.uid "
			+ "WHERE product_code = #{product_code} "
			+ "order by rid desc")	
	List<ReviewVo> reviewList(ReviewVo vo);
	
	//제품별 후기 건수 보기
	@Select("SELECT product_code, COUNT(*) AS review_count "
			+ "FROM review "
			+ "GROUP BY product_code")		
	List<ReviewVo> reviewCountList();

Service

@Transactional
	public void insertReview(ReviewVo vo, String oid) {
		reviewMapper.addReview(vo); //후기 등록 수행
		reviewMapper.oidReviewed(oid); // 해당 oid 상태를 '후기 있음'으로 변경
		
		// 변경된 beReviewed 값을 조회
	    int beReviewed = reviewMapper.getBeReviewedByOid(oid);
	    System.out.println("beReviewed: " + beReviewed);
	}

Ctrl
A, ReviewCtrl
마이페이지에서 나의 후기(등록된 건 + 등록 가능한 건) 조회, 새로 등록 담당

@GetMapping("myReview")
	public String myReview(Model model, ReviewVo vo) {
		System.out.println("myReview");
		
		String uid = (String) session.getAttribute("username");
		vo.setUid(uid);
		
		List<ReviewVo> li = reviewSvc.myReview(vo);
		model.addAttribute("li", li);
		model.addAttribute("lisize", li.size());
		
		return "review/myReview";
	}
	
	@GetMapping("reviewable")
	public String reviewable(Model model, OrdVo vo) {
		System.out.println("reviewable");
		
        String uid = (String) session.getAttribute("username");
        vo.setUid(uid);
        
        List<OrdVo> li = ordSvc.reviewable(vo);
        model.addAttribute("li", li);
        model.addAttribute("lisize", li.size());
        
        return "review/reviewable";
    }	
	
	@GetMapping("reviewForm")
	public String reviewForm(Model model, OrdVo vo) {
		System.out.println("reviewForm");
		
		String uid = (String) session.getAttribute("username");
		vo.setUid(uid);
		
		model.addAttribute("m", ordSvc.reviewByOid(vo));
		
		return "review/reviewForm";
	}
		
	@PostMapping("addReview")
	public String addReview(ReviewVo vo) {				
        String uid = (String) session.getAttribute("username");
        vo.setUid(uid);
        
        String oid = vo.getOid();
        System.out.println("addReview to oid: " + oid);
        
        reviewSvc.insertReview(vo, oid);

        return "redirect:myReview";
    }

B, PrdCtrl
제품별 후기 및 후기건수 출력 담당
+) 제품별 후기건수 표시 위해 PrdVo에 reviewCount(int) 필드 추가

@GetMapping("prdList4user") //상품 전체보기
	public String prdList4user(Model model) {
	    System.out.println("prdList4user");
	    
	    List<PrdVo> li = prdSvc.prdList(null);
	    model.addAttribute("li", li);
	    model.addAttribute("lisize", li.size());

	    //제품별 후기 건수 가져오기
	    List<ReviewVo> reviewCountList = reviewSvc.getReviewCountList();
	    
	    // product_code를 기준으로 후기 건수를 매칭하기 위해 Map으로 변환
	    Map<String, Integer> reviewCountMap = reviewCountList.stream()
	            .collect(Collectors.toMap(ReviewVo::getProduct_code, ReviewVo::getReview_count));
	    
	    // 각 상품에 후기 건수 추가
	    li.forEach(m -> {
	        int reviewCount= reviewCountMap.getOrDefault(m.getProduct_code(), 0);
	        m.setReviewCount(reviewCount); // PrdVo의 reviewCount에 값 전달
	    });
	    
	    return "prd/prdList4user";
	}

@GetMapping("viewPrd") //상품 상세보기
	public String viewPrd(Model model, PrdVo vo, ReviewVo rvo) {
		System.out.println("viewPrd code: " + vo.getProduct_code());
		
		//사용자 로그인 여부에 따라 구매 버튼 출력 결정
		String uid=(String) session.getAttribute("username");
		vo.setUid(uid);
		
		// 모델에 제품 정보 추가
		PrdVo product = prdSvc.viewPrd(vo);
	    model.addAttribute("product", product);
	    
	    // 제품별 후기 조회
	    List<ReviewVo> reviewList = reviewSvc.reviewList(rvo);

	    // 각 후기 작성자 이름 마스킹 처리
	    reviewList.forEach(rv -> {
	    	rv.setUname(maskName(rv.getUname()));
	    });

	    model.addAttribute("reviewList", reviewList); // 후기 리스트
	    model.addAttribute("revListSize", reviewList.size()); // 후기 건수
	    
		return "prd/viewPrd";
	}

Front
후기 작성 가능한 주문건:

후기 작성 가능한 건 없을 경우:

주문내역에도 후기 작성 버튼이 있는데, 도착 여부 + 후기 있는지에 대한 조건이 있다.
따라서, 주문내역 불러오는 sql문에도 beReviewed값이 포함돼야 한다.

<div th:if="${oitem.ostatus == '도착' and oitem.beReviewed == 0}">
		            <button type="button" class="subbtn" 
					        th:attr="data-url=@{'/review/reviewForm?oid=' + ${oitem.oid}}"
					        onclick="location.href=this.getAttribute('data-url')">
					    후기 작성하기
					</button>		                
	            </div>	            
	            
	            <div th:if="${oitem.ostatus == '도착' and oitem.beReviewed == 1}" style="text-align: center;">
				    후기 작성 완료
	            </div>

후기 작성 양식의 경우,
ui상 제출 버튼이 상단 우측에 위치하면 보기 딱 좋은데, 그러려면 form 영역 밖에 있어야 하기에 form에 id 지정해주면 된다.

<div><!-- 상단... -->
<div style="text-align: right; margin: 5px 15px;">			    
		    <button class="button" type="submit" form="reviewForm">작성 완료</button>
	    </div>	

	</div><!-- 상단 끝 -->
	
	 <form action="/review/addReview" method="post" id="reviewForm">
    	<input type="hidden" name="uid" th:value="${session.username}">
	    <input type="hidden" name="oid" th:value="${m.oid}">
	    <input type="hidden" name="product_code" th:value="${m.product_code}">
	    
	    <!-- 후기 작성란 -->
	    <div style="width: 100%; display: flex; flex-wrap: wrap; justify-content: center; margin-top: 10px">
		    <textarea id="summernote" name="rcontent"></textarea>
	    </div>
	    <!-- 후기 작성란 끝 -->
    </form>

작성한 후기들을 모아보기:

상품 전체보기 페이지에서 각각 후기건수 출력:
(후기 없는데 0으로 출력하면 서운하니까 있는 건만 출력)

<div style="width: 25%;" th:if="${m.reviewCount > 0}" th:text="'💬' + ${m.reviewCount}"><!-- 문자 아닌 int이기에 !=null 조건 필요x -->
			</div>

상세보기에서는 제품별 후기를 볼 수 있다.

p/s: 후기 남긴 사용자명 마스킹 처리는 PrdCtrl에서:

//이름 별표 처리
	public String maskName(String uname) {
	    if (uname == null || uname.length() < 2) {
	        return uname; // 이름이 1글자이면 그대로 반환
	    }
	    
	    // 첫 글자만 표시하고 나머지는 별표 처리
	    String visiblePart= uname.substring(0, 1);
	    String maskedPart= uname.substring(1).replaceAll(".", "*");

	    return visiblePart + maskedPart;
	}

+) 후기작성 후 바로 후기목록으로 보내지 않고 먼저 '작성완료' 알럿 띄우고 페이지 전환하도록 살짝 수정하고 js 추가했다.

form에는 변동사항 없고,
제출 버튼 수정하고:

<div style="text-align: right; margin: 5px 15px;">			    
	<button class="button" type="button" id="submitButton">작성 완료</button>
</div>	

js 추가하면 끝~

<script>
//제출 완료 알림
    document.getElementById('submitButton').addEventListener('click', function() {
        // 알럿 띄우기
        alert('후기 작성이 완료되었습니다.');

        // 폼 제출
        document.getElementById('reviewForm').submit();
    });
</script>
profile
기기 좋아하는 여자

0개의 댓글