기획의도:
동일 제품을 2회 이상 주문 가능하기에 후기는 '제품' 상세보기에서 출력되지만 '주문' 기준으로 생성된다.
조건:
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>