- Integrated `TableManagementService` to validate unique constraints before insert, update, and upsert actions in various controllers, including `dataflowExecutionController`, `dynamicFormController`, and `tableManagementController`. - Improved error handling in `errorHandler` to provide detailed messages indicating which field has a unique constraint violation. - Updated the `formatPgError` utility to extract and display specific column labels for unique constraint violations, enhancing user feedback. - Adjusted the table schema retrieval to include company-specific nullable and unique constraints, ensuring accurate representation of database rules. These changes improve data integrity by preventing duplicate entries and enhance user experience through clearer error messages related to unique constraints.
711 lines
19 KiB
TypeScript
711 lines
19 KiB
TypeScript
import { Response } from "express";
|
|
import { dynamicFormService } from "../services/dynamicFormService";
|
|
import { enhancedDynamicFormService } from "../services/enhancedDynamicFormService";
|
|
import { TableManagementService } from "../services/tableManagementService";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
import { formatPgError } from "../utils/pgErrorUtil";
|
|
|
|
// 폼 데이터 저장 (기존 버전 - 레거시 지원)
|
|
export const saveFormData = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { companyCode, userId } = req.user as any;
|
|
const { screenId, tableName, data } = req.body;
|
|
|
|
// 🔍 디버깅: 사용자 정보 확인
|
|
console.log("🔍 [saveFormData] 사용자 정보:", {
|
|
userId,
|
|
companyCode,
|
|
reqUser: req.user,
|
|
dataWriter: data.writer,
|
|
});
|
|
|
|
// 필수 필드 검증 (screenId는 0일 수 있으므로 undefined 체크)
|
|
if (screenId === undefined || screenId === null || !tableName || !data) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다. (screenId, tableName, data)",
|
|
});
|
|
}
|
|
|
|
// 메타데이터 추가 (사용자가 입력한 경우에만 company_code 추가)
|
|
const formDataWithMeta = {
|
|
...data,
|
|
created_by: userId,
|
|
updated_by: userId,
|
|
writer: data.writer || userId, // ✅ writer가 없으면 userId로 설정
|
|
screen_id: screenId,
|
|
};
|
|
|
|
console.log("✅ [saveFormData] 최종 writer 값:", formDataWithMeta.writer);
|
|
|
|
// company_code는 사용자가 명시적으로 입력한 경우에만 추가
|
|
if (data.company_code !== undefined) {
|
|
formDataWithMeta.company_code = data.company_code;
|
|
} else if (companyCode && companyCode !== "*") {
|
|
// 기본 company_code가 '*'가 아닌 경우에만 추가
|
|
formDataWithMeta.company_code = companyCode;
|
|
}
|
|
|
|
// UNIQUE 제약조건 검증 (INSERT 전)
|
|
const tms = new TableManagementService();
|
|
const uniqueViolations = await tms.validateUniqueConstraints(
|
|
tableName,
|
|
formDataWithMeta,
|
|
companyCode || "*"
|
|
);
|
|
if (uniqueViolations.length > 0) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 클라이언트 IP 주소 추출
|
|
const ipAddress =
|
|
req.ip ||
|
|
(req.headers["x-forwarded-for"] as string) ||
|
|
req.socket.remoteAddress ||
|
|
"unknown";
|
|
|
|
const result = await dynamicFormService.saveFormData(
|
|
screenId,
|
|
tableName,
|
|
formDataWithMeta,
|
|
ipAddress
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
message: "데이터가 성공적으로 저장되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 폼 데이터 저장 실패:", error);
|
|
const { companyCode } = req.user as any;
|
|
const friendlyMsg = await formatPgError(error, companyCode);
|
|
const statusCode = error.code?.startsWith("23") ? 400 : 500;
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
message: friendlyMsg,
|
|
});
|
|
}
|
|
};
|
|
|
|
// 개선된 폼 데이터 저장 (새 버전)
|
|
export const saveFormDataEnhanced = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { companyCode, userId } = req.user as any;
|
|
const { screenId, tableName, data } = req.body;
|
|
|
|
// 필수 필드 검증
|
|
if (screenId === undefined || screenId === null || !tableName || !data) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다. (screenId, tableName, data)",
|
|
});
|
|
}
|
|
|
|
// 메타데이터 추가
|
|
const formDataWithMeta = {
|
|
...data,
|
|
created_by: userId,
|
|
updated_by: userId,
|
|
writer: data.writer || userId, // ✅ writer가 없으면 userId로 설정
|
|
screen_id: screenId,
|
|
};
|
|
|
|
// company_code 처리
|
|
if (data.company_code !== undefined) {
|
|
formDataWithMeta.company_code = data.company_code;
|
|
} else if (companyCode && companyCode !== "*") {
|
|
formDataWithMeta.company_code = companyCode;
|
|
}
|
|
|
|
// UNIQUE 제약조건 검증 (INSERT 전)
|
|
const tmsEnhanced = new TableManagementService();
|
|
const uniqueViolations = await tmsEnhanced.validateUniqueConstraints(
|
|
tableName,
|
|
formDataWithMeta,
|
|
companyCode || "*"
|
|
);
|
|
if (uniqueViolations.length > 0) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 개선된 서비스 사용
|
|
const result = await enhancedDynamicFormService.saveFormData(
|
|
screenId,
|
|
tableName,
|
|
formDataWithMeta
|
|
);
|
|
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error("❌ 개선된 폼 데이터 저장 실패:", error);
|
|
const { companyCode } = req.user as any;
|
|
const friendlyMsg = await formatPgError(error, companyCode);
|
|
const statusCode = error.code?.startsWith("23") ? 400 : 500;
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
message: friendlyMsg,
|
|
});
|
|
}
|
|
};
|
|
|
|
// 폼 데이터 업데이트
|
|
export const updateFormData = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { companyCode, userId } = req.user as any;
|
|
const { tableName, data } = req.body;
|
|
|
|
if (!tableName || !data) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다. (tableName, data)",
|
|
});
|
|
}
|
|
|
|
// 메타데이터 추가
|
|
const formDataWithMeta = {
|
|
...data,
|
|
updated_by: userId,
|
|
writer: data.writer || userId,
|
|
updated_at: new Date(),
|
|
};
|
|
|
|
// UNIQUE 제약조건 검증 (UPDATE 시 자기 자신 제외)
|
|
const tmsUpdate = new TableManagementService();
|
|
const uniqueViolations = await tmsUpdate.validateUniqueConstraints(
|
|
tableName,
|
|
formDataWithMeta,
|
|
companyCode || "*",
|
|
id
|
|
);
|
|
if (uniqueViolations.length > 0) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const result = await dynamicFormService.updateFormData(
|
|
id,
|
|
tableName,
|
|
formDataWithMeta
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
message: "데이터가 성공적으로 업데이트되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 폼 데이터 업데이트 실패:", error);
|
|
const { companyCode } = req.user as any;
|
|
const friendlyMsg = await formatPgError(error, companyCode);
|
|
const statusCode = error.code?.startsWith("23") ? 400 : 500;
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
message: friendlyMsg,
|
|
});
|
|
}
|
|
};
|
|
|
|
// 폼 데이터 부분 업데이트 (변경된 필드만)
|
|
export const updateFormDataPartial = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { companyCode, userId } = req.user as any;
|
|
const { tableName, originalData, newData } = req.body;
|
|
|
|
if (!tableName || !originalData || !newData) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message:
|
|
"필수 필드가 누락되었습니다. (tableName, originalData, newData)",
|
|
});
|
|
}
|
|
|
|
console.log("🔄 컨트롤러: 부분 업데이트 요청:", {
|
|
id,
|
|
tableName,
|
|
originalData,
|
|
newData,
|
|
});
|
|
|
|
// 메타데이터 추가
|
|
const newDataWithMeta = {
|
|
...newData,
|
|
updated_by: userId,
|
|
writer: newData.writer || userId,
|
|
};
|
|
|
|
// UNIQUE 제약조건 검증 (부분 UPDATE 시 자기 자신 제외)
|
|
const tmsPartial = new TableManagementService();
|
|
const uniqueViolations = await tmsPartial.validateUniqueConstraints(
|
|
tableName,
|
|
newDataWithMeta,
|
|
companyCode || "*",
|
|
id
|
|
);
|
|
if (uniqueViolations.length > 0) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const result = await dynamicFormService.updateFormDataPartial(
|
|
id,
|
|
tableName,
|
|
originalData,
|
|
newDataWithMeta
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
message: "데이터가 성공적으로 업데이트되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 부분 업데이트 실패:", error);
|
|
const { companyCode } = req.user as any;
|
|
const friendlyMsg = await formatPgError(error, companyCode);
|
|
const statusCode = error.code?.startsWith("23") ? 400 : 500;
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
message: friendlyMsg,
|
|
});
|
|
}
|
|
};
|
|
|
|
// 폼 데이터 삭제
|
|
export const deleteFormData = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { companyCode, userId } = req.user as any;
|
|
const { tableName, screenId } = req.body;
|
|
|
|
if (!tableName) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다. (tableName)",
|
|
});
|
|
}
|
|
|
|
// screenId를 숫자로 변환 (문자열로 전달될 수 있음)
|
|
const parsedScreenId = screenId ? parseInt(screenId, 10) : undefined;
|
|
|
|
await dynamicFormService.deleteFormData(
|
|
id,
|
|
tableName,
|
|
companyCode,
|
|
userId,
|
|
parsedScreenId // screenId 추가 (제어관리 실행용)
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "데이터가 성공적으로 삭제되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 폼 데이터 삭제 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "데이터 삭제에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 테이블의 기본키 조회
|
|
export const getTablePrimaryKeys = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { tableName } = req.params;
|
|
|
|
if (!tableName) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "테이블명이 누락되었습니다.",
|
|
});
|
|
}
|
|
|
|
console.log(`🔑 테이블 ${tableName}의 기본키 조회 요청`);
|
|
|
|
const primaryKeys = await dynamicFormService.getTablePrimaryKeys(tableName);
|
|
|
|
console.log(`✅ 테이블 ${tableName}의 기본키:`, primaryKeys);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: primaryKeys,
|
|
message: "기본키 조회가 완료되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 기본키 조회 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "기본키 조회에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 단일 폼 데이터 조회
|
|
export const getFormData = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { companyCode } = req.user as any;
|
|
|
|
const data = await dynamicFormService.getFormData(parseInt(id));
|
|
|
|
if (!data) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "데이터를 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: data,
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 폼 데이터 단건 조회 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "데이터 조회에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 화면별 폼 데이터 목록 조회
|
|
export const getFormDataList = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { screenId } = req.params;
|
|
const { companyCode } = req.user as any;
|
|
const {
|
|
page = 1,
|
|
size = 10,
|
|
search = "",
|
|
sortBy = "created_at",
|
|
sortOrder = "desc",
|
|
} = req.query;
|
|
|
|
const result = await dynamicFormService.getFormDataList(
|
|
parseInt(screenId as string),
|
|
{
|
|
page: parseInt(page as string),
|
|
size: parseInt(size as string),
|
|
search: search as string,
|
|
sortBy: sortBy as string,
|
|
sortOrder: sortOrder as "asc" | "desc",
|
|
}
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 폼 데이터 목록 조회 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "데이터 조회에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 폼 데이터 검증
|
|
export const validateFormData = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { tableName, data } = req.body;
|
|
|
|
if (!tableName || !data) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다. (tableName, data)",
|
|
});
|
|
}
|
|
|
|
const validationResult = await dynamicFormService.validateFormData(
|
|
tableName,
|
|
data
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: validationResult,
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 폼 데이터 검증 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "데이터 검증에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 테이블 컬럼 정보 조회 (검증용)
|
|
export const getTableColumns = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { tableName } = req.params;
|
|
|
|
const columns = await dynamicFormService.getTableColumns(tableName);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
tableName,
|
|
columns,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ 테이블 컬럼 정보 조회 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "테이블 정보 조회에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 특정 필드만 업데이트 (다른 테이블 지원)
|
|
export const updateFieldValue = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { companyCode, userId } = req.user as any;
|
|
const { tableName, keyField, keyValue, updateField, updateValue } =
|
|
req.body;
|
|
|
|
console.log("🔄 [updateFieldValue] 요청:", {
|
|
tableName,
|
|
keyField,
|
|
keyValue,
|
|
updateField,
|
|
updateValue,
|
|
userId,
|
|
companyCode,
|
|
});
|
|
|
|
// 필수 필드 검증
|
|
if (
|
|
!tableName ||
|
|
!keyField ||
|
|
keyValue === undefined ||
|
|
!updateField ||
|
|
updateValue === undefined
|
|
) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message:
|
|
"필수 필드가 누락되었습니다. (tableName, keyField, keyValue, updateField, updateValue)",
|
|
});
|
|
}
|
|
|
|
// SQL 인젝션 방지를 위한 테이블명/컬럼명 검증
|
|
const validNamePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
if (
|
|
!validNamePattern.test(tableName) ||
|
|
!validNamePattern.test(keyField) ||
|
|
!validNamePattern.test(updateField)
|
|
) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "유효하지 않은 테이블명 또는 컬럼명입니다.",
|
|
});
|
|
}
|
|
|
|
// 업데이트 쿼리 실행
|
|
const result = await dynamicFormService.updateFieldValue(
|
|
tableName,
|
|
keyField,
|
|
keyValue,
|
|
updateField,
|
|
updateValue,
|
|
companyCode,
|
|
userId
|
|
);
|
|
|
|
console.log("✅ [updateFieldValue] 성공:", result);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
message: "필드 값이 업데이트되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ [updateFieldValue] 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "필드 업데이트에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 위치 이력 저장 (연속 위치 추적용)
|
|
* POST /api/dynamic-form/location-history
|
|
*/
|
|
export const saveLocationHistory = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { companyCode, userId: loginUserId } = req.user as any;
|
|
const {
|
|
latitude,
|
|
longitude,
|
|
accuracy,
|
|
altitude,
|
|
speed,
|
|
heading,
|
|
tripId,
|
|
tripStatus,
|
|
departure,
|
|
arrival,
|
|
departureName,
|
|
destinationName,
|
|
recordedAt,
|
|
vehicleId,
|
|
userId: requestUserId, // 프론트엔드에서 보낸 userId (차량 번호판 등)
|
|
} = req.body;
|
|
|
|
// 프론트엔드에서 보낸 userId가 있으면 그것을 사용 (차량 번호판 등)
|
|
// 없으면 로그인한 사용자의 userId 사용
|
|
const userId = requestUserId || loginUserId;
|
|
|
|
console.log("📍 [saveLocationHistory] 요청:", {
|
|
userId,
|
|
requestUserId,
|
|
loginUserId,
|
|
companyCode,
|
|
latitude,
|
|
longitude,
|
|
tripId,
|
|
});
|
|
|
|
// 필수 필드 검증
|
|
if (latitude === undefined || longitude === undefined) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다. (latitude, longitude)",
|
|
});
|
|
}
|
|
|
|
const result = await dynamicFormService.saveLocationHistory({
|
|
userId,
|
|
companyCode,
|
|
latitude,
|
|
longitude,
|
|
accuracy,
|
|
altitude,
|
|
speed,
|
|
heading,
|
|
tripId,
|
|
tripStatus: tripStatus || "active",
|
|
departure,
|
|
arrival,
|
|
departureName,
|
|
destinationName,
|
|
recordedAt: recordedAt || new Date().toISOString(),
|
|
vehicleId,
|
|
});
|
|
|
|
console.log("✅ [saveLocationHistory] 성공:", result);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
message: "위치 이력이 저장되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ [saveLocationHistory] 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "위치 이력 저장에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 위치 이력 조회 (경로 조회용)
|
|
* GET /api/dynamic-form/location-history/:tripId
|
|
*/
|
|
export const getLocationHistory = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<Response | void> => {
|
|
try {
|
|
const { companyCode } = req.user as any;
|
|
const { tripId } = req.params;
|
|
const { userId, startDate, endDate, limit } = req.query;
|
|
|
|
console.log("📍 [getLocationHistory] 요청:", {
|
|
tripId,
|
|
userId,
|
|
startDate,
|
|
endDate,
|
|
limit,
|
|
});
|
|
|
|
const result = await dynamicFormService.getLocationHistory({
|
|
companyCode,
|
|
tripId,
|
|
userId: userId as string,
|
|
startDate: startDate as string,
|
|
endDate: endDate as string,
|
|
limit: limit ? parseInt(limit as string) : 1000,
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
count: result.length,
|
|
});
|
|
} catch (error: any) {
|
|
console.error("❌ [getLocationHistory] 실패:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || "위치 이력 조회에 실패했습니다.",
|
|
});
|
|
}
|
|
};
|