Skip to content

[REFACTOR] 컴포넌트 구조 정리 - #221

Merged
shooukie merged 10 commits into
developfrom
refactor/components/#220
Mar 24, 2026
Merged

[REFACTOR] 컴포넌트 구조 정리#221
shooukie merged 10 commits into
developfrom
refactor/components/#220

Conversation

@shooukie

@shooukie shooukie commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

PR 제목

[REFACTOR] 컴포넌트 구조 정리

PR을 한 이유

그동안 불필요하게 컴포넌트를 분리해두어 파일 이동이 잦았던 문제가 있었습니다.
따라서 가독성을 높이기 위해 불필요하게 분리된 컴포넌트들을 정리했습니다.

#️⃣연관된 이슈

#220

📝작업 내용

컴포넌트 정리

  • TodayMeal/WeekMeal/ReviewModal
    -> meal-plan-page.tsx 안에서 로컬 함수로 변경
  • ListHeader
    -> meal-plan-page.tsx에 내부 인라인 처리
  • ChooseWeek/MealPlanResult
    -> meal-plan-create-page.tsx 안에서 로컬 함수로 변경
  • BottomSheet
    -> meal-plan-create-page.tsx 안에서 로컬 함수로 변경
  • Footer
    -> mobile-layout.tsx에 내부 인라인 처리

그 외 컴포넌트

  • 기존에 components/meal-plan 폴더 내에 있던 MenuChip, CalendarChipS, CalendarChipM을 meal-plan폴더 대신 chip 폴더를 만들어 정리했습니다
  • components/meal-plan/DateMenuList.tsx의 경우 components/mypage/DateRangeBlock.tsx와 중복되는 부분이 있는 걸 확인해서 일단 인라인 처리 하지 않고 후순위로 두 컴포넌트를 하나의 공용컴포넌트로 합친 후 components/meal-plan 폴더는 삭제하려고 합니다.

스크린샷 (선택)

image

💬리뷰 요구사항(선택)

혹시 이 외에도 식단 과 관련해서 정리하면 좋을 것 같은 컴포넌트가 있다면 알려주세요!
코드 내부의 로직 리팩토링은 이슈를 새로 파서 하려고 합니다.
코드 내에서 Button 컴포넌트를 많이 사용하여 충돌 가능성이 있어 #218 머지 후에 PR 머지할 예정입니다!

Summary by CodeRabbit

릴리스 노트

  • 리팩토링
    • 식단 관리 UI 구조를 개선하여 코드 유지보수성을 향상했습니다.
    • 식단 생성 및 조회 워크플로우를 재구성했습니다.
    • 식사 리뷰 기능을 통합하여 더욱 효율적으로 개선했습니다.

@shooukie shooukie linked an issue Mar 5, 2026 that may be closed by this pull request
@vercel

vercel Bot commented Mar 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
urisik-frontend Ready Ready Preview, Comment Mar 5, 2026 2:40pm

@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

여러 공유 컴포넌트(Footer, BottomSheet, ChooseWeek 등 9개)를 삭제하고, 해당 로직과 UI를 각 페이지 파일로 직접 이동하여 통합하는 리팩토링. 모듈식 아키텍처에서 페이지 기반 자체 포함 구조로 전환.

Changes

Cohort / File(s) Summary
삭제된 공통 및 식단 계획 컴포넌트
src/components/common/Footer.tsx, src/components/meal-plan/BottomSheet.tsx, src/components/meal-plan/ChooseWeek.tsx, src/components/meal-plan/ListHeader.tsx, src/components/meal-plan/MealPlanResult.tsx, src/components/meal-plan/MemberMealPlanView.tsx, src/components/meal-plan/ReviewModal.tsx, src/components/meal-plan/TodayMeal.tsx, src/components/meal-plan/WeekMeal.tsx
9개의 공유 컴포넌트 파일 완전 삭제(총 852줄 제거). Footer 네비게이션, 주간 선택, 식단 결과 표시, 리뷰 모달, 오늘/주간 식단 렌더링 등의 기능 포함.
레이아웃 인라인 변경
src/layouts/mobile-layout.tsx
Footer 컴포넌트 참조를 제거하고 인라인 footer UI로 대체. 홈, 식단표, 식단 짜기 버튼 및 배경 이미지 직접 구현 (+49줄).
식단 생성 페이지 통합
src/pages/meal-plan/meal-plan-create-page.tsx
ChooseWeek, MemberMealPlanView, MealPlanResult 컴포넌트 로직을 파일 내 구현으로 통합. 2단계 워크플로우(생성 → 결과) 추가, SlotRequest 변환 로직, 확인 버튼 플로우 포함 (+215줄).
식단 편집 페이지 통합
src/pages/meal-plan/meal-plan-edit-page.tsx
BottomSheet 컴포넌트를 페이지 내부에 직접 구현. 탭 관리, 무한 스크롤 패칭, 메뉴 변경 모달 플로우, 데이터 선택 로직 포함 (+228줄).
식단 페이지 탭 리팩토링
src/pages/meal-plan/meal-plan-page.tsx
TodayMeal, WeekMeal, ReviewModal 컴포넌트를 TodayMealTab, WeekMealTab, 인라인 ReviewModal로 변환. 탭 기반 렌더링, 별점 평가, 취향 선택, 이번주/다음주 주간 식단 표시 로직 추가 (+316줄).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 아홉 개의 컴포넌트를 한 곳에 모았네, 공유에서 자체포함으로 쌩쌩 달려가네! 레이아웃 인라인, 페이지 통합, 로직 응축 완벽하게, 모듈에서 페이지로 우아하게 이동했네! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 컴포넌트 구조 정리를 명확하게 요약하고 있으며, 전체 체인지셋과 일치합니다.
Description check ✅ Passed PR 설명은 템플릿의 모든 필수 섹션(제목, 이유, 연관 이슈, 작업 내용)을 포함하고 있으며 상세한 내용이 작성되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/components/#220

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/pages/meal-plan/meal-plan-create-page.tsx (4)

41-42: 🛠️ Refactor suggestion | 🟠 Major

상수 네이밍을 UPPER_SNAKE_CASE로 통일해 주세요.

Line 41의 regenerate, Line 167의 week, Line 238의 dayKor는 상수 규칙과 불일치합니다.

수정 예시
-const regenerate = true;
+const REGENERATE = true;
@@
-const week: { label: string; value: DayOfWeek }[] = [
+const WEEK_DAYS: { label: string; value: DayOfWeek }[] = [
@@
-const dayKor: Record<string, string> = {
+const DAY_KOR: Record<string, string> = {
-      regenerate: regenerate,
+      regenerate: REGENERATE,
@@
-        {week.map((day, idx) => (
+        {WEEK_DAYS.map((day, idx) => (
@@
-          const date = dayKor[day] ?? day;
+          const date = DAY_KOR[day] ?? day;

As per coding guidelines Constants must be UPPER_SNAKE_CASE.

Also applies to: 167-175, 238-246

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-create-page.tsx` around lines 41 - 42, Rename
the lowercase constants to UPPER_SNAKE_CASE and update all their usages: change
regenerate -> REGENERATE, week -> WEEK (or WEEK_CONST if more descriptive), and
dayKor -> DAY_KOR (or DAY_KOREAN) wherever referenced in meal-plan-create-page
component; ensure you update imports/exports, any JSX/handler references, and
tests so identifiers match exactly and run a quick build/grep to catch remaining
occurrences.

44-57: ⚠️ Potential issue | 🟠 Major

결과 단계 전환 시점이 너무 이릅니다.

Line 44에서 먼저 setStep("result")를 호출해, Line 54~56에서 familyRoomId가 없을 때 조기 종료되면 결과 화면 상태로 남을 수 있습니다. 검증/생성 성공 이후에 단계 전환하세요.

수정 예시
 const handleCreate = async () => {
-  if (step === "create") setStep("result");
+  if (familyRoomId == null) {
+    toast.error("가족방 정보를 찾을 수 없어요.");
+    return;
+  }
+
+  if (step === "create") setStep("result");

   const body: CreateMealPlan = {
     weekStartDate: weekParam === "THIS" ? getThisMonday() : getNextMonday(),
     selectedSlots: [...lunchSlots, ...dinnerSlots],
     regenerate: regenerate,
   };

   setIsLoading(true); //로딩 시작
   try {
-    if (familyRoomId == null) {
-      alert("familyRoomId 없음");
-      return;
-    }
     const response = await postCreateMealPlans({
       familyRoomId: familyRoomId,
       createMeal: body,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-create-page.tsx` around lines 44 - 57, The call
to setStep("result") is happening too early and can leave the UI in the result
state if validation fails (e.g., when familyRoomId is null); move the
setStep("result") invocation to after successful validation and/or after the
meal-plan creation completes (i.e., after you check familyRoomId and after the
try block succeeds). Specifically, keep the body construction (CreateMealPlan),
setIsLoading(true), and the familyRoomId null check (familyRoomId == null)
before changing step, and only call setStep("result") once creation succeeds
(inside the try after the API/create call and before setIsLoading(false) in the
success path).

71-76: 🧹 Nitpick | 🔵 Trivial

catch 타입을 unknown으로 고정해 타입 안정성을 지켜주세요.

Line 71의 error: any와 Line 280의 암묵적 타입 처리는 타입 안전성을 낮춥니다. 에러를 unknown으로 받고 좁혀서 메시지를 추출하세요.

수정 예시
-    } catch (error: any) {
-      if (error.response?.data?.code === "MEAL_PLAN_409") {
+    } catch (error: unknown) {
+      const axiosError = error as { response?: { data?: { code?: string; message?: string } } };
+      if (axiosError.response?.data?.code === "MEAL_PLAN_409") {
         toast.error("이미 생성된 다음주 식단이 있어요");
       } else {
-        toast.error(error.response?.data?.message);
+        toast.error(axiosError.response?.data?.message ?? "식단 생성에 실패했어요.");
       }
@@
-    } catch (error) {
-      alert("주간 식단 확정 실패" + error);
+    } catch (_error: unknown) {
+      toast.error("주간 식단 확정에 실패했어요.");
     }

As per coding guidelines Focus on: Strict TypeScript typing.

Also applies to: 280-282

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-create-page.tsx` around lines 71 - 76, Change
both catch clauses in meal-plan-create-page.tsx from catch(error: any) to
catch(error: unknown) and narrow the type before accessing response/data;
implement a small type guard (e.g., isAxiosError or isErrorWithResponse) to
check that error is an object with response?.data?.code and
response?.data?.message, then handle the "MEAL_PLAN_409" branch and fallback
message using the safely extracted fields; ensure a final fallback toast.error
for unknown errors so no any casts are used when reading error.response.data.

28-28: 🧹 Nitpick | 🔵 Trivial

Zustand 구독 범위를 줄여 불필요한 리렌더를 방지하세요.

Line 28의 useProfileStore().isLeader는 스토어 전체 구독 경로가 되어 리렌더가 늘어날 수 있습니다. selector 형태로 바꾸는 편이 안전합니다.

수정 예시
-  const isLeader = useProfileStore().isLeader;
+  const isLeader = useProfileStore((state) => state.isLeader);

As per coding guidelines Focus on: Zustand state management correctness and Avoid unnecessary re-renders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-create-page.tsx` at line 28, The component
directly calls useProfileStore().isLeader which subscribes to the whole store
and can cause unnecessary re-renders; change this to use a selector form such as
const isLeader = useProfileStore(state => state.isLeader) so the component only
subscribes to the isLeader slice (locate the usage of useProfileStore and the
isLeader reference in meal-plan-create-page and replace the direct property
access with a selector callback).
src/pages/meal-plan/meal-plan-page.tsx (1)

40-46: ⚠️ Potential issue | 🟠 Major

React 컴포넌트 내 getState() 직접 호출로 Zustand 구독 메커니즘이 작동하지 않습니다

Line 40에서 useFamilyStore.getState()를 사용하면 스냅샷만 반환되고 구독이 이루어지지 않습니다. familyRoomId가 변경되어도 컴포넌트가 리렌더링되지 않아 Line 46의 useGetTodayMealPlan 훅이 이전 값을 계속 사용할 수 있습니다.

🔧 제안 수정
-  const { familyRoomId } = useFamilyStore.getState();
+  const familyRoomId = useFamilyStore((state) => state.familyRoomId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-page.tsx` around lines 40 - 46, You're using
useFamilyStore.getState() which returns a snapshot and does not subscribe, so
replace that call with the subscribing selector form (call useFamilyStore(state
=> state.familyRoomId)) so the component re-renders when familyRoomId changes
and useGetTodayMealPlan receives the updated id; remove the getState() usage,
pass the subscribed familyRoomId into useGetTodayMealPlan, and optionally guard
useGetTodayMealPlan calls for undefined/nullable familyRoomId in the component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/layouts/mobile-layout.tsx`:
- Around line 27-30: The three JSX <button> elements in mobile-layout.tsx (the
one with onClick={() => navigate("/")} and the other two buttons around lines
referenced) lack an explicit type and can unintentionally submit surrounding
forms; update each button element to include type="button" to prevent default
form submit behavior while preserving their onClick handlers.
- Line 23: The background <img> using the Background asset in the mobile layout
is decorative and should be hidden from assistive tech; update the <img> element
that renders Background (in src/layouts/mobile-layout.tsx) to include alt="" and
aria-hidden="true" so screen readers ignore it (locate the img that currently
has src={Background} and replace its attributes accordingly).
- Around line 10-12: Rename the constant footerPaths to FOOTER_PATHS and move
its declaration out of the component scope to avoid repeated creation; update
any usages (e.g., the showFooter computation that uses
footerPaths.includes(location.pathname)) to reference FOOTER_PATHS instead,
ensuring the constant follows UPPER_SNAKE_CASE and is declared at module level.

In `@src/pages/meal-plan/meal-plan-create-page.tsx`:
- Around line 307-310: The three plain <button> elements (the one with
onClick={onClick} shown in the diff and the other buttons around the same area)
are missing an explicit type, causing accessibility/lint errors; update each
button element to include type="button" so they don't default to type="submit"
(locate the button with onClick={onClick} and the other sibling buttons in the
meal-plan-create-page component and add type="button" to each).
- Around line 280-288: The current finally block always calls
navigate(`/meal-plan?tab=...`) even when the API call failed, causing navigation
on error; modify the control flow in the function containing the
try/catch/finally (the block that calls the API and uses weekParam and navigate)
so that navigation is performed only on success—move the navigate(...) calls out
of finally and into the try after the successful API response (or set a success
flag and check it before navigating); ensure the catch shows the alert("주간 식단 확정
실패" + error) and returns/keeps the user on the current page so they can retry,
while keeping weekParam and navigate references unchanged.
- Around line 257-262: The component currently calls toast.error during render
when sessionStorage.getItem("mealPlan") is falsy; change the early return to
explicitly "return null" and move the toast.error call into a useEffect that
runs when the local "response" (or session value) changes and is falsy (and only
when not isLoading), so the side effect is not executed during render (modify
the component where response is read and add a useEffect tied to
response/isLoading to show the toast instead of invoking toast.error inline).

In `@src/pages/meal-plan/meal-plan-edit-page.tsx`:
- Around line 261-269: The infinite-scroll trigger (useInView ref and inView
effect) should be limited to the Wishlist tab to avoid unnecessary fetchNextPage
calls from the Recommend tab; update the component so the ref from useInView is
only attached/rendered when the active tab is the Wishlist, and keep the
existing useEffect that watches inView, hasNextPage, isFetching, and
fetchNextPage but it will only run when the ref exists; locate the useInView
invocation and the effect (symbols: useInView, ref, inView, useEffect,
hasNextPage, isFetching, fetchNextPage) and conditionally render/attach ref
based on the selected tab (e.g., only when tab === 'wishlist').
- Line 253: The code uses useFamilyStore.getState().familyRoomId (and a similar
call in the parent component) which is non-reactive; change both usages to
subscribe to the Zustand store by using the hook selector form (e.g.,
useFamilyStore(state => state.familyRoomId)) so the component re-renders when
familyRoomId changes, and remove or replace any other direct getState() reads in
these components with selector-based hooks.
- Around line 330-355: The three <button> elements shown (the toggle button that
calls setIsOpen and the two tab buttons that call setTab with "common" and
"recommend") are missing explicit type attributes; add type="button" to each of
these buttons (the Chevron toggle button and both tab buttons) to prevent the
default "submit" behavior inside forms and avoid unintended form submission.
- Around line 294-301: The current handleButton calls navigate() before the
async onSubmit completes, causing navigation prior to API completion; update
handleButton to await the submission by making it async and calling await
onSubmit() (or ensure the onSubmit prop is typed as returning Promise<void>),
then perform navigate(...) after the await; alternatively move navigation
responsibility to the parent so handleButton only calls onSubmit; reference the
handleButton function and the onSubmit prop and ensure navigate and weekParam
logic remains the same but executes only after the awaited onSubmit resolves.
- Line 24: 타입 별칭 mealPlanResponse이 소문자 카멜케이스로 되어 있으니 PascalCase로 변경하세요: type
MealPlanResponse = Record<string, SlotItem[]>; 그리고 이 타입을 참조하는 모든 위치(예: 현재 파일의
사용처인 식별자 참조(사용된 곳은 현재 파일의 line 33 근처))를 MealPlanResponse로 업데이트해 타입 정의와 사용이 일치하도록
만드세요.

In `@src/pages/meal-plan/meal-plan-page.tsx`:
- Around line 325-331: Rename the non-camelCase function handlebutton to
handleButton and update all its references/exports/usages accordingly; also scan
the related block around the other occurrence noted (lines 396-405) and rename
any other functions or variables that use snake/lowercase-without-camel (e.g.,
change xyzFunctionName) to camelCase, updating all callers and tests to maintain
consistency with the project's naming convention.
- Around line 285-304: The map callbacks for futureDays and pastDays sometimes
return nothing causing a lint error; change each usage to first filter out days
with empty meals (e.g., futureDays.filter(d => d.meals.length > 0)) and then map
the filtered array to render the JSX, keeping the existing render logic that
uses CalendarChipS and DateMenuList (preserve isSelect values: true for
futureDays, false for pastDays) so every map callback always returns an element.
- Around line 373-405: Add explicit type="button" to all standalone buttons in
this modal to prevent accidental form submission: the close button that invokes
onClick, each star-rating button that calls setScore, and the two preference
buttons that call handlebutton(true/false). Locate the button elements in the
modal JSX (the one using onClick={onClick}, the star map that calls
setScore(idx), and the preference buttons that call handlebutton) and add
type="button" to each.
- Around line 236-237: The weekly header calculation using listHeaderDate
currently concatenates substrings and adds 6 to the day string (date + "~" +
date[5] + date[6] + "." + (Number(date[8] + date[9]) + 6)), which yields invalid
days at month/ year boundaries; change it to parse the input date into a Date
object (or use an existing date util), add 6 days via date arithmetic (e.g.,
create Date from date, setDate(getDate() + 6)), then format the resulting month
and day with zero-padding as needed and build listHeaderDate from the original
date string and the correctly computed end date; update code paths that
reference listHeaderDate accordingly.
- Around line 340-357: Move the score validation out of the mutate onError path
and perform it at the start of handleSubmitReview so you never call mutate({
recipeId, score, isFavorite, type }) with an invalid score; if score is falsy,
call toast.error("별을 클릭해주세요") and return before mutate. Change the onError
handler signature from (e: any) to (e: AxiosError<{ message?: string }>) and use
e.response?.data?.message ?? "요청에 실패했습니다" when calling toast.error to provide a
fallback message; keep existing calls to setIsOpen/onClick as they are but
ensure onClick() remains only in the error branch after logging the error.

---

Outside diff comments:
In `@src/pages/meal-plan/meal-plan-create-page.tsx`:
- Around line 41-42: Rename the lowercase constants to UPPER_SNAKE_CASE and
update all their usages: change regenerate -> REGENERATE, week -> WEEK (or
WEEK_CONST if more descriptive), and dayKor -> DAY_KOR (or DAY_KOREAN) wherever
referenced in meal-plan-create-page component; ensure you update
imports/exports, any JSX/handler references, and tests so identifiers match
exactly and run a quick build/grep to catch remaining occurrences.
- Around line 44-57: The call to setStep("result") is happening too early and
can leave the UI in the result state if validation fails (e.g., when
familyRoomId is null); move the setStep("result") invocation to after successful
validation and/or after the meal-plan creation completes (i.e., after you check
familyRoomId and after the try block succeeds). Specifically, keep the body
construction (CreateMealPlan), setIsLoading(true), and the familyRoomId null
check (familyRoomId == null) before changing step, and only call
setStep("result") once creation succeeds (inside the try after the API/create
call and before setIsLoading(false) in the success path).
- Around line 71-76: Change both catch clauses in meal-plan-create-page.tsx from
catch(error: any) to catch(error: unknown) and narrow the type before accessing
response/data; implement a small type guard (e.g., isAxiosError or
isErrorWithResponse) to check that error is an object with response?.data?.code
and response?.data?.message, then handle the "MEAL_PLAN_409" branch and fallback
message using the safely extracted fields; ensure a final fallback toast.error
for unknown errors so no any casts are used when reading error.response.data.
- Line 28: The component directly calls useProfileStore().isLeader which
subscribes to the whole store and can cause unnecessary re-renders; change this
to use a selector form such as const isLeader = useProfileStore(state =>
state.isLeader) so the component only subscribes to the isLeader slice (locate
the usage of useProfileStore and the isLeader reference in meal-plan-create-page
and replace the direct property access with a selector callback).

In `@src/pages/meal-plan/meal-plan-page.tsx`:
- Around line 40-46: You're using useFamilyStore.getState() which returns a
snapshot and does not subscribe, so replace that call with the subscribing
selector form (call useFamilyStore(state => state.familyRoomId)) so the
component re-renders when familyRoomId changes and useGetTodayMealPlan receives
the updated id; remove the getState() usage, pass the subscribed familyRoomId
into useGetTodayMealPlan, and optionally guard useGetTodayMealPlan calls for
undefined/nullable familyRoomId in the component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9c3bed44-ef73-440c-a88e-bdaa40012ed6

📥 Commits

Reviewing files that changed from the base of the PR and between 1980ef9 and 27af3e6.

📒 Files selected for processing (16)
  • src/components/chip/CalendarChip/CalendarChipM.tsx
  • src/components/chip/CalendarChip/CalendarChipS.tsx
  • src/components/chip/MenuChip.tsx
  • src/components/common/Footer.tsx
  • src/components/meal-plan/BottomSheet.tsx
  • src/components/meal-plan/ChooseWeek.tsx
  • src/components/meal-plan/ListHeader.tsx
  • src/components/meal-plan/MealPlanResult.tsx
  • src/components/meal-plan/MemberMealPlanView.tsx
  • src/components/meal-plan/ReviewModal.tsx
  • src/components/meal-plan/TodayMeal.tsx
  • src/components/meal-plan/WeekMeal.tsx
  • src/layouts/mobile-layout.tsx
  • src/pages/meal-plan/meal-plan-create-page.tsx
  • src/pages/meal-plan/meal-plan-edit-page.tsx
  • src/pages/meal-plan/meal-plan-page.tsx
💤 Files with no reviewable changes (9)
  • src/components/meal-plan/WeekMeal.tsx
  • src/components/meal-plan/ReviewModal.tsx
  • src/components/meal-plan/ChooseWeek.tsx
  • src/components/meal-plan/MemberMealPlanView.tsx
  • src/components/meal-plan/TodayMeal.tsx
  • src/components/common/Footer.tsx
  • src/components/meal-plan/ListHeader.tsx
  • src/components/meal-plan/MealPlanResult.tsx
  • src/components/meal-plan/BottomSheet.tsx

Comment on lines 10 to 12
const footerPaths = ["/", "/meal-plan"];

const showFooter = footerPaths.includes(location.pathname);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

상수 네이밍을 가이드에 맞게 UPPER_SNAKE_CASE로 맞춰주세요.

footerPaths는 상수 성격이므로 FOOTER_PATHS로 변경하는 게 규칙에 맞고, 컴포넌트 바깥으로 올리면 불필요한 재생성도 줄일 수 있습니다.

♻️ Proposed refactor
+const FOOTER_PATHS = ["/", "/meal-plan"];
+
 export default function MobileLayout() {
   const location = useLocation();
-  const footerPaths = ["/", "/meal-plan"];
-
-  const showFooter = footerPaths.includes(location.pathname);
+  const showFooter = FOOTER_PATHS.includes(location.pathname);

As per coding guidelines, Constants must be UPPER_SNAKE_CASE.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/layouts/mobile-layout.tsx` around lines 10 - 12, Rename the constant
footerPaths to FOOTER_PATHS and move its declaration out of the component scope
to avoid repeated creation; update any usages (e.g., the showFooter computation
that uses footerPaths.includes(location.pathname)) to reference FOOTER_PATHS
instead, ensuring the constant follows UPPER_SNAKE_CASE and is declared at
module level.

{showFooter && (
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 w-full max-w-[400px] text-[10px] font-medium z-10 text-center">
<div className="relative">
<img src={Background} className="w-full" alt="푸터 배경" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

장식용 이미지는 스크린리더에서 제외해주세요.

Line 23의 배경 이미지는 정보 전달용이 아니므로 alt=""aria-hidden="true"가 적절합니다.

♿ Proposed fix
-              <img src={Background} className="w-full" alt="푸터 배경" />
+              <img src={Background} className="w-full" alt="" aria-hidden="true" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img src={Background} className="w-full" alt="푸터 배경" />
<img src={Background} className="w-full" alt="" aria-hidden="true" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/layouts/mobile-layout.tsx` at line 23, The background <img> using the
Background asset in the mobile layout is decorative and should be hidden from
assistive tech; update the <img> element that renders Background (in
src/layouts/mobile-layout.tsx) to include alt="" and aria-hidden="true" so
screen readers ignore it (locate the img that currently has src={Background} and
replace its attributes accordingly).

Comment on lines +27 to +30
<button
className="flex flex-col cursor-pointer gap-2"
onClick={() => navigate("/")}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

button 기본 submit 동작을 명시적으로 차단해주세요.

Line 27, Line 37, Line 47의 버튼 모두 type이 없어, 폼 컨텍스트에서 의도치 않은 submit이 발생할 수 있습니다(정적 분석 에러와 동일).

🔧 Proposed fix
                   <button
+                    type="button"
                     className="flex flex-col cursor-pointer gap-2"
                     onClick={() => navigate("/")}
                   >
@@
                   <button
+                    type="button"
                     className="flex flex-col items-center cursor-pointer gap-2"
                     onClick={() => navigate("/meal-plan")}
                   >
@@
               <button
+                type="button"
                 className="
             absolute left-1/2 -translate-x-1/2 bottom-[clamp(32px,calc(56/400*100vw),56px)]
             flex flex-col justify-center items-center gap-1 

Also applies to: 37-40, 47-55

🧰 Tools
🪛 Biome (2.4.4)

[error] 27-30: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/layouts/mobile-layout.tsx` around lines 27 - 30, The three JSX <button>
elements in mobile-layout.tsx (the one with onClick={() => navigate("/")} and
the other two buttons around lines referenced) lack an explicit type and can
unintentionally submit surrounding forms; update each button element to include
type="button" to prevent default form submit behavior while preserving their
onClick handlers.

Comment on lines +257 to +262
const response = sessionStorage.getItem("mealPlan");
if (isLoading) return null;
if (!response) {
toast.error("올바른 접근이 아닙니다. 식단 생성부터 해주세요!");
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "meal-plan-create-page.tsx" --type f

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 116


🏁 Script executed:

cat -n src/pages/meal-plan/meal-plan-create-page.tsx | head -280 | tail -40

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1237


🏁 Script executed:

wc -l src/pages/meal-plan/meal-plan-create-page.tsx

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 120


🏁 Script executed:

cat -n src/pages/meal-plan/meal-plan-create-page.tsx | head -100

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 4362


🏁 Script executed:

cat -n src/pages/meal-plan/meal-plan-create-page.tsx | sed -n '248,300p'

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1754


🏁 Script executed:

cat -n src/pages/meal-plan/meal-plan-create-page.tsx | sed -n '248,320p'

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 2738


🏁 Script executed:

rg "sessionStorage.getItem|sessionStorage.setItem" src/pages/meal-plan/meal-plan-create-page.tsx -n

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 195


🏁 Script executed:

cat -n src/pages/meal-plan/meal-plan-create-page.tsx | sed -n '43,82p'

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1507


렌더링 중 부수효과를 실행하고 있습니다.

Line 260의 toast.error는 렌더링 중 부수효과라 Strict Mode에서 중복 실행될 수 있습니다. Line 261의 returnreturn null로 명시하고, 알림은 useEffect로 분리하세요.

수정 예시
-  const response = sessionStorage.getItem("mealPlan");
-  if (isLoading) return null;
-  if (!response) {
-    toast.error("올바른 접근이 아닙니다. 식단 생성부터 해주세요!");
-    return;
-  }
+  const response = sessionStorage.getItem("mealPlan");
+  if (isLoading) return null;
+  if (!response) return null;
+
+  useEffect(() => {
+    if (!response) {
+      toast.error("올바른 접근이 아닙니다. 식단 생성부터 해주세요!");
+      navigate("/meal-plan/create");
+    }
+  }, [response, navigate]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-create-page.tsx` around lines 257 - 262, The
component currently calls toast.error during render when
sessionStorage.getItem("mealPlan") is falsy; change the early return to
explicitly "return null" and move the toast.error call into a useEffect that
runs when the local "response" (or session value) changes and is falsy (and only
when not isLoading), so the side effect is not executed during render (modify
the component where response is read and add a useEffect tied to
response/isLoading to show the toast instead of invoking toast.error inline).

Comment on lines +280 to +288
} catch (error) {
alert("주간 식단 확정 실패" + error);
} finally {
if (weekParam === "THIS") {
navigate(`/meal-plan?tab=THIS`);
} else {
navigate(`/meal-plan?tab=NEXT`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

확정 실패 시에도 성공 경로로 이동합니다.

Line 283~287이 finally에 있어 API 실패여도 탭 화면으로 이동합니다. 성공 시에만 이동하고, 실패 시 현재 화면 유지 후 재시도 가능하게 처리하는 게 안전합니다.

수정 예시
   const handleClick = async () => {
     try {
       if (familyRoomId == null) {
         toast.error("familyRoomId 없음");
         return;
       }
       await postConfirmMealPlan({
         familyRoomId: familyRoomId,
         mealPlanId: mealPlanId,
       });
+      if (weekParam === "THIS") {
+        navigate(`/meal-plan?tab=THIS`);
+      } else {
+        navigate(`/meal-plan?tab=NEXT`);
+      }
     } catch (error) {
-      alert("주간 식단 확정 실패" + error);
-    } finally {
-      if (weekParam === "THIS") {
-        navigate(`/meal-plan?tab=THIS`);
-      } else {
-        navigate(`/meal-plan?tab=NEXT`);
-      }
+      toast.error("주간 식단 확정에 실패했어요. 잠시 후 다시 시도해주세요.");
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-create-page.tsx` around lines 280 - 288, The
current finally block always calls navigate(`/meal-plan?tab=...`) even when the
API call failed, causing navigation on error; modify the control flow in the
function containing the try/catch/finally (the block that calls the API and uses
weekParam and navigate) so that navigation is performed only on success—move the
navigate(...) calls out of finally and into the try after the successful API
response (or set a success flag and check it before navigating); ensure the
catch shows the alert("주간 식단 확정 실패" + error) and returns/keeps the user on the
current page so they can retry, while keeping weekParam and navigate references
unchanged.

Comment on lines +236 to +237
const listHeaderDate =
date + "~" + date[5] + date[6] + "." + (Number(date[8] + date[9]) + 6);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

주간 헤더 날짜 계산이 월말/연말에서 잘못됩니다

Line 237은 문자열 일자에 +6을 하는 방식이라 03.36 같은 잘못된 날짜를 만들 수 있습니다.

🔧 제안 수정
-  const listHeaderDate =
-    date + "~" + date[5] + date[6] + "." + (Number(date[8] + date[9]) + 6);
+  const startDate = new Date(date);
+  const endDate = new Date(startDate);
+  endDate.setDate(startDate.getDate() + 6);
+  const endMonth = String(endDate.getMonth() + 1).padStart(2, "0");
+  const endDay = String(endDate.getDate()).padStart(2, "0");
+  const listHeaderDate = `${date}~${endMonth}.${endDay}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const listHeaderDate =
date + "~" + date[5] + date[6] + "." + (Number(date[8] + date[9]) + 6);
const startDate = new Date(date);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
const endMonth = String(endDate.getMonth() + 1).padStart(2, "0");
const endDay = String(endDate.getDate()).padStart(2, "0");
const listHeaderDate = `${date}~${endMonth}.${endDay}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-page.tsx` around lines 236 - 237, The weekly
header calculation using listHeaderDate currently concatenates substrings and
adds 6 to the day string (date + "~" + date[5] + date[6] + "." + (Number(date[8]
+ date[9]) + 6)), which yields invalid days at month/ year boundaries; change it
to parse the input date into a Date object (or use an existing date util), add 6
days via date arithmetic (e.g., create Date from date, setDate(getDate() + 6)),
then format the resulting month and day with zero-padding as needed and build
listHeaderDate from the original date string and the correctly computed end
date; update code paths that reference listHeaderDate accordingly.

Comment on lines +285 to +304
{futureDays.map((day) => {
if (day.meals.length > 0) {
return (
<div key={day.dayKor} className="flex gap-3">
<CalendarChipS text={day.dayKor} type="primary" />
<DateMenuList isSelect={true} data={day.meals} />
</div>
);
}
})}
{pastDays.map((day) => {
if (day.meals.length > 0) {
return (
<div key={day.dayKor} className="flex gap-3">
<CalendarChipS text={day.dayKor} type="gray" />
<DateMenuList isSelect={false} data={day.meals} />
</div>
);
}
})}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

map() 콜백이 일부 분기에서 값을 반환하지 않아 lint 에러가 발생합니다

Line 285, Line 295의 map 콜백은 조건 불일치 시 반환값이 없습니다. filter + map으로 분리해 명시적으로 반환해 주세요.

🔧 제안 수정
-        {futureDays.map((day) => {
-          if (day.meals.length > 0) {
-            return (
-              <div key={day.dayKor} className="flex gap-3">
-                <CalendarChipS text={day.dayKor} type="primary" />
-                <DateMenuList isSelect={true} data={day.meals} />
-              </div>
-            );
-          }
-        })}
-        {pastDays.map((day) => {
-          if (day.meals.length > 0) {
-            return (
-              <div key={day.dayKor} className="flex gap-3">
-                <CalendarChipS text={day.dayKor} type="gray" />
-                <DateMenuList isSelect={false} data={day.meals} />
-              </div>
-            );
-          }
-        })}
+        {futureDays
+          .filter((day) => day.meals.length > 0)
+          .map((day) => (
+            <div key={day.dayKor} className="flex gap-3">
+              <CalendarChipS text={day.dayKor} type="primary" />
+              <DateMenuList isSelect={true} data={day.meals} />
+            </div>
+          ))}
+        {pastDays
+          .filter((day) => day.meals.length > 0)
+          .map((day) => (
+            <div key={day.dayKor} className="flex gap-3">
+              <CalendarChipS text={day.dayKor} type="gray" />
+              <DateMenuList isSelect={false} data={day.meals} />
+            </div>
+          ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{futureDays.map((day) => {
if (day.meals.length > 0) {
return (
<div key={day.dayKor} className="flex gap-3">
<CalendarChipS text={day.dayKor} type="primary" />
<DateMenuList isSelect={true} data={day.meals} />
</div>
);
}
})}
{pastDays.map((day) => {
if (day.meals.length > 0) {
return (
<div key={day.dayKor} className="flex gap-3">
<CalendarChipS text={day.dayKor} type="gray" />
<DateMenuList isSelect={false} data={day.meals} />
</div>
);
}
})}
{futureDays
.filter((day) => day.meals.length > 0)
.map((day) => (
<div key={day.dayKor} className="flex gap-3">
<CalendarChipS text={day.dayKor} type="primary" />
<DateMenuList isSelect={true} data={day.meals} />
</div>
))}
{pastDays
.filter((day) => day.meals.length > 0)
.map((day) => (
<div key={day.dayKor} className="flex gap-3">
<CalendarChipS text={day.dayKor} type="gray" />
<DateMenuList isSelect={false} data={day.meals} />
</div>
))}
🧰 Tools
🪛 Biome (2.4.4)

[error] 285-285: This callback passed to map() iterable method should always return a value.

(lint/suspicious/useIterableCallbackReturn)


[error] 295-295: This callback passed to map() iterable method should always return a value.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-page.tsx` around lines 285 - 304, The map
callbacks for futureDays and pastDays sometimes return nothing causing a lint
error; change each usage to first filter out days with empty meals (e.g.,
futureDays.filter(d => d.meals.length > 0)) and then map the filtered array to
render the JSX, keeping the existing render logic that uses CalendarChipS and
DateMenuList (preserve isSelect values: true for futureDays, false for pastDays)
so every map callback always returns an element.

Comment on lines +325 to +331
const handlebutton = (select: Preference) => {
if (select == preference) {
setPreference(null);
} else {
setPreference(select);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

함수명 handlebutton을 camelCase(handleButton)로 맞춰주세요

Line 325의 함수명은 팀 네이밍 규칙과 일관성이 떨어집니다.

🔧 제안 수정
-  const handlebutton = (select: Preference) => {
+  const handleButton = (select: Preference) => {
@@
-              onClick={() => handlebutton(false)}
+              onClick={() => handleButton(false)}
@@
-              onClick={() => handlebutton(true)}
+              onClick={() => handleButton(true)}

As per coding guidelines Variables and functions must use camelCase.

Also applies to: 396-405

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-page.tsx` around lines 325 - 331, Rename the
non-camelCase function handlebutton to handleButton and update all its
references/exports/usages accordingly; also scan the related block around the
other occurrence noted (lines 396-405) and rename any other functions or
variables that use snake/lowercase-without-camel (e.g., change xyzFunctionName)
to camelCase, updating all callers and tests to maintain consistency with the
project's naming convention.

Comment on lines +340 to +357
const handleSubmitReview = ({
recipeId,
score,
isFavorite,
}: createReview) => {
mutate(
{ recipeId, score, isFavorite, type },
{
onSuccess: () => {
setIsOpen(true);
},
onError: (e: any) => {
if (!score) {
toast.error("별을 클릭해주세요");
return;
}
toast.error(e.response?.data?.message);
onClick();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/pages/meal-plan/meal-plan-page.tsx | sed -n '330,365p'

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1023


🏁 Script executed:

# Check for any type usage in error handlers
rg -n 'onError.*any' src/pages/meal-plan/meal-plan-page.tsx -A 5

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 269


🏁 Script executed:

# Check for AxiosError import in the file
rg -n 'import.*AxiosError|from.*axios' src/pages/meal-plan/meal-plan-page.tsx

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 53


🏁 Script executed:

# Check the createReview type definition
rg -n 'createReview|interface.*Review|type.*Review' src/pages/meal-plan/meal-plan-page.tsx -B 2 -A 3

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1199


🏁 Script executed:

find src -name "*use-post-review*" -type f

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 109


🏁 Script executed:

# Check the usePostReview hook definition
cat -n src/hooks/mutations/use-post-review.ts 2>/dev/null || cat -n src/hooks/mutations/use-post-review.tsx

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1243


🏁 Script executed:

# Check for other onError handlers with 'any' type in the file
rg -n 'onError.*:.*any' src/pages/meal-plan/meal-plan-page.tsx

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 105


🏁 Script executed:

# Check what types are imported from axios or other API libraries
rg -n 'import.*from.*axios|import.*from.*api' src/pages/meal-plan/meal-plan-page.tsx

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 53


🏁 Script executed:

# Check the postReview API function to understand error type
rg -n 'export.*postReview|function postReview' src/api/review.ts -A 10

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 357


🏁 Script executed:

# Check axios configuration or API setup in the project
find src -name "*api*" -o -name "*axios*" | head -20

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 115


🏁 Script executed:

# Check for TypeScript types related to API errors
rg -n 'AxiosError|Error.*type|interface.*Error' src/types/ -B 1 -A 2

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 53


🏁 Script executed:

# Check axios configuration
cat -n src/api/axios/axios.ts

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1024


🏁 Script executed:

# Check what error types are used across the codebase
rg -n 'import.*AxiosError|from.*axios' src/ -A 2 | head -30

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 1229


🏁 Script executed:

# Check the review types file
cat -n src/types/review.ts

Repository: Urisik-serv/URISIK_FRONTEND

Length of output: 606


리뷰 제출 전 검증을 먼저 수행하고 에러 타입을 명시해야 합니다

345번 라인에서 mutate() 호출 시 score 검증이 선행되지 않아 유효하지 않은 요청이 전송되고 있으며, 351번 라인의 any 타입 사용으로 인한 타입 안정성 부재가 확인됩니다.

  • score 검증을 mutate() 호출 전에 이동
  • 에러 핸들러의 타입을 AxiosError<{ message?: string }>로 명시
  • 폴백 메시지 추가 권장
🔧 제안 수정
+import type { AxiosError } from "axios";
 import type { createReview } from "../../types/review";
 
 const handleSubmitReview = ({
   recipeId,
   score,
   isFavorite,
 }: createReview) => {
+  if (!score) {
+    toast.error("별을 클릭해주세요");
+    return;
+  }
+
   mutate(
     { recipeId, score, isFavorite, type },
     {
       onSuccess: () => {
         setIsOpen(true);
       },
-      onError: (e: any) => {
-        if (!score) {
-          toast.error("별을 클릭해주세요");
-          return;
-        }
-        toast.error(e.response?.data?.message);
+      onError: (e: AxiosError<{ message?: string }>) => {
+        toast.error(e.response?.data?.message ?? "리뷰 등록에 실패했어요");
         onClick();
       },
     },
   );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSubmitReview = ({
recipeId,
score,
isFavorite,
}: createReview) => {
mutate(
{ recipeId, score, isFavorite, type },
{
onSuccess: () => {
setIsOpen(true);
},
onError: (e: any) => {
if (!score) {
toast.error("별을 클릭해주세요");
return;
}
toast.error(e.response?.data?.message);
onClick();
const handleSubmitReview = ({
recipeId,
score,
isFavorite,
}: createReview) => {
if (!score) {
toast.error("별을 클릭해주세요");
return;
}
mutate(
{ recipeId, score, isFavorite, type },
{
onSuccess: () => {
setIsOpen(true);
},
onError: (e: AxiosError<{ message?: string }>) => {
toast.error(e.response?.data?.message ?? "리뷰 등록에 실패했어요");
onClick();
},
},
);
};
🧰 Tools
🪛 ESLint

[error] 351-351: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-page.tsx` around lines 340 - 357, Move the
score validation out of the mutate onError path and perform it at the start of
handleSubmitReview so you never call mutate({ recipeId, score, isFavorite, type
}) with an invalid score; if score is falsy, call toast.error("별을 클릭해주세요") and
return before mutate. Change the onError handler signature from (e: any) to (e:
AxiosError<{ message?: string }>) and use e.response?.data?.message ?? "요청에
실패했습니다" when calling toast.error to provide a fallback message; keep existing
calls to setIsOpen/onClick as they are but ensure onClick() remains only in the
error branch after logging the error.

Comment on lines +373 to +405
<button
className="pt-[10px] cursor-pointer flex justify-center items-center"
onClick={onClick}
>
<img src={X} alt="닫기버튼" className="size-6" />
</button>
<div className="font-semibold">
<p className="text-[20px] pb-6">오늘의 메뉴는 어떠셨나요</p>
<div className="pb-4 flex justify-center">
{star.map((idx) => (
<button
className="size-[58px] flex justify-center items-center cursor-pointer"
key={idx}
onClick={() => setScore(idx)}
>
<img
src={idx <= score ? SelectedStar : UnselectedStar}
alt="별점"
/>
</button>
))}
</div>
<div className="flex justify-center gap-[10px]">
<button
className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === false ? "text-white bg-primary-700" : "text-black bg-[#F0F0F0]"}`}
onClick={() => handlebutton(false)}
>
내 취향은 아니에요
</button>
<button
className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === true ? "text-white bg-primary-700" : "text-black bg-[#F0F0F0]"}`}
onClick={() => handlebutton(true)}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

모달 내 버튼들에 type="button"이 누락되어 있습니다

Line 373, Line 383, Line 396, Line 402의 버튼은 폼 컨텍스트에서 의도치 않은 submit을 유발할 수 있습니다.

🔧 제안 수정
         <button
+          type="button"
           className="pt-[10px] cursor-pointer flex justify-center items-center"
           onClick={onClick}
         >
@@
               <button
+                type="button"
                 className="size-[58px] flex justify-center items-center cursor-pointer"
                 key={idx}
                 onClick={() => setScore(idx)}
               >
@@
             <button
+              type="button"
               className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === false ? "text-white bg-primary-700" : "text-black bg-[`#F0F0F0`]"}`}
               onClick={() => handlebutton(false)}
             >
@@
             <button
+              type="button"
               className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === true ? "text-white bg-primary-700" : "text-black bg-[`#F0F0F0`]"}`}
               onClick={() => handlebutton(true)}
             >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
className="pt-[10px] cursor-pointer flex justify-center items-center"
onClick={onClick}
>
<img src={X} alt="닫기버튼" className="size-6" />
</button>
<div className="font-semibold">
<p className="text-[20px] pb-6">오늘의 메뉴는 어떠셨나요</p>
<div className="pb-4 flex justify-center">
{star.map((idx) => (
<button
className="size-[58px] flex justify-center items-center cursor-pointer"
key={idx}
onClick={() => setScore(idx)}
>
<img
src={idx <= score ? SelectedStar : UnselectedStar}
alt="별점"
/>
</button>
))}
</div>
<div className="flex justify-center gap-[10px]">
<button
className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === false ? "text-white bg-primary-700" : "text-black bg-[#F0F0F0]"}`}
onClick={() => handlebutton(false)}
>
취향은 아니에요
</button>
<button
className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === true ? "text-white bg-primary-700" : "text-black bg-[#F0F0F0]"}`}
onClick={() => handlebutton(true)}
>
<button
type="button"
className="pt-[10px] cursor-pointer flex justify-center items-center"
onClick={onClick}
>
<img src={X} alt="닫기버튼" className="size-6" />
</button>
<div className="font-semibold">
<p className="text-[20px] pb-6">오늘의 메뉴는 어떠셨나요</p>
<div className="pb-4 flex justify-center">
{star.map((idx) => (
<button
type="button"
className="size-[58px] flex justify-center items-center cursor-pointer"
key={idx}
onClick={() => setScore(idx)}
>
<img
src={idx <= score ? SelectedStar : UnselectedStar}
alt="별점"
/>
</button>
))}
</div>
<div className="flex justify-center gap-[10px]">
<button
type="button"
className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === false ? "text-white bg-primary-700" : "text-black bg-[`#F0F0F0`]"}`}
onClick={() => handlebutton(false)}
>
취향은 아니에요
</button>
<button
type="button"
className={`px-[10px] py-2 rounded-lg cursor-pointer ${preference === true ? "text-white bg-primary-700" : "text-black bg-[`#F0F0F0`]"}`}
onClick={() => handlebutton(true)}
>
🧰 Tools
🪛 Biome (2.4.4)

[error] 373-376: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)


[error] 383-387: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)


[error] 396-399: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)


[error] 402-405: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/meal-plan/meal-plan-page.tsx` around lines 373 - 405, Add explicit
type="button" to all standalone buttons in this modal to prevent accidental form
submission: the close button that invokes onClick, each star-rating button that
calls setScore, and the two preference buttons that call
handlebutton(true/false). Locate the button elements in the modal JSX (the one
using onClick={onClick}, the star map that calls setScore(idx), and the
preference buttons that call handlebutton) and add type="button" to each.

Comment on lines +233 to +253
const { month, weekKor } = getWeekOfMonth(new Date(date));

const dayKorMap: Record<string, string> = {
MONDAY: "월",
TUESDAY: "화",
WEDNESDAY: "수",
THURSDAY: "목",
FRIDAY: "금",
SATURDAY: "토",
SUNDAY: "일",
};
const dayNames = [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY",
];
const todayIdx = new Date().getDay();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 utils/changeAdditionalProp.ts 파일 보시면 day 배열이 존재하는데, 한번 확인해보고 통합해도 좋을 것 같아요.

확인해보니 constants/date-record.ts에도 월~일까지의 Record가 있습니다. date는 여러곳에서 사용되니까 한 번 확인해보시고 통일하면 좋을 것 같아요!

Comment on lines +159 to +172
type ChooseWeekProps = {
mealTime: "점심" | "저녁";
onChangeSelected: (slots: SlotRequest[]) => void;
};

const week: { label: string; value: DayOfWeek }[] = [
{ label: "월", value: "MONDAY" },
{ label: "화", value: "TUESDAY" },
{ label: "수", value: "WEDNESDAY" },
{ label: "목", value: "THURSDAY" },
{ label: "금", value: "FRIDAY" },
{ label: "토", value: "SATURDAY" },
{ label: "일", value: "SUNDAY" },
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위의 리뷰와 동일하게 같이 선언하거나 모아둬도 괜찮을 것 같아요!

Comment on lines +265 to +267
const handleButton = () => {
setIsOpen(true);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleClickhandleButton 둘의 차이가 모호한 것 같아요. 핸들러 명만 봤을 때는 둘이 어떤 차이를 가지는지 와닿지 않습니다! 그리고 setIsOpen(true)의 기능만 한다면 굳이 핸들러를 만들지 않고 {()=>setIsOpen(ture)} 이런 식으로 전달해 주는게 더 나아보여요!

Comment on lines +241 to +243
const [isOpen, setIsOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selected, setSelected] = useState<{

@tablemin03 tablemin03 Mar 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네이밍이 헷갈립니다..! tab, isOpen, selected가 어떤 걸 의미하는 건지 잘 모르겠어요..

@shooukie

Copy link
Copy Markdown
Contributor Author

PR 218 머지 -> PR 221 내 충돌 해결 후 머지-> 새 이슈를 파서 리펙토링 진행 계획이었는데
PR 221 머지 -> PR 218의 브랜치에서 충돌 해결 방식으로 진행하겠습니다!

@shooukie
shooukie merged commit cd4f620 into develop Mar 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] 불필요한 컴포넌트 리팩토링

2 participants