Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-02-23 10:53:55 +09:00
14 changed files with 573 additions and 346 deletions

View File

@@ -565,12 +565,32 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
return newActiveIds;
}, [formData, groupData, conditionalLayers, screenData?.components]);
// 🆕 활성화된 조건부 레이어의 컴포넌트 가져오기
// 활성화된 조건부 레이어의 컴포넌트 가져오기 (Zone 오프셋 적용)
const activeConditionalComponents = useMemo(() => {
return conditionalLayers
.filter((layer) => activeConditionalLayerIds.includes(layer.id))
.flatMap((layer) => (layer as LayerDefinition & { components: ComponentData[] }).components || []);
}, [conditionalLayers, activeConditionalLayerIds]);
.flatMap((layer) => {
const layerWithComps = layer as LayerDefinition & { components: ComponentData[] };
const comps = layerWithComps.components || [];
// Zone 오프셋 적용: 조건부 레이어 컴포넌트는 Zone 내부 상대 좌표로 저장되므로
// Zone의 절대 좌표를 더해줘야 EditModal에서 올바른 위치에 렌더링됨
const associatedZone = zones.find((z) => z.zone_id === (layer as any).zoneId);
if (!associatedZone) return comps;
const zoneOffsetX = associatedZone.x || 0;
const zoneOffsetY = associatedZone.y || 0;
return comps.map((comp) => ({
...comp,
position: {
...comp.position,
x: parseFloat(comp.position?.x?.toString() || "0") + zoneOffsetX,
y: parseFloat(comp.position?.y?.toString() || "0") + zoneOffsetY,
},
}));
});
}, [conditionalLayers, activeConditionalLayerIds, zones]);
const handleClose = () => {
setModalState({
@@ -881,14 +901,31 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
}
// V2Repeater 저장 이벤트 발생 (디테일 테이블 데이터 저장)
const hasRepeaterInstances = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;
if (hasRepeaterInstances) {
const masterRecordId = groupData[0]?.id || formData.id;
window.dispatchEvent(
new CustomEvent("repeaterSave", {
detail: {
parentId: masterRecordId,
masterRecordId,
mainFormData: formData,
tableName: screenData.screenInfo.tableName,
},
}),
);
console.log("📋 [EditModal] 그룹 저장 후 repeaterSave 이벤트 발생:", { masterRecordId });
}
// 결과 메시지
const messages: string[] = [];
if (insertedCount > 0) messages.push(`${insertedCount}개 추가`);
if (updatedCount > 0) messages.push(`${updatedCount}개 수정`);
if (deletedCount > 0) messages.push(`${deletedCount}개 삭제`);
if (messages.length > 0) {
toast.success(`품목이 저장되었습니다 (${messages.join(", ")})`);
if (messages.length > 0 || hasRepeaterInstances) {
toast.success(messages.length > 0 ? `품목이 저장되었습니다 (${messages.join(", ")})` : "저장되었습니다.");
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
if (modalState.onSave) {

View File

@@ -2231,11 +2231,20 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
}
: component;
// 🆕 모든 레이어의 컴포넌트 통합 (조건부 레이어 내 컴포넌트가 기본 레이어 formData 참조 가능하도록)
// 모든 레이어의 컴포넌트 통합 (조건 평가용 - 트리거 컴포넌트 검색에 필요)
const allLayerComponents = useMemo(() => {
return layers.flatMap((layer) => layer.components);
}, [layers]);
// 🔧 활성 레이어 컴포넌트만 통합 (저장/데이터 수집용)
// 기본 레이어(base) + 현재 활성화된 조건부 레이어만 포함
// 비활성 레이어의 중복 columnName 컴포넌트가 저장 데이터를 오염시키는 문제 해결
const visibleLayerComponents = useMemo(() => {
return layers
.filter((layer) => layer.type === "base" || activeLayerIds.includes(layer.id))
.flatMap((layer) => layer.components);
}, [layers, activeLayerIds]);
// 🆕 레이어별 컴포넌트 렌더링 함수
const renderLayerComponents = useCallback((layer: LayerDefinition) => {
// 활성화되지 않은 레이어는 렌더링하지 않음
@@ -2272,7 +2281,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
>
<InteractiveScreenViewer
component={comp}
allComponents={allLayerComponents}
allComponents={visibleLayerComponents}
formData={externalFormData}
onFormDataChange={onFormDataChange}
screenInfo={screenInfo}
@@ -2344,7 +2353,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
>
<InteractiveScreenViewer
component={comp}
allComponents={allLayerComponents}
allComponents={visibleLayerComponents}
formData={externalFormData}
onFormDataChange={onFormDataChange}
screenInfo={screenInfo}
@@ -2387,7 +2396,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
>
<InteractiveScreenViewer
component={comp}
allComponents={allLayerComponents}
allComponents={visibleLayerComponents}
formData={externalFormData}
onFormDataChange={onFormDataChange}
screenInfo={screenInfo}
@@ -2423,7 +2432,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
>
<InteractiveScreenViewer
component={comp}
allComponents={allLayerComponents}
allComponents={visibleLayerComponents}
formData={externalFormData}
onFormDataChange={onFormDataChange}
screenInfo={screenInfo}
@@ -2433,7 +2442,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
})}
</div>
);
}, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, allLayerComponents, layers]);
}, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, visibleLayerComponents, layers]);
return (
<SplitPanelProvider>
@@ -2485,7 +2494,13 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
setPopupScreen(null);
setPopupFormData({}); // 팝업 닫을 때 formData도 초기화
}}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden p-0">
<DialogContent
className="max-w-none w-auto max-h-[90vh] overflow-hidden p-0"
style={popupScreenResolution ? {
width: `${Math.min(popupScreenResolution.width + 48, window.innerWidth * 0.98)}px`,
maxWidth: "98vw",
} : { maxWidth: "56rem" }}
>
<DialogHeader className="px-6 pt-4 pb-2">
<DialogTitle>{popupScreen?.title || "상세 정보"}</DialogTitle>
</DialogHeader>

View File

@@ -5556,8 +5556,12 @@ export default function ScreenDesigner({
return false;
}
// 6. 삭제 (단일/다중 선택 지원)
if (e.key === "Delete" && (selectedComponent || groupState.selectedComponents.length > 0)) {
// 6. 삭제 (단일/다중 선택 지원) - Delete 또는 Backspace(Mac)
const isInputFocused = document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement ||
document.activeElement instanceof HTMLSelectElement ||
(document.activeElement as HTMLElement)?.isContentEditable;
if ((e.key === "Delete" || (e.key === "Backspace" && !isInputFocused)) && (selectedComponent || groupState.selectedComponents.length > 0)) {
// console.log("🗑️ 컴포넌트 삭제 (단축키)");
e.preventDefault();
e.stopPropagation();
@@ -7419,7 +7423,7 @@ export default function ScreenDesigner({
</p>
<p>
<span className="font-medium">:</span> Ctrl+C(), Ctrl+V(), Ctrl+S(),
Ctrl+Z(), Delete()
Ctrl+Z(), Delete/Backspace()
</p>
<p className="text-warning flex items-center justify-center gap-2">
<span></span>