feat: enhance EditModal and input components for improved layout and functionality

- Updated EditModal to prioritize screen resolution settings from layout data, ensuring accurate dimension calculations.
- Refined modal styling for better responsiveness and consistency with ScreenModal.
- Adjusted V2Input and V2Select components to remove unnecessary gap styling, streamlining their layout.
- Enhanced DynamicComponentRenderer to handle horizontal labels more effectively with an external wrapper.

These changes aim to improve the user experience and visual consistency across the application.
This commit is contained in:
kmh 2026-03-17 16:25:42 +09:00
parent c35e217001
commit ab98c655b5
4 changed files with 44 additions and 41 deletions

View File

@ -563,8 +563,20 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
if (screenInfo && layoutData) {
const components = layoutData.components || [];
// 화면의 실제 크기 계산
const dimensions = calculateScreenDimensions(components);
// 화면 관리에서 설정한 해상도 우선 사용 (ScreenModal과 동일)
const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution;
let dimensions;
if (screenResolution && screenResolution.width && screenResolution.height) {
dimensions = {
width: screenResolution.width,
height: screenResolution.height,
offsetX: 0,
offsetY: 0,
};
} else {
dimensions = calculateScreenDimensions(components);
}
setScreenDimensions(dimensions);
setScreenData({
@ -1547,31 +1559,25 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
};
// 모달 크기 설정 - 화면관리 설정 크기 + 헤더
// 모달 크기 설정 - ScreenModal과 동일한 방식 (maxHeight로 유연 처리)
const getModalStyle = () => {
if (!screenDimensions) {
return {
className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0",
style: undefined, // undefined로 변경 - defaultWidth/defaultHeight 사용
className: "w-fit min-w-[400px] max-w-4xl overflow-hidden",
style: { padding: 0, gap: 0, maxHeight: "calc(100dvh - 8px)" },
};
}
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
// 실제 모달 크기 = 컨텐츠 + 헤더 + gap + padding + 라벨 공간
const headerHeight = 52; // DialogHeader (타이틀 + border-b + py-3)
const dialogGap = 16; // DialogContent gap-4
const extraPadding = 24; // 추가 여백 (안전 마진)
const labelSpace = 30; // 입력 필드 위 라벨 공간 (-top-6 = 24px + 여유)
const totalHeight = screenDimensions.height + headerHeight + dialogGap + extraPadding + labelSpace;
const finalWidth = Math.min(screenDimensions.width, window.innerWidth * 0.98);
return {
className: "overflow-hidden p-0",
style: {
width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 좌우 패딩 추가
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`,
width: `${finalWidth}px`,
maxHeight: "calc(100dvh - 8px)",
maxWidth: "98vw",
maxHeight: "95vh",
padding: 0,
gap: 0,
},
};
};
@ -1593,7 +1599,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
</div>
</DialogHeader>
<div className="[&::-webkit-scrollbar-thumb]:bg-muted/60 flex flex-1 justify-center overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-transparent">
<div className="flex-1 min-h-0 flex items-start justify-center overflow-auto">
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@ -1608,42 +1614,41 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
>
<div
data-screen-runtime="true"
className="bg-card relative m-auto"
className="relative bg-white"
style={{
width: screenDimensions?.width || 800,
// 조건부 레이어가 활성화되면 높이 자동 확장
width: `${screenDimensions?.width || 800}px`,
minHeight: `${screenDimensions?.height || 600}px`,
height: (() => {
const baseHeight = (screenDimensions?.height || 600) + 30;
const baseHeight = screenDimensions?.height || 600;
if (activeConditionalComponents.length > 0) {
// 조건부 레이어 컴포넌트 중 가장 아래 위치 계산
const offsetY = screenDimensions?.offsetY || 0;
let maxBottom = 0;
activeConditionalComponents.forEach((comp) => {
const y = parseFloat(comp.position?.y?.toString() || "0") - offsetY + 30;
const y = parseFloat(comp.position?.y?.toString() || "0") - offsetY;
const h = parseFloat(comp.size?.height?.toString() || "40");
maxBottom = Math.max(maxBottom, y + h);
});
return Math.max(baseHeight, maxBottom + 20); // 20px 여백
return `${Math.max(baseHeight, maxBottom + 20)}px`;
}
return baseHeight;
return `${baseHeight}px`;
})(),
transformOrigin: "center center",
maxWidth: "100%",
overflow: "visible",
}}
>
{/* 기본 레이어 컴포넌트 렌더링 */}
{screenData.components.map((component) => {
// 컴포넌트 위치를 offset만큼 조정
const offsetX = screenDimensions?.offsetX || 0;
const offsetY = screenDimensions?.offsetY || 0;
const labelSpace = 30; // 라벨 공간 (입력 필드 위 -top-6 라벨용)
// screenResolution이 있으면 offsetY=0이므로 디자이너 좌표 그대로 사용
// offsetY > 0 (자동 계산)일 때만 라벨 공간 보정
const labelSpace = offsetY > 0 ? 30 : 0;
const adjustedComponent = {
...component,
position: {
...component.position,
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
y: parseFloat(component.position?.y?.toString() || "0") - offsetY + labelSpace, // 라벨 공간 추가
y: parseFloat(component.position?.y?.toString() || "0") - offsetY + labelSpace,
},
};
@ -1709,11 +1714,11 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
);
})}
{/* 🆕 조건부 레이어 컴포넌트 렌더링 */}
{/* 조건부 레이어 컴포넌트 렌더링 */}
{activeConditionalComponents.map((component) => {
const offsetX = screenDimensions?.offsetX || 0;
const offsetY = screenDimensions?.offsetY || 0;
const labelSpace = 30;
const labelSpace = offsetY > 0 ? 30 : 0;
const adjustedComponent = {
...component,

View File

@ -89,12 +89,12 @@ function formatTel(value: string): string {
return `${digits.slice(0, 4)}-${digits.slice(4, 8)}`;
}
// 서울: 02 → 2-4-4
// 서울: 02 → 9자리 2-3-4, 10자리 2-4-4
if (digits.startsWith("02")) {
if (digits.length <= 2) return digits;
if (digits.length <= 6) return `${digits.slice(0, 2)}-${digits.slice(2)}`;
if (digits.length <= 10) return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;
return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6, 10)}`;
if (digits.length <= 5) return `${digits.slice(0, 2)}-${digits.slice(2)}`;
const mid = digits.length >= 10 ? 4 : 3;
return `${digits.slice(0, 2)}-${digits.slice(2, 2 + mid)}-${digits.slice(2 + mid, 2 + mid + 4)}`;
}
// 안심번호: 050x → 4-4-4
@ -1202,7 +1202,7 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
ref={ref}
id={id}
className={cn(
"flex gap-1",
"flex",
labelPos === "left" ? "flex-row items-center" : "flex-row-reverse items-center",
)}
style={{

View File

@ -1291,7 +1291,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>((props, ref) =
ref={ref}
id={id}
className={cn(
"flex gap-1",
"flex",
labelPos === "left" ? "flex-row items-center" : "flex-row-reverse items-center",
isDesignMode && "pointer-events-none",
)}

View File

@ -808,12 +808,10 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
? component.style?.labelText || (component as any).label || component.componentConfig?.label
: undefined;
// 🔧 수평 라벨(left/right) 감지 → 런타임에서만 외부 flex 컨테이너로 라벨 처리
// 디자인 모드에서는 V2 컴포넌트가 자체적으로 라벨을 렌더링 (height 체인 문제 방지)
// 🔧 수평 라벨(left/right) 감지 → 외부 absolute 래퍼로 라벨 처리 (카테고리 셀렉트와 동일 방식)
const labelPosition = component.style?.labelPosition;
const isV2Component = componentType?.startsWith("v2-");
const needsExternalHorizLabel = !!(
!props.isDesignMode &&
isV2Component &&
effectiveLabel &&
(labelPosition === "left" || labelPosition === "right")