fix(numbering-rule): 채번규칙 저장 시 allocateNumberingCode로 실제 순번 할당

- generateNumberingCode를 allocateNumberingCode로 변경 (순번 실제 증가)

- saveSingleRow/saveMultipleRows/saveWithMultiTable 모두 적용

- NumberingRuleCard: 파트 타입 변경 시 defaultAutoConfig 적용

- NumberingRuleDesigner: 저장 시 partsWithDefaults로 기본값 병합

- sequenceLength/numberLength 기본값 4에서 3으로 변경

- 불필요한 console.log 제거
This commit is contained in:
SeongHyun Kim 2025-12-08 19:10:07 +09:00
parent b15b6e21ea
commit d908de7f66
6 changed files with 101 additions and 90 deletions

View File

@ -2010,8 +2010,6 @@ export async function multiTableSave(
mainSubItem.company_code = companyCode;
}
logger.info(`서브 테이블 ${tableName} 메인 데이터 저장 준비:`, JSON.stringify(mainSubItem));
// 먼저 기존 데이터 존재 여부 확인 (user_id + is_primary 조합)
const checkQuery = `
SELECT * FROM "${tableName}"
@ -2027,9 +2025,6 @@ export async function multiTableSave(
if (companyCode !== "*") {
checkParams.push(companyCode);
}
logger.info(`서브 테이블 ${tableName} 기존 데이터 확인 - 쿼리: ${checkQuery}`);
logger.info(`서브 테이블 ${tableName} 기존 데이터 확인 - 파라미터: ${JSON.stringify(checkParams)}`);
const existingResult = await client.query(checkQuery, checkParams);
@ -2061,13 +2056,9 @@ export async function multiTableSave(
updateParams.push(companyCode);
}
logger.info(`서브 테이블 ${tableName} 메인 데이터 UPDATE - 쿼리: ${updateQuery}`);
logger.info(`서브 테이블 ${tableName} 메인 데이터 UPDATE - 값: ${JSON.stringify(updateParams)}`);
const updateResult = await client.query(updateQuery, updateParams);
subTableResults.push({ tableName, type: "main", data: updateResult.rows[0] });
} else {
logger.info(`서브 테이블 ${tableName} 메인 데이터 - 업데이트할 컬럼 없음, 기존 데이터 유지`);
subTableResults.push({ tableName, type: "main", data: existingResult.rows[0] });
}
} else {
@ -2082,9 +2073,6 @@ export async function multiTableSave(
RETURNING *
`;
logger.info(`서브 테이블 ${tableName} 메인 데이터 INSERT - 쿼리: ${insertQuery}`);
logger.info(`서브 테이블 ${tableName} 메인 데이터 INSERT - 값: ${JSON.stringify(mainSubValues)}`);
const insertResult = await client.query(insertQuery, mainSubValues);
subTableResults.push({ tableName, type: "main", data: insertResult.rows[0] });
}

View File

@ -897,13 +897,13 @@ class NumberingRuleService {
switch (part.partType) {
case "sequence": {
// 순번 (현재 순번으로 미리보기, 증가 안 함)
const length = autoConfig.sequenceLength || 4;
const length = autoConfig.sequenceLength || 3;
return String(rule.currentSequence || 1).padStart(length, "0");
}
case "number": {
// 숫자 (고정 자릿수)
const length = autoConfig.numberLength || 4;
const length = autoConfig.numberLength || 3;
const value = autoConfig.numberValue || 1;
return String(value).padStart(length, "0");
}
@ -957,13 +957,13 @@ class NumberingRuleService {
switch (part.partType) {
case "sequence": {
// 순번 (자동 증가 숫자)
const length = autoConfig.sequenceLength || 4;
const length = autoConfig.sequenceLength || 3;
return String(rule.currentSequence || 1).padStart(length, "0");
}
case "number": {
// 숫자 (고정 자릿수)
const length = autoConfig.numberLength || 4;
const length = autoConfig.numberLength || 3;
const value = autoConfig.numberValue || 1;
return String(value).padStart(length, "0");
}

View File

@ -48,7 +48,20 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Select
value={part.partType}
onValueChange={(value) => onUpdate({ partType: value as CodePartType })}
onValueChange={(value) => {
const newPartType = value as CodePartType;
// 타입 변경 시 해당 타입의 기본 autoConfig 설정
const defaultAutoConfig: Record<string, any> = {
sequence: { sequenceLength: 3, startFrom: 1 },
number: { numberLength: 4, numberValue: 1 },
date: { dateFormat: "YYYYMMDD" },
text: { textValue: "CODE" },
};
onUpdate({
partType: newPartType,
autoConfig: defaultAutoConfig[newPartType] || {}
});
}}
disabled={isPreview}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">

View File

@ -196,10 +196,31 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
try {
const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId);
// 파트별 기본 autoConfig 정의
const defaultAutoConfigs: Record<string, any> = {
sequence: { sequenceLength: 3, startFrom: 1 },
number: { numberLength: 4, numberValue: 1 },
date: { dateFormat: "YYYYMMDD" },
text: { textValue: "" },
};
// 저장 전에 각 파트의 autoConfig에 기본값 채우기
const partsWithDefaults = currentRule.parts.map((part) => {
if (part.generationMethod === "auto") {
const defaults = defaultAutoConfigs[part.partType] || {};
return {
...part,
autoConfig: { ...defaults, ...part.autoConfig },
};
}
return part;
});
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
// 메뉴 기반으로 채번규칙 관리 (menuObjid로 필터링)
const ruleToSave = {
...currentRule,
parts: partsWithDefaults,
scopeType: "menu" as const, // 메뉴 기반 채번규칙
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용)
menuObjid: menuObjid || currentRule.menuObjid || null, // 메뉴 OBJID (필터링 기준)

View File

@ -23,7 +23,7 @@ import { ChevronDown, ChevronUp, Plus, Trash2, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { generateNumberingCode } from "@/lib/api/numberingRule";
import { generateNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import {
UniversalFormModalComponentProps,
@ -123,14 +123,12 @@ export function UniversalFormModalComponent({
useEffect(() => {
// 이미 초기화되었으면 스킵
if (hasInitialized.current) {
console.log("[UniversalFormModal] 이미 초기화됨, 스킵");
return;
}
// 최초 initialData 캡처 (이후 변경되어도 이 값 사용)
if (initialData && Object.keys(initialData).length > 0) {
capturedInitialData.current = JSON.parse(JSON.stringify(initialData)); // 깊은 복사
console.log("[UniversalFormModal] initialData 캡처:", capturedInitialData.current);
}
hasInitialized.current = true;
@ -142,7 +140,6 @@ export function UniversalFormModalComponent({
useEffect(() => {
if (!hasInitialized.current) return; // 최초 초기화 전이면 스킵
console.log("[UniversalFormModal] config 변경 감지, 재초기화");
initializeForm();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
@ -164,7 +161,6 @@ export function UniversalFormModalComponent({
// 각 테이블 데이터 로드
for (const tableName of tablesToLoad) {
if (!linkedFieldDataCache[tableName]) {
console.log(`[UniversalFormModal] linkedFieldGroup 데이터 로드: ${tableName}`);
await loadLinkedFieldData(tableName);
}
}
@ -178,7 +174,6 @@ export function UniversalFormModalComponent({
const initializeForm = useCallback(async () => {
// 캡처된 initialData 사용 (props로 전달된 initialData가 아닌)
const effectiveInitialData = capturedInitialData.current || initialData;
console.log("[UniversalFormModal] 폼 초기화 시작, effectiveInitialData:", effectiveInitialData);
const newFormData: FormDataState = {};
const newRepeatSections: { [sectionId: string]: RepeatSectionItem[] } = {};
@ -212,7 +207,6 @@ export function UniversalFormModalComponent({
// receiveFromParent가 true이거나, effectiveInitialData에 값이 있으면 적용
if (field.receiveFromParent || value === "" || value === undefined) {
value = effectiveInitialData[parentField];
console.log(`[UniversalFormModal] 필드 ${field.columnName}: initialData에서 값 적용 = ${value}`);
}
}
}
@ -506,18 +500,26 @@ export function UniversalFormModalComponent({
}
});
// 저장 시점 채번규칙 처리
// 저장 시점 채번규칙 처리 (allocateNumberingCode로 실제 순번 증가)
for (const section of config.sections) {
for (const field of section.fields) {
if (
field.numberingRule?.enabled &&
field.numberingRule?.generateOnSave &&
field.numberingRule?.ruleId &&
!dataToSave[field.columnName]
field.numberingRule?.ruleId
) {
const response = await generateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
dataToSave[field.columnName] = response.data.generatedCode;
// generateOnSave: 저장 시 새로 생성
// generateOnOpen: 열 때 미리보기로 표시했지만, 저장 시 실제 순번 할당 필요
if (field.numberingRule.generateOnSave && !dataToSave[field.columnName]) {
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
dataToSave[field.columnName] = response.data.generatedCode;
}
} else if (field.numberingRule.generateOnOpen && dataToSave[field.columnName]) {
// generateOnOpen인 경우, 미리보기 값이 있더라도 실제 순번 할당
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
dataToSave[field.columnName] = response.data.generatedCode;
}
}
}
}
@ -542,7 +544,6 @@ export function UniversalFormModalComponent({
if (commonFields.length === 0) {
const nonRepeatableSections = config.sections.filter((s) => !s.repeatable);
commonFields = nonRepeatableSections.flatMap((s) => s.fields.map((f) => f.columnName));
console.log("[UniversalFormModal] 공통 필드 자동 설정:", commonFields);
}
// 반복 섹션 ID가 설정되지 않은 경우, 첫 번째 반복 섹션 사용
@ -550,22 +551,9 @@ export function UniversalFormModalComponent({
const repeatableSection = config.sections.find((s) => s.repeatable);
if (repeatableSection) {
repeatSectionId = repeatableSection.id;
console.log("[UniversalFormModal] 반복 섹션 자동 설정:", repeatSectionId);
}
}
// 디버깅: 설정 확인
console.log("[UniversalFormModal] 다중 행 저장 설정:", {
commonFields,
repeatSectionId,
mainSectionFields,
typeColumn,
mainTypeValue,
subTypeValue,
repeatSections,
formData,
});
// 반복 섹션 데이터
const repeatItems = repeatSections[repeatSectionId] || [];
@ -588,10 +576,6 @@ export function UniversalFormModalComponent({
}
});
console.log("[UniversalFormModal] 공통 데이터:", commonData);
console.log("[UniversalFormModal] 메인 섹션 데이터:", mainSectionData);
console.log("[UniversalFormModal] 반복 항목:", repeatItems);
// 메인 행 (공통 데이터 + 메인 섹션 필드)
const mainRow: any = { ...commonData, ...mainSectionData };
if (typeColumn) {
@ -623,16 +607,20 @@ export function UniversalFormModalComponent({
if (section.repeatable) continue;
for (const field of section.fields) {
if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) {
const response = await generateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
// 모든 행에 동일한 채번 값 적용 (공통 필드인 경우)
if (commonFields.includes(field.columnName)) {
rowsToSave.forEach((row) => {
row[field.columnName] = response.data?.generatedCode;
});
} else {
rowsToSave[0][field.columnName] = response.data?.generatedCode;
if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
// generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당
const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen;
if (shouldAllocate) {
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
// 모든 행에 동일한 채번 값 적용 (공통 필드인 경우)
if (commonFields.includes(field.columnName)) {
rowsToSave.forEach((row) => {
row[field.columnName] = response.data?.generatedCode;
});
} else {
rowsToSave[0][field.columnName] = response.data?.generatedCode;
}
}
}
}
@ -640,16 +628,11 @@ export function UniversalFormModalComponent({
}
// 모든 행 저장
console.log("[UniversalFormModal] 저장할 행들:", rowsToSave);
console.log("[UniversalFormModal] 저장 테이블:", config.saveConfig.tableName);
for (let i = 0; i < rowsToSave.length; i++) {
const row = rowsToSave[i];
console.log(`[UniversalFormModal] ${i + 1}번째 행 저장 시도:`, row);
// 빈 객체 체크
if (Object.keys(row).length === 0) {
console.warn(`[UniversalFormModal] ${i + 1}번째 행이 비어있습니다. 건너뜁니다.`);
continue;
}
@ -659,8 +642,6 @@ export function UniversalFormModalComponent({
throw new Error(response.data?.message || `${i + 1}번째 행 저장 실패`);
}
}
console.log(`[UniversalFormModal] ${rowsToSave.length}개 행 저장 완료`);
}, [config.sections, config.saveConfig, formData, repeatSections]);
// 다중 테이블 저장 (범용)
@ -669,9 +650,6 @@ export function UniversalFormModalComponent({
if (!customApiSave?.multiTable) return;
const { multiTable } = customApiSave;
console.log("[UniversalFormModal] 다중 테이블 저장 시작:", multiTable);
console.log("[UniversalFormModal] 현재 formData:", formData);
console.log("[UniversalFormModal] 현재 repeatSections:", repeatSections);
// 1. 메인 테이블 데이터 구성
const mainData: Record<string, any> = {};
@ -685,6 +663,35 @@ export function UniversalFormModalComponent({
});
});
// 1-1. 채번규칙 처리 (저장 시점에 실제 순번 할당)
for (const section of config.sections) {
if (section.repeatable) continue;
for (const field of section.fields) {
// 채번규칙이 활성화된 필드 처리
if (
field.numberingRule?.enabled &&
field.numberingRule?.ruleId
) {
// 신규 생성이거나 값이 없는 경우에만 채번
const isNewRecord = !initialData?.[multiTable.mainTable.primaryKeyColumn];
const hasNoValue = !mainData[field.columnName];
if (isNewRecord || hasNoValue) {
try {
// allocateNumberingCode로 실제 순번 증가
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
mainData[field.columnName] = response.data.generatedCode;
}
} catch (error) {
console.error(`채번규칙 할당 실패 (${field.columnName}):`, error);
}
}
}
}
}
// 2. 서브 테이블 데이터 구성
const subTablesData: Array<{
tableName: string;
@ -770,8 +777,6 @@ export function UniversalFormModalComponent({
mainFieldMappings = mainFieldMappings.filter((m, idx, arr) =>
arr.findIndex(x => x.targetColumn === m.targetColumn) === idx
);
console.log("[UniversalFormModal] 메인 필드 매핑 생성:", mainFieldMappings);
}
subTablesData.push({
@ -786,12 +791,6 @@ export function UniversalFormModalComponent({
}
// 3. 범용 다중 테이블 저장 API 호출
console.log("[UniversalFormModal] 다중 테이블 저장 데이터:", {
mainTable: multiTable.mainTable,
mainData,
subTablesData,
});
const response = await apiClient.post("/table-management/multi-table-save", {
mainTable: multiTable.mainTable,
mainData,
@ -802,8 +801,6 @@ export function UniversalFormModalComponent({
if (!response.data?.success) {
throw new Error(response.data?.message || "다중 테이블 저장 실패");
}
console.log("[UniversalFormModal] 다중 테이블 저장 완료:", response.data);
}, [config.sections, config.saveConfig, formData, repeatSections, initialData]);
// 커스텀 API 저장
@ -811,8 +808,6 @@ export function UniversalFormModalComponent({
const { customApiSave } = config.saveConfig;
if (!customApiSave) return;
console.log("[UniversalFormModal] 커스텀 API 저장 시작:", customApiSave.apiType);
const saveWithGenericCustomApi = async () => {
if (!customApiSave.customEndpoint) {
throw new Error("커스텀 API 엔드포인트가 설정되지 않았습니다.");
@ -856,12 +851,6 @@ export function UniversalFormModalComponent({
// 저장 처리
const handleSave = useCallback(async () => {
console.log("[UniversalFormModal] 저장 시작, saveConfig:", {
tableName: config.saveConfig.tableName,
customApiSave: config.saveConfig.customApiSave,
multiRowSave: config.saveConfig.multiRowSave,
});
// 커스텀 API 저장 모드가 아닌 경우에만 테이블명 체크
if (!config.saveConfig.customApiSave?.enabled && !config.saveConfig.tableName) {
toast.error("저장할 테이블이 설정되지 않았습니다.");

View File

@ -728,7 +728,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
<div className="flex flex-col">
<span>{table.label || table.name}</span>
{table.label && <span className="text-[9px] text-muted-foreground">{table.name}</span>}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
@ -743,7 +743,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
<Select
value={config.saveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn || "_none_"}
onValueChange={(value) =>
updateSaveConfig({
updateSaveConfig({
customApiSave: {
...config.saveConfig.customApiSave,
multiTable: {
@ -893,7 +893,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
<div className="flex flex-col">
<span>{table.label || table.name}</span>
{table.label && <span className="text-[8px] text-muted-foreground">{table.name}</span>}
</div>
</div>
</CommandItem>
))}
</CommandGroup>