feat: ScreenModal 및 V2Select 컴포넌트 개선

- ScreenModal에서 모달 크기 계산 로직을 개선하여, 콘텐츠가 화면 높이를 초과할 때만 스크롤이 필요하도록 수정하였습니다.
- V2Select 및 관련 컴포넌트에서 height 및 style props를 추가하여, 사용자 정의 스타일을 보다 효과적으로 적용할 수 있도록 하였습니다.
- DropdownSelect에서 height 스타일을 직접 전달하여, 다양한 높이 설정을 지원하도록 개선하였습니다.
- CategorySelectComponent에서 라벨 표시 및 높이 계산 로직을 추가하여, 사용자 경험을 향상시켰습니다.
This commit is contained in:
DDD1542
2026-02-05 14:07:18 +09:00
parent 1de67a88b5
commit dd867efd0a
10 changed files with 230 additions and 66 deletions

View File

@@ -531,26 +531,34 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
return {
className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0",
style: undefined, // undefined로 변경 - defaultWidth/defaultHeight 사용
needsScroll: false,
};
}
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
// 실제 모달 크기 = 컨텐츠 + 헤더 + 연속등록 체크박스
// 🔧 여백 최소화: 디자이너와 일치하도록 조정
// 실제 모달 크기 = 컨텐츠 + 헤더 + 연속등록 체크박스 + gap + 패딩
// 🔧 DialogContent의 gap-4 (16px × 2) + 컨텐츠 pt-6 (24px) 포함
const headerHeight = 48; // DialogHeader (타이틀 + border-b + py-3)
const footerHeight = 44; // 연속 등록 모드 체크박스 영역
const dialogGap = 32; // gap-4 × 2 (header-content, content-footer 사이)
const contentTopPadding = 24; // pt-6 (컨텐츠 영역 상단 패딩)
const horizontalPadding = 16; // 좌우 패딩 최소화
const totalHeight = screenDimensions.height + headerHeight + footerHeight;
const totalHeight = screenDimensions.height + headerHeight + footerHeight + dialogGap + contentTopPadding;
const maxAvailableHeight = window.innerHeight * 0.95;
// 콘텐츠가 화면 높이를 초과할 때만 스크롤 필요
const needsScroll = totalHeight > maxAvailableHeight;
return {
className: "overflow-hidden p-0",
style: {
width: `${Math.min(screenDimensions.width + horizontalPadding, window.innerWidth * 0.98)}px`,
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`,
// 🔧 height 대신 max-height만 설정 - 콘텐츠가 작으면 자동으로 줄어듦
maxHeight: `${maxAvailableHeight}px`,
maxWidth: "98vw",
maxHeight: "95vh",
},
needsScroll,
};
};
@@ -634,7 +642,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</DialogHeader>
<div
className="flex-1 overflow-auto flex items-start justify-center pt-6"
className={`flex-1 min-h-0 flex items-start justify-center pt-6 ${modalStyle.needsScroll ? 'overflow-auto' : 'overflow-visible'}`}
>
{loading ? (
<div className="flex h-full items-center justify-center">

View File

@@ -22,18 +22,25 @@ function SelectTrigger({
className,
size = "xs",
children,
style,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "xs" | "sm" | "default";
}) {
// className에 h-full/h-[ 또는 style.height가 있으면 data-size 높이를 무시
const hasCustomHeight = className?.includes("h-full") || className?.includes("h-[") || !!style?.height;
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
data-size={hasCustomHeight ? undefined : size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-transparent text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-10 data-[size=default]:px-3 data-[size=default]:py-2 data-[size=sm]:h-9 data-[size=sm]:px-3 data-[size=sm]:py-1 data-[size=xs]:h-6 data-[size=xs]:px-2 data-[size=xs]:py-0 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
// 커스텀 높이일 때 기본 패딩 적용
hasCustomHeight && "px-2 py-1",
className,
)}
style={style}
{...props}
>
{children}

View File

@@ -222,14 +222,14 @@ const RangeDatePicker = forwardRef<
);
return (
<div ref={ref} className={cn("flex items-center gap-2", className)}>
<div ref={ref} className={cn("flex items-center gap-2 h-full", className)}>
{/* 시작 날짜 */}
<Popover open={openStart} onOpenChange={setOpenStart}>
<PopoverTrigger asChild>
<Button
variant="outline"
disabled={disabled || readonly}
className={cn("h-10 flex-1 justify-start text-left font-normal", !value[0] && "text-muted-foreground")}
className={cn("h-full flex-1 justify-start text-left font-normal", !value[0] && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value[0] || "시작일"}
@@ -259,7 +259,7 @@ const RangeDatePicker = forwardRef<
<Button
variant="outline"
disabled={disabled || readonly}
className={cn("h-10 flex-1 justify-start text-left font-normal", !value[1] && "text-muted-foreground")}
className={cn("h-full flex-1 justify-start text-left font-normal", !value[1] && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value[1] || "종료일"}
@@ -301,7 +301,7 @@ const TimePicker = forwardRef<
}
>(({ value, onChange, disabled, readonly, className }, ref) => {
return (
<div className={cn("relative", className)}>
<div className={cn("relative h-full", className)}>
<Clock className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
ref={ref}
@@ -310,7 +310,7 @@ const TimePicker = forwardRef<
onChange={(e) => onChange?.(e.target.value)}
disabled={disabled}
readOnly={readonly}
className="h-10 pl-10"
className="h-full pl-10"
/>
</div>
);
@@ -357,8 +357,8 @@ const DateTimePicker = forwardRef<
);
return (
<div ref={ref} className={cn("flex gap-2", className)}>
<div className="flex-1">
<div ref={ref} className={cn("flex gap-2 h-full", className)}>
<div className="flex-1 h-full">
<SingleDatePicker
value={datePart}
onChange={handleDateChange}
@@ -369,7 +369,7 @@ const DateTimePicker = forwardRef<
readonly={readonly}
/>
</div>
<div className="w-32">
<div className="w-1/3 min-w-[100px] h-full">
<TimePicker value={timePart} onChange={handleTimeChange} disabled={disabled} readonly={readonly} />
</div>
</div>

View File

@@ -317,7 +317,7 @@ const TextareaInput = forwardRef<
readOnly={readonly}
disabled={disabled}
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-full w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
/>

View File

@@ -134,7 +134,7 @@ export const V2List = forwardRef<HTMLDivElement, V2ListProps>((props, ref) => {
className="bg-muted/20 flex items-center justify-center rounded-lg border p-8"
style={{
width: size?.width || style?.width || "100%",
height: size?.height || style?.height || 400,
height: size?.height || style?.height || "100%",
}}
>
<p className="text-muted-foreground text-sm"> .</p>
@@ -149,7 +149,7 @@ export const V2List = forwardRef<HTMLDivElement, V2ListProps>((props, ref) => {
className="flex flex-col overflow-auto"
style={{
width: size?.width || style?.width || "100%",
height: size?.height || style?.height || 400,
height: size?.height || style?.height || "100%",
}}
>
<TableListComponent

View File

@@ -42,6 +42,7 @@ const DropdownSelect = forwardRef<HTMLButtonElement, {
allowClear?: boolean;
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
}>(({
options,
value,
@@ -52,7 +53,8 @@ const DropdownSelect = forwardRef<HTMLButtonElement, {
maxSelect,
allowClear = true,
disabled,
className
className,
style,
}, ref) => {
const [open, setOpen] = useState(false);
@@ -64,7 +66,8 @@ const DropdownSelect = forwardRef<HTMLButtonElement, {
onValueChange={(v) => onChange?.(v)}
disabled={disabled}
>
<SelectTrigger ref={ref} className={cn("h-10", className)}>
{/* SelectTrigger에 style로 직접 height 전달 (Radix Select.Root는 DOM 없어서 h-full 체인 끊김) */}
<SelectTrigger ref={ref} className={cn("w-full", className)} style={style}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
@@ -112,13 +115,15 @@ const DropdownSelect = forwardRef<HTMLButtonElement, {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{/* Button에 style로 직접 height 전달 (Popover도 DOM 체인 끊김) */}
<Button
ref={ref}
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn("h-10 w-full justify-between font-normal", className)}
className={cn("w-full justify-between font-normal", className)}
style={style}
>
<span className="truncate flex-1 text-left">
{selectedLabels.length > 0
@@ -368,9 +373,9 @@ const SwapSelect = forwardRef<HTMLDivElement, {
return (
<div ref={ref} className={cn("flex gap-2 items-stretch", className)}>
{/* 왼쪽: 선택 가능 */}
<div className="flex-1 border rounded-md">
<div className="p-2 bg-muted text-xs font-medium border-b"> </div>
<div className="p-2 space-y-1 max-h-40 overflow-y-auto">
<div className="flex-1 border rounded-md flex flex-col min-h-0">
<div className="p-2 bg-muted text-xs font-medium border-b shrink-0"> </div>
<div className="p-2 space-y-1 flex-1 overflow-y-auto min-h-0">
{available.map((option) => (
<div
key={option.value}
@@ -412,9 +417,9 @@ const SwapSelect = forwardRef<HTMLDivElement, {
</div>
{/* 오른쪽: 선택됨 */}
<div className="flex-1 border rounded-md">
<div className="p-2 bg-primary/10 text-xs font-medium border-b"></div>
<div className="p-2 space-y-1 max-h-40 overflow-y-auto">
<div className="flex-1 border rounded-md flex flex-col min-h-0">
<div className="p-2 bg-primary/10 text-xs font-medium border-b shrink-0"></div>
<div className="p-2 space-y-1 flex-1 overflow-y-auto min-h-0">
{selected.map((option) => (
<div
key={option.value}
@@ -654,24 +659,31 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
// 모드별 컴포넌트 렌더링
const renderSelect = () => {
if (loading) {
return <div className="h-10 flex items-center text-sm text-muted-foreground"> ...</div>;
return <div className="h-full flex items-center text-sm text-muted-foreground"> ...</div>;
}
const isDisabled = disabled || readonly;
// 명시적 높이가 있을 때만 style 전달, 없으면 undefined (기본 높이 h-6 사용)
const heightStyle: React.CSSProperties | undefined = componentHeight
? { height: componentHeight }
: undefined;
switch (config.mode) {
case "dropdown":
case "combobox": // 🔧 콤보박스는 검색 가능한 드롭다운
return (
<DropdownSelect
options={options}
value={value}
onChange={onChange}
placeholder="선택"
searchable={config.searchable}
searchable={config.mode === "combobox" ? true : config.searchable}
multiple={config.multiple}
maxSelect={config.maxSelect}
allowClear={config.allowClear}
disabled={isDisabled}
style={heightStyle}
/>
);
@@ -686,6 +698,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
);
case "check":
case "checkbox": // 🔧 기존 저장된 값 호환
return (
<CheckSelect
options={options}
@@ -735,6 +748,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
value={value}
onChange={onChange}
disabled={isDisabled}
style={heightStyle}
/>
);
}
@@ -744,6 +758,16 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
// 🔍 디버깅: 높이값 확인 (warn으로 변경하여 캡처되도록)
console.warn("🔍 [V2Select] 높이 디버깅:", {
id,
"size?.height": size?.height,
"style?.height": style?.height,
componentHeight,
size,
style,
});
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;