Merge branch 'mhkim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

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

View File

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

View File

@ -1291,7 +1291,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>((props, ref) =
ref={ref} ref={ref}
id={id} id={id}
className={cn( className={cn(
"flex gap-1", "flex",
labelPos === "left" ? "flex-row items-center" : "flex-row-reverse items-center", labelPos === "left" ? "flex-row items-center" : "flex-row-reverse items-center",
isDesignMode && "pointer-events-none", 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 ? component.style?.labelText || (component as any).label || component.componentConfig?.label
: undefined; : undefined;
// 🔧 수평 라벨(left/right) 감지 → 런타임에서만 외부 flex 컨테이너로 라벨 처리 // 🔧 수평 라벨(left/right) 감지 → 외부 absolute 래퍼로 라벨 처리 (카테고리 셀렉트와 동일 방식)
// 디자인 모드에서는 V2 컴포넌트가 자체적으로 라벨을 렌더링 (height 체인 문제 방지)
const labelPosition = component.style?.labelPosition; const labelPosition = component.style?.labelPosition;
const isV2Component = componentType?.startsWith("v2-"); const isV2Component = componentType?.startsWith("v2-");
const needsExternalHorizLabel = !!( const needsExternalHorizLabel = !!(
!props.isDesignMode &&
isV2Component && isV2Component &&
effectiveLabel && effectiveLabel &&
(labelPosition === "left" || labelPosition === "right") (labelPosition === "left" || labelPosition === "right")