fix(numbering-rule): 채번 미리보기 순번 수정 및 저장 시 재할당 로직 추가

- 미리보기 시 currentSequence + 1로 다음 순번 표시
- UniversalFormModal에서 미리보기/실제할당 분리
- _needsAllocation 플래그로 저장 시 재할당 여부 판단
- RepeatScreenModal 외부 데이터 소스 조인/필터 설정 UI 추가
This commit is contained in:
SeongHyun Kim 2025-12-11 18:26:33 +09:00
parent 6a676dcf5c
commit 038c5a0973
3 changed files with 256 additions and 28 deletions

View File

@ -898,9 +898,10 @@ class NumberingRuleService {
switch (part.partType) {
case "sequence": {
// 순번 (현재 순번으로 미리보기, 증가 안 함)
// 순번 (다음 할당될 순번으로 미리보기, 실제 증가는 allocate 시)
const length = autoConfig.sequenceLength || 3;
return String(rule.currentSequence || 1).padStart(length, "0");
const nextSequence = (rule.currentSequence || 0) + 1;
return String(nextSequence).padStart(length, "0");
}
case "number": {

View File

@ -2517,7 +2517,7 @@ function LayoutRowConfigModal({
</div>
{/* 외부 데이터 소스 설정 */}
<div className="border rounded p-3 bg-blue-50 space-y-2">
<div className="border rounded p-3 bg-blue-50 space-y-3">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold"> </Label>
<Switch
@ -2529,28 +2529,234 @@ function LayoutRowConfigModal({
/>
</div>
{row.tableDataSource?.enabled && (
<div className="grid grid-cols-2 gap-2">
<>
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Select
<TableSelector
value={row.tableDataSource?.sourceTable || ""}
onValueChange={(value) => onUpdateRow({
onChange={(value) => onUpdateRow({
tableDataSource: { ...row.tableDataSource!, sourceTable: value }
})}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{allTables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.displayName || table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
/>
</div>
</div>
{/* 조인 조건 설정 */}
<div className="space-y-2 pt-2 border-t border-blue-200">
<div className="space-y-1">
<Label className="text-[10px] font-semibold"> </Label>
<p className="text-[9px] text-muted-foreground">
</p>
</div>
{(row.tableDataSource?.joinConditions || []).map((condition, conditionIndex) => (
<div key={`join-${conditionIndex}`} className="space-y-2 p-2 border rounded bg-white">
<div className="flex items-center justify-between">
<span className="text-[9px] font-medium"> {conditionIndex + 1}</span>
<Button
size="sm"
variant="ghost"
onClick={() => {
const newConditions = [...(row.tableDataSource?.joinConditions || [])];
newConditions.splice(conditionIndex, 1);
onUpdateRow({
tableDataSource: { ...row.tableDataSource!, joinConditions: newConditions }
});
}}
className="h-5 w-5 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[9px]"> ()</Label>
<SourceColumnSelector
sourceTable={row.tableDataSource?.sourceTable || ""}
value={condition.sourceKey}
onChange={(value) => {
const newConditions = [...(row.tableDataSource?.joinConditions || [])];
newConditions[conditionIndex] = { ...condition, sourceKey: value };
onUpdateRow({
tableDataSource: { ...row.tableDataSource!, joinConditions: newConditions }
});
}}
placeholder="예: sales_order_id"
/>
<p className="text-[8px] text-muted-foreground">
</p>
</div>
<div className="space-y-1">
<Label className="text-[9px]"> ()</Label>
<SourceColumnSelector
sourceTable={dataSourceTable || ""}
value={condition.referenceKey}
onChange={(value) => {
const newConditions = [...(row.tableDataSource?.joinConditions || [])];
newConditions[conditionIndex] = { ...condition, referenceKey: value };
onUpdateRow({
tableDataSource: { ...row.tableDataSource!, joinConditions: newConditions }
});
}}
placeholder="예: id"
/>
<p className="text-[8px] text-muted-foreground">
</p>
</div>
</div>
<div className="mt-1 p-1.5 bg-blue-50 rounded border border-blue-100">
<p className="text-[8px] text-blue-700 font-mono">
{row.tableDataSource?.sourceTable}.{condition.sourceKey} = {dataSourceTable}.{condition.referenceKey}
</p>
<p className="text-[8px] text-muted-foreground mt-0.5">
</p>
</div>
</div>
))}
<Button
size="sm"
variant="outline"
onClick={() => {
const newConditions = [
...(row.tableDataSource?.joinConditions || []),
{ sourceKey: "", referenceKey: "", referenceType: "card" as const },
];
onUpdateRow({
tableDataSource: { ...row.tableDataSource!, joinConditions: newConditions }
});
}}
className="w-full h-7 text-[9px]"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{/* 필터 설정 */}
<div className="space-y-2 pt-2 border-t border-blue-200">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-[10px] font-semibold"> </Label>
<p className="text-[9px] text-muted-foreground">
</p>
</div>
<Switch
checked={row.tableDataSource?.filterConfig?.enabled || false}
onCheckedChange={(checked) => {
onUpdateRow({
tableDataSource: {
...row.tableDataSource!,
filterConfig: {
enabled: checked,
filterField: "",
filterType: "notEquals",
referenceField: "",
referenceSource: "representativeData",
},
},
});
}}
className="scale-75"
/>
</div>
{row.tableDataSource?.filterConfig?.enabled && (
<div className="space-y-2 p-2 bg-amber-50 rounded border border-amber-200">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[9px]"> </Label>
<SourceColumnSelector
sourceTable={row.tableDataSource?.sourceTable || ""}
value={row.tableDataSource?.filterConfig?.filterField || ""}
onChange={(value) => {
onUpdateRow({
tableDataSource: {
...row.tableDataSource!,
filterConfig: {
...row.tableDataSource!.filterConfig!,
filterField: value,
},
},
});
}}
placeholder="예: order_no"
/>
<p className="text-[8px] text-muted-foreground">
</p>
</div>
<div className="space-y-1">
<Label className="text-[9px]"> </Label>
<SourceColumnSelector
sourceTable={dataSourceTable || ""}
value={row.tableDataSource?.filterConfig?.referenceField || ""}
onChange={(value) => {
onUpdateRow({
tableDataSource: {
...row.tableDataSource!,
filterConfig: {
...row.tableDataSource!.filterConfig!,
referenceField: value,
},
},
});
}}
placeholder="예: order_no"
/>
<p className="text-[8px] text-muted-foreground">
</p>
</div>
</div>
<div className="space-y-1">
<Label className="text-[9px]"> </Label>
<Select
value={row.tableDataSource?.filterConfig?.filterType || "notEquals"}
onValueChange={(value: "equals" | "notEquals") => {
onUpdateRow({
tableDataSource: {
...row.tableDataSource!,
filterConfig: {
...row.tableDataSource!.filterConfig!,
filterType: value,
},
},
});
}}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="notEquals"> ( )</SelectItem>
<SelectItem value="equals"> ()</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-2 p-1.5 bg-amber-100 rounded border border-amber-200">
<p className="text-[8px] text-amber-800 font-mono">
{row.tableDataSource?.sourceTable}.{row.tableDataSource?.filterConfig?.filterField} != .{row.tableDataSource?.filterConfig?.referenceField}
</p>
<p className="text-[8px] text-muted-foreground mt-0.5">
{row.tableDataSource?.filterConfig?.filterType === "notEquals"
? "현재 선택한 행과 다른 데이터만 표시합니다"
: "현재 선택한 행과 같은 데이터만 표시합니다"}
</p>
</div>
</div>
)}
</div>
</>
)}
</div>

View File

@ -386,10 +386,14 @@ export function UniversalFormModalComponent({
!updatedData[field.columnName]
) {
try {
// generateOnOpen: 미리보기만 표시 (실제 순번 할당은 저장 시)
const response = await generateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
// 임시 플래그 추가하여 저장 시 실제 순번으로 교체할 것을 표시
updatedData[field.columnName] = response.data.generatedCode;
updatedData[`_${field.columnName}_needsAllocation`] = true; // 저장 시 재할당 필요
hasChanges = true;
console.log(`[채번 미리보기] ${field.columnName} = ${response.data.generatedCode} (저장 시 실제 순번으로 교체)`);
}
} catch (error) {
console.error(`채번규칙 생성 실패 (${field.columnName}):`, error);
@ -633,18 +637,35 @@ export function UniversalFormModalComponent({
for (const section of config.sections) {
for (const field of section.fields) {
if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
// 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인 경우, 미리보기 값이 있더라도 실제 순번 할당
console.log(`[채번 체크] ${field.columnName}:`, {
enabled: field.numberingRule.enabled,
ruleId: field.numberingRule.ruleId,
generateOnSave: field.numberingRule.generateOnSave,
generateOnOpen: field.numberingRule.generateOnOpen,
currentValue: dataToSave[field.columnName],
needsAllocation: dataToSave[`_${field.columnName}_needsAllocation`]
});
// generateOnSave: 항상 새로 생성
// generateOnOpen: 미리보기 값이 있으면 실제 순번으로 교체
const shouldAllocate =
field.numberingRule.generateOnSave ||
(field.numberingRule.generateOnOpen && dataToSave[`_${field.columnName}_needsAllocation`]);
console.log(`[채번] shouldAllocate = ${shouldAllocate}`);
if (shouldAllocate) {
console.log(`[채번] allocateNumberingCode 호출 시작: ${field.columnName}, ruleId: ${field.numberingRule.ruleId}`);
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
const oldValue = dataToSave[field.columnName];
dataToSave[field.columnName] = response.data.generatedCode;
console.log(`[채번 성공] ${field.columnName}: ${oldValue}${response.data.generatedCode}`);
// 임시 플래그 제거
delete dataToSave[`_${field.columnName}_needsAllocation`];
} else {
console.error(`[채번 실패] ${field.columnName}:`, response.error);
}
}
}