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:
@@ -172,6 +172,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||
selectedData: eventSelectedData,
|
||||
selectedIds,
|
||||
isCreateMode, // 🆕 복사 모드 플래그 (true면 editData가 있어도 originalData 설정 안 함)
|
||||
fieldMappings, // 🆕 필드 매핑 정보 (명시적 매핑이 있으면 모든 매핑된 필드 전달)
|
||||
} = event.detail;
|
||||
|
||||
// 🆕 모달 열린 시간 기록
|
||||
@@ -267,6 +268,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||
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에 정의된 필드만 전달
|
||||
for (const mapping of parentDataMapping) {
|
||||
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 excludeFields = [
|
||||
"id",
|
||||
@@ -293,6 +314,29 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||
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));
|
||||
if (isLinkField) {
|
||||
parentData[key] = value;
|
||||
|
||||
@@ -23,15 +23,26 @@ import { AutoGenerationConfig } from "@/types/screen";
|
||||
import { previewNumberingCode } from "@/lib/api/numberingRule";
|
||||
|
||||
// 형식별 입력 마스크 및 검증 패턴
|
||||
const FORMAT_PATTERNS: Record<V2InputFormat, { pattern: RegExp; placeholder: string }> = {
|
||||
none: { pattern: /.*/, placeholder: "" },
|
||||
email: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, placeholder: "example@email.com" },
|
||||
tel: { pattern: /^\d{2,3}-\d{3,4}-\d{4}$/, placeholder: "010-1234-5678" },
|
||||
url: { pattern: /^https?:\/\/.+/, placeholder: "https://example.com" },
|
||||
currency: { pattern: /^[\d,]+$/, placeholder: "1,000,000" },
|
||||
biz_no: { pattern: /^\d{3}-\d{2}-\d{5}$/, placeholder: "123-45-67890" },
|
||||
const FORMAT_PATTERNS: Record<V2InputFormat, { pattern: RegExp; placeholder: string; errorMessage: string }> = {
|
||||
none: { pattern: /.*/, placeholder: "", errorMessage: "" },
|
||||
email: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, placeholder: "example@email.com", errorMessage: "올바른 이메일 형식이 아닙니다" },
|
||||
tel: { pattern: /^\d{2,3}-\d{3,4}-\d{4}$/, placeholder: "010-1234-5678", errorMessage: "올바른 전화번호 형식이 아닙니다" },
|
||||
url: { pattern: /^https?:\/\/.+/, placeholder: "https://example.com", errorMessage: "올바른 URL 형식이 아닙니다 (https://로 시작)" },
|
||||
currency: { pattern: /^[\d,]+$/, placeholder: "1,000,000", errorMessage: "숫자만 입력 가능합니다" },
|
||||
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 {
|
||||
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
||||
@@ -70,8 +81,13 @@ const TextInput = forwardRef<
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
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(
|
||||
(val: string): string => {
|
||||
@@ -104,29 +120,101 @@ const TextInput = forwardRef<
|
||||
newValue = formatTel(newValue);
|
||||
}
|
||||
|
||||
// 입력 중 에러 표시 해제 (입력 중에는 관대하게)
|
||||
if (hasBlurred && validationError) {
|
||||
const { isValid } = validateInputFormat(newValue, format);
|
||||
if (isValid) {
|
||||
setValidationError("");
|
||||
}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (value === undefined || value === null) return "";
|
||||
return formatValue(String(value));
|
||||
}, [value, formatValue]);
|
||||
|
||||
const inputPlaceholder = placeholder || FORMAT_PATTERNS[format].placeholder;
|
||||
const hasError = hasBlurred && !!validationError;
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
placeholder={inputPlaceholder}
|
||||
readOnly={readonly}
|
||||
disabled={disabled}
|
||||
className={cn("h-full w-full", className)}
|
||||
/>
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder={inputPlaceholder}
|
||||
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";
|
||||
@@ -678,6 +766,7 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||
placeholder={config.placeholder}
|
||||
readonly={readonly || (autoGeneration.enabled && hasGeneratedRef.current)}
|
||||
disabled={disabled}
|
||||
columnName={columnName}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -835,9 +924,11 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||
setAutoGeneratedValue(null);
|
||||
onChange?.(v);
|
||||
}}
|
||||
format={config.format}
|
||||
placeholder={config.placeholder}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
columnName={columnName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user