๐ FEATURE : #99 ๋ฐ์ํ ๊ตฌํ#101
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
๐ WalkthroughWalkthrough๋ฐ์ํ UI๋ฅผ ์ง์ํ๋ ๋ ์ด์์ ๋ฐ ์ปดํฌ๋ํธ ๊ฐ์ ์ ๊ตฌํํฉ๋๋ค. Drawer ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํ๊ณ , ๋ชจ๋ฐ์ผ/๋ฐ์คํฌํฑ ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ ํตํด ๊ตฌ๋งค ๋ชฉ๋ก ๋ฐ ์ ๊ท ๊ตฌ๋งค ํ์ด์ง๋ฅผ ๋ฐ์ํ์ผ๋ก ๋ฆฌํฉํ ๋งํฉ๋๋ค. ํํฐ๋ง UI๋ฅผ ์ฌ์ค๊ณํ๊ณ ๊ธฐ์กด ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ ํฉ๋๋ค. Changes๋ฐ์ํ UI ๊ตฌํ
Estimated code review effort๐ฏ 3 (Moderate) | โฑ๏ธ ~25 minutes Possibly related PRs
๐ฅ Pre-merge checks | โ 2 | โ 3โ Failed checks (1 warning, 2 inconclusive)
โ Passed checks (2 passed)
โ๏ธ Tip: You can configure your own custom pre-merge checks in the settings. โจ Finishing Touches๐ Generate docstrings
๐งช Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
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
โ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yamland included by none
๐ Files selected for processing (25)
client/package.jsonclient/src/components/Header/BaseHeader.tsxclient/src/components/Header/Header.tsxclient/src/components/button/Button.tsxclient/src/components/drawer/Drawer.tsxclient/src/components/drawer/index.tsclient/src/components/index.tsclient/src/components/input/Input.tsxclient/src/components/product-table/ProductTable.tsxclient/src/pages/purchases/list/PurchaseList.tsxclient/src/pages/purchases/list/components/AddonInput.tsxclient/src/pages/purchases/list/components/FilterBar.tsxclient/src/pages/purchases/list/components/FilterChip.tsxclient/src/pages/purchases/list/components/FilterDrawer.tsxclient/src/pages/purchases/list/components/KebabMenu.tsxclient/src/pages/purchases/list/components/PurchaseCard.tsxclient/src/pages/purchases/list/components/ToggleGroup.tsxclient/src/pages/purchases/list/constants/result.tsclient/src/pages/purchases/list/sections/FilterSection.tsxclient/src/pages/purchases/list/sections/ResultSection.tsxclient/src/pages/purchases/list/types/search.tsclient/src/pages/purchases/new/PurchaseNew.tsxclient/src/pages/purchases/new/components/ProductCard.tsxclient/src/pages/purchases/new/components/UploadButton.tsxclient/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', |
There was a problem hiding this comment.
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.
| 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).
| <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> |
There was a problem hiding this comment.
๋ซํ ๋ชจ๋ฐ์ผ ๋ฉ๋ด๋ 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.
| <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.
| {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> |
There was a problem hiding this comment.
๊ฒ์ ๋ฒํผ์ 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.
| {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.
| <button | ||
| onClick={onOpenDrawer} | ||
| aria-label="ํํฐ ์ด๊ธฐ" | ||
| className="border-gray-2 text-gray-5 shrink-0 rounded-full border p-[0.8rem]" | ||
| > | ||
| <SlidersHorizontal size={16} /> | ||
| </button> |
There was a problem hiding this comment.
ํํฐ ์ด๊ธฐ ๋ฒํผ์๋ 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.
| <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.
| 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> | ||
| ); |
There was a problem hiding this comment.
๋ฒํผ ์์ฑ ์ ๋ฌ๊ณผ ์ ํ ์ํ ๋ ธ์ถ์ ๊ฐ์ด ์ ๋ฆฌํ์ธ์.
์ง๊ธ์ 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.
| 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.
| <Input | ||
| type="date" | ||
| aria-label="์กฐํ ์ข ๋ฃ์ผ" | ||
| min={draft.startDate} | ||
| max={new Date().toISOString().split('T')[0]} | ||
| value={draft.endDate} |
There was a problem hiding this comment.
์ข ๋ฃ์ผ์ ์ต๋๊ฐ์ ๋ก์ปฌ ์๊ฐ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐํ์ธ์.
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}.
| 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); | ||
| }, []); |
There was a problem hiding this comment.
์ธ๋ถ ํด๋ฆญ ๊ฐ์ง๋ 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).
| const handleSearch = () => setParams(draft); | ||
| const handleInstantSearch = (partial: Partial<Draft>) => { | ||
| setDraft((prev) => ({ ...prev, ...partial })); | ||
| setParams((prev) => ({ ...prev, ...partial })); | ||
| }; |
There was a problem hiding this comment.
์ฆ์ ๊ฒ์ ์ ํ์ด์ง๋ฅผ 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.
| 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.
| 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} | ||
| /> |
There was a problem hiding this comment.
๋ชจ๋ฐ์ผ ์ญ์ ์ฝ๋ฐฑ์ด ์คํ๋์ง ์์ต๋๋ค.
ํ์ฌ 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.
| 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.
| <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" | ||
| /> |
There was a problem hiding this comment.
๊ณต๋ฐฑ์ 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.
| <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.
๐ Related Issue
๐ฌ Description
๐น Screenshot
๐ Notes
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
New Features
Style