ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,96 @@
+const MAX_RANDOM_NUMBER = 9;
+const MIN_RANDOM_NUMBER = 1;
+const $inputNumber = document.querySelector('.input-number');
+const $gameTable = document.querySelector(".game-table");
+
+let answer;
+
+function inputDigit(numberOfDigit){
+    $gameTable.innerHTML = "<tr><td>순서</td><td>숫자</td><td>결과</td></tr>";
+    if(numberOfDigit === "3자리"){
+        answer = randomNumber(3);
+    } else if(numberOfDigit === "4자리"){
+        answer = randomNumber(4);
+    } else if(numberOfDigit === "5자리"){
+        answer = randomNumber(5);
+    }
+}
+
+function randomNumber(numberOfDigit){
+    let result = 0;
+    let existingNumbers = [];
+    for (step = 0; step < numberOfDigit; step++) {
+        let randomNumber = Math.floor(Math.random() * (MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER);
+        while (existingNumbers.includes(randomNumber)){
+            randomNumber = Math.floor(Math.random() * (MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER);
+        }
+        existingNumbers.push(randomNumber);
+    }
+
+    let weight = 1;
+    for(j = 0; j < numberOfDigit; j++){
+        result += existingNumbers[j] * weight;
+        weight *= 10;
+    }
+
+    console.log(result);
+    return result;
+}
+
+let index = 1;
+function addValue(){
+    const newRow = $gameTable.insertRow();
+    const pitchResult = checkBallAndStrike(answer);
+    const convertedAnswer = answer.toString();
+
+    let isValid = 0;
+    if($inputNumber.value.length === convertedAnswer.length){
+        for(let i = 0; i < $inputNumber.value.length; i++){
+            for(let j = 0; j < $inputNumber.value.length; j++){
+                if(i !== j){
+                    if($inputNumber.value[i] === $inputNumber.value[j]){
+                        isValid = 1;
+                    }
+                }
+            }
+        }
+        if(isValid === 0){
+            const turn = newRow.insertCell(0);
+            const number = newRow.insertCell(1);
+            const result = newRow.insertCell(2);
+            turn.innerText = `${index++}`;
+            number.innerText = `${$inputNumber.value}`
+            if(pitchResult[1] === convertedAnswer.length){
+                result.innerText = `O.K.`;
+            } else{
+                result.innerText = `B: ${pitchResult[0]}, S: ${pitchResult[1]}`;
+            }
+        } else{
+            alert("올바르지 않은 값입니다.");
+        }
+       
+    } else{
+        alert("올바르지 않은 값입니다.");
+    }
+
+}
+
+function checkBallAndStrike(answer){
+    const convertedAnswer = answer.toString();
+    const convertedInputNumber = $inputNumber.value.toString();
+    let k = 0;
+    let score = [0, 0];
+    while(k < convertedAnswer.length){
+        if(convertedAnswer[k] === convertedInputNumber[k]){
+            score[1] += 1;
+        } else{
+            for(let item of convertedAnswer){
+                if(item === convertedInputNumber[k]){
+                    score[0] += 1;
+                }
+            }
+        }
+        k += 1;
+    }
+    return score;
+}
\ No newline at end of file

리뷰: do .. while 문을 사용해서 중복 없애보겠습니다!! do .. while 문을 생각 못했네요..
@@ -0,0 +1,96 @@
+const MAX_RANDOM_NUMBER = 9;
+const MIN_RANDOM_NUMBER = 1;
+const $inputNumber = document.querySelector('.input-number');
+const $gameTable = document.querySelector(".game-table");
+
+let answer;
+
+function inputDigit(numberOfDigit){
+    $gameTable.innerHTML = "<tr><td>순서</td><td>숫자</td><td>결과</td></tr>";
+    if(numberOfDigit === "3자리"){
+        answer = randomNumber(3);
+    } else if(numberOfDigit === "4자리"){
+        answer = randomNumber(4);
+    } else if(numberOfDigit === "5자리"){
+        answer = randomNumber(5);
+    }
+}
+
+function randomNumber(numberOfDigit){
+    let result = 0;
+    let existingNumbers = [];
+    for (step = 0; step < numberOfDigit; step++) {
+        let randomNumber = Math.floor(Math.random() * (MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER);
+        while (existingNumbers.includes(randomNumber)){
+            randomNumber = Math.floor(Math.random() * (MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER);
+        }
+        existingNumbers.push(randomNumber);
+    }
+
+    let weight = 1;
+    for(j = 0; j < numberOfDigit; j++){
+        result += existingNumbers[j] * weight;
+        weight *= 10;
+    }
+
+    console.log(result);
+    return result;
+}
+
+let index = 1;
+function addValue(){
+    const newRow = $gameTable.insertRow();
+    const pitchResult = checkBallAndStrike(answer);
+    const convertedAnswer = answer.toString();
+
+    let isValid = 0;
+    if($inputNumber.value.length === convertedAnswer.length){
+        for(let i = 0; i < $inputNumber.value.length; i++){
+            for(let j = 0; j < $inputNumber.value.length; j++){
+                if(i !== j){
+                    if($inputNumber.value[i] === $inputNumber.value[j]){
+                        isValid = 1;
+                    }
+                }
+            }
+        }
+        if(isValid === 0){
+            const turn = newRow.insertCell(0);
+            const number = newRow.insertCell(1);
+            const result = newRow.insertCell(2);
+            turn.innerText = `${index++}`;
+            number.innerText = `${$inputNumber.value}`
+            if(pitchResult[1] === convertedAnswer.length){
+                result.innerText = `O.K.`;
+            } else{
+                result.innerText = `B: ${pitchResult[0]}, S: ${pitchResult[1]}`;
+            }
+        } else{
+            alert("올바르지 않은 값입니다.");
+        }
+       
+    } else{
+        alert("올바르지 않은 값입니다.");
+    }
+
+}
+
+function checkBallAndStrike(answer){
+    const convertedAnswer = answer.toString();
+    const convertedInputNumber = $inputNumber.value.toString();
+    let k = 0;
+    let score = [0, 0];
+    while(k < convertedAnswer.length){
+        if(convertedAnswer[k] === convertedInputNumber[k]){
+            score[1] += 1;
+        } else{
+            for(let item of convertedAnswer){
+                if(item === convertedInputNumber[k]){
+                    score[0] += 1;
+                }
+            }
+        }
+        k += 1;
+    }
+    return score;
+}
\ No newline at end of file

리뷰: $를 붙이는 것이 맞는 것 같습니다!
@@ -0,0 +1,96 @@
+const MAX_RANDOM_NUMBER = 9;
+const MIN_RANDOM_NUMBER = 1;
+const $inputNumber = document.querySelector('.input-number');
+const $gameTable = document.querySelector(".game-table");
+
+let answer;
+
+function inputDigit(numberOfDigit){
+    $gameTable.innerHTML = "<tr><td>순서</td><td>숫자</td><td>결과</td></tr>";
+    if(numberOfDigit === "3자리"){
+        answer = randomNumber(3);
+    } else if(numberOfDigit === "4자리"){
+        answer = randomNumber(4);
+    } else if(numberOfDigit === "5자리"){
+        answer = randomNumber(5);
+    }
+}
+
+function randomNumber(numberOfDigit){
+    let result = 0;
+    let existingNumbers = [];
+    for (step = 0; step < numberOfDigit; step++) {
+        let randomNumber = Math.floor(Math.random() * (MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER);
+        while (existingNumbers.includes(randomNumber)){
+            randomNumber = Math.floor(Math.random() * (MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER);
+        }
+        existingNumbers.push(randomNumber);
+    }
+
+    let weight = 1;
+    for(j = 0; j < numberOfDigit; j++){
+        result += existingNumbers[j] * weight;
+        weight *= 10;
+    }
+
+    console.log(result);
+    return result;
+}
+
+let index = 1;
+function addValue(){
+    const newRow = $gameTable.insertRow();
+    const pitchResult = checkBallAndStrike(answer);
+    const convertedAnswer = answer.toString();
+
+    let isValid = 0;
+    if($inputNumber.value.length === convertedAnswer.length){
+        for(let i = 0; i < $inputNumber.value.length; i++){
+            for(let j = 0; j < $inputNumber.value.length; j++){
+                if(i !== j){
+                    if($inputNumber.value[i] === $inputNumber.value[j]){
+                        isValid = 1;
+                    }
+                }
+            }
+        }
+        if(isValid === 0){
+            const turn = newRow.insertCell(0);
+            const number = newRow.insertCell(1);
+            const result = newRow.insertCell(2);
+            turn.innerText = `${index++}`;
+            number.innerText = `${$inputNumber.value}`
+            if(pitchResult[1] === convertedAnswer.length){
+                result.innerText = `O.K.`;
+            } else{
+                result.innerText = `B: ${pitchResult[0]}, S: ${pitchResult[1]}`;
+            }
+        } else{
+            alert("올바르지 않은 값입니다.");
+        }
+       
+    } else{
+        alert("올바르지 않은 값입니다.");
+    }
+
+}
+
+function checkBallAndStrike(answer){
+    const convertedAnswer = answer.toString();
+    const convertedInputNumber = $inputNumber.value.toString();
+    let k = 0;
+    let score = [0, 0];
+    while(k < convertedAnswer.length){
+        if(convertedAnswer[k] === convertedInputNumber[k]){
+            score[1] += 1;
+        } else{
+            for(let item of convertedAnswer){
+                if(item === convertedInputNumber[k]){
+                    score[0] += 1;
+                }
+            }
+        }
+        k += 1;
+    }
+    return score;
+}
\ No newline at end of file

리뷰: 그러면 index값을 turn에 넣는 것, index가 1씩 증가 하는 것 두 개를 나누도록 수정하겠습니다!
@@ -0,0 +1,53 @@
+'use client';
+
+import Button from '@/app/components/common/button/Button';
+import Modal from '@/app/components/common/modal/Modal';
+import IconAlert from '@/app/components/icons/IconAlert';
+
+interface ConfirmModalInterface {
+  title: string;
+  cancelLabel?: string;
+  confirmLabel?: string;
+  isModalOpen: boolean;
+  handleCancel: () => void;
+  handleConfirm: () => void;
+}
+
+function ConfirmModal({
+  title,
+  cancelLabel,
+  confirmLabel,
+  isModalOpen,
+  handleCancel,
+  handleConfirm,
+}: ConfirmModalInterface) {
+  return (
+    <Modal isOpen={isModalOpen} closeModal={handleCancel}>
+      <div className="flex flex-col items-center">
+        <IconAlert />
+        <div className="mt-4 flex w-[239px] flex-col items-center">
+          <h2 className="mb-4 text-lg font-light">{title}</h2>
+        </div>
+
+        <div className="flex justify-end gap-4">
+          <Button
+            onClick={handleCancel}
+            variant="secondary"
+            className="w-[8.5rem]"
+          >
+            {cancelLabel || '닫기'}
+          </Button>
+          <Button
+            onClick={handleConfirm}
+            variant="danger"
+            className="w-[8.5rem]"
+          >
+            {confirmLabel || '회원 탈퇴'}
+          </Button>
+        </div>
+      </div>
+    </Modal>
+  );
+}
+
+export default ConfirmModal;

리뷰: 공통 컴포넌트 감사합니다!!!!
@@ -10,6 +10,7 @@ import useModal from '@/app/hooks/useModal';
 import DetailMemberModal from '@/app/components/team/DetailMemberModal';
 import deleteMember from '@/app/lib/group/deleteMemeber';
 import { useMutation, useQueryClient } from '@tanstack/react-query';
+import ConfirmModal from '@/app/components/common/modal/ConfirmModal';
 
 interface GroupMember {
   role: 'ADMIN' | 'MEMBER';
@@ -24,6 +25,11 @@ function MemberCard({ member }: { member: GroupMember }) {
   const queryClient = useQueryClient();
   const { isOpen, toggleDropdown, closeDropdown } = useDropdown();
   const { isOpen: isModalOpen, openModal, closeModal } = useModal();
+  const {
+    isOpen: isConfirmModalOpen,
+    openModal: openConfirmModal,
+    closeModal: closeConfirmModal,
+  } = useModal();
 
   const { mutate: expelMember } = useMutation({
     mutationFn: deleteMember,
@@ -66,7 +72,7 @@ function MemberCard({ member }: { member: GroupMember }) {
             <DropdownItem onClick={openModal} onClose={closeDropdown}>
               멤버 정보
             </DropdownItem>
-            <DropdownItem onClick={handleExpel} onClose={closeDropdown}>
+            <DropdownItem onClick={openConfirmModal} onClose={closeDropdown}>
               추방하기
             </DropdownItem>
           </DropdownList>
@@ -77,6 +83,14 @@ function MemberCard({ member }: { member: GroupMember }) {
         isOpen={isModalOpen}
         closeModal={closeModal}
       />
+      <ConfirmModal
+        isModalOpen={isConfirmModalOpen}
+        title={`${member.userName}님을 추방하시겠어요?`}
+        cancelLabel="취소"
+        confirmLabel="추방"
+        handleCancel={closeConfirmModal}
+        handleConfirm={handleExpel}
+      />
     </>
   );
 }

리뷰: `를 추방하시겠어요?` 라고 명시하면 이름 마지막 글자에 받침이 있는 사람은 문구가 어색하게 보일 수 있을 것 같습니다. 
예): 홍지윤를 추방하시겠어요?

그래서 `${member.userName}님을 추방하시겠어요?` 가 더 괜찮을 것 같습니다!
@@ -10,6 +10,7 @@ import useModal from '@/app/hooks/useModal';
 import DetailMemberModal from '@/app/components/team/DetailMemberModal';
 import deleteMember from '@/app/lib/group/deleteMemeber';
 import { useMutation, useQueryClient } from '@tanstack/react-query';
+import ConfirmModal from '@/app/components/common/modal/ConfirmModal';
 
 interface GroupMember {
   role: 'ADMIN' | 'MEMBER';
@@ -24,6 +25,11 @@ function MemberCard({ member }: { member: GroupMember }) {
   const queryClient = useQueryClient();
   const { isOpen, toggleDropdown, closeDropdown } = useDropdown();
   const { isOpen: isModalOpen, openModal, closeModal } = useModal();
+  const {
+    isOpen: isConfirmModalOpen,
+    openModal: openConfirmModal,
+    closeModal: closeConfirmModal,
+  } = useModal();
 
   const { mutate: expelMember } = useMutation({
     mutationFn: deleteMember,
@@ -66,7 +72,7 @@ function MemberCard({ member }: { member: GroupMember }) {
             <DropdownItem onClick={openModal} onClose={closeDropdown}>
               멤버 정보
             </DropdownItem>
-            <DropdownItem onClick={handleExpel} onClose={closeDropdown}>
+            <DropdownItem onClick={openConfirmModal} onClose={closeDropdown}>
               추방하기
             </DropdownItem>
           </DropdownList>
@@ -77,6 +83,14 @@ function MemberCard({ member }: { member: GroupMember }) {
         isOpen={isModalOpen}
         closeModal={closeModal}
       />
+      <ConfirmModal
+        isModalOpen={isConfirmModalOpen}
+        title={`${member.userName}님을 추방하시겠어요?`}
+        cancelLabel="취소"
+        confirmLabel="추방"
+        handleCancel={closeConfirmModal}
+        handleConfirm={handleExpel}
+      />
     </>
   );
 }

리뷰: 문구 디테일을 놓쳤네요.
반영해서 올리겠습니다. 감사합니다😊
@@ -0,0 +1,31 @@
+package com.antique.dto.review;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ReviewRequestDTO {
+    @Schema(description = "리뷰 작성자 ID")
+    @NotNull
+    private Long reviewerId; // 리뷰 작성자 ID
+
+    @Schema(description = "리뷰 대상자 ID")
+    @NotNull
+    private Long reviewedUserId; // 리뷰 대상자 ID
+
+    @Schema(description = "제품 ID")
+    @NotNull
+    private Long productId; // 제품 ID
+
+    @Schema(description = "평점")
+    @NotNull
+    private int rating; // 평점
+
+    @Schema(description = "리뷰 내용")
+    private String content; // 리뷰 내용
+}

리뷰: schema 어노테이션을 사용해서 표현하면 더 좋을 것 같아..!!
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 변수 선언할 때는 `scope` 관리를 위해서 항상 `const`, `let` 등의 키워드를 사용해주는게 좋습니다!
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 여기도 위와 마찬가지로 변수 선언 시에 적절한 키워드가 필요할 것 같습니다.
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 반복문 안의 `validType` 은 재할당할 필요는 없어 보입니다. 그러면 `const` 가 더 적절하지 않을까요?
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 전체적으로 String.prototype.match 를 많이 사용하셨는데, Regex 객체를 사용해 보는 것은 어떯까요?
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: `req.body` 에 다음 키값들 중 하나가 아예 누락된 경우, 런타임에서 에러가 발생할 것 같습니다. 구조화 할당 시에 전체적으로 주의하는게 좋을 것 같습니다
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 여기 반복문 내의 `info` 도 재할당할 일은 없어보이니 `const` 가 더 적절해 보입니다.
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 에러를 던지는 부분을 모듈화하게 되면 더 높은 사용성을 갖게 될 것 같습니다
그리고 직접 에러를 반환하는 것보다 에러를 `throw` 하고, `catch` 구문에서 다음 미들웨어로 건내면서 최종적으로 에러를 헨들링하는 함수에서 한번에 에러를 처리하는 것이 더 좋아 보입니다.
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 전체적으로 하나의 컨트롤러 안에 여러 개의 절차들이 나열되어 있는 것 같습니다. 
함수 단위로 더 쪼게게되면, 나중에 unit test 시에도 더 편리할 것 같습니다. 
검증하고 있는 여러 로직들도, 적절히 Service 안에 넣어둘 필요도 있을 것 같습니다.
예를 들어, 비밀번호를 hash 하는 부분도 req, res 만 다루게 되는 컨트롤러에서 다루는 것이 조금 어색해 보이기도 합니다. 
전반적으로 이 각 함수가 하나의 명확한 일만 하도록 구성하는 것이 좋을 것 같습니다.
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 응답 값도 클라이언트와의 협약에 따라 정해진 폼을 항상 반환할 수 있도록 모듈화해서 사용하는 것이 유지/보수에 더 용이할 것 같습니다.
@@ -0,0 +1,13 @@
+const express = require('express');
+const router = express.Router();
+const middleware = require('../middlewares');
+const { userController } = require('../controllers');
+// Route 는 오직 Controller 에만 의존 합니다.
+
+router.post(
+  '/signup',
+  [middleware.validateSignUpUserData, middleware.hashPassword],
+  userController.signUp,
+);
+
+module.exports = router;

리뷰: 이 부분 정상적으로 작동하나요? 키값이 다른 것 같습니다.