Skip to content

Commit 424cb2b

Browse files
committed
feat: 후기 수정 기능 추가 (#126)
1 parent 0d29455 commit 424cb2b

7 files changed

Lines changed: 80 additions & 23 deletions

File tree

src/main/java/com/knoc/global/exception/ErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public enum ErrorCode {
5454
// 리뷰 관련 (Review)
5555
REVIEW_ALREADY_EXISTS(409, "이미 해당 주문에 대한 후기가 존재합니다."),
5656
REVIEW_NOT_ALLOWED(403, "결제 완료된 주문만 후기를 작성할 수 있습니다."),
57+
REVIEW_NOT_FOUND(404, "해당 주문에 대한 후기가 존재하지 않습니다."),
58+
REVIEW_UPDATE_NOT_ALLOWED(403, "후기를 수정할 권한이 없습니다."),
5759

5860
// GitHub 관련 (Github)
5961
GITHUB_PR_NOT_FOUND(404, "존재하지 않는 PR이거나 접근 권한이 없습니다."),

src/main/java/com/knoc/reviewFeedback/entity/ReviewFeedback.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,9 @@ public ReviewFeedback(Order order, Member junior, SeniorProfile seniorProfile,
5757
this.rating = rating;
5858
this.comment = comment;
5959
}
60+
61+
public void update(byte rating, String comment) {
62+
this.rating = rating;
63+
this.comment = comment;
64+
}
6065
}

src/main/java/com/knoc/reviewFeedback/service/ReviewFeedbackService.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ public void createReview(ReviewFeedbackRequestDto dto, Long juniorId) {
7070

7171
}
7272

73+
@Transactional
74+
public void updateReview(Long orderId, ReviewFeedbackRequestDto dto, Long juniorId) {
75+
ReviewFeedback feedback = reviewFeedbackRepository.findByOrderId(orderId)
76+
.orElseThrow(() -> new BusinessException(ErrorCode.REVIEW_NOT_FOUND));
77+
78+
if (!feedback.getJunior().getId().equals(juniorId)) {
79+
throw new BusinessException(ErrorCode.REVIEW_UPDATE_NOT_ALLOWED);
80+
}
81+
82+
// rating/comment validation은 DTO(@Min/@Max) + 컨트롤러 @Valid에서 처리된다는 전제
83+
feedback.update(dto.getRating(), dto.getComment());
84+
}
85+
7386
public ReviewPageDto getReviewPage() {
7487
List<ReviewFeedback> feedbacks = reviewFeedbackRepository.findAllByOrderByCreatedAtDesc();
7588

src/main/java/com/knoc/workspace/WorkspaceController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ public ResponseEntity<Void> submitFeedback(@PathVariable Long orderId,
7878
return ResponseEntity.ok().build();
7979
}
8080

81+
@Operation(summary = "후기 수정", description = "주니어가 이미 작성한 후기를 수정합니다.")
82+
@PatchMapping("/orders/{orderId}/feedback")
83+
@ResponseBody
84+
@PreAuthorize("hasRole('USER')")
85+
public ResponseEntity<Void> updateFeedback(@PathVariable Long orderId,
86+
@RequestBody ReviewFeedbackRequestDto dto,
87+
Principal principal) {
88+
workspaceFacadeService.updateFeedback(orderId, principal.getName(), dto);
89+
return ResponseEntity.ok().build();
90+
}
91+
8192
@Operation(summary = "후기 조회", description = "제출된 후기(평점/코멘트) 정보를 조회합니다.")
8293
@GetMapping("/orders/{orderId}/feedback")
8394
@ResponseBody

src/main/java/com/knoc/workspace/WorkspaceFacadeService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ public void submitFeedback(Long orderId, String email, ReviewFeedbackRequestDto
222222
));
223223
}
224224

225+
@Transactional
226+
public void updateFeedback(Long orderId, String email, ReviewFeedbackRequestDto dto) {
227+
Member member = memberRepository.findByEmail(email)
228+
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
229+
reviewFeedbackService.updateReview(orderId, dto, member.getId());
230+
}
231+
225232
@Transactional(readOnly = true)
226233
public ReviewFeedbackResponse getReviewFeedback(Long orderId, String email) {
227234
Order order = orderRepository.findById(orderId)

src/main/resources/templates/review/posts.html

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -272,21 +272,21 @@ <h2 class="fw-bold text-white mb-2">성장의 기록이 모이는 곳</h2>
272272
<span th:class="${(i - 0.5) <= review.rating} ? 'star-filled' : 'star-empty'"></span>
273273
</th:block>
274274
</div>
275-
<!-- JS가 '내 후기'에만 표시하도록 토글 -->
276-
<a href="#"
277-
class="btn btn-sm btn-outline-secondary rounded-pill px-2 d-none edit-review-link"
278-
style="font-size:0.78rem; color:#8b93a7; border-color:#2a2f40;"
279-
title="후기 수정"
280-
aria-label="후기 수정"
281-
th:attr="data-order-id=${review.orderId}">
282-
283-
</a>
284275
</div>
285276
</div>
286277
<p class="review-body" th:text="${review.content}"></p>
287-
<div class="d-flex justify-content-end mt-3">
278+
<div class="d-flex align-items-center mt-3">
279+
<!-- JS가 '내 후기'에만 표시하도록 토글 -->
280+
<a th:href="@{|/orders/${review.orderId}?reviewOnly=1|}"
281+
class="btn btn-sm btn-outline-secondary rounded-pill px-2 d-none edit-review-link"
282+
style="font-size:0.78rem; color:#8b93a7; border-color:#2a2f40;"
283+
title="후기 수정"
284+
aria-label="후기 수정"
285+
th:attr="data-order-id=${review.orderId}">
286+
287+
</a>
288288
<a th:href="@{/senior/{id}(id=${review.seniorProfileId})}"
289-
class="btn btn-sm btn-outline-secondary rounded-pill px-3"
289+
class="btn btn-sm btn-outline-secondary rounded-pill px-3 ms-auto"
290290
style="font-size:0.78rem; color:#8b93a7; border-color:#2a2f40;">
291291
멘토 프로필 보기 →
292292
</a>
@@ -527,22 +527,22 @@ <h2 class="fw-bold text-white mb-2">성장의 기록이 모이는 곳</h2>
527527
528528
<div class="d-flex align-items-center gap-2">
529529
<div class="star-row mt-1">${starsHtml(review.rating)}</div>
530-
<a href="#"
531-
class="btn btn-sm btn-outline-secondary rounded-pill px-2 edit-review-link"
532-
style="font-size:0.78rem; color:#8b93a7; border-color:#2a2f40;"
533-
title="후기 수정"
534-
aria-label="후기 수정"
535-
data-order-id="${escapeHtml(String(orderId))}">
536-
537-
</a>
538530
</div>
539531
</div>
540532
541533
<p class="review-body">${content}</p>
542534
543-
<div class="d-flex justify-content-end mt-3">
535+
<div class="d-flex align-items-center mt-3">
536+
<a href="/orders/${encodeURIComponent(orderId)}?reviewOnly=1"
537+
class="btn btn-sm btn-outline-secondary rounded-pill px-2 edit-review-link"
538+
style="font-size:0.78rem; color:#8b93a7; border-color:#2a2f40;"
539+
title="후기 수정"
540+
aria-label="후기 수정"
541+
data-order-id="${escapeHtml(String(orderId))}">
542+
543+
</a>
544544
<a href="/senior/${seniorProfileId}"
545-
class="btn btn-sm btn-outline-secondary rounded-pill px-3"
545+
class="btn btn-sm btn-outline-secondary rounded-pill px-3 ms-auto"
546546
style="font-size:0.78rem; color:#8b93a7; border-color:#2a2f40;">
547547
멘토 프로필 보기 →
548548
</a>

src/main/resources/templates/workspace/workspace.html

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@
806806
function closeConfirmModal() {
807807
document.getElementById('confirmModal').style.display = 'none';
808808
document.getElementById('confirmModal').removeAttribute('data-review-only');
809+
document.getElementById('confirmModal').removeAttribute('data-review-edit');
809810
}
810811

811812
function onBackdropClick(e) { if (e.target === document.getElementById('confirmModal')) closeConfirmModal(); }
@@ -816,6 +817,7 @@
816817
btnOnly.style.display = ''; btnOnly.disabled = false; btnOnly.textContent = '후기 없이 멘토링 종료하기';
817818
btnReview.disabled = false; btnReview.textContent = '멘토링 종료 및 정산 완료';
818819
document.getElementById('confirmModal').removeAttribute('data-review-only');
820+
document.getElementById('confirmModal').removeAttribute('data-review-edit');
819821
}
820822

821823
function updateStars(val) {
@@ -832,6 +834,7 @@
832834

833835
function doSettle(withReview) {
834836
const isReviewOnly = document.getElementById('confirmModal').getAttribute('data-review-only') === 'true';
837+
const isReviewEdit = document.getElementById('confirmModal').getAttribute('data-review-edit') === 'true';
835838
if (withReview && selectedRating === 0) { alert('별점을 선택해 주세요.'); return; }
836839
const btnOnly = document.getElementById('btnSettleOnly');
837840
const btnReview = document.getElementById('btnSettleReview');
@@ -844,7 +847,8 @@
844847
const chain = withReview
845848
? settlePromise.then(function() {
846849
return fetch('/orders/' + orderId + '/feedback', {
847-
method: 'POST', headers: { 'Content-Type': 'application/json' },
850+
method: (isReviewOnly && isReviewEdit) ? 'PATCH' : 'POST',
851+
headers: { 'Content-Type': 'application/json' },
848852
body: JSON.stringify({ orderId: orderId, rating: selectedRating, comment: comment || null })
849853
}).then(function(r) { if (!r.ok) throw new Error('review'); });
850854
})
@@ -1001,7 +1005,22 @@
10011005
// query param으로 진입 시 후기 모달 자동 오픈
10021006
const openReviewOnlyModalOnLoad = /*[[${openReviewOnlyModal}]]*/ false;
10031007
if (openReviewOnlyModalOnLoad) {
1004-
try { openReviewOnlyModal(); } catch (e) {}
1008+
try {
1009+
openReviewOnlyModal();
1010+
const hasReviewOnLoad = /*[[${workspace.hasReview}]]*/ false;
1011+
if (hasReviewOnLoad) {
1012+
fetch('/orders/' + orderId + '/feedback', { method: 'GET', headers: { 'Accept': 'application/json' } })
1013+
.then(function(r) { if (!r.ok) throw new Error('fetch'); return r.json(); })
1014+
.then(function(data) {
1015+
selectedRating = parseInt(data.rating || 0);
1016+
updateStars(selectedRating);
1017+
document.getElementById('commentInput').value = (data.comment || '');
1018+
document.getElementById('confirmModal').setAttribute('data-review-edit', 'true');
1019+
document.getElementById('btnSettleReview').textContent = '후기 수정하기';
1020+
})
1021+
.catch(function() { /* ignore */ });
1022+
}
1023+
} catch (e) {}
10051024
}
10061025
</script>
10071026
</th:block>

0 commit comments

Comments
 (0)