Merge branch 'ksh'
This commit is contained in:
commit
aca39f23d2
|
|
@ -305,84 +305,173 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 수정
|
// 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 처리 (추가/수정/삭제)
|
||||||
if (groupData.length > 0) {
|
if (groupData.length > 0 || originalGroupData.length > 0) {
|
||||||
console.log("🔄 그룹 데이터 일괄 수정 시작:", {
|
console.log("🔄 그룹 데이터 일괄 처리 시작:", {
|
||||||
groupDataLength: groupData.length,
|
groupDataLength: groupData.length,
|
||||||
originalGroupDataLength: originalGroupData.length,
|
originalGroupDataLength: originalGroupData.length,
|
||||||
|
groupData,
|
||||||
|
originalGroupData,
|
||||||
|
tableName: screenData.screenInfo.tableName,
|
||||||
|
screenId: modalState.screenId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let insertedCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
for (let i = 0; i < groupData.length; i++) {
|
// 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외)
|
||||||
const currentData = groupData[i];
|
const salesOrderColumns = [
|
||||||
const originalItemData = originalGroupData[i];
|
"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",
|
||||||
|
];
|
||||||
|
|
||||||
if (!originalItemData) {
|
// 1️⃣ 신규 품목 추가 (id가 없는 항목)
|
||||||
console.warn(`원본 데이터가 없습니다 (index: ${i})`);
|
for (const currentData of groupData) {
|
||||||
continue;
|
if (!currentData.id) {
|
||||||
}
|
console.log("➕ 신규 품목 추가:", currentData);
|
||||||
|
|
||||||
// 변경된 필드만 추출
|
// 실제 테이블 컬럼만 추출
|
||||||
const changedData: Record<string, any> = {};
|
const insertData: Record<string, any> = {};
|
||||||
|
Object.keys(currentData).forEach((key) => {
|
||||||
// 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외)
|
if (salesOrderColumns.includes(key) && key !== "id") {
|
||||||
const salesOrderColumns = [
|
insertData[key] = currentData[key];
|
||||||
"id",
|
}
|
||||||
"order_no",
|
});
|
||||||
"customer_code",
|
|
||||||
"customer_name",
|
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
|
||||||
"order_date",
|
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
||||||
"delivery_date",
|
modalState.groupByColumns.forEach((colName) => {
|
||||||
"item_code",
|
// 기존 품목(groupData[0])에서 groupByColumns 값 가져오기
|
||||||
"quantity",
|
const referenceData = originalGroupData[0] || groupData.find(item => item.id);
|
||||||
"unit_price",
|
if (referenceData && referenceData[colName]) {
|
||||||
"amount",
|
insertData[colName] = referenceData[colName];
|
||||||
"status",
|
console.log(`🔑 [신규 품목] ${colName} 값 추가:`, referenceData[colName]);
|
||||||
"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]) {
|
console.log("📦 [신규 품목] 최종 insertData:", insertData);
|
||||||
changedData[key] = currentData[key];
|
|
||||||
|
try {
|
||||||
|
const response = await dynamicFormApi.saveFormData({
|
||||||
|
screenId: modalState.screenId || 0,
|
||||||
|
tableName: screenData.screenInfo.tableName,
|
||||||
|
data: insertData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
insertedCount++;
|
||||||
|
console.log("✅ 신규 품목 추가 성공:", response.data);
|
||||||
|
} else {
|
||||||
|
console.error("❌ 신규 품목 추가 실패:", response.message);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ 신규 품목 추가 오류:", error);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 변경사항이 없으면 스킵
|
|
||||||
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) {
|
// 2️⃣ 기존 품목 수정 (id가 있는 항목)
|
||||||
toast.success(`${updatedCount}개의 품목이 수정되었습니다.`);
|
for (const currentData of groupData) {
|
||||||
|
if (currentData.id) {
|
||||||
|
// id 기반 매칭 (인덱스 기반 X)
|
||||||
|
const originalItemData = originalGroupData.find(
|
||||||
|
(orig) => orig.id === currentData.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!originalItemData) {
|
||||||
|
console.warn(`원본 데이터를 찾을 수 없습니다 (id: ${currentData.id})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 변경된 필드만 추출
|
||||||
|
const changedData: Record<string, any> = {};
|
||||||
|
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(`변경사항 없음 (id: ${currentData.id})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE 실행
|
||||||
|
try {
|
||||||
|
const response = await dynamicFormApi.updateFormDataPartial(
|
||||||
|
currentData.id,
|
||||||
|
originalItemData,
|
||||||
|
changedData,
|
||||||
|
screenData.screenInfo.tableName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
updatedCount++;
|
||||||
|
console.log(`✅ 품목 수정 성공 (id: ${currentData.id})`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ 품목 수정 실패 (id: ${currentData.id}):`, response.message);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ 품목 수정 오류 (id: ${currentData.id}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ 삭제된 품목 제거 (원본에는 있지만 현재 데이터에는 없는 항목)
|
||||||
|
const currentIds = new Set(groupData.map((item) => item.id).filter(Boolean));
|
||||||
|
const deletedItems = originalGroupData.filter(
|
||||||
|
(orig) => orig.id && !currentIds.has(orig.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const deletedItem of deletedItems) {
|
||||||
|
console.log("🗑️ 품목 삭제:", deletedItem);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await dynamicFormApi.deleteFormDataFromTable(
|
||||||
|
deletedItem.id,
|
||||||
|
screenData.screenInfo.tableName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
deletedCount++;
|
||||||
|
console.log(`✅ 품목 삭제 성공 (id: ${deletedItem.id})`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ 품목 삭제 실패 (id: ${deletedItem.id}):`, response.message);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ 품목 삭제 오류 (id: ${deletedItem.id}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결과 메시지
|
||||||
|
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(", ")})`);
|
||||||
|
|
||||||
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
|
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
|
||||||
if (modalState.onSave) {
|
if (modalState.onSave) {
|
||||||
|
|
@ -585,6 +674,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
tableName: screenData.screenInfo?.tableName,
|
tableName: screenData.screenInfo?.tableName,
|
||||||
}}
|
}}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
|
isInModal={true}
|
||||||
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
||||||
groupedData={groupData.length > 0 ? groupData : undefined}
|
groupedData={groupData.length > 0 ? groupData : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ interface InteractiveScreenViewerProps {
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
// 🆕 그룹 데이터 (EditModal에서 전달)
|
// 🆕 그룹 데이터 (EditModal에서 전달)
|
||||||
groupedData?: Record<string, any>[];
|
groupedData?: Record<string, any>[];
|
||||||
|
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
||||||
|
isInModal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
||||||
|
|
@ -64,6 +66,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
userName: externalUserName,
|
userName: externalUserName,
|
||||||
companyCode: externalCompanyCode,
|
companyCode: externalCompanyCode,
|
||||||
groupedData,
|
groupedData,
|
||||||
|
isInModal = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
const { userName: authUserName, user: authUser } = useAuth();
|
const { userName: authUserName, user: authUser } = useAuth();
|
||||||
|
|
@ -329,6 +332,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
userId={user?.userId} // ✅ 사용자 ID 전달
|
userId={user?.userId} // ✅ 사용자 ID 전달
|
||||||
userName={user?.userName} // ✅ 사용자 이름 전달
|
userName={user?.userName} // ✅ 사용자 이름 전달
|
||||||
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
||||||
|
onSave={onSave} // 🆕 EditModal의 handleSave 콜백 전달
|
||||||
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
|
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
onSelectedRowsChange={(selectedRows, selectedData) => {
|
onSelectedRowsChange={(selectedRows, selectedData) => {
|
||||||
|
|
@ -401,6 +405,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
required: required,
|
required: required,
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
className: "w-full h-full",
|
className: "w-full h-full",
|
||||||
|
isInModal: isInModal, // 🆕 EditModal 내부 여부 전달
|
||||||
|
onSave: onSave, // 🆕 EditModal의 handleSave 콜백 전달
|
||||||
}}
|
}}
|
||||||
config={widget.webTypeConfig}
|
config={widget.webTypeConfig}
|
||||||
onEvent={(event: string, data: any) => {
|
onEvent={(event: string, data: any) => {
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ export interface DynamicComponentRendererProps {
|
||||||
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
selectedRows?: any[];
|
selectedRows?: any[];
|
||||||
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
||||||
|
|
@ -244,6 +245,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
selectedScreen, // 🆕 화면 정보
|
selectedScreen, // 🆕 화면 정보
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
screenId,
|
screenId,
|
||||||
userId, // 🆕 사용자 ID
|
userId, // 🆕 사용자 ID
|
||||||
userName, // 🆕 사용자 이름
|
userName, // 🆕 사용자 이름
|
||||||
|
|
@ -358,6 +360,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
selectedScreen, // 🆕 화면 정보
|
selectedScreen, // 🆕 화면 정보
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
screenId,
|
screenId,
|
||||||
userId, // 🆕 사용자 ID
|
userId, // 🆕 사용자 ID
|
||||||
userName, // 🆕 사용자 이름
|
userName, // 🆕 사용자 이름
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onFlowRefresh?: () => void;
|
onFlowRefresh?: () => void;
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
|
|
||||||
// 폼 데이터 관련
|
// 폼 데이터 관련
|
||||||
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||||
|
|
@ -83,6 +84,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
onFlowRefresh,
|
onFlowRefresh,
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
sortBy, // 🆕 정렬 컬럼
|
sortBy, // 🆕 정렬 컬럼
|
||||||
sortOrder, // 🆕 정렬 방향
|
sortOrder, // 🆕 정렬 방향
|
||||||
columnOrder, // 🆕 컬럼 순서
|
columnOrder, // 🆕 컬럼 순서
|
||||||
|
|
@ -95,6 +97,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
|
|
||||||
|
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
|
||||||
|
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
|
||||||
|
const finalOnSave = onSave || propsOnSave;
|
||||||
|
|
||||||
// 🆕 플로우 단계별 표시 제어
|
// 🆕 플로우 단계별 표시 제어
|
||||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
||||||
|
|
@ -415,6 +421,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
onFlowRefresh, // 플로우 새로고침 콜백 추가
|
onFlowRefresh, // 플로우 새로고침 콜백 추가
|
||||||
|
onSave: finalOnSave, // 🆕 EditModal의 handleSave 콜백 (props에서도 추출)
|
||||||
// 테이블 선택된 행 정보 추가
|
// 테이블 선택된 행 정보 추가
|
||||||
selectedRows,
|
selectedRows,
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export function ConditionalContainerComponent({
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
groupedData, // 🆕 그룹 데이터
|
groupedData, // 🆕 그룹 데이터
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
}: ConditionalContainerProps) {
|
}: ConditionalContainerProps) {
|
||||||
console.log("🎯 ConditionalContainerComponent 렌더링!", {
|
console.log("🎯 ConditionalContainerComponent 렌더링!", {
|
||||||
isDesignMode,
|
isDesignMode,
|
||||||
|
|
@ -179,6 +180,7 @@ export function ConditionalContainerComponent({
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={onFormDataChange}
|
onFormDataChange={onFormDataChange}
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,6 +201,7 @@ export function ConditionalContainerComponent({
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={onFormDataChange}
|
onFormDataChange={onFormDataChange}
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export function ConditionalSectionViewer({
|
||||||
formData,
|
formData,
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
groupedData, // 🆕 그룹 데이터
|
groupedData, // 🆕 그룹 데이터
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
}: ConditionalSectionViewerProps) {
|
}: ConditionalSectionViewerProps) {
|
||||||
const { userId, userName, user } = useAuth();
|
const { userId, userName, user } = useAuth();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
@ -153,17 +154,18 @@ export function ConditionalSectionViewer({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={component}
|
component={component}
|
||||||
isInteractive={true}
|
isInteractive={true}
|
||||||
screenId={screenInfo?.id}
|
screenId={screenInfo?.id}
|
||||||
tableName={screenInfo?.tableName}
|
tableName={screenInfo?.tableName}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
userName={userName}
|
userName={userName}
|
||||||
companyCode={user?.companyCode}
|
companyCode={user?.companyCode}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={onFormDataChange}
|
onFormDataChange={onFormDataChange}
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
/>
|
onSave={onSave}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export interface ConditionalContainerProps {
|
||||||
formData?: Record<string, any>;
|
formData?: Record<string, any>;
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
|
|
||||||
// 화면 편집기 관련
|
// 화면 편집기 관련
|
||||||
isDesignMode?: boolean; // 디자인 모드 여부
|
isDesignMode?: boolean; // 디자인 모드 여부
|
||||||
|
|
@ -77,5 +78,6 @@ export interface ConditionalSectionViewerProps {
|
||||||
formData?: Record<string, any>;
|
formData?: Record<string, any>;
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터
|
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||||
|
const hasInitializedSort = useRef(false);
|
||||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
|
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
|
||||||
const [tableLabel, setTableLabel] = useState<string>("");
|
const [tableLabel, setTableLabel] = useState<string>("");
|
||||||
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
||||||
|
|
@ -508,6 +509,28 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
unregisterTable,
|
unregisterTable,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return;
|
||||||
|
|
||||||
|
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
|
||||||
|
const savedSort = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (savedSort) {
|
||||||
|
try {
|
||||||
|
const { column, direction } = JSON.parse(savedSort);
|
||||||
|
if (column && direction) {
|
||||||
|
setSortColumn(column);
|
||||||
|
setSortDirection(direction);
|
||||||
|
hasInitializedSort.current = true;
|
||||||
|
console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 정렬 상태 복원 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [tableConfig.selectedTable, userId]);
|
||||||
|
|
||||||
// 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기
|
// 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tableConfig.selectedTable || !userId) return;
|
if (!tableConfig.selectedTable || !userId) return;
|
||||||
|
|
@ -955,6 +978,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
newSortDirection = "asc";
|
newSortDirection = "asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 정렬 상태를 localStorage에 저장 (사용자별)
|
||||||
|
if (tableConfig.selectedTable && userId) {
|
||||||
|
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify({
|
||||||
|
column: newSortColumn,
|
||||||
|
direction: newSortDirection
|
||||||
|
}));
|
||||||
|
console.log("💾 정렬 상태 저장:", { column: newSortColumn, direction: newSortDirection });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 정렬 상태 저장 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
|
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
|
||||||
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
|
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
|
||||||
|
|
||||||
|
|
@ -1876,11 +1913,59 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
};
|
};
|
||||||
}, [tableConfig.selectedTable, isDesignMode]);
|
}, [tableConfig.selectedTable, isDesignMode]);
|
||||||
|
|
||||||
// 초기 컬럼 너비 측정 (한 번만)
|
// 🎯 컬럼 너비 자동 계산 (내용 기반)
|
||||||
|
const calculateOptimalColumnWidth = useCallback((columnName: string, displayName: string): number => {
|
||||||
|
// 기본 너비 설정
|
||||||
|
const MIN_WIDTH = 100;
|
||||||
|
const MAX_WIDTH = 400;
|
||||||
|
const PADDING = 48; // 좌우 패딩 + 여유 공간
|
||||||
|
const HEADER_PADDING = 60; // 헤더 추가 여유 (정렬 아이콘 등)
|
||||||
|
|
||||||
|
// 헤더 텍스트 너비 계산 (대략 8px per character)
|
||||||
|
const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING;
|
||||||
|
|
||||||
|
// 데이터 셀 너비 계산 (상위 50개 샘플링)
|
||||||
|
const sampleSize = Math.min(50, data.length);
|
||||||
|
let maxDataWidth = headerWidth;
|
||||||
|
|
||||||
|
for (let i = 0; i < sampleSize; i++) {
|
||||||
|
const cellValue = data[i]?.[columnName];
|
||||||
|
if (cellValue !== null && cellValue !== undefined) {
|
||||||
|
const cellText = String(cellValue);
|
||||||
|
// 숫자는 좁게, 텍스트는 넓게 계산
|
||||||
|
const isNumber = !isNaN(Number(cellValue)) && cellValue !== "";
|
||||||
|
const charWidth = isNumber ? 8 : 9;
|
||||||
|
const cellWidth = cellText.length * charWidth + PADDING;
|
||||||
|
maxDataWidth = Math.max(maxDataWidth, cellWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최소/최대 범위 내로 제한
|
||||||
|
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth)));
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// 🎯 localStorage에서 컬럼 너비 불러오기 및 초기 계산
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasInitializedWidths.current && visibleColumns.length > 0) {
|
if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) {
|
||||||
// 약간의 지연을 두고 DOM이 완전히 렌더링된 후 측정
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
|
const storageKey = tableConfig.selectedTable && userId
|
||||||
|
? `table_column_widths_${tableConfig.selectedTable}_${userId}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 1. localStorage에서 저장된 너비 불러오기
|
||||||
|
let savedWidths: Record<string, number> = {};
|
||||||
|
if (storageKey) {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
if (saved) {
|
||||||
|
savedWidths = JSON.parse(saved);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("컬럼 너비 불러오기 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 자동 계산 또는 저장된 너비 적용
|
||||||
const newWidths: Record<string, number> = {};
|
const newWidths: Record<string, number> = {};
|
||||||
let hasAnyWidth = false;
|
let hasAnyWidth = false;
|
||||||
|
|
||||||
|
|
@ -1888,13 +1973,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// 체크박스 컬럼은 제외 (고정 48px)
|
// 체크박스 컬럼은 제외 (고정 48px)
|
||||||
if (column.columnName === "__checkbox__") return;
|
if (column.columnName === "__checkbox__") return;
|
||||||
|
|
||||||
const thElement = columnRefs.current[column.columnName];
|
// 저장된 너비가 있으면 우선 사용
|
||||||
if (thElement) {
|
if (savedWidths[column.columnName]) {
|
||||||
const measuredWidth = thElement.offsetWidth;
|
newWidths[column.columnName] = savedWidths[column.columnName];
|
||||||
if (measuredWidth > 0) {
|
hasAnyWidth = true;
|
||||||
newWidths[column.columnName] = measuredWidth;
|
} else {
|
||||||
hasAnyWidth = true;
|
// 저장된 너비가 없으면 자동 계산
|
||||||
}
|
const optimalWidth = calculateOptimalColumnWidth(
|
||||||
|
column.columnName,
|
||||||
|
columnLabels[column.columnName] || column.displayName
|
||||||
|
);
|
||||||
|
newWidths[column.columnName] = optimalWidth;
|
||||||
|
hasAnyWidth = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1902,11 +1992,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
setColumnWidths(newWidths);
|
setColumnWidths(newWidths);
|
||||||
hasInitializedWidths.current = true;
|
hasInitializedWidths.current = true;
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 150); // DOM 렌더링 대기
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [visibleColumns]);
|
}, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]);
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// 페이지네이션 JSX
|
// 페이지네이션 JSX
|
||||||
|
|
@ -2241,7 +2331,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// 최종 너비를 state에 저장
|
// 최종 너비를 state에 저장
|
||||||
if (thElement) {
|
if (thElement) {
|
||||||
const finalWidth = Math.max(80, thElement.offsetWidth);
|
const finalWidth = Math.max(80, thElement.offsetWidth);
|
||||||
setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth }));
|
setColumnWidths((prev) => {
|
||||||
|
const newWidths = { ...prev, [column.columnName]: finalWidth };
|
||||||
|
|
||||||
|
// 🎯 localStorage에 컬럼 너비 저장 (사용자별)
|
||||||
|
if (tableConfig.selectedTable && userId) {
|
||||||
|
const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(newWidths));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("컬럼 너비 저장 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newWidths;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 텍스트 선택 복원
|
// 텍스트 선택 복원
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ export interface ButtonActionContext {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onFlowRefresh?: () => void; // 플로우 새로고침 콜백
|
onFlowRefresh?: () => void; // 플로우 새로고침 콜백
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
|
|
||||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
selectedRows?: any[];
|
selectedRows?: any[];
|
||||||
|
|
@ -213,9 +214,23 @@ export class ButtonActionExecutor {
|
||||||
* 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
|
* 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
|
||||||
*/
|
*/
|
||||||
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
const { formData, originalData, tableName, screenId } = context;
|
const { formData, originalData, tableName, screenId, onSave } = context;
|
||||||
|
|
||||||
console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId });
|
console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId, hasOnSave: !!onSave });
|
||||||
|
|
||||||
|
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
||||||
|
if (onSave) {
|
||||||
|
console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행");
|
||||||
|
try {
|
||||||
|
await onSave();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ [handleSave] onSave 콜백 실행 오류:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("⚠️ [handleSave] onSave 콜백 없음 - 기본 저장 로직 실행");
|
||||||
|
|
||||||
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
|
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
|
||||||
// context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함
|
// context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue