feat: ScreenModal 및 V2Select 컴포넌트 개선
- ScreenModal에서 모달 크기 계산 로직을 개선하여, 콘텐츠가 화면 높이를 초과할 때만 스크롤이 필요하도록 수정하였습니다. - V2Select 및 관련 컴포넌트에서 height 및 style props를 추가하여, 사용자 정의 스타일을 보다 효과적으로 적용할 수 있도록 하였습니다. - DropdownSelect에서 height 스타일을 직접 전달하여, 다양한 높이 설정을 지원하도록 개선하였습니다. - CategorySelectComponent에서 라벨 표시 및 높이 계산 로직을 추가하여, 사용자 경험을 향상시켰습니다.
This commit is contained in:
parent
1de67a88b5
commit
dd867efd0a
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -299,14 +299,15 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
const columnName = (component as any).columnName;
|
||||
|
||||
// 카테고리 셀렉트: webType이 "category"이고 tableName과 columnName이 있는 경우만
|
||||
// ⚠️ 단, componentType이 "select-basic"인 경우는 ComponentRegistry로 처리 (다중선택 등 고급 기능 지원)
|
||||
// ⚠️ 단, componentType이 "select-basic" 또는 "v2-select"인 경우는 ComponentRegistry로 처리
|
||||
// (다중선택, 체크박스, 라디오 등 고급 모드 지원)
|
||||
if (
|
||||
(inputType === "category" || webType === "category") &&
|
||||
tableName &&
|
||||
columnName &&
|
||||
componentType === "select-basic"
|
||||
(componentType === "select-basic" || componentType === "v2-select")
|
||||
) {
|
||||
// select-basic은 ComponentRegistry에서 처리하도록 아래로 통과
|
||||
// select-basic, v2-select는 ComponentRegistry에서 처리하도록 아래로 통과
|
||||
} else if ((inputType === "category" || webType === "category") && tableName && columnName) {
|
||||
try {
|
||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
|
|
@ -323,6 +324,12 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
const isFieldDisabled = props.disabledFields?.includes(columnName) || (component as any).disabled;
|
||||
const isFieldReadonly = (component as any).readonly || (component as any).componentConfig?.readonly;
|
||||
|
||||
// 🔧 높이 계산: component.size에서 height 추출
|
||||
const categorySize = (component as any).size;
|
||||
const categoryStyle = (component as any).style;
|
||||
const categoryLabel = (component as any).label;
|
||||
const categoryId = component.id;
|
||||
|
||||
return (
|
||||
<CategorySelectComponent
|
||||
tableName={tableName}
|
||||
|
|
@ -334,6 +341,11 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
disabled={isFieldDisabled}
|
||||
readonly={isFieldReadonly}
|
||||
className="w-full"
|
||||
size={categorySize}
|
||||
style={categoryStyle}
|
||||
label={categoryLabel}
|
||||
id={categoryId}
|
||||
isDesignMode={props.isDesignMode}
|
||||
/>
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -520,10 +532,12 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
? (component.style?.labelText || (component as any).label || component.componentConfig?.label)
|
||||
: undefined;
|
||||
|
||||
// 🔧 순서 중요! finalStyle 먼저, component.style 나중에 (커스텀 속성이 CSS 속성을 덮어써야 함)
|
||||
// 🔧 순서 중요! component.style 먼저, CSS 크기 속성은 size 기반으로 덮어씀
|
||||
const mergedStyle = {
|
||||
...finalStyle, // CSS 속성 (width, height 등) - 먼저!
|
||||
...component.style, // 원본 style (labelDisplay, labelText 등) - 나중에! (덮어씀)
|
||||
...component.style, // 원본 style (labelDisplay, labelText 등) - 먼저!
|
||||
// CSS 크기 속성은 size에서 계산한 값으로 명시적 덮어쓰기 (우선순위 최고)
|
||||
width: finalStyle.width,
|
||||
height: finalStyle.height,
|
||||
};
|
||||
|
||||
const rendererProps = {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
|
@ -23,6 +24,20 @@ interface CategorySelectComponentProps {
|
|||
readonly?: boolean;
|
||||
tableName?: string;
|
||||
columnName?: string;
|
||||
// 🔧 높이 조절을 위한 props 추가
|
||||
style?: React.CSSProperties & {
|
||||
labelDisplay?: boolean;
|
||||
labelFontSize?: string | number;
|
||||
labelColor?: string;
|
||||
labelFontWeight?: string | number;
|
||||
labelMarginBottom?: string | number;
|
||||
};
|
||||
size?: { width?: number | string; height?: number | string };
|
||||
// 🔧 라벨 표시를 위한 props 추가
|
||||
label?: string;
|
||||
id?: string;
|
||||
// 🔧 디자인 모드 처리
|
||||
isDesignMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,7 +58,27 @@ export const CategorySelectComponent: React.FC<
|
|||
readonly = false,
|
||||
tableName: propTableName,
|
||||
columnName: propColumnName,
|
||||
style,
|
||||
size,
|
||||
label: propLabel,
|
||||
id: propId,
|
||||
isDesignMode = false,
|
||||
}) => {
|
||||
// 🔧 높이 계산: size.height > style.height > 기본값(40px)
|
||||
const componentHeight = size?.height || style?.height;
|
||||
const heightStyle: React.CSSProperties = componentHeight
|
||||
? { height: componentHeight }
|
||||
: {};
|
||||
|
||||
// 🔧 라벨 관련 계산
|
||||
const label = propLabel || component?.label;
|
||||
const id = propId || component?.id;
|
||||
const showLabel = label && style?.labelDisplay !== false;
|
||||
|
||||
// 라벨 높이 계산 (기본 20px)
|
||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||
const [categoryValues, setCategoryValues] = useState<TableCategoryValue[]>(
|
||||
[]
|
||||
);
|
||||
|
|
@ -97,12 +132,49 @@ export const CategorySelectComponent: React.FC<
|
|||
onChange?.(newValue);
|
||||
};
|
||||
|
||||
// 🔧 공통 라벨 렌더링 함수
|
||||
const renderLabel = () => {
|
||||
if (!showLabel) return null;
|
||||
return (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: `-${estimatedLabelHeight}px`,
|
||||
left: 0,
|
||||
fontSize: style?.labelFontSize || "14px",
|
||||
color: style?.labelColor || "#64748b",
|
||||
fontWeight: style?.labelFontWeight || "500",
|
||||
}}
|
||||
className="text-sm font-medium whitespace-nowrap"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||
</Label>
|
||||
);
|
||||
};
|
||||
|
||||
// 🔧 공통 wrapper 스타일
|
||||
const wrapperStyle: React.CSSProperties = {
|
||||
width: size?.width || style?.width,
|
||||
height: componentHeight,
|
||||
};
|
||||
|
||||
// 🔧 디자인 모드일 때 클릭 방지
|
||||
const designModeClass = isDesignMode ? "pointer-events-none" : "";
|
||||
|
||||
// 로딩 중
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-10 w-full items-center justify-center rounded-md border bg-background px-3 text-sm text-muted-foreground">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
로딩 중...
|
||||
<div id={id} className={`relative ${designModeClass}`} style={wrapperStyle}>
|
||||
{renderLabel()}
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-center rounded-md border bg-background px-3 text-sm text-muted-foreground"
|
||||
style={heightStyle}
|
||||
>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
로딩 중...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -110,8 +182,14 @@ export const CategorySelectComponent: React.FC<
|
|||
// 에러
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-10 w-full items-center rounded-md border border-destructive bg-destructive/10 px-3 text-sm text-destructive">
|
||||
⚠️ {error}
|
||||
<div id={id} className={`relative ${designModeClass}`} style={wrapperStyle}>
|
||||
{renderLabel()}
|
||||
<div
|
||||
className="flex h-full w-full items-center rounded-md border border-destructive bg-destructive/10 px-3 text-sm text-destructive"
|
||||
style={heightStyle}
|
||||
>
|
||||
⚠️ {error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -119,33 +197,44 @@ export const CategorySelectComponent: React.FC<
|
|||
// 카테고리 값이 없음
|
||||
if (categoryValues.length === 0) {
|
||||
return (
|
||||
<div className="flex h-10 w-full items-center rounded-md border bg-muted px-3 text-sm text-muted-foreground">
|
||||
설정된 카테고리 값이 없습니다
|
||||
<div id={id} className={`relative ${designModeClass}`} style={wrapperStyle}>
|
||||
{renderLabel()}
|
||||
<div
|
||||
className="flex h-full w-full items-center rounded-md border bg-muted px-3 text-sm text-muted-foreground"
|
||||
style={heightStyle}
|
||||
>
|
||||
설정된 카테고리 값이 없습니다
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
disabled={disabled || readonly}
|
||||
required={required}
|
||||
>
|
||||
<SelectTrigger className={`w-full ${className}`}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryValues.map((categoryValue) => (
|
||||
<SelectItem
|
||||
key={categoryValue.valueId}
|
||||
value={categoryValue.valueCode}
|
||||
>
|
||||
{categoryValue.valueLabel}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div id={id} className={`relative ${designModeClass}`} style={wrapperStyle}>
|
||||
{renderLabel()}
|
||||
<div className="h-full w-full">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
disabled={disabled || readonly}
|
||||
required={required}
|
||||
>
|
||||
<SelectTrigger className={`w-full h-full ${className}`} style={heightStyle}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryValues.map((categoryValue) => (
|
||||
<SelectItem
|
||||
key={categoryValue.valueId}
|
||||
value={categoryValue.valueCode}
|
||||
>
|
||||
{categoryValue.valueLabel}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,25 @@ export class V2SelectRenderer extends AutoRegisteringComponentRenderer {
|
|||
}
|
||||
};
|
||||
|
||||
// 🔧 DynamicComponentRenderer에서 전달한 style/size를 우선 사용 (height 포함)
|
||||
// restProps.style에 mergedStyle(height 변환됨)이 있고, restProps.size에도 size가 있음
|
||||
const effectiveStyle = restProps.style || component.style;
|
||||
const effectiveSize = restProps.size || component.size;
|
||||
|
||||
// 🔍 디버깅: props 확인 (warn으로 변경하여 캡처되도록)
|
||||
console.warn("🔍 [V2SelectRenderer] props 디버깅:", {
|
||||
componentId: component.id,
|
||||
"component.style": component.style,
|
||||
"component.size": component.size,
|
||||
"restProps.style": restProps.style,
|
||||
"restProps.size": restProps.size,
|
||||
effectiveStyle,
|
||||
effectiveSize,
|
||||
});
|
||||
|
||||
// 🔧 restProps에서 style, size 제외 (effectiveStyle/effectiveSize가 우선되어야 함)
|
||||
const { style: _style, size: _size, ...restPropsClean } = restProps as any;
|
||||
|
||||
return (
|
||||
<V2Select
|
||||
id={component.id}
|
||||
|
|
@ -63,12 +82,12 @@ export class V2SelectRenderer extends AutoRegisteringComponentRenderer {
|
|||
entityLabelColumn: config.entityLabelColumn,
|
||||
entityValueColumn: config.entityValueColumn,
|
||||
}}
|
||||
style={component.style}
|
||||
size={component.size}
|
||||
tableName={tableName}
|
||||
columnName={columnName}
|
||||
formData={formData}
|
||||
{...restProps}
|
||||
{...restPropsClean}
|
||||
style={effectiveStyle}
|
||||
size={effectiveSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ export const V2SelectDefinition = createComponentDefinition({
|
|||
{ value: "dropdown", label: "드롭다운" },
|
||||
{ value: "combobox", label: "콤보박스 (검색)" },
|
||||
{ value: "radio", label: "라디오 버튼" },
|
||||
{ value: "checkbox", label: "체크박스" },
|
||||
{ value: "check", label: "체크박스" },
|
||||
{ value: "tag", label: "태그" },
|
||||
{ value: "toggle", label: "토글" },
|
||||
{ value: "swap", label: "스왑 (좌우 이동)" },
|
||||
],
|
||||
},
|
||||
source: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue