ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,39 @@
+package nextstep.subway.unit;
+
+import nextstep.subway.applicaion.LineService;
+import nextstep.subway.applicaion.PathService;
+import nextstep.subway.applicaion.StationService;
+import nextstep.subway.domain.Station;
+import nextstep.subway.utils.TestObjectFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@ExtendWith(MockitoExtension.class)
+public class PathServiceMockTest {
+
+    @Mock
+    LineService lineService;
+
+    @Mock
+    StationService stationService;
+
+    @InjectMocks
+    PathService pathService;
+
+    TestObjectFactory testObjectFactory = new TestObjectFactory();
+
+    @Test
+    @DisplayName("실패_출발역과 도착역이 같은 경우")
+    void fail_same_start_end() {
+        Station 강남역 = testObjectFactory.역생성("강남역");
+        Station 양재역 = testObjectFactory.역생성("양재역");
+
+        assertThrows(IllegalArgumentException.class, () -> pathService.getPath(강남역.getId(), 양재역.getId()));
+    }
+}

리뷰: @sang-eun 
네네 mock을 사용한 테스팅의 경우 의존하는 객체를 매번 설정을 하는 것이 매우 번거롭죠...!
그래서 첫번째 단계에서 얘기드렸듯이 저는 fake 객체를 사용하는 걸 선호하는 편입니다!

지금과 같은 경우 길을 찾는 역할을 갖는 객체를 따로 만든다면 좀 더 가벼운 테스팅이 가능할 것 같습니다!
(이미 `Path` 객체에서 처리하도록 하셨네요 👍 )
@@ -0,0 +1,32 @@
+package nextstep.subway.exception;
+
+import nextstep.subway.auth.application.AuthorizationException;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionAdvisor {
+
+    @ExceptionHandler({IllegalArgumentException.class})
+    public ResponseEntity handleIllegalArgsException(Exception e) {
+        return ResponseEntity.badRequest().build();
+    }
+
+    @ExceptionHandler({RuntimeException.class})
+    public ResponseEntity handleRuntimeException(Exception e) {
+        return ResponseEntity.badRequest().build();
+    }
+
+    @ExceptionHandler(AuthorizationException.class)
+    public ResponseEntity handleAuthorizationException(Exception e) {
+        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
+    }
+
+    @ExceptionHandler(DataIntegrityViolationException.class)
+    public ResponseEntity handleIllegalArgsException(DataIntegrityViolationException e) {
+        return ResponseEntity.badRequest().build();
+    }
+}

리뷰: 공통 예외 처리 좋습니다. 👍
@@ -0,0 +1,50 @@
+package nextstep.subway.fare.domain;
+
+import java.util.Arrays;
+
+public enum DiscountPolicy {
+    ADULT(19, 500, 0, 0),
+    TEENAGER(13, 19, 350, 0.2),
+    CHILD(6, 13, 350, 0.5),
+    INFANT(0, 6, 0, 1);
+
+    private int ageStart;
+    private final int ageEnd;
+    private final int minusFare;
+    private final double discountRatio;
+
+    DiscountPolicy(int ageStart, int ageEnd, int minusFare, double discountRatio) {
+
+        this.ageStart = ageStart;
+        this.ageEnd = ageEnd;
+        this.minusFare = minusFare;
+        this.discountRatio = discountRatio;
+    }
+
+    public int getDiscount(int fare) {
+        if (fare < 0) {
+            throw new IllegalArgumentException();
+        }
+        double discounted = (fare - this.minusFare) * (1 - this.discountRatio);
+        return (int) discounted;
+    }
+
+    public static DiscountPolicy getPolicy(int age) {
+        return Arrays.stream(values()).filter(
+                policy -> policy.isMatch(age)
+            ).findFirst()
+            .orElseThrow(IllegalArgumentException::new);
+    }
+
+    public int getAgeStart() {
+        return ageStart;
+    }
+
+    public int getAgeEnd() {
+        return ageEnd;
+    }
+
+    public boolean isMatch(int age) {
+        return getAgeStart() <= age && age < getAgeEnd();
+    }
+}

리뷰: 나이에 따른 할인 요금 계산 로직 잘 구현해주셨습니다. 👍 
`ageStart`, `ageEnd`도 충분히 무슨 역할을 하는지 알 수 있는 이름인데, 비교에 사용되니까 이상/초과, 이하/미만 구분을 위해 `ageMinInclusive`, ageMaxExclusive`와 같이 수정해보셔도 좋겠습니다.
@@ -0,0 +1,50 @@
+package nextstep.subway.fare.domain;
+
+import java.util.Arrays;
+
+public enum DiscountPolicy {
+    ADULT(19, 500, 0, 0),
+    TEENAGER(13, 19, 350, 0.2),
+    CHILD(6, 13, 350, 0.5),
+    INFANT(0, 6, 0, 1);
+
+    private int ageStart;
+    private final int ageEnd;
+    private final int minusFare;
+    private final double discountRatio;
+
+    DiscountPolicy(int ageStart, int ageEnd, int minusFare, double discountRatio) {
+
+        this.ageStart = ageStart;
+        this.ageEnd = ageEnd;
+        this.minusFare = minusFare;
+        this.discountRatio = discountRatio;
+    }
+
+    public int getDiscount(int fare) {
+        if (fare < 0) {
+            throw new IllegalArgumentException();
+        }
+        double discounted = (fare - this.minusFare) * (1 - this.discountRatio);
+        return (int) discounted;
+    }
+
+    public static DiscountPolicy getPolicy(int age) {
+        return Arrays.stream(values()).filter(
+                policy -> policy.isMatch(age)
+            ).findFirst()
+            .orElseThrow(IllegalArgumentException::new);
+    }
+
+    public int getAgeStart() {
+        return ageStart;
+    }
+
+    public int getAgeEnd() {
+        return ageEnd;
+    }
+
+    public boolean isMatch(int age) {
+        return getAgeStart() <= age && age < getAgeEnd();
+    }
+}

리뷰: 예외를 발생시킬 때 예외 내용, 디버깅에 필요한 값을 메시지로 함께 전달하시면, 문제 상황에서 원인 추적이 수월해서 좋습니다. :)
@@ -0,0 +1,42 @@
+package nextstep.subway.fare.domain;
+
+import java.util.Arrays;
+
+public enum DistanceExtraFare {
+    NEAR(10, 50, 100, 5),
+    FAR(50, 500, 100, 8);
+
+    private final int distanceStart;
+    private final int distanceEnd;
+    private final int fare;
+    private final int unitDistanceInKiloMeter;
+
+    DistanceExtraFare(int distanceStart, int distanceEnd, int fare, int unitDistanceInKiloMeter) {
+
+        this.distanceStart = distanceStart;
+        this.distanceEnd = distanceEnd;
+        this.fare = fare;
+        this.unitDistanceInKiloMeter = unitDistanceInKiloMeter;
+    }
+
+    public static int calculate(int distance) {
+        return Arrays.stream(values())
+            .map(distanceExtraFare -> distanceExtraFare.calculateBy(distance))
+            .reduce(Integer::sum)
+            .orElse(0);
+    }
+
+    private int calculateBy(int distance) {
+        if (distance <= this.distanceStart) {
+            return 0;
+        }
+        if (distance > this.distanceEnd) {
+            distance = this.distanceEnd;
+        }
+        return ((distance - this.distanceStart) / this.unitDistanceInKiloMeter) * this.fare;
+    }
+
+    public int getFare() {
+        return this.fare;
+    }
+}

리뷰: 16km를 가는 경우 추가 요금은 ((16 - 10) / 5) * 100 = 100원이 될 것 같아요!
참고 링크에 있는 값과 결과가 다른 것 같습니다.
> http://www.seoulmetro.co.kr/kr/page.do?menuIdx=354

미션 페이지에 있는 식을 사용해서 추가 요금을 계산해보시면 좋겠습니다. :)
> https://edu.nextstep.camp/s/urD8knTu/ls/4r6iyUcO
@@ -0,0 +1,44 @@
+package nextstep.subway.fare.domain;
+
+import java.util.Objects;
+
+public class Fare {
+    private static final int basicFare = 1250;
+
+    private int fare;
+    private final DiscountPolicy discountPolicy;
+
+    private Fare(int fare, DiscountPolicy discountPolicy) {
+        this.fare = fare;
+        this.discountPolicy = discountPolicy;
+    }
+
+    public static Fare of(DiscountPolicy discountPolicy) {
+        return new Fare(Fare.basicFare, discountPolicy);
+    }
+
+    public static Fare of(int fare, DiscountPolicy discountPolicy) {
+        return new Fare(fare, discountPolicy);
+    }
+
+    public Fare plus(int fare) {
+        return Fare.of(this.fare + fare, this.discountPolicy);
+    }
+
+    public int discount() {
+        return this.discountPolicy.getDiscount(this.fare);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Fare fare1 = (Fare) o;
+        return fare == fare1.fare && discountPolicy == fare1.discountPolicy;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fare, discountPolicy);
+    }
+}

리뷰: 값 객체 사용 좋습니다. 👍 
고민해보시면 좋을 것 같은 부분은, `Fare` 클래스를 사용하는 사람이 `plus`, `discount` 순서를 잘 알고 호출해야 요금 계산이 정상적으로 될 것 같아요. 그리고, 할인 정책은 생성하는 시점에 받지만, 실제로는 추가 요금 계산이 끝난 후 사용이 되어야 하네요.

요금 계산은 `FareCalculator`가 담당하는 것으로 보이니, `DiscountPolicy`를 요금 계산 메서드에서 관리해보셔도 좋겠습니다. :)
@@ -17,12 +17,11 @@
 import java.util.stream.Collectors;
 
 @Service
-@Transactional(readOnly = true)
+@Transactional
 public class FavoriteService {
 
     private final String MESSAGE_FAVORITE_ENTITY_NOT_FOUND = "즐겨찾기가가 존재하지 않습니다";
 
-
     private FavoriteRepository favoriteRepository;
     private MemberService memberService;
     private StationService stationService;
@@ -42,7 +41,7 @@ public FavoriteResponse saveFavorite(Long memberId, FavoriteRequest favoriteRequ
         Long sourceStationId = favoriteRequest.getSourceId();
         Long targetStationId = favoriteRequest.getTargetId();
 
-        checkSourceTargetHasPath(sourceStationId, targetStationId);
+        checkSourceTargetHasPath(memberId, sourceStationId, targetStationId);
 
         Favorite favorite = saveFavoriteEntity(memberId, sourceStationId, targetStationId);
         return FavoriteResponse.of(favorite);
@@ -55,10 +54,11 @@ private Favorite saveFavoriteEntity(Long memberId, Long sourceStationId, Long ta
         return favoriteRepository.save(new Favorite(member, sourceStation, targetStation));
     }
 
-    private void checkSourceTargetHasPath(Long sourceStationId, Long targetStationId) {
-        pathService.findShortestPath(sourceStationId, targetStationId);
+    private void checkSourceTargetHasPath(Long memberId, Long sourceStationId, Long targetStationId) {
+        pathService.findShortestPath(memberId, sourceStationId, targetStationId);
     }
 
+    @Transactional(readOnly = true)
     public List<FavoriteResponse> findFavorites(Long memberId) {
         Member member = findMember(memberId);
         List<Favorite> favorites = favoriteRepository.findByMemberId(member.getId());

리뷰: 최단 경로를 조회하는 메서드가 요금 계산도 하기 때문에 `memberId`가 필요하게 된 걸까요?
경로 조회와 요금 계산을 분리해봐도 좋을 것 같네요! :)
@@ -1,14 +1,16 @@
 package nextstep.subway.path.application;
 
 import nextstep.subway.line.domain.Section;
+import nextstep.subway.path.domain.SectionEdge;
+import nextstep.subway.path.domain.ShortestPath;
 import nextstep.subway.station.domain.Station;
-import org.jgrapht.graph.DefaultWeightedEdge;
+import org.jgrapht.GraphPath;
 import org.jgrapht.graph.WeightedMultigraph;
 
 import java.util.List;
 
 public class PathFinder {
-    private final WeightedMultigraph<Station, DefaultWeightedEdge> graph;
+    private final WeightedMultigraph<Station, SectionEdge> graph;
     private PathFindAlgorithm algorithm;
 
     private PathFinder(List<Section> allSections, PathFindAlgorithm algorithm) {
@@ -17,13 +19,13 @@ private PathFinder(List<Section> allSections, PathFindAlgorithm algorithm) {
     }
 
 
-    private WeightedMultigraph<Station, DefaultWeightedEdge> makeGraph(final List<Section> allSections) {
-        WeightedMultigraph<Station, DefaultWeightedEdge> graph = new WeightedMultigraph<>(DefaultWeightedEdge.class);
+    private WeightedMultigraph<Station, SectionEdge> makeGraph(final List<Section> allSections) {
+        WeightedMultigraph<Station, SectionEdge> graph = new WeightedMultigraph<>(SectionEdge.class);
         allSections.forEach(
             section -> {
                 graph.addVertex(section.getUpStation());
                 graph.addVertex(section.getDownStation());
-                graph.setEdgeWeight(graph.addEdge(section.getUpStation(), section.getDownStation()), section.getDistance());
+                graph.addEdge(section.getUpStation(), section.getDownStation(), SectionEdge.of(section));
             }
         );
         return graph;
@@ -34,6 +36,11 @@ public static PathFinder of(List<Section> allSections, PathFindAlgorithm algorit
     }
 
     public List<Station> find(Station departStation, Station destStation) {
-        return algorithm.findShortestPath(graph, departStation, destStation);
+        return algorithm.findShortestPathStations(graph, departStation, destStation);
+    }
+
+    public ShortestPath getShortestGraph(Station departStation, Station destStation) {
+        GraphPath<Station, SectionEdge> shortestPathGraph = algorithm.getShortestPathGraph(graph, departStation, destStation);
+        return ShortestPath.of(shortestPathGraph);
     }
 }

리뷰: 사용처가 없어진 것 같으니, 이 메서드를 포함해 `PathFindAlgorithm`의 `findShortestPathStations`를 삭제하시면 좋겠습니다. :)
@@ -1,8 +1,12 @@
 package nextstep.subway.path.application;
 
+import nextstep.subway.fare.domain.FareCalculator;
 import nextstep.subway.line.domain.Section;
 import nextstep.subway.line.domain.SectionRepository;
 import nextstep.subway.line.dto.PathResponse;
+import nextstep.subway.member.application.MemberService;
+import nextstep.subway.member.domain.Member;
+import nextstep.subway.path.domain.ShortestPath;
 import nextstep.subway.station.application.StationService;
 import nextstep.subway.station.domain.Station;
 import org.springframework.stereotype.Service;
@@ -20,22 +24,30 @@ public class PathService {
 
     private final StationService stationService;
     private final SectionRepository sectionRepository;
-
     private final PathFindAlgorithm pathFinder;
+    private final MemberService memberService;
 
 
-    public PathService(StationService stationService, SectionRepository sectionRepository, PathFindAlgorithm pathFinder) {
+    public PathService(StationService stationService, SectionRepository sectionRepository, PathFindAlgorithm pathFinder,
+                       MemberService memberService) {
         this.stationService = stationService;
         this.sectionRepository = sectionRepository;
         this.pathFinder = pathFinder;
+        this.memberService = memberService;
     }
 
-    public PathResponse findShortestPath(Long source, Long target) {
+    public PathResponse findShortestPath(Long loginId, Long source, Long target) {
         validateInput(source, target);
         Station departStation = stationService.findStationById(source);
         Station destStation = stationService.findStationById(target);
         List<Section> allSections = sectionRepository.findAll();
-        return PathResponse.from(PathFinder.of(allSections, pathFinder).find(departStation, destStation));
+        ShortestPath shortestGraph = PathFinder.of(allSections, this.pathFinder)
+            .getShortestGraph(departStation, destStation);
+
+        Member memberEntity = memberService.findMemberEntity(loginId);
+        int fare = FareCalculator.calculate(shortestGraph.getPathDistance(),
+            shortestGraph.getMaxLineFare(), memberEntity.getAge());
+        return PathResponse.from(shortestGraph, fare);
     }
 
     private static void validateInput(Long sourceStationId, Long targetStationId) {
@@ -46,5 +58,4 @@ private static void validateInput(Long sourceStationId, Long targetStationId) {
             throw new IllegalArgumentException(MESSAGE_SOURCE_TARGET_SHOULD_BE_DIFFERENT);
         }
     }
-
 }

리뷰: 경로 계산하는 부분과 요금 계산하는 로직을 잘 분리할 수 있게 구현하신 것으로 보여요.
실제로 담당하는 클래스를 나눠보면 어떨까요? `FavoriteService` 에서는 경로가 존재하는지만 확인하면 되니 재사용도 좀 더 효율적으로 할 수 있을 것 같네요!
@@ -1,14 +1,13 @@
 package nextstep.subway.path.ui;
 
+import nextstep.subway.auth.domain.AuthenticationPrincipal;
+import nextstep.subway.auth.domain.LoginMember;
 import nextstep.subway.line.dto.PathResponse;
-import nextstep.subway.member.dto.MemberRequest;
-import nextstep.subway.member.dto.MemberResponse;
 import nextstep.subway.path.application.PathService;
 import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.*;
-
-import java.net.URI;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
 @RestController
 public class PathController {
@@ -20,18 +19,10 @@ public PathController(PathService service) {
 
     @GetMapping("/paths")
     public ResponseEntity<PathResponse> findShortestPath(
-            @RequestParam("source") Long source,
-            @RequestParam("target") Long target) {
-        PathResponse response = service.findShortestPath(source, target);
+        @AuthenticationPrincipal LoginMember loginMember,
+        @RequestParam("source") Long source,
+        @RequestParam("target") Long target) {
+        PathResponse response = service.findShortestPath(loginMember.getId(), source, target);
         return ResponseEntity.ok(response);
     }
-
-    @ExceptionHandler({IllegalArgumentException.class})
-    public ResponseEntity handleIllegalArgsException(Exception e) {
-        return ResponseEntity.badRequest().build();
-    }
-    @ExceptionHandler({RuntimeException.class})
-    public ResponseEntity handleRuntimeException(Exception e) {
-        return ResponseEntity.badRequest().build();
-    }
 }

리뷰: 로그인하지 않은 경우에도 경로를 조회할 수 있어야 할 것 같아요.
이를 위해 AuthenticationPrincipal에 인증이 필요한지 여부 필드를 추가하고, 이를 통해 인증을 하지 않은 회원도 사용할 수 있는 기능(경로 조회 등)과 인증을 해야만 사용할 수 있는 기능(회원 정보 변경 등)에서 모두 LoginMember를 이용할 수 있도록 관리해보시면 좋겠습니다.

> https://edu.nextstep.camp/s/urD8knTu/ls/4r6iyUcO
"/paths 요청 시 LoginMember 객체 처리" 부분을 참고 부탁드립니다.
@@ -0,0 +1,47 @@
+import { FollowUserType, followList } from '@/types/types';
+import axios from 'axios';
+import { SetterOrUpdater } from 'recoil';
+
+const PER_PAGE = 100;
+export const getFollowingList = async (setFollowings: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followingData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/following?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+
+    const data = followingData.data;
+
+    const followingList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowings(followingList);
+  } catch {
+    console.error('error');
+  }
+};
+
+export const getFollowerList = async (setFollowers: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followerData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/followers?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+    const data = followerData.data;
+
+    const followerList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowers(followerList);
+  } catch {
+    console.error('error');
+  }
+};

리뷰: 팔로잉이 100명이 넘으면 혹시 어떻게 되나요?
@@ -0,0 +1,47 @@
+import { FollowUserType, followList } from '@/types/types';
+import axios from 'axios';
+import { SetterOrUpdater } from 'recoil';
+
+const PER_PAGE = 100;
+export const getFollowingList = async (setFollowings: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followingData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/following?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+
+    const data = followingData.data;
+
+    const followingList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowings(followingList);
+  } catch {
+    console.error('error');
+  }
+};
+
+export const getFollowerList = async (setFollowers: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followerData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/followers?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+    const data = followerData.data;
+
+    const followerList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowers(followerList);
+  } catch {
+    console.error('error');
+  }
+};

리뷰: Github API 에서는 [API 버저닝을 위해 헤더에 API 버전 정보를 포함시켜 요청](https://docs.github.com/en/rest/about-the-rest-api/api-versions?apiVersion=2022-11-28)하는 것을 권장하고 있어요. 이를 추가해주는게 좋을 것 같아요!
@@ -0,0 +1,47 @@
+import { FollowUserType, followList } from '@/types/types';
+import axios from 'axios';
+import { SetterOrUpdater } from 'recoil';
+
+const PER_PAGE = 100;
+export const getFollowingList = async (setFollowings: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followingData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/following?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+
+    const data = followingData.data;
+
+    const followingList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowings(followingList);
+  } catch {
+    console.error('error');
+  }
+};
+
+export const getFollowerList = async (setFollowers: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followerData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/followers?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+    const data = followerData.data;
+
+    const followerList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowers(followerList);
+  } catch {
+    console.error('error');
+  }
+};

리뷰: 이런 방법도 있어요!
```suggestion
    const { data } = followingData;
```
@@ -0,0 +1,47 @@
+import { FollowUserType, followList } from '@/types/types';
+import axios from 'axios';
+import { SetterOrUpdater } from 'recoil';
+
+const PER_PAGE = 100;
+export const getFollowingList = async (setFollowings: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followingData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/following?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+
+    const data = followingData.data;
+
+    const followingList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowings(followingList);
+  } catch {
+    console.error('error');
+  }
+};
+
+export const getFollowerList = async (setFollowers: SetterOrUpdater<followList[]>, tokenValue: string) => {
+  try {
+    const followerData = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/user/followers?per_page=${PER_PAGE}`, {
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${tokenValue}`,
+      },
+    });
+    const data = followerData.data;
+
+    const followerList = data.map((user: FollowUserType) => {
+      const { id, login, avatar_url } = user;
+      return { id, login, avatar_url };
+    });
+
+    setFollowers(followerList);
+  } catch {
+    console.error('error');
+  }
+};

리뷰: try catch 문은 일반적으로 에러 상황에서 에러를 복구해 정상 동작을 시키거나, 로깅하는 용도로 사용돼요. 그런데 현재와 같이 특별한 목적이 있지 않는 try catch 문은 오히려 디버그할 때 엄청 헷갈리게 될 수 있어 권장하지 않아요.