feat: Enhance MasterDetailExcelService with table alias for JOIN operations
- Added a new property `tableAlias` to distinguish between master ("m") and detail ("d") tables during JOIN operations.
- Updated the SELECT clause to include the appropriate table alias for master and detail tables.
- Improved the entity join clause construction to utilize the new table alias, ensuring clarity in SQL queries.
This commit is contained in:
parent
30ee36f881
commit
219f7724e7
|
|
@ -310,6 +310,7 @@ class MasterDetailExcelService {
|
||||||
sourceColumn: string;
|
sourceColumn: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
displayColumn: string;
|
displayColumn: string;
|
||||||
|
tableAlias: string; // "m" (마스터) 또는 "d" (디테일) - JOIN 시 소스 테이블 구분
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
// SELECT 절 구성
|
// SELECT 절 구성
|
||||||
|
|
@ -332,6 +333,7 @@ class MasterDetailExcelService {
|
||||||
sourceColumn: fkColumn.sourceColumn,
|
sourceColumn: fkColumn.sourceColumn,
|
||||||
alias,
|
alias,
|
||||||
displayColumn,
|
displayColumn,
|
||||||
|
tableAlias: "m", // 마스터 테이블에서 조인
|
||||||
});
|
});
|
||||||
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
|
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -360,6 +362,7 @@ class MasterDetailExcelService {
|
||||||
sourceColumn: fkColumn.sourceColumn,
|
sourceColumn: fkColumn.sourceColumn,
|
||||||
alias,
|
alias,
|
||||||
displayColumn,
|
displayColumn,
|
||||||
|
tableAlias: "d", // 디테일 테이블에서 조인
|
||||||
});
|
});
|
||||||
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
|
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -373,9 +376,9 @@ class MasterDetailExcelService {
|
||||||
|
|
||||||
const selectClause = selectParts.join(", ");
|
const selectClause = selectParts.join(", ");
|
||||||
|
|
||||||
// 엔티티 조인 절 구성
|
// 엔티티 조인 절 구성 (마스터/디테일 테이블 alias 구분)
|
||||||
const entityJoinClauses = entityJoins.map(ej =>
|
const entityJoinClauses = entityJoins.map(ej =>
|
||||||
`LEFT JOIN "${ej.refTable}" ${ej.alias} ON m."${ej.sourceColumn}" = ${ej.alias}."${ej.refColumn}"`
|
`LEFT JOIN "${ej.refTable}" ${ej.alias} ON ${ej.tableAlias}."${ej.sourceColumn}" = ${ej.alias}."${ej.refColumn}"`
|
||||||
).join("\n ");
|
).join("\n ");
|
||||||
|
|
||||||
// WHERE 절 구성
|
// WHERE 절 구성
|
||||||
|
|
|
||||||
|
|
@ -1371,39 +1371,66 @@ class TableCategoryValueService {
|
||||||
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
// 동적으로 파라미터 플레이스홀더 생성
|
const n = valueCodes.length;
|
||||||
const placeholders = valueCodes.map((_, i) => `$${i + 1}`).join(", ");
|
|
||||||
|
// 첫 번째 쿼리용 플레이스홀더: $1 ~ $n
|
||||||
|
const placeholders1 = valueCodes.map((_, i) => `$${i + 1}`).join(", ");
|
||||||
|
|
||||||
let query: string;
|
let query: string;
|
||||||
let params: any[];
|
let params: any[];
|
||||||
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
// 최고 관리자: 모든 카테고리 값 조회
|
// 최고 관리자: 두 테이블 모두에서 조회 (UNION으로 병합)
|
||||||
|
// 두 번째 쿼리용 플레이스홀더: $n+1 ~ $2n
|
||||||
|
const placeholders2 = valueCodes.map((_, i) => `$${n + i + 1}`).join(", ");
|
||||||
query = `
|
query = `
|
||||||
SELECT value_code, value_label
|
SELECT value_code, value_label FROM (
|
||||||
FROM table_column_category_values
|
SELECT value_code, value_label
|
||||||
WHERE value_code IN (${placeholders})
|
FROM table_column_category_values
|
||||||
AND is_active = true
|
WHERE value_code IN (${placeholders1})
|
||||||
|
AND is_active = true
|
||||||
|
UNION ALL
|
||||||
|
SELECT value_code, value_label
|
||||||
|
FROM category_values
|
||||||
|
WHERE value_code IN (${placeholders2})
|
||||||
|
AND is_active = true
|
||||||
|
) combined
|
||||||
`;
|
`;
|
||||||
params = valueCodes;
|
params = [...valueCodes, ...valueCodes];
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
|
// 일반 회사: 두 테이블에서 자신의 카테고리 값 + 공통 카테고리 값 조회
|
||||||
|
// 첫 번째: $1~$n (valueCodes), $n+1 (companyCode)
|
||||||
|
// 두 번째: $n+2~$2n+1 (valueCodes), $2n+2 (companyCode)
|
||||||
|
const companyIdx1 = n + 1;
|
||||||
|
const placeholders2 = valueCodes.map((_, i) => `$${n + 1 + i + 1}`).join(", ");
|
||||||
|
const companyIdx2 = 2 * n + 2;
|
||||||
|
|
||||||
query = `
|
query = `
|
||||||
SELECT value_code, value_label
|
SELECT value_code, value_label FROM (
|
||||||
FROM table_column_category_values
|
SELECT value_code, value_label
|
||||||
WHERE value_code IN (${placeholders})
|
FROM table_column_category_values
|
||||||
AND is_active = true
|
WHERE value_code IN (${placeholders1})
|
||||||
AND (company_code = $${valueCodes.length + 1} OR company_code = '*')
|
AND is_active = true
|
||||||
|
AND (company_code = $${companyIdx1} OR company_code = '*')
|
||||||
|
UNION ALL
|
||||||
|
SELECT value_code, value_label
|
||||||
|
FROM category_values
|
||||||
|
WHERE value_code IN (${placeholders2})
|
||||||
|
AND is_active = true
|
||||||
|
AND (company_code = $${companyIdx2} OR company_code = '*')
|
||||||
|
) combined
|
||||||
`;
|
`;
|
||||||
params = [...valueCodes, companyCode];
|
params = [...valueCodes, companyCode, ...valueCodes, companyCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
const result = await pool.query(query, params);
|
||||||
|
|
||||||
// { [code]: label } 형태로 변환
|
// { [code]: label } 형태로 변환 (중복 시 첫 번째 결과 우선)
|
||||||
const labels: Record<string, string> = {};
|
const labels: Record<string, string> = {};
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
labels[row.value_code] = row.value_label;
|
if (!labels[row.value_code]) {
|
||||||
|
labels[row.value_code] = row.value_label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`카테고리 라벨 ${Object.keys(labels).length}개 조회 완료`, { companyCode });
|
logger.info(`카테고리 라벨 ${Object.keys(labels).length}개 조회 완료`, { companyCode });
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
selectedData: eventSelectedData,
|
selectedData: eventSelectedData,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
isCreateMode, // 🆕 복사 모드 플래그 (true면 editData가 있어도 originalData 설정 안 함)
|
isCreateMode, // 🆕 복사 모드 플래그 (true면 editData가 있어도 originalData 설정 안 함)
|
||||||
|
fieldMappings, // 🆕 필드 매핑 정보 (명시적 매핑이 있으면 모든 매핑된 필드 전달)
|
||||||
} = event.detail;
|
} = event.detail;
|
||||||
|
|
||||||
// 🆕 모달 열린 시간 기록
|
// 🆕 모달 열린 시간 기록
|
||||||
|
|
@ -267,6 +268,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
parentData.company_code = rawParentData.company_code;
|
parentData.company_code = rawParentData.company_code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 명시적 필드 매핑이 있으면 매핑된 타겟 필드를 모두 보존
|
||||||
|
// (버튼 설정에서 fieldMappings로 지정한 필드는 link 필드가 아니어도 전달)
|
||||||
|
const mappedTargetFields = new Set<string>();
|
||||||
|
if (fieldMappings && Array.isArray(fieldMappings)) {
|
||||||
|
for (const mapping of fieldMappings) {
|
||||||
|
if (mapping.targetField) {
|
||||||
|
mappedTargetFields.add(mapping.targetField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parentDataMapping에 정의된 필드만 전달
|
// parentDataMapping에 정의된 필드만 전달
|
||||||
for (const mapping of parentDataMapping) {
|
for (const mapping of parentDataMapping) {
|
||||||
const sourceValue = rawParentData[mapping.sourceColumn];
|
const sourceValue = rawParentData[mapping.sourceColumn];
|
||||||
|
|
@ -275,8 +287,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parentDataMapping이 비어있으면 연결 필드 자동 감지 (equipment_code, xxx_code, xxx_id 패턴)
|
// 🆕 명시적 필드 매핑이 있으면 해당 필드를 모두 전달
|
||||||
if (parentDataMapping.length === 0) {
|
if (mappedTargetFields.size > 0) {
|
||||||
|
for (const [key, value] of Object.entries(rawParentData)) {
|
||||||
|
if (mappedTargetFields.has(key) && value !== undefined && value !== null) {
|
||||||
|
parentData[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentDataMapping이 비어있고 명시적 필드 매핑도 없으면 연결 필드 자동 감지
|
||||||
|
if (parentDataMapping.length === 0 && mappedTargetFields.size === 0) {
|
||||||
const linkFieldPatterns = ["_code", "_id"];
|
const linkFieldPatterns = ["_code", "_id"];
|
||||||
const excludeFields = [
|
const excludeFields = [
|
||||||
"id",
|
"id",
|
||||||
|
|
@ -293,6 +314,29 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
if (value === undefined || value === null) continue;
|
if (value === undefined || value === null) continue;
|
||||||
|
|
||||||
// 연결 필드 패턴 확인
|
// 연결 필드 패턴 확인
|
||||||
|
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
||||||
|
if (isLinkField) {
|
||||||
|
parentData[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parentDataMapping.length === 0 && mappedTargetFields.size > 0) {
|
||||||
|
// 🆕 명시적 매핑이 있어도 연결 필드(_code, _id)는 추가로 전달
|
||||||
|
const linkFieldPatterns = ["_code", "_id"];
|
||||||
|
const excludeFields = [
|
||||||
|
"id",
|
||||||
|
"company_code",
|
||||||
|
"created_date",
|
||||||
|
"updated_date",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"writer",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rawParentData)) {
|
||||||
|
if (excludeFields.includes(key)) continue;
|
||||||
|
if (parentData[key] !== undefined) continue; // 이미 매핑된 필드는 스킵
|
||||||
|
if (value === undefined || value === null) continue;
|
||||||
|
|
||||||
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
||||||
if (isLinkField) {
|
if (isLinkField) {
|
||||||
parentData[key] = value;
|
parentData[key] = value;
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,26 @@ import { AutoGenerationConfig } from "@/types/screen";
|
||||||
import { previewNumberingCode } from "@/lib/api/numberingRule";
|
import { previewNumberingCode } from "@/lib/api/numberingRule";
|
||||||
|
|
||||||
// 형식별 입력 마스크 및 검증 패턴
|
// 형식별 입력 마스크 및 검증 패턴
|
||||||
const FORMAT_PATTERNS: Record<V2InputFormat, { pattern: RegExp; placeholder: string }> = {
|
const FORMAT_PATTERNS: Record<V2InputFormat, { pattern: RegExp; placeholder: string; errorMessage: string }> = {
|
||||||
none: { pattern: /.*/, placeholder: "" },
|
none: { pattern: /.*/, placeholder: "", errorMessage: "" },
|
||||||
email: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, placeholder: "example@email.com" },
|
email: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, placeholder: "example@email.com", errorMessage: "올바른 이메일 형식이 아닙니다" },
|
||||||
tel: { pattern: /^\d{2,3}-\d{3,4}-\d{4}$/, placeholder: "010-1234-5678" },
|
tel: { pattern: /^\d{2,3}-\d{3,4}-\d{4}$/, placeholder: "010-1234-5678", errorMessage: "올바른 전화번호 형식이 아닙니다" },
|
||||||
url: { pattern: /^https?:\/\/.+/, placeholder: "https://example.com" },
|
url: { pattern: /^https?:\/\/.+/, placeholder: "https://example.com", errorMessage: "올바른 URL 형식이 아닙니다 (https://로 시작)" },
|
||||||
currency: { pattern: /^[\d,]+$/, placeholder: "1,000,000" },
|
currency: { pattern: /^[\d,]+$/, placeholder: "1,000,000", errorMessage: "숫자만 입력 가능합니다" },
|
||||||
biz_no: { pattern: /^\d{3}-\d{2}-\d{5}$/, placeholder: "123-45-67890" },
|
biz_no: { pattern: /^\d{3}-\d{2}-\d{5}$/, placeholder: "123-45-67890", errorMessage: "올바른 사업자번호 형식이 아닙니다" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 형식 검증 함수 (외부에서도 사용 가능)
|
||||||
|
export function validateInputFormat(value: string, format: V2InputFormat): { isValid: boolean; errorMessage: string } {
|
||||||
|
if (!value || value.trim() === "" || format === "none") {
|
||||||
|
return { isValid: true, errorMessage: "" };
|
||||||
|
}
|
||||||
|
const formatConfig = FORMAT_PATTERNS[format];
|
||||||
|
if (!formatConfig) return { isValid: true, errorMessage: "" };
|
||||||
|
const isValid = formatConfig.pattern.test(value);
|
||||||
|
return { isValid, errorMessage: isValid ? "" : formatConfig.errorMessage };
|
||||||
|
}
|
||||||
|
|
||||||
// 통화 형식 변환
|
// 통화 형식 변환
|
||||||
function formatCurrency(value: string | number): string {
|
function formatCurrency(value: string | number): string {
|
||||||
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
||||||
|
|
@ -70,8 +81,13 @@ const TextInput = forwardRef<
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
columnName?: string;
|
||||||
}
|
}
|
||||||
>(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => {
|
>(({ value, onChange, format = "none", placeholder, readonly, disabled, className, columnName }, ref) => {
|
||||||
|
// 검증 상태
|
||||||
|
const [hasBlurred, setHasBlurred] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<string>("");
|
||||||
|
|
||||||
// 형식에 따른 값 포맷팅
|
// 형식에 따른 값 포맷팅
|
||||||
const formatValue = useCallback(
|
const formatValue = useCallback(
|
||||||
(val: string): string => {
|
(val: string): string => {
|
||||||
|
|
@ -104,29 +120,101 @@ const TextInput = forwardRef<
|
||||||
newValue = formatTel(newValue);
|
newValue = formatTel(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 입력 중 에러 표시 해제 (입력 중에는 관대하게)
|
||||||
|
if (hasBlurred && validationError) {
|
||||||
|
const { isValid } = validateInputFormat(newValue, format);
|
||||||
|
if (isValid) {
|
||||||
|
setValidationError("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onChange?.(newValue);
|
onChange?.(newValue);
|
||||||
},
|
},
|
||||||
[format, onChange],
|
[format, onChange, hasBlurred, validationError],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// blur 시 형식 검증
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
setHasBlurred(true);
|
||||||
|
const currentValue = value !== undefined && value !== null ? String(value) : "";
|
||||||
|
if (currentValue && format !== "none") {
|
||||||
|
const { isValid, errorMessage } = validateInputFormat(currentValue, format);
|
||||||
|
setValidationError(isValid ? "" : errorMessage);
|
||||||
|
} else {
|
||||||
|
setValidationError("");
|
||||||
|
}
|
||||||
|
}, [value, format]);
|
||||||
|
|
||||||
|
// 값 변경 시 검증 상태 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasBlurred) {
|
||||||
|
const currentValue = value !== undefined && value !== null ? String(value) : "";
|
||||||
|
if (currentValue && format !== "none") {
|
||||||
|
const { isValid, errorMessage } = validateInputFormat(currentValue, format);
|
||||||
|
setValidationError(isValid ? "" : errorMessage);
|
||||||
|
} else {
|
||||||
|
setValidationError("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value, format, hasBlurred]);
|
||||||
|
|
||||||
|
// 글로벌 폼 검증 이벤트 리스너 (저장 시 호출)
|
||||||
|
useEffect(() => {
|
||||||
|
if (format === "none" || !columnName) return;
|
||||||
|
|
||||||
|
const handleValidateForm = (event: CustomEvent) => {
|
||||||
|
const currentValue = value !== undefined && value !== null ? String(value) : "";
|
||||||
|
if (currentValue) {
|
||||||
|
const { isValid, errorMessage } = validateInputFormat(currentValue, format);
|
||||||
|
if (!isValid) {
|
||||||
|
setHasBlurred(true);
|
||||||
|
setValidationError(errorMessage);
|
||||||
|
// 검증 결과를 이벤트에 기록
|
||||||
|
if (event.detail?.errors) {
|
||||||
|
event.detail.errors.push({
|
||||||
|
columnName,
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("validateFormInputs", handleValidateForm as EventListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("validateFormInputs", handleValidateForm as EventListener);
|
||||||
|
};
|
||||||
|
}, [format, value, columnName]);
|
||||||
|
|
||||||
const displayValue = useMemo(() => {
|
const displayValue = useMemo(() => {
|
||||||
if (value === undefined || value === null) return "";
|
if (value === undefined || value === null) return "";
|
||||||
return formatValue(String(value));
|
return formatValue(String(value));
|
||||||
}, [value, formatValue]);
|
}, [value, formatValue]);
|
||||||
|
|
||||||
const inputPlaceholder = placeholder || FORMAT_PATTERNS[format].placeholder;
|
const inputPlaceholder = placeholder || FORMAT_PATTERNS[format].placeholder;
|
||||||
|
const hasError = hasBlurred && !!validationError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<div className="flex h-full w-full flex-col">
|
||||||
ref={ref}
|
<Input
|
||||||
type="text"
|
ref={ref}
|
||||||
value={displayValue}
|
type="text"
|
||||||
onChange={handleChange}
|
value={displayValue}
|
||||||
placeholder={inputPlaceholder}
|
onChange={handleChange}
|
||||||
readOnly={readonly}
|
onBlur={handleBlur}
|
||||||
disabled={disabled}
|
placeholder={inputPlaceholder}
|
||||||
className={cn("h-full w-full", className)}
|
readOnly={readonly}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full",
|
||||||
|
hasError && "border-destructive focus-visible:ring-destructive",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{hasError && (
|
||||||
|
<p className="text-destructive mt-1 text-[11px]">{validationError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
TextInput.displayName = "TextInput";
|
TextInput.displayName = "TextInput";
|
||||||
|
|
@ -678,6 +766,7 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
placeholder={config.placeholder}
|
placeholder={config.placeholder}
|
||||||
readonly={readonly || (autoGeneration.enabled && hasGeneratedRef.current)}
|
readonly={readonly || (autoGeneration.enabled && hasGeneratedRef.current)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
columnName={columnName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -835,9 +924,11 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
setAutoGeneratedValue(null);
|
setAutoGeneratedValue(null);
|
||||||
onChange?.(v);
|
onChange?.(v);
|
||||||
}}
|
}}
|
||||||
|
format={config.format}
|
||||||
placeholder={config.placeholder}
|
placeholder={config.placeholder}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
columnName={columnName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,8 @@ export class V2SelectRenderer extends AutoRegisteringComponentRenderer {
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
config={{
|
config={{
|
||||||
mode: config.mode || "dropdown",
|
mode: config.mode || "dropdown",
|
||||||
// 🔧 카테고리 타입이면 source를 "category"로 설정
|
// 🔧 카테고리 타입이면 source를 무조건 "category"로 강제 (테이블 타입 관리 설정 우선)
|
||||||
source: config.source || (isCategoryType ? "category" : "distinct"),
|
source: isCategoryType ? "category" : (config.source || "distinct"),
|
||||||
multiple: config.multiple || false,
|
multiple: config.multiple || false,
|
||||||
searchable: config.searchable ?? true,
|
searchable: config.searchable ?? true,
|
||||||
placeholder: config.placeholder || "선택하세요",
|
placeholder: config.placeholder || "선택하세요",
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,23 @@ export class ButtonActionExecutor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 입력 형식 검증 (이메일, 전화번호, URL 등)
|
||||||
|
const formatValidationDetail = { errors: [] as Array<{ columnName: string; message: string }> };
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("validateFormInputs", {
|
||||||
|
detail: formatValidationDetail,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// 약간의 대기 (이벤트 핸들러가 동기적으로 실행되지만 안전을 위해)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
if (formatValidationDetail.errors.length > 0) {
|
||||||
|
const errorMessages = formatValidationDetail.errors.map((e) => e.message);
|
||||||
|
console.log("❌ [handleSave] 입력 형식 검증 실패:", formatValidationDetail.errors);
|
||||||
|
toast.error(`입력 형식을 확인해주세요: ${errorMessages.join(", ")}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
||||||
if (onSave) {
|
if (onSave) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -3144,6 +3161,8 @@ export class ButtonActionExecutor {
|
||||||
editData: useAsEditData && isPassDataMode ? parentData : undefined,
|
editData: useAsEditData && isPassDataMode ? parentData : undefined,
|
||||||
splitPanelParentData: isPassDataMode ? parentData : undefined,
|
splitPanelParentData: isPassDataMode ? parentData : undefined,
|
||||||
urlParams: dataSourceId ? { dataSourceId } : undefined,
|
urlParams: dataSourceId ? { dataSourceId } : undefined,
|
||||||
|
// 🆕 필드 매핑 정보 전달 - ScreenModal에서 매핑된 필드를 필터링하지 않도록
|
||||||
|
fieldMappings: config.fieldMappings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue