feat: ScreenModal 및 V2 컴포넌트 레이아웃 개선

- ScreenModal 컴포넌트의 모달 크기 조정을 위해 헤더 및 푸터 높이를 수정하고, 여백을 최소화하여 디자인 일치를 도모하였습니다.
- V2 컴포넌트에서 라벨 높이를 계산하여 절대 위치로 배치하도록 변경하였으며, 입력 필드 및 선택 컴포넌트의 구조를 개선하여 일관된 사용자 경험을 제공하였습니다.
- 플레이스홀더 기능을 추가하여 날짜 선택 시 사용자 편의성을 높였습니다.
- 관련 CSS 클래스를 업데이트하여 반응형 디자인을 강화하였습니다.
This commit is contained in:
DDD1542 2026-02-04 11:26:51 +09:00
parent faf4f566f7
commit 942eb079e8
12 changed files with 150 additions and 119 deletions

View File

@ -9,6 +9,7 @@ services:
- "9771:3000"
environment:
- NEXT_PUBLIC_API_URL=http://localhost:8080/api
- NODE_OPTIONS=--max-old-space-size=4096
volumes:
- ../../frontend:/app
- /app/node_modules

View File

@ -504,18 +504,18 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
}
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
// 실제 모달 크기 = 컨텐츠 + 헤더 + 연속등록 체크박스 + gap + padding
const headerHeight = 52; // DialogHeader (타이틀 + border-b + py-3)
const footerHeight = 52; // 연속 등록 모드 체크박스 영역
const dialogGap = 16; // DialogContent gap-4
const extraPadding = 24; // 추가 여백 (안전 마진)
// 실제 모달 크기 = 컨텐츠 + 헤더 + 연속등록 체크박스
// 🔧 여백 최소화: 디자이너와 일치하도록 조정
const headerHeight = 48; // DialogHeader (타이틀 + border-b + py-3)
const footerHeight = 44; // 연속 등록 모드 체크박스 영역
const horizontalPadding = 16; // 좌우 패딩 최소화
const totalHeight = screenDimensions.height + headerHeight + footerHeight + dialogGap + extraPadding;
const totalHeight = screenDimensions.height + headerHeight + footerHeight;
return {
className: "overflow-hidden p-0",
style: {
width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 좌우 패딩 추가
width: `${Math.min(screenDimensions.width + horizontalPadding, window.innerWidth * 0.98)}px`,
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`,
maxWidth: "98vw",
maxHeight: "95vh",
@ -587,7 +587,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
return (
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
<DialogContent
className={`${modalStyle.className} ${className || ""} max-w-none`}
className={`${modalStyle.className} ${className || ""} max-w-none flex flex-col`}
{...(modalStyle.style && { style: modalStyle.style })}
>
<DialogHeader className="shrink-0 border-b px-4 py-3">
@ -602,7 +602,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
<div
className="flex-1 overflow-hidden flex items-center justify-center"
>
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@ -614,11 +616,10 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
<ActiveTabProvider>
<TableOptionsProvider>
<div
className="relative mx-auto bg-white"
className="relative bg-white"
style={{
width: `${screenDimensions?.width || 800}px`,
height: `${screenDimensions?.height || 600}px`,
transformOrigin: "center center",
}}
>
{screenData.components.map((component) => {

View File

@ -598,12 +598,7 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
(contentRef as any).current = node;
}
}}
className={`${
(component.type === "component" && (component as any).componentType === "flow-widget") ||
((component as any).componentType === "conditional-container" && !isDesignMode)
? "h-auto"
: "h-full"
} overflow-visible`}
className="h-full overflow-visible"
style={{ width: "100%", maxWidth: "100%" }}
>
<DynamicComponentRenderer

View File

@ -311,36 +311,39 @@ export const V2Biz = forwardRef<HTMLDivElement, V2BizProps>(
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
return (
<div
ref={ref}
id={id}
className="flex flex-col"
className="relative"
style={{
width: componentWidth,
// 🔧 높이는 컨테이너가 아닌 입력 필드에만 적용 (라벨 높이는 별도)
height: componentHeight,
}}
>
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
position: "absolute",
top: `-${estimatedLabelHeight}px`,
left: 0,
fontSize: style?.labelFontSize || "14px",
color: style?.labelColor || "#64748b",
fontWeight: style?.labelFontWeight || "500",
}}
className="text-sm font-medium flex-shrink-0"
className="text-sm font-medium whitespace-nowrap"
>
{label}
</Label>
)}
<div
className="min-h-0"
style={{
height: componentHeight,
}}
>
<div className="h-full w-full">
{renderBiz()}
</div>
</div>

View File

@ -75,10 +75,11 @@ const SingleDatePicker = forwardRef<
disabled?: boolean;
readonly?: boolean;
className?: string;
placeholder?: string;
}
>(
(
{ value, onChange, dateFormat = "YYYY-MM-DD", showToday = true, minDate, maxDate, disabled, readonly, className },
{ value, onChange, dateFormat = "YYYY-MM-DD", showToday = true, minDate, maxDate, disabled, readonly, className, placeholder = "날짜 선택" },
ref,
) => {
const [open, setOpen] = useState(false);
@ -87,6 +88,16 @@ const SingleDatePicker = forwardRef<
const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]);
const maxDateObj = useMemo(() => parseDate(maxDate, dateFormat), [maxDate, dateFormat]);
// 표시할 날짜 텍스트 계산 (ISO 형식이면 포맷팅, 아니면 그대로)
const displayText = useMemo(() => {
if (!value) return "";
// Date 객체로 변환 후 포맷팅
if (date && isValid(date)) {
return formatDate(date, dateFormat);
}
return value;
}, [value, date, dateFormat]);
const handleSelect = useCallback(
(selectedDate: Date | undefined) => {
if (selectedDate) {
@ -115,13 +126,13 @@ const SingleDatePicker = forwardRef<
variant="outline"
disabled={disabled || readonly}
className={cn(
"h-10 w-full justify-start text-left font-normal",
!value && "text-muted-foreground",
"h-full w-full justify-start text-left font-normal",
!displayText && "text-muted-foreground",
className,
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value || "날짜 선택"}
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
{displayText || placeholder}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
@ -409,6 +420,7 @@ export const V2Date = forwardRef<HTMLDivElement, V2DateProps>((props, ref) => {
maxDate={config.maxDate}
disabled={isDisabled}
readonly={readonly}
placeholder={config.placeholder}
/>
);
@ -444,6 +456,7 @@ export const V2Date = forwardRef<HTMLDivElement, V2DateProps>((props, ref) => {
showToday={config.showToday}
disabled={isDisabled}
readonly={readonly}
placeholder={config.placeholder}
/>
);
}
@ -453,37 +466,40 @@ export const V2Date = forwardRef<HTMLDivElement, V2DateProps>((props, ref) => {
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
return (
<div
ref={ref}
id={id}
className="flex flex-col"
className="relative"
style={{
width: componentWidth,
// 🔧 높이는 컨테이너가 아닌 입력 필드에만 적용 (라벨 높이는 별도)
height: componentHeight,
}}
>
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
position: "absolute",
top: `-${estimatedLabelHeight}px`,
left: 0,
fontSize: style?.labelFontSize || "14px",
color: style?.labelColor || "#64748b",
fontWeight: style?.labelFontWeight || "500",
}}
className="flex-shrink-0 text-sm font-medium"
className="text-sm font-medium whitespace-nowrap"
>
{label}
{required && <span className="ml-0.5 text-orange-500">*</span>}
</Label>
)}
<div
className="min-h-0"
style={{
height: componentHeight,
}}
>
<div className="h-full w-full">
{renderDatePicker()}
</div>
</div>

View File

@ -462,37 +462,40 @@ export const V2Hierarchy = forwardRef<HTMLDivElement, V2HierarchyProps>(
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
return (
<div
ref={ref}
id={id}
className="flex flex-col"
className="relative"
style={{
width: componentWidth,
// 🔧 높이는 컨테이너가 아닌 입력 필드에만 적용 (라벨 높이는 별도)
height: componentHeight,
}}
>
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
position: "absolute",
top: `-${estimatedLabelHeight}px`,
left: 0,
fontSize: style?.labelFontSize || "14px",
color: style?.labelColor || "#64748b",
fontWeight: style?.labelFontWeight || "500",
}}
className="text-sm font-medium flex-shrink-0"
className="text-sm font-medium whitespace-nowrap"
>
{label}
{required && <span className="text-orange-500 ml-0.5">*</span>}
</Label>
)}
<div
className="min-h-0"
style={{
height: componentHeight,
}}
>
<div className="h-full w-full">
{renderHierarchy()}
</div>
</div>

View File

@ -792,37 +792,40 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2; // 라벨 높이 + 여백
return (
<div
ref={ref}
id={id}
className="flex flex-col"
className="relative"
style={{
width: componentWidth,
// 🔧 높이는 컨테이너가 아닌 입력 필드에만 적용 (라벨 높이는 별도)
height: componentHeight,
}}
>
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 (높이에 포함되지 않음) */}
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
position: "absolute",
top: `-${estimatedLabelHeight}px`,
left: 0,
fontSize: style?.labelFontSize || "14px",
color: style?.labelColor || "#64748b",
fontWeight: style?.labelFontWeight || "500",
}}
className="flex-shrink-0 text-sm font-medium"
className="text-sm font-medium whitespace-nowrap"
>
{label}
{required && <span className="ml-0.5 text-orange-500">*</span>}
</Label>
)}
<div
className="min-h-0"
style={{
height: componentHeight,
}}
>
<div className="h-full w-full">
{renderInput()}
</div>
</div>

View File

@ -536,37 +536,40 @@ export const V2Media = forwardRef<HTMLDivElement, V2MediaProps>(
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
return (
<div
ref={ref}
id={id}
className="flex w-full flex-col"
className="relative w-full"
style={{
width: componentWidth,
// 🔧 높이는 컨테이너가 아닌 컨텐츠 영역에만 적용 (라벨 높이는 별도)
height: componentHeight,
}}
>
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
position: "absolute",
top: `-${estimatedLabelHeight}px`,
left: 0,
fontSize: style?.labelFontSize || "14px",
color: style?.labelColor || "#64748b",
fontWeight: style?.labelFontWeight || "500",
}}
className="text-sm font-medium flex-shrink-0"
className="text-sm font-medium whitespace-nowrap"
>
{label}
{required && <span className="text-orange-500 ml-0.5">*</span>}
</Label>
)}
<div
className="min-h-0"
style={{
height: componentHeight,
}}
>
<div className="h-full w-full">
{renderMedia()}
</div>
</div>

View File

@ -744,37 +744,40 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
return (
<div
ref={ref}
id={id}
className="flex flex-col"
className="relative"
style={{
width: componentWidth,
// 🔧 높이는 컨테이너가 아닌 입력 필드에만 적용 (라벨 높이는 별도)
height: componentHeight,
}}
>
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
position: "absolute",
top: `-${estimatedLabelHeight}px`,
left: 0,
fontSize: style?.labelFontSize || "14px",
color: style?.labelColor || "#64748b",
fontWeight: style?.labelFontWeight || "500",
}}
className="text-sm font-medium flex-shrink-0"
className="text-sm font-medium whitespace-nowrap"
>
{label}
{required && <span className="text-orange-500 ml-0.5">*</span>}
</Label>
)}
<div
className="min-h-0"
style={{
height: componentHeight,
}}
>
<div className="h-full w-full">
{renderSelect()}
</div>
</div>

View File

@ -48,6 +48,20 @@ export const V2DateConfigPanel: React.FC<V2DateConfigPanelProps> = ({
<Separator />
{/* 플레이스홀더 */}
<div className="space-y-2">
<Label className="text-xs font-medium"></Label>
<Input
value={config.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="날짜 선택"
className="h-8 text-xs"
/>
<p className="text-muted-foreground text-[10px]"> </p>
</div>
<Separator />
{/* 표시 형식 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>

View File

@ -29,10 +29,15 @@ export class V2DateRenderer extends AutoRegisteringComponentRenderer {
}
};
// 라벨: style.labelText 우선, 없으면 component.label 사용
// style.labelDisplay가 false면 라벨 숨김
const style = component.style || {};
const effectiveLabel = style.labelDisplay === false ? undefined : (style.labelText || component.label);
return (
<V2Date
id={component.id}
label={component.label}
label={effectiveLabel}
required={component.required}
readonly={config.readonly || component.readonly}
disabled={config.disabled || component.disabled}
@ -41,7 +46,7 @@ export class V2DateRenderer extends AutoRegisteringComponentRenderer {
config={{
dateType: config.dateType || config.webType || "date",
format: config.format || "YYYY-MM-DD",
placeholder: config.placeholder || "날짜 선택",
placeholder: config.placeholder || style.placeholder || "날짜 선택",
showTime: config.showTime || false,
use24Hours: config.use24Hours ?? true,
minDate: config.minDate,

View File

@ -84,23 +84,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
return (
<div style={componentStyle} className={className} {...domProps}>
{/* 라벨 렌더링 */}
{component.label && (component.style?.labelDisplay ?? true) && (
<label
style={{
position: "absolute",
top: "-25px",
left: "0px",
fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#64748b",
fontWeight: "500",
}}
>
{component.label}
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
</label>
)}
{/* v2-text-display는 텍스트 표시 전용이므로 별도 라벨 불필요 */}
<div style={textStyle} onClick={handleClick} onDragStart={onDragStart} onDragEnd={onDragEnd}>
{componentConfig.text || "텍스트를 입력하세요"}
</div>