<p th:text="'리뷰 ' + ${reviewPageResponseDTO.avgRate} + ' / 5'"></p>
<div class="reviews">
<div>
<p th:text="'총 ' + ${reviewPageResponseDTO.total} + '건'"></p>
<div>
<a href="#" class="sort-link" data-sort-type="highestRate">평점 높은 순</a> |
<a href="#" class="sort-link" data-sort-type="lowestRate">평점 낮은 순</a> |
<a href="#" class="sort-link" data-sort-type="latest">최신순</a>
</div>
</div>
<div th:each="review : ${reviewPageResponseDTO.dtoList}" class="reviews_content">
<p th:text="${#strings.substring(review.uid, 0, 4) + '***'}"></p>
<div class="stars">
<img src="/images/product/full_star.png"
th:each="i : ${#numbers.sequence(1, review.rating)}"
alt="★">
<p th:text="${review.rating}">5</p>
</div>
<p th:text="'판매자 : ' + ${review?.product?.company}">판매자 : 롯데백화점 본점</p>
<p th:utext="${review.content}">
리뷰 내용
</p>
<img th:if="${review.sNameImage1 != null}" th:src="${review.sNameImage1}" alt="리뷰 이미지">
<img th:if="${review.sNameImage2 != null}" th:src="${review.sNameImage2}" alt="리뷰 이미지">
</div>
<div class="reviews_page">
<th:block th:if="${reviewPageResponseDTO.prev}">
<a href="#" class="prev"><</a>
</th:block>
<th:block th:each="num : ${#numbers.sequence(reviewPageResponseDTO.start, reviewPageResponseDTO.end)}">
<a href="#" th:class="${num == reviewPageResponseDTO.pg} ? 'current' : 'num'" th:data-page="${num}">
[[${num}]]
</a>
</th:block>
<th:block th:if="${reviewPageResponseDTO.next}">
<a href="#" class="next">></a>
</th:block>
</div>
</div>
기술 설명
<p th:text="'리뷰 ' + ${reviewPageResponseDTO.avgRate} + ' / 5'": 서버에서 계산한 평균 평점(avgRate) 뒤에" / 5"를 붙여 출력.
<p th:text="'총 ' + ${reviewPageResponseDTO.total} + '건'": 총 리뷰 개수(total) 앞뒤에 문자열을 결합하여 출력.정렬 링크 (
.sort-link):data-sort-type속성에highestRate,lowestRate,latest값을 설정해 JavaScript에서 정렬 타입을 구분 가능.
th:each="review : ${reviewPageResponseDTO.dtoList}":dtoList(리뷰 리스트)를 순회하며 각 리뷰 블록(.reviews_content) 생성.UID 마스킹:
<p th:text="${#strings.substring(review.uid, 0, 4) + '***'}"></p>
#strings.substring(review.uid, 0, 4)로 UID의 앞 4자리만 가져오고 뒤에'***'를 붙여 출력.별점 이미지 반복 렌더링:
<img src="/images/product/full_star.png" th:each="i : ${#numbers.sequence(1, review.rating)}" alt="★"> <p th:text="${review.rating}">5</p>
#numbers.sequence(1, review.rating)를 사용해 1부터review.rating까지 반복하며<img>태그를 생성.- 별점 개수만큼
full_star.png이미지를 렌더링하고, 인접한<p>태그에 숫자(review.rating) 표시.판매자 정보 출력:
<p th:text="'판매자 : ' + ${review?.product?.company}">판매자 : 롯데백화점 본점</p>
- 널 안전 연산자(
?.)를 사용해review.product또는company가 널인 경우 에러 방지.리뷰 내용(HTML 허용):
<p th:utext="${review.content}"> 리뷰 내용 </p>
th:utext로 HTML 태그가 포함된 리뷰 내용을 그대로 렌더링.리뷰 이미지 조건부 출력:
<img th:if="${review.sNameImage1 != null}" th:src="${review.sNameImage1}" alt="리뷰 이미지"> <img th:if="${review.sNameImage2 != null}" th:src="${review.sNameImage2}" alt="리뷰 이미지">
sNameImage1,sNameImage2필드가 널이 아닐 때만 각 이미지를 출력.페이지네이션:
<div class="reviews_page"> <th:block th:if="${reviewPageResponseDTO.prev}"> <a href="#" class="prev"><</a> </th:block> <th:block th:each="num : ${#numbers.sequence(reviewPageResponseDTO.start, reviewPageResponseDTO.end)}"> <a href="#" th:class="${num == reviewPageResponseDTO.pg} ? 'current' : 'num'" th:data-page="${num}"> [[${num}]] </a> </th:block> <th:block th:if="${reviewPageResponseDTO.next}"> <a href="#" class="next">></a> </th:block> </div>
prev가 true일 때<a class="prev">링크 생성.#numbers.sequence(start, end)로 시작 페이지(start)부터 끝 페이지(end)까지 번호 반복 출력.
- 현재 페이지(
pg)와 비교해 삼항 연산자로 클래스(current또는num)를 구분.th:data-page속성에 페이지 번호를 설정.next가 true일 때<a class="next">링크 생성.
const reviewsContainer = document.querySelector('.reviews');
let currentSortType = null;
// AJAX 응답 데이터로 리뷰 리스트와 페이징 생성
function updateReviewListUI(reviewData) {
// 기존 리뷰 내용 제거
const existingReviewsContent = reviewsContainer.querySelectorAll('.reviews_content');
existingReviewsContent.forEach(review => review.remove());
// 리뷰 항목 생성
reviewData.dtoList.forEach(review => {
const reviewContent = document.createElement('div');
reviewContent.classList.add('reviews_content');
let starsHTML = '';
for (let i = 0; i < Math.floor(review.rating); i++) {
starsHTML += '<span>⭐</span>';
}
reviewContent.innerHTML = `
<p>${review.uid.substring(0, 4)}***</p>
<div class="stars">
${starsHTML}
<p>${review.rating}</p>
</div>
<p>판매자 : ${review?.product?.company || '정보 없음'}</p>
<p>${review.content.replace(/\\\\n/g, '<br>')}</p>
${review.snameImage1 ? `<img src="/upload/review/${review.snameImage1}" alt="리뷰 이미지 1">` : ''}
${review.snameImage2 ? `<img src="/upload/review/${review.snameImage2}" alt="리뷰 이미지 2">` : ''}
`;
reviewsContainer.appendChild(reviewContent);
});
// 페이징 컨테이너 준비
let reviewsPage = reviewsContainer.querySelector('.reviews_page');
if (!reviewsPage) {
reviewsPage = document.createElement('div');
reviewsPage.classList.add('reviews_page');
}
reviewsPage.innerHTML = '';
// 이전 페이지 링크
if (reviewData.prev) {
reviewsPage.innerHTML += `<a href="#" class="prev" data-page="${reviewData.start - 1}"><</a>`;
}
// 페이지 번호 링크
for (let i = reviewData.start; i <= reviewData.end; i++) {
const currentClass = i === reviewData.pg ? 'current' : 'num';
reviewsPage.innerHTML += `<a href="#" class="${currentClass}" data-page="${i}">${i}</a>`;
}
// 다음 페이지 링크
if (reviewData.next) {
reviewsPage.innerHTML += `<a href="#" class="next" data-page="${reviewData.end + 1}">></a>`;
}
reviewsContainer.appendChild(reviewsPage);
}
// 리뷰 리스트를 AJAX로 요청
function fetchReviews(page, prodNo, sortType) {
let requestUrl = `/product/reviewList?pg=${page}&prodNo=${prodNo}`;
if (sortType) {
requestUrl += `&sortType=${sortType}`;
}
fetch(requestUrl)
.then(response => response.json())
.then(data => {
updateReviewListUI(data);
})
.catch(error => {
console.error('리뷰 목록 로드 실패:', error);
});
}
// 페이지 번호 클릭 이벤트 처리
function handlePaginationClick(event) {
if (event.target.matches('.reviews_page a')) {
event.preventDefault();
const pageNumber = parseInt(event.target.getAttribute('data-page'));
if (!isNaN(pageNumber) && pageNumber > 0) {
fetchReviews(pageNumber, document.getElementById('prodNo').value, currentSortType);
}
}
}
// 정렬 링크 클릭 이벤트 처리
function handleSortClick(event) {
if (event.target.matches('.sort-link')) {
event.preventDefault();
const sortType = event.target.getAttribute('data-sort-type');
currentSortType = sortType; // 선택된 정렬 타입 저장
fetchReviews(1, document.getElementById('prodNo').value, currentSortType);
}
}
// 초기 리뷰 요청 및 이벤트 바인딩
fetchReviews(1, document.getElementById('prodNo').value, currentSortType);
reviewsContainer.addEventListener('click', handlePaginationClick);
reviewsContainer.addEventListener('click', handleSortClick);
기능 설명
- fetchReviews:
/product/reviewList?pg=…&prodNo=…&sortType=…로 AJAX GET 요청- 응답 JSON(
PageResponseDTO)을updateReviewListUI에 전달- updateReviewListUI:
- 기존
.reviews_content요소 제거dtoList배열을 순회하며, 사용자 ID(uid) 마스킹(앞 4글자 +**), 별점(review.rating)만큼 ‘⭐’ 생성- 판매자 정보, 리뷰 내용, 이미지 1·2차 이미지 조건부 삽입
- 페이징 컨테이너(
.reviews_page) 생성:
prev가 true면 이전 페이지 링크(data-page = start–1)start부터end까지 링크 생성(current또는num클래스)next가 true면 다음 페이지 링크(data-page = end+1)- handlePaginationClick:
- 페이지 번호 클릭 시
fetchReviews재호출currentSortType을 유지하면서 해당 페이지 번호 요청- handleSortClick:
- 정렬 링크 클릭 시
data-sort-type값을currentSortType에 저장- 첫 페이지(1)로 정렬된 리뷰 목록 로드