Conversation
빌드 결과빌드 성공 🎊 |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/shared/components/v2/bottomSheet/BottomSheetBase.css.ts (1)
164-174: 🧹 Nitpick | 🔵 Trivial스크롤바 숨김은 공통 base 기본값보다 개별 시트 opt-in이 안전합니다.
contentSlot은 모든 v2 바텀시트가 공유하는 슬롯이라, 여기서 스크롤바를 숨기면 필터 시트까지 포함해 전체 동작이 같이 바뀝니다. 이번 요구가 선택 상품 시트 전용이라면 shared base가 아니라 해당 시트 스타일로 내리는 편이 부작용과 접근성 회귀를 줄입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/components/v2/bottomSheet/BottomSheetBase.css.ts` around lines 164 - 174, contentSlot currently hides scrollbars (scrollbarWidth, msOverflowStyle, and the '&::-webkit-scrollbar' selector), which affects all v2 bottom sheets; remove those scrollbar-hiding lines from contentSlot and instead add them to the specific bottom sheet style that requires hidden scrollbars (e.g., the select-product sheet's content class), so only that sheet opts in; reference and remove the properties named scrollbarWidth, msOverflowStyle, and the selectors '&::-webkit-scrollbar' from contentSlot and add equivalent rules into the target sheet's style definition.
♻️ Duplicate comments (2)
src/pages/home/components/product/ProductTab.tsx (2)
164-167:⚠️ Potential issue | 🟠 Major시트를 닫을 때 draft 필터를 적용값으로 복원해 주세요.
ProductFilterSheet는 ref 기반 내부 상태를 유지하는데, 여기서는 닫을 때appliedFilterValues를 다시 넣지 않습니다. 그래서 적용하지 않은 변경이나초기화결과가 다음 오픈 때 그대로 남아서, 바깥 칩 요약과 시트 내부 선택값이 쉽게 어긋납니다.수정 예시
const handleFilterSheetClose = useCallback(() => { + productFilterSheetRef.current?.setValues(appliedFilterValues); setFilterSheetOpen(false); setChipSelected({ ...INITIAL_CHIP_SELECTED }); -}, []); +}, [appliedFilterValues]);Also applies to: 169-182
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/home/components/product/ProductTab.tsx` around lines 164 - 167, When closing the ProductFilterSheet, restore its internal draft state to the currently applied filters so the sheet's selections don't out-of-sync with the chip summary: update handleFilterSheetClose to call whatever setter/prop on ProductFilterSheet that sets appliedFilterValues (or pass appliedFilterValues back into the sheet) before calling setFilterSheetOpen(false), and also reset the visible chips via setChipSelected({ ...INITIAL_CHIP_SELECTED }) as currently done; ensure the same fix is applied for the similar block around lines 169-182 so both close paths restore appliedFilterValues into the ProductFilterSheet.
242-250:⚠️ Potential issue | 🟠 Major주요 CTA를 실제 액션에 연결하거나 준비 전까지 비활성화해 주세요.
현재
onClick={() => {}}라서 사용자 입장에서는 명백한 dead-end입니다. 실제 동작이 아직 없으면 숨기거나 비활성화하는 쪽이 낫습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/home/components/product/ProductTab.tsx` around lines 242 - 250, The primary CTA in ProductTab is wired to an empty handler (primaryButton -> ActionButton with onClick={() => {}}), creating a dead-end; either connect it to the intended action (e.g., call the existing handler that applies selected products to the home or navigate to the design flow) or mark the button disabled/hidden until the feature is implemented. Update the ActionButton inside ProductTab to invoke the real function (or pass a noop-disabled flag) and ensure any related state/props (selectedProducts, applyProducts, or navigation helper) are used so the button is actionable or visibly disabled.
🤖 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/pages/home/components/product/ProductTab.tsx`:
- Around line 204-210: The current handleSelectProduct silently ignores clicks
when selected items hit MAX_SELECTED_PRODUCTS; update it to set and expose an
explicit "limit reached" state (e.g., setSelectionLimitReached(true)) instead of
just returning prev so the parent/SearchSection can receive this flag to disable
the CTA and/or show feedback; specifically modify handleSelectProduct and the
analogous callback used at the other location to (1) check prev.length >=
MAX_SELECTED_PRODUCTS, (2) set a boolean limit state or call a provided
onLimitReached callback, and (3) still return prev to avoid changing selection —
then pass that state/prop into SearchSection to disable the button or show a
message.
In
`@src/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.css.ts`:
- Around line 177-187: The closeButtonBase style duplicates button reset rules
(border, background, padding) that should be provided by the global button
reset; remove the redundant reset properties from closeButtonBase and keep only
positioning/layout props (position, zIndex, display, alignItems, justifyContent,
and optional borderRadius) so the IconButton's actual button element relies on
the global reset; verify the globalStyle('button', ...) or shared reset file
exists and relies on it rather than re-declaring resets inside closeButtonBase.
In `@src/shared/components/v2/productCard/ProductCard.tsx`:
- Around line 35-39: The shoppingAction prop on ProductCard is currently
optional which allows a shopping card to render an active CTA without an
onClick; update ProductCard (the shoppingAction prop/type and the rendering
logic around cardType === "shopping") to either make shoppingAction required
when cardType === "shopping" or, if absent, render the shopping button as
disabled and ensure no onClick is used; specifically adjust the shoppingAction
type definition and the component branches that check cardType and render the
button so the shopping branch either enforces presence of shoppingAction or
guards against undefined by disabling the button and avoiding calling
shoppingAction.onClick.
---
Outside diff comments:
In `@src/shared/components/v2/bottomSheet/BottomSheetBase.css.ts`:
- Around line 164-174: contentSlot currently hides scrollbars (scrollbarWidth,
msOverflowStyle, and the '&::-webkit-scrollbar' selector), which affects all v2
bottom sheets; remove those scrollbar-hiding lines from contentSlot and instead
add them to the specific bottom sheet style that requires hidden scrollbars
(e.g., the select-product sheet's content class), so only that sheet opts in;
reference and remove the properties named scrollbarWidth, msOverflowStyle, and
the selectors '&::-webkit-scrollbar' from contentSlot and add equivalent rules
into the target sheet's style definition.
---
Duplicate comments:
In `@src/pages/home/components/product/ProductTab.tsx`:
- Around line 164-167: When closing the ProductFilterSheet, restore its internal
draft state to the currently applied filters so the sheet's selections don't
out-of-sync with the chip summary: update handleFilterSheetClose to call
whatever setter/prop on ProductFilterSheet that sets appliedFilterValues (or
pass appliedFilterValues back into the sheet) before calling
setFilterSheetOpen(false), and also reset the visible chips via
setChipSelected({ ...INITIAL_CHIP_SELECTED }) as currently done; ensure the same
fix is applied for the similar block around lines 169-182 so both close paths
restore appliedFilterValues into the ProductFilterSheet.
- Around line 242-250: The primary CTA in ProductTab is wired to an empty
handler (primaryButton -> ActionButton with onClick={() => {}}), creating a
dead-end; either connect it to the intended action (e.g., call the existing
handler that applies selected products to the home or navigate to the design
flow) or mark the button disabled/hidden until the feature is implemented.
Update the ActionButton inside ProductTab to invoke the real function (or pass a
noop-disabled flag) and ensure any related state/props (selectedProducts,
applyProducts, or navigation helper) are used so the button is actionable or
visibly disabled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 72b29159-b3f4-4600-8c76-9364dc0efabe
📒 Files selected for processing (6)
src/pages/home/components/product/ProductTab.tsxsrc/pages/home/components/product/SearchSection/SearchSection.tsxsrc/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.css.tssrc/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.tsxsrc/shared/components/v2/bottomSheet/BottomSheetBase.css.tssrc/shared/components/v2/productCard/ProductCard.tsx
| const handleSelectProduct = useCallback((product: SelectedProduct) => { | ||
| setSelectedProducts((prev) => { | ||
| if (prev.some((item) => item.id === product.id)) return prev; | ||
| if (prev.length >= MAX_SELECTED_PRODUCTS) return prev; | ||
| return [...prev, product]; | ||
| }); | ||
| }, []); |
There was a problem hiding this comment.
최대 선택 수 도달 후에는 클릭만 무시되지 않게 해 주세요.
여기서는 6개 초과 선택을 조용히 무시해서, 목록 쪽 CTA는 계속 활성화되어 있는데 눌러도 아무 일도 일어나지 않는 상태가 됩니다. limit reached 상태를 SearchSection에 내려 버튼을 비활성화하거나, 최소한 안내 피드백을 같이 노출하는 편이 UX상 안전합니다.
Also applies to: 221-228
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/home/components/product/ProductTab.tsx` around lines 204 - 210, The
current handleSelectProduct silently ignores clicks when selected items hit
MAX_SELECTED_PRODUCTS; update it to set and expose an explicit "limit reached"
state (e.g., setSelectionLimitReached(true)) instead of just returning prev so
the parent/SearchSection can receive this flag to disable the CTA and/or show
feedback; specifically modify handleSelectProduct and the analogous callback
used at the other location to (1) check prev.length >= MAX_SELECTED_PRODUCTS,
(2) set a boolean limit state or call a provided onLimitReached callback, and
(3) still return prev to avoid changing selection — then pass that state/prop
into SearchSection to disable the button or show a message.
| shoppingAction?: { | ||
| label?: string; | ||
| onClick: () => void; | ||
| disabled?: boolean; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
shopping 카드의 액션 계약을 타입으로 강제해 주세요.
지금은 shoppingAction이 optional이라 cardType="shopping"인데도 버튼이 활성화된 채 onClick 없이 렌더링될 수 있습니다. shared 컴포넌트라 호출부 실수가 바로 dead-end CTA로 이어지니, shopping 분기에서는 shoppingAction을 필수로 강제하거나 값이 없을 때 버튼을 disabled 처리하는 쪽이 안전합니다.
Also applies to: 210-218
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/components/v2/productCard/ProductCard.tsx` around lines 35 - 39,
The shoppingAction prop on ProductCard is currently optional which allows a
shopping card to render an active CTA without an onClick; update ProductCard
(the shoppingAction prop/type and the rendering logic around cardType ===
"shopping") to either make shoppingAction required when cardType === "shopping"
or, if absent, render the shopping button as disabled and ensure no onClick is
used; specifically adjust the shoppingAction type definition and the component
branches that check cardType and render the button so the shopping branch either
enforces presence of shoppingAction or guards against undefined by disabling the
button and avoiding calling shoppingAction.onClick.
빌드 결과빌드 성공 🎊 |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (4)
src/pages/home/components/product/IntroSection/IntroSection.tsx (1)
19-20:⚠️ Potential issue | 🟡 Minor주석을 80자 이하로 분리하고 현재 도메인 용어로 맞춰 주세요.
Line 20 주석이 80자 제한을 넘고,
mediaUrl표기가 현재 props(bannerUrl)와 달라 혼동을 유발합니다.✂️ 수정 예시
- // 이미지 로드 실패 시 fallback src를 유지하고, - // 부모가 새 mediaUrl을 내려주면 그 값으로 다시 동기화 (RoomTypeCard와 동일 패턴) + // 이미지 로드 실패 시 fallback src를 유지하고, + // 부모가 새 bannerUrl을 내려주면 그 값으로 다시 동기화 + // (RoomTypeCard와 동일 패턴)As per coding guidelines "Maintain a maximum line width of 80 characters".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/home/components/product/IntroSection/IntroSection.tsx` around lines 19 - 20, The comment above IntroSection needs to be split into lines under 80 characters and use the actual prop name; replace the long comment that mentions mediaUrl with two or three short lines referencing bannerUrl and the behavior (keep fallback src on load failure and resync when parent provides a new bannerUrl), updating the comment near the IntroSection/IntroSection.tsx code to match the component's prop name and 80-column guideline.src/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.css.ts (1)
177-186: 🧹 Nitpick | 🔵 Trivial
closeButton에서paddingreset은 제거해도 됩니다.전역 button reset이 이미 있다면 여기서는 위치/정렬 속성만 두는 편이 재사용성과 일관성 면에서 더 안전합니다.
✂️ 제안 수정안
base: { position: 'absolute', zIndex: 2, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: unitVars.unit.radius.full, - padding: 0, },Based on learnings, Do not duplicate button reset styles inside Vanilla Extract base blocks when a global reset (e.g., globalStyle('button', { all: 'unset', … })) already exists in a shared styles file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.css.ts` around lines 177 - 186, The closeButton recipe currently duplicates a button padding reset (padding: 0) which should be removed to avoid repeating global button resets; edit the closeButton base block to delete the padding property and leave only positioning/alignment rules (position, zIndex, display, alignItems, justifyContent, borderRadius) so the component relies on the shared global button reset and keeps styles consistent and reusable.src/pages/home/components/product/SearchSection/SearchSection.tsx (1)
213-215:⚠️ Potential issue | 🟠 Major목업 카드가 아직 실제 인터랙션을 발생시킵니다.
지금은 저장 액션이 no-op이고 카드 링크도 placeholder 값으로 고정돼 있어서, 연결 전 상태에서 사용자가 반응 없는 저장 버튼을 누르거나 잘못된 이동을 하게 됩니다. 실제 플로우를 붙이기 전까지는
link를 생략하고 저장 액션도 비인터랙티브 상태로 내려 주세요.Also applies to: 288-295
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/home/components/product/SearchSection/SearchSection.tsx` around lines 213 - 215, 목업 카드가 아직 실제 플로우에 연결되지 않아 사용자에게 반응 없는 저장 버튼이나 잘못된 링크로 이동을 유발하므로, 목업 카드 렌더링 부분(현재 handleMockSaveToggle 및 handleSelectMockProduct 호출이 있는 블록과 288-295 영역)을 수정해 주세요: Product 카드에 전달하는 link/href props를 제거하거나 undefined로 설정하여 링크를 남기지 않고, handleMockSaveToggle는 실제 동작 대신 비활성 상태로 유지하되(버튼에 disabled 또는 aria-disabled 설정) 클릭 핸들러를 연결하지 않거나 no-op로 남겨 사용자 상호작용이 비인터랙티브하게 보이도록 변경하고, handleSelectMockProduct도 네비게이션을 트리거하지 않도록 제거하거나 no-op로 바꿔 주세요.src/pages/home/components/product/ProductTab.tsx (1)
306-311:⚠️ Potential issue | 🟠 Major선택 후 주요 CTA가 아직 dead-end입니다.
selectedProducts.length > 0인 정상 경로에서handleDecorateWithProductsClick가 그대로 끝나서, 활성화된 버튼이 아무 일도 하지 않습니다. 실제 액션을 붙이기 전까지는 버튼을 계속 비활성/비노출 처리하거나, 여기서 다음 플로우로 바로 연결해 주세요.Also applies to: 341-347
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/home/components/product/ProductTab.tsx` around lines 306 - 311, The click handler handleDecorateWithProductsClick currently only notifies when selectedProducts.length === 0 and otherwise does nothing, leaving the CTA as a dead-end; update the function to perform the intended next step when products are selected (for example call the existing flow entry such as openDecorateModal(), navigate to the decorate route, or invoke a prop callback like onDecorateSelected(selectedProducts)), or alternatively ensure the CTA is disabled/hidden unless a real action is wired; also apply the same fix to the duplicate handler instance mentioned (the second occurrence around the other handler block).
🤖 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/pages/home/components/product/ProductFilterSheet/ProductFilterSheet.tsx`:
- Around line 79-80: forwardRef로 선언된 ProductFilterSheet 컴포넌트에 displayName이 빠져 있어
DevTools/스택 추적에 이름이 나오지 않습니다; ProductFilterSheet(및 필요한 경우 ProductFilterSheetRef
표기)를 정의한 후에 ProductFilterSheet.displayName = 'ProductFilterSheet'를 추가하여 명시적으로
컴포넌트명을 설정해 주세요. 이 변경은 forwardRef<ProductFilterSheetRef>(function
ProductFilterSheet...) 바로 아래나 파일 하단에 추가하면 됩니다.
In `@src/pages/home/components/product/ProductTab.tsx`:
- Around line 217-238: The updater passed to setChipSelected (in
handleFilterChipClick) must remain pure: remove queueMicrotask calls that call
setFilterSheetOpen from inside the updater; instead, compute the current
conditions (isClosing / isAtLimit / attemptedOverMax) synchronously outside the
updater, call setChipSelected with a plain state object, then call
setFilterSheetOpen and any notify/toast actions after setChipSelected based on
those precomputed booleans. Also move the attemptedOverMax calculation out of
the updater so it is read/derived before state updates, and update
handleDecorateWithProductsClick to actually execute its action even when
products are already selected (don’t early-return based solely on selection
state). Use the existing symbols handleFilterChipClick, setChipSelected,
setFilterSheetOpen, attemptedOverMax, and handleDecorateWithProductsClick to
locate and apply these changes.
In
`@src/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.tsx`:
- Around line 109-113: The wrapper currently uses a <span>
(className={styles.addCardContent}, aria-hidden) but contains a <p> which is
invalid; change the <span> to a <div> so phrasing/flow content rules are
respected—update the element around Icon and the <p> in SelectedProductSheet
(the node with className addCardContent) to a <div>, preserve the className and
aria-hidden attribute (and any styling) so layout and accessibility behavior
remain the same.
In `@src/shared/components/v2/button/actionButton/ActionButton.tsx`:
- Around line 69-70: ARIA state and the actual disabled behavior are
inconsistent: ActionButton sets disabled using isDomDisabled but may set
aria-disabled from isVisuallyDisabled, causing assistive tech to think the
button is disabled when it is still interactive; update the aria-disabled usage
in ActionButton.tsx so it reflects the real interactive state (use isDomDisabled
as the source of truth, e.g., only set aria-disabled when the DOM is actually
disabled) and keep disabled={isDomDisabled} unchanged; ensure references to
isVisuallyDisabled remain for visual styling only and do not change ARIA
semantics.
---
Duplicate comments:
In `@src/pages/home/components/product/IntroSection/IntroSection.tsx`:
- Around line 19-20: The comment above IntroSection needs to be split into lines
under 80 characters and use the actual prop name; replace the long comment that
mentions mediaUrl with two or three short lines referencing bannerUrl and the
behavior (keep fallback src on load failure and resync when parent provides a
new bannerUrl), updating the comment near the IntroSection/IntroSection.tsx code
to match the component's prop name and 80-column guideline.
In `@src/pages/home/components/product/ProductTab.tsx`:
- Around line 306-311: The click handler handleDecorateWithProductsClick
currently only notifies when selectedProducts.length === 0 and otherwise does
nothing, leaving the CTA as a dead-end; update the function to perform the
intended next step when products are selected (for example call the existing
flow entry such as openDecorateModal(), navigate to the decorate route, or
invoke a prop callback like onDecorateSelected(selectedProducts)), or
alternatively ensure the CTA is disabled/hidden unless a real action is wired;
also apply the same fix to the duplicate handler instance mentioned (the second
occurrence around the other handler block).
In `@src/pages/home/components/product/SearchSection/SearchSection.tsx`:
- Around line 213-215: 목업 카드가 아직 실제 플로우에 연결되지 않아 사용자에게 반응 없는 저장 버튼이나 잘못된 링크로 이동을
유발하므로, 목업 카드 렌더링 부분(현재 handleMockSaveToggle 및 handleSelectMockProduct 호출이 있는 블록과
288-295 영역)을 수정해 주세요: Product 카드에 전달하는 link/href props를 제거하거나 undefined로 설정하여
링크를 남기지 않고, handleMockSaveToggle는 실제 동작 대신 비활성 상태로 유지하되(버튼에 disabled 또는
aria-disabled 설정) 클릭 핸들러를 연결하지 않거나 no-op로 남겨 사용자 상호작용이 비인터랙티브하게 보이도록 변경하고,
handleSelectMockProduct도 네비게이션을 트리거하지 않도록 제거하거나 no-op로 바꿔 주세요.
In
`@src/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.css.ts`:
- Around line 177-186: The closeButton recipe currently duplicates a button
padding reset (padding: 0) which should be removed to avoid repeating global
button resets; edit the closeButton base block to delete the padding property
and leave only positioning/alignment rules (position, zIndex, display,
alignItems, justifyContent, borderRadius) so the component relies on the shared
global button reset and keeps styles consistent and reusable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 52557036-0428-4731-9878-45a36e1f71b5
📒 Files selected for processing (8)
src/pages/home/components/product/IntroSection/IntroSection.tsxsrc/pages/home/components/product/ProductFilterSheet/ProductFilterSheet.tsxsrc/pages/home/components/product/ProductTab.tsxsrc/pages/home/components/product/SearchSection/SearchSection.tsxsrc/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.css.tssrc/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.tsxsrc/shared/components/v2/button/actionButton/ActionButton.tsxsrc/shared/components/v2/productCard/ProductCard.css.ts
| const ProductFilterSheet = forwardRef<ProductFilterSheetRef>( | ||
| function ProductFilterSheet(_props, ref) { |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In React, should a forwardRefcomponent setdisplayName explicitly for better DevTools and stack traces?
💡 Result:
Yes, for a forwardRef component, you should explicitly set the displayName property on the component returned by forwardRef (or name the render function) to improve readability in React DevTools and debugging stack traces. Without it, the component appears as "ForwardRef" or "Anonymous", making it harder to identify. Example: const MyComponent = React.forwardRef((props, ref) => { return
Citations:
- 1: https://reactjs.org/docs/forwarding-refs.html
- 2: https://react.dev/reference/react/forwardRef
- 3: https://julesblom.com/writing/component-displayname
- 4: displayName not supported in combination with forwardRef in "React Developer Tools" facebook/react#13703
- 5: https://stevekinney.com/courses/react-typescript/forwardref-memo-and-displayname
- 6: https://stackoverflow.com/questions/67992894/component-definition-is-missing-display-name-for-forwardref
- 7: devtools: Remove ForwardRef/Memo from display name if
displayNameis set facebook/react#21952 - 8: https://github.com/jsx-eslint/eslint-plugin-react/blob/HEAD/docs/rules/display-name.md
🏁 Script executed:
fd -t f ProductFilterSheet.tsxRepository: TEAM-HOUME/HOUME-CLIENT
Length of output: 142
🏁 Script executed:
cat -n src/pages/home/components/product/ProductFilterSheet/ProductFilterSheet.tsx | sed -n '75,85p'Repository: TEAM-HOUME/HOUME-CLIENT
Length of output: 608
🏁 Script executed:
cat -n src/pages/home/components/product/ProductFilterSheet/ProductFilterSheet.tsx | sed -n '215,225p'Repository: TEAM-HOUME/HOUME-CLIENT
Length of output: 151
🏁 Script executed:
grep -n "displayName" src/pages/home/components/product/ProductFilterSheet/ProductFilterSheet.tsxRepository: TEAM-HOUME/HOUME-CLIENT
Length of output: 49
forwardRef 컴포넌트에 displayName을 명시해 주세요.
현재 코드는 정상 작동하지만, DevTools와 에러 스택 추적에서 컴포넌트명이 명확하게 표시되지 않습니다. 팀 가이드라인에 따라 명시적으로 설정이 필요합니다.
🏷️ 제안 수정안
const ProductFilterSheet = forwardRef<ProductFilterSheetRef>(
function ProductFilterSheet(_props, ref) {
// 구현부
}
);
+ProductFilterSheet.displayName = 'ProductFilterSheet';
+
export default ProductFilterSheet;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/home/components/product/ProductFilterSheet/ProductFilterSheet.tsx`
around lines 79 - 80, forwardRef로 선언된 ProductFilterSheet 컴포넌트에 displayName이 빠져
있어 DevTools/스택 추적에 이름이 나오지 않습니다; ProductFilterSheet(및 필요한 경우
ProductFilterSheetRef 표기)를 정의한 후에 ProductFilterSheet.displayName =
'ProductFilterSheet'를 추가하여 명시적으로 컴포넌트명을 설정해 주세요. 이 변경은
forwardRef<ProductFilterSheetRef>(function ProductFilterSheet...) 바로 아래나 파일 하단에
추가하면 됩니다.
| const handleFilterChipClick = useCallback( | ||
| (category: ProductFilterChipCategory) => { | ||
| setChipSelected((prev) => { | ||
| if (prev[category]) { | ||
| queueMicrotask(() => { | ||
| setFilterSheetOpen(false); | ||
| }); | ||
| return { ...INITIAL_CHIP_SELECTED }; | ||
| } | ||
|
|
||
| queueMicrotask(() => { | ||
| setFilterSheetOpen(true); | ||
| }); | ||
| return { | ||
| furniture: category === 'furniture', | ||
| price: category === 'price', | ||
| color: category === 'color', | ||
| }; | ||
| }); | ||
| }, | ||
| [] | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does React guarantee that a useState functional updater runs synchronously and exactly once, or should updater functions remain pure without side effects?
💡 Result:
React does not guarantee that a useState functional updater runs synchronously. The updater function is queued by React and executed asynchronously during the next render phase, after the current event handler completes. This is due to React's batching mechanism, where multiple state updates are processed together to optimize performance. However, React does guarantee that each queued updater function runs exactly once during the render (though in Strict Mode during development, it runs twice to detect impurities, discarding the second result—this does not affect production). Updater functions must remain pure without side effects. They should only compute and return the next state based on the provided previous state. Side effects (e.g., API calls, dispatches, mutations) violate React principles, can interfere with rendering, batching, and Strict Mode double-invocation detection, and lead to unpredictable behavior. Side effects belong in useEffect or event handlers outside the updater. Official React documentation explicitly states: "It must be pure, should take the pending state as its only argument, and should return the next state." and "Updater functions run during rendering, so updater functions must be pure and only return the result. Don't try to set state from inside of them or run other side effects."
Citations:
- 1: https://react.dev/reference/react/useState
- 2: https://react.dev/learn/queueing-a-series-of-state-updates
- 3: https://stackoverflow.com/questions/71780110/is-it-okey-to-use-side-effects-in-the-usestate-hook-callback
🏁 Script executed:
# Check the full context of ProductTab.tsx to verify the secondary issues
fd ProductTab.tsx -x wc -l {}Repository: TEAM-HOUME/HOUME-CLIENT
Length of output: 121
🏁 Script executed:
# Extract lines around 280-295 to verify the attemptedOverMax issue
fd ProductTab.tsx -x sed -n '270,300p' {}Repository: TEAM-HOUME/HOUME-CLIENT
Length of output: 998
🏁 Script executed:
# Also check the CTA/primary button implementation mentioned in the comment
fd ProductTab.tsx -x sed -n '306,350p' {}Repository: TEAM-HOUME/HOUME-CLIENT
Length of output: 1510
setState 업데이터 함수는 순수 함수로 유지해 주세요.
업데이터 내부에서 queueMicrotask를 통해 다른 state를 변경하고 있으며(217-238줄), 또한 외부 클로저 변수 attemptedOverMax를 업데이터 내에서 mutate한 뒤 외부에서 바로 읽고 있습니다(280-295줄). React는 업데이터 함수의 동기 실행을 보장하지 않으며, 여러 상태 업데이트를 배치 처리하는 메커니즘을 사용합니다. 이러한 패턴은 바텀시트 토글이나 최대 선택 토스트가 환경과 React 버전에 따라 불안정하게 동작할 수 있습니다.
현재 state를 기준으로 조건(isClosing, isAtLimit)을 먼저 계산한 후, 그 결과에 따라 setState와 notify 호출을 분리하여 처리하세요.
- 217-238줄:
setChipSelected업데이터에서setFilterSheetOpen호출 제거 - 280-295줄:
attemptedOverMax플래그 계산을 업데이터 외부로 이동 - 306-311줄:
handleDecorateWithProductsClick이 선택된 상품이 있을 때도 실제 동작을 수행하도록 구현 필요
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/home/components/product/ProductTab.tsx` around lines 217 - 238, The
updater passed to setChipSelected (in handleFilterChipClick) must remain pure:
remove queueMicrotask calls that call setFilterSheetOpen from inside the
updater; instead, compute the current conditions (isClosing / isAtLimit /
attemptedOverMax) synchronously outside the updater, call setChipSelected with a
plain state object, then call setFilterSheetOpen and any notify/toast actions
after setChipSelected based on those precomputed booleans. Also move the
attemptedOverMax calculation out of the updater so it is read/derived before
state updates, and update handleDecorateWithProductsClick to actually execute
its action even when products are already selected (don’t early-return based
solely on selection state). Use the existing symbols handleFilterChipClick,
setChipSelected, setFilterSheetOpen, attemptedOverMax, and
handleDecorateWithProductsClick to locate and apply these changes.
| <div className={styles.addCardSquare}> | ||
| <span className={styles.addCardContent} aria-hidden> | ||
| <Icon name="PlusFill" size="20" /> | ||
| <p className={styles.addLabel}>상품 추가하기</p> | ||
| </span> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Is a
element allowed inside ain HTML, and can invalid nesting trigger ReactvalidateDOMNesting warnings?
💡 Result:
No, a <p> element is not allowed inside a <span> element in HTML. The <span> element's content model is phrasing content, which excludes block-level elements like <p> (flow content). This violates HTML5 specifications. Yes, this invalid nesting will trigger React's validateDOMNesting warning, as React enforces HTML content model rules during rendering to prevent invalid DOM structures.
Citations:
- 1: https://html.spec.whatwg.org/C/grouping-content.html
- 2: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span
- 3: https://stackoverflow.com/questions/18492256/displaying-an-element-similar-to-p-inside-a-span
- 4: https://forum.bootstrapstudio.io/t/unable-to-add-p-tags-inside-a-span-tag/6149
- 5: https://stackoverflow.com/questions/51378696/why-p-nested-in-span-leads-for-a-block-element
- 6: https://www.mintlify.com/DanielVillalbaD/HTML-tags-checker/concepts/html-nesting-rules
- 7: https://deepscan.io/docs/rules/react-invalid-dom-nesting/
<span> 안에 <p> 태그를 중첩하지 마세요.
<span>의 콘텐츠 모델은 프레이징 콘텐츠(phrasing content)만 허용하므로, 블록 레벨 요소인 <p>를 자식으로 가질 수 없습니다. 이는 HTML5 사양 위반이며 React의 validateDOMNesting 경고를 발생시킵니다. 래퍼를 <div>로 변경하세요.
제안 수정안
- <span className={styles.addCardContent} aria-hidden>
+ <div className={styles.addCardContent} aria-hidden>
<Icon name="PlusFill" size="20" />
<p className={styles.addLabel}>상품 추가하기</p>
- </span>
+ </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.
| <div className={styles.addCardSquare}> | |
| <span className={styles.addCardContent} aria-hidden> | |
| <Icon name="PlusFill" size="20" /> | |
| <p className={styles.addLabel}>상품 추가하기</p> | |
| </span> | |
| <div className={styles.addCardSquare}> | |
| <div className={styles.addCardContent} aria-hidden> | |
| <Icon name="PlusFill" size="20" /> | |
| <p className={styles.addLabel}>상품 추가하기</p> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/pages/home/components/product/SelectedProductSheet/SelectedProductSheet.tsx`
around lines 109 - 113, The wrapper currently uses a <span>
(className={styles.addCardContent}, aria-hidden) but contains a <p> which is
invalid; change the <span> to a <div> so phrasing/flow content rules are
respected—update the element around Icon and the <p> in SelectedProductSheet
(the node with className addCardContent) to a <div>, preserve the className and
aria-hidden attribute (and any styling) so layout and accessibility behavior
remain the same.
| disabled={isDomDisabled} | ||
| aria-disabled={isVisuallyDisabled || undefined} |
There was a problem hiding this comment.
aria-disabled와 실제 동작 상태가 불일치합니다.
Line 69에서는 클릭 가능한 버튼인데, Line 70에서 aria-disabled가 true가
될 수 있어 보조기기에 “비활성”으로 전달됩니다. 접근성 의미와 실제 동작을
일치시켜야 합니다.
수정 제안
- disabled={isDomDisabled}
- aria-disabled={isVisuallyDisabled || undefined}
+ disabled={isDomDisabled}
+ aria-disabled={isDomDisabled || undefined}As per coding guidelines src/**/components/**/*.tsx: "접근성 준수: ARIA 속성, label 연결, 키보드 포커스".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/components/v2/button/actionButton/ActionButton.tsx` around lines
69 - 70, ARIA state and the actual disabled behavior are inconsistent:
ActionButton sets disabled using isDomDisabled but may set aria-disabled from
isVisuallyDisabled, causing assistive tech to think the button is disabled when
it is still interactive; update the aria-disabled usage in ActionButton.tsx so
it reflects the real interactive state (use isDomDisabled as the source of
truth, e.g., only set aria-disabled when the DOM is actually disabled) and keep
disabled={isDomDisabled} unchanged; ensure references to isVisuallyDisabled
remain for visual styling only and do not change ARIA semantics.
빌드 결과빌드 성공 🎊 |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/pages/home/components/product/SearchSection/SearchSection.tsx (1)
287-294:⚠️ Potential issue | 🟠 Major목업 인터랙션은 비활성으로 내려주세요.
지금
link.href가example.com이고save.onToggle도 noop이라 카드와 팝업에서 둘 다 가짜 동작이 노출됩니다. 실제 연결 전이라면link를 생략하고 저장 액션도 비활성 상태로 내려서 dead-end CTA를 막는 편이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/home/components/product/SearchSection/SearchSection.tsx` around lines 287 - 294, The current card is exposing mock interactions: remove the fake navigation and disable the mock save action by omitting the link prop entirely (remove the link={{ href: 'https://example.com' }} passed to the card) and change the save prop so it represents a disabled state (keep save.count as saveCount but remove or null out save.onToggle and ensure save.isSaved reflects a non-interactive state) instead of passing handleMockSaveToggle; refer to the save prop and handleMockSaveToggle in SearchSection to make these updates.src/shared/components/v2/productCard/ProductCard.tsx (1)
29-42:⚠️ Potential issue | 🟠 Major
shopping카드의 액션 계약을 타입으로 고정해 주세요.지금 타입이면
cardType="shopping"인데shoppingAction없이도 하단 CTA와 팝업 confirm이 활성 상태로 렌더링될 수 있습니다.cardType별 discriminated union으로shopping분기에서shoppingAction을 필수로 만들거나, 누락 시 disabled 처리로 막아 주세요.As per coding guidelines,
src/shared/components/**: 공통 컴포넌트 리뷰 시 높은 재사용성을 위한 props 설계.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/components/v2/productCard/ProductCard.tsx` around lines 29 - 42, The ProductCardProps currently allows cardType="shopping" without enforcing shoppingAction, which can render active CTA/confirm without a handler; change the props to a discriminated union so the 'shopping' branch requires shoppingAction (e.g., ProductCardProps = { cardType: 'shopping'; shoppingAction: { label?: string; onClick: () => void; disabled?: boolean } } | { cardType?: Exclude<CardType, 'shopping'>; shoppingAction?: never }) and update the component (ProductCard) to use the discriminant (cardType) when rendering CTA/confirm; alternatively, if you prefer not to change the type shape, ensure ProductCard disables/hides shopping CTA and confirm whenever shoppingAction is missing by checking shoppingAction before enabling click handlers and UI.
🤖 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/shared/components/v2/popup/Popup.tsx`:
- Around line 11-22: The Popup component lacks an accessible name for the
dialog; add an optional prop (e.g., title?: string or ariaLabel?: string) to
PopupProps and wire it into the dialog element so that when provided it sets
aria-labelledby (preferable) or aria-label on the element with role="dialog"
(create a stable header id if using aria-labelledby). Update the Popup render
logic (the component that uses PopupProps and the element with role="dialog") to
render a visible title element when title is supplied and ensure its id is
referenced by aria-labelledby, or fall back to aria-label when only ariaLabel is
provided.
In `@src/shared/components/v2/productCard/ProductCard.css.ts`:
- Around line 257-300: Several popup preview styles (popupPreviewBrand,
popupPreviewOriginalPrice, popupPreviewDiscountRow, popupPreviewDiscountRate,
popupPreviewDiscountPrice) duplicate common typography and layout rules; extract
those shared properties into a single base style (e.g., popupPreviewBase or
popupPreviewTextBase) and then refactor each of the listed styles to
spread/compose that base and only override the differing props (like
textDecoration, display/WebkitLineClamp, gap, color, font variants). Ensure you
reference and reuse the existing fontVars and colorVars in the new base so only
the unique fields remain in popupPreviewBrand, popupPreviewOriginalPrice,
popupPreviewDiscountRow, popupPreviewDiscountRate, and
popupPreviewDiscountPrice.
In `@src/shared/components/v2/productCard/ProductCard.tsx`:
- Around line 74-84: The popup confirm button ignores shoppingAction.disabled;
update handleShoppingViewDetailClick (which calls overlay.open and renders
Popup) to pass a disabled/confirmDisabled prop (e.g.,
confirmDisabled={shoppingAction?.disabled}) into Popup and ensure the Popup
component respects that prop by disabling the confirm CTA and
preventing/ignoring onConfirm when true; also keep passing
onCancel={save.onToggle} and ensure onConfirm only invokes
shoppingAction.onClick() and unmount if not disabled.
---
Duplicate comments:
In `@src/pages/home/components/product/SearchSection/SearchSection.tsx`:
- Around line 287-294: The current card is exposing mock interactions: remove
the fake navigation and disable the mock save action by omitting the link prop
entirely (remove the link={{ href: 'https://example.com' }} passed to the card)
and change the save prop so it represents a disabled state (keep save.count as
saveCount but remove or null out save.onToggle and ensure save.isSaved reflects
a non-interactive state) instead of passing handleMockSaveToggle; refer to the
save prop and handleMockSaveToggle in SearchSection to make these updates.
In `@src/shared/components/v2/productCard/ProductCard.tsx`:
- Around line 29-42: The ProductCardProps currently allows cardType="shopping"
without enforcing shoppingAction, which can render active CTA/confirm without a
handler; change the props to a discriminated union so the 'shopping' branch
requires shoppingAction (e.g., ProductCardProps = { cardType: 'shopping';
shoppingAction: { label?: string; onClick: () => void; disabled?: boolean } } |
{ cardType?: Exclude<CardType, 'shopping'>; shoppingAction?: never }) and update
the component (ProductCard) to use the discriminant (cardType) when rendering
CTA/confirm; alternatively, if you prefer not to change the type shape, ensure
ProductCard disables/hides shopping CTA and confirm whenever shoppingAction is
missing by checking shoppingAction before enabling click handlers and UI.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4769273d-2365-4fcd-beec-36313c15145d
📒 Files selected for processing (5)
src/pages/home/components/product/SearchSection/SearchSection.tsxsrc/shared/components/v2/popup/Popup.css.tssrc/shared/components/v2/popup/Popup.tsxsrc/shared/components/v2/productCard/ProductCard.css.tssrc/shared/components/v2/productCard/ProductCard.tsx
| interface PopupProps { | ||
| content?: ReactNode; | ||
| onCancel: () => void; | ||
| onConfirm: () => void; | ||
| onClose: () => void; | ||
| btnStyle: PopupBtnStyle; | ||
| btnText: string; | ||
| weakBtnText?: string; | ||
| btnIcon?: IconName; | ||
| showCloseButton?: boolean; | ||
| sideIconName?: IconName; | ||
| } |
There was a problem hiding this comment.
다이얼로그에 접근 가능한 이름이 없습니다.
role="dialog"와 aria-modal만 있고 aria-label/aria-labelledby가 없어 스크린리더에서는 무명 dialog로 읽힙니다. shared popup이면 title 또는 ariaLabel prop을 받아 연결해 두는 편이 안전합니다.
As per coding guidelines, src/shared/components/**: 공통 컴포넌트 리뷰 시 접근성(a11y) 준수.
Also applies to: 81-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/components/v2/popup/Popup.tsx` around lines 11 - 22, The Popup
component lacks an accessible name for the dialog; add an optional prop (e.g.,
title?: string or ariaLabel?: string) to PopupProps and wire it into the dialog
element so that when provided it sets aria-labelledby (preferable) or aria-label
on the element with role="dialog" (create a stable header id if using
aria-labelledby). Update the Popup render logic (the component that uses
PopupProps and the element with role="dialog") to render a visible title element
when title is supplied and ensure its id is referenced by aria-labelledby, or
fall back to aria-label when only ariaLabel is provided.
| export const popupPreviewBrand = style({ | ||
| ...fontVars.font.caption_r_12, | ||
| overflow: 'hidden', | ||
| textOverflow: 'ellipsis', | ||
| whiteSpace: 'nowrap', | ||
| color: colorVars.color.text.tertiary, | ||
| }); | ||
|
|
||
| export const popupPreviewTitle = style({ | ||
| ...fontVars.font.body_r_14, | ||
| display: '-webkit-box', | ||
| overflow: 'hidden', | ||
| wordBreak: 'break-all', | ||
| color: colorVars.color.text.primary, | ||
| WebkitLineClamp: 2, | ||
| WebkitBoxOrient: 'vertical', | ||
| }); | ||
|
|
||
| export const popupPreviewPriceSection = style({ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| paddingTop: unitVars.unit.gapPadding['100'], | ||
| }); | ||
|
|
||
| export const popupPreviewOriginalPrice = style({ | ||
| ...fontVars.font.caption_r_11, | ||
| textDecoration: 'line-through', | ||
| color: colorVars.color.text.tertiary, | ||
| }); | ||
|
|
||
| export const popupPreviewDiscountRow = style({ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: '0.1rem', | ||
| }); | ||
|
|
||
| export const popupPreviewDiscountRate = style({ | ||
| ...fontVars.font.title_sb_15, | ||
| color: colorVars.color.text.brand, | ||
| }); | ||
|
|
||
| export const popupPreviewDiscountPrice = style({ | ||
| ...fontVars.font.title_sb_15, | ||
| color: colorVars.color.text.primary, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
카드/팝업 가격·텍스트 스타일이 중복됩니다.
popupPreviewBrand, popupPreviewOriginalPrice, popupPreviewDiscountRow, popupPreviewDiscountRate, popupPreviewDiscountPrice가 기존 카드 스타일과 거의 동일해서 한쪽만 수정될 때 쉽게 드리프트가 납니다. 공통 base 스타일로 묶고 필요한 차이만 덧씌우는 쪽이 유지보수에 더 안전합니다.
As per coding guidelines, src/**/*.css.ts: 불필요한 중복 스타일 제거.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/components/v2/productCard/ProductCard.css.ts` around lines 257 -
300, Several popup preview styles (popupPreviewBrand, popupPreviewOriginalPrice,
popupPreviewDiscountRow, popupPreviewDiscountRate, popupPreviewDiscountPrice)
duplicate common typography and layout rules; extract those shared properties
into a single base style (e.g., popupPreviewBase or popupPreviewTextBase) and
then refactor each of the listed styles to spread/compose that base and only
override the differing props (like textDecoration, display/WebkitLineClamp, gap,
color, font variants). Ensure you reference and reuse the existing fontVars and
colorVars in the new base so only the unique fields remain in popupPreviewBrand,
popupPreviewOriginalPrice, popupPreviewDiscountRow, popupPreviewDiscountRate,
and popupPreviewDiscountPrice.
| const handleShoppingViewDetailClick = useCallback(() => { | ||
| overlay.open(({ unmount }) => ( | ||
| <Popup | ||
| btnStyle="solid" | ||
| btnText={shoppingAction?.label ?? '선택'} | ||
| onClose={unmount} | ||
| onCancel={save.onToggle} | ||
| onConfirm={() => { | ||
| shoppingAction?.onClick(); | ||
| unmount(); | ||
| }} |
There was a problem hiding this comment.
팝업 확인 버튼이 shoppingAction.disabled를 무시합니다.
리스트 하단 CTA는 shoppingAction?.disabled를 반영하지만, 상세 팝업의 confirm은 항상 활성입니다. 이미 선택된 상품도 팝업 경로에서는 다시 CTA를 누를 수 있으니, 이 경로에도 동일한 disabled 상태를 전달하도록 Popup API를 확장하는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/components/v2/productCard/ProductCard.tsx` around lines 74 - 84,
The popup confirm button ignores shoppingAction.disabled; update
handleShoppingViewDetailClick (which calls overlay.open and renders Popup) to
pass a disabled/confirmDisabled prop (e.g.,
confirmDisabled={shoppingAction?.disabled}) into Popup and ensure the Popup
component respects that prop by disabling the confirm CTA and
preventing/ignoring onConfirm when true; also keep passing
onCancel={save.onToggle} and ensure onConfirm only invokes
shoppingAction.onClick() and unmount if not disabled.
📌 Summary
해당 PR에 대한 작업 내용을 요약하여 작성해주세요.
📄 Tasks
해당 PR에 수행한 작업을 작성해주세요.
🔍 To Reviewer
리뷰어에게 요청하는 내용을 작성해주세요.
📸 Screenshot
작업한 내용에 대한 스크린샷을 첨부해주세요.