ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -8,9 +8,11 @@
 import com.chone.server.domains.review.domain.Review;
 import com.chone.server.domains.review.dto.request.CreateRequestDto;
 import com.chone.server.domains.review.dto.request.ReviewListRequestDto;
+import com.chone.server.domains.review.dto.request.UpdateRequestDto;
 import com.chone.server.domains.review.dto.response.ReviewDetailResponseDto;
 import com.chone.server.domains.review.dto.response.ReviewListResponseDto;
 import com.chone.server.domains.review.dto.response.ReviewResponseDto;
+import com.chone.server.domains.review.dto.response.ReviewUpdateResponseDto;
 import com.chone.server.domains.review.exception.ReviewExceptionCode;
 import com.chone.server.domains.review.repository.ReviewRepository;
 import com.chone.server.domains.review.repository.ReviewSearchRepository;
@@ -19,6 +21,8 @@
 import com.chone.server.domains.user.domain.User;
 import jakarta.transaction.Transactional;
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.UUID;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -106,7 +110,33 @@ public ReviewDetailResponseDto getReviewById(UUID reviewId, CustomUserDetails pr
 
   private void validateAccess(User user, Review review) {
     if (!review.getIsPublic() && !review.getUser().getId().equals(user.getId())) {
-      throw new ApiBusinessException(ReviewExceptionCode.REVIEW_ACCESS_DENIED);
+      throw new ApiBusinessException(ReviewExceptionCode.REVIEW_FORBIDDEN_ACTION);
     }
   }
+
+  @Transactional
+  public ReviewUpdateResponseDto updateReview(
+      UUID reviewId, UpdateRequestDto request, CustomUserDetails principal) {
+    if (principal == null || principal.getUser() == null) {
+      throw new ApiBusinessException(ReviewExceptionCode.REVIEW_UNAUTHORIZED);
+    }
+
+    Review review =
+        reviewRepository
+            .findById(reviewId)
+            .orElseThrow(() -> new ApiBusinessException(ReviewExceptionCode.REVIEW_NOT_FOUND));
+
+    if (!review.getUser().getId().equals(principal.getUser().getId())) {
+      throw new ApiBusinessException(ReviewExceptionCode.REVIEW_FORBIDDEN_ACTION);
+    }
+
+    if (ChronoUnit.HOURS.between(review.getCreatedAt(), LocalDateTime.now()) > 72) {
+      throw new ApiBusinessException(ReviewExceptionCode.REVIEW_UPDATE_EXPIRED);
+    }
+
+    review.update(
+        request.getContent(), request.getRating(), request.getImageUrl(), request.getIsPublic());
+
+    return new ReviewUpdateResponseDto(review.getId(), review.getUpdatedAt());
+  }
 }

리뷰: 조회 시 삭제된 리뷰 조회되지 않도록 수정하겠습니다~
@@ -0,0 +1,24 @@
+import { BASE_URL } from '@/constants';
+import { GetReviewsParams, ReviewScore } from '@/types/reviewType';
+
+export const getReviewScore = async ({
+	gatheringId,
+	type,
+}: GetReviewsParams): Promise<ReviewScore[]> => {
+	const params = new URLSearchParams();
+	if (gatheringId) params.append('gatheringId', gatheringId);
+	if (type) params.append('type', type);
+
+	const res = await fetch(`${BASE_URL}/reviews/scores?${params.toString()}`, {
+		method: 'GET',
+		headers: {
+			'Content-Type': 'application/json',
+		},
+	});
+
+	if (!res.ok) {
+		throw new Error(`${res.status}`);
+	}
+
+	return res.json();
+};

리뷰: [p5] BASE_URL을 import해서 사용한 부분 좋습니다!🐱
@@ -0,0 +1,18 @@
+export interface PageInfoData {
+	title: string;
+	description: string;
+	src: string;
+}
+
+export interface PageNavData {
+	id: string;
+	label: string;
+	icon?: string;
+	active: boolean;
+	subItems?: PageNavData[];
+}
+
+export interface FilteringData {
+	label: string;
+	value: string;
+}

리뷰: [p5] type 파일을 따로 분리해서 가독성이 좋아졌네요~😁
@@ -0,0 +1,16 @@
+export interface ReviewScore {
+	teamId: number;
+	gatheringId: number;
+	type: string;
+	averageScore: number;
+	oneStar: number;
+	twoStars: number;
+	threeStars: number;
+	fourStars: number;
+	fiveStars: number;
+}
+
+export interface GetReviewsParams {
+	gatheringId?: string;
+	type?: '' | 'DALLAEMFIT' | 'OFFICE_STRETCHING' | 'MINDFULNESS' | 'WORKATION';
+}

리뷰: [p5] type 파일을 따로 분리해서 보기 좋습니다~!🐱
@@ -1,8 +1,6 @@
 package reviewme.review.repository;
 
-import java.time.LocalDate;
 import java.util.List;
-import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import reviewme.review.domain.Review;
@@ -23,17 +21,16 @@ public interface ReviewRepository extends JpaRepository<Review, Long> {
             ORDER BY r.createdAt DESC, r.id DESC
             LIMIT :limit
             """)
-    List<Review> findByReviewGroupIdWithLimit(long reviewGroupId, Long lastReviewId, int limit);
-
-    Optional<Review> findByIdAndReviewGroupId(long reviewId, long reviewGroupId);
+    List<Review> findAllByReviewGroupIdWithLimit(long reviewGroupId, Long lastReviewId, int limit);
 
     @Query("""
-            SELECT COUNT(r.id) > 0 FROM Review r
-            WHERE r.reviewGroupId = :reviewGroupId
-            AND r.id < :reviewId
-            AND CAST(r.createdAt AS java.time.LocalDate) <= :createdDate
+            SELECT r FROM Review r
+            WHERE r.memberId = :memberId
+            AND (:lastReviewId IS NULL OR r.id < :lastReviewId)
+            ORDER BY r.createdAt DESC, r.id DESC
+            LIMIT :limit
             """)
-    boolean existsOlderReviewInGroup(long reviewGroupId, long reviewId, LocalDate createdDate);
+    List<Review> findAllByMemberIdWithLimit(long memberId, Long lastReviewId, int limit);
 
     int countByReviewGroupId(long reviewGroupId);
 }

리뷰: 메서드 이름이 findAll 로 시작하면 리스트로 반환한다는게 더 잘 전달될 것 같아요!
컨벤션에도 맞고요.

그리고 지금 보니 `findByReviewGroupIdWithLimit` 함수도 findAll 이 아니게 되어있는데, 
이 함수 이름도 이 PR에서 수정해도 괜찮다 생각합니다!
@@ -1,14 +1,15 @@
 package reviewme.review.service.dto.response.list;
 
-import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 public record AuthoredReviewElementResponse(
+
         long reviewId,
         String revieweeName,
         String projectName,
-        LocalDate createdAt,
+        LocalDateTime createdAt,
         String contentPreview,
-        List<ReviewCategoryResponse> categories
+        List<SelectedCategoryOptionResponse> categories
 ) {
 }

리뷰: LocalDateTime 으로 바꿔준 이유를 듣고 싶어요~

그리고 응답값의 타입을 우리 마음대로 바꿔도 될까요?
예를 들어서 프론트에서 문자열을 기준으로 파싱을 하고 있었는데,
그 형식이 달라지면 문제가 생길수도 있어요.

(문제가 안될수도 있지만, 지금처럼 급하고 돌아가는 상황에서는 이런 문제 요소를 안만드는게 좋다 생각해서요)
@@ -1,12 +1,13 @@
 package reviewme.review.service.dto.response.list;
 
-import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 public record ReceivedReviewPageElementResponse(
+
         long reviewId,
-        LocalDate createdAt,
+        LocalDateTime createdAt,
         String contentPreview,
-        List<ReviewCategoryResponse> categories
+        List<SelectedCategoryOptionResponse> categories
 ) {
 }

리뷰: 저는 이 네이밍 괜찮은데,
(저는 이 필드 네이밍에 대한 강한 의견이 없어서, 팀원의 선호를 따라가도 됨)
**응답 필드 이름을 지역 변수 이름 바꾸듯이 바꿔도 될까요..! 🤔**

프론트에 변경 사항 알려야 할텐데.. 
백엔드의 선호에 맞게 "이렇게 변경했습니다" 통보하는 것도 좀 아닌 것 같고요😅

네이밍 변경은 이 PR에 필수적인 부분은 아니니 넘겨두고,
api 변경에 대해서 회의할 때 바꾸자고 해보는건 어떨까요?
@@ -1,14 +1,15 @@
 package reviewme.review.service.dto.response.list;
 
-import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 public record AuthoredReviewElementResponse(
+
         long reviewId,
         String revieweeName,
         String projectName,
-        LocalDate createdAt,
+        LocalDateTime createdAt,
         String contentPreview,
-        List<ReviewCategoryResponse> categories
+        List<SelectedCategoryOptionResponse> categories
 ) {
 }

리뷰: LocalDate로 변환해서 응답하면서, 로직 많은 부분에 변환 작업이 들어가고 있었어요.
레포지토리에서 데이터를 조회할 때 SQL에서도 타입을 변경하고, 심지어 엔티티 객체에서도 LocalDate로 변환해서 응답해주는 메서드가 존재하기도 하고요. LocalDate로의 변환은 우리가 홈페이지 화면에서 초 단위까지는 안나오니 그렇게 응답해주는 거예요. 즉, View에 대한 부분이 도메인까지 적용되고 있는거예요. 그리고 지금은 프론트에서 우리가 내려주는 그대로 노출해주고 있지만, 정렬 기준이 최신순만 있는 것이 아니라, 날짜순도 존재하게 되면 우리는 또 LocalDateTime으로 로직을 변경해야 해요. 그럼 프론트의 요청에 따라서 계속해서 로직을 변경할 건가요?  확장성에 닫혀버린 구조로 구현되면 계속해서 프론트에 맞춰진 구현이 될 수 밖에 없다고 생각해요.

그래서 근본적으로 백엔드에서는 원래의 데이터를 내려주고, 프론트에서도 활용 가능한 LocalDateTime여야 하고, 화면에 표시되는 yy:mm:dd 형태는 프론트에서 가공해서 사용함이 맞다고 생각해요. (다른 조회 로직에서는 LocalDateTime을 사용하고 있는 부분도 존재합니다.)


그리고 제 생각에 응답에 대한 타입을 바꾼다고 막 바꿔도 되는지를 눈치보고 걱정할게 아니라, 이정도는 PR 올라오면 백엔드에서 먼저 같이 결정하고 프론트에 충분히 전달할 수 있다고 생각해요. 지금 급하다는 이유로 쌓여있는 리팩터링이 많은 것 같은데, 이정도 작업은 기한 생각해도 충분히 같이 진행될 수 있는 부분이라고 생각합니다.
@@ -1,12 +1,13 @@
 package reviewme.review.service.dto.response.list;
 
-import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 public record ReceivedReviewPageElementResponse(
+
         long reviewId,
-        LocalDate createdAt,
+        LocalDateTime createdAt,
         String contentPreview,
-        List<ReviewCategoryResponse> categories
+        List<SelectedCategoryOptionResponse> categories
 ) {
 }

리뷰: 계속해서 리팩터링 요소를 남겨두는 것이 맞을까요? 작업 단위가 크지 않은 것은 같이 해도 되지 않을까요?

ReviewCategoryResponse categories 이 단어가 무엇을 뜻하나요? "리뷰 카테코리 응답 / 카테고리"인데, 리뷰에 카테고리가 존재하나요?
그냥 네이밍만 보면 리뷰에 카테고리 종류가 있고 그 부분을 나타내는 것처럼 보여요.
이 수정은 선호에 맞게 변경한 부분이 아니라, 올바르게 변경한 부분이고 변경 여파가 크지 않다고 생각해요.
그리고 나중에 다시 보게 되어도 바로 이해할 수 있게 정확한 네이밍을 나타내는 것이 좋다고 생각해요.
@@ -1,14 +1,15 @@
 package reviewme.review.service.dto.response.list;
 
-import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 public record AuthoredReviewElementResponse(
+
         long reviewId,
         String revieweeName,
         String projectName,
-        LocalDate createdAt,
+        LocalDateTime createdAt,
         String contentPreview,
-        List<ReviewCategoryResponse> categories
+        List<SelectedCategoryOptionResponse> categories
 ) {
 }

리뷰: > 날짜순도 존재하게 되면 우리는 또 LocalDateTime으로 로직을 변경해야 해요. 그럼 프론트의 요청에 따라서 계속해서 로직을 변경할 건가요?

저도 본질적으로는 LocalDateTime이 맞다고 생각해요.
이 코멘트는 LocalDate → LocalDateTime 변경이 잘못되었다는 말이 아니라,
백엔드가 api를 작은 부분이라도 논의 없이 바꾸는게 위험하다는 말이었어요.
우리가 변경을 했는데 프론트에 전달을 안한 사항이 있다면 그건 그것대로 리소스가 들어갈 것이고요.

그리고 제가 계속 PR의 단위, 작업의 범위에 대해서 이야기를 하게되는데,
작업을 하다가 다른 코드에서 개선해야할 사항이 보이더라도, 
지금은 내가 해결해야 하는 문제에만 집중하고 PR을 올리는게 맞다 생각해요.
왜냐하면 이 PR의 목적은 "내가 작성한 리뷰 목록 조회를 구현하는 것"이 목적이잖아요.
심지어 우리는 지금 빠르게 머지해야하는 상황이라,
**작업의 범위에 해당되지 부분을 변경해서 불필요한 논의를 만들지 않는게 좋다고 생각해요.**

---

우리 (백엔드 팀 - 산초, 테드)의 협업의 기준을 어떻게 잡아야 할까요?
어느 정도 융통성을 발휘하는게 좋을까요?
아니면 코드 퀄리티라던가, 협업 방식 같은 것들도 어떻게 하는게 좋을지를 이야기하며 진행하는게 좋을까요?

전자를 기준으로 한다면 저는 테드의 방식도 괜찮아요.
그런데 "어떤게 좋은 협업일까"를 생각하다보니

- PR은 작업의 목적에 맞게 만들어야 한다.
- api 문서는 프론트&백 합의하에 만들어진다.

와 같은 것들이 걸려서 저런 코멘트를 남기게돼요.
그리고 제가 맞다고 생각하는 부분을 말해서 테드에게도 도움이 되길 바라고요.

예를 들어서, 테드가 다른 PR에 남겨준 
"String reviewRequeset으로 인한 변경은 임시니까 서비스 코드는 변경 없게 하자"는 리뷰에서 많이 배웠고,
이런 부분을 말해준 테드에게 고마움을 느꼈거든요.

---

만약 테드도 우리 팀의 협업이 "좋은 협업이 무엇일지를 고민하는 방식"으로 진행되는 것에 동의한다면,
저는 "response 이름을 이 PR에서 바꾸는 것은 불필요하다"고 다시 주장하고 싶어요.
그리고 PR은 왜 그것의 목적에만 집중해야하는지에 대해 다시 생각해봤으면 좋겠어요.