feat: Enhance Excel upload functionality with automatic numbering column detection

- Implemented automatic detection of numbering columns in the Excel upload modal, improving user experience by streamlining the upload process.
- Updated the master-detail Excel upload configuration to reflect changes in how numbering rules are applied, ensuring consistency across uploads.
- Refactored related components to remove deprecated properties and improve clarity in the configuration settings.
- Enhanced error handling and logging for better debugging during the upload process.
This commit is contained in:
kjs 2026-02-11 15:43:50 +09:00
parent eac2fa63b1
commit 2bbb5d7013
4 changed files with 362 additions and 305 deletions

View File

@ -413,6 +413,16 @@ class MasterDetailExcelService {
? `WHERE ${whereConditions.join(" AND ")}`
: "";
// 디테일 테이블의 id 컬럼 존재 여부 확인 (user_info 등 id가 없는 테이블 대응)
const detailIdCheck = await queryOne<{ exists: boolean }>(
`SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = $1 AND column_name = 'id'
) as exists`,
[detailTable]
);
const detailOrderColumn = detailIdCheck?.exists ? `d."id"` : `d."${detailFkColumn}"`;
// JOIN 쿼리 실행
const sql = `
SELECT ${selectClause}
@ -422,7 +432,7 @@ class MasterDetailExcelService {
AND m.company_code = d.company_code
${entityJoinClauses}
${whereClause}
ORDER BY m."${masterKeyColumn}", d.id
ORDER BY m."${masterKeyColumn}", ${detailOrderColumn}
`;
logger.info(`마스터-디테일 JOIN 쿼리:`, { sql, params });
@ -481,14 +491,67 @@ class MasterDetailExcelService {
}
}
/**
* , ID를
* , (*) fallback
*/
private async detectNumberingRuleForColumn(
tableName: string,
columnName: string,
companyCode?: string
): Promise<{ numberingRuleId: string } | null> {
try {
// 회사별 설정 우선, 공통 설정 fallback (company_code DESC로 회사별이 먼저)
const companyCondition = companyCode && companyCode !== "*"
? `AND company_code IN ($3, '*')`
: `AND company_code = '*'`;
const params = companyCode && companyCode !== "*"
? [tableName, columnName, companyCode]
: [tableName, columnName];
const result = await query<any>(
`SELECT input_type, detail_settings, company_code
FROM table_type_columns
WHERE table_name = $1 AND column_name = $2 ${companyCondition}
ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
params
);
logger.info(`채번 컬럼 조회 결과: ${tableName}.${columnName}`, {
rowCount: result.length,
rows: result.map((r: any) => ({ input_type: r.input_type, company_code: r.company_code })),
});
// 채번 타입인 행 찾기 (회사별 우선)
for (const row of result) {
if (row.input_type === "numbering") {
const settings = typeof row.detail_settings === "string"
? JSON.parse(row.detail_settings || "{}")
: row.detail_settings;
if (settings?.numberingRuleId) {
logger.info(`채번 컬럼 감지: ${tableName}.${columnName} → 규칙 ID: ${settings.numberingRuleId} (company: ${row.company_code})`);
return { numberingRuleId: settings.numberingRuleId };
}
}
}
logger.info(`채번 컬럼 아님: ${tableName}.${columnName}`);
return null;
} catch (error) {
logger.error(`채번 컬럼 감지 실패: ${tableName}.${columnName}`, error);
return null;
}
}
/**
* - ( )
*
* :
* 1.
* 2. UPSERT
* 3.
* 4. INSERT
* 1.
* 2-A. 경우: 다른 INSERT
* 2-B. 경우: 마스터 UPSERT
* 3. INSERT
*/
async uploadJoinedData(
relation: MasterDetailRelation,
@ -513,49 +576,123 @@ class MasterDetailExcelService {
const { masterTable, detailTable, masterKeyColumn, detailFkColumn, masterColumns, detailColumns } = relation;
// 1. 데이터를 마스터 키로 그룹화
// 마스터/디테일 테이블의 실제 컬럼 존재 여부 확인 (writer, created_date 등 하드코딩 방지)
const masterColsResult = await client.query(
`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`,
[masterTable]
);
const masterExistingCols = new Set(masterColsResult.rows.map((r: any) => r.column_name));
const detailColsResult = await client.query(
`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`,
[detailTable]
);
const detailExistingCols = new Set(detailColsResult.rows.map((r: any) => r.column_name));
// 마스터 키 컬럼의 채번 규칙 자동 감지 (회사별 설정 우선)
const numberingInfo = await this.detectNumberingRuleForColumn(masterTable, masterKeyColumn, companyCode);
const isAutoNumbering = !!numberingInfo;
logger.info(`마스터 키 채번 감지:`, {
masterKeyColumn,
isAutoNumbering,
numberingRuleId: numberingInfo?.numberingRuleId
});
// 데이터 그룹화
const groupedData = new Map<string, Record<string, any>[]>();
if (isAutoNumbering) {
// 채번 모드: 마스터 키 제외한 다른 마스터 컬럼 값으로 그룹화
const otherMasterCols = masterColumns.filter(c => c.name !== masterKeyColumn).map(c => c.name);
for (const row of data) {
// 다른 마스터 컬럼 값들을 조합해 그룹 키 생성
const groupKey = otherMasterCols.map(col => row[col] ?? "").join("|||");
if (!groupedData.has(groupKey)) {
groupedData.set(groupKey, []);
}
groupedData.get(groupKey)!.push(row);
}
logger.info(`채번 모드 그룹화 완료: ${groupedData.size}개 그룹 (기준: ${otherMasterCols.join(", ")})`);
} else {
// 일반 모드: 마스터 키 값으로 그룹화
for (const row of data) {
const masterKey = row[masterKeyColumn];
if (!masterKey) {
result.errors.push(`마스터 키(${masterKeyColumn}) 값이 없는 행이 있습니다.`);
continue;
}
if (!groupedData.has(masterKey)) {
groupedData.set(masterKey, []);
}
groupedData.get(masterKey)!.push(row);
}
logger.info(`데이터 그룹화 완료: ${groupedData.size}개 마스터 그룹`);
logger.info(`일반 모드 그룹화 완료: ${groupedData.size}개 마스터 그룹`);
}
// 2. 각 그룹 처리
for (const [masterKey, rows] of groupedData.entries()) {
// 각 그룹 처리
for (const [groupKey, rows] of groupedData.entries()) {
try {
// 2a. 마스터 데이터 추출 (첫 번째 행에서)
// 마스터 키 결정 (채번이면 자동 생성, 아니면 그룹 키 자체가 마스터 키)
let masterKey: string;
if (isAutoNumbering) {
// 채번 규칙으로 마스터 키 자동 생성
masterKey = await this.generateNumberWithRule(client, numberingInfo!.numberingRuleId, companyCode);
logger.info(`채번 생성: ${masterKey}`);
} else {
masterKey = groupKey;
}
// 마스터 데이터 추출 (첫 번째 행에서)
const masterData: Record<string, any> = {};
// 마스터 키 컬럼은 항상 설정 (분할패널 컬럼 목록에 없어도)
masterData[masterKeyColumn] = masterKey;
for (const col of masterColumns) {
if (col.name === masterKeyColumn) continue; // 이미 위에서 설정
if (rows[0][col.name] !== undefined) {
masterData[col.name] = rows[0][col.name];
}
}
// 회사 코드, 작성자 추가
// 회사 코드, 작성자 추가 (테이블에 해당 컬럼이 있을 때만)
if (masterExistingCols.has("company_code")) {
masterData.company_code = companyCode;
if (userId) {
}
if (userId && masterExistingCols.has("writer")) {
masterData.writer = userId;
}
// 2b. 마스터 UPSERT
// INSERT SQL 생성 헬퍼 (created_date 존재 시만 추가)
const buildInsertSQL = (table: string, data: Record<string, any>, existingCols: Set<string>) => {
const cols = Object.keys(data);
const hasCreatedDate = existingCols.has("created_date");
const colList = hasCreatedDate ? [...cols, "created_date"] : cols;
const placeholders = cols.map((_, i) => `$${i + 1}`);
const valList = hasCreatedDate ? [...placeholders, "NOW()"] : placeholders;
const values = cols.map(k => data[k]);
return {
sql: `INSERT INTO "${table}" (${colList.map(c => `"${c}"`).join(", ")}) VALUES (${valList.join(", ")})`,
values,
};
};
if (isAutoNumbering) {
// 채번 모드: 항상 INSERT (새 마스터 생성)
const { sql, values } = buildInsertSQL(masterTable, masterData, masterExistingCols);
await client.query(sql, values);
result.masterInserted++;
} else {
// 일반 모드: UPSERT (있으면 UPDATE, 없으면 INSERT)
const existingMaster = await client.query(
`SELECT id FROM "${masterTable}" WHERE "${masterKeyColumn}" = $1 AND company_code = $2`,
`SELECT 1 FROM "${masterTable}" WHERE "${masterKeyColumn}" = $1 AND company_code = $2`,
[masterKey, companyCode]
);
if (existingMaster.rows.length > 0) {
// UPDATE
const updateCols = Object.keys(masterData)
.filter(k => k !== masterKeyColumn && k !== "id")
.map((k, i) => `"${k}" = $${i + 1}`);
@ -564,43 +701,39 @@ class MasterDetailExcelService {
.map(k => masterData[k]);
if (updateCols.length > 0) {
const updatedDateClause = masterExistingCols.has("updated_date") ? `, updated_date = NOW()` : "";
await client.query(
`UPDATE "${masterTable}"
SET ${updateCols.join(", ")}, updated_date = NOW()
SET ${updateCols.join(", ")}${updatedDateClause}
WHERE "${masterKeyColumn}" = $${updateValues.length + 1} AND company_code = $${updateValues.length + 2}`,
[...updateValues, masterKey, companyCode]
);
}
result.masterUpdated++;
} else {
// INSERT
const insertCols = Object.keys(masterData);
const insertPlaceholders = insertCols.map((_, i) => `$${i + 1}`);
const insertValues = insertCols.map(k => masterData[k]);
await client.query(
`INSERT INTO "${masterTable}" (${insertCols.map(c => `"${c}"`).join(", ")}, created_date)
VALUES (${insertPlaceholders.join(", ")}, NOW())`,
insertValues
);
const { sql, values } = buildInsertSQL(masterTable, masterData, masterExistingCols);
await client.query(sql, values);
result.masterInserted++;
}
// 2c. 기존 디테일 삭제
// 일반 모드에서만 기존 디테일 삭제 (채번 모드는 새 마스터이므로 삭제할 디테일 없음)
const deleteResult = await client.query(
`DELETE FROM "${detailTable}" WHERE "${detailFkColumn}" = $1 AND company_code = $2`,
[masterKey, companyCode]
);
result.detailDeleted += deleteResult.rowCount || 0;
}
// 2d. 새 디테일 INSERT
// 디테일 INSERT
for (const row of rows) {
const detailData: Record<string, any> = {};
// FK 컬럼 추가
// FK 컬럼에 마스터 키 주입
detailData[detailFkColumn] = masterKey;
if (detailExistingCols.has("company_code")) {
detailData.company_code = companyCode;
if (userId) {
}
if (userId && detailExistingCols.has("writer")) {
detailData.writer = userId;
}
@ -611,20 +744,13 @@ class MasterDetailExcelService {
}
}
const insertCols = Object.keys(detailData);
const insertPlaceholders = insertCols.map((_, i) => `$${i + 1}`);
const insertValues = insertCols.map(k => detailData[k]);
await client.query(
`INSERT INTO "${detailTable}" (${insertCols.map(c => `"${c}"`).join(", ")}, created_date)
VALUES (${insertPlaceholders.join(", ")}, NOW())`,
insertValues
);
const { sql, values } = buildInsertSQL(detailTable, detailData, detailExistingCols);
await client.query(sql, values);
result.detailInserted++;
}
} catch (error: any) {
result.errors.push(`마스터 키 ${masterKey} 처리 실패: ${error.message}`);
logger.error(`마스터 키 ${masterKey} 처리 실패:`, error);
result.errors.push(`그룹 처리 실패: ${error.message}`);
logger.error(`그룹 처리 실패:`, error);
}
}

View File

@ -84,12 +84,9 @@ export interface ExcelUploadModalProps {
masterColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>;
detailColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>;
};
// 🆕 마스터-디테일 엑셀 업로드 설정
// 마스터-디테일 엑셀 업로드 설정
masterDetailExcelConfig?: MasterDetailExcelConfig;
// 🆕 단일 테이블 채번 설정
numberingRuleId?: string;
numberingTargetColumn?: string;
// 🆕 업로드 후 제어 실행 설정
// 업로드 후 제어 실행 설정
afterUploadFlows?: Array<{ flowId: string; order: number }>;
}
@ -112,9 +109,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
isMasterDetail = false,
masterDetailRelation,
masterDetailExcelConfig,
// 단일 테이블 채번 설정
numberingRuleId,
numberingTargetColumn,
// 업로드 후 제어 실행 설정
afterUploadFlows,
}) => {
@ -627,6 +621,44 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
setCurrentStep((prev) => Math.max(prev - 1, 1));
};
// 테이블 타입 관리에서 채번 컬럼 자동 감지
const detectNumberingColumn = async (
targetTableName: string
): Promise<{ columnName: string; numberingRuleId: string } | null> => {
try {
const { getTableColumns } = await import("@/lib/api/tableManagement");
const response = await getTableColumns(targetTableName);
if (response.success && response.data?.columns) {
for (const col of response.data.columns) {
if (col.inputType === "numbering") {
try {
const settings =
typeof col.detailSettings === "string"
? JSON.parse(col.detailSettings)
: col.detailSettings;
if (settings?.numberingRuleId) {
console.log(
`✅ 채번 컬럼 자동 감지: ${col.columnName} → 규칙 ID: ${settings.numberingRuleId}`
);
return {
columnName: col.columnName,
numberingRuleId: settings.numberingRuleId,
};
}
} catch {
// detailSettings 파싱 실패 시 무시
}
}
}
}
return null;
} catch (error) {
console.error("채번 컬럼 감지 실패:", error);
return null;
}
};
// 업로드 핸들러
const handleUpload = async () => {
if (!file || !tableName) {
@ -667,19 +699,24 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
`📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}`
);
// 🆕 마스터-디테일 간단 모드 처리 (마스터 필드 선택 + 채번)
// 마스터-디테일 간단 모드 처리 (마스터 필드 선택 + 채번 자동 감지)
if (isSimpleMasterDetailMode && screenId && masterDetailRelation) {
// 마스터 테이블에서 채번 컬럼 자동 감지
const masterNumberingInfo = await detectNumberingColumn(masterDetailRelation.masterTable);
const detectedNumberingRuleId = masterNumberingInfo?.numberingRuleId || masterDetailExcelConfig?.numberingRuleId;
console.log("📊 마스터-디테일 간단 모드 업로드:", {
masterDetailRelation,
masterFieldValues,
numberingRuleId: masterDetailExcelConfig?.numberingRuleId,
detectedNumberingRuleId,
autoDetected: !!masterNumberingInfo,
});
const uploadResult = await DynamicFormApi.uploadMasterDetailSimple(
screenId,
filteredData,
masterFieldValues,
masterDetailExcelConfig?.numberingRuleId || undefined,
detectedNumberingRuleId || undefined,
masterDetailExcelConfig?.afterUploadFlowId || undefined, // 하위 호환성
masterDetailExcelConfig?.afterUploadFlows || undefined // 다중 제어
);
@ -704,6 +741,24 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
else if (isMasterDetail && screenId && masterDetailRelation) {
console.log("📊 마스터-디테일 업로드 모드:", masterDetailRelation);
// 마스터 키 컬럼 매핑 검증 (채번 타입이면 자동 생성되므로 검증 생략)
const masterKeyCol = masterDetailRelation.masterKeyColumn;
const hasMasterKey = filteredData.length > 0 && filteredData[0][masterKeyCol] !== undefined && filteredData[0][masterKeyCol] !== null && filteredData[0][masterKeyCol] !== "";
if (!hasMasterKey) {
// 채번 여부 확인 - 채번이면 백엔드에서 자동 생성하므로 통과
const numberingInfo = await detectNumberingColumn(masterDetailRelation.masterTable);
const isMasterKeyAutoNumbering = numberingInfo && numberingInfo.columnName === masterKeyCol;
if (!isMasterKeyAutoNumbering) {
toast.error(
`마스터 키 컬럼(${masterKeyCol})이 매핑되지 않았습니다. 컬럼 매핑에서 [마스터] 항목을 확인해주세요.`
);
setIsUploading(false);
return;
}
console.log(`✅ 마스터 키(${masterKeyCol})는 채번 타입 → 백엔드에서 자동 생성`);
}
const uploadResult = await DynamicFormApi.uploadMasterDetailData(
screenId,
filteredData
@ -731,8 +786,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
let skipCount = 0;
let overwriteCount = 0;
// 단일 테이블 채번 설정 확인
const hasNumbering = numberingRuleId && numberingTargetColumn;
// 단일 테이블 채번 자동 감지 (테이블 타입 관리에서 input_type = 'numbering' 컬럼)
const numberingInfo = await detectNumberingColumn(tableName);
const hasNumbering = !!numberingInfo;
// 중복 체크 설정 확인
const duplicateCheckMappings = columnMappings.filter(
@ -816,14 +872,14 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
continue;
}
// 채번 적용: 각 행마다 채번 API 호출 (신규 등록 시에만)
if (hasNumbering && uploadMode === "insert" && !shouldUpdate) {
// 채번 적용: 각 행마다 채번 API 호출 (신규 등록 시에만, 자동 감지된 채번 규칙 사용)
if (hasNumbering && numberingInfo && uploadMode === "insert" && !shouldUpdate) {
try {
const { apiClient } = await import("@/lib/api/client");
const numberingResponse = await apiClient.post(`/numbering-rules/${numberingRuleId}/allocate`);
const numberingResponse = await apiClient.post(`/numbering-rules/${numberingInfo.numberingRuleId}/allocate`);
const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code;
if (numberingResponse.data?.success && generatedCode) {
dataToSave[numberingTargetColumn] = generatedCode;
dataToSave[numberingInfo.columnName] = generatedCode;
}
} catch (numError) {
console.error("채번 오류:", numError);

View File

@ -3777,7 +3777,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
/**
* -
* + column_labels에서 ,
* + column_labels에서 ( )
*/
const MasterDetailExcelUploadConfig: React.FC<{
config: any;
@ -4005,7 +4005,7 @@ const MasterDetailExcelUploadConfig: React.FC<{
{/* 마스터 키 자동 생성 안내 */}
{relationInfo && (
<p className="text-muted-foreground border-t pt-2 text-xs">
<strong>{relationInfo.masterKeyColumn}</strong>
<strong>{relationInfo.masterKeyColumn}</strong>
.
</p>
)}
@ -4114,165 +4114,15 @@ const MasterDetailExcelUploadConfig: React.FC<{
};
/**
* ( /- )
* ( )
*/
const ExcelNumberingRuleConfig: React.FC<{
config: { numberingRuleId?: string; numberingTargetColumn?: string };
updateConfig: (updates: { numberingRuleId?: string; numberingTargetColumn?: string }) => void;
tableName?: string; // 단일 테이블인 경우 테이블명
hasSplitPanel?: boolean; // 분할 패널 여부 (마스터-디테일)
}> = ({ config, updateConfig, tableName, hasSplitPanel }) => {
const [numberingRules, setNumberingRules] = useState<any[]>([]);
const [ruleSelectOpen, setRuleSelectOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [tableColumns, setTableColumns] = useState<Array<{ columnName: string; columnLabel: string }>>([]);
const [columnsLoading, setColumnsLoading] = useState(false);
// 채번 규칙 목록 로드
useEffect(() => {
const loadNumberingRules = async () => {
setIsLoading(true);
try {
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get("/numbering-rules");
if (response.data?.success && response.data?.data) {
setNumberingRules(response.data.data);
}
} catch (error) {
console.error("채번 규칙 목록 로드 실패:", error);
} finally {
setIsLoading(false);
}
};
loadNumberingRules();
}, []);
// 단일 테이블인 경우 컬럼 목록 로드
useEffect(() => {
if (!tableName || hasSplitPanel) {
setTableColumns([]);
return;
}
const loadColumns = async () => {
setColumnsLoading(true);
try {
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
if (response.data?.success && response.data?.data?.columns) {
const cols = response.data.data.columns.map((col: any) => ({
columnName: col.columnName || col.column_name,
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
}));
setTableColumns(cols);
}
} catch (error) {
console.error("컬럼 목록 로드 실패:", error);
} finally {
setColumnsLoading(false);
}
};
loadColumns();
}, [tableName, hasSplitPanel]);
const selectedRule = numberingRules.find((r) => String(r.rule_id || r.ruleId) === String(config.numberingRuleId));
const ExcelNumberingRuleInfo: React.FC = () => {
return (
<div className="border-t pt-3">
<Label className="text-xs"> </Label>
<p className="text-muted-foreground mb-2 text-xs">
/ .
<p className="text-muted-foreground mt-1 text-xs">
"채번" .
</p>
<Popover open={ruleSelectOpen} onOpenChange={setRuleSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={ruleSelectOpen}
className="h-8 w-full justify-between text-xs"
disabled={isLoading}
>
{isLoading ? "로딩 중..." : selectedRule?.rule_name || selectedRule?.ruleName || "채번 없음"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="채번 규칙 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
<CommandItem
value="__none__"
onSelect={() => {
updateConfig({ numberingRuleId: undefined, numberingTargetColumn: undefined });
setRuleSelectOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-4 w-4", !config.numberingRuleId ? "opacity-100" : "opacity-0")} />
</CommandItem>
{numberingRules.map((rule, idx) => {
const ruleId = String(rule.rule_id || rule.ruleId || `rule-${idx}`);
const ruleName = rule.rule_name || rule.ruleName || "(이름 없음)";
return (
<CommandItem
key={ruleId}
value={ruleName}
onSelect={() => {
updateConfig({ numberingRuleId: ruleId });
setRuleSelectOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
String(config.numberingRuleId) === ruleId ? "opacity-100" : "opacity-0",
)}
/>
{ruleName}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* 단일 테이블이고 채번 규칙이 선택된 경우, 적용할 컬럼 선택 */}
{config.numberingRuleId && !hasSplitPanel && tableName && (
<div className="mt-2">
<Label className="text-xs"> </Label>
<Select
value={config.numberingTargetColumn || ""}
onValueChange={(value) => updateConfig({ numberingTargetColumn: value || undefined })}
disabled={columnsLoading}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={columnsLoading ? "로딩 중..." : "컬럼 선택"} />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel} ({col.columnName})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-xs"> .</p>
</div>
)}
{/* 분할 패널인 경우 안내 메시지 */}
{config.numberingRuleId && hasSplitPanel && (
<p className="text-muted-foreground mt-1 text-xs">- .</p>
)}
</div>
);
};
@ -4440,14 +4290,10 @@ const ExcelUploadConfigSection: React.FC<{
allComponents: ComponentData[];
currentTableName?: string; // 현재 화면의 테이블명 (ButtonConfigPanel에서 전달)
}> = ({ config, onUpdateProperty, allComponents, currentTableName: propTableName }) => {
// 엑셀 업로드 설정 상태 관리
// 엑셀 업로드 설정 상태 관리 (채번은 테이블 타입 관리에서 자동 감지)
const [excelUploadConfig, setExcelUploadConfig] = useState<{
numberingRuleId?: string;
numberingTargetColumn?: string;
afterUploadFlows?: Array<{ flowId: string; order: number }>;
}>({
numberingRuleId: config.action?.excelNumberingRuleId,
numberingTargetColumn: config.action?.excelNumberingTargetColumn,
afterUploadFlows: config.action?.excelAfterUploadFlows || [],
});
@ -4529,17 +4375,11 @@ const ExcelUploadConfigSection: React.FC<{
);
}, [hasSplitPanel, singleTableName, propTableName]);
// 설정 업데이트 함수
// 설정 업데이트 함수 (채번은 테이블 타입 관리에서 자동 감지되므로 제어 실행만 관리)
const updateExcelUploadConfig = (updates: Partial<typeof excelUploadConfig>) => {
const newConfig = { ...excelUploadConfig, ...updates };
setExcelUploadConfig(newConfig);
if (updates.numberingRuleId !== undefined) {
onUpdateProperty("componentConfig.action.excelNumberingRuleId", updates.numberingRuleId);
}
if (updates.numberingTargetColumn !== undefined) {
onUpdateProperty("componentConfig.action.excelNumberingTargetColumn", updates.numberingTargetColumn);
}
if (updates.afterUploadFlows !== undefined) {
onUpdateProperty("componentConfig.action.excelAfterUploadFlows", updates.afterUploadFlows);
}
@ -4548,15 +4388,9 @@ const ExcelUploadConfigSection: React.FC<{
// config 변경 시 로컬 상태 동기화
useEffect(() => {
setExcelUploadConfig({
numberingRuleId: config.action?.excelNumberingRuleId,
numberingTargetColumn: config.action?.excelNumberingTargetColumn,
afterUploadFlows: config.action?.excelAfterUploadFlows || [],
});
}, [
config.action?.excelNumberingRuleId,
config.action?.excelNumberingTargetColumn,
config.action?.excelAfterUploadFlows,
]);
}, [config.action?.excelAfterUploadFlows]);
return (
<div className="bg-muted/50 mt-4 space-y-4 rounded-lg border p-4">
@ -4595,13 +4429,8 @@ const ExcelUploadConfigSection: React.FC<{
</div>
)}
{/* 채번 규칙 설정 (항상 표시) */}
<ExcelNumberingRuleConfig
config={excelUploadConfig}
updateConfig={updateExcelUploadConfig}
tableName={singleTableName}
hasSplitPanel={hasSplitPanel}
/>
{/* 채번 규칙 안내 (테이블 타입 관리에서 자동 감지) */}
<ExcelNumberingRuleInfo />
{/* 업로드 후 제어 실행 (항상 표시) */}
<ExcelAfterUploadControlConfig config={excelUploadConfig} updateConfig={updateExcelUploadConfig} />

View File

@ -4984,7 +4984,7 @@ export class ButtonActionExecutor {
// visible이 true인 컬럼만 추출
visibleColumns = columns.filter((col: any) => col.visible !== false).map((col: any) => col.columnName);
// 🎯 column_labels 테이블에서 실제 라벨 가져오기
// column_labels 테이블에서 실제 라벨 가져오기
try {
const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, {
params: { page: 1, size: 9999 },
@ -5021,19 +5021,77 @@ export class ButtonActionExecutor {
}
});
}
} else {
console.warn("⚠️ 화면 레이아웃에서 테이블 리스트 컴포넌트를 찾을 수 없습니다.");
}
}
} catch (error) {
console.error("❌ 화면 레이아웃 조회 실패:", error);
}
// 🎨 카테고리 값들 조회 (한 번만)
// Fallback: 레이아웃에서 컬럼 정보를 못 가져온 경우, table_type_columns에서 직접 조회
// 시스템 컬럼 제외 + 라벨 적용으로 raw 컬럼명 노출 방지
const SYSTEM_COLUMNS = ["id", "company_code", "created_date", "updated_date", "writer"];
if ((!visibleColumns || visibleColumns.length === 0) && context.tableName && dataToExport.length > 0) {
console.log("⚠️ 레이아웃에서 컬럼 설정을 찾지 못함 → table_type_columns에서 fallback 조회");
try {
const { apiClient } = await import("@/lib/api/client");
const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, {
params: { page: 1, size: 9999 },
});
if (columnsResponse.data?.success && columnsResponse.data?.data) {
let columnData = columnsResponse.data.data;
if (columnData.columns && Array.isArray(columnData.columns)) {
columnData = columnData.columns;
}
if (Array.isArray(columnData) && columnData.length > 0) {
// visible이 false가 아닌 컬럼만 + 시스템 컬럼 제외
const filteredCols = columnData.filter((col: any) => {
const colName = (col.column_name || col.columnName || "").toLowerCase();
if (SYSTEM_COLUMNS.includes(colName)) return false;
if (col.isVisible === false || col.is_visible === false) return false;
return true;
});
visibleColumns = filteredCols.map((col: any) => col.column_name || col.columnName);
columnLabels = {};
filteredCols.forEach((col: any) => {
const colName = col.column_name || col.columnName;
const labelValue = col.column_label || col.label || col.displayName || colName;
if (colName) {
columnLabels![colName] = labelValue;
}
});
console.log(`✅ Fallback 컬럼 ${visibleColumns.length}개 로드 완료`);
}
}
} catch (fallbackError) {
console.error("❌ Fallback 컬럼 조회 실패:", fallbackError);
}
}
// 최종 안전장치: 여전히 컬럼 정보가 없으면 데이터의 키에서 시스템 컬럼만 제외
if ((!visibleColumns || visibleColumns.length === 0) && dataToExport.length > 0) {
console.log("⚠️ 최종 fallback: 데이터 키에서 시스템 컬럼 제외");
const allKeys = Object.keys(dataToExport[0]);
visibleColumns = allKeys.filter((key) => {
const lowerKey = key.toLowerCase();
// 시스템 컬럼 제외
if (SYSTEM_COLUMNS.includes(lowerKey)) return false;
// _name, _label 등 조인된 보조 필드 제외
if (lowerKey.endsWith("_name") || lowerKey.endsWith("_label") || lowerKey.endsWith("_value_label")) return false;
return true;
});
// 라벨이 없으므로 최소한 column_labels 비워두지 않음 (컬럼명 그대로 표시되지만 시스템 컬럼은 제외됨)
if (!columnLabels) {
columnLabels = {};
}
}
// 카테고리 값들 조회 (한 번만)
const categoryMap: Record<string, Record<string, string>> = {};
let categoryColumns: string[] = [];
// 백엔드에서 카테고리 컬럼 정보 가져오기
if (context.tableName) {
try {
const { getCategoryColumns, getCategoryValues } = await import("@/lib/api/tableCategoryValue");
@ -5072,7 +5130,7 @@ export class ButtonActionExecutor {
}
}
// 🎨 컬럼 필터링 및 라벨 적용 (항상 실행)
// 컬럼 필터링 및 라벨 적용
if (visibleColumns && visibleColumns.length > 0 && dataToExport.length > 0) {
dataToExport = dataToExport.map((row: any) => {
const filteredRow: Record<string, any> = {};
@ -5165,6 +5223,8 @@ export class ButtonActionExecutor {
? config.excelAfterUploadFlows
: config.masterDetailExcel?.afterUploadFlows;
// masterDetailExcel 설정이 명시적으로 있을 때만 간단 모드 (디테일만 업로드)
// 설정이 없으면 기본 모드 (마스터+디테일 둘 다 업로드)
if (config.masterDetailExcel) {
masterDetailExcelConfig = {
...config.masterDetailExcel,
@ -5173,25 +5233,13 @@ export class ButtonActionExecutor {
detailTable: relationResponse.data.detailTable,
masterKeyColumn: relationResponse.data.masterKeyColumn,
detailFkColumn: relationResponse.data.detailFkColumn,
// 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑)
numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId,
// 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
afterUploadFlows,
};
} else {
// 버튼 설정이 없으면 분할 패널 정보만 사용
masterDetailExcelConfig = {
masterTable: relationResponse.data.masterTable,
detailTable: relationResponse.data.detailTable,
masterKeyColumn: relationResponse.data.masterKeyColumn,
detailFkColumn: relationResponse.data.detailFkColumn,
simpleMode: true, // 기본값으로 간단 모드 사용
// 채번 규칙 ID 추가 (excelNumberingRuleId 사용)
numberingRuleId: config.excelNumberingRuleId,
// 채번은 ExcelUploadModal에서 마스터 테이블 기반 자동 감지
// 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
afterUploadFlows,
};
}
// masterDetailExcel 설정 없으면 masterDetailExcelConfig는 undefined 유지
// → ExcelUploadModal에서 기본 모드로 동작 (마스터+디테일 둘 다 매핑/업로드)
}
}
@ -5233,9 +5281,7 @@ export class ButtonActionExecutor {
isMasterDetail,
masterDetailRelation,
masterDetailExcelConfig,
// 🆕 단일 테이블 채번 설정
numberingRuleId: config.excelNumberingRuleId,
numberingTargetColumn: config.excelNumberingTargetColumn,
// 채번은 ExcelUploadModal에서 테이블 타입 관리 기반 자동 감지
// 🆕 업로드 후 제어 실행 설정
afterUploadFlows: config.excelAfterUploadFlows,
onSuccess: () => {