엑셀 업로드,다운로드 기능 개선
This commit is contained in:
parent
ee3a648917
commit
aa0698556e
|
|
@ -187,6 +187,69 @@ router.post(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마스터-디테일 간단 모드 엑셀 업로드
|
||||||
|
* - 마스터 정보는 UI에서 선택
|
||||||
|
* - 디테일 정보만 엑셀에서 업로드
|
||||||
|
* - 채번 규칙을 통해 마스터 키 자동 생성
|
||||||
|
*
|
||||||
|
* POST /api/data/master-detail/upload-simple
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/master-detail/upload-simple",
|
||||||
|
authenticateToken,
|
||||||
|
async (req: AuthenticatedRequest, res) => {
|
||||||
|
try {
|
||||||
|
const { screenId, detailData, masterFieldValues, numberingRuleId } = req.body;
|
||||||
|
const companyCode = req.user?.companyCode || "*";
|
||||||
|
const userId = req.user?.userId || "system";
|
||||||
|
|
||||||
|
if (!screenId || !detailData || !Array.isArray(detailData)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "screenId와 detailData 배열이 필요합니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📤 마스터-디테일 간단 모드 업로드: screenId=${screenId}, rows=${detailData.length}`);
|
||||||
|
console.log(` 마스터 필드 값:`, masterFieldValues);
|
||||||
|
console.log(` 채번 규칙 ID:`, numberingRuleId);
|
||||||
|
|
||||||
|
// 업로드 실행
|
||||||
|
const result = await masterDetailExcelService.uploadSimple(
|
||||||
|
parseInt(screenId),
|
||||||
|
detailData,
|
||||||
|
masterFieldValues || {},
|
||||||
|
numberingRuleId,
|
||||||
|
companyCode,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ 마스터-디테일 간단 모드 업로드 완료:`, {
|
||||||
|
masterInserted: result.masterInserted,
|
||||||
|
detailInserted: result.detailInserted,
|
||||||
|
generatedKey: result.generatedKey,
|
||||||
|
errors: result.errors.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: result.success,
|
||||||
|
data: result,
|
||||||
|
message: result.success
|
||||||
|
? `마스터 1건(${result.generatedKey}), 디테일 ${result.detailInserted}건 처리되었습니다.`
|
||||||
|
: "업로드 중 오류가 발생했습니다.",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("마스터-디테일 간단 모드 업로드 오류:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "마스터-디테일 업로드 중 오류가 발생했습니다.",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// 기존 데이터 API
|
// 기존 데이터 API
|
||||||
// ================================
|
// ================================
|
||||||
|
|
|
||||||
|
|
@ -283,10 +283,81 @@ class MasterDetailExcelService {
|
||||||
try {
|
try {
|
||||||
const { masterTable, detailTable, masterKeyColumn, detailFkColumn, masterColumns, detailColumns } = relation;
|
const { masterTable, detailTable, masterKeyColumn, detailFkColumn, masterColumns, detailColumns } = relation;
|
||||||
|
|
||||||
|
// 조인 컬럼과 일반 컬럼 분리
|
||||||
|
// 조인 컬럼 형식: "테이블명.컬럼명" (예: customer_mng.customer_name)
|
||||||
|
const entityJoins: Array<{
|
||||||
|
refTable: string;
|
||||||
|
refColumn: string;
|
||||||
|
sourceColumn: string;
|
||||||
|
alias: string;
|
||||||
|
displayColumn: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
// SELECT 절 구성
|
// SELECT 절 구성
|
||||||
const masterSelectCols = masterColumns.map(col => `m."${col.name}"`);
|
const selectParts: string[] = [];
|
||||||
const detailSelectCols = detailColumns.map(col => `d."${col.name}"`);
|
let aliasIndex = 0;
|
||||||
const selectClause = [...masterSelectCols, ...detailSelectCols].join(", ");
|
|
||||||
|
// 마스터 컬럼 처리
|
||||||
|
for (const col of masterColumns) {
|
||||||
|
if (col.name.includes(".")) {
|
||||||
|
// 조인 컬럼: 테이블명.컬럼명
|
||||||
|
const [refTable, displayColumn] = col.name.split(".");
|
||||||
|
const alias = `ej${aliasIndex++}`;
|
||||||
|
|
||||||
|
// column_labels에서 FK 컬럼 찾기
|
||||||
|
const fkColumn = await this.findForeignKeyColumn(masterTable, refTable);
|
||||||
|
if (fkColumn) {
|
||||||
|
entityJoins.push({
|
||||||
|
refTable,
|
||||||
|
refColumn: fkColumn.referenceColumn,
|
||||||
|
sourceColumn: fkColumn.sourceColumn,
|
||||||
|
alias,
|
||||||
|
displayColumn,
|
||||||
|
});
|
||||||
|
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
|
||||||
|
} else {
|
||||||
|
// FK를 못 찾으면 NULL로 처리
|
||||||
|
selectParts.push(`NULL AS "${col.name}"`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 일반 컬럼
|
||||||
|
selectParts.push(`m."${col.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디테일 컬럼 처리
|
||||||
|
for (const col of detailColumns) {
|
||||||
|
if (col.name.includes(".")) {
|
||||||
|
// 조인 컬럼: 테이블명.컬럼명
|
||||||
|
const [refTable, displayColumn] = col.name.split(".");
|
||||||
|
const alias = `ej${aliasIndex++}`;
|
||||||
|
|
||||||
|
// column_labels에서 FK 컬럼 찾기
|
||||||
|
const fkColumn = await this.findForeignKeyColumn(detailTable, refTable);
|
||||||
|
if (fkColumn) {
|
||||||
|
entityJoins.push({
|
||||||
|
refTable,
|
||||||
|
refColumn: fkColumn.referenceColumn,
|
||||||
|
sourceColumn: fkColumn.sourceColumn,
|
||||||
|
alias,
|
||||||
|
displayColumn,
|
||||||
|
});
|
||||||
|
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
|
||||||
|
} else {
|
||||||
|
selectParts.push(`NULL AS "${col.name}"`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 일반 컬럼
|
||||||
|
selectParts.push(`d."${col.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectClause = selectParts.join(", ");
|
||||||
|
|
||||||
|
// 엔티티 조인 절 구성
|
||||||
|
const entityJoinClauses = entityJoins.map(ej =>
|
||||||
|
`LEFT JOIN "${ej.refTable}" ${ej.alias} ON m."${ej.sourceColumn}" = ${ej.alias}."${ej.refColumn}"`
|
||||||
|
).join("\n ");
|
||||||
|
|
||||||
// WHERE 절 구성
|
// WHERE 절 구성
|
||||||
const whereConditions: string[] = [];
|
const whereConditions: string[] = [];
|
||||||
|
|
@ -304,6 +375,8 @@ class MasterDetailExcelService {
|
||||||
if (filters) {
|
if (filters) {
|
||||||
for (const [key, value] of Object.entries(filters)) {
|
for (const [key, value] of Object.entries(filters)) {
|
||||||
if (value !== undefined && value !== null && value !== "") {
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
// 조인 컬럼인지 확인
|
||||||
|
if (key.includes(".")) continue;
|
||||||
// 마스터 테이블 컬럼인지 확인
|
// 마스터 테이블 컬럼인지 확인
|
||||||
const isMasterCol = masterColumns.some(c => c.name === key);
|
const isMasterCol = masterColumns.some(c => c.name === key);
|
||||||
const tableAlias = isMasterCol ? "m" : "d";
|
const tableAlias = isMasterCol ? "m" : "d";
|
||||||
|
|
@ -325,6 +398,7 @@ class MasterDetailExcelService {
|
||||||
LEFT JOIN "${detailTable}" d
|
LEFT JOIN "${detailTable}" d
|
||||||
ON m."${masterKeyColumn}" = d."${detailFkColumn}"
|
ON m."${masterKeyColumn}" = d."${detailFkColumn}"
|
||||||
AND m.company_code = d.company_code
|
AND m.company_code = d.company_code
|
||||||
|
${entityJoinClauses}
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY m."${masterKeyColumn}", d.id
|
ORDER BY m."${masterKeyColumn}", d.id
|
||||||
`;
|
`;
|
||||||
|
|
@ -353,6 +427,37 @@ class MasterDetailExcelService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 테이블에서 참조 테이블로의 FK 컬럼 찾기
|
||||||
|
*/
|
||||||
|
private async findForeignKeyColumn(
|
||||||
|
sourceTable: string,
|
||||||
|
referenceTable: string
|
||||||
|
): Promise<{ sourceColumn: string; referenceColumn: string } | null> {
|
||||||
|
try {
|
||||||
|
const result = await query<{ column_name: string; reference_column: string }>(
|
||||||
|
`SELECT column_name, reference_column
|
||||||
|
FROM column_labels
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND reference_table = $2
|
||||||
|
AND input_type = 'entity'
|
||||||
|
LIMIT 1`,
|
||||||
|
[sourceTable, referenceTable]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return {
|
||||||
|
sourceColumn: result[0].column_name,
|
||||||
|
referenceColumn: result[0].reference_column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`FK 컬럼 조회 실패: ${sourceTable} -> ${referenceTable}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마스터-디테일 데이터 업로드 (엑셀 업로드용)
|
* 마스터-디테일 데이터 업로드 (엑셀 업로드용)
|
||||||
*
|
*
|
||||||
|
|
@ -521,6 +626,168 @@ class MasterDetailExcelService {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마스터-디테일 간단 모드 업로드
|
||||||
|
*
|
||||||
|
* 마스터 정보는 UI에서 선택하고, 엑셀은 디테일 데이터만 포함
|
||||||
|
* 채번 규칙을 통해 마스터 키 자동 생성
|
||||||
|
*
|
||||||
|
* @param screenId 화면 ID
|
||||||
|
* @param detailData 디테일 데이터 배열
|
||||||
|
* @param masterFieldValues UI에서 선택한 마스터 필드 값
|
||||||
|
* @param numberingRuleId 채번 규칙 ID (optional)
|
||||||
|
* @param companyCode 회사 코드
|
||||||
|
* @param userId 사용자 ID
|
||||||
|
*/
|
||||||
|
async uploadSimple(
|
||||||
|
screenId: number,
|
||||||
|
detailData: Record<string, any>[],
|
||||||
|
masterFieldValues: Record<string, any>,
|
||||||
|
numberingRuleId: string | undefined,
|
||||||
|
companyCode: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
masterInserted: number;
|
||||||
|
detailInserted: number;
|
||||||
|
generatedKey: string;
|
||||||
|
errors: string[];
|
||||||
|
}> {
|
||||||
|
const result = {
|
||||||
|
success: false,
|
||||||
|
masterInserted: 0,
|
||||||
|
detailInserted: 0,
|
||||||
|
generatedKey: "",
|
||||||
|
errors: [] as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
|
// 1. 마스터-디테일 관계 정보 조회
|
||||||
|
const relation = await this.getMasterDetailRelation(screenId);
|
||||||
|
if (!relation) {
|
||||||
|
throw new Error("마스터-디테일 관계 정보를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { masterTable, detailTable, masterKeyColumn, detailFkColumn } = relation;
|
||||||
|
|
||||||
|
// 2. 채번 처리
|
||||||
|
let generatedKey: string;
|
||||||
|
|
||||||
|
if (numberingRuleId) {
|
||||||
|
// 채번 규칙으로 키 생성
|
||||||
|
generatedKey = await this.generateNumberWithRule(client, numberingRuleId, companyCode);
|
||||||
|
} else {
|
||||||
|
// 채번 규칙 없으면 마스터 필드에서 키 값 사용
|
||||||
|
generatedKey = masterFieldValues[masterKeyColumn];
|
||||||
|
if (!generatedKey) {
|
||||||
|
throw new Error(`마스터 키(${masterKeyColumn}) 값이 필요합니다.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.generatedKey = generatedKey;
|
||||||
|
logger.info(`채번 결과: ${generatedKey}`);
|
||||||
|
|
||||||
|
// 3. 마스터 레코드 생성
|
||||||
|
const masterData: Record<string, any> = {
|
||||||
|
...masterFieldValues,
|
||||||
|
[masterKeyColumn]: generatedKey,
|
||||||
|
company_code: companyCode,
|
||||||
|
writer: userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 마스터 컬럼명 목록 구성
|
||||||
|
const masterCols = Object.keys(masterData).filter(k => masterData[k] !== undefined);
|
||||||
|
const masterPlaceholders = masterCols.map((_, i) => `$${i + 1}`);
|
||||||
|
const masterValues = masterCols.map(k => masterData[k]);
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO "${masterTable}" (${masterCols.map(c => `"${c}"`).join(", ")}, created_date)
|
||||||
|
VALUES (${masterPlaceholders.join(", ")}, NOW())`,
|
||||||
|
masterValues
|
||||||
|
);
|
||||||
|
result.masterInserted = 1;
|
||||||
|
logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`);
|
||||||
|
|
||||||
|
// 4. 디테일 레코드들 생성
|
||||||
|
for (const row of detailData) {
|
||||||
|
try {
|
||||||
|
const detailRowData: Record<string, any> = {
|
||||||
|
...row,
|
||||||
|
[detailFkColumn]: generatedKey,
|
||||||
|
company_code: companyCode,
|
||||||
|
writer: userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 빈 값 필터링 및 id 제외
|
||||||
|
const detailCols = Object.keys(detailRowData).filter(k =>
|
||||||
|
k !== "id" &&
|
||||||
|
detailRowData[k] !== undefined &&
|
||||||
|
detailRowData[k] !== null &&
|
||||||
|
detailRowData[k] !== ""
|
||||||
|
);
|
||||||
|
const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`);
|
||||||
|
const detailValues = detailCols.map(k => detailRowData[k]);
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO "${detailTable}" (${detailCols.map(c => `"${c}"`).join(", ")}, created_date)
|
||||||
|
VALUES (${detailPlaceholders.join(", ")}, NOW())`,
|
||||||
|
detailValues
|
||||||
|
);
|
||||||
|
result.detailInserted++;
|
||||||
|
} catch (error: any) {
|
||||||
|
result.errors.push(`디테일 행 처리 실패: ${error.message}`);
|
||||||
|
logger.error(`디테일 행 처리 실패:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
result.success = result.errors.length === 0 || result.detailInserted > 0;
|
||||||
|
|
||||||
|
logger.info(`마스터-디테일 간단 모드 업로드 완료:`, {
|
||||||
|
masterInserted: result.masterInserted,
|
||||||
|
detailInserted: result.detailInserted,
|
||||||
|
generatedKey: result.generatedKey,
|
||||||
|
errors: result.errors.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
result.errors.push(`트랜잭션 실패: ${error.message}`);
|
||||||
|
logger.error(`마스터-디테일 간단 모드 업로드 실패:`, error);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 채번 규칙으로 번호 생성 (기존 numberingRuleService 사용)
|
||||||
|
*/
|
||||||
|
private async generateNumberWithRule(
|
||||||
|
client: any,
|
||||||
|
ruleId: string,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
// 기존 numberingRuleService를 사용하여 코드 할당
|
||||||
|
const { numberingRuleService } = await import("./numberingRuleService");
|
||||||
|
const generatedCode = await numberingRuleService.allocateCode(ruleId, companyCode);
|
||||||
|
|
||||||
|
logger.info(`채번 생성 (numberingRuleService): rule=${ruleId}, result=${generatedCode}`);
|
||||||
|
|
||||||
|
return generatedCode;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`채번 생성 실패: rule=${ruleId}, error=${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const masterDetailExcelService = new MasterDetailExcelService();
|
export const masterDetailExcelService = new MasterDetailExcelService();
|
||||||
|
|
|
||||||
|
|
@ -2761,33 +2761,64 @@ export class TableManagementService {
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const additionalColumn of options.additionalJoinColumns) {
|
for (const additionalColumn of options.additionalJoinColumns) {
|
||||||
// 🔍 sourceColumn을 기준으로 기존 조인 설정 찾기 (dept_code로 찾기)
|
// 🔍 1차: sourceColumn을 기준으로 기존 조인 설정 찾기
|
||||||
const baseJoinConfig = joinConfigs.find(
|
let baseJoinConfig = joinConfigs.find(
|
||||||
(config) => config.sourceColumn === additionalColumn.sourceColumn
|
(config) => config.sourceColumn === additionalColumn.sourceColumn
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🔍 2차: referenceTable을 기준으로 찾기 (프론트엔드가 customer_mng.customer_name 같은 형식을 요청할 때)
|
||||||
|
// 실제 소스 컬럼이 partner_id인데 프론트엔드가 customer_id로 추론하는 경우 대응
|
||||||
|
if (!baseJoinConfig && (additionalColumn as any).referenceTable) {
|
||||||
|
baseJoinConfig = joinConfigs.find(
|
||||||
|
(config) => config.referenceTable === (additionalColumn as any).referenceTable
|
||||||
|
);
|
||||||
|
if (baseJoinConfig) {
|
||||||
|
logger.info(`🔄 referenceTable로 조인 설정 찾음: ${(additionalColumn as any).referenceTable} → ${baseJoinConfig.sourceColumn}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (baseJoinConfig) {
|
if (baseJoinConfig) {
|
||||||
// joinAlias에서 실제 컬럼명 추출 (예: dept_code_location_name -> location_name)
|
// joinAlias에서 실제 컬럼명 추출
|
||||||
// sourceColumn을 제거한 나머지 부분이 실제 컬럼명
|
const sourceColumn = baseJoinConfig.sourceColumn; // 실제 소스 컬럼 (예: partner_id)
|
||||||
const sourceColumn = baseJoinConfig.sourceColumn; // dept_code
|
const originalJoinAlias = additionalColumn.joinAlias; // 프론트엔드가 보낸 별칭 (예: customer_id_customer_name)
|
||||||
const joinAlias = additionalColumn.joinAlias; // dept_code_company_name
|
|
||||||
const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // company_name
|
// 🔄 프론트엔드가 잘못된 소스 컬럼으로 추론한 경우 처리
|
||||||
|
// customer_id_customer_name → customer_name 추출 (customer_id_ 부분 제거)
|
||||||
|
// 또는 partner_id_customer_name → customer_name 추출 (partner_id_ 부분 제거)
|
||||||
|
let actualColumnName: string;
|
||||||
|
|
||||||
|
// 프론트엔드가 보낸 joinAlias에서 실제 컬럼명 추출
|
||||||
|
const frontendSourceColumn = additionalColumn.sourceColumn; // 프론트엔드가 추론한 소스 컬럼 (customer_id)
|
||||||
|
if (originalJoinAlias.startsWith(`${frontendSourceColumn}_`)) {
|
||||||
|
// 프론트엔드가 추론한 소스 컬럼으로 시작하면 그 부분 제거
|
||||||
|
actualColumnName = originalJoinAlias.replace(`${frontendSourceColumn}_`, "");
|
||||||
|
} else if (originalJoinAlias.startsWith(`${sourceColumn}_`)) {
|
||||||
|
// 실제 소스 컬럼으로 시작하면 그 부분 제거
|
||||||
|
actualColumnName = originalJoinAlias.replace(`${sourceColumn}_`, "");
|
||||||
|
} else {
|
||||||
|
// 어느 것도 아니면 원본 사용
|
||||||
|
actualColumnName = originalJoinAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 올바른 joinAlias 재생성 (실제 소스 컬럼 기반)
|
||||||
|
const correctedJoinAlias = `${sourceColumn}_${actualColumnName}`;
|
||||||
|
|
||||||
logger.info(`🔍 조인 컬럼 상세 분석:`, {
|
logger.info(`🔍 조인 컬럼 상세 분석:`, {
|
||||||
sourceColumn,
|
sourceColumn,
|
||||||
joinAlias,
|
frontendSourceColumn,
|
||||||
|
originalJoinAlias,
|
||||||
|
correctedJoinAlias,
|
||||||
actualColumnName,
|
actualColumnName,
|
||||||
referenceTable: additionalColumn.sourceTable,
|
referenceTable: (additionalColumn as any).referenceTable,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🚨 기본 Entity 조인과 중복되지 않도록 체크
|
// 🚨 기본 Entity 조인과 중복되지 않도록 체크
|
||||||
const isBasicEntityJoin =
|
const isBasicEntityJoin =
|
||||||
additionalColumn.joinAlias ===
|
correctedJoinAlias === `${sourceColumn}_name`;
|
||||||
`${baseJoinConfig.sourceColumn}_name`;
|
|
||||||
|
|
||||||
if (isBasicEntityJoin) {
|
if (isBasicEntityJoin) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`⚠️ 기본 Entity 조인과 중복: ${additionalColumn.joinAlias} - 건너뜀`
|
`⚠️ 기본 Entity 조인과 중복: ${correctedJoinAlias} - 건너뜀`
|
||||||
);
|
);
|
||||||
continue; // 기본 Entity 조인과 중복되면 추가하지 않음
|
continue; // 기본 Entity 조인과 중복되면 추가하지 않음
|
||||||
}
|
}
|
||||||
|
|
@ -2795,14 +2826,14 @@ export class TableManagementService {
|
||||||
// 추가 조인 컬럼 설정 생성
|
// 추가 조인 컬럼 설정 생성
|
||||||
const additionalJoinConfig: EntityJoinConfig = {
|
const additionalJoinConfig: EntityJoinConfig = {
|
||||||
sourceTable: tableName,
|
sourceTable: tableName,
|
||||||
sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code)
|
sourceColumn: sourceColumn, // 실제 소스 컬럼 (partner_id)
|
||||||
referenceTable:
|
referenceTable:
|
||||||
(additionalColumn as any).referenceTable ||
|
(additionalColumn as any).referenceTable ||
|
||||||
baseJoinConfig.referenceTable, // 참조 테이블 (dept_info)
|
baseJoinConfig.referenceTable, // 참조 테이블 (customer_mng)
|
||||||
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code)
|
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (customer_code)
|
||||||
displayColumns: [actualColumnName], // 표시할 컬럼들 (company_name)
|
displayColumns: [actualColumnName], // 표시할 컬럼들 (customer_name)
|
||||||
displayColumn: actualColumnName, // 하위 호환성
|
displayColumn: actualColumnName, // 하위 호환성
|
||||||
aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_company_name)
|
aliasColumn: correctedJoinAlias, // 수정된 별칭 (partner_id_customer_name)
|
||||||
separator: " - ", // 기본 구분자
|
separator: " - ", // 기본 구분자
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,35 @@ import { cn } from "@/lib/utils";
|
||||||
import { findMappingByColumns, saveMappingTemplate } from "@/lib/api/excelMapping";
|
import { findMappingByColumns, saveMappingTemplate } from "@/lib/api/excelMapping";
|
||||||
import { EditableSpreadsheet } from "./EditableSpreadsheet";
|
import { EditableSpreadsheet } from "./EditableSpreadsheet";
|
||||||
|
|
||||||
|
// 마스터-디테일 엑셀 업로드 설정 (버튼 설정에서 설정)
|
||||||
|
export interface MasterDetailExcelConfig {
|
||||||
|
// 테이블 정보
|
||||||
|
masterTable?: string;
|
||||||
|
detailTable?: string;
|
||||||
|
masterKeyColumn?: string;
|
||||||
|
detailFkColumn?: string;
|
||||||
|
// 채번
|
||||||
|
numberingRuleId?: string;
|
||||||
|
// 업로드 전 사용자가 선택할 마스터 테이블 필드
|
||||||
|
masterSelectFields?: Array<{
|
||||||
|
columnName: string;
|
||||||
|
columnLabel: string;
|
||||||
|
required: boolean;
|
||||||
|
inputType: "entity" | "date" | "text" | "select";
|
||||||
|
referenceTable?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
displayColumn?: string;
|
||||||
|
}>;
|
||||||
|
// 엑셀에서 매핑할 디테일 테이블 필드
|
||||||
|
detailExcelFields?: Array<{
|
||||||
|
columnName: string;
|
||||||
|
columnLabel: string;
|
||||||
|
required: boolean;
|
||||||
|
}>;
|
||||||
|
masterDefaults?: Record<string, any>;
|
||||||
|
detailDefaults?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExcelUploadModalProps {
|
export interface ExcelUploadModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
|
@ -53,6 +82,8 @@ export interface ExcelUploadModalProps {
|
||||||
masterColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>;
|
masterColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>;
|
||||||
detailColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>;
|
detailColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>;
|
||||||
};
|
};
|
||||||
|
// 🆕 마스터-디테일 엑셀 업로드 설정
|
||||||
|
masterDetailExcelConfig?: MasterDetailExcelConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColumnMapping {
|
interface ColumnMapping {
|
||||||
|
|
@ -71,6 +102,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
screenId,
|
screenId,
|
||||||
isMasterDetail = false,
|
isMasterDetail = false,
|
||||||
masterDetailRelation,
|
masterDetailRelation,
|
||||||
|
masterDetailExcelConfig,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
|
||||||
|
|
@ -93,6 +125,116 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
// 3단계: 확인
|
// 3단계: 확인
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
|
// 🆕 마스터-디테일 모드: 마스터 필드 입력값
|
||||||
|
const [masterFieldValues, setMasterFieldValues] = useState<Record<string, any>>({});
|
||||||
|
const [entitySearchData, setEntitySearchData] = useState<Record<string, any[]>>({});
|
||||||
|
const [entitySearchLoading, setEntitySearchLoading] = useState<Record<string, boolean>>({});
|
||||||
|
const [entityDisplayColumns, setEntityDisplayColumns] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 🆕 엔티티 참조 데이터 로드
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("🔍 엔티티 데이터 로드 체크:", {
|
||||||
|
masterSelectFields: masterDetailExcelConfig?.masterSelectFields,
|
||||||
|
open,
|
||||||
|
isMasterDetail,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!masterDetailExcelConfig?.masterSelectFields) return;
|
||||||
|
|
||||||
|
const loadEntityData = async () => {
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const { DynamicFormApi } = await import("@/lib/api/dynamicForm");
|
||||||
|
|
||||||
|
for (const field of masterDetailExcelConfig.masterSelectFields!) {
|
||||||
|
console.log("🔍 필드 처리:", field);
|
||||||
|
|
||||||
|
if (field.inputType === "entity") {
|
||||||
|
setEntitySearchLoading((prev) => ({ ...prev, [field.columnName]: true }));
|
||||||
|
try {
|
||||||
|
let refTable = field.referenceTable;
|
||||||
|
console.log("🔍 초기 refTable:", refTable);
|
||||||
|
|
||||||
|
let displayCol = field.displayColumn;
|
||||||
|
|
||||||
|
// referenceTable 또는 displayColumn이 없으면 DB에서 동적으로 조회
|
||||||
|
if ((!refTable || !displayCol) && masterDetailExcelConfig.masterTable) {
|
||||||
|
console.log("🔍 DB에서 referenceTable/displayColumn 조회 시도:", masterDetailExcelConfig.masterTable);
|
||||||
|
const colResponse = await apiClient.get(
|
||||||
|
`/table-management/tables/${masterDetailExcelConfig.masterTable}/columns`
|
||||||
|
);
|
||||||
|
console.log("🔍 컬럼 조회 응답:", colResponse.data);
|
||||||
|
|
||||||
|
if (colResponse.data?.success && colResponse.data?.data?.columns) {
|
||||||
|
const colInfo = colResponse.data.data.columns.find(
|
||||||
|
(c: any) => (c.columnName || c.column_name) === field.columnName
|
||||||
|
);
|
||||||
|
console.log("🔍 찾은 컬럼 정보:", colInfo);
|
||||||
|
if (colInfo) {
|
||||||
|
if (!refTable) {
|
||||||
|
refTable = colInfo.referenceTable || colInfo.reference_table;
|
||||||
|
console.log("🔍 DB에서 가져온 refTable:", refTable);
|
||||||
|
}
|
||||||
|
if (!displayCol) {
|
||||||
|
displayCol = colInfo.displayColumn || colInfo.display_column;
|
||||||
|
console.log("🔍 DB에서 가져온 displayColumn:", displayCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayColumn 저장 (Select 렌더링 시 사용)
|
||||||
|
if (displayCol) {
|
||||||
|
setEntityDisplayColumns((prev) => ({ ...prev, [field.columnName]: displayCol }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refTable) {
|
||||||
|
console.log("🔍 엔티티 데이터 조회:", refTable);
|
||||||
|
const response = await DynamicFormApi.getTableData(refTable, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1000,
|
||||||
|
});
|
||||||
|
console.log("🔍 엔티티 데이터 응답:", response);
|
||||||
|
// getTableData는 { success, data: [...] } 형식으로 반환
|
||||||
|
const rows = response.data?.rows || response.data;
|
||||||
|
if (response.success && rows && Array.isArray(rows)) {
|
||||||
|
setEntitySearchData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.columnName]: rows,
|
||||||
|
}));
|
||||||
|
console.log("✅ 엔티티 데이터 로드 성공:", field.columnName, rows.length, "개");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("❌ 엔티티 필드의 referenceTable을 찾을 수 없음:", field.columnName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 엔티티 데이터 로드 실패:", field.columnName, error);
|
||||||
|
} finally {
|
||||||
|
setEntitySearchLoading((prev) => ({ ...prev, [field.columnName]: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open && isMasterDetail && masterDetailExcelConfig?.masterSelectFields?.length > 0) {
|
||||||
|
loadEntityData();
|
||||||
|
}
|
||||||
|
}, [open, isMasterDetail, masterDetailExcelConfig]);
|
||||||
|
|
||||||
|
// 마스터-디테일 모드에서 마스터 필드 입력 여부 확인
|
||||||
|
const isSimpleMasterDetailMode = isMasterDetail && masterDetailExcelConfig;
|
||||||
|
const hasMasterSelectFields = isSimpleMasterDetailMode &&
|
||||||
|
(masterDetailExcelConfig?.masterSelectFields?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
// 마스터 필드가 모두 입력되었는지 확인
|
||||||
|
const isMasterFieldsValid = () => {
|
||||||
|
if (!hasMasterSelectFields) return true;
|
||||||
|
return masterDetailExcelConfig!.masterSelectFields!.every((field) => {
|
||||||
|
if (!field.required) return true;
|
||||||
|
const value = masterFieldValues[field.columnName];
|
||||||
|
return value !== undefined && value !== null && value !== "";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 파일 선택 핸들러
|
// 파일 선택 핸들러
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selectedFile = e.target.files?.[0];
|
const selectedFile = e.target.files?.[0];
|
||||||
|
|
@ -198,12 +340,51 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
|
|
||||||
const loadTableSchema = async () => {
|
const loadTableSchema = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("🔍 테이블 스키마 로드 시작:", { tableName, isMasterDetail });
|
console.log("🔍 테이블 스키마 로드 시작:", { tableName, isMasterDetail, isSimpleMasterDetailMode });
|
||||||
|
|
||||||
let allColumns: TableColumn[] = [];
|
let allColumns: TableColumn[] = [];
|
||||||
|
|
||||||
// 🆕 마스터-디테일 모드: 두 테이블의 컬럼 합치기
|
// 🆕 마스터-디테일 간단 모드: 디테일 테이블 컬럼만 로드 (마스터 필드는 UI에서 선택)
|
||||||
if (isMasterDetail && masterDetailRelation) {
|
if (isSimpleMasterDetailMode && masterDetailRelation) {
|
||||||
|
const { detailTable, detailFkColumn } = masterDetailRelation;
|
||||||
|
|
||||||
|
console.log("📊 마스터-디테일 간단 모드 스키마 로드 (디테일만):", { detailTable });
|
||||||
|
|
||||||
|
// 디테일 테이블 스키마만 로드 (마스터 정보는 UI에서 선택)
|
||||||
|
const detailResponse = await getTableSchema(detailTable);
|
||||||
|
if (detailResponse.success && detailResponse.data) {
|
||||||
|
// 설정된 detailExcelFields가 있으면 해당 필드만, 없으면 전체
|
||||||
|
const configuredFields = masterDetailExcelConfig?.detailExcelFields;
|
||||||
|
|
||||||
|
const detailCols = detailResponse.data.columns
|
||||||
|
.filter((col) => {
|
||||||
|
// 자동 생성 컬럼, FK 컬럼 제외
|
||||||
|
if (AUTO_GENERATED_COLUMNS.includes(col.name.toLowerCase())) return false;
|
||||||
|
if (col.name === detailFkColumn) return false;
|
||||||
|
|
||||||
|
// 설정된 필드가 있으면 해당 필드만
|
||||||
|
if (configuredFields && configuredFields.length > 0) {
|
||||||
|
return configuredFields.some((f) => f.columnName === col.name);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((col) => {
|
||||||
|
// 설정에서 라벨 찾기
|
||||||
|
const configField = configuredFields?.find((f) => f.columnName === col.name);
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
label: configField?.columnLabel || col.label || col.name,
|
||||||
|
originalName: col.name,
|
||||||
|
sourceTable: detailTable,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
allColumns = detailCols;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ 마스터-디테일 간단 모드 컬럼 로드 완료:", allColumns.length);
|
||||||
|
}
|
||||||
|
// 🆕 마스터-디테일 기존 모드: 두 테이블의 컬럼 합치기
|
||||||
|
else if (isMasterDetail && masterDetailRelation) {
|
||||||
const { masterTable, detailTable, detailFkColumn } = masterDetailRelation;
|
const { masterTable, detailTable, detailFkColumn } = masterDetailRelation;
|
||||||
|
|
||||||
console.log("📊 마스터-디테일 스키마 로드:", { masterTable, detailTable });
|
console.log("📊 마스터-디테일 스키마 로드:", { masterTable, detailTable });
|
||||||
|
|
@ -365,6 +546,12 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 마스터-디테일 간단 모드: 마스터 필드 유효성 검사
|
||||||
|
if (currentStep === 1 && hasMasterSelectFields && !isMasterFieldsValid()) {
|
||||||
|
toast.error("마스터 정보를 모두 입력해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 1단계 → 2단계 전환 시: 빈 헤더 열 제외
|
// 1단계 → 2단계 전환 시: 빈 헤더 열 제외
|
||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
// 빈 헤더가 아닌 열만 필터링
|
// 빈 헤더가 아닌 열만 필터링
|
||||||
|
|
@ -449,8 +636,39 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
`📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}행`
|
`📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}행`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🆕 마스터-디테일 모드 처리
|
// 🆕 마스터-디테일 간단 모드 처리 (마스터 필드 선택 + 채번)
|
||||||
if (isMasterDetail && screenId && masterDetailRelation) {
|
if (isSimpleMasterDetailMode && screenId && masterDetailRelation) {
|
||||||
|
console.log("📊 마스터-디테일 간단 모드 업로드:", {
|
||||||
|
masterDetailRelation,
|
||||||
|
masterFieldValues,
|
||||||
|
numberingRuleId: masterDetailExcelConfig?.numberingRuleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadResult = await DynamicFormApi.uploadMasterDetailSimple(
|
||||||
|
screenId,
|
||||||
|
filteredData,
|
||||||
|
masterFieldValues,
|
||||||
|
masterDetailExcelConfig?.numberingRuleId || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uploadResult.success && uploadResult.data) {
|
||||||
|
const { masterInserted, detailInserted, generatedKey, errors } = uploadResult.data;
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`마스터 ${masterInserted}건(${generatedKey || ""}), 디테일 ${detailInserted}건 처리되었습니다.` +
|
||||||
|
(errors?.length > 0 ? ` (오류: ${errors.length}건)` : "")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 매핑 템플릿 저장
|
||||||
|
await saveMappingTemplateInternal();
|
||||||
|
|
||||||
|
onSuccess?.();
|
||||||
|
} else {
|
||||||
|
toast.error(uploadResult.message || "마스터-디테일 업로드에 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 🆕 마스터-디테일 기존 모드 처리
|
||||||
|
else if (isMasterDetail && screenId && masterDetailRelation) {
|
||||||
console.log("📊 마스터-디테일 업로드 모드:", masterDetailRelation);
|
console.log("📊 마스터-디테일 업로드 모드:", masterDetailRelation);
|
||||||
|
|
||||||
const uploadResult = await DynamicFormApi.uploadMasterDetailData(
|
const uploadResult = await DynamicFormApi.uploadMasterDetailData(
|
||||||
|
|
@ -558,6 +776,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
setExcelColumns([]);
|
setExcelColumns([]);
|
||||||
setSystemColumns([]);
|
setSystemColumns([]);
|
||||||
setColumnMappings([]);
|
setColumnMappings([]);
|
||||||
|
// 🆕 마스터-디테일 모드 초기화
|
||||||
|
setMasterFieldValues({});
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
|
@ -647,6 +867,87 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
{/* 1단계: 파일 선택 & 미리보기 (통합) */}
|
{/* 1단계: 파일 선택 & 미리보기 (통합) */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* 🆕 마스터-디테일 간단 모드: 마스터 필드 입력 */}
|
||||||
|
{hasMasterSelectFields && (
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
{masterDetailExcelConfig?.masterSelectFields?.map((field) => (
|
||||||
|
<div key={field.columnName} className="space-y-1">
|
||||||
|
<Label className="text-xs">
|
||||||
|
{field.columnLabel}
|
||||||
|
{field.required && <span className="ml-1 text-destructive">*</span>}
|
||||||
|
</Label>
|
||||||
|
{field.inputType === "entity" ? (
|
||||||
|
<Select
|
||||||
|
value={masterFieldValues[field.columnName]?.toString() || ""}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setMasterFieldValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.columnName]: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 text-xs">
|
||||||
|
<SelectValue placeholder={`${field.columnLabel} 선택`} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{entitySearchLoading[field.columnName] ? (
|
||||||
|
<SelectItem value="loading" disabled>
|
||||||
|
로딩 중...
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
entitySearchData[field.columnName]?.map((item: any) => {
|
||||||
|
const keyValue = item[field.referenceColumn || "id"];
|
||||||
|
// displayColumn: 저장된 값 → DB에서 조회한 값 → referenceColumn → id
|
||||||
|
const displayColName =
|
||||||
|
field.displayColumn ||
|
||||||
|
entityDisplayColumns[field.columnName] ||
|
||||||
|
field.referenceColumn ||
|
||||||
|
"id";
|
||||||
|
const displayValue = item[displayColName] || keyValue;
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
key={keyValue}
|
||||||
|
value={keyValue?.toString()}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : field.inputType === "date" ? (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={masterFieldValues[field.columnName] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setMasterFieldValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.columnName]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="h-9 w-full rounded-md border px-3 text-xs"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={masterFieldValues[field.columnName] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setMasterFieldValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.columnName]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder={field.columnLabel}
|
||||||
|
className="h-9 w-full rounded-md border px-3 text-xs"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 파일 선택 영역 */}
|
{/* 파일 선택 영역 */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="file-upload" className="text-xs sm:text-sm">
|
<Label htmlFor="file-upload" className="text-xs sm:text-sm">
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -655,6 +655,52 @@ export class DynamicFormApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마스터-디테일 간단 모드 엑셀 업로드
|
||||||
|
* - 마스터 정보는 UI에서 선택
|
||||||
|
* - 디테일 정보만 엑셀에서 업로드
|
||||||
|
* - 채번 규칙을 통해 마스터 키 자동 생성
|
||||||
|
* @param screenId 화면 ID
|
||||||
|
* @param detailData 디테일 데이터 배열
|
||||||
|
* @param masterFieldValues UI에서 선택한 마스터 필드 값
|
||||||
|
* @param numberingRuleId 채번 규칙 ID (optional)
|
||||||
|
* @returns 업로드 결과
|
||||||
|
*/
|
||||||
|
static async uploadMasterDetailSimple(
|
||||||
|
screenId: number,
|
||||||
|
detailData: Record<string, any>[],
|
||||||
|
masterFieldValues: Record<string, any>,
|
||||||
|
numberingRuleId?: string
|
||||||
|
): Promise<ApiResponse<MasterDetailSimpleUploadResult>> {
|
||||||
|
try {
|
||||||
|
console.log("📤 마스터-디테일 간단 모드 업로드:", {
|
||||||
|
screenId,
|
||||||
|
detailRowCount: detailData.length,
|
||||||
|
masterFieldValues,
|
||||||
|
numberingRuleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await apiClient.post(`/data/master-detail/upload-simple`, {
|
||||||
|
screenId,
|
||||||
|
detailData,
|
||||||
|
masterFieldValues,
|
||||||
|
numberingRuleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: response.data?.success,
|
||||||
|
data: response.data?.data,
|
||||||
|
message: response.data?.message,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ 마스터-디테일 간단 모드 업로드 실패:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.response?.data?.message || error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마스터-디테일 관계 타입
|
// 마스터-디테일 관계 타입
|
||||||
|
|
@ -687,5 +733,14 @@ export interface MasterDetailUploadResult {
|
||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 마스터-디테일 간단 모드 업로드 결과 타입
|
||||||
|
export interface MasterDetailSimpleUploadResult {
|
||||||
|
success: boolean;
|
||||||
|
masterInserted: number;
|
||||||
|
detailInserted: number;
|
||||||
|
generatedKey: string; // 생성된 마스터 키
|
||||||
|
errors?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
// 편의를 위한 기본 export
|
// 편의를 위한 기본 export
|
||||||
export const dynamicFormApi = DynamicFormApi;
|
export const dynamicFormApi = DynamicFormApi;
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
if (item[underscoreKey] !== undefined) {
|
if (item[underscoreKey] !== undefined) {
|
||||||
return item[underscoreKey];
|
return item[underscoreKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6️⃣ 🆕 모든 키에서 _fieldName으로 끝나는 키 찾기
|
||||||
|
// 예: partner_id_customer_name (프론트엔드가 customer_id로 추론했지만 실제는 partner_id인 경우)
|
||||||
|
const matchingKey = Object.keys(item).find((key) => key.endsWith(`_${fieldName}`));
|
||||||
|
if (matchingKey && item[matchingKey] !== undefined) {
|
||||||
|
return item[matchingKey];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
||||||
|
|
@ -2886,3 +2886,4 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4840,10 +4840,12 @@ export class ButtonActionExecutor {
|
||||||
screenId: context.screenId,
|
screenId: context.screenId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 마스터-디테일 구조 확인
|
// 🆕 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지)
|
||||||
let isMasterDetail = false;
|
let isMasterDetail = false;
|
||||||
let masterDetailRelation: any = null;
|
let masterDetailRelation: any = null;
|
||||||
|
let masterDetailExcelConfig: any = undefined;
|
||||||
|
|
||||||
|
// 화면 레이아웃에서 분할 패널 자동 감지
|
||||||
if (context.screenId) {
|
if (context.screenId) {
|
||||||
const { DynamicFormApi } = await import("@/lib/api/dynamicForm");
|
const { DynamicFormApi } = await import("@/lib/api/dynamicForm");
|
||||||
const relationResponse = await DynamicFormApi.getMasterDetailRelation(context.screenId);
|
const relationResponse = await DynamicFormApi.getMasterDetailRelation(context.screenId);
|
||||||
|
|
@ -4851,7 +4853,34 @@ export class ButtonActionExecutor {
|
||||||
if (relationResponse.success && relationResponse.data) {
|
if (relationResponse.success && relationResponse.data) {
|
||||||
isMasterDetail = true;
|
isMasterDetail = true;
|
||||||
masterDetailRelation = relationResponse.data;
|
masterDetailRelation = relationResponse.data;
|
||||||
console.log("📊 마스터-디테일 구조 감지:", masterDetailRelation);
|
|
||||||
|
// 버튼 설정에서 채번 규칙 등 추가 설정 가져오기
|
||||||
|
if (config.masterDetailExcel) {
|
||||||
|
masterDetailExcelConfig = {
|
||||||
|
...config.masterDetailExcel,
|
||||||
|
// 분할 패널에서 감지한 테이블 정보로 덮어쓰기
|
||||||
|
masterTable: relationResponse.data.masterTable,
|
||||||
|
detailTable: relationResponse.data.detailTable,
|
||||||
|
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
||||||
|
detailFkColumn: relationResponse.data.detailFkColumn,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 버튼 설정이 없으면 분할 패널 정보만 사용
|
||||||
|
masterDetailExcelConfig = {
|
||||||
|
masterTable: relationResponse.data.masterTable,
|
||||||
|
detailTable: relationResponse.data.detailTable,
|
||||||
|
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
||||||
|
detailFkColumn: relationResponse.data.detailFkColumn,
|
||||||
|
simpleMode: true, // 기본값으로 간단 모드 사용
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📊 마스터-디테일 구조 자동 감지:", {
|
||||||
|
masterTable: relationResponse.data.masterTable,
|
||||||
|
detailTable: relationResponse.data.detailTable,
|
||||||
|
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
||||||
|
numberingRuleId: masterDetailExcelConfig?.numberingRuleId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4901,6 +4930,7 @@ export class ButtonActionExecutor {
|
||||||
screenId: context.screenId,
|
screenId: context.screenId,
|
||||||
isMasterDetail,
|
isMasterDetail,
|
||||||
masterDetailRelation,
|
masterDetailRelation,
|
||||||
|
masterDetailExcelConfig,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
|
// 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
|
||||||
context.onRefresh?.();
|
context.onRefresh?.();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue