Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
d799e1334e
|
|
@ -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 (userType === "COMPANY_ADMIN") {
|
||||||
// 회사 관리자: 권한 그룹 기반 필터링 적용
|
// 회사 관리자: 권한 그룹 기반 필터링 적용
|
||||||
if (userRoleGroups.length > 0) {
|
if (userRoleGroups.length > 0) {
|
||||||
|
|
@ -141,6 +148,7 @@ export class AdminService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
} else if (
|
} else if (
|
||||||
menuType !== undefined &&
|
menuType !== undefined &&
|
||||||
userType === "SUPER_ADMIN" &&
|
userType === "SUPER_ADMIN" &&
|
||||||
|
|
@ -412,6 +420,15 @@ export class AdminService {
|
||||||
let queryParams: any[] = [userLang];
|
let queryParams: any[] = [userLang];
|
||||||
let paramIndex = 2;
|
let paramIndex = 2;
|
||||||
|
|
||||||
|
// [임시 비활성화] 메뉴 권한 그룹 체크 - 모든 사용자에게 전체 메뉴 표시
|
||||||
|
// TODO: 권한 체크 다시 활성화 필요
|
||||||
|
logger.info(
|
||||||
|
`⚠️ [임시 비활성화] getUserMenuList 권한 그룹 체크 스킵 - 사용자 ${userId}(${userType})에게 전체 메뉴 표시`
|
||||||
|
);
|
||||||
|
authFilter = "";
|
||||||
|
unionFilter = "";
|
||||||
|
|
||||||
|
/* [원본 코드 - getUserMenuList 권한 그룹 체크]
|
||||||
if (userType === "SUPER_ADMIN") {
|
if (userType === "SUPER_ADMIN") {
|
||||||
// SUPER_ADMIN: 권한 그룹 체크 없이 해당 회사의 모든 메뉴 표시
|
// SUPER_ADMIN: 권한 그룹 체크 없이 해당 회사의 모든 메뉴 표시
|
||||||
logger.info(`✅ 좌측 사이드바 (SUPER_ADMIN): 회사 ${userCompanyCode}의 모든 메뉴 표시`);
|
logger.info(`✅ 좌측 사이드바 (SUPER_ADMIN): 회사 ${userCompanyCode}의 모든 메뉴 표시`);
|
||||||
|
|
@ -471,6 +488,7 @@ export class AdminService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// 2. 회사별 필터링 조건 생성
|
// 2. 회사별 필터링 조건 생성
|
||||||
let companyFilter = "";
|
let companyFilter = "";
|
||||||
|
|
|
||||||
|
|
@ -2409,11 +2409,19 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SET 절 생성 (수정할 데이터) - 먼저 생성
|
// SET 절 생성 (수정할 데이터) - 먼저 생성
|
||||||
|
// 🔧 테이블에 존재하는 컬럼만 UPDATE (가상 컬럼 제외)
|
||||||
const setConditions: string[] = [];
|
const setConditions: string[] = [];
|
||||||
const setValues: any[] = [];
|
const setValues: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
const skippedColumns: string[] = [];
|
||||||
|
|
||||||
Object.keys(updatedData).forEach((column) => {
|
Object.keys(updatedData).forEach((column) => {
|
||||||
|
// 테이블에 존재하지 않는 컬럼은 스킵
|
||||||
|
if (!columnTypeMap.has(column)) {
|
||||||
|
skippedColumns.push(column);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dataType = columnTypeMap.get(column) || "text";
|
const dataType = columnTypeMap.get(column) || "text";
|
||||||
setConditions.push(
|
setConditions.push(
|
||||||
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
||||||
|
|
@ -2424,6 +2432,10 @@ export class TableManagementService {
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (skippedColumns.length > 0) {
|
||||||
|
logger.info(`⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
|
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
|
||||||
let whereConditions: string[] = [];
|
let whereConditions: string[] = [];
|
||||||
let whereValues: any[] = [];
|
let whereValues: any[] = [];
|
||||||
|
|
@ -3930,9 +3942,10 @@ export class TableManagementService {
|
||||||
`컬럼 입력타입 정보 조회: ${tableName}, company: ${companyCode}`
|
`컬럼 입력타입 정보 조회: ${tableName}, company: ${companyCode}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// table_type_columns에서 입력타입 정보 조회 (company_code 필터링)
|
// table_type_columns에서 입력타입 정보 조회
|
||||||
|
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
||||||
const rawInputTypes = await query<any>(
|
const rawInputTypes = await query<any>(
|
||||||
`SELECT
|
`SELECT DISTINCT ON (ttc.column_name)
|
||||||
ttc.column_name as "columnName",
|
ttc.column_name as "columnName",
|
||||||
COALESCE(cl.column_label, ttc.column_name) as "displayName",
|
COALESCE(cl.column_label, ttc.column_name) as "displayName",
|
||||||
ttc.input_type as "inputType",
|
ttc.input_type as "inputType",
|
||||||
|
|
@ -3946,8 +3959,10 @@ export class TableManagementService {
|
||||||
LEFT JOIN information_schema.columns ic
|
LEFT JOIN information_schema.columns ic
|
||||||
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
|
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
|
||||||
WHERE ttc.table_name = $1
|
WHERE ttc.table_name = $1
|
||||||
AND ttc.company_code = $2
|
AND ttc.company_code IN ($2, '*')
|
||||||
ORDER BY ttc.display_order, ttc.column_name`,
|
ORDER BY ttc.column_name,
|
||||||
|
CASE WHEN ttc.company_code = $2 THEN 0 ELSE 1 END,
|
||||||
|
ttc.display_order`,
|
||||||
[tableName, companyCode]
|
[tableName, companyCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -3961,17 +3976,20 @@ export class TableManagementService {
|
||||||
const mappingTableExists = tableExistsResult[0]?.table_exists === true;
|
const mappingTableExists = tableExistsResult[0]?.table_exists === true;
|
||||||
|
|
||||||
// 카테고리 컬럼의 경우, 매핑된 메뉴 목록 조회
|
// 카테고리 컬럼의 경우, 매핑된 메뉴 목록 조회
|
||||||
|
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
||||||
let categoryMappings: Map<string, number[]> = new Map();
|
let categoryMappings: Map<string, number[]> = new Map();
|
||||||
if (mappingTableExists) {
|
if (mappingTableExists) {
|
||||||
logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });
|
logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });
|
||||||
|
|
||||||
const mappings = await query<any>(
|
const mappings = await query<any>(
|
||||||
`SELECT
|
`SELECT DISTINCT ON (logical_column_name, menu_objid)
|
||||||
logical_column_name as "columnName",
|
logical_column_name as "columnName",
|
||||||
menu_objid as "menuObjid"
|
menu_objid as "menuObjid"
|
||||||
FROM category_column_mapping
|
FROM category_column_mapping
|
||||||
WHERE table_name = $1
|
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]
|
[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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -370,32 +370,13 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 모드 전환 핸들러
|
// 모드 전환 핸들러
|
||||||
const handleModeSwitch = async () => {
|
const handleModeSwitch = () => {
|
||||||
if (isAdminMode) {
|
if (isAdminMode) {
|
||||||
// 관리자 → 사용자 모드: 선택한 회사 유지
|
// 관리자 → 사용자 모드: 선택한 회사 유지
|
||||||
router.push("/main");
|
router.push("/main");
|
||||||
} else {
|
} else {
|
||||||
// 사용자 → 관리자 모드: WACE로 복귀 필요 (SUPER_ADMIN만)
|
// 사용자 → 관리자 모드: 선택한 회사 유지 (회사 전환 없음)
|
||||||
if ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN") {
|
router.push("/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)`,
|
maxWidth: `calc(100% - ${compPosition.x || 0}px)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 formDataVersion을 key에서 제거하여 불필요한 remount 방지
|
||||||
|
// universal-form-modal 같은 컴포넌트가 채번 후 unmount되는 문제 해결
|
||||||
return (
|
return (
|
||||||
<div key={`${component.id}-${formDataVersion}`} className="absolute" style={componentStyle}>
|
<div key={component.id} className="absolute" style={componentStyle}>
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={component}
|
component={component}
|
||||||
isInteractive={true}
|
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 displayField = config?.displayField || propDisplayField || "";
|
||||||
const displayFields = config?.displayFields || (displayField ? [displayField] : []); // 다중 표시 필드
|
const displayFields = config?.displayFields || (displayField ? [displayField] : []); // 다중 표시 필드
|
||||||
const displaySeparator = config?.displaySeparator || " → "; // 구분자
|
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 searchFields = config?.searchFields || propSearchFields || displayFields; // 검색 필드도 다중 표시 필드 사용
|
||||||
const placeholder = config?.placeholder || propPlaceholder || "검색...";
|
const placeholder = config?.placeholder || propPlaceholder || "검색...";
|
||||||
|
|
||||||
|
|
@ -76,11 +111,39 @@ export function AutocompleteSearchInputComponent({
|
||||||
// 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지)
|
// 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지)
|
||||||
const selectedDataRef = useRef<EntitySearchResult | null>(null);
|
const selectedDataRef = useRef<EntitySearchResult | null>(null);
|
||||||
const inputValueRef = useRef<string>("");
|
const inputValueRef = useRef<string>("");
|
||||||
|
const initialValueLoadedRef = useRef<string | null>(null); // 초기값 로드 추적
|
||||||
|
|
||||||
// formData에서 현재 값 가져오기 (isInteractive 모드)
|
// formData에서 현재 값 가져오기 (isInteractive 모드)
|
||||||
const currentValue = isInteractive && formData && component?.columnName
|
// 우선순위: 1) component.columnName, 2) fieldMappings에서 valueField에 매핑된 targetField
|
||||||
? formData[component.columnName]
|
const getCurrentValue = () => {
|
||||||
: value;
|
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도 업데이트
|
// selectedData 변경 시 ref도 업데이트
|
||||||
useEffect(() => {
|
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가 있으면 유지
|
// value가 변경되면 표시값 업데이트 - 단, selectedData가 있으면 유지
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우)
|
// selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우)
|
||||||
|
|
@ -107,6 +243,7 @@ export function AutocompleteSearchInputComponent({
|
||||||
|
|
||||||
if (!currentValue) {
|
if (!currentValue) {
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
|
initialValueLoadedRef.current = null; // 값이 없어지면 초기화
|
||||||
}
|
}
|
||||||
}, [currentValue, selectedData]);
|
}, [currentValue, selectedData]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,20 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
// 🆕 modalDataStore에서 선택된 데이터 확인 (분할 패널 등에서 저장됨)
|
// 🆕 modalDataStore에서 선택된 데이터 확인 (분할 패널 등에서 저장됨)
|
||||||
const [modalStoreData, setModalStoreData] = useState<Record<string, any[]>>({});
|
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 상태 구독 (실시간 업데이트)
|
// modalDataStore 상태 구독 (실시간 업데이트)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const actionConfig = component.componentConfig?.action;
|
const actionConfig = component.componentConfig?.action;
|
||||||
|
|
@ -357,8 +371,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
|
|
||||||
// 2. 분할 패널 좌측 선택 데이터 확인
|
// 2. 분할 패널 좌측 선택 데이터 확인
|
||||||
if (rowSelectionSource === "auto" || rowSelectionSource === "splitPanelLeft") {
|
if (rowSelectionSource === "auto" || rowSelectionSource === "splitPanelLeft") {
|
||||||
// SplitPanelContext에서 확인
|
// SplitPanelContext에서 확인 (trackedSelectedLeftData 사용으로 리렌더링 보장)
|
||||||
if (splitPanelContext?.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0) {
|
if (trackedSelectedLeftData && Object.keys(trackedSelectedLeftData).length > 0) {
|
||||||
if (!hasSelection) {
|
if (!hasSelection) {
|
||||||
hasSelection = true;
|
hasSelection = true;
|
||||||
selectionCount = 1;
|
selectionCount = 1;
|
||||||
|
|
@ -397,7 +411,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
selectionCount,
|
selectionCount,
|
||||||
selectionSource,
|
selectionSource,
|
||||||
hasSplitPanelContext: !!splitPanelContext,
|
hasSplitPanelContext: !!splitPanelContext,
|
||||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
trackedSelectedLeftData: trackedSelectedLeftData,
|
||||||
selectedRowsData: selectedRowsData?.length,
|
selectedRowsData: selectedRowsData?.length,
|
||||||
selectedRows: selectedRows?.length,
|
selectedRows: selectedRows?.length,
|
||||||
flowSelectedData: flowSelectedData?.length,
|
flowSelectedData: flowSelectedData?.length,
|
||||||
|
|
@ -429,7 +443,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
component.label,
|
component.label,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
splitPanelContext?.selectedLeftData,
|
trackedSelectedLeftData,
|
||||||
flowSelectedData,
|
flowSelectedData,
|
||||||
splitPanelContext,
|
splitPanelContext,
|
||||||
modalStoreData,
|
modalStoreData,
|
||||||
|
|
|
||||||
|
|
@ -2043,7 +2043,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return row.id || row.uuid || `row-${index}`;
|
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);
|
const newSelectedRows = new Set(selectedRows);
|
||||||
if (checked) {
|
if (checked) {
|
||||||
newSelectedRows.add(rowKey);
|
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)));
|
const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index)));
|
||||||
setIsAllSelected(allRowsSelected && filteredData.length > 0);
|
setIsAllSelected(allRowsSelected && filteredData.length > 0);
|
||||||
};
|
};
|
||||||
|
|
@ -2155,35 +2180,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const rowKey = getRowKey(row, index);
|
const rowKey = getRowKey(row, index);
|
||||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||||
|
|
||||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
// handleRowSelection에서 분할 패널 데이터 처리도 함께 수행됨
|
||||||
|
handleRowSelection(rowKey, !isCurrentlySelected, row);
|
||||||
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
|
|
||||||
// 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] 분할 패널 좌측 데이터 초기화");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||||
};
|
};
|
||||||
|
|
@ -3918,7 +3916,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
if (enterRow) {
|
if (enterRow) {
|
||||||
const rowKey = getRowKey(enterRow, rowIndex);
|
const rowKey = getRowKey(enterRow, rowIndex);
|
||||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
handleRowSelection(rowKey, !isCurrentlySelected, enterRow);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case " ": // Space
|
case " ": // Space
|
||||||
|
|
@ -3928,7 +3926,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
if (spaceRow) {
|
if (spaceRow) {
|
||||||
const currentRowKey = getRowKey(spaceRow, rowIndex);
|
const currentRowKey = getRowKey(spaceRow, rowIndex);
|
||||||
const isChecked = selectedRows.has(currentRowKey);
|
const isChecked = selectedRows.has(currentRowKey);
|
||||||
handleRowSelection(currentRowKey, !isChecked);
|
handleRowSelection(currentRowKey, !isChecked, spaceRow);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "F2":
|
case "F2":
|
||||||
|
|
@ -4142,7 +4140,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)}
|
onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean, row)}
|
||||||
aria-label={`행 ${index + 1} 선택`}
|
aria-label={`행 ${index + 1} 선택`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,12 @@ export function UniversalFormModalComponent({
|
||||||
|
|
||||||
// 초기화 - 최초 마운트 시 또는 initialData가 변경되었을 때 실행
|
// 초기화 - 최초 마운트 시 또는 initialData가 변경되었을 때 실행
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("[UniversalFormModal] useEffect 시작", {
|
||||||
|
initialData,
|
||||||
|
hasInitialized: hasInitialized.current,
|
||||||
|
lastInitializedId: lastInitializedId.current,
|
||||||
|
});
|
||||||
|
|
||||||
// initialData에서 ID 값 추출 (id, ID, objid 등)
|
// initialData에서 ID 값 추출 (id, ID, objid 등)
|
||||||
const currentId = initialData?.id || initialData?.ID || initialData?.objid;
|
const currentId = initialData?.id || initialData?.ID || initialData?.objid;
|
||||||
const currentIdString = currentId !== undefined ? String(currentId) : undefined;
|
const currentIdString = currentId !== undefined ? String(currentId) : undefined;
|
||||||
|
|
@ -229,10 +235,21 @@ export function UniversalFormModalComponent({
|
||||||
if (hasInitialized.current && lastInitializedId.current === currentIdString) {
|
if (hasInitialized.current && lastInitializedId.current === currentIdString) {
|
||||||
// 생성 모드에서 데이터가 새로 전달된 경우는 재초기화 필요
|
// 생성 모드에서 데이터가 새로 전달된 경우는 재초기화 필요
|
||||||
if (!createModeDataHash || capturedInitialData.current) {
|
if (!createModeDataHash || capturedInitialData.current) {
|
||||||
|
console.log("[UniversalFormModal] 초기화 스킵 - 이미 초기화됨");
|
||||||
|
// 🆕 채번 플래그가 true인데 formData에 값이 없으면 재생성 필요
|
||||||
|
// (컴포넌트 remount로 인해 state가 초기화된 경우)
|
||||||
return;
|
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가 변경된 경우 재초기화
|
// 🆕 수정 모드: initialData에 데이터가 있으면서 ID가 변경된 경우 재초기화
|
||||||
if (hasInitialized.current && currentIdString && lastInitializedId.current !== currentIdString) {
|
if (hasInitialized.current && currentIdString && lastInitializedId.current !== currentIdString) {
|
||||||
console.log("[UniversalFormModal] ID 변경 감지 - 재초기화:", {
|
console.log("[UniversalFormModal] ID 변경 감지 - 재초기화:", {
|
||||||
|
|
@ -252,6 +269,7 @@ export function UniversalFormModalComponent({
|
||||||
console.log("[UniversalFormModal] 초기 데이터 캡처:", capturedInitialData.current);
|
console.log("[UniversalFormModal] 초기 데이터 캡처:", capturedInitialData.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[UniversalFormModal] initializeForm 호출 예정");
|
||||||
hasInitialized.current = true;
|
hasInitialized.current = true;
|
||||||
initializeForm();
|
initializeForm();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -389,6 +407,94 @@ export function UniversalFormModalComponent({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [config.sections]);
|
}, [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 () => {
|
const initializeForm = useCallback(async () => {
|
||||||
console.log("[initializeForm] 시작");
|
console.log("[initializeForm] 시작");
|
||||||
|
|
@ -585,82 +691,6 @@ export function UniversalFormModalComponent({
|
||||||
return item;
|
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(
|
const handleFieldChange = useCallback(
|
||||||
(columnName: string, value: any) => {
|
(columnName: string, value: any) => {
|
||||||
|
|
|
||||||
|
|
@ -995,6 +995,40 @@ export class ButtonActionExecutor {
|
||||||
console.log("📋 [handleSave] 범용 폼 모달 공통 필드:", commonFields);
|
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) {
|
for (const item of parsedData) {
|
||||||
// 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리)
|
// 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리)
|
||||||
|
|
||||||
|
|
@ -5931,6 +5965,69 @@ export class ButtonActionExecutor {
|
||||||
return false;
|
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;
|
const { formData, splitPanelContext, userId, userName, companyCode } = context;
|
||||||
|
|
||||||
console.log("⚡ Quick Insert 상세 정보:", {
|
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() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue