Merge remote-tracking branch 'upstream/main'
All checks were successful
Build and Push Images / build-and-push (push) Successful in 14m50s
All checks were successful
Build and Push Images / build-and-push (push) Successful in 14m50s
This commit is contained in:
@@ -55,3 +55,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -51,3 +51,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -67,3 +67,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,3 +55,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,13 @@ export class AdminService {
|
||||
}
|
||||
);
|
||||
|
||||
// [임시 비활성화] 메뉴 권한 그룹 체크 - 모든 사용자에게 전체 메뉴 표시
|
||||
// TODO: 권한 체크 다시 활성화 필요
|
||||
logger.info(
|
||||
`⚠️ [임시 비활성화] 권한 그룹 체크 스킵 - 사용자 ${userId}(${userType})에게 전체 메뉴 표시`
|
||||
);
|
||||
|
||||
/* [원본 코드 - 권한 그룹 체크]
|
||||
if (userType === "COMPANY_ADMIN") {
|
||||
// 회사 관리자: 권한 그룹 기반 필터링 적용
|
||||
if (userRoleGroups.length > 0) {
|
||||
@@ -141,6 +148,7 @@ export class AdminService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
*/
|
||||
} else if (
|
||||
menuType !== undefined &&
|
||||
userType === "SUPER_ADMIN" &&
|
||||
@@ -412,6 +420,15 @@ export class AdminService {
|
||||
let queryParams: any[] = [userLang];
|
||||
let paramIndex = 2;
|
||||
|
||||
// [임시 비활성화] 메뉴 권한 그룹 체크 - 모든 사용자에게 전체 메뉴 표시
|
||||
// TODO: 권한 체크 다시 활성화 필요
|
||||
logger.info(
|
||||
`⚠️ [임시 비활성화] getUserMenuList 권한 그룹 체크 스킵 - 사용자 ${userId}(${userType})에게 전체 메뉴 표시`
|
||||
);
|
||||
authFilter = "";
|
||||
unionFilter = "";
|
||||
|
||||
/* [원본 코드 - getUserMenuList 권한 그룹 체크]
|
||||
if (userType === "SUPER_ADMIN") {
|
||||
// SUPER_ADMIN: 권한 그룹 체크 없이 해당 회사의 모든 메뉴 표시
|
||||
logger.info(`✅ 좌측 사이드바 (SUPER_ADMIN): 회사 ${userCompanyCode}의 모든 메뉴 표시`);
|
||||
@@ -471,6 +488,7 @@ export class AdminService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// 2. 회사별 필터링 조건 생성
|
||||
let companyFilter = "";
|
||||
|
||||
@@ -2409,11 +2409,19 @@ export class TableManagementService {
|
||||
}
|
||||
|
||||
// SET 절 생성 (수정할 데이터) - 먼저 생성
|
||||
// 🔧 테이블에 존재하는 컬럼만 UPDATE (가상 컬럼 제외)
|
||||
const setConditions: string[] = [];
|
||||
const setValues: any[] = [];
|
||||
let paramIndex = 1;
|
||||
const skippedColumns: string[] = [];
|
||||
|
||||
Object.keys(updatedData).forEach((column) => {
|
||||
// 테이블에 존재하지 않는 컬럼은 스킵
|
||||
if (!columnTypeMap.has(column)) {
|
||||
skippedColumns.push(column);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataType = columnTypeMap.get(column) || "text";
|
||||
setConditions.push(
|
||||
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
||||
@@ -2424,6 +2432,10 @@ export class TableManagementService {
|
||||
paramIndex++;
|
||||
});
|
||||
|
||||
if (skippedColumns.length > 0) {
|
||||
logger.info(`⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`);
|
||||
}
|
||||
|
||||
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
|
||||
let whereConditions: string[] = [];
|
||||
let whereValues: any[] = [];
|
||||
@@ -3930,9 +3942,10 @@ export class TableManagementService {
|
||||
`컬럼 입력타입 정보 조회: ${tableName}, company: ${companyCode}`
|
||||
);
|
||||
|
||||
// table_type_columns에서 입력타입 정보 조회 (company_code 필터링)
|
||||
// table_type_columns에서 입력타입 정보 조회
|
||||
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
||||
const rawInputTypes = await query<any>(
|
||||
`SELECT
|
||||
`SELECT DISTINCT ON (ttc.column_name)
|
||||
ttc.column_name as "columnName",
|
||||
COALESCE(cl.column_label, ttc.column_name) as "displayName",
|
||||
ttc.input_type as "inputType",
|
||||
@@ -3946,8 +3959,10 @@ export class TableManagementService {
|
||||
LEFT JOIN information_schema.columns ic
|
||||
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
|
||||
WHERE ttc.table_name = $1
|
||||
AND ttc.company_code = $2
|
||||
ORDER BY ttc.display_order, ttc.column_name`,
|
||||
AND ttc.company_code IN ($2, '*')
|
||||
ORDER BY ttc.column_name,
|
||||
CASE WHEN ttc.company_code = $2 THEN 0 ELSE 1 END,
|
||||
ttc.display_order`,
|
||||
[tableName, companyCode]
|
||||
);
|
||||
|
||||
@@ -3961,17 +3976,20 @@ export class TableManagementService {
|
||||
const mappingTableExists = tableExistsResult[0]?.table_exists === true;
|
||||
|
||||
// 카테고리 컬럼의 경우, 매핑된 메뉴 목록 조회
|
||||
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
||||
let categoryMappings: Map<string, number[]> = new Map();
|
||||
if (mappingTableExists) {
|
||||
logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });
|
||||
|
||||
const mappings = await query<any>(
|
||||
`SELECT
|
||||
`SELECT DISTINCT ON (logical_column_name, menu_objid)
|
||||
logical_column_name as "columnName",
|
||||
menu_objid as "menuObjid"
|
||||
FROM category_column_mapping
|
||||
WHERE table_name = $1
|
||||
AND company_code = $2`,
|
||||
AND company_code IN ($2, '*')
|
||||
ORDER BY logical_column_name, menu_objid,
|
||||
CASE WHEN company_code = $2 THEN 0 ELSE 1 END`,
|
||||
[tableName, companyCode]
|
||||
);
|
||||
|
||||
|
||||
@@ -587,3 +587,4 @@ const result = await executeNodeFlow(flowId, {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -360,3 +360,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -346,3 +346,4 @@ const getComponentValue = (componentId: string) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -127,3 +127,4 @@ export default function ScreenManagementPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -370,33 +370,14 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
};
|
||||
|
||||
// 모드 전환 핸들러
|
||||
const handleModeSwitch = async () => {
|
||||
const handleModeSwitch = () => {
|
||||
if (isAdminMode) {
|
||||
// 관리자 → 사용자 모드: 선택한 회사 유지
|
||||
router.push("/main");
|
||||
} else {
|
||||
// 사용자 → 관리자 모드: WACE로 복귀 필요 (SUPER_ADMIN만)
|
||||
if ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN") {
|
||||
const currentCompanyCode = (user as ExtendedUserInfo)?.companyCode;
|
||||
|
||||
// 이미 WACE("*")가 아니면 WACE로 전환 후 관리자 페이지로 이동
|
||||
if (currentCompanyCode !== "*") {
|
||||
const result = await switchCompany("*");
|
||||
if (result.success) {
|
||||
// 페이지 새로고침 (관리자 페이지로 이동)
|
||||
window.location.href = "/admin";
|
||||
} else {
|
||||
toast.error("WACE로 전환 실패");
|
||||
}
|
||||
} else {
|
||||
// 이미 WACE면 바로 관리자 페이지로 이동
|
||||
// 사용자 → 관리자 모드: 선택한 회사 유지 (회사 전환 없음)
|
||||
router.push("/admin");
|
||||
}
|
||||
} else {
|
||||
// 일반 관리자는 바로 관리자 페이지로 이동
|
||||
router.push("/admin");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 로그아웃 핸들러
|
||||
|
||||
@@ -415,8 +415,10 @@ export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenPro
|
||||
maxWidth: `calc(100% - ${compPosition.x || 0}px)`,
|
||||
};
|
||||
|
||||
// 🆕 formDataVersion을 key에서 제거하여 불필요한 remount 방지
|
||||
// universal-form-modal 같은 컴포넌트가 채번 후 unmount되는 문제 해결
|
||||
return (
|
||||
<div key={`${component.id}-${formDataVersion}`} className="absolute" style={componentStyle}>
|
||||
<div key={component.id} className="absolute" style={componentStyle}>
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
isInteractive={true}
|
||||
|
||||
@@ -140,3 +140,4 @@ export const useActiveTabOptional = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -197,3 +197,4 @@ export function applyAutoFillToFormData(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,42 @@ export function AutocompleteSearchInputComponent({
|
||||
const displayField = config?.displayField || propDisplayField || "";
|
||||
const displayFields = config?.displayFields || (displayField ? [displayField] : []); // 다중 표시 필드
|
||||
const displaySeparator = config?.displaySeparator || " → "; // 구분자
|
||||
const valueField = config?.valueField || propValueField || "";
|
||||
|
||||
// valueField 결정: fieldMappings 기반으로 추론 (config.valueField가 fieldMappings에 없으면 무시)
|
||||
const getValueField = () => {
|
||||
// fieldMappings가 있으면 그 안에서 추론 (가장 신뢰할 수 있는 소스)
|
||||
if (config?.fieldMappings && config.fieldMappings.length > 0) {
|
||||
// config.valueField가 fieldMappings의 sourceField에 있으면 사용
|
||||
if (config?.valueField) {
|
||||
const hasValueFieldInMappings = config.fieldMappings.some(
|
||||
(m: any) => m.sourceField === config.valueField
|
||||
);
|
||||
if (hasValueFieldInMappings) {
|
||||
return config.valueField;
|
||||
}
|
||||
// fieldMappings에 없으면 무시하고 추론
|
||||
}
|
||||
|
||||
// _code 또는 _id로 끝나는 필드 우선 (보통 PK나 코드 필드)
|
||||
const codeMapping = config.fieldMappings.find(
|
||||
(m: any) => m.sourceField?.endsWith("_code") || m.sourceField?.endsWith("_id")
|
||||
);
|
||||
if (codeMapping) {
|
||||
return codeMapping.sourceField;
|
||||
}
|
||||
|
||||
// 없으면 첫 번째 매핑 사용
|
||||
return config.fieldMappings[0].sourceField || "";
|
||||
}
|
||||
|
||||
// fieldMappings가 없으면 기존 방식
|
||||
if (config?.valueField) return config.valueField;
|
||||
if (propValueField) return propValueField;
|
||||
|
||||
return "";
|
||||
};
|
||||
const valueField = getValueField();
|
||||
|
||||
const searchFields = config?.searchFields || propSearchFields || displayFields; // 검색 필드도 다중 표시 필드 사용
|
||||
const placeholder = config?.placeholder || propPlaceholder || "검색...";
|
||||
|
||||
@@ -76,11 +111,39 @@ export function AutocompleteSearchInputComponent({
|
||||
// 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지)
|
||||
const selectedDataRef = useRef<EntitySearchResult | null>(null);
|
||||
const inputValueRef = useRef<string>("");
|
||||
const initialValueLoadedRef = useRef<string | null>(null); // 초기값 로드 추적
|
||||
|
||||
// formData에서 현재 값 가져오기 (isInteractive 모드)
|
||||
const currentValue = isInteractive && formData && component?.columnName
|
||||
? formData[component.columnName]
|
||||
: value;
|
||||
// 우선순위: 1) component.columnName, 2) fieldMappings에서 valueField에 매핑된 targetField
|
||||
const getCurrentValue = () => {
|
||||
if (!isInteractive || !formData) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 1. component.columnName으로 직접 바인딩된 경우
|
||||
if (component?.columnName && formData[component.columnName] !== undefined) {
|
||||
return formData[component.columnName];
|
||||
}
|
||||
|
||||
// 2. fieldMappings에서 valueField와 매핑된 targetField에서 값 가져오기
|
||||
if (config?.fieldMappings && Array.isArray(config.fieldMappings)) {
|
||||
const valueFieldMapping = config.fieldMappings.find(
|
||||
(mapping: any) => mapping.sourceField === valueField
|
||||
);
|
||||
|
||||
if (valueFieldMapping) {
|
||||
const targetField = valueFieldMapping.targetField || valueFieldMapping.targetColumn;
|
||||
|
||||
if (targetField && formData[targetField] !== undefined) {
|
||||
return formData[targetField];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const currentValue = getCurrentValue();
|
||||
|
||||
// selectedData 변경 시 ref도 업데이트
|
||||
useEffect(() => {
|
||||
@@ -98,6 +161,79 @@ export function AutocompleteSearchInputComponent({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 초기값이 있을 때 해당 값의 표시 텍스트를 조회하여 설정
|
||||
useEffect(() => {
|
||||
const loadInitialDisplayValue = async () => {
|
||||
// 이미 로드된 값이거나, 값이 없거나, 이미 선택된 데이터가 있으면 스킵
|
||||
if (!currentValue || selectedData || selectedDataRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미 같은 값을 로드한 적이 있으면 스킵
|
||||
if (initialValueLoadedRef.current === currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 테이블명과 필드 정보가 없으면 스킵
|
||||
if (!tableName || !valueField) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔄 AutocompleteSearchInput 초기값 로드:", {
|
||||
currentValue,
|
||||
tableName,
|
||||
valueField,
|
||||
displayFields,
|
||||
});
|
||||
|
||||
try {
|
||||
// API를 통해 해당 값의 표시 텍스트 조회
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const filterConditionWithValue = {
|
||||
...filterCondition,
|
||||
[valueField]: currentValue,
|
||||
};
|
||||
|
||||
const params = new URLSearchParams({
|
||||
searchText: "",
|
||||
searchFields: searchFields.join(","),
|
||||
filterCondition: JSON.stringify(filterConditionWithValue),
|
||||
page: "1",
|
||||
limit: "10",
|
||||
});
|
||||
|
||||
const response = await apiClient.get<{ success: boolean; data: EntitySearchResult[] }>(
|
||||
`/entity-search/${tableName}?${params.toString()}`
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||
const matchedItem = response.data.data.find((item: EntitySearchResult) =>
|
||||
String(item[valueField]) === String(currentValue)
|
||||
);
|
||||
|
||||
if (matchedItem) {
|
||||
const displayText = getDisplayValue(matchedItem);
|
||||
console.log("✅ 초기값 표시 텍스트 로드 성공:", {
|
||||
currentValue,
|
||||
displayText,
|
||||
matchedItem,
|
||||
});
|
||||
|
||||
setSelectedData(matchedItem);
|
||||
setInputValue(displayText);
|
||||
selectedDataRef.current = matchedItem;
|
||||
inputValueRef.current = displayText;
|
||||
initialValueLoadedRef.current = currentValue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 초기값 표시 텍스트 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadInitialDisplayValue();
|
||||
}, [currentValue, tableName, valueField, displayFields, filterCondition, searchFields, selectedData]);
|
||||
|
||||
// value가 변경되면 표시값 업데이트 - 단, selectedData가 있으면 유지
|
||||
useEffect(() => {
|
||||
// selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우)
|
||||
@@ -107,6 +243,7 @@ export function AutocompleteSearchInputComponent({
|
||||
|
||||
if (!currentValue) {
|
||||
setInputValue("");
|
||||
initialValueLoadedRef.current = null; // 값이 없어지면 초기화
|
||||
}
|
||||
}, [currentValue, selectedData]);
|
||||
|
||||
|
||||
@@ -299,6 +299,20 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
// 🆕 modalDataStore에서 선택된 데이터 확인 (분할 패널 등에서 저장됨)
|
||||
const [modalStoreData, setModalStoreData] = useState<Record<string, any[]>>({});
|
||||
|
||||
// 🆕 splitPanelContext?.selectedLeftData를 로컬 상태로 추적 (리렌더링 보장)
|
||||
const [trackedSelectedLeftData, setTrackedSelectedLeftData] = useState<Record<string, any> | null>(null);
|
||||
|
||||
// splitPanelContext?.selectedLeftData 변경 감지 및 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
const newData = splitPanelContext?.selectedLeftData ?? null;
|
||||
setTrackedSelectedLeftData(newData);
|
||||
console.log("🔄 [ButtonPrimary] selectedLeftData 변경 감지:", {
|
||||
label: component.label,
|
||||
hasData: !!newData,
|
||||
dataKeys: newData ? Object.keys(newData) : [],
|
||||
});
|
||||
}, [splitPanelContext?.selectedLeftData, component.label]);
|
||||
|
||||
// modalDataStore 상태 구독 (실시간 업데이트)
|
||||
useEffect(() => {
|
||||
const actionConfig = component.componentConfig?.action;
|
||||
@@ -357,8 +371,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
|
||||
// 2. 분할 패널 좌측 선택 데이터 확인
|
||||
if (rowSelectionSource === "auto" || rowSelectionSource === "splitPanelLeft") {
|
||||
// SplitPanelContext에서 확인
|
||||
if (splitPanelContext?.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0) {
|
||||
// SplitPanelContext에서 확인 (trackedSelectedLeftData 사용으로 리렌더링 보장)
|
||||
if (trackedSelectedLeftData && Object.keys(trackedSelectedLeftData).length > 0) {
|
||||
if (!hasSelection) {
|
||||
hasSelection = true;
|
||||
selectionCount = 1;
|
||||
@@ -397,7 +411,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
selectionCount,
|
||||
selectionSource,
|
||||
hasSplitPanelContext: !!splitPanelContext,
|
||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||
trackedSelectedLeftData: trackedSelectedLeftData,
|
||||
selectedRowsData: selectedRowsData?.length,
|
||||
selectedRows: selectedRows?.length,
|
||||
flowSelectedData: flowSelectedData?.length,
|
||||
@@ -429,7 +443,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
component.label,
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
splitPanelContext?.selectedLeftData,
|
||||
trackedSelectedLeftData,
|
||||
flowSelectedData,
|
||||
splitPanelContext,
|
||||
modalStoreData,
|
||||
|
||||
@@ -2043,7 +2043,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
return row.id || row.uuid || `row-${index}`;
|
||||
};
|
||||
|
||||
const handleRowSelection = (rowKey: string, checked: boolean) => {
|
||||
const handleRowSelection = (rowKey: string, checked: boolean, rowData?: any) => {
|
||||
const newSelectedRows = new Set(selectedRows);
|
||||
if (checked) {
|
||||
newSelectedRows.add(rowKey);
|
||||
@@ -2086,6 +2086,31 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장/해제 (체크박스 선택 시에도 작동)
|
||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (checked && selectedRowsData.length > 0) {
|
||||
// 선택된 경우: 첫 번째 선택된 데이터 저장 (또는 전달된 rowData)
|
||||
const dataToStore = rowData || selectedRowsData[selectedRowsData.length - 1];
|
||||
splitPanelContext.setSelectedLeftData(dataToStore);
|
||||
console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 저장:", {
|
||||
rowKey,
|
||||
dataToStore,
|
||||
});
|
||||
} else if (!checked && selectedRowsData.length === 0) {
|
||||
// 모든 선택이 해제된 경우: 데이터 초기화
|
||||
splitPanelContext.setSelectedLeftData(null);
|
||||
console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 초기화");
|
||||
} else if (selectedRowsData.length > 0) {
|
||||
// 일부 선택 해제된 경우: 남은 첫 번째 데이터로 업데이트
|
||||
splitPanelContext.setSelectedLeftData(selectedRowsData[0]);
|
||||
console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 업데이트:", {
|
||||
remainingCount: selectedRowsData.length,
|
||||
firstData: selectedRowsData[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index)));
|
||||
setIsAllSelected(allRowsSelected && filteredData.length > 0);
|
||||
};
|
||||
@@ -2155,35 +2180,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
const rowKey = getRowKey(row, index);
|
||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||
|
||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
||||
|
||||
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
|
||||
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
|
||||
// currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음)
|
||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
||||
|
||||
console.log("🔗 [TableList] 행 클릭 - 분할 패널 위치 확인:", {
|
||||
splitPanelPosition,
|
||||
currentSplitPosition,
|
||||
effectiveSplitPosition,
|
||||
hasSplitPanelContext: !!splitPanelContext,
|
||||
disableAutoDataTransfer: splitPanelContext?.disableAutoDataTransfer,
|
||||
});
|
||||
|
||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (!isCurrentlySelected) {
|
||||
// 선택된 경우: 데이터 저장
|
||||
splitPanelContext.setSelectedLeftData(row);
|
||||
console.log("🔗 [TableList] 분할 패널 좌측 데이터 저장:", {
|
||||
row,
|
||||
parentDataMapping: splitPanelContext.parentDataMapping,
|
||||
});
|
||||
} else {
|
||||
// 선택 해제된 경우: 데이터 초기화
|
||||
splitPanelContext.setSelectedLeftData(null);
|
||||
console.log("🔗 [TableList] 분할 패널 좌측 데이터 초기화");
|
||||
}
|
||||
}
|
||||
// handleRowSelection에서 분할 패널 데이터 처리도 함께 수행됨
|
||||
handleRowSelection(rowKey, !isCurrentlySelected, row);
|
||||
|
||||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||
};
|
||||
@@ -3918,7 +3916,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
if (enterRow) {
|
||||
const rowKey = getRowKey(enterRow, rowIndex);
|
||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
||||
handleRowSelection(rowKey, !isCurrentlySelected, enterRow);
|
||||
}
|
||||
break;
|
||||
case " ": // Space
|
||||
@@ -3928,7 +3926,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
if (spaceRow) {
|
||||
const currentRowKey = getRowKey(spaceRow, rowIndex);
|
||||
const isChecked = selectedRows.has(currentRowKey);
|
||||
handleRowSelection(currentRowKey, !isChecked);
|
||||
handleRowSelection(currentRowKey, !isChecked, spaceRow);
|
||||
}
|
||||
break;
|
||||
case "F2":
|
||||
@@ -4142,7 +4140,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
return (
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)}
|
||||
onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean, row)}
|
||||
aria-label={`행 ${index + 1} 선택`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -216,6 +216,12 @@ export function UniversalFormModalComponent({
|
||||
|
||||
// 초기화 - 최초 마운트 시 또는 initialData가 변경되었을 때 실행
|
||||
useEffect(() => {
|
||||
console.log("[UniversalFormModal] useEffect 시작", {
|
||||
initialData,
|
||||
hasInitialized: hasInitialized.current,
|
||||
lastInitializedId: lastInitializedId.current,
|
||||
});
|
||||
|
||||
// initialData에서 ID 값 추출 (id, ID, objid 등)
|
||||
const currentId = initialData?.id || initialData?.ID || initialData?.objid;
|
||||
const currentIdString = currentId !== undefined ? String(currentId) : undefined;
|
||||
@@ -229,10 +235,21 @@ export function UniversalFormModalComponent({
|
||||
if (hasInitialized.current && lastInitializedId.current === currentIdString) {
|
||||
// 생성 모드에서 데이터가 새로 전달된 경우는 재초기화 필요
|
||||
if (!createModeDataHash || capturedInitialData.current) {
|
||||
console.log("[UniversalFormModal] 초기화 스킵 - 이미 초기화됨");
|
||||
// 🆕 채번 플래그가 true인데 formData에 값이 없으면 재생성 필요
|
||||
// (컴포넌트 remount로 인해 state가 초기화된 경우)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 컴포넌트 remount 감지: hasInitialized가 true인데 formData가 비어있으면 재초기화
|
||||
// (React의 Strict Mode나 EmbeddedScreen 리렌더링으로 인한 remount)
|
||||
if (hasInitialized.current && !currentIdString) {
|
||||
console.log("[UniversalFormModal] 컴포넌트 remount 감지 - 채번 플래그 초기화");
|
||||
numberingGeneratedRef.current = false;
|
||||
isGeneratingRef.current = false;
|
||||
}
|
||||
|
||||
// 🆕 수정 모드: initialData에 데이터가 있으면서 ID가 변경된 경우 재초기화
|
||||
if (hasInitialized.current && currentIdString && lastInitializedId.current !== currentIdString) {
|
||||
console.log("[UniversalFormModal] ID 변경 감지 - 재초기화:", {
|
||||
@@ -252,6 +269,7 @@ export function UniversalFormModalComponent({
|
||||
console.log("[UniversalFormModal] 초기 데이터 캡처:", capturedInitialData.current);
|
||||
}
|
||||
|
||||
console.log("[UniversalFormModal] initializeForm 호출 예정");
|
||||
hasInitialized.current = true;
|
||||
initializeForm();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -389,6 +407,94 @@ export function UniversalFormModalComponent({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config.sections]);
|
||||
|
||||
// 채번규칙 자동 생성 (중복 호출 방지)
|
||||
// 중요: initializeForm에서 호출되므로 반드시 initializeForm보다 먼저 선언해야 함
|
||||
const numberingGeneratedRef = useRef(false);
|
||||
const isGeneratingRef = useRef(false); // 진행 중 플래그 추가
|
||||
|
||||
const generateNumberingValues = useCallback(
|
||||
async (currentFormData: FormDataState) => {
|
||||
// 이미 생성되었거나 진행 중이면 스킵
|
||||
if (numberingGeneratedRef.current) {
|
||||
console.log("[채번] 이미 생성됨 - 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGeneratingRef.current) {
|
||||
console.log("[채번] 생성 진행 중 - 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
isGeneratingRef.current = true; // 진행 중 표시
|
||||
console.log("[채번] 생성 시작", { sectionsCount: config.sections.length });
|
||||
|
||||
const updatedData = { ...currentFormData };
|
||||
let hasChanges = false;
|
||||
|
||||
for (const section of config.sections) {
|
||||
console.log("[채번] 섹션 검사:", section.title, { type: section.type, repeatable: section.repeatable, fieldsCount: section.fields?.length });
|
||||
if (section.repeatable || section.type === "table") continue;
|
||||
|
||||
for (const field of (section.fields || [])) {
|
||||
// generateOnOpen은 기본값 true (undefined일 경우 true로 처리)
|
||||
const shouldGenerateOnOpen = field.numberingRule?.generateOnOpen !== false;
|
||||
console.log("[채번] 필드 검사:", field.columnName, {
|
||||
hasNumberingRule: !!field.numberingRule,
|
||||
enabled: field.numberingRule?.enabled,
|
||||
generateOnOpen: field.numberingRule?.generateOnOpen,
|
||||
shouldGenerateOnOpen,
|
||||
ruleId: field.numberingRule?.ruleId,
|
||||
currentValue: updatedData[field.columnName],
|
||||
});
|
||||
if (
|
||||
field.numberingRule?.enabled &&
|
||||
shouldGenerateOnOpen &&
|
||||
field.numberingRule?.ruleId &&
|
||||
!updatedData[field.columnName]
|
||||
) {
|
||||
try {
|
||||
console.log(`[채번 미리보기 API 호출] ${field.columnName}, ruleId: ${field.numberingRule.ruleId}`);
|
||||
// generateOnOpen: 미리보기만 표시 (DB 시퀀스 증가 안 함)
|
||||
const response = await previewNumberingCode(field.numberingRule.ruleId);
|
||||
if (response.success && response.data?.generatedCode) {
|
||||
updatedData[field.columnName] = response.data.generatedCode;
|
||||
|
||||
// 저장 시 실제 할당을 위해 ruleId 저장 (TextInput과 동일한 키 형식)
|
||||
const ruleIdKey = `${field.columnName}_numberingRuleId`;
|
||||
updatedData[ruleIdKey] = field.numberingRule.ruleId;
|
||||
|
||||
hasChanges = true;
|
||||
numberingGeneratedRef.current = true; // 생성 완료 표시
|
||||
console.log(
|
||||
`[채번 미리보기 완료] ${field.columnName} = ${response.data.generatedCode} (저장 시 실제 할당)`,
|
||||
);
|
||||
console.log(`[채번 규칙 ID 저장] ${ruleIdKey} = ${field.numberingRule.ruleId}`);
|
||||
|
||||
// 부모 컴포넌트에도 ruleId 전달 (ModalRepeaterTable → ScreenModal)
|
||||
if (onChange) {
|
||||
onChange({
|
||||
...updatedData,
|
||||
[ruleIdKey]: field.numberingRule.ruleId,
|
||||
});
|
||||
console.log(`[채번] 부모에게 ruleId 전달: ${ruleIdKey}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`채번규칙 미리보기 실패 (${field.columnName}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isGeneratingRef.current = false; // 진행 완료
|
||||
|
||||
if (hasChanges) {
|
||||
setFormData(updatedData);
|
||||
}
|
||||
},
|
||||
[config, onChange],
|
||||
);
|
||||
|
||||
// 폼 초기화
|
||||
const initializeForm = useCallback(async () => {
|
||||
console.log("[initializeForm] 시작");
|
||||
@@ -585,82 +691,6 @@ export function UniversalFormModalComponent({
|
||||
return item;
|
||||
};
|
||||
|
||||
// 채번규칙 자동 생성 (중복 호출 방지)
|
||||
const numberingGeneratedRef = useRef(false);
|
||||
const isGeneratingRef = useRef(false); // 진행 중 플래그 추가
|
||||
|
||||
const generateNumberingValues = useCallback(
|
||||
async (currentFormData: FormDataState) => {
|
||||
// 이미 생성되었거나 진행 중이면 스킵
|
||||
if (numberingGeneratedRef.current) {
|
||||
console.log("[채번] 이미 생성됨 - 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGeneratingRef.current) {
|
||||
console.log("[채번] 생성 진행 중 - 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
isGeneratingRef.current = true; // 진행 중 표시
|
||||
console.log("[채번] 생성 시작");
|
||||
|
||||
const updatedData = { ...currentFormData };
|
||||
let hasChanges = false;
|
||||
|
||||
for (const section of config.sections) {
|
||||
if (section.repeatable || section.type === "table") continue;
|
||||
|
||||
for (const field of (section.fields || [])) {
|
||||
if (
|
||||
field.numberingRule?.enabled &&
|
||||
field.numberingRule?.generateOnOpen &&
|
||||
field.numberingRule?.ruleId &&
|
||||
!updatedData[field.columnName]
|
||||
) {
|
||||
try {
|
||||
console.log(`[채번 미리보기 API 호출] ${field.columnName}, ruleId: ${field.numberingRule.ruleId}`);
|
||||
// generateOnOpen: 미리보기만 표시 (DB 시퀀스 증가 안 함)
|
||||
const response = await previewNumberingCode(field.numberingRule.ruleId);
|
||||
if (response.success && response.data?.generatedCode) {
|
||||
updatedData[field.columnName] = response.data.generatedCode;
|
||||
|
||||
// 저장 시 실제 할당을 위해 ruleId 저장 (TextInput과 동일한 키 형식)
|
||||
const ruleIdKey = `${field.columnName}_numberingRuleId`;
|
||||
updatedData[ruleIdKey] = field.numberingRule.ruleId;
|
||||
|
||||
hasChanges = true;
|
||||
numberingGeneratedRef.current = true; // 생성 완료 표시
|
||||
console.log(
|
||||
`[채번 미리보기 완료] ${field.columnName} = ${response.data.generatedCode} (저장 시 실제 할당)`,
|
||||
);
|
||||
console.log(`[채번 규칙 ID 저장] ${ruleIdKey} = ${field.numberingRule.ruleId}`);
|
||||
|
||||
// 부모 컴포넌트에도 ruleId 전달 (ModalRepeaterTable → ScreenModal)
|
||||
if (onChange) {
|
||||
onChange({
|
||||
...updatedData,
|
||||
[ruleIdKey]: field.numberingRule.ruleId,
|
||||
});
|
||||
console.log(`[채번] 부모에게 ruleId 전달: ${ruleIdKey}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`채번규칙 미리보기 실패 (${field.columnName}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isGeneratingRef.current = false; // 진행 완료
|
||||
|
||||
if (hasChanges) {
|
||||
setFormData(updatedData);
|
||||
}
|
||||
},
|
||||
[config, onChange],
|
||||
);
|
||||
|
||||
// 필드 값 변경 핸들러
|
||||
const handleFieldChange = useCallback(
|
||||
(columnName: string, value: any) => {
|
||||
|
||||
@@ -995,6 +995,40 @@ export class ButtonActionExecutor {
|
||||
console.log("📋 [handleSave] 범용 폼 모달 공통 필드:", commonFields);
|
||||
}
|
||||
|
||||
// 🆕 루트 레벨 formData에서 RepeaterFieldGroup에 전달할 공통 필드 추출
|
||||
// 주문번호, 발주번호 등 마스터-디테일 관계에서 필요한 필드만 명시적으로 지정
|
||||
const masterDetailFields = [
|
||||
// 번호 필드
|
||||
"order_no", // 발주번호
|
||||
"sales_order_no", // 수주번호
|
||||
"shipment_no", // 출하번호
|
||||
"receipt_no", // 입고번호
|
||||
"work_order_no", // 작업지시번호
|
||||
// 거래처 필드
|
||||
"supplier_code", // 공급처 코드
|
||||
"supplier_name", // 공급처 이름
|
||||
"customer_code", // 고객 코드
|
||||
"customer_name", // 고객 이름
|
||||
// 날짜 필드
|
||||
"order_date", // 발주일
|
||||
"sales_date", // 수주일
|
||||
"shipment_date", // 출하일
|
||||
"receipt_date", // 입고일
|
||||
"due_date", // 납기일
|
||||
// 담당자/메모 필드
|
||||
"manager", // 담당자
|
||||
"memo", // 메모
|
||||
"remark", // 비고
|
||||
];
|
||||
|
||||
for (const fieldName of masterDetailFields) {
|
||||
const value = context.formData[fieldName];
|
||||
if (value !== undefined && value !== "" && value !== null && !(fieldName in commonFields)) {
|
||||
commonFields[fieldName] = value;
|
||||
}
|
||||
}
|
||||
console.log("📋 [handleSave] 최종 공통 필드 (마스터-디테일 필드 포함):", commonFields);
|
||||
|
||||
for (const item of parsedData) {
|
||||
// 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리)
|
||||
|
||||
@@ -5931,6 +5965,69 @@ export class ButtonActionExecutor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ allComponents가 있으면 기존 필수 항목 검증 수행
|
||||
if (context.allComponents && context.allComponents.length > 0) {
|
||||
console.log("🔍 [handleQuickInsert] 필수 항목 검증 시작:", {
|
||||
hasAllComponents: !!context.allComponents,
|
||||
allComponentsLength: context.allComponents?.length || 0,
|
||||
});
|
||||
const requiredValidation = this.validateRequiredFields(context);
|
||||
if (!requiredValidation.isValid) {
|
||||
console.log("❌ [handleQuickInsert] 필수 항목 누락:", requiredValidation.missingFields);
|
||||
toast.error(`필수 항목을 입력해주세요: ${requiredValidation.missingFields.join(", ")}`);
|
||||
return false;
|
||||
}
|
||||
console.log("✅ [handleQuickInsert] 필수 항목 검증 통과");
|
||||
}
|
||||
|
||||
// ✅ quickInsert 전용 검증: component 타입 매핑에서 값이 비어있는지 확인
|
||||
const mappingsForValidation = quickInsertConfig.columnMappings || [];
|
||||
const missingMappingFields: string[] = [];
|
||||
|
||||
for (const mapping of mappingsForValidation) {
|
||||
// component 타입 매핑은 필수 입력으로 간주
|
||||
if (mapping.sourceType === "component" && mapping.sourceComponentId) {
|
||||
let value: any = undefined;
|
||||
|
||||
// 값 가져오기 (formData에서)
|
||||
if (mapping.sourceColumnName) {
|
||||
value = context.formData?.[mapping.sourceColumnName];
|
||||
}
|
||||
if (value === undefined || value === null) {
|
||||
value = context.formData?.[mapping.sourceComponentId];
|
||||
}
|
||||
// allComponents에서 컴포넌트 찾아서 columnName으로 시도
|
||||
if ((value === undefined || value === null) && context.allComponents) {
|
||||
const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
||||
if (comp?.columnName) {
|
||||
value = context.formData?.[comp.columnName];
|
||||
}
|
||||
}
|
||||
// targetColumn으로 폴백
|
||||
if ((value === undefined || value === null) && mapping.targetColumn) {
|
||||
value = context.formData?.[mapping.targetColumn];
|
||||
}
|
||||
|
||||
// 값이 비어있으면 필수 누락으로 처리
|
||||
if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
|
||||
console.log("❌ [handleQuickInsert] component 매핑 값 누락:", {
|
||||
targetColumn: mapping.targetColumn,
|
||||
sourceComponentId: mapping.sourceComponentId,
|
||||
sourceColumnName: mapping.sourceColumnName,
|
||||
value,
|
||||
});
|
||||
missingMappingFields.push(mapping.targetColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingMappingFields.length > 0) {
|
||||
console.log("❌ [handleQuickInsert] 필수 입력 항목 누락:", missingMappingFields);
|
||||
toast.error(`다음 항목을 입력해주세요: ${missingMappingFields.join(", ")}`);
|
||||
return false;
|
||||
}
|
||||
console.log("✅ [handleQuickInsert] quickInsert 매핑 검증 통과");
|
||||
|
||||
const { formData, splitPanelContext, userId, userName, companyCode } = context;
|
||||
|
||||
console.log("⚡ Quick Insert 상세 정보:", {
|
||||
|
||||
@@ -1689,3 +1689,4 @@ const 출고등록_설정: ScreenSplitPanel = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -536,3 +536,4 @@ const { data: config } = await getScreenSplitPanel(screenId);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -523,3 +523,4 @@ function ScreenViewPage() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user