lhj #343

Merged
hjlee merged 6 commits from lhj into main 2026-01-08 17:15:05 +09:00
6 changed files with 211 additions and 240 deletions
Showing only changes of commit 85f8637ce0 - Show all commits

View File

@ -217,11 +217,14 @@ router.post("/:ruleId/allocate", authenticateToken, async (req: AuthenticatedReq
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
logger.info("코드 할당 요청", { ruleId, companyCode });
try {
const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode);
logger.info("코드 할당 성공", { ruleId, allocatedCode });
return res.json({ success: true, data: { generatedCode: allocatedCode } });
} catch (error: any) {
logger.error("코드 할당 실패", { error: error.message });
logger.error("코드 할당 실패", { ruleId, companyCode, error: error.message });
return res.status(500).json({ success: false, error: error.message });
}
});

View File

@ -761,10 +761,74 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// INSERT 모드
console.log("[EditModal] INSERT 모드 - 새 데이터 생성:", formData);
// 🆕 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)
const dataToSave = { ...formData };
const fieldsWithNumbering: Record<string, string> = {};
// formData에서 채번 규칙이 설정된 필드 찾기
for (const [key, value] of Object.entries(formData)) {
if (key.endsWith("_numberingRuleId") && value) {
const fieldName = key.replace("_numberingRuleId", "");
fieldsWithNumbering[fieldName] = value as string;
console.log(`🎯 [EditModal] 채번 규칙 발견: ${fieldName} → 규칙 ${value}`);
}
}
// 채번 규칙이 있는 필드에 대해 allocateCode 호출
if (Object.keys(fieldsWithNumbering).length > 0) {
console.log("🎯 [EditModal] 채번 규칙 할당 시작");
const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
let hasAllocationFailure = false;
const failedFields: string[] = [];
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
try {
console.log(`🔄 [EditModal] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`);
const allocateResult = await allocateNumberingCode(ruleId);
if (allocateResult.success && allocateResult.data?.generatedCode) {
const newCode = allocateResult.data.generatedCode;
console.log(`✅ [EditModal] ${fieldName} 새 코드 할당: ${dataToSave[fieldName]}${newCode}`);
dataToSave[fieldName] = newCode;
} else {
console.warn(`⚠️ [EditModal] ${fieldName} 코드 할당 실패:`, allocateResult.error);
if (!dataToSave[fieldName] || dataToSave[fieldName] === "") {
hasAllocationFailure = true;
failedFields.push(fieldName);
}
}
} catch (allocateError) {
console.error(`❌ [EditModal] ${fieldName} 코드 할당 오류:`, allocateError);
if (!dataToSave[fieldName] || dataToSave[fieldName] === "") {
hasAllocationFailure = true;
failedFields.push(fieldName);
}
}
}
// 채번 규칙 할당 실패 시 저장 중단
if (hasAllocationFailure) {
const fieldNames = failedFields.join(", ");
toast.error(`채번 규칙 할당에 실패했습니다 (${fieldNames}). 화면 설정에서 채번 규칙을 확인해주세요.`);
console.error(`❌ [EditModal] 채번 규칙 할당 실패로 저장 중단. 실패 필드: ${fieldNames}`);
return;
}
// _numberingRuleId 필드 제거 (실제 저장하지 않음)
for (const key of Object.keys(dataToSave)) {
if (key.endsWith("_numberingRuleId")) {
delete dataToSave[key];
}
}
}
console.log("[EditModal] 최종 저장 데이터:", dataToSave);
const response = await dynamicFormApi.saveFormData({
screenId: modalState.screenId!,
tableName: screenData.screenInfo.tableName,
data: formData,
data: dataToSave,
});
if (response.success) {

View File

@ -1,218 +1,23 @@
"use client";
/**
* PivotGrid
* PivotGrid를
*/
import React, { useState, useEffect, useMemo } from "react";
import { ComponentRegistry } from "../../ComponentRegistry";
import React from "react";
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { PivotGridComponent } from "./PivotGridComponent";
import { PivotGridConfigPanel } from "./PivotGridConfigPanel";
import {
PivotGridComponentConfig,
PivotFieldConfig,
PivotCellData,
} from "./types";
import { apiClient } from "@/lib/api/client";
// ==================== 타입 ====================
interface PivotGridRendererProps {
// 위젯 ID
id?: string;
// 컴포넌트 설정
config?: PivotGridComponentConfig;
// 외부 데이터 (formData 등에서 주입)
data?: Record<string, any>[];
// 화면 관리 컨텍스트
formData?: Record<string, any>;
// 이벤트 핸들러
onCellClick?: (cellData: PivotCellData) => void;
onDataLoad?: (data: Record<string, any>[]) => void;
// 제어관리 연동
buttonControlOptions?: {
buttonId?: string;
actionType?: string;
};
// 자동 필터 (멀티테넌시)
autoFilter?: {
companyCode?: string;
};
}
// ==================== 메인 컴포넌트 ====================
export const PivotGridRenderer: React.FC<PivotGridRendererProps> = ({
id,
config,
data: externalData,
formData,
onCellClick,
onDataLoad,
buttonControlOptions,
autoFilter,
}) => {
const [data, setData] = useState<Record<string, any>[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 데이터 로드
useEffect(() => {
const loadData = async () => {
// 외부 데이터가 있으면 사용
if (externalData && externalData.length > 0) {
setData(externalData);
onDataLoad?.(externalData);
return;
}
// 데이터 소스 설정 확인
if (!config?.dataSource?.tableName) {
setData([]);
return;
}
setLoading(true);
setError(null);
try {
// 테이블 데이터 조회
const params: any = {
tableName: config.dataSource.tableName,
};
// 멀티테넌시 필터 적용
if (autoFilter?.companyCode) {
params.companyCode = autoFilter.companyCode;
}
// 필터 조건 적용
if (config.dataSource.filterConditions) {
const filters: Record<string, any> = {};
config.dataSource.filterConditions.forEach((cond) => {
if (cond.valueFromField && formData) {
filters[cond.field] = formData[cond.valueFromField];
} else if (cond.value !== undefined) {
filters[cond.field] = cond.value;
}
});
params.filters = JSON.stringify(filters);
}
const response = await apiClient.get(
`/api/table-management/data/${config.dataSource.tableName}`,
{ params }
);
if (response.data.success) {
const loadedData = response.data.data || [];
setData(loadedData);
onDataLoad?.(loadedData);
} else {
throw new Error(response.data.message || "데이터 로드 실패");
}
} catch (err: any) {
console.error("PivotGrid 데이터 로드 실패:", err);
setError(err.message || "데이터를 불러오는데 실패했습니다");
setData([]);
} finally {
setLoading(false);
}
};
loadData();
}, [
config?.dataSource?.tableName,
config?.dataSource?.filterConditions,
externalData,
formData,
autoFilter?.companyCode,
onDataLoad,
]);
// 필드 설정에서 formData 값 적용
const processedFields = useMemo<PivotFieldConfig[]>(() => {
if (!config?.fields) return [];
return config.fields.map((field) => {
// 필터 값에 formData 적용
if (field.filterValues && formData) {
return {
...field,
filterValues: field.filterValues.map((v) => {
if (typeof v === "string" && v.startsWith("{{") && v.endsWith("}}")) {
const key = v.slice(2, -2).trim();
return formData[key] ?? v;
}
return v;
}),
};
}
return field;
});
}, [config?.fields, formData]);
// 로딩 상태
if (loading) {
return (
<div className="flex items-center justify-center p-8 text-muted-foreground">
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="text-sm"> ...</span>
</div>
</div>
);
}
// 에러 상태
if (error) {
return (
<div className="flex items-center justify-center p-8 text-destructive">
<div className="text-center">
<p className="text-sm font-medium"> </p>
<p className="text-xs mt-1">{error}</p>
</div>
</div>
);
}
return (
<PivotGridComponent
id={id}
title={config?.dataSource?.tableName}
data={data}
fields={processedFields}
totals={config?.totals}
style={config?.style}
fieldChooser={config?.fieldChooser}
chart={config?.chart}
allowSortingBySummary={config?.allowSortingBySummary}
allowFiltering={config?.allowFiltering}
allowExpandAll={config?.allowExpandAll}
wordWrapEnabled={config?.wordWrapEnabled}
height={config?.height}
maxHeight={config?.maxHeight}
exportConfig={config?.exportConfig}
onCellClick={onCellClick}
/>
);
};
// ==================== 컴포넌트 등록 ====================
ComponentRegistry.register({
type: "pivot-grid",
label: "피벗 그리드",
category: "data",
icon: "BarChart3",
/**
* PivotGrid
*/
const PivotGridDefinition = createComponentDefinition({
id: "pivot-grid",
name: "피벗 그리드",
nameEng: "PivotGrid Component",
description: "다차원 데이터 분석을 위한 피벗 테이블 컴포넌트",
category: ComponentCategory.DISPLAY,
webType: "text",
component: PivotGridComponent,
defaultConfig: {
dataSource: {
type: "table",
@ -239,8 +44,41 @@ ComponentRegistry.register({
},
height: "400px",
},
Renderer: PivotGridRenderer,
ConfigPanel: PivotGridConfigPanel,
defaultSize: { width: 800, height: 500 },
configPanel: PivotGridConfigPanel,
icon: "BarChart3",
tags: ["피벗", "분석", "집계", "그리드", "데이터"],
version: "1.0.0",
author: "개발팀",
documentation: "",
});
export default PivotGridRenderer;
/**
* PivotGrid
*
*/
export class PivotGridRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = PivotGridDefinition;
render(): React.ReactElement {
return (
<PivotGridComponent
{...this.props}
/>
);
}
}
// 자동 등록 실행
PivotGridRenderer.registerSelf();
// 강제 등록 (디버깅용)
if (typeof window !== "undefined") {
setTimeout(() => {
try {
PivotGridRenderer.registerSelf();
} catch (error) {
console.error("❌ PivotGrid 강제 등록 실패:", error);
}
}, 1000);
}

View File

@ -3,12 +3,7 @@
*
*/
// 메인 컴포넌트
export { PivotGridComponent } from "./PivotGridComponent";
export { PivotGridRenderer } from "./PivotGridRenderer";
export { PivotGridConfigPanel } from "./PivotGridConfigPanel";
// 타입
// 타입 내보내기
export type {
// 기본 타입
PivotAreaType,
@ -45,6 +40,10 @@ export type {
PivotGridComponentConfig,
} from "./types";
// 컴포넌트 내보내기
export { PivotGridComponent } from "./PivotGridComponent";
export { PivotGridConfigPanel } from "./PivotGridConfigPanel";
// 유틸리티
export {
aggregate,
@ -60,4 +59,3 @@ export {
} from "./utils/aggregation";
export { processPivotData, pathToKey, keyToPath } from "./utils/pivotEngine";

View File

@ -104,6 +104,20 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
const currentFormValue = formData?.[component.columnName];
const currentComponentValue = component.value;
// 🆕 채번 규칙이 설정되어 있으면 항상 _numberingRuleId를 formData에 설정
// (값 생성 성공 여부와 관계없이, 저장 시점에 allocateCode를 호출하기 위함)
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
const ruleId = testAutoGeneration.options.numberingRuleId;
if (ruleId && ruleId !== "undefined" && ruleId !== "null" && ruleId !== "") {
const ruleIdKey = `${component.columnName}_numberingRuleId`;
// formData에 아직 설정되지 않은 경우에만 설정
if (isInteractive && onFormDataChange && !formData?.[ruleIdKey]) {
onFormDataChange(ruleIdKey, ruleId);
console.log("📝 채번 규칙 ID 사전 설정:", ruleIdKey, ruleId);
}
}
}
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
isGeneratingRef.current = true; // 생성 시작 플래그
@ -144,13 +158,6 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
if (isInteractive && onFormDataChange && component.columnName) {
console.log("📝 formData 업데이트:", component.columnName, generatedValue);
onFormDataChange(component.columnName, generatedValue);
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
const ruleIdKey = `${component.columnName}_numberingRuleId`;
onFormDataChange(ruleIdKey, testAutoGeneration.options.numberingRuleId);
console.log("📝 채번 규칙 ID 저장:", ruleIdKey, testAutoGeneration.options.numberingRuleId);
}
}
}
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {

View File

@ -801,6 +801,9 @@ export class ButtonActionExecutor {
console.log("🎯 채번 규칙 할당 시작 (allocateCode 호출)");
const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
let hasAllocationFailure = false;
const failedFields: string[] = [];
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
try {
console.log(`🔄 ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`);
@ -811,13 +814,31 @@ export class ButtonActionExecutor {
console.log(`${fieldName} 새 코드 할당: ${formData[fieldName]}${newCode}`);
formData[fieldName] = newCode;
} else {
console.warn(`⚠️ ${fieldName} 코드 할당 실패, 기존 값 유지:`, allocateResult.error);
console.warn(`⚠️ ${fieldName} 코드 할당 실패:`, allocateResult.error);
// 🆕 기존 값이 빈 문자열이면 실패로 표시
if (!formData[fieldName] || formData[fieldName] === "") {
hasAllocationFailure = true;
failedFields.push(fieldName);
}
}
} catch (allocateError) {
console.error(`${fieldName} 코드 할당 오류:`, allocateError);
// 오류 시 기존 값 유지
// 🆕 기존 값이 빈 문자열이면 실패로 표시
if (!formData[fieldName] || formData[fieldName] === "") {
hasAllocationFailure = true;
failedFields.push(fieldName);
}
}
}
// 🆕 채번 규칙 할당 실패 시 저장 중단
if (hasAllocationFailure) {
const fieldNames = failedFields.join(", ");
toast.error(`채번 규칙 할당에 실패했습니다 (${fieldNames}). 화면 설정에서 채번 규칙을 확인해주세요.`);
console.error(`❌ 채번 규칙 할당 실패로 저장 중단. 실패 필드: ${fieldNames}`);
console.error("💡 해결 방법: 화면관리에서 해당 필드의 채번 규칙 설정을 확인하세요.");
return false;
}
}
console.log("✅ 채번 규칙 할당 완료");
@ -3039,6 +3060,7 @@ export class ButtonActionExecutor {
config: ButtonActionConfig,
rowData: any,
context: ButtonActionContext,
isCreateMode: boolean = false, // 🆕 복사 모드에서 true로 전달
): Promise<void> {
const { groupByColumns = [] } = config;
@ -3112,10 +3134,11 @@ export class ButtonActionExecutor {
const modalEvent = new CustomEvent("openEditModal", {
detail: {
screenId: config.targetScreenId,
title: config.editModalTitle || "데이터 수정",
title: isCreateMode ? (config.editModalTitle || "데이터 복사") : (config.editModalTitle || "데이터 수정"),
description: description,
modalSize: config.modalSize || "lg",
editData: rowData,
isCreateMode: isCreateMode, // 🆕 복사 모드에서 INSERT로 처리되도록
groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달
tableName: context.tableName, // 🆕 테이블명 전달
buttonConfig: config, // 🆕 버튼 설정 전달 (제어로직 실행용)
@ -3230,23 +3253,61 @@ export class ButtonActionExecutor {
"code",
];
// 🆕 화면 설정에서 채번 규칙 가져오기
let screenNumberingRules: Record<string, string> = {};
if (config.targetScreenId) {
try {
const { screenApi } = await import("@/lib/api/screen");
const layout = await screenApi.getLayout(config.targetScreenId);
// 레이아웃에서 채번 규칙이 설정된 컴포넌트 찾기
const findNumberingRules = (components: any[]): void => {
for (const comp of components) {
const compConfig = comp.componentConfig || {};
// text-input 컴포넌트의 채번 규칙 확인
if (compConfig.autoGeneration?.type === "numbering_rule" && compConfig.autoGeneration?.options?.numberingRuleId) {
const columnName = compConfig.columnName || comp.columnName;
if (columnName) {
screenNumberingRules[columnName] = compConfig.autoGeneration.options.numberingRuleId;
console.log(`📋 화면 설정에서 채번 규칙 발견: ${columnName}${compConfig.autoGeneration.options.numberingRuleId}`);
}
}
// 중첩된 컴포넌트 확인
if (comp.children && Array.isArray(comp.children)) {
findNumberingRules(comp.children);
}
}
};
if (layout?.components) {
findNumberingRules(layout.components);
}
console.log("📋 화면 설정에서 찾은 채번 규칙:", screenNumberingRules);
} catch (error) {
console.warn("⚠️ 화면 레이아웃 조회 실패:", error);
}
}
// 품목코드 필드를 찾아서 무조건 공백으로 초기화
let resetFieldName = "";
for (const field of itemCodeFields) {
if (copiedData[field] !== undefined) {
const originalValue = copiedData[field];
const ruleIdKey = `${field}_numberingRuleId`;
const hasNumberingRule =
rowData[ruleIdKey] !== undefined && rowData[ruleIdKey] !== null && rowData[ruleIdKey] !== "";
// 1순위: 원본 데이터에서 채번 규칙 ID 확인
// 2순위: 화면 설정에서 채번 규칙 ID 확인
const numberingRuleId = rowData[ruleIdKey] || screenNumberingRules[field];
const hasNumberingRule = numberingRuleId !== undefined && numberingRuleId !== null && numberingRuleId !== "";
// 품목코드를 무조건 공백으로 초기화
copiedData[field] = "";
// 채번 규칙 ID가 있으면 복사 (저장 시 자동 생성)
if (hasNumberingRule) {
copiedData[ruleIdKey] = rowData[ruleIdKey];
copiedData[ruleIdKey] = numberingRuleId;
console.log(`✅ 품목코드 초기화 (채번 규칙 있음): ${field} (기존값: ${originalValue})`);
console.log(`📋 채번 규칙 ID 복사: ${ruleIdKey} = ${rowData[ruleIdKey]}`);
console.log(`📋 채번 규칙 ID 설정: ${ruleIdKey} = ${numberingRuleId}`);
} else {
console.log(`✅ 품목코드 초기화 (수동 입력 필요): ${field} (기존값: ${originalValue})`);
}
@ -3303,9 +3364,9 @@ export class ButtonActionExecutor {
switch (editMode) {
case "modal":
// 모달로 복사 폼 열기 (편집 모달 재사용)
console.log("📋 모달로 복사 폼 열기");
await this.openEditModal(config, rowData, context);
// 모달로 복사 폼 열기 (편집 모달 재사용, INSERT 모드로)
console.log("📋 모달로 복사 폼 열기 (INSERT 모드)");
await this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true
break;
case "navigate":
@ -3316,8 +3377,8 @@ export class ButtonActionExecutor {
default:
// 기본값: 모달
console.log("📋 기본 모달로 복사 폼 열기");
this.openEditModal(config, rowData, context);
console.log("📋 기본 모달로 복사 폼 열기 (INSERT 모드)");
this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true
}
} catch (error: any) {
console.error("❌ openCopyForm 실행 중 오류:", error);