ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,39 @@
+function formatUrl(config) {
+  const { href, query } = config;
+  const searchParams = new URLSearchParams(query);
+
+  return `${href}?${searchParams.toString()}`;
+}
+
+function formatHeaders(headersRecord) {
+  const headers = new Headers();
+
+  headers.append("content-type", "application/json");
+  Object.entries(headersRecord).forEach(([key, value]) => {
+    headers.append(key, value);
+  });
+  return headers;
+}
+
+export async function createAPIRequest(request) {
+  const response = await fetch(
+    formatUrl({
+      href: request.url,
+      query: request.query ?? {},
+    }),
+    {
+      method: request.method || "GET",
+      headers: formatHeaders(request.headers),
+      body: request.body,
+    },
+  ).catch((error) => {
+    throw new Error("Network error: ", error);
+  });
+
+  if (!response.ok) {
+    throw new Error("Http error: ", response.status);
+  }
+
+  const data = !response.body ? null : await response.json();
+  return data;
+}

리뷰: [MDN Error() 생성자](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Error/Error) 문서 첨부드립니다!
error 객체는 첫번째 인자로 `message`를 받으며, 두번째 세번째 인자가 있긴하지만, 비표준입니다!

실제로 에러를 일으켜보면 alert에서 `Http error: ` , `Network error: ` 
해당 문자열만 노출되는 걸 확인하실 수 있을거에요!
@@ -0,0 +1,39 @@
+function formatUrl(config) {
+  const { href, query } = config;
+  const searchParams = new URLSearchParams(query);
+
+  return `${href}?${searchParams.toString()}`;
+}
+
+function formatHeaders(headersRecord) {
+  const headers = new Headers();
+
+  headers.append("content-type", "application/json");
+  Object.entries(headersRecord).forEach(([key, value]) => {
+    headers.append(key, value);
+  });
+  return headers;
+}
+
+export async function createAPIRequest(request) {
+  const response = await fetch(
+    formatUrl({
+      href: request.url,
+      query: request.query ?? {},
+    }),
+    {
+      method: request.method || "GET",
+      headers: formatHeaders(request.headers),
+      body: request.body,
+    },
+  ).catch((error) => {
+    throw new Error("Network error: ", error);
+  });
+
+  if (!response.ok) {
+    throw new Error("Http error: ", response.status);
+  }
+
+  const data = !response.body ? null : await response.json();
+  return data;
+}

리뷰: 이 파일에 대한 전반적인 느낌은 `formatUrl`, `formatHeaders` 함수의 필요성을 모르겠다.. 이긴 합니다!
`createAPIRequest` 처럼 전체적인 요청에 대한 공통 함수는 있으면 당연히 좋다! 
이긴한데 형태에 대해서는 조금 아쉬운 부분이 있다고 느껴집니다!

코멘트로 하나 하나 작성해볼게요!
@@ -0,0 +1,39 @@
+function formatUrl(config) {
+  const { href, query } = config;
+  const searchParams = new URLSearchParams(query);
+
+  return `${href}?${searchParams.toString()}`;
+}
+
+function formatHeaders(headersRecord) {
+  const headers = new Headers();
+
+  headers.append("content-type", "application/json");
+  Object.entries(headersRecord).forEach(([key, value]) => {
+    headers.append(key, value);
+  });
+  return headers;
+}
+
+export async function createAPIRequest(request) {
+  const response = await fetch(
+    formatUrl({
+      href: request.url,
+      query: request.query ?? {},
+    }),
+    {
+      method: request.method || "GET",
+      headers: formatHeaders(request.headers),
+      body: request.body,
+    },
+  ).catch((error) => {
+    throw new Error("Network error: ", error);
+  });
+
+  if (!response.ok) {
+    throw new Error("Http error: ", response.status);
+  }
+
+  const data = !response.body ? null : await response.json();
+  return data;
+}

리뷰: `headers.append("content-type", "application/json")`;
사실상 역할은 모든 요청에 해당 헤더를 더해주는 역할 밖에 없는데
`createAPIRequest`의 headers에서 해주면 되지 않을까 싶네요!

```js
headers: {
   ...request.headers
   'content-type': "application/json"
}
```
@@ -0,0 +1,39 @@
+function formatUrl(config) {
+  const { href, query } = config;
+  const searchParams = new URLSearchParams(query);
+
+  return `${href}?${searchParams.toString()}`;
+}
+
+function formatHeaders(headersRecord) {
+  const headers = new Headers();
+
+  headers.append("content-type", "application/json");
+  Object.entries(headersRecord).forEach(([key, value]) => {
+    headers.append(key, value);
+  });
+  return headers;
+}
+
+export async function createAPIRequest(request) {
+  const response = await fetch(
+    formatUrl({
+      href: request.url,
+      query: request.query ?? {},
+    }),
+    {
+      method: request.method || "GET",
+      headers: formatHeaders(request.headers),
+      body: request.body,
+    },
+  ).catch((error) => {
+    throw new Error("Network error: ", error);
+  });
+
+  if (!response.ok) {
+    throw new Error("Http error: ", response.status);
+  }
+
+  const data = !response.body ? null : await response.json();
+  return data;
+}

리뷰: 가장 먼저 searchParams의 여부와 관계 없이 URL에 `?`가 붙습니다!
그리고 `createAPIRequest`보다 실제 요청하는 곳에서 사용하는 것이 어떨까 싶긴합니다!

어떤 의미냐면요!
fetch API를 사용하는 것을 드러내고, 사용처에서 이를 알아볼 수 있으면 더 좋겠다는 생각입니다. 
`fetchPopularMovies`는 fetch API에 직접 사용할 수 없는 형태의 `createAPIRequest`에 알맞은 인자를 넘겨줍니다.
`createAPIRequest`는 이 인자를 가공해서 fetch API에 사용할 수 있는 형태로 변경을 하고 있는데요!

이 함수를 사용하는 입장에서 `createAPIRequest`의 동작과 파라미터를 모르더라도
fetch API의 request 정보에 맞는 인자를 넘겨주면, 필요한 추가적인 처리를 해서 요청을 해준다! 가 될 수 있을 것 같아요~

그런 의미에서, fetch는 첫번째 인자로 URL을 받으니, createAPIRequest에서 사용된다면,
createAPIRequest는 인자로 fetch의 request 정보와 동일한 형태의 인자를 받을 수 없게 되고, 사용하는 입장에서 추가적인 코드 학습이 필요하게될 것 같아요~
@@ -0,0 +1,39 @@
+function formatUrl(config) {
+  const { href, query } = config;
+  const searchParams = new URLSearchParams(query);
+
+  return `${href}?${searchParams.toString()}`;
+}
+
+function formatHeaders(headersRecord) {
+  const headers = new Headers();
+
+  headers.append("content-type", "application/json");
+  Object.entries(headersRecord).forEach(([key, value]) => {
+    headers.append(key, value);
+  });
+  return headers;
+}
+
+export async function createAPIRequest(request) {
+  const response = await fetch(
+    formatUrl({
+      href: request.url,
+      query: request.query ?? {},
+    }),
+    {
+      method: request.method || "GET",
+      headers: formatHeaders(request.headers),
+      body: request.body,
+    },
+  ).catch((error) => {
+    throw new Error("Network error: ", error);
+  });
+
+  if (!response.ok) {
+    throw new Error("Http error: ", response.status);
+  }
+
+  const data = !response.body ? null : await response.json();
+  return data;
+}

리뷰: 코멘트가 많이 길어졌네요.. ㅎㅎㅎㅎ 
요약하자면, 인터페이스를 동료가 따로 뜯어보지 않아도 바로 이해할 수 있는 형태로 작성하자..! 입니다
@@ -0,0 +1,39 @@
+function formatUrl(config) {
+  const { href, query } = config;
+  const searchParams = new URLSearchParams(query);
+
+  return `${href}?${searchParams.toString()}`;
+}
+
+function formatHeaders(headersRecord) {
+  const headers = new Headers();
+
+  headers.append("content-type", "application/json");
+  Object.entries(headersRecord).forEach(([key, value]) => {
+    headers.append(key, value);
+  });
+  return headers;
+}
+
+export async function createAPIRequest(request) {
+  const response = await fetch(
+    formatUrl({
+      href: request.url,
+      query: request.query ?? {},
+    }),
+    {
+      method: request.method || "GET",
+      headers: formatHeaders(request.headers),
+      body: request.body,
+    },
+  ).catch((error) => {
+    throw new Error("Network error: ", error);
+  });
+
+  if (!response.ok) {
+    throw new Error("Http error: ", response.status);
+  }
+
+  const data = !response.body ? null : await response.json();
+  return data;
+}

리뷰: `data` 변수를 생성할 필요할 없이 바로 결과값을 반환하면 될 것 같네요~
@@ -0,0 +1,37 @@
+import "./components/layout/app-header.js";
+import "./components/movie/movie-card.js";
+import "./components/movie/movie-list.js";
+import "./components/movie/movie-container.js";
+
+import { html } from "./utils/template.js";
+
+class App extends HTMLElement {
+  constructor() {
+    super();
+    this.attachShadow({ mode: "open" });
+  }
+
+  connectedCallback() {
+    this.render();
+  }
+
+  render() {
+    this.shadowRoot.innerHTML = this.template();
+  }
+
+  template() {
+    return html`
+      <style>
+        * {
+          padding-bottom: 48px;
+        }
+      </style>
+      <app-header></app-header>
+      <main>
+        <movie-container></movie-container>
+      </main>
+    `;
+  }
+}
+
+customElements.define("movie-app", App);

리뷰: 제거 해도 괜찮은 스타일 같은데 어떤 의미가 있을까요?
@@ -0,0 +1,58 @@
+import endedPopularMovies from "../fixtures/ended-popular-movies.json";
+
+describe("영화 목록", () => {
+  beforeEach(() => {
+    const url = "http://localhost:8080";
+    cy.visit(url);
+
+    cy.get("movie-app")
+      .as("movieApp")
+      .shadow()
+      .find("movie-container")
+      .as("movieContainer")
+      .shadow()
+      .find("movie-list")
+      .as("movieList")
+      .shadow()
+      .find("movie-card")
+      .as("movieCard");
+  });
+
+  it("최초 로드시 20개의 영화 카드를 보여준다.", () => {
+    cy.get("@movieCard").should("have.length", 20);
+  });
+
+  it("더보기 버튼 클릭시 20개의 영화를 추가로 보여준다.", () => {
+    cy.get("@movieContainer").shadow().find(".show-more").as("showMoreButton");
+
+    cy.get("@movieCard").should("have.length", 20);
+    cy.get("@showMoreButton").contains("더보기").click();
+    cy.get("@movieCard").should("have.length", 40);
+  });
+
+  it("로딩중일 때는 스켈레톤 카드를 보여준다.", () => {
+    cy.intercept("GET", "**/popular*", (req) => {
+      req.on("response", (res) => {
+        res.setDelay(1000); // Delay the response to simulate loading
+      });
+    }).as("getPopularMoviesDelayed");
+
+    cy.reload();
+    cy.get("@movieCard").shadow().find(".skeleton").should("exist");
+    cy.wait("@getPopularMoviesDelayed");
+
+    cy.get("@movieCard").should("have.length", 20);
+  });
+
+  it("영화 목록을 더이상 불러올 수 없으면 '더보기' 버튼이 사라진다.", () => {
+    cy.intercept("GET", "**/popular*", endedPopularMovies).as(
+      "getEndedPopularMovies",
+    );
+    cy.get("@movieContainer").shadow().find(".show-more").as("showMoreButton");
+    cy.get("@showMoreButton").should("exist");
+    cy.get("@showMoreButton").click();
+
+    cy.wait("@getEndedPopularMovies");
+    cy.get("@movieContainer").shadow().find("show-more").should("not.exist");
+  });
+});

리뷰: 테스트가 모두 해피케이스를 가정하고 작성된 것 같아요!
네트워크 에러나 HTTP 에러에 관한 상황에 적절하게 작성된 코드대로 동작하는지도 확인하면 좋을 것 같네요!
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="ko">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+    <title>영화관</title>
+  <script defer src="./bundle.js"></script></head>
+  <body>
+    <div id="app"></div>
+  </body>
+</html>

리뷰: 아마 배포 때문에 dist 폴더를 이렇게 올려주신 것 같네요 ㅎㅎ

gh-pages 브랜치를 이용하면 어떨까 싶어요!
@@ -0,0 +1,20 @@
+import { createComponent } from "../createComponent";
+import { Header } from "./Header";
+import { Main } from "./Main";
+
+export async function App() {
+  const header = await createComponent(Header);
+  const main = await createComponent(Main);
+
+  const bindEvents = () => {
+    main.bindEvents();
+  };
+
+  return {
+    element: `
+        ${header.element}
+        ${main.element}
+    `,
+    bindEvents,
+  };
+}

리뷰: 이제보니 header랑 main 때문이군요 ㅎㅎ
@@ -0,0 +1,13 @@
+export let currentComponent = null;
+
+export async function createComponent(component, props) {
+  const previousComponent = currentComponent;
+
+  currentComponent = { name: component.name, stateIndex: 0 };
+
+  const componentInstance = await component(props);
+
+  currentComponent = previousComponent;
+
+  return componentInstance;
+}

리뷰: 여기서 previousComponent가 하는 역할은 무엇일까요?
@@ -0,0 +1,55 @@
+import { createComponent, currentComponent } from "../createComponent";
+import { fetchMovies } from "../fetch";
+import { useState } from "../state";
+import { MovieCard } from "./MovieCard";
+
+export async function MovieList() {
+  const [page, setPage] = useState(1);
+
+  const movieData = await fetchMovies({ page });
+
+  const [movies, setMovies] = useState([...movieData.results]);
+  const [totalPage] = useState(movieData.total_page);
+
+  const movieCards = await Promise.all(
+    movies.map((movie, index) =>
+      createComponent(MovieCard, {
+        index,
+        title: movie.title,
+        score: movie.vote_average,
+        thumbnailUrl: `https://image.tmdb.org/t/p/w200/${movie.poster_path}`,
+      })
+    )
+  );
+
+  const handleMoreButton = async () => {
+    if (page >= totalPage) {
+      return;
+    }
+
+    await setPage(page + 1);
+
+    const movieData = await fetchMovies({ page: page + 1 });
+    const newMovies = [...movies, ...movieData.results];
+
+    await setMovies(newMovies);
+  };
+
+  const bindEvents = () => {
+    movieCards.forEach((movieCard) => movieCard.bindEvents());
+
+    const $moreButton = document.querySelector("#more-button");
+
+    $moreButton.addEventListener("click", handleMoreButton);
+  };
+
+  return {
+    element: /* html */ `
+        <ul class="item-list">
+            ${movieCards.map((movieCard) => movieCard.element).join("")}
+        </ul>
+        <button id="more-button" class="btn primary full-width">더 보기</button>
+    `,
+    bindEvents,
+  };
+}

리뷰: 만약 한 페이지에 카테고리별로 여러 개의 영화목록을 띄워준다고 했을 때,
이 컴포넌트를 재활용할 수 있을까요?
@@ -0,0 +1,55 @@
+import { createComponent, currentComponent } from "../createComponent";
+import { fetchMovies } from "../fetch";
+import { useState } from "../state";
+import { MovieCard } from "./MovieCard";
+
+export async function MovieList() {
+  const [page, setPage] = useState(1);
+
+  const movieData = await fetchMovies({ page });
+
+  const [movies, setMovies] = useState([...movieData.results]);
+  const [totalPage] = useState(movieData.total_page);
+
+  const movieCards = await Promise.all(
+    movies.map((movie, index) =>
+      createComponent(MovieCard, {
+        index,
+        title: movie.title,
+        score: movie.vote_average,
+        thumbnailUrl: `https://image.tmdb.org/t/p/w200/${movie.poster_path}`,
+      })
+    )
+  );
+
+  const handleMoreButton = async () => {
+    if (page >= totalPage) {
+      return;
+    }
+
+    await setPage(page + 1);
+
+    const movieData = await fetchMovies({ page: page + 1 });
+    const newMovies = [...movies, ...movieData.results];
+
+    await setMovies(newMovies);
+  };
+
+  const bindEvents = () => {
+    movieCards.forEach((movieCard) => movieCard.bindEvents());
+
+    const $moreButton = document.querySelector("#more-button");
+
+    $moreButton.addEventListener("click", handleMoreButton);
+  };
+
+  return {
+    element: /* html */ `
+        <ul class="item-list">
+            ${movieCards.map((movieCard) => movieCard.element).join("")}
+        </ul>
+        <button id="more-button" class="btn primary full-width">더 보기</button>
+    `,
+    bindEvents,
+  };
+}

리뷰: 모든 구간이 다 async/await 으로 만들어진 이유가 여기서 출발이 되네요.
이 부분만 개선하면 async/awiat 을 전체적으로 제거할수 있지 않을까요?
@@ -0,0 +1,55 @@
+import { createComponent, currentComponent } from "../createComponent";
+import { fetchMovies } from "../fetch";
+import { useState } from "../state";
+import { MovieCard } from "./MovieCard";
+
+export async function MovieList() {
+  const [page, setPage] = useState(1);
+
+  const movieData = await fetchMovies({ page });
+
+  const [movies, setMovies] = useState([...movieData.results]);
+  const [totalPage] = useState(movieData.total_page);
+
+  const movieCards = await Promise.all(
+    movies.map((movie, index) =>
+      createComponent(MovieCard, {
+        index,
+        title: movie.title,
+        score: movie.vote_average,
+        thumbnailUrl: `https://image.tmdb.org/t/p/w200/${movie.poster_path}`,
+      })
+    )
+  );
+
+  const handleMoreButton = async () => {
+    if (page >= totalPage) {
+      return;
+    }
+
+    await setPage(page + 1);
+
+    const movieData = await fetchMovies({ page: page + 1 });
+    const newMovies = [...movies, ...movieData.results];
+
+    await setMovies(newMovies);
+  };
+
+  const bindEvents = () => {
+    movieCards.forEach((movieCard) => movieCard.bindEvents());
+
+    const $moreButton = document.querySelector("#more-button");
+
+    $moreButton.addEventListener("click", handleMoreButton);
+  };
+
+  return {
+    element: /* html */ `
+        <ul class="item-list">
+            ${movieCards.map((movieCard) => movieCard.element).join("")}
+        </ul>
+        <button id="more-button" class="btn primary full-width">더 보기</button>
+    `,
+    bindEvents,
+  };
+}

리뷰: movieData를 props로 받아오면 어떨까요?
@@ -0,0 +1,13 @@
+export async function fetchMovies({ page }) {
+  const baseUrl = "https://api.themoviedb.org/3/movie/popular";
+  const param = new URLSearchParams({
+    page,
+    language: "ko-KR",
+  });
+
+  const response = await fetch(`${baseUrl}?${param}`, {
+    headers: { authorization: `Bearer ${process.env.API_KEY}` },
+  });
+
+  return await response.json();
+}

리뷰: 사실 배포된 사이트의 네트워크 요청을 까보면 API_KEY 가 그대로 노출되는걸 확인할 수 있답니다 ㅎㅎ
이건 어떻게 방지할 수 있을까요?
@@ -0,0 +1,13 @@
+export async function fetchMovies({ page }) {
+  const baseUrl = "https://api.themoviedb.org/3/movie/popular";
+  const param = new URLSearchParams({
+    page,
+    language: "ko-KR",
+  });
+
+  const response = await fetch(`${baseUrl}?${param}`, {
+    headers: { authorization: `Bearer ${process.env.API_KEY}` },
+  });
+
+  return await response.json();
+}

리뷰: 여기서 await을 사용하는 것과 사용하지 않는게 어떤 차이가 있는지 혹시 아실까요~?
@@ -1,14 +1,40 @@
-import React from "react";
-import "./PostList.scss";
+import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import Posts from './components/Posts';
+import './PostList.scss';
 
 const PostList = () => {
+  const [postData, setPostData] = useState([]);
 
-return(
-  <div className="postList">
-    <div>로그인</div>
-  </div>
-);
+  const navigate = useNavigate();
+  const goToWrite = () => {
+    navigate('/post-add');
+  };
 
+  useEffect(() => {
+    fetch('/data/postList.json', {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json;charset=utf-8',
+      },
+    })
+      .then(res => res.json())
+      .then(result => {
+        setPostData(result.data);
+      });
+  }, []);
+
+  if (postData.length === 0) return null;
+  return (
+    <div className="postList">
+      <div className="posts">
+        <Posts postData={postData} />
+      </div>
+      <div className="buttonPlace">
+        <button onClick={goToWrite}>글쓰기</button>
+      </div>
+    </div>
+  );
 };
 
-export default PostList;
\ No newline at end of file
+export default PostList;

리뷰: import 순서에도 유지보수 및 가독성을 위한 컨벤션이 있습니다.
위코드의 컨벤션은 간략하게 아래와 같고, 참고해서 수정해 주세요!

- 라이브러리
    - React 관련 패키지
    - 외부 라이브러리
- 컴포넌트
    - 공통 컴포넌트 → 먼 컴포넌트 → 가까운 컴포넌트
- 함수, 변수 및 설정 파일
- 사진 등 미디어 파일(`.png`)
- css 파일 (.`scss`)
@@ -0,0 +1,63 @@
+.postList {
+  margin: 0 auto;
+  display: flex;
+  width: 576px;
+  height: 80vh;
+  padding: 40px 24px;
+  flex-direction: column;
+  align-items: center;
+  border-radius: 16px;
+  border: 1px solid #e5e5e5;
+  background: white;
+
+  .posts {
+    width: 100%;
+    overflow-y: scroll;
+    flex: 1;
+    cursor: pointer;
+  }
+  & ::-webkit-scrollbar {
+    display: none;
+  }
+
+  .buttonPlace {
+    display: flex;
+    flex-direction: column;
+    width: 528px;
+    height: 56px;
+    margin-top: 28px;
+    align-items: flex-end;
+    gap: 8px;
+    align-self: stretch;
+
+    a {
+      text-decoration: none;
+    }
+
+    button {
+      display: flex;
+      width: 120px;
+      height: 56px;
+      padding: 0px 34px;
+      justify-content: center;
+      align-items: center;
+      border-radius: 6px;
+      background-color: #2d71f7;
+      color: #fff;
+      text-align: center;
+      font-feature-settings:
+        'clig' off,
+        'liga' off;
+      font-family: Apple SD Gothic Neo;
+      font-size: 16px;
+      font-style: normal;
+      font-weight: 800;
+      line-height: normal;
+
+      &:hover {
+        border-radius: 6px;
+        background: #083e7f;
+      }
+    }
+  }
+}

리뷰: 부모 선택자(`&`)를 사용해서 해당 요소에만 적용되는 스타일임을 좀 더 깔끔하게 명시할 수 있습니다!
```suggestion
  .posts {
    overflow-y: scroll;
    flex: 1;
    
    &::-webkit-scrollbar {
      display: none;
    }
  }
```
@@ -1,7 +1,18 @@
 @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap');
 
-
 * {
   box-sizing: border-box;
   font-family: 'Noto Sans KR', sans-serif;
-}
\ No newline at end of file
+}
+
+body {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+button {
+  cursor: pointer;
+}

리뷰: 공용 파일을 수정해서 올려주셨는데, 공용 파일의 수정은
1. 팀원과의 충분한 상의 후에
2. 해당 파일만 수정하는 내용의 PR

을 올려주셔야 합니다!
@@ -0,0 +1,13 @@
+package msa.customer.exception;
+
+public class DeliveryCustomerException extends RuntimeException {
+
+    public DeliveryCustomerException(String message) {
+        super(message);
+    }
+
+    public DeliveryCustomerException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}

리뷰: (C) Exception은 서비스의 `정책 위반`을 나타내는 documentation의 역할을 하기도 합니다.
따라서 Exception을 통해 서비스 정책을 위반하는 경우, 정책을 명확하게 나타낼 수 있는 자체 exception을 정의하곤 하는데 delivery application에 적용해볼만한 exception 몇 개를 간략하게 구성해봤으니 참고해보시고 Exception들을 잘 정의해주시면 좋을 것 같습니다 :)
@@ -3,6 +3,7 @@
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import msa.customer.entity.store.Store;
+import msa.customer.exception.store.StoreEmptyException;
 import msa.customer.service.store.StoreService;
 import org.springframework.web.servlet.HandlerInterceptor;
 import org.springframework.web.servlet.HandlerMapping;
@@ -11,6 +12,7 @@
 import java.util.Optional;
 
 public class StoreCheckInterceptor implements HandlerInterceptor {
+
     private final StoreService storeService;
 
     public StoreCheckInterceptor(StoreService storeService) {
@@ -19,13 +21,14 @@ public StoreCheckInterceptor(StoreService storeService) {
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-        Map pathVariables  = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
-        String storeId = (String) pathVariables.get("storeId");
+        Map pathVariables = (Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+        String storeId = (String)pathVariables.get("storeId");
         Optional<Store> storeOptional = storeService.getStore(storeId);
-        if(storeOptional.isEmpty()){
-            throw new NullPointerException("Store doesn't exist. " + storeId + " is not correct store id.");
+        if (storeOptional.isEmpty()) {
+            throw new StoreEmptyException(storeId);
         }
         request.setAttribute("storeEntity", storeOptional.get());
         return true;
     }
+
 }

리뷰: (C) optional은 벗겨진 상태로 오는게 좋겠지만, 일단 현재 상태를 가정하고 custom exception을 throw 했습니다