569 lines
16 KiB
TypeScript
569 lines
16 KiB
TypeScript
/**
|
|
* 자동 입력 (Auto-Fill) 컨트롤러
|
|
* 마스터 선택 시 여러 필드 자동 입력 기능
|
|
*/
|
|
|
|
import { Request, Response } from "express";
|
|
import { query, queryOne } from "../database/db";
|
|
import logger from "../utils/logger";
|
|
|
|
// =====================================================
|
|
// 자동 입력 그룹 CRUD
|
|
// =====================================================
|
|
|
|
/**
|
|
* 자동 입력 그룹 목록 조회
|
|
*/
|
|
export const getAutoFillGroups = async (req: Request, res: Response) => {
|
|
try {
|
|
const companyCode = req.user?.companyCode || "*";
|
|
const { isActive } = req.query;
|
|
|
|
let sql = `
|
|
SELECT
|
|
g.*,
|
|
COUNT(m.mapping_id) as mapping_count
|
|
FROM cascading_auto_fill_group g
|
|
LEFT JOIN cascading_auto_fill_mapping m
|
|
ON g.group_code = m.group_code AND g.company_code = m.company_code
|
|
WHERE 1=1
|
|
`;
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
// 회사 필터
|
|
if (companyCode !== "*") {
|
|
sql += ` AND g.company_code = $${paramIndex++}`;
|
|
params.push(companyCode);
|
|
}
|
|
|
|
// 활성 상태 필터
|
|
if (isActive) {
|
|
sql += ` AND g.is_active = $${paramIndex++}`;
|
|
params.push(isActive);
|
|
}
|
|
|
|
sql += ` GROUP BY g.group_id ORDER BY g.group_name`;
|
|
|
|
const result = await query(sql, params);
|
|
|
|
logger.info("자동 입력 그룹 목록 조회", { count: result.length, companyCode });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("자동 입력 그룹 목록 조회 실패", { error: error.message });
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "자동 입력 그룹 목록 조회에 실패했습니다.",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 자동 입력 그룹 상세 조회 (매핑 포함)
|
|
*/
|
|
export const getAutoFillGroupDetail = async (req: Request, res: Response) => {
|
|
try {
|
|
const { groupCode } = req.params;
|
|
const companyCode = req.user?.companyCode || "*";
|
|
|
|
// 그룹 정보 조회
|
|
let groupSql = `
|
|
SELECT * FROM cascading_auto_fill_group
|
|
WHERE group_code = $1
|
|
`;
|
|
const groupParams: any[] = [groupCode];
|
|
|
|
if (companyCode !== "*") {
|
|
groupSql += ` AND company_code = $2`;
|
|
groupParams.push(companyCode);
|
|
}
|
|
|
|
const groupResult = await queryOne(groupSql, groupParams);
|
|
|
|
if (!groupResult) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "자동 입력 그룹을 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
// 매핑 정보 조회
|
|
const mappingSql = `
|
|
SELECT * FROM cascading_auto_fill_mapping
|
|
WHERE group_code = $1 AND company_code = $2
|
|
ORDER BY sort_order, mapping_id
|
|
`;
|
|
const mappingResult = await query(mappingSql, [groupCode, groupResult.company_code]);
|
|
|
|
logger.info("자동 입력 그룹 상세 조회", { groupCode, companyCode });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
...groupResult,
|
|
mappings: mappingResult,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("자동 입력 그룹 상세 조회 실패", { error: error.message });
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "자동 입력 그룹 상세 조회에 실패했습니다.",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 그룹 코드 자동 생성 함수
|
|
*/
|
|
const generateAutoFillGroupCode = async (companyCode: string): Promise<string> => {
|
|
const prefix = "AF";
|
|
const result = await queryOne(
|
|
`SELECT COUNT(*) as cnt FROM cascading_auto_fill_group WHERE company_code = $1`,
|
|
[companyCode]
|
|
);
|
|
const count = parseInt(result?.cnt || "0", 10) + 1;
|
|
const timestamp = Date.now().toString(36).toUpperCase().slice(-4);
|
|
return `${prefix}_${timestamp}_${count.toString().padStart(3, "0")}`;
|
|
};
|
|
|
|
/**
|
|
* 자동 입력 그룹 생성
|
|
*/
|
|
export const createAutoFillGroup = async (req: Request, res: Response) => {
|
|
try {
|
|
const companyCode = req.user?.companyCode || "*";
|
|
const userId = req.user?.userId || "system";
|
|
const {
|
|
groupName,
|
|
description,
|
|
masterTable,
|
|
masterValueColumn,
|
|
masterLabelColumn,
|
|
mappings = [],
|
|
} = req.body;
|
|
|
|
// 필수 필드 검증
|
|
if (!groupName || !masterTable || !masterValueColumn) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다. (groupName, masterTable, masterValueColumn)",
|
|
});
|
|
}
|
|
|
|
// 그룹 코드 자동 생성
|
|
const groupCode = await generateAutoFillGroupCode(companyCode);
|
|
|
|
// 그룹 생성
|
|
const insertGroupSql = `
|
|
INSERT INTO cascading_auto_fill_group (
|
|
group_code, group_name, description,
|
|
master_table, master_value_column, master_label_column,
|
|
company_code, is_active, created_date
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, 'Y', CURRENT_TIMESTAMP)
|
|
RETURNING *
|
|
`;
|
|
|
|
const groupResult = await queryOne(insertGroupSql, [
|
|
groupCode,
|
|
groupName,
|
|
description || null,
|
|
masterTable,
|
|
masterValueColumn,
|
|
masterLabelColumn || null,
|
|
companyCode,
|
|
]);
|
|
|
|
// 매핑 생성
|
|
if (mappings.length > 0) {
|
|
for (let i = 0; i < mappings.length; i++) {
|
|
const m = mappings[i];
|
|
await query(
|
|
`INSERT INTO cascading_auto_fill_mapping (
|
|
group_code, company_code, source_column, target_field, target_label,
|
|
is_editable, is_required, default_value, sort_order
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
[
|
|
groupCode,
|
|
companyCode,
|
|
m.sourceColumn,
|
|
m.targetField,
|
|
m.targetLabel || null,
|
|
m.isEditable || "Y",
|
|
m.isRequired || "N",
|
|
m.defaultValue || null,
|
|
m.sortOrder || i + 1,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
logger.info("자동 입력 그룹 생성", { groupCode, companyCode, userId });
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: "자동 입력 그룹이 생성되었습니다.",
|
|
data: groupResult,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("자동 입력 그룹 생성 실패", { error: error.message });
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "자동 입력 그룹 생성에 실패했습니다.",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 자동 입력 그룹 수정
|
|
*/
|
|
export const updateAutoFillGroup = async (req: Request, res: Response) => {
|
|
try {
|
|
const { groupCode } = req.params;
|
|
const companyCode = req.user?.companyCode || "*";
|
|
const userId = req.user?.userId || "system";
|
|
const {
|
|
groupName,
|
|
description,
|
|
masterTable,
|
|
masterValueColumn,
|
|
masterLabelColumn,
|
|
isActive,
|
|
mappings,
|
|
} = req.body;
|
|
|
|
// 기존 그룹 확인
|
|
let checkSql = `SELECT * FROM cascading_auto_fill_group WHERE group_code = $1`;
|
|
const checkParams: any[] = [groupCode];
|
|
|
|
if (companyCode !== "*") {
|
|
checkSql += ` AND company_code = $2`;
|
|
checkParams.push(companyCode);
|
|
}
|
|
|
|
const existing = await queryOne(checkSql, checkParams);
|
|
|
|
if (!existing) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "자동 입력 그룹을 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
// 그룹 업데이트
|
|
const updateSql = `
|
|
UPDATE cascading_auto_fill_group SET
|
|
group_name = COALESCE($1, group_name),
|
|
description = COALESCE($2, description),
|
|
master_table = COALESCE($3, master_table),
|
|
master_value_column = COALESCE($4, master_value_column),
|
|
master_label_column = COALESCE($5, master_label_column),
|
|
is_active = COALESCE($6, is_active),
|
|
updated_date = CURRENT_TIMESTAMP
|
|
WHERE group_code = $7 AND company_code = $8
|
|
RETURNING *
|
|
`;
|
|
|
|
const updateResult = await queryOne(updateSql, [
|
|
groupName,
|
|
description,
|
|
masterTable,
|
|
masterValueColumn,
|
|
masterLabelColumn,
|
|
isActive,
|
|
groupCode,
|
|
existing.company_code,
|
|
]);
|
|
|
|
// 매핑 업데이트 (전체 교체 방식)
|
|
if (mappings !== undefined) {
|
|
// 기존 매핑 삭제
|
|
await query(
|
|
`DELETE FROM cascading_auto_fill_mapping WHERE group_code = $1 AND company_code = $2`,
|
|
[groupCode, existing.company_code]
|
|
);
|
|
|
|
// 새 매핑 추가
|
|
for (let i = 0; i < mappings.length; i++) {
|
|
const m = mappings[i];
|
|
await query(
|
|
`INSERT INTO cascading_auto_fill_mapping (
|
|
group_code, company_code, source_column, target_field, target_label,
|
|
is_editable, is_required, default_value, sort_order
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
[
|
|
groupCode,
|
|
existing.company_code,
|
|
m.sourceColumn,
|
|
m.targetField,
|
|
m.targetLabel || null,
|
|
m.isEditable || "Y",
|
|
m.isRequired || "N",
|
|
m.defaultValue || null,
|
|
m.sortOrder || i + 1,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
logger.info("자동 입력 그룹 수정", { groupCode, companyCode, userId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "자동 입력 그룹이 수정되었습니다.",
|
|
data: updateResult,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("자동 입력 그룹 수정 실패", { error: error.message });
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "자동 입력 그룹 수정에 실패했습니다.",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 자동 입력 그룹 삭제
|
|
*/
|
|
export const deleteAutoFillGroup = async (req: Request, res: Response) => {
|
|
try {
|
|
const { groupCode } = req.params;
|
|
const companyCode = req.user?.companyCode || "*";
|
|
const userId = req.user?.userId || "system";
|
|
|
|
let deleteSql = `DELETE FROM cascading_auto_fill_group WHERE group_code = $1`;
|
|
const deleteParams: any[] = [groupCode];
|
|
|
|
if (companyCode !== "*") {
|
|
deleteSql += ` AND company_code = $2`;
|
|
deleteParams.push(companyCode);
|
|
}
|
|
|
|
deleteSql += ` RETURNING group_code`;
|
|
|
|
const result = await queryOne(deleteSql, deleteParams);
|
|
|
|
if (!result) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "자동 입력 그룹을 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
logger.info("자동 입력 그룹 삭제", { groupCode, companyCode, userId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "자동 입력 그룹이 삭제되었습니다.",
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("자동 입력 그룹 삭제 실패", { error: error.message });
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "자동 입력 그룹 삭제에 실패했습니다.",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
// =====================================================
|
|
// 자동 입력 데이터 조회 (실제 사용)
|
|
// =====================================================
|
|
|
|
/**
|
|
* 마스터 옵션 목록 조회
|
|
* 자동 입력 그룹의 마스터 테이블에서 선택 가능한 옵션 목록
|
|
*/
|
|
export const getAutoFillMasterOptions = async (req: Request, res: Response) => {
|
|
try {
|
|
const { groupCode } = req.params;
|
|
const companyCode = req.user?.companyCode || "*";
|
|
|
|
// 그룹 정보 조회
|
|
let groupSql = `SELECT * FROM cascading_auto_fill_group WHERE group_code = $1 AND is_active = 'Y'`;
|
|
const groupParams: any[] = [groupCode];
|
|
|
|
if (companyCode !== "*") {
|
|
groupSql += ` AND company_code = $2`;
|
|
groupParams.push(companyCode);
|
|
}
|
|
|
|
const group = await queryOne(groupSql, groupParams);
|
|
|
|
if (!group) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "자동 입력 그룹을 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
// 마스터 테이블에서 옵션 조회
|
|
const labelColumn = group.master_label_column || group.master_value_column;
|
|
let optionsSql = `
|
|
SELECT
|
|
${group.master_value_column} as value,
|
|
${labelColumn} as label
|
|
FROM ${group.master_table}
|
|
WHERE 1=1
|
|
`;
|
|
const optionsParams: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
// 멀티테넌시 필터 (테이블에 company_code가 있는 경우)
|
|
if (companyCode !== "*") {
|
|
// company_code 컬럼 존재 여부 확인
|
|
const columnCheck = await queryOne(
|
|
`SELECT column_name FROM information_schema.columns
|
|
WHERE table_name = $1 AND column_name = 'company_code'`,
|
|
[group.master_table]
|
|
);
|
|
|
|
if (columnCheck) {
|
|
optionsSql += ` AND company_code = $${paramIndex++}`;
|
|
optionsParams.push(companyCode);
|
|
}
|
|
}
|
|
|
|
optionsSql += ` ORDER BY ${labelColumn}`;
|
|
|
|
const optionsResult = await query(optionsSql, optionsParams);
|
|
|
|
logger.info("자동 입력 마스터 옵션 조회", { groupCode, count: optionsResult.length });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: optionsResult,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("자동 입력 마스터 옵션 조회 실패", { error: error.message });
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "자동 입력 마스터 옵션 조회에 실패했습니다.",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 자동 입력 데이터 조회
|
|
* 마스터 값 선택 시 자동으로 입력할 데이터 조회
|
|
*/
|
|
export const getAutoFillData = async (req: Request, res: Response) => {
|
|
try {
|
|
const { groupCode } = req.params;
|
|
const { masterValue } = req.query;
|
|
const companyCode = req.user?.companyCode || "*";
|
|
|
|
if (!masterValue) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "masterValue 파라미터가 필요합니다.",
|
|
});
|
|
}
|
|
|
|
// 그룹 정보 조회
|
|
let groupSql = `SELECT * FROM cascading_auto_fill_group WHERE group_code = $1 AND is_active = 'Y'`;
|
|
const groupParams: any[] = [groupCode];
|
|
|
|
if (companyCode !== "*") {
|
|
groupSql += ` AND company_code = $2`;
|
|
groupParams.push(companyCode);
|
|
}
|
|
|
|
const group = await queryOne(groupSql, groupParams);
|
|
|
|
if (!group) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "자동 입력 그룹을 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
// 매핑 정보 조회
|
|
const mappingSql = `
|
|
SELECT * FROM cascading_auto_fill_mapping
|
|
WHERE group_code = $1 AND company_code = $2
|
|
ORDER BY sort_order
|
|
`;
|
|
const mappings = await query(mappingSql, [groupCode, group.company_code]);
|
|
|
|
if (mappings.length === 0) {
|
|
return res.json({
|
|
success: true,
|
|
data: {},
|
|
mappings: [],
|
|
});
|
|
}
|
|
|
|
// 마스터 테이블에서 데이터 조회
|
|
const sourceColumns = mappings.map((m: any) => m.source_column).join(", ");
|
|
let dataSql = `
|
|
SELECT ${sourceColumns}
|
|
FROM ${group.master_table}
|
|
WHERE ${group.master_value_column} = $1
|
|
`;
|
|
const dataParams: any[] = [masterValue];
|
|
let paramIndex = 2;
|
|
|
|
// 멀티테넌시 필터
|
|
if (companyCode !== "*") {
|
|
const columnCheck = await queryOne(
|
|
`SELECT column_name FROM information_schema.columns
|
|
WHERE table_name = $1 AND column_name = 'company_code'`,
|
|
[group.master_table]
|
|
);
|
|
|
|
if (columnCheck) {
|
|
dataSql += ` AND company_code = $${paramIndex++}`;
|
|
dataParams.push(companyCode);
|
|
}
|
|
}
|
|
|
|
const dataResult = await queryOne(dataSql, dataParams);
|
|
|
|
// 결과를 target_field 기준으로 변환
|
|
const autoFillData: Record<string, any> = {};
|
|
const mappingInfo: any[] = [];
|
|
|
|
for (const mapping of mappings) {
|
|
const sourceValue = dataResult?.[mapping.source_column];
|
|
const finalValue = sourceValue !== null && sourceValue !== undefined
|
|
? sourceValue
|
|
: mapping.default_value;
|
|
|
|
autoFillData[mapping.target_field] = finalValue;
|
|
mappingInfo.push({
|
|
targetField: mapping.target_field,
|
|
targetLabel: mapping.target_label,
|
|
value: finalValue,
|
|
isEditable: mapping.is_editable === "Y",
|
|
isRequired: mapping.is_required === "Y",
|
|
});
|
|
}
|
|
|
|
logger.info("자동 입력 데이터 조회", { groupCode, masterValue, fieldCount: mappingInfo.length });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: autoFillData,
|
|
mappings: mappingInfo,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("자동 입력 데이터 조회 실패", { error: error.message });
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "자동 입력 데이터 조회에 실패했습니다.",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|