ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,68 @@
+package com.example.review.controller;
+
+import com.example.review.dto.request.ReviewCreateRequestDto;
+import com.example.review.dto.request.ReviewUpdateRequestDto;
+import com.example.review.dto.response.ReviewResponseSimpleDto;
+import com.example.review.dto.response.ReviewResponseDetailDto;
+import com.example.review.service.ReviewService;
+import com.example.utils.AuthUtil;
+import com.example.utils.PageQuery;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/reviews")
+@RequiredArgsConstructor
+public class ReviewController {
+
+    private final ReviewService reviewService;
+
+    // 리뷰 생성
+    @PostMapping
+    public ResponseEntity<ReviewResponseDetailDto> create(
+            @RequestBody ReviewCreateRequestDto requestDto
+    ) {
+        ReviewResponseDetailDto responseDto = reviewService.create(requestDto);
+        return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
+    }
+
+    // 리뷰 수정
+    @PatchMapping("/{id}")
+    public ResponseEntity<ReviewResponseDetailDto> update(
+            @PathVariable int id,
+            @RequestBody ReviewUpdateRequestDto requestDto
+    ) {
+        ReviewResponseDetailDto responseDto = reviewService.update(id, requestDto);
+        return ResponseEntity.ok(responseDto);
+    }
+
+    // 특정 상점의 리뷰 페이지 조회
+    @GetMapping("/shops/{shopId}")
+    public ResponseEntity<Page<ReviewResponseSimpleDto>> findAllReviewByShopId(
+            @PathVariable int shopId, @RequestParam int page) {
+        Pageable pageable = PageRequest.of(page, 10, Sort.by(Sort.Direction.DESC, "updatedAt"));
+        Page<ReviewResponseSimpleDto> reviews = reviewService.getReviewsByShopId(shopId, pageable);
+        return ResponseEntity.ok(reviews);
+    }
+
+    // 특정 유저의 모든 리뷰 조회
+    @GetMapping
+    public ResponseEntity<com.example.utils.Page<ReviewResponseSimpleDto>> findAllReviewByUserId(PageQuery pageQuery){
+        return ResponseEntity.ok(reviewService.findAllByUserId(pageQuery));
+    }
+
+    // 리뷰 삭제
+    @DeleteMapping("/{reviewId}")
+    public ResponseEntity<Void> delete(@PathVariable int reviewId) {
+        reviewService.delete(reviewId);
+        return ResponseEntity.noContent().build();
+    }
+}

리뷰: 수정 했습니다!
@@ -1,16 +1,18 @@
 package com.example.review.entity;
+
+import com.example.common.entity.BaseEntity;
 import com.example.order.entity.Order;
+import com.example.review.dto.request.ReviewUpdateRequestDto;
 import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
 import lombok.Getter;
-import org.springframework.data.annotation.CreatedDate;
-import org.springframework.data.annotation.LastModifiedDate;
-
-import java.time.LocalDateTime;
+import lombok.NoArgsConstructor;
 
 @Getter
-@Table(name = "reviews")
 @Entity
-public class Review {
+@NoArgsConstructor
+@AllArgsConstructor
+public class Review extends BaseEntity {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private int id;
@@ -20,15 +22,32 @@ public class Review {
     private Order order;
 
     @Column(nullable = false)
-    private Integer rating;
+    private int rating;
+
+    @Column(nullable = false, length = 50)
+    private String description;
 
-    @Column(length = 50)
-    private String content;
+    public Review(Order order, Integer rating, String description) {
+        this.order = order;
+        this.rating = rating;
+        this.description = description;
+    }
 
-    @Column(updatable = false)
-    @CreatedDate
-    private LocalDateTime createdAt;
+    public Review partialUpdate(ReviewUpdateRequestDto dto){
+        this.rating = dto.rating();
+        this.description = dto.description();
+        return this;
+    }
 
-    @LastModifiedDate
-    private LocalDateTime updatedAt;
+    public static Review from(
+            Order order,
+            int rating,
+            String description
+    ) {
+        return new Review(
+                order,
+                rating,
+                description
+        );
+    }
 }

리뷰: 수정 했습니다!
@@ -1,6 +1,6 @@
 import Rating from '@/components/@shared/rating/Rating';
 import ProgressBar from '@/components/@shared/ProgressBar';
-import { useQuery } from '@tanstack/react-query';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
 import { getReviewsRating } from '@/axios/allReview/apis';
 
 interface RatingSectionProps {
@@ -11,6 +11,7 @@ export default function RatingSection({ selectedGenre }: RatingSectionProps) {
   const { data: ratingsData, isLoading } = useQuery({
     queryKey: ['ratings', selectedGenre],
     queryFn: () => getReviewsRating(selectedGenre),
+    placeholderData: keepPreviousData,
   });
 
   if (isLoading)

리뷰: `placeholderData`는 여기서 어떤 역할을 하나요?
@@ -1,6 +1,6 @@
 import Rating from '@/components/@shared/rating/Rating';
 import ProgressBar from '@/components/@shared/ProgressBar';
-import { useQuery } from '@tanstack/react-query';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
 import { getReviewsRating } from '@/axios/allReview/apis';
 
 interface RatingSectionProps {
@@ -11,6 +11,7 @@ export default function RatingSection({ selectedGenre }: RatingSectionProps) {
   const { data: ratingsData, isLoading } = useQuery({
     queryKey: ['ratings', selectedGenre],
     queryFn: () => getReviewsRating(selectedGenre),
+    placeholderData: keepPreviousData,
   });
 
   if (isLoading)

리뷰: 장르를 바꿀 때 queryKey selecedGenre 가 같이 바뀌면서 데이터 패칭이 일어나는데, 그 데이터 패칭이 완료되기 전까지 기존데이터를 유지하는 역할을 합니다.

따라서, isLoading이 따로 실행되지 않아서 화면이 깜빡이는듯한 연출을 없앨 수 있습니다.
@@ -1,6 +1,6 @@
 import Rating from '@/components/@shared/rating/Rating';
 import ProgressBar from '@/components/@shared/ProgressBar';
-import { useQuery } from '@tanstack/react-query';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
 import { getReviewsRating } from '@/axios/allReview/apis';
 
 interface RatingSectionProps {
@@ -11,6 +11,7 @@ export default function RatingSection({ selectedGenre }: RatingSectionProps) {
   const { data: ratingsData, isLoading } = useQuery({
     queryKey: ['ratings', selectedGenre],
     queryFn: () => getReviewsRating(selectedGenre),
+    placeholderData: keepPreviousData,
   });
 
   if (isLoading)

리뷰: "데이터 페칭이 일어나는데" -> 왜 분명 데이터 페칭이 일어나는건데 isLoading이 실행되지 않을까요? 
또 리액트쿼리는 캐시데이터가 있는데 왜 캐시데이터를 사용하지 않으실까요?
캐시데이터와 placeholderData는 어떻게 다를까요?
@@ -3,7 +3,7 @@ import { IReviewController } from '#reviews/interfaces/review.controller.interfa
 import { Body, Controller, Delete, Get, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
 import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
 import { ReviewService } from './review.service.js';
-import { DriverReviewResponseDTO, MyReviewResponseDTO, ReviewInputDTO, ReviewOutputDTO } from './review.types.js';
+import { DriverReviewResponseDTO, MyReviewResponseDTO, ReviewBodyDTO, ReviewInputDTO, ReviewOutputDTO } from './review.types.js';
 import { GetQueries } from '#types/queries.type.js';
 import { SortOrder } from '#types/options.type.js';
 
@@ -39,23 +39,23 @@ export class ReviewController implements IReviewController {
     const { page = 1, pageSize = 10, orderBy = SortOrder.Recent, keyword = '' } = query;
     const options = { page, pageSize, orderBy, keyword };
 
-    const { totalCount, list } = await this.reviewService.getDriverReviews(driverId, options);
+    const { totalCount, stats, list } = await this.reviewService.getDriverReviews(driverId, options);
 
-    return { totalCount, list };
+    return { totalCount, stats, list };
   }
 
-  @Post(':driverId')
+  @Post(':estimationId')
   @UseGuards(AccessTokenGuard)
   @ApiBearerAuth('accessToken')
   @ApiOperation({ summary: '리뷰 생성' })
-  @ApiParam({ name: 'driverId', description: '기사 ID', type: 'string' })
-  @ApiBody({ type: ReviewInputDTO })
+  @ApiParam({ name: 'estimationId', description: '견적 ID', type: 'string' })
+  @ApiBody({ type: ReviewBodyDTO })
   @ApiResponse({
     status: HttpStatus.CREATED,
     type: ReviewOutputDTO,
   })
-  async postReview(@Param('driverId') driverId: string, @Body() body: ReviewInputDTO) {
-    const review = await this.reviewService.postReview(driverId, body);
+  async postReview(@Param('estimationId') estimationId: string, @Body() body: ReviewInputDTO) {
+    const review = await this.reviewService.postReview(estimationId, body);
 
     return review;
   }
@@ -65,7 +65,7 @@ export class ReviewController implements IReviewController {
   @ApiBearerAuth('accessToken')
   @ApiOperation({ summary: '리뷰 수정' })
   @ApiParam({ name: 'reviewId', description: '리뷰 ID', type: 'string' })
-  @ApiBody({ type: ReviewInputDTO })
+  @ApiBody({ type: ReviewBodyDTO })
   @ApiResponse({
     status: HttpStatus.OK,
     type: ReviewOutputDTO,

리뷰: 여기도 각각의 메소드를 분리하는게 좋을 것 같습니다
@@ -1,9 +1,30 @@
 import React from 'react';
 import './LoginKang.scss';
+import LoginValidation from './components/LoginValidation';
 
 class LoginKang extends React.Component {
   render() {
-    return <div className="LoginKang">LoginKang</div>;
+    return (
+      <div className="whole-area">
+        <section>
+          <main className="login-main">
+            <div className="login-wrapper">
+              <div className="brand-logo">westagram</div>
+              <LoginValidation />
+            </div>
+            <div className="forgot-pw">
+              <a href="https://www.instagram.com/accounts/password/reset/">
+                비밀번호를 잊으셨나요?
+              </a>
+            </div>
+          </main>
+
+          <footer>
+            <span>© 2021 Instagram from Facebook</span>
+          </footer>
+        </section>
+      </div>
+    );
   }
 }
 

리뷰: kebab-case와 camelCase를 혼용하고 계시네요! 통일해서 사용해주셔야 합니다.
@@ -1,9 +1,30 @@
 import React from 'react';
 import './LoginKang.scss';
+import LoginValidation from './components/LoginValidation';
 
 class LoginKang extends React.Component {
   render() {
-    return <div className="LoginKang">LoginKang</div>;
+    return (
+      <div className="whole-area">
+        <section>
+          <main className="login-main">
+            <div className="login-wrapper">
+              <div className="brand-logo">westagram</div>
+              <LoginValidation />
+            </div>
+            <div className="forgot-pw">
+              <a href="https://www.instagram.com/accounts/password/reset/">
+                비밀번호를 잊으셨나요?
+              </a>
+            </div>
+          </main>
+
+          <footer>
+            <span>© 2021 Instagram from Facebook</span>
+          </footer>
+        </section>
+      </div>
+    );
   }
 }
 

리뷰: 확인 후 수정하였습니다 :)
@@ -1,7 +1,77 @@
-import React from 'react';
+import React, { Component } from 'react';
+import './Login.scss';
 
-function Login() {
-  return <div>Nav bar</div>;
+class Login extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: '',
+      pwd: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  handleSubmit = event => {
+    event.preventDefault();
+    this.handleValidate();
+
+    this.setState({
+      id: '',
+      pwd: '',
+    });
+  };
+
+  render() {
+    const { id, pwd } = this.state;
+    return (
+      <div className="login">
+        <div className="login-container">
+          <h2 className="westagram-logo">westagram</h2>
+          <form className="login-form" onSubmit={this.handleSubmit}>
+            <input
+              type="email"
+              className="login-input"
+              name="id"
+              value={this.state.id}
+              placeholder="전화번호, 사용자 이름 또는 이메일"
+              onChange={this.handleInput}
+              aria-label="이메일"
+            />
+            <input
+              type="password"
+              className="login-input"
+              name="pwd"
+              value={this.state.pwd}
+              placeholder="비밀번호"
+              autoComplete="off"
+              aria-label="비밀번호"
+              onChange={this.handleInput}
+            />
+            <button
+              type="submit"
+              className={
+                id.includes('@') && pwd.length >= 5
+                  ? 'login-btn--active'
+                  : 'login-btn'
+              }
+            >
+              로그인
+            </button>
+          </form>
+          <p className="singUp">
+            <span>비밀번호 찾기</span> / <a href="/signup">가입하기</a>
+          </p>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default Login;

리뷰: 송현님, validation을 기본값으로는 false로 주신 뒤
조건 (in.includes('@') && pwd.length>=5) 이 true일 경우 validation도 true로 변해서 버튼이 활성화 되는 기능을 구현하신 것이죠?
저는 render()안의 버튼에 조건에 따라 삼항 연산자로 class명을 주고, 스타일링을 다르게 해서 색상만 변경됐었는데 버튼 활성화 자체를 구현하신 것 보고 많이 배웠습니다👍
@@ -1,7 +1,77 @@
-import React from 'react';
+import React, { Component } from 'react';
+import './Login.scss';
 
-function Login() {
-  return <div>Nav bar</div>;
+class Login extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: '',
+      pwd: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  handleSubmit = event => {
+    event.preventDefault();
+    this.handleValidate();
+
+    this.setState({
+      id: '',
+      pwd: '',
+    });
+  };
+
+  render() {
+    const { id, pwd } = this.state;
+    return (
+      <div className="login">
+        <div className="login-container">
+          <h2 className="westagram-logo">westagram</h2>
+          <form className="login-form" onSubmit={this.handleSubmit}>
+            <input
+              type="email"
+              className="login-input"
+              name="id"
+              value={this.state.id}
+              placeholder="전화번호, 사용자 이름 또는 이메일"
+              onChange={this.handleInput}
+              aria-label="이메일"
+            />
+            <input
+              type="password"
+              className="login-input"
+              name="pwd"
+              value={this.state.pwd}
+              placeholder="비밀번호"
+              autoComplete="off"
+              aria-label="비밀번호"
+              onChange={this.handleInput}
+            />
+            <button
+              type="submit"
+              className={
+                id.includes('@') && pwd.length >= 5
+                  ? 'login-btn--active'
+                  : 'login-btn'
+              }
+            >
+              로그인
+            </button>
+          </form>
+          <p className="singUp">
+            <span>비밀번호 찾기</span> / <a href="/signup">가입하기</a>
+          </p>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default Login;

리뷰: 송현님, 여기서 bind(this)를 사용하신 것은 어떤 의미인지 여쭤봐도 될까요?
꼭 필요한 요소인지 궁금합니다!
@@ -1,7 +1,77 @@
-import React from 'react';
+import React, { Component } from 'react';
+import './Login.scss';
 
-function Login() {
-  return <div>Nav bar</div>;
+class Login extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: '',
+      pwd: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  handleSubmit = event => {
+    event.preventDefault();
+    this.handleValidate();
+
+    this.setState({
+      id: '',
+      pwd: '',
+    });
+  };
+
+  render() {
+    const { id, pwd } = this.state;
+    return (
+      <div className="login">
+        <div className="login-container">
+          <h2 className="westagram-logo">westagram</h2>
+          <form className="login-form" onSubmit={this.handleSubmit}>
+            <input
+              type="email"
+              className="login-input"
+              name="id"
+              value={this.state.id}
+              placeholder="전화번호, 사용자 이름 또는 이메일"
+              onChange={this.handleInput}
+              aria-label="이메일"
+            />
+            <input
+              type="password"
+              className="login-input"
+              name="pwd"
+              value={this.state.pwd}
+              placeholder="비밀번호"
+              autoComplete="off"
+              aria-label="비밀번호"
+              onChange={this.handleInput}
+            />
+            <button
+              type="submit"
+              className={
+                id.includes('@') && pwd.length >= 5
+                  ? 'login-btn--active'
+                  : 'login-btn'
+              }
+            >
+              로그인
+            </button>
+          </form>
+          <p className="singUp">
+            <span>비밀번호 찾기</span> / <a href="/signup">가입하기</a>
+          </p>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default Login;

리뷰: 네 맞아요. form 자체에 submit 이벤트를 발생시키면 추후에 문제가 생길 수 있어서 유효성 검사 함수는 따로 빼주는게 좋습니다.
@@ -1,7 +1,77 @@
-import React from 'react';
+import React, { Component } from 'react';
+import './Login.scss';
 
-function Login() {
-  return <div>Nav bar</div>;
+class Login extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: '',
+      pwd: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  handleSubmit = event => {
+    event.preventDefault();
+    this.handleValidate();
+
+    this.setState({
+      id: '',
+      pwd: '',
+    });
+  };
+
+  render() {
+    const { id, pwd } = this.state;
+    return (
+      <div className="login">
+        <div className="login-container">
+          <h2 className="westagram-logo">westagram</h2>
+          <form className="login-form" onSubmit={this.handleSubmit}>
+            <input
+              type="email"
+              className="login-input"
+              name="id"
+              value={this.state.id}
+              placeholder="전화번호, 사용자 이름 또는 이메일"
+              onChange={this.handleInput}
+              aria-label="이메일"
+            />
+            <input
+              type="password"
+              className="login-input"
+              name="pwd"
+              value={this.state.pwd}
+              placeholder="비밀번호"
+              autoComplete="off"
+              aria-label="비밀번호"
+              onChange={this.handleInput}
+            />
+            <button
+              type="submit"
+              className={
+                id.includes('@') && pwd.length >= 5
+                  ? 'login-btn--active'
+                  : 'login-btn'
+              }
+            >
+              로그인
+            </button>
+          </form>
+          <p className="singUp">
+            <span>비밀번호 찾기</span> / <a href="/signup">가입하기</a>
+          </p>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default Login;

리뷰: 저도 궁금합니다! 펑션 안에서 this 쓸 때, 바깥의 this 받으려고 사용하는 걸로 알고 있었는데 this.setState 때문에 사용하신건가요?
@@ -1,7 +1,77 @@
-import React from 'react';
+import React, { Component } from 'react';
+import './Login.scss';
 
-function Login() {
-  return <div>Nav bar</div>;
+class Login extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: '',
+      pwd: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  handleSubmit = event => {
+    event.preventDefault();
+    this.handleValidate();
+
+    this.setState({
+      id: '',
+      pwd: '',
+    });
+  };
+
+  render() {
+    const { id, pwd } = this.state;
+    return (
+      <div className="login">
+        <div className="login-container">
+          <h2 className="westagram-logo">westagram</h2>
+          <form className="login-form" onSubmit={this.handleSubmit}>
+            <input
+              type="email"
+              className="login-input"
+              name="id"
+              value={this.state.id}
+              placeholder="전화번호, 사용자 이름 또는 이메일"
+              onChange={this.handleInput}
+              aria-label="이메일"
+            />
+            <input
+              type="password"
+              className="login-input"
+              name="pwd"
+              value={this.state.pwd}
+              placeholder="비밀번호"
+              autoComplete="off"
+              aria-label="비밀번호"
+              onChange={this.handleInput}
+            />
+            <button
+              type="submit"
+              className={
+                id.includes('@') && pwd.length >= 5
+                  ? 'login-btn--active'
+                  : 'login-btn'
+              }
+            >
+              로그인
+            </button>
+          </form>
+          <p className="singUp">
+            <span>비밀번호 찾기</span> / <a href="/signup">가입하기</a>
+          </p>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default Login;

리뷰: 리액트 클래스 컴포넌트에서 bind를 사용하지 this는 `undefined`로 인식해요 그래서 이 경우에 this를 알려줘야 하는데 두가지 방법이 있어요. 
1. 태그 자체에서 이벤트를 화살표 함수로 써주는 것입니다.  ex) `<input onChange={(e) => handleChange(e)} >`
2. 저는 첫 번째 방식이 가독성이 좋지 않아서 차라리 리액트 문서에서 권장하는 두 번째 방식으로 [this를 연결](https://ko.reactjs.org/docs/handling-events.html)했습니다.
@@ -1,7 +1,77 @@
-import React from 'react';
+import React, { Component } from 'react';
+import './Login.scss';
 
-function Login() {
-  return <div>Nav bar</div>;
+class Login extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: '',
+      pwd: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  handleSubmit = event => {
+    event.preventDefault();
+    this.handleValidate();
+
+    this.setState({
+      id: '',
+      pwd: '',
+    });
+  };
+
+  render() {
+    const { id, pwd } = this.state;
+    return (
+      <div className="login">
+        <div className="login-container">
+          <h2 className="westagram-logo">westagram</h2>
+          <form className="login-form" onSubmit={this.handleSubmit}>
+            <input
+              type="email"
+              className="login-input"
+              name="id"
+              value={this.state.id}
+              placeholder="전화번호, 사용자 이름 또는 이메일"
+              onChange={this.handleInput}
+              aria-label="이메일"
+            />
+            <input
+              type="password"
+              className="login-input"
+              name="pwd"
+              value={this.state.pwd}
+              placeholder="비밀번호"
+              autoComplete="off"
+              aria-label="비밀번호"
+              onChange={this.handleInput}
+            />
+            <button
+              type="submit"
+              className={
+                id.includes('@') && pwd.length >= 5
+                  ? 'login-btn--active'
+                  : 'login-btn'
+              }
+            >
+              로그인
+            </button>
+          </form>
+          <p className="singUp">
+            <span>비밀번호 찾기</span> / <a href="/signup">가입하기</a>
+          </p>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default Login;

리뷰: 궁금해서 질문 남깁니다! [event.target.name]은 위에 state 선언할 때 없는데, 
input 태그에 이름을 주기 위한건가요? 
그렇다면, 혹시 처음 render 할때는 'id'를 넣고, 값 변경될때 값을 이름에 넣어주신 이유가 궁금합니다!