기본정보 눌렀을때 뜨는 오류해결
This commit is contained in:
@@ -230,15 +230,115 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
...prev,
|
||||
[identifier]: result.data.rows[0],
|
||||
}));
|
||||
} else {
|
||||
// 데이터가 없는 경우에도 "로드 완료" 상태로 표시 (빈 객체 저장)
|
||||
setTripInfo((prev) => ({
|
||||
...prev,
|
||||
[identifier]: { _noData: true },
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// API 실패 시에도 "로드 완료" 상태로 표시
|
||||
setTripInfo((prev) => ({
|
||||
...prev,
|
||||
[identifier]: { _noData: true },
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("공차/운행 정보 로드 실패:", err);
|
||||
// 에러 시에도 "로드 완료" 상태로 표시
|
||||
setTripInfo((prev) => ({
|
||||
...prev,
|
||||
[identifier]: { _noData: true },
|
||||
}));
|
||||
}
|
||||
|
||||
setTripInfoLoading(null);
|
||||
}, [tripInfo]);
|
||||
|
||||
// 마커 로드 시 운행/공차 정보 미리 일괄 조회
|
||||
const preloadTripInfo = useCallback(async (loadedMarkers: MarkerData[]) => {
|
||||
if (!loadedMarkers || loadedMarkers.length === 0) return;
|
||||
|
||||
// 마커에서 identifier 추출 (user_id 또는 vehicle_number)
|
||||
const identifiers: string[] = [];
|
||||
loadedMarkers.forEach((marker) => {
|
||||
try {
|
||||
const parsed = JSON.parse(marker.description || "{}");
|
||||
const identifier = parsed.user_id || parsed.vehicle_number || parsed.id;
|
||||
if (identifier && !tripInfo[identifier]) {
|
||||
identifiers.push(identifier);
|
||||
}
|
||||
} catch {
|
||||
// 파싱 실패 시 무시
|
||||
}
|
||||
});
|
||||
|
||||
if (identifiers.length === 0) return;
|
||||
|
||||
try {
|
||||
// 모든 마커의 운행/공차 정보를 한 번에 조회
|
||||
const placeholders = identifiers.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const query = `SELECT
|
||||
id, vehicle_number, user_id,
|
||||
last_trip_start, last_trip_end, last_trip_distance, last_trip_time,
|
||||
last_empty_start, last_empty_end, last_empty_distance, last_empty_time,
|
||||
departure, arrival, status
|
||||
FROM vehicles
|
||||
WHERE user_id IN (${identifiers.map(id => `'${id}'`).join(", ")})
|
||||
OR vehicle_number IN (${identifiers.map(id => `'${id}'`).join(", ")})`;
|
||||
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`,
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success && result.data.rows.length > 0) {
|
||||
const newTripInfo: Record<string, any> = {};
|
||||
|
||||
// 조회된 데이터를 identifier별로 매핑
|
||||
result.data.rows.forEach((row: any) => {
|
||||
const hasData = row.last_trip_start || row.last_trip_end ||
|
||||
row.last_trip_distance || row.last_trip_time ||
|
||||
row.last_empty_start || row.last_empty_end ||
|
||||
row.last_empty_distance || row.last_empty_time;
|
||||
|
||||
if (row.user_id) {
|
||||
newTripInfo[row.user_id] = hasData ? row : { _noData: true };
|
||||
}
|
||||
if (row.vehicle_number) {
|
||||
newTripInfo[row.vehicle_number] = hasData ? row : { _noData: true };
|
||||
}
|
||||
});
|
||||
|
||||
// 조회되지 않은 identifier는 _noData로 표시
|
||||
identifiers.forEach((id) => {
|
||||
if (!newTripInfo[id]) {
|
||||
newTripInfo[id] = { _noData: true };
|
||||
}
|
||||
});
|
||||
|
||||
setTripInfo((prev) => ({ ...prev, ...newTripInfo }));
|
||||
} else {
|
||||
// 결과가 없으면 모든 identifier를 _noData로 표시
|
||||
const noDataInfo: Record<string, any> = {};
|
||||
identifiers.forEach((id) => {
|
||||
noDataInfo[id] = { _noData: true };
|
||||
});
|
||||
setTripInfo((prev) => ({ ...prev, ...noDataInfo }));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("운행/공차 정보 미리 로드 실패:", err);
|
||||
}
|
||||
}, [tripInfo]);
|
||||
|
||||
// 다중 데이터 소스 로딩
|
||||
const loadMultipleDataSources = useCallback(async () => {
|
||||
if (!dataSources || dataSources.length === 0) {
|
||||
@@ -311,6 +411,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
setMarkers(markersWithHeading);
|
||||
setPolygons(allPolygons);
|
||||
setLastRefreshTime(new Date());
|
||||
|
||||
// 마커 로드 후 운행/공차 정보 미리 일괄 조회
|
||||
preloadTripInfo(markersWithHeading);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1856,6 +1959,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
return mins > 0 ? `${hours}시간 ${mins}분` : `${hours}시간`;
|
||||
};
|
||||
|
||||
// 이미 로드했는데 데이터가 없는 경우 (버튼 숨김)
|
||||
const loadedInfo = tripInfo[identifier];
|
||||
if (loadedInfo && loadedInfo._noData) {
|
||||
return null; // 데이터 없음 - 버튼도 정보도 표시 안 함
|
||||
}
|
||||
|
||||
// 데이터가 없고 아직 로드 안 했으면 로드 버튼 표시
|
||||
if (!hasEmptyTripInfo && !hasTripInfo && !tripInfo[identifier]) {
|
||||
return (
|
||||
|
||||
@@ -62,7 +62,7 @@ import {
|
||||
CostType,
|
||||
costTypeLabels,
|
||||
} from "@/lib/api/taxInvoice";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { uploadFiles } from "@/lib/api/file";
|
||||
|
||||
interface TaxInvoiceFormProps {
|
||||
open: boolean;
|
||||
@@ -223,36 +223,35 @@ export function TaxInvoiceForm({ open, onClose, onSave, invoice }: TaxInvoiceFor
|
||||
});
|
||||
};
|
||||
|
||||
// 파일 업로드
|
||||
// 파일 업로드 (화면 관리 파일 업로드 컴포넌트와 동일한 방식 사용)
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
for (const file of Array.from(files)) {
|
||||
const formDataUpload = new FormData();
|
||||
formDataUpload.append("files", file); // 백엔드 Multer 필드명: "files"
|
||||
formDataUpload.append("category", "tax-invoice");
|
||||
// 화면 관리 파일 업로드 컴포넌트와 동일한 uploadFiles 함수 사용
|
||||
const response = await uploadFiles({
|
||||
files: files,
|
||||
tableName: "tax_invoice",
|
||||
fieldName: "attachments",
|
||||
recordId: invoice?.id,
|
||||
docType: "tax-invoice",
|
||||
docTypeName: "세금계산서",
|
||||
});
|
||||
|
||||
const response = await apiClient.post("/files/upload", formDataUpload, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
|
||||
if (response.data.success && response.data.files?.length > 0) {
|
||||
const uploadedFile = response.data.files[0];
|
||||
const newAttachment: TaxInvoiceAttachment = {
|
||||
id: uploadedFile.objid || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
file_name: uploadedFile.realFileName || file.name,
|
||||
file_path: uploadedFile.filePath,
|
||||
file_size: uploadedFile.fileSize || file.size,
|
||||
file_type: file.type,
|
||||
uploaded_at: new Date().toISOString(),
|
||||
uploaded_by: "",
|
||||
};
|
||||
setAttachments((prev) => [...prev, newAttachment]);
|
||||
toast.success(`'${file.name}' 업로드 완료`);
|
||||
}
|
||||
if (response.success && response.files?.length > 0) {
|
||||
const newAttachments: TaxInvoiceAttachment[] = response.files.map((uploadedFile) => ({
|
||||
id: uploadedFile.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
file_name: uploadedFile.name,
|
||||
file_path: uploadedFile.serverPath || "",
|
||||
file_size: uploadedFile.size,
|
||||
file_type: uploadedFile.type,
|
||||
uploaded_at: uploadedFile.uploadedAt || new Date().toISOString(),
|
||||
uploaded_by: "",
|
||||
}));
|
||||
setAttachments((prev) => [...prev, ...newAttachments]);
|
||||
toast.success(`${response.files.length}개 파일 업로드 완료`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error("파일 업로드 실패", { description: error.message });
|
||||
|
||||
@@ -447,10 +447,13 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
// 디자인 모드 플래그 전달 - isPreview와 명확히 구분
|
||||
isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false,
|
||||
// 🆕 그룹 데이터 전달 (EditModal → ConditionalContainer → ModalRepeaterTable)
|
||||
groupedData: props.groupedData,
|
||||
// Note: 이 props들은 DOM 요소에 전달되면 안 됨
|
||||
// 각 컴포넌트에서 명시적으로 destructure하여 사용해야 함
|
||||
_groupedData: props.groupedData,
|
||||
// 🆕 UniversalFormModal용 initialData 전달
|
||||
// originalData를 사용 (최초 전달된 값, formData는 계속 변경되므로 사용하면 안됨)
|
||||
initialData: originalData || formData,
|
||||
_initialData: originalData || formData,
|
||||
_originalData: originalData,
|
||||
};
|
||||
|
||||
// 렌더러가 클래스인지 함수인지 확인
|
||||
|
||||
@@ -48,17 +48,28 @@ export function SimpleRepeaterTableComponent({
|
||||
allowAdd: propAllowAdd,
|
||||
maxHeight: propMaxHeight,
|
||||
|
||||
// DOM에 전달되면 안 되는 props 명시적 제거 (부모에서 전달될 수 있음)
|
||||
initialData: _initialData,
|
||||
originalData: _originalData,
|
||||
groupedData: _groupedData,
|
||||
// DynamicComponentRenderer에서 전달되는 props (DOM 전달 방지를 위해 _ prefix 사용)
|
||||
_initialData,
|
||||
_originalData,
|
||||
_groupedData,
|
||||
// 레거시 호환성 (일부 컴포넌트에서 직접 전달할 수 있음)
|
||||
initialData: legacyInitialData,
|
||||
originalData: legacyOriginalData,
|
||||
groupedData: legacyGroupedData,
|
||||
|
||||
...props
|
||||
}: SimpleRepeaterTableComponentProps & {
|
||||
_initialData?: any;
|
||||
_originalData?: any;
|
||||
_groupedData?: any;
|
||||
initialData?: any;
|
||||
originalData?: any;
|
||||
groupedData?: any;
|
||||
}) {
|
||||
// 실제 사용할 데이터 (새 props 우선, 레거시 fallback)
|
||||
const effectiveInitialData = _initialData || legacyInitialData;
|
||||
const effectiveOriginalData = _originalData || legacyOriginalData;
|
||||
const effectiveGroupedData = _groupedData || legacyGroupedData;
|
||||
// config 또는 component.config 또는 개별 prop 우선순위로 병합
|
||||
const componentConfig = {
|
||||
...config,
|
||||
|
||||
@@ -48,11 +48,16 @@ export function UniversalFormModalComponent({
|
||||
isSelected = false,
|
||||
className,
|
||||
style,
|
||||
initialData,
|
||||
initialData: propInitialData,
|
||||
// DynamicComponentRenderer에서 전달되는 props (DOM 전달 방지를 위해 _ prefix 사용)
|
||||
_initialData,
|
||||
onSave,
|
||||
onCancel,
|
||||
onChange,
|
||||
}: UniversalFormModalComponentProps) {
|
||||
...restProps // 나머지 props는 DOM에 전달하지 않음
|
||||
}: UniversalFormModalComponentProps & { _initialData?: any }) {
|
||||
// initialData 우선순위: 직접 전달된 prop > DynamicComponentRenderer에서 전달된 prop
|
||||
const initialData = propInitialData || _initialData;
|
||||
// 설정 병합
|
||||
const config: UniversalFormModalConfig = useMemo(() => {
|
||||
const componentConfig = component?.config || {};
|
||||
|
||||
Reference in New Issue
Block a user