Skip to content

๐ŸŒ€ FEATURE : #99 ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„#101

Open
sminha wants to merge 16 commits into
developfrom
feat/#99/responsive
Open

๐ŸŒ€ FEATURE : #99 ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„#101
sminha wants to merge 16 commits into
developfrom
feat/#99/responsive

Conversation

@sminha

@sminha sminha commented May 4, 2026

Copy link
Copy Markdown
Owner

๐ŸŒ€ Related Issue

๐Ÿ’ฌ Description

๐Ÿ“น Screenshot

๐Ÿ“‘ Notes

Summary by CodeRabbit

๋ฆด๋ฆฌ์Šค ๋…ธํŠธ

  • New Features

    • ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด ์ถ”๊ฐ€
    • ๊ตฌ๋งค ๋ชฉ๋ก ๋ชจ๋ฐ”์ผ ์ „์šฉ ์นด๋“œ ๊ธฐ๋ฐ˜ ๋ ˆ์ด์•„์›ƒ ์ถ”๊ฐ€
    • ํ•„ํ„ฐ ๋ฐ ์ •๋ ฌ ๊ธฐ๋Šฅ ๊ฐœ์„  (๋ชจ๋ฐ”์ผ/๋ฐ์Šคํฌํ†ฑ ๋ถ„๊ธฐ UI)
    • ๋“œ๋กœ์–ด ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
    • ๊ฒ€์ƒ‰ ์•„์ด์ฝ˜์ด ํฌํ•จ๋œ ์ž…๋ ฅ ํ•„๋“œ ์ง€์›
  • Style

    • ํ—ค๋” ๋ ˆ์ด์•„์›ƒ ๋ฐ ๋กœ๊ณ  ํ…์ŠคํŠธ ๋ฐ˜์‘ํ˜• ์Šคํƒ€์ผ ๊ฐœ์„ 
    • ๋ฒ„ํŠผ ๋ฐ ํ† ๊ธ€ ๊ทธ๋ฃน ํฌ๊ธฐ ๋ฐ ๋ ˆ์ด์•„์›ƒ ์กฐ์ •
    • ์ƒˆ๋กœ์šด ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค ์ถ”๊ฐ€

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
jumble Ready Ready Preview, Comment May 4, 2026 7:34am

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
๐Ÿ“ Walkthrough

Walkthrough

๋ฐ˜์‘ํ˜• UI๋ฅผ ์ง€์›ํ•˜๋Š” ๋ ˆ์ด์•„์›ƒ ๋ฐ ์ปดํฌ๋„ŒํŠธ ๊ฐœ์„ ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. Drawer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋ชจ๋ฐ”์ผ/๋ฐ์Šคํฌํ†ฑ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ํ†ตํ•ด ๊ตฌ๋งค ๋ชฉ๋ก ๋ฐ ์‹ ๊ทœ ๊ตฌ๋งค ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜์‘ํ˜•์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•ฉ๋‹ˆ๋‹ค. ํ•„ํ„ฐ๋ง UI๋ฅผ ์žฌ์„ค๊ณ„ํ•˜๊ณ  ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.

Changes

๋ฐ˜์‘ํ˜• UI ๊ตฌํ˜„

Layer / File(s) Summary
์˜์กด์„ฑ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€
client/package.json, client/src/components/drawer/Drawer.tsx, client/src/components/drawer/index.ts, client/src/components/index.ts
vaul ๋“œ๋กœ์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ์Šคํƒ€์ผ๋ง๋œ ๋ž˜ํผ ์ปดํฌ๋„ŒํŠธ(Drawer, DrawerTrigger, DrawerContent, DrawerOverlay ๋“ฑ)๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ ์ธ๋ฑ์Šค์—์„œ ์žฌ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค.
ํƒ€์ž… ๋ฐ ์ƒ์ˆ˜ ์ •์˜
client/src/pages/purchases/list/types/search.ts, client/src/pages/purchases/list/constants/result.ts
๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ(SearchProps)๊ณผ ๊ตฌ๋งค ๋ชฉ๋ก ๊ฒฐ๊ณผ(ํŽ˜์ด์ง€ ํฌ๊ธฐ, ์ •๋ ฌ ๋ ˆ์ด๋ธ”, ํ…Œ์ด๋ธ” ํ—ค๋”)์— ๋Œ€ํ•œ ํƒ€์ž… ๋ฐ ์ƒ์ˆ˜๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ๊ฐœ์„ 
client/src/components/Header/BaseHeader.tsx, client/src/components/Header/Header.tsx, client/src/components/button/Button.tsx, client/src/components/input/Input.tsx, client/src/components/product-table/ProductTable.tsx, client/src/styles/tokens/typography.css
๋ฐ˜์‘ํ˜• ํŒจ๋”ฉ(clamp), ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด ํ† ๊ธ€(์ƒํƒœ ๊ด€๋ฆฌ ๋ฐ ์™ธ๋ถ€ ํด๋ฆญ ๋‹ซ๊ธฐ), ๊ฒ€์ƒ‰ ์•„์ด์ฝ˜ ์ง€์›, ํผ ์ž…๋ ฅ ๋ณ€ํ™˜ ๊ฐœ์„ , ์ƒˆ๋กœ์šด ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
Purchase List ๋ชจ๋ฐ”์ผ ํ•„ํ„ฐ๋ง UI
client/src/pages/purchases/list/components/FilterBar.tsx, client/src/pages/purchases/list/components/FilterChip.tsx, client/src/pages/purchases/list/components/FilterDrawer.tsx
ํ•„ํ„ฐ ์นฉ(ํ™œ์„ฑ/๋น„ํ™œ์„ฑ ์Šคํƒ€์ผ), ํ•„ํ„ฐ๋ฐ”(๊ธฐ๊ฐ„ ๋ผ๋ฒจ ํฌ๋งคํŒ… ๋ฐ ์นฉ ๋ Œ๋”๋ง), ํ•„ํ„ฐ ๋“œ๋กœ์–ด(๋‚ ์งœ ๋ฒ”์œ„, ์ฃผ๊ธฐ ์„ ํƒ, ๋ฐฑ์˜ค๋” ์ฒดํฌ๋ฐ•์Šค)๋ฅผ ์ƒˆ๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
Purchase List ๋ชจ๋ฐ”์ผ ์นด๋“œ ๋ฐ ๋ฉ”๋‰ด
client/src/pages/purchases/list/components/KebabMenu.tsx, client/src/pages/purchases/list/components/PurchaseCard.tsx
์ผ€๋ฐฅ ๋ฉ”๋‰ด(์™ธ๋ถ€ ํด๋ฆญ์œผ๋กœ ๋‹ซ๊ธฐ), ๊ตฌ๋งค ์นด๋“œ(ํ™•์žฅ/์ถ•์†Œ ์ƒํƒœ, ์ƒ์„ธ ํ•„๋“œ, ๋ชจ๋‹ฌ ํŠธ๋ฆฌ๊ฑฐ)๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
Purchase List ํŽ˜์ด์ง€ ๋ฐ ์„น์…˜ ๋ ˆ์ด์•„์›ƒ
client/src/pages/purchases/list/PurchaseList.tsx, client/src/pages/purchases/list/sections/FilterSection.tsx, client/src/pages/purchases/list/sections/ResultSection.tsx, client/src/pages/purchases/list/components/AddonInput.tsx, client/src/components/product-table/ToggleGroup.tsx
๋ชจ๋ฐ”์ผ/๋ฐ์Šคํฌํ†ฑ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ์ถ”๊ฐ€ํ•˜๊ณ , ์ฆ‰์‹œ ๊ฒ€์ƒ‰(onInstantSearch) ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์Šคํฌํ†ฑ์€ ๊ธฐ์กด ํ…Œ์ด๋ธ” ๋ ˆ์ด์•„์›ƒ, ๋ชจ๋ฐ”์ผ์€ ์นด๋“œ ๊ธฐ๋ฐ˜ ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์ง€์›์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
Purchase New ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ
client/src/pages/purchases/new/PurchaseNew.tsx, client/src/pages/purchases/new/components/ProductCard.tsx, client/src/pages/purchases/new/components/UploadButton.tsx
๋ชจ๋ฐ”์ผ ๊ฐ์ง€(matchMedia)๋ฅผ ํ†ตํ•ด ์ƒํ’ˆ ์ถ”๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜์‘ํ˜•์œผ๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋ฐ”์ผ์€ ์นด๋“œ ๋ ˆ์ด์•„์›ƒ, ๋ฐ์Šคํฌํ†ฑ์€ ํ…Œ์ด๋ธ” ๋ ˆ์ด์•„์›ƒ์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ์กฐ์ • ๊ฐ€๋Šฅํ•œ ํŒŒ์ผ ์—…๋กœ๋“œ ID๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

Estimated code review effort

๐ŸŽฏ 3 (Moderate) | โฑ๏ธ ~25 minutes

Possibly related PRs

  • PR #60: Purchase ์ถ”๊ฐ€ ํŽ˜์ด์ง€ ๋ฐ ๊ณต์œ  ํผ ์ปดํฌ๋„ŒํŠธ(UploadButton, Input ๋™์ž‘, PurchaseNew ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ)์—์„œ ์ค‘๋ณต๋˜๋Š” ๋ณ€๊ฒฝ
  • PR #82: Purchase ๋ชฉ๋ก ResultSection ๋ฐ ๋ฐฑ์˜ค๋”/์˜์ˆ˜์ฆ ๋ชจ๋‹ฌ ์‚ฌ์šฉ(props ์—ฐ๊ฒฐ, ๋ชจ๋‹ฌ ๋ Œ๋”๋ง)์ด ๊ฒน์นจ
  • PR #76: Purchase ๋ชฉ๋ก UI ๋ฐ ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ(PurchaseList, FilterSection, ResultSection, AddonInput)์˜ ์ง์ ‘์ ์ธ ์ฝ”๋“œ ๋ ˆ๋ฒจ ๊ด€๋ จ์„ฑ
๐Ÿšฅ Pre-merge checks | โœ… 2 | โŒ 3

โŒ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check โ“ Inconclusive PR ์„ค๋ช…์ด ํ…œํ”Œ๋ฆฟ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅด์ง€๋งŒ Description, Screenshot, Notes ์„น์…˜์ด ๋น„์–ด์žˆ์–ด ๋ณ€๊ฒฝ์‚ฌํ•ญ์— ๋Œ€ํ•œ ์ƒ์„ธ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. Description ์„น์…˜์— ๊ตฌํ˜„๋œ ๋ฐ˜์‘ํ˜• ๊ธฐ๋Šฅ ๋ฐ ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž‘์„ฑํ•˜๊ณ , ํ•„์š”์‹œ Screenshot๊ณผ Notes๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.
Out of Scope Changes check โ“ Inconclusive ๋Œ€๋ถ€๋ถ„์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„๊ณผ ๊ด€๋ จ๋˜์ง€๋งŒ, Button ์Šคํƒ€์ผ ๋ณ€๊ฒฝ(border-primay-3 ์˜คํƒ€ ํฌํ•จ), ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ์ถ”๊ฐ€, vaul ํŒจํ‚ค์ง€ ์„ค์น˜ ๋“ฑ ์ผ๋ถ€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„์˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฉ๋‹ˆ๋‹ค. Button ์ปดํฌ๋„ŒํŠธ์˜ border-primay-3 ์˜คํƒ€ ์ˆ˜์ •, ๋ฐ˜์‘ํ˜•๊ณผ ๋ฌด๊ด€ํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ๋“ค์˜ ํ•„์š”์„ฑ์„ ์žฌ๊ฒ€ํ† ํ•˜๊ณ , ๋ฒ”์œ„๊ฐ€ ๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ๋ณ„๋„ ์ด์Šˆ๋กœ ๋ถ„๋ฆฌํ•ด์ฃผ์„ธ์š”.
โœ… Passed checks (2 passed)
Check name Status Explanation
Title check โœ… Passed PR ์ œ๋ชฉ์€ ์ด์Šˆ #99์˜ ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ ์ž‘์—…๊ณผ ์ง์ ‘ ๊ด€๋ จ๋˜์–ด ์žˆ์œผ๋ฉฐ, ๋ณ€๊ฒฝ์‚ฌํ•ญ์˜ ์ฃผ์š” ๋‚ด์šฉ์„ ๋ช…ํ™•ํžˆ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค.
Linked Issues check โœ… Passed PR์˜ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€์™€ ์ฝ”๋“œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ด์Šˆ #99์˜ ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•ฉ๋‹ˆ๋‹ค. Header, PurchaseList, FilterSection, ResultSection, PurchaseNew ๋“ฑ ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

โœ๏ธ Tip: You can configure your own custom pre-merge checks in the settings.

โœจ Finishing Touches
๐Ÿ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
๐Ÿงช Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#99/responsive

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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: 11

Caution

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

โš ๏ธ Outside diff range comments (1)
client/src/components/product-table/ProductTable.tsx (1)

213-267: โš ๏ธ Potential issue | ๐ŸŸก Minor | โšก Quick win

๊ณต๋ฐฑ์„ 0์œผ๋กœ ๋ฐ”๊พธ์ง€ ๋งˆ์„ธ์š”.

Number(value)๋Š” ์ž…๋ ฅ์„ ์ง€์šฐ๋ฉด 0์œผ๋กœ coercionํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ backorderQuantity๋Š” ๊ณต๋ž€์ด ์œ ํšจ๊ฐ’์ฒ˜๋Ÿผ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ, ๊ณต๋ฐฑ์€ undefined๋กœ ๋‚จ๊ธฐ๊ณ  ์ˆซ์ž์ผ ๋•Œ๋งŒ ๋ณ€ํ™˜ํ•˜๋Š” ์ชฝ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
-            setValueAs: (value: string) => Number(value),
+            setValueAs: (value: string) => (value === '' ? undefined : Number(value)),
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/product-table/ProductTable.tsx` around lines 213 - 267,
The current setValueAs handlers in ProductTable.tsx (used in register calls like
`products.${index}.price`, `products.${index}.quantity`, and
`products.${index}.backorderQuantity`) coerce empty input to 0 via
Number(value); change each handler to return undefined for empty strings and
only coerce when the value is non-empty (e.g., if value === '' return undefined
else return Number(value)) so blank inputs remain undefined rather than 0.
๐Ÿค– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/components/button/Button.tsx`:
- Line 11: Fix the typo in the Button component's style mapping: change the
value for the "primary" key in Button.tsx from 'border-primay-3 text-white
bg-primary-3' to use the correct class name 'border-primary-3 text-white
bg-primary-3' so the intended border utility is applied (look for the "primary"
property in the class mapping inside Button or Button.tsx).

In `@client/src/components/Header/Header.tsx`:
- Around line 51-62: The mobile menu is only height-collapsed which leaves links
focusable; change the render so the Menu is removed from the DOM when closed. In
Header.tsx, wrap or conditionally render the outer <div> and/or the <Menu>
component with the isMenuOpen boolean (e.g., render the div/Menu only when
isMenuOpen is true) instead of relying on the 'max-h-0' class; keep the existing
props (navClassName, ulClassName, onClick) intact so behavior and handlers
(setIsMenuOpen) remain unchanged.

In `@client/src/components/input/Input.tsx`:
- Around line 73-80: The search icon button in Input.tsx (rendered when
hasSearchIcon === true) is missing an explicit type, causing it to default to
type="submit" and potentially submit parent forms when clicked; update the
button rendered alongside the Search component (the element using onSearch) to
include type="button" to prevent unintended form submission.

In `@client/src/pages/purchases/list/components/FilterBar.tsx`:
- Around line 29-35: The filter-open button in FilterBar.tsx (the button element
with onClick={onOpenDrawer} and aria-label="ํ•„ํ„ฐ ์—ด๊ธฐ") lacks an explicit type and
will default to submit inside a form; update that button to include
type="button" to prevent accidental form submission when clicked.

In `@client/src/pages/purchases/list/components/FilterChip.tsx`:
- Around line 4-20: The FilterChip component currently defines FilterChipProps
extending React.ButtonHTMLAttributes but only uses onClick/children and doesn't
forward other button attributes or expose selection via ARIA; update
FilterChipProps to accept and pass through the remaining button props (e.g.,
...rest from FilterChipProps) and spread them onto the <button> so attributes
like disabled, type, and aria-* are preserved, keep existing onClick and
children handling, and add aria-pressed={isSelected} to the button (while
retaining className composition using CHIP_BASE, CHIP_ACTIVE, CHIP_INACTIVE and
cn) so selection state is programmatically exposed.

In `@client/src/pages/purchases/list/components/FilterDrawer.tsx`:
- Around line 6-24: The handleSearch in FilterDrawer always calls
onOpenChange(false) even when onSearch fails; change it to treat onSearch as a
boolean-returning function (or Promise<boolean>) and only call
onOpenChange(false) when onSearch indicates success (true). Update references to
onSearch in FilterDrawer (handleSearch) to await/inspect the returned value and
conditionally close the drawer, and coordinate with the parent
FilterSection.handleSearch to ensure it returns true/false on success/failure so
the drawer can rely on that result.
- Around line 63-68: The Input date max currently uses new
Date().toISOString().split('T')[0] (UTC) which can yield the wrong local date;
update the max calculation in the FilterDrawer component (the Input with
value={draft.endDate} and min={draft.startDate}) to compute today's date in
local time (e.g., build YYYY-MM-DD from new Date() using getFullYear(),
getMonth()+1, getDate() with zero-padding) and use that localToday string as the
max prop so the end date never exceeds the local current date and still respects
min={draft.startDate}.

In `@client/src/pages/purchases/list/components/KebabMenu.tsx`:
- Around line 13-21: The outside-click handler in useEffect currently listens
for 'mousedown' which misses touch events; update the event listener in the
effect (the handleClickOutside closure used with kebabRef and setIsKebabOpen) to
listen for 'pointerdown' instead of 'mousedown', and make the cleanup remove the
same 'pointerdown' listener so the add/remove pair matches; keep the existing
logic that checks kebabRef.current.contains and calls setIsKebabOpen(false).

In `@client/src/pages/purchases/list/PurchaseList.tsx`:
- Around line 13-17: handleInstantSearch currently merges partial into draft and
params but leaves the existing page intact; update handleInstantSearch so when
merging partial into setDraft and setParams you also set page: 1 (e.g.,
setDraft(prev => ({ ...prev, ...partial, page: 1 })) and setParams(prev => ({
...prev, ...partial, page: 1 }))) to ensure searches reset to the first page
when filters or keywords change; modify the function that calls setDraft and
setParams (handleInstantSearch) accordingly.

In `@client/src/pages/purchases/list/sections/ResultSection.tsx`:
- Around line 197-243: The onDelete prop currently passes a function reference
instead of invoking the delete handler, so the mutation never runs; in the
PurchaseCard instantiation inside the records.map change the prop from
onDelete={() => handleDeleteProduct} to call the handler with the current record
(e.g., onDelete={() => handleDeleteProduct(record)}) so handleDeleteProducts
(used inside handleDeleteProduct) is executed when the delete button is clicked.

In `@client/src/pages/purchases/new/components/ProductCard.tsx`:
- Around line 71-105: The current setValueAs handlers on
register(`products.${index}.price`), register(`products.${index}.quantity`), and
register(`products.${index}.backorderQuantity`) use Number(value) which converts
an empty string to 0; change each handler to only convert when the input is
non-empty (e.g. return undefined or the original empty value when value === ''),
otherwise return Number(value) for numeric strings so blank inputs remain blank
instead of becoming 0.

---

Outside diff comments:
In `@client/src/components/product-table/ProductTable.tsx`:
- Around line 213-267: The current setValueAs handlers in ProductTable.tsx (used
in register calls like `products.${index}.price`, `products.${index}.quantity`,
and `products.${index}.backorderQuantity`) coerce empty input to 0 via
Number(value); change each handler to return undefined for empty strings and
only coerce when the value is non-empty (e.g., if value === '' return undefined
else return Number(value)) so blank inputs remain undefined rather than 0.
๐Ÿช„ 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: c121d2dd-fb40-43ff-a9fe-a90a0bde2f4f

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 52b399b and 2f0190e.

โ›” Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml and included by none
๐Ÿ“’ Files selected for processing (25)
  • client/package.json
  • client/src/components/Header/BaseHeader.tsx
  • client/src/components/Header/Header.tsx
  • client/src/components/button/Button.tsx
  • client/src/components/drawer/Drawer.tsx
  • client/src/components/drawer/index.ts
  • client/src/components/index.ts
  • client/src/components/input/Input.tsx
  • client/src/components/product-table/ProductTable.tsx
  • client/src/pages/purchases/list/PurchaseList.tsx
  • client/src/pages/purchases/list/components/AddonInput.tsx
  • client/src/pages/purchases/list/components/FilterBar.tsx
  • client/src/pages/purchases/list/components/FilterChip.tsx
  • client/src/pages/purchases/list/components/FilterDrawer.tsx
  • client/src/pages/purchases/list/components/KebabMenu.tsx
  • client/src/pages/purchases/list/components/PurchaseCard.tsx
  • client/src/pages/purchases/list/components/ToggleGroup.tsx
  • client/src/pages/purchases/list/constants/result.ts
  • client/src/pages/purchases/list/sections/FilterSection.tsx
  • client/src/pages/purchases/list/sections/ResultSection.tsx
  • client/src/pages/purchases/list/types/search.ts
  • client/src/pages/purchases/new/PurchaseNew.tsx
  • client/src/pages/purchases/new/components/ProductCard.tsx
  • client/src/pages/purchases/new/components/UploadButton.tsx
  • client/src/styles/tokens/typography.css

const VARIANT_STYLE = {
white: 'border-gray-3 border text-gray-5 bg-white',
primary: 'text-white bg-primary-3',
primary: 'border-primay-3 text-white bg-primary-3',

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 | โšก Quick win

border-primay-3 ์˜คํƒ€๋ฅผ ์ˆ˜์ •ํ•˜์„ธ์š”.

primay ์˜คํƒ€ ๋•Œ๋ฌธ์— ์˜๋„ํ•œ border ํด๋ž˜์Šค๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ๋Š” border-primary-3๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
-  primary: 'border-primay-3 text-white bg-primary-3',
+  primary: 'border-primary-3 text-white bg-primary-3',
๐Ÿ“ 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
primary: 'border-primay-3 text-white bg-primary-3',
primary: 'border-primary-3 text-white bg-primary-3',
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/button/Button.tsx` at line 11, Fix the typo in the
Button component's style mapping: change the value for the "primary" key in
Button.tsx from 'border-primay-3 text-white bg-primary-3' to use the correct
class name 'border-primary-3 text-white bg-primary-3' so the intended border
utility is applied (look for the "primary" property in the class mapping inside
Button or Button.tsx).

Comment on lines +51 to +62
<div
className={cn(
'border-b-gray-1 absolute top-full right-0 left-0 z-50 overflow-hidden border-b bg-white transition-all duration-300',
isMenuOpen ? 'max-h-[20rem]' : 'max-h-0',
)}
>
<Menu
navClassName="px-[2rem] py-[1.6rem]"
ulClassName="flex-col gap-[1.6rem]"
onClick={() => setIsMenuOpen(false)}
/>
</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 | โšก Quick win

๋‹ซํžŒ ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด๋Š” DOM์—์„œ ์ˆจ๊ฒจ์ฃผ์„ธ์š”.

max-h-0๋งŒ ๋ฐ”๊พธ๋ฉด ๋งํฌ๊ฐ€ ์—ฌ์ „ํžˆ ํฌ์ปค์Šค ๊ฐ€๋Šฅํ•ด์„œ, ํ‚ค๋ณด๋“œ ํƒ์ƒ‰์ด๋‚˜ ์Šคํฌ๋ฆฐ๋ฆฌ๋”์—์„œ ๋ฉ”๋‰ด๊ฐ€ ๋‚จ์•„ ๋ณด์ž…๋‹ˆ๋‹ค. ๋‹ซํžŒ ์ƒํƒœ์—์„œ๋Š” ์•„์˜ˆ ๋ Œ๋”๋งํ•˜์ง€ ์•Š๋Š” ์ชฝ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
-        <div
-          className={cn(
-            'border-b-gray-1 absolute top-full right-0 left-0 z-50 overflow-hidden border-b bg-white transition-all duration-300',
-            isMenuOpen ? 'max-h-[20rem]' : 'max-h-0',
-          )}
-        >
-          <Menu
-            navClassName="px-[2rem] py-[1.6rem]"
-            ulClassName="flex-col gap-[1.6rem]"
-            onClick={() => setIsMenuOpen(false)}
-          />
-        </div>
+        {isMenuOpen && (
+          <div className="border-b-gray-1 absolute top-full right-0 left-0 z-50 border-b bg-white">
+            <Menu
+              navClassName="px-[2rem] py-[1.6rem]"
+              ulClassName="flex-col gap-[1.6rem]"
+              onClick={() => setIsMenuOpen(false)}
+            />
+          </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
<div
className={cn(
'border-b-gray-1 absolute top-full right-0 left-0 z-50 overflow-hidden border-b bg-white transition-all duration-300',
isMenuOpen ? 'max-h-[20rem]' : 'max-h-0',
)}
>
<Menu
navClassName="px-[2rem] py-[1.6rem]"
ulClassName="flex-col gap-[1.6rem]"
onClick={() => setIsMenuOpen(false)}
/>
</div>
{isMenuOpen && (
<div className="border-b-gray-1 absolute top-full right-0 left-0 z-50 border-b bg-white">
<Menu
navClassName="px-[2rem] py-[1.6rem]"
ulClassName="flex-col gap-[1.6rem]"
onClick={() => setIsMenuOpen(false)}
/>
</div>
)}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/Header/Header.tsx` around lines 51 - 62, The mobile
menu is only height-collapsed which leaves links focusable; change the render so
the Menu is removed from the DOM when closed. In Header.tsx, wrap or
conditionally render the outer <div> and/or the <Menu> component with the
isMenuOpen boolean (e.g., render the div/Menu only when isMenuOpen is true)
instead of relying on the 'max-h-0' class; keep the existing props
(navClassName, ulClassName, onClick) intact so behavior and handlers
(setIsMenuOpen) remain unchanged.

Comment on lines +73 to +80
{hasSearchIcon === true && (
<button
onClick={onSearch}
aria-label="๊ฒ€์ƒ‰"
className="text-gray-5 absolute top-1/2 right-[1rem] shrink-0 -translate-y-1/2"
>
<Search size={20} />
</button>

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 | โšก Quick win

๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์— type="button"์„ ์ง€์ •ํ•˜์„ธ์š”.

ํ˜„์žฌ ๊ธฐ๋ณธ ํƒ€์ž…์ด submit์ด๋ผ, ์ด ์ž…๋ ฅ์ด ํผ ์•ˆ์—์„œ ์“ฐ์ด๋ฉด ๊ฒ€์ƒ‰ ์•„์ด์ฝ˜ ํด๋ฆญ ์‹œ ๋ถ€๋ชจ form์ด ์˜๋„์น˜ ์•Š๊ฒŒ ์ œ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ •์•ˆ
       {hasSearchIcon === true && (
         <button
+          type="button"
           onClick={onSearch}
           aria-label="๊ฒ€์ƒ‰"
           className="text-gray-5 absolute top-1/2 right-[1rem] shrink-0 -translate-y-1/2"
         >
๐Ÿ“ 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
{hasSearchIcon === true && (
<button
onClick={onSearch}
aria-label="๊ฒ€์ƒ‰"
className="text-gray-5 absolute top-1/2 right-[1rem] shrink-0 -translate-y-1/2"
>
<Search size={20} />
</button>
{hasSearchIcon === true && (
<button
type="button"
onClick={onSearch}
aria-label="๊ฒ€์ƒ‰"
className="text-gray-5 absolute top-1/2 right-[1rem] shrink-0 -translate-y-1/2"
>
<Search size={20} />
</button>
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/input/Input.tsx` around lines 73 - 80, The search icon
button in Input.tsx (rendered when hasSearchIcon === true) is missing an
explicit type, causing it to default to type="submit" and potentially submit
parent forms when clicked; update the button rendered alongside the Search
component (the element using onSearch) to include type="button" to prevent
unintended form submission.

Comment on lines +29 to +35
<button
onClick={onOpenDrawer}
aria-label="ํ•„ํ„ฐ ์—ด๊ธฐ"
className="border-gray-2 text-gray-5 shrink-0 rounded-full border p-[0.8rem]"
>
<SlidersHorizontal size={16} />
</button>

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 | โšก Quick win

ํ•„ํ„ฐ ์—ด๊ธฐ ๋ฒ„ํŠผ์—๋„ type="button"์„ ๋ช…์‹œํ•˜์„ธ์š”.

์ด ๋ฒ„ํŠผ๋„ ๊ธฐ๋ณธ ํƒ€์ž…์ด submit์ด๋ผ, ์ƒ์œ„์— form์ด ์ƒ๊ธฐ๋ฉด ํ•„ํ„ฐ ์—ด๊ธฐ๋งŒ ๋ˆŒ๋Ÿฌ๋„ ์ œ์ถœ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ •์•ˆ
       <button
+        type="button"
         onClick={onOpenDrawer}
         aria-label="ํ•„ํ„ฐ ์—ด๊ธฐ"
         className="border-gray-2 text-gray-5 shrink-0 rounded-full border p-[0.8rem]"
       >
๐Ÿ“ 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
onClick={onOpenDrawer}
aria-label="ํ•„ํ„ฐ ์—ด๊ธฐ"
className="border-gray-2 text-gray-5 shrink-0 rounded-full border p-[0.8rem]"
>
<SlidersHorizontal size={16} />
</button>
<button
type="button"
onClick={onOpenDrawer}
aria-label="ํ•„ํ„ฐ ์—ด๊ธฐ"
className="border-gray-2 text-gray-5 shrink-0 rounded-full border p-[0.8rem]"
>
<SlidersHorizontal size={16} />
</button>
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/purchases/list/components/FilterBar.tsx` around lines 29 -
35, The filter-open button in FilterBar.tsx (the button element with
onClick={onOpenDrawer} and aria-label="ํ•„ํ„ฐ ์—ด๊ธฐ") lacks an explicit type and will
default to submit inside a form; update that button to include type="button" to
prevent accidental form submission when clicked.

Comment on lines +4 to +20
interface FilterChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick: () => void;
isSelected: boolean;
children: ReactNode;
}

const CHIP_BASE =
'font-14-m shrink-0 rounded-full px-[1.2rem] py-[0.6rem] transition-colors duration-150';
const CHIP_ACTIVE = 'bg-primary-3 text-white';
const CHIP_INACTIVE = 'bg-gray-1 text-gray-5';

export default function FilterChip({ onClick, isSelected, children }: FilterChipProps) {
return (
<button onClick={onClick} className={cn(CHIP_BASE, isSelected ? CHIP_ACTIVE : CHIP_INACTIVE)}>
{children}
</button>
);

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 | โšก Quick win

๋ฒ„ํŠผ ์†์„ฑ ์ „๋‹ฌ๊ณผ ์„ ํƒ ์ƒํƒœ ๋…ธ์ถœ์„ ๊ฐ™์ด ์ •๋ฆฌํ•˜์„ธ์š”.

์ง€๊ธˆ์€ ButtonHTMLAttributes๋ฅผ ์ƒ์†ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” onClick/children๋งŒ ์‚ฌ์šฉํ•ด์„œ disabled, type, aria-* ๊ฐ™์€ ํ‘œ์ค€ ๋ฒ„ํŠผ ์†์„ฑ์ด ๋ชจ๋‘ ๋ฒ„๋ ค์ง‘๋‹ˆ๋‹ค. ๋˜ ์„ ํƒ ์ƒํƒœ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ๋งŒ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์–ด์„œ aria-pressed๋กœ๋„ ๋…ธ์ถœํ•˜๋Š” ํŽธ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ˆ˜์ •์•ˆ
 interface FilterChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
-  onClick: () => void;
   isSelected: boolean;
   children: ReactNode;
 }
 
-export default function FilterChip({ onClick, isSelected, children }: FilterChipProps) {
+export default function FilterChip({
+  onClick,
+  isSelected,
+  children,
+  type = 'button',
+  ...buttonProps
+}: FilterChipProps) {
   return (
-    <button onClick={onClick} className={cn(CHIP_BASE, isSelected ? CHIP_ACTIVE : CHIP_INACTIVE)}>
+    <button
+      type={type}
+      aria-pressed={isSelected}
+      onClick={onClick}
+      className={cn(CHIP_BASE, isSelected ? CHIP_ACTIVE : CHIP_INACTIVE)}
+      {...buttonProps}
+    >
       {children}
     </button>
   );
 }
๐Ÿ“ 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
interface FilterChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick: () => void;
isSelected: boolean;
children: ReactNode;
}
const CHIP_BASE =
'font-14-m shrink-0 rounded-full px-[1.2rem] py-[0.6rem] transition-colors duration-150';
const CHIP_ACTIVE = 'bg-primary-3 text-white';
const CHIP_INACTIVE = 'bg-gray-1 text-gray-5';
export default function FilterChip({ onClick, isSelected, children }: FilterChipProps) {
return (
<button onClick={onClick} className={cn(CHIP_BASE, isSelected ? CHIP_ACTIVE : CHIP_INACTIVE)}>
{children}
</button>
);
interface FilterChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isSelected: boolean;
children: ReactNode;
}
const CHIP_BASE =
'font-14-m shrink-0 rounded-full px-[1.2rem] py-[0.6rem] transition-colors duration-150';
const CHIP_ACTIVE = 'bg-primary-3 text-white';
const CHIP_INACTIVE = 'bg-gray-1 text-gray-5';
export default function FilterChip({
onClick,
isSelected,
children,
type = 'button',
...buttonProps
}: FilterChipProps) {
return (
<button
type={type}
aria-pressed={isSelected}
onClick={onClick}
className={cn(CHIP_BASE, isSelected ? CHIP_ACTIVE : CHIP_INACTIVE)}
{...buttonProps}
>
{children}
</button>
);
}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/purchases/list/components/FilterChip.tsx` around lines 4 -
20, The FilterChip component currently defines FilterChipProps extending
React.ButtonHTMLAttributes but only uses onClick/children and doesn't forward
other button attributes or expose selection via ARIA; update FilterChipProps to
accept and pass through the remaining button props (e.g., ...rest from
FilterChipProps) and spread them onto the <button> so attributes like disabled,
type, and aria-* are preserved, keep existing onClick and children handling, and
add aria-pressed={isSelected} to the button (while retaining className
composition using CHIP_BASE, CHIP_ACTIVE, CHIP_INACTIVE and cn) so selection
state is programmatically exposed.

Comment on lines +63 to +68
<Input
type="date"
aria-label="์กฐํšŒ ์ข…๋ฃŒ์ผ"
min={draft.startDate}
max={new Date().toISOString().split('T')[0]}
value={draft.endDate}

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 | โšก Quick win

์ข…๋ฃŒ์ผ์˜ ์ตœ๋Œ€๊ฐ’์€ ๋กœ์ปฌ ์‹œ๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐํ•˜์„ธ์š”.

toISOString()์€ UTC๋ผ์„œ ์ผ๋ถ€ ํƒ€์ž„์กด์—์„œ๋Š” ์˜ค๋Š˜ ๋‚ ์งœ๊ฐ€ ํ•˜๋ฃจ ์–ด๊ธ‹๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
+  const today = new Date();
+  today.setMinutes(today.getMinutes() - today.getTimezoneOffset());
+  const todayIso = today.toISOString().split('T')[0];
...
-                max={new Date().toISOString().split('T')[0]}
+                max={todayIso}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/purchases/list/components/FilterDrawer.tsx` around lines 63
- 68, The Input date max currently uses new Date().toISOString().split('T')[0]
(UTC) which can yield the wrong local date; update the max calculation in the
FilterDrawer component (the Input with value={draft.endDate} and
min={draft.startDate}) to compute today's date in local time (e.g., build
YYYY-MM-DD from new Date() using getFullYear(), getMonth()+1, getDate() with
zero-padding) and use that localToday string as the max prop so the end date
never exceeds the local current date and still respects min={draft.startDate}.

Comment on lines +13 to +21
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (kebabRef.current && !kebabRef.current.contains(e.target as Node)) {
setIsKebabOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

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 | โšก Quick win

์™ธ๋ถ€ ํด๋ฆญ ๊ฐ์ง€๋Š” pointerdown์œผ๋กœ ๋ฐ”๊พธ์„ธ์š”.

ํ˜„์žฌ mousedown๋งŒ ๋“ฃ๊ณ  ์žˆ์–ด์„œ ํ„ฐ์น˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ”๊นฅ ํƒญ์œผ๋กœ ๋ฉ”๋‰ด๊ฐ€ ๋‹ซํžˆ์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜์‘ํ˜• UI๋ผ๋ฉด ๋งˆ์šฐ์Šค/ํ„ฐ์น˜๋ฅผ ๊ฐ™์ด ์ฒ˜๋ฆฌํ•˜๋Š” ์ชฝ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ •์•ˆ
-    document.addEventListener('mousedown', handleClickOutside);
-    return () => document.removeEventListener('mousedown', handleClickOutside);
+    document.addEventListener('pointerdown', handleClickOutside);
+    return () => document.removeEventListener('pointerdown', handleClickOutside);
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/purchases/list/components/KebabMenu.tsx` around lines 13 -
21, The outside-click handler in useEffect currently listens for 'mousedown'
which misses touch events; update the event listener in the effect (the
handleClickOutside closure used with kebabRef and setIsKebabOpen) to listen for
'pointerdown' instead of 'mousedown', and make the cleanup remove the same
'pointerdown' listener so the add/remove pair matches; keep the existing logic
that checks kebabRef.current.contains and calls setIsKebabOpen(false).

Comment on lines +13 to +17
const handleSearch = () => setParams(draft);
const handleInstantSearch = (partial: Partial<Draft>) => {
setDraft((prev) => ({ ...prev, ...partial }));
setParams((prev) => ({ ...prev, ...partial }));
};

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 | โšก Quick win

์ฆ‰์‹œ ๊ฒ€์ƒ‰ ์‹œ ํŽ˜์ด์ง€๋ฅผ 1๋กœ ๋ฆฌ์…‹ํ•ด ์ฃผ์„ธ์š”.

์ง€๊ธˆ์€ partial๋งŒ ๋ณ‘ํ•ฉํ•ด์„œ ๊ธฐ์กด ํŽ˜์ด์ง€๊ฐ€ ๊ทธ๋Œ€๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ํ•„ํ„ฐ๋‚˜ ํ‚ค์›Œ๋“œ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ด์ „ ํŽ˜์ด์ง€๊ฐ€ ๋น„์–ด ๋ณด์ผ ์ˆ˜ ์žˆ์œผ๋‹ˆ, page: 1์„ ํ•จ๊ป˜ ๋„ฃ๋Š” ํŽธ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
   const handleInstantSearch = (partial: Partial<Draft>) => {
-    setDraft((prev) => ({ ...prev, ...partial }));
-    setParams((prev) => ({ ...prev, ...partial }));
+    setDraft((prev) => ({ ...prev, ...partial, page: 1 }));
+    setParams((prev) => ({ ...prev, ...partial, page: 1 }));
   };
๐Ÿ“ 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 handleSearch = () => setParams(draft);
const handleInstantSearch = (partial: Partial<Draft>) => {
setDraft((prev) => ({ ...prev, ...partial }));
setParams((prev) => ({ ...prev, ...partial }));
};
const handleSearch = () => setParams(draft);
const handleInstantSearch = (partial: Partial<Draft>) => {
setDraft((prev) => ({ ...prev, ...partial, page: 1 }));
setParams((prev) => ({ ...prev, ...partial, page: 1 }));
};
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/purchases/list/PurchaseList.tsx` around lines 13 - 17,
handleInstantSearch currently merges partial into draft and params but leaves
the existing page intact; update handleInstantSearch so when merging partial
into setDraft and setParams you also set page: 1 (e.g., setDraft(prev => ({
...prev, ...partial, page: 1 })) and setParams(prev => ({ ...prev, ...partial,
page: 1 }))) to ensure searches reset to the first page when filters or keywords
change; modify the function that calls setDraft and setParams
(handleInstantSearch) accordingly.

Comment on lines +197 to +243
const handleDeleteProduct = (record: PurchaseRecord) => {
handleDeleteProducts(
{ productIds: [record.productId], purchaseIds: [record.purchaseId] },
{
onSuccess: () => toast.success('์‚ฌ์ž…๋‚ด์—ญ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'),
onError: () => toast.error('์‚ฌ์ž…๋‚ด์—ญ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'),
},
);
};

return (
<section className="flex flex-col gap-[2rem] rounded-[1.6rem] bg-white py-[3rem] pr-[2.4rem] pl-[3.8rem]">
{/* ์ „์ฒด ๊ฐœ์ˆ˜, ๋“œ๋กญ๋‹ค์šด, ๋ฒ„ํŠผ 3๊ฐœ */}
<div className="flex justify-between">
<span className="font-14-m text-gray-9 flex shrink-0 items-center gap-[0.6rem]">
์ „์ฒด {pagination.total}
</span>
<div className="flex gap-[0.2rem]">
<Dropdown
options={PAGE_SIZE_LABEL}
value={params.limit}
onChange={(v) => handleChangeLimit(v)}
className="border-none"
/>
<div className="flex gap-[1rem]">
<Button size="small" variant="white" onClick={handleDelete}>
์„ ํƒ์‚ญ์ œ
<>
{/* ๋ชจ๋ฐ”์ผ */}
<section className="flex flex-col gap-[1.2rem] rounded-[1.6rem] bg-white px-[1.6rem] py-[1.6rem] sm:hidden">
{/* ์ƒ๋‹จ ๋ฐ” */}
<div className="flex items-center justify-between">
<UnstyledButton className="font-12-r text-gray-5 px-[0.8rem]" onClick={handleToggleAll}>
์ „์ฒด ์„ ํƒ
</UnstyledButton>
<div className="flex items-center">
<Dropdown
options={SORT_BY_LABEL}
value={params.sortBy}
onChange={handleSortBy}
className="border-none"
/>
<Dropdown
options={PAGE_SIZE_LABEL}
value={params.limit}
onChange={handleChangeLimit}
className="border-none"
/>
</div>
</div>
{/* ์นด๋“œ ๋ชฉ๋ก */}
<div className="flex flex-col gap-[1.2rem]">
{records.map((record) => (
<PurchaseCard
key={record.productId}
record={record}
isSelected={selectedProductIds.has(record.productId)}
onToggle={() => handleToggleRow(record.productId)}
onBackorderModalOpenChange={handleBackorderModalOpenChange}
onReceiptModalOpenChange={setSelectedReceiptPurchaseId}
onEdit={() => handleEditProduct(record)}
onDelete={() => handleDeleteProduct}
/>

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 | โšก Quick win

๋ชจ๋ฐ”์ผ ์‚ญ์ œ ์ฝœ๋ฐฑ์ด ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ onDelete={() => handleDeleteProduct}๋Š” ํ•จ์ˆ˜๋งŒ ๋ฐ˜ํ™˜ํ•ด์„œ ์‹ค์ œ ์‚ญ์ œ mutation์ด ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. handleDeleteProduct(record)๋ฅผ ์ง์ ‘ ์‹คํ–‰ํ•˜๋„๋ก ๋ฐ”๊ฟ” ์ฃผ์„ธ์š”.

์ˆ˜์ • ์˜ˆ์‹œ
-              onDelete={() => handleDeleteProduct}
+              onDelete={() => handleDeleteProduct(record)}
๐Ÿ“ 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 handleDeleteProduct = (record: PurchaseRecord) => {
handleDeleteProducts(
{ productIds: [record.productId], purchaseIds: [record.purchaseId] },
{
onSuccess: () => toast.success('์‚ฌ์ž…๋‚ด์—ญ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'),
onError: () => toast.error('์‚ฌ์ž…๋‚ด์—ญ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'),
},
);
};
return (
<section className="flex flex-col gap-[2rem] rounded-[1.6rem] bg-white py-[3rem] pr-[2.4rem] pl-[3.8rem]">
{/* ์ „์ฒด ๊ฐœ์ˆ˜, ๋“œ๋กญ๋‹ค์šด, ๋ฒ„ํŠผ 3๊ฐœ */}
<div className="flex justify-between">
<span className="font-14-m text-gray-9 flex shrink-0 items-center gap-[0.6rem]">
์ „์ฒด {pagination.total}
</span>
<div className="flex gap-[0.2rem]">
<Dropdown
options={PAGE_SIZE_LABEL}
value={params.limit}
onChange={(v) => handleChangeLimit(v)}
className="border-none"
/>
<div className="flex gap-[1rem]">
<Button size="small" variant="white" onClick={handleDelete}>
์„ ํƒ์‚ญ์ œ
<>
{/* ๋ชจ๋ฐ”์ผ */}
<section className="flex flex-col gap-[1.2rem] rounded-[1.6rem] bg-white px-[1.6rem] py-[1.6rem] sm:hidden">
{/* ์ƒ๋‹จ ๋ฐ” */}
<div className="flex items-center justify-between">
<UnstyledButton className="font-12-r text-gray-5 px-[0.8rem]" onClick={handleToggleAll}>
์ „์ฒด ์„ ํƒ
</UnstyledButton>
<div className="flex items-center">
<Dropdown
options={SORT_BY_LABEL}
value={params.sortBy}
onChange={handleSortBy}
className="border-none"
/>
<Dropdown
options={PAGE_SIZE_LABEL}
value={params.limit}
onChange={handleChangeLimit}
className="border-none"
/>
</div>
</div>
{/* ์นด๋“œ ๋ชฉ๋ก */}
<div className="flex flex-col gap-[1.2rem]">
{records.map((record) => (
<PurchaseCard
key={record.productId}
record={record}
isSelected={selectedProductIds.has(record.productId)}
onToggle={() => handleToggleRow(record.productId)}
onBackorderModalOpenChange={handleBackorderModalOpenChange}
onReceiptModalOpenChange={setSelectedReceiptPurchaseId}
onEdit={() => handleEditProduct(record)}
onDelete={() => handleDeleteProduct}
/>
const handleDeleteProduct = (record: PurchaseRecord) => {
handleDeleteProducts(
{ productIds: [record.productId], purchaseIds: [record.purchaseId] },
{
onSuccess: () => toast.success('์‚ฌ์ž…๋‚ด์—ญ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'),
onError: () => toast.error('์‚ฌ์ž…๋‚ด์—ญ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'),
},
);
};
return (
<>
{/* ๋ชจ๋ฐ”์ผ */}
<section className="flex flex-col gap-[1.2rem] rounded-[1.6rem] bg-white px-[1.6rem] py-[1.6rem] sm:hidden">
{/* ์ƒ๋‹จ ๋ฐ” */}
<div className="flex items-center justify-between">
<UnstyledButton className="font-12-r text-gray-5 px-[0.8rem]" onClick={handleToggleAll}>
์ „์ฒด ์„ ํƒ
</UnstyledButton>
<div className="flex items-center">
<Dropdown
options={SORT_BY_LABEL}
value={params.sortBy}
onChange={handleSortBy}
className="border-none"
/>
<Dropdown
options={PAGE_SIZE_LABEL}
value={params.limit}
onChange={handleChangeLimit}
className="border-none"
/>
</div>
</div>
{/* ์นด๋“œ ๋ชฉ๋ก */}
<div className="flex flex-col gap-[1.2rem]">
{records.map((record) => (
<PurchaseCard
key={record.productId}
record={record}
isSelected={selectedProductIds.has(record.productId)}
onToggle={() => handleToggleRow(record.productId)}
onBackorderModalOpenChange={handleBackorderModalOpenChange}
onReceiptModalOpenChange={setSelectedReceiptPurchaseId}
onEdit={() => handleEditProduct(record)}
onDelete={() => handleDeleteProduct(record)}
/>
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/purchases/list/sections/ResultSection.tsx` around lines 197
- 243, The onDelete prop currently passes a function reference instead of
invoking the delete handler, so the mutation never runs; in the PurchaseCard
instantiation inside the records.map change the prop from onDelete={() =>
handleDeleteProduct} to call the handler with the current record (e.g.,
onDelete={() => handleDeleteProduct(record)}) so handleDeleteProducts (used
inside handleDeleteProduct) is executed when the delete button is clicked.

Comment on lines +71 to +105
<Field label="๋‹จ๊ฐ€">
<Input
numeric
placeholder="0"
{...register(`products.${index}.price`, {
setValueAs: (value: string) => Number(value),
})}
status={errors.products?.[index]?.price ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
</Field>
<Field label="์ˆ˜๋Ÿ‰">
<Input
numeric
placeholder="0"
{...register(`products.${index}.quantity`, {
setValueAs: (value: string) => Number(value),
})}
status={errors.products?.[index]?.quantity ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
</Field>
<Field label="๊ธˆ์•กํ•ฉ๊ณ„">
<Input disabled value={totalPrice} className="text-right" />
</Field>
<Field label="๋ฏธ์†ก์ˆ˜๋Ÿ‰">
<Input
numeric
placeholder="0"
{...register(`products.${index}.backorderQuantity`, {
setValueAs: (value: string) => Number(value),
})}
status={errors.products?.[index]?.backorderQuantity ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>

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 | โšก Quick win

๊ณต๋ฐฑ์„ 0์œผ๋กœ ๋ฐ”๊พธ์ง€ ๋งˆ์„ธ์š”.

Number(value)๋Š” ๋นˆ ๋ฌธ์ž์—ด์„ 0์œผ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. ๋ชจ๋ฐ”์ผ ์นด๋“œ์—์„œ๋„ ์ˆซ์ž ์ž…๋ ฅ์„ ์ง€์› ์„ ๋•Œ ๊ฐ’์ด ๋‚จ์•„ ๋ฒ„๋ฆฌ๋ฏ€๋กœ, ๊ณต๋ฐฑ์€ ๊ทธ๋Œ€๋กœ ๋น„์›Œ ๋‘๊ณ  ์ˆซ์ž์ผ ๋•Œ๋งŒ ๋ณ€ํ™˜ํ•˜๋Š” ํŽธ์ด ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
-            setValueAs: (value: string) => Number(value),
+            setValueAs: (value: string) => (value === '' ? undefined : Number(value)),
๐Ÿ“ 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
<Field label="๋‹จ๊ฐ€">
<Input
numeric
placeholder="0"
{...register(`products.${index}.price`, {
setValueAs: (value: string) => Number(value),
})}
status={errors.products?.[index]?.price ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
</Field>
<Field label="์ˆ˜๋Ÿ‰">
<Input
numeric
placeholder="0"
{...register(`products.${index}.quantity`, {
setValueAs: (value: string) => Number(value),
})}
status={errors.products?.[index]?.quantity ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
</Field>
<Field label="๊ธˆ์•กํ•ฉ๊ณ„">
<Input disabled value={totalPrice} className="text-right" />
</Field>
<Field label="๋ฏธ์†ก์ˆ˜๋Ÿ‰">
<Input
numeric
placeholder="0"
{...register(`products.${index}.backorderQuantity`, {
setValueAs: (value: string) => Number(value),
})}
status={errors.products?.[index]?.backorderQuantity ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
<Field label="๋‹จ๊ฐ€">
<Input
numeric
placeholder="0"
{...register(`products.${index}.price`, {
setValueAs: (value: string) => (value === '' ? undefined : Number(value)),
})}
status={errors.products?.[index]?.price ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
</Field>
<Field label="์ˆ˜๋Ÿ‰">
<Input
numeric
placeholder="0"
{...register(`products.${index}.quantity`, {
setValueAs: (value: string) => (value === '' ? undefined : Number(value)),
})}
status={errors.products?.[index]?.quantity ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
</Field>
<Field label="๊ธˆ์•กํ•ฉ๊ณ„">
<Input disabled value={totalPrice} className="text-right" />
</Field>
<Field label="๋ฏธ์†ก์ˆ˜๋Ÿ‰">
<Input
numeric
placeholder="0"
{...register(`products.${index}.backorderQuantity`, {
setValueAs: (value: string) => (value === '' ? undefined : Number(value)),
})}
status={errors.products?.[index]?.backorderQuantity ? STATUS.ERROR : STATUS.DEFAULT}
className="text-right"
/>
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/purchases/new/components/ProductCard.tsx` around lines 71 -
105, The current setValueAs handlers on register(`products.${index}.price`),
register(`products.${index}.quantity`), and
register(`products.${index}.backorderQuantity`) use Number(value) which converts
an empty string to 0; change each handler to only convert when the input is
non-empty (e.g. return undefined or the original empty value when value === ''),
otherwise return Number(value) for numeric strings so blank inputs remain blank
instead of becoming 0.

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.

๐ŸŒ€ FEATURE : ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„

1 participant