This commit is contained in:
dohyeons
2025-11-25 09:48:18 +09:00
9 changed files with 340 additions and 46 deletions

View File

@@ -24,6 +24,8 @@ interface EditModalState {
modalSize: "sm" | "md" | "lg" | "xl";
editData: Record<string, any>;
onSave?: () => void;
groupByColumns?: string[]; // 🆕 그룹핑 컬럼 (예: ["order_no"])
tableName?: string; // 🆕 테이블명 (그룹 조회용)
}
interface EditModalProps {
@@ -40,6 +42,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
modalSize: "md",
editData: {},
onSave: undefined,
groupByColumns: undefined,
tableName: undefined,
});
const [screenData, setScreenData] = useState<{
@@ -58,6 +62,10 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// 폼 데이터 상태 (편집 데이터로 초기화됨)
const [formData, setFormData] = useState<Record<string, any>>({});
const [originalData, setOriginalData] = useState<Record<string, any>>({});
// 🆕 그룹 데이터 상태 (같은 order_no의 모든 품목)
const [groupData, setGroupData] = useState<Record<string, any>[]>([]);
const [originalGroupData, setOriginalGroupData] = useState<Record<string, any>[]>([]);
// 화면의 실제 크기 계산 함수 (ScreenModal과 동일)
const calculateScreenDimensions = (components: ComponentData[]) => {
@@ -110,7 +118,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// 전역 모달 이벤트 리스너
useEffect(() => {
const handleOpenEditModal = (event: CustomEvent) => {
const { screenId, title, description, modalSize, editData, onSave } = event.detail;
const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName } = event.detail;
setModalState({
isOpen: true,
@@ -120,6 +128,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
modalSize: modalSize || "lg",
editData: editData || {},
onSave,
groupByColumns, // 🆕 그룹핑 컬럼
tableName, // 🆕 테이블명
});
// 편집 데이터로 폼 데이터 초기화
@@ -154,9 +164,78 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
useEffect(() => {
if (modalState.isOpen && modalState.screenId) {
loadScreenData(modalState.screenId);
// 🆕 그룹 데이터 조회 (groupByColumns가 있는 경우)
if (modalState.groupByColumns && modalState.groupByColumns.length > 0 && modalState.tableName) {
loadGroupData();
}
}
}, [modalState.isOpen, modalState.screenId]);
// 🆕 그룹 데이터 조회 함수
const loadGroupData = async () => {
if (!modalState.tableName || !modalState.groupByColumns || modalState.groupByColumns.length === 0) {
console.warn("테이블명 또는 그룹핑 컬럼이 없습니다.");
return;
}
try {
console.log("🔍 그룹 데이터 조회 시작:", {
tableName: modalState.tableName,
groupByColumns: modalState.groupByColumns,
editData: modalState.editData,
});
// 그룹핑 컬럼 값 추출 (예: order_no = "ORD-20251124-001")
const groupValues: Record<string, any> = {};
modalState.groupByColumns.forEach((column) => {
if (modalState.editData[column]) {
groupValues[column] = modalState.editData[column];
}
});
if (Object.keys(groupValues).length === 0) {
console.warn("그룹핑 컬럼 값이 없습니다:", modalState.groupByColumns);
return;
}
console.log("🔍 그룹 조회 요청:", {
tableName: modalState.tableName,
groupValues,
});
// 같은 그룹의 모든 레코드 조회 (entityJoinApi 사용)
const { entityJoinApi } = await import("@/lib/api/entityJoin");
const response = await entityJoinApi.getTableDataWithJoins(modalState.tableName, {
page: 1,
size: 1000,
search: groupValues, // search 파라미터로 전달 (백엔드에서 WHERE 조건으로 처리)
enableEntityJoin: true,
});
console.log("🔍 그룹 조회 응답:", response);
// entityJoinApi는 배열 또는 { data: [] } 형식으로 반환
const dataArray = Array.isArray(response) ? response : response?.data || [];
if (dataArray.length > 0) {
console.log("✅ 그룹 데이터 조회 성공:", dataArray);
setGroupData(dataArray);
setOriginalGroupData(JSON.parse(JSON.stringify(dataArray))); // Deep copy
toast.info(`${dataArray.length}개의 관련 품목을 불러왔습니다.`);
} else {
console.warn("그룹 데이터가 없습니다:", response);
setGroupData([modalState.editData]); // 기본값: 선택된 행만
setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]);
}
} catch (error: any) {
console.error("❌ 그룹 데이터 조회 오류:", error);
toast.error("관련 데이터를 불러오는 중 오류가 발생했습니다.");
setGroupData([modalState.editData]); // 기본값: 선택된 행만
setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]);
}
};
const loadScreenData = async (screenId: number) => {
try {
setLoading(true);
@@ -208,10 +287,14 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
modalSize: "md",
editData: {},
onSave: undefined,
groupByColumns: undefined,
tableName: undefined,
});
setScreenData(null);
setFormData({});
setOriginalData({});
setGroupData([]); // 🆕
setOriginalGroupData([]); // 🆕
};
// 저장 버튼 클릭 시 - UPDATE 액션 실행
@@ -222,7 +305,104 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
try {
// 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 수정
if (groupData.length > 0) {
console.log("🔄 그룹 데이터 일괄 수정 시작:", {
groupDataLength: groupData.length,
originalGroupDataLength: originalGroupData.length,
});
let updatedCount = 0;
for (let i = 0; i < groupData.length; i++) {
const currentData = groupData[i];
const originalItemData = originalGroupData[i];
if (!originalItemData) {
console.warn(`원본 데이터가 없습니다 (index: ${i})`);
continue;
}
// 변경된 필드만 추출
const changedData: Record<string, any> = {};
// 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외)
const salesOrderColumns = [
"id",
"order_no",
"customer_code",
"customer_name",
"order_date",
"delivery_date",
"item_code",
"quantity",
"unit_price",
"amount",
"status",
"notes",
"created_at",
"updated_at",
"company_code",
];
Object.keys(currentData).forEach((key) => {
// sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외)
if (!salesOrderColumns.includes(key)) {
return;
}
if (currentData[key] !== originalItemData[key]) {
changedData[key] = currentData[key];
}
});
// 변경사항이 없으면 스킵
if (Object.keys(changedData).length === 0) {
console.log(`변경사항 없음 (index: ${i})`);
continue;
}
// 기본키 확인
const recordId = originalItemData.id || Object.values(originalItemData)[0];
// UPDATE 실행
const response = await dynamicFormApi.updateFormDataPartial(
recordId,
originalItemData,
changedData,
screenData.screenInfo.tableName,
);
if (response.success) {
updatedCount++;
console.log(`✅ 품목 ${i + 1} 수정 성공 (id: ${recordId})`);
} else {
console.error(`❌ 품목 ${i + 1} 수정 실패 (id: ${recordId}):`, response.message);
}
}
if (updatedCount > 0) {
toast.success(`${updatedCount}개의 품목이 수정되었습니다.`);
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
if (modalState.onSave) {
try {
modalState.onSave();
} catch (callbackError) {
console.error("⚠️ onSave 콜백 에러:", callbackError);
}
}
handleClose();
} else {
toast.info("변경된 내용이 없습니다.");
handleClose();
}
return;
}
// 기존 로직: 단일 레코드 수정
const changedData: Record<string, any> = {};
Object.keys(formData).forEach((key) => {
if (formData[key] !== originalData[key]) {
@@ -341,6 +521,13 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
maxHeight: "100%",
}}
>
{/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */}
{groupData.length > 1 && (
<div className="absolute left-4 top-4 z-10 rounded-md bg-blue-50 px-3 py-2 text-xs text-blue-700 shadow-sm">
{groupData.length}
</div>
)}
{screenData.components.map((component) => {
// 컴포넌트 위치를 offset만큼 조정
const offsetX = screenDimensions?.offsetX || 0;
@@ -355,23 +542,51 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
},
};
// 🔍 디버깅: 컴포넌트 렌더링 시점의 groupData 확인
if (component.id === screenData.components[0]?.id) {
console.log("🔍 [EditModal] InteractiveScreenViewerDynamic props:", {
componentId: component.id,
groupDataLength: groupData.length,
groupData: groupData,
formData: groupData.length > 0 ? groupData[0] : formData,
});
}
return (
<InteractiveScreenViewerDynamic
key={component.id}
component={adjustedComponent}
allComponents={screenData.components}
formData={formData}
formData={groupData.length > 0 ? groupData[0] : formData}
onFormDataChange={(fieldName, value) => {
// 🆕 그룹 데이터가 있으면 처리
if (groupData.length > 0) {
// ModalRepeaterTable의 경우 배열 전체를 받음
if (Array.isArray(value)) {
setGroupData(value);
} else {
// 일반 필드는 모든 항목에 동일하게 적용
setGroupData((prev) =>
prev.map((item) => ({
...item,
[fieldName]: value,
}))
);
}
} else {
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
}
}}
screenInfo={{
id: modalState.screenId!,
tableName: screenData.screenInfo?.tableName,
}}
onSave={handleSave}
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
groupedData={groupData.length > 0 ? groupData : undefined}
/>
);
})}

View File

@@ -46,6 +46,8 @@ interface InteractiveScreenViewerProps {
userId?: string;
userName?: string;
companyCode?: string;
// 🆕 그룹 데이터 (EditModal에서 전달)
groupedData?: Record<string, any>[];
}
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
@@ -61,6 +63,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
userId: externalUserId,
userName: externalUserName,
companyCode: externalCompanyCode,
groupedData,
}) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { userName: authUserName, user: authUser } = useAuth();
@@ -332,6 +335,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
groupedData={groupedData}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}
onFlowSelectedDataChange={(selectedData, stepId) => {