-
Notifications
You must be signed in to change notification settings - Fork 1
✨Feat: date picker components 구현 #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
bea968d
c4c0392
9661456
37c18d4
92cae74
2c76f92
47201ca
54b3529
0eef37f
2e2e032
65e6258
4c35e90
cd676c8
ae2be31
4ea5ec7
b33775a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CustomDropdown } from './CustomDropdown'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { DropdownProps } from 'react-day-picker'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function CalendarDropdownAdapter({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'aria-label': ariaLabel, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: DropdownProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <CustomDropdown | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={value as number} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options={options ?? []} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={ariaLabel} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(next) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const evt = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target: { value: String(next) }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } as unknown as React.ChangeEvent<HTMLSelectElement>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange?.(evt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 타입 안전성 문제: value의 타입 변환
타입 가드를 추가하여 안전하게 처리하세요: export function CalendarDropdownAdapter({
value,
onChange,
options,
'aria-label': ariaLabel,
}: DropdownProps) {
+ const numericValue = typeof value === 'number' ? value : undefined;
+
return (
<CustomDropdown
- value={value as number}
+ value={numericValue}
options={options ?? []}
aria-label={ariaLabel}
onChange={(next) => {
const evt = {
target: { value: String(next) },
} as unknown as React.ChangeEvent<HTMLSelectElement>;
onChange?.(evt);
}}
/>
);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,177 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/shared/lib'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useRef, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { DropdownOption } from 'react-day-picker'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface CustomDropdownProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| value?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange: (next: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| options: DropdownOption[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'aria-label'?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function CustomDropdown({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| options, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'aria-label': ariaLabel, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: CustomDropdownProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [open, setOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const buttonRef = useRef<HTMLButtonElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const listRef = useRef<HTMLUListElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const calcSelectedIndex = useCallback(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const i = options.findIndex((o) => o.value === value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return i >= 0 ? i : 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [options, value]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [activeIndex, setActiveIndex] = useState<number>(calcSelectedIndex()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setActiveIndex(calcSelectedIndex()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [calcSelectedIndex]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const label = | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.find((o) => o.value === value)?.label ?? options[0]?.label ?? ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!open) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handler = (e: Event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const t = (e as Event).target as Node | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!t) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!buttonRef.current?.contains(t) && !listRef.current?.contains(t)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const usePointer = | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof window !== 'undefined' && 'PointerEvent' in window; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (usePointer) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.addEventListener('pointerdown', handler as EventListener, true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.removeEventListener( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'pointerdown', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler as EventListener, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.addEventListener('mousedown', handler as EventListener, true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.addEventListener('touchstart', handler as EventListener, true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.removeEventListener( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'mousedown', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler as EventListener, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.removeEventListener( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'touchstart', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler as EventListener, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [open]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onKeyDown = (e: React.KeyboardEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| !open && | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === ' ') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOpen(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!open) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Escape') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'ArrowDown') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let i = activeIndex + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (i < options.length && options[i].disabled) i++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setActiveIndex(Math.min(i, options.length - 1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (e.key === 'ArrowUp') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let i = activeIndex - 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (i >= 0 && options[i].disabled) i--; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setActiveIndex(Math.max(i, 0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+94
to
+103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 키보드 내비게이션 버그: 비활성 옵션에 멈출 수 있음
재현 시나리오:
비활성 옵션을 건너뛸 수 있도록 수정하세요: if (e.key === 'ArrowDown') {
e.preventDefault();
let i = activeIndex + 1;
while (i < options.length && options[i].disabled) i++;
- setActiveIndex(Math.min(i, options.length - 1));
+ if (i < options.length) {
+ setActiveIndex(i);
+ }
} else if (e.key === 'ArrowUp') {
e.preventDefault();
let i = activeIndex - 1;
while (i >= 0 && options[i].disabled) i--;
- setActiveIndex(Math.max(i, 0));
+ if (i >= 0) {
+ setActiveIndex(i);
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (e.key === 'Enter') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const opt = options[activeIndex]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (opt && !opt.disabled) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange(opt.value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className={cn('relative inline-block', className)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={buttonRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type='button' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-haspopup='listbox' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-expanded={open} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={ariaLabel} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setOpen((v) => !v)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onKeyDown={onKeyDown} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'h-[32px] rounded-[12px] bg-pink-50 border border-pink-100', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'px-3 pr-8 text-label-lg text-pink-800 inline-flex items-center gap-1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'focus:outline-none focus:ring-2 focus:ring-pink-200', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {label} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| {open && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ul | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={listRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role='listbox' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| tabIndex={-1} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={ariaLabel} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'absolute z-50 mt-2 w-[14rem] max-h-[20rem] overflow-auto', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'rounded-[12px] border border-pink-200 bg-white p-1 shadow-xl', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {options.map((opt, idx) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const selected = opt.value === value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const highlighted = idx === activeIndex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={opt.value} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role='option' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-selected={selected} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-disabled={opt.disabled || undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMouseEnter={() => setActiveIndex(idx)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPointerDown={(e) => e.preventDefault()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!opt.disabled) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange(opt.value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'relative cursor-pointer select-none rounded-[8px] px-3 py-2 text-body-md', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| opt.disabled && 'opacity-40 pointer-events-none', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| highlighted && 'bg-pink-50', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| selected && 'text-pink-800 font-medium', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {opt.label} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ul> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| 'use client'; | ||
| import { Calendar } from './calendar'; | ||
| import { Button } from './button'; | ||
| import { Popover, PopoverContent, PopoverTrigger } from './popover'; | ||
| import { Icon } from '@/shared/icons'; | ||
| import { cn } from '@/shared/lib'; | ||
| import { useEffect, useState } from 'react'; | ||
|
|
||
| interface DatePickerProps { | ||
| value?: Date; | ||
| onChange?: (date: Date) => void; | ||
| defaultValue?: Date; | ||
| className?: string; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const toFirstOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1); | ||
| export function DatePicker({ | ||
| value, | ||
| onChange, | ||
| defaultValue, | ||
| className, | ||
| ...calendarProps | ||
| }: DatePickerProps) { | ||
| const [open, setOpen] = useState(false); | ||
| const [innerDate, setInnerDate] = useState<Date | undefined>(defaultValue); | ||
|
|
||
| const [displayMonth, setDisplayMonth] = useState<Date>( | ||
| toFirstOfMonth(value ?? innerDate ?? new Date()), | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (value !== undefined) { | ||
| setInnerDate(value); | ||
| setDisplayMonth(toFirstOfMonth(value)); | ||
| } | ||
| }, [value]); | ||
|
|
||
| useEffect(() => { | ||
| if (open) { | ||
| const base = value ?? innerDate ?? new Date(); | ||
| setDisplayMonth(toFirstOfMonth(base)); | ||
| } | ||
| }, [open, value, innerDate]); | ||
|
|
||
| const selected = value ?? innerDate; | ||
|
|
||
| const handleSelect = (d?: Date) => { | ||
| if (!d) return; | ||
| onChange ? onChange(d) : setInnerDate(d); | ||
| setDisplayMonth(toFirstOfMonth(d)); | ||
| setOpen(false); | ||
| }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분 value랑 deafultvalue 있어서 물어봐요..
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엇.. 네 따로 문제는 없었어요 한번 더 체크해볼게요 |
||
|
|
||
| const today = new Date(); | ||
| const startOfToday = new Date( | ||
| today.getFullYear(), | ||
| today.getMonth(), | ||
| today.getDate(), | ||
| ); | ||
|
|
||
| return ( | ||
| <div className={cn('flex flex-col gap-3', className)}> | ||
| <Popover open={open} onOpenChange={setOpen}> | ||
| <PopoverTrigger asChild> | ||
| <Button className='w-[16rem] h-[4rem] justify-between rounded-[500px] text-label-lg text-pink-300 bg-pink-50 border border-pink-100'> | ||
| {selected ? selected.toLocaleDateString() : 'Select date'} | ||
| <Icon name='CalendarBlank' color='pink-400' size={14} /> | ||
| </Button> | ||
| </PopoverTrigger> | ||
|
|
||
| <PopoverContent className='w-[24rem] p-0' align='start'> | ||
| <Calendar | ||
| mode='single' | ||
| className='w-[24rem] h-auto p-3 [--cell-size:2.8rem]' | ||
| selected={selected} | ||
| onSelect={handleSelect} | ||
| month={displayMonth} | ||
| onMonthChange={setDisplayMonth} | ||
| captionLayout='dropdown' | ||
| disabled={{ before: startOfToday }} | ||
| fromDate={startOfToday} | ||
| toYear={new Date(new Date().getFullYear() + 5, 0, 1).getFullYear()} | ||
| {...calendarProps} | ||
| /> | ||
| </PopoverContent> | ||
| </Popover> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 'use client'; | ||
| import * as PopoverPrimitive from '@radix-ui/react-popover'; | ||
| import { cn } from '@/shared/lib'; | ||
| import { forwardRef } from 'react'; | ||
|
|
||
| export const PopoverContent = forwardRef< | ||
| React.ElementRef<typeof PopoverPrimitive.Content>, | ||
| React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> | ||
| >(function PopoverContent( | ||
| { className, align = 'center', sideOffset = 4, ...props }, | ||
| ref, | ||
| ) { | ||
| return ( | ||
| <PopoverPrimitive.Portal> | ||
| <PopoverPrimitive.Content | ||
| ref={ref} | ||
| data-slot='popover-content' | ||
| align={align} | ||
| sideOffset={sideOffset} | ||
| className={cn( | ||
| 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0', | ||
| 'data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', | ||
| 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2', | ||
| 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | ||
| 'z-50 w-72 origin-(--radix-popover-content-transform-origin)', | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| </PopoverPrimitive.Portal> | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
런타임 의존성이 devDependencies에 들어가 있어 배포 시 누락 위험
@radix-ui/react-popover,@radix-ui/react-slot,react-day-picker,date-fns,class-variance-authority,lucide-react는 런타임에 import되는 UI/유틸입니다. prod 설치(예: CI/CD에서--prod, Docker multi-stage)에서는 devDependencies가 제외되어 앱이 빌드/실행 중 터질 수 있습니다. 또한 반대로svgo는 빌드 스크립트용이므로 devDependencies가 적절합니다.권장 수정:
svgo는 devDependencies로 이동"dependencies": { "axios": "^1.12.2", "clsx": "^2.1.1", "next": "15.5.4", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.64.0", - "svgo": "^4.0.0", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "class-variance-authority": "^0.7.1", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-slot": "^1.2.3", + "react-day-picker": "^9.11.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.540.0" }, "devDependencies": { + "svgo": "^4.0.0", ... - "class-variance-authority": "^0.7.1", - "@radix-ui/react-popover": "^1.1.15", - "date-fns": "^4.1.0", - "react-day-picker": "^9.11.1", - "@radix-ui/react-slot": "^1.2.3", - "lucide-react": "^0.540.0" }의존성 위치를 점검하는 빠른 확인 스크립트입니다(런타임 import되는 패키지가 devDependencies에 남아있는지 검사).
🏁 Script executed:
Length of output: 47
Length of output: 72
🏁 Script executed:
Length of output: 1810
런타임 의존성이 devDependencies에 있어 프로덕션 배포 시 누락 위험 – 즉시 수정 필요
검증 결과
@radix-ui/react-popover,@radix-ui/react-slot,class-variance-authority,react-day-picker는src/shared/components/datePicker/파일들에서 런타임에 직접 import되고 있습니다.--prod플래그나 Docker multi-stage 빌드에서는 devDependencies가 제외되어 앱 실행 중 모듈을 찾을 수 없는 오류가 발생합니다.동시에
svgo는package.json스크립트에서만 사용되므로(build 시icons:clean실행) 현재 dependencies에 있는 것이 잘못되었습니다.필수 수정:
svgo를 devDependencies로 이동"dependencies": { "axios": "^1.12.2", "clsx": "^2.1.1", "next": "15.5.4", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.64.0", - "svgo": "^4.0.0", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "class-variance-authority": "^0.7.1", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-slot": "^1.2.3", + "react-day-picker": "^9.11.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.540.0" }, "devDependencies": { + "svgo": "^4.0.0", "@eslint/eslintrc": "^3", ... - "class-variance-authority": "^0.7.1", - "@radix-ui/react-popover": "^1.1.15", - "date-fns": "^4.1.0", - "react-day-picker": "^9.11.1", - "@radix-ui/react-slot": "^1.2.3", - "lucide-react": "^0.540.0" }🤖 Prompt for AI Agents