코드 할당 요청 시 폼 데이터 추가: numberingRuleController에서 코드 할당 요청 시 폼 데이터를 포함하도록 수정하였습니다. 이를 통해 날짜 컬럼 기준 생성 시 필요한 정보를 전달할 수 있도록 개선하였습니다.

This commit is contained in:
kjs 2026-01-19 18:21:30 +09:00
parent 95da69ec70
commit d3701cfe1e
27 changed files with 1148 additions and 295 deletions

View File

@ -216,11 +216,12 @@ router.post("/:ruleId/preview", authenticateToken, async (req: AuthenticatedRequ
router.post("/:ruleId/allocate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
const { formData } = req.body; // 폼 데이터 (날짜 컬럼 기준 생성 시 사용)
logger.info("코드 할당 요청", { ruleId, companyCode });
logger.info("코드 할당 요청", { ruleId, companyCode, hasFormData: !!formData });
try {
const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode);
const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode, formData);
logger.info("코드 할당 성공", { ruleId, allocatedCode });
return res.json({ success: true, data: { generatedCode: allocatedCode } });
} catch (error: any) {

View File

@ -58,3 +58,4 @@ export default router;

View File

@ -54,3 +54,4 @@ export default router;

View File

@ -70,3 +70,4 @@ export default router;

View File

@ -58,3 +58,4 @@ export default router;

View File

@ -883,16 +883,21 @@ class MasterDetailExcelService {
/**
* ( numberingRuleService )
* @param client DB
* @param ruleId ID
* @param companyCode
* @param formData ( )
*/
private async generateNumberWithRule(
client: any,
ruleId: string,
companyCode: string
companyCode: string,
formData?: Record<string, any>
): Promise<string> {
try {
// 기존 numberingRuleService를 사용하여 코드 할당
const { numberingRuleService } = await import("./numberingRuleService");
const generatedCode = await numberingRuleService.allocateCode(ruleId, companyCode);
const generatedCode = await numberingRuleService.allocateCode(ruleId, companyCode, formData);
logger.info(`채번 생성 (numberingRuleService): rule=${ruleId}, result=${generatedCode}`);

View File

@ -984,9 +984,11 @@ export class NodeFlowExecutionService {
// 자동 생성 (채번 규칙)
const companyCode = context.buttonContext?.companyCode || "*";
try {
// 폼 데이터를 전달하여 날짜 컬럼 기준 생성 지원
value = await numberingRuleService.allocateCode(
mapping.numberingRuleId,
companyCode
companyCode,
data // 폼 데이터 전달 (날짜 컬럼 기준 생성 시 사용)
);
console.log(
` 🔢 자동 생성(채번): ${mapping.targetField} = ${value} (규칙: ${mapping.numberingRuleId})`

View File

@ -937,8 +937,15 @@ class NumberingRuleService {
/**
* ( )
* @param ruleId ID
* @param companyCode
* @param formData ( )
*/
async allocateCode(ruleId: string, companyCode: string): Promise<string> {
async allocateCode(
ruleId: string,
companyCode: string,
formData?: Record<string, any>
): Promise<string> {
const pool = getPool();
const client = await pool.connect();
@ -974,10 +981,40 @@ class NumberingRuleService {
case "date": {
// 날짜 (다양한 날짜 형식)
return this.formatDate(
new Date(),
autoConfig.dateFormat || "YYYYMMDD"
);
const dateFormat = autoConfig.dateFormat || "YYYYMMDD";
// 컬럼 기준 생성인 경우 폼 데이터에서 날짜 추출
if (autoConfig.useColumnValue && autoConfig.sourceColumnName && formData) {
const columnValue = formData[autoConfig.sourceColumnName];
if (columnValue) {
// 날짜 문자열 또는 Date 객체를 Date로 변환
const dateValue = columnValue instanceof Date
? columnValue
: new Date(columnValue);
if (!isNaN(dateValue.getTime())) {
logger.info("컬럼 기준 날짜 생성", {
sourceColumn: autoConfig.sourceColumnName,
columnValue,
parsedDate: dateValue.toISOString(),
});
return this.formatDate(dateValue, dateFormat);
} else {
logger.warn("날짜 변환 실패, 현재 날짜 사용", {
sourceColumn: autoConfig.sourceColumnName,
columnValue,
});
}
} else {
logger.warn("소스 컬럼 값이 없음, 현재 날짜 사용", {
sourceColumn: autoConfig.sourceColumnName,
formDataKeys: Object.keys(formData),
});
}
}
// 기본: 현재 날짜 사용
return this.formatDate(new Date(), dateFormat);
}
case "text": {

View File

@ -590,3 +590,4 @@ const result = await executeNodeFlow(flowId, {

View File

@ -596,3 +596,4 @@ POST /multilang/keys/123/override

View File

@ -363,3 +363,4 @@

View File

@ -349,3 +349,4 @@ const getComponentValue = (componentId: string) => {

View File

@ -208,3 +208,4 @@ console.log("[AggregationWidget] selectableComponents:", filtered);
- `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` - `allComponents` 전달
- `frontend/components/screen/ScreenDesigner.tsx` - `layout.components` 전달

View File

@ -1,10 +1,17 @@
"use client";
import React from "react";
import React, { useState, useEffect, useMemo } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { CodePartType, DATE_FORMAT_OPTIONS } from "@/types/numbering-rule";
import { tableManagementApi } from "@/lib/api/tableManagement";
interface AutoConfigPanelProps {
partType: CodePartType;
@ -13,6 +20,18 @@ interface AutoConfigPanelProps {
isPreview?: boolean;
}
interface TableInfo {
tableName: string;
displayName: string;
}
interface ColumnInfo {
columnName: string;
displayName: string;
dataType: string;
inputType?: string;
}
export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
partType,
config = {},
@ -104,28 +123,11 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
// 3. 날짜
if (partType === "date") {
return (
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Select
value={config.dateFormat || "YYYYMMDD"}
onValueChange={(value) => onChange({ ...config, dateFormat: value })}
disabled={isPreview}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{DATE_FORMAT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value} className="text-xs sm:text-sm">
{option.label} ({option.example})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
</p>
</div>
<DateConfigPanel
config={config}
onChange={onChange}
isPreview={isPreview}
/>
);
}
@ -150,3 +152,314 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
return null;
};
/**
*
* -
* -
*/
interface DateConfigPanelProps {
config?: any;
onChange: (config: any) => void;
isPreview?: boolean;
}
const DateConfigPanel: React.FC<DateConfigPanelProps> = ({
config = {},
onChange,
isPreview = false,
}) => {
// 테이블 목록
const [tables, setTables] = useState<TableInfo[]>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
// 컬럼 목록
const [columns, setColumns] = useState<ColumnInfo[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
const [columnComboboxOpen, setColumnComboboxOpen] = useState(false);
// 체크박스 상태
const useColumnValue = config.useColumnValue || false;
const sourceTableName = config.sourceTableName || "";
const sourceColumnName = config.sourceColumnName || "";
// 테이블 목록 로드
useEffect(() => {
if (useColumnValue && tables.length === 0) {
loadTables();
}
}, [useColumnValue]);
// 테이블 변경 시 컬럼 로드
useEffect(() => {
if (sourceTableName) {
loadColumns(sourceTableName);
} else {
setColumns([]);
}
}, [sourceTableName]);
const loadTables = async () => {
setLoadingTables(true);
try {
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
const tableList = response.data.map((t: any) => ({
tableName: t.tableName || t.table_name,
displayName: t.displayName || t.table_label || t.tableName || t.table_name,
}));
setTables(tableList);
}
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
} finally {
setLoadingTables(false);
}
};
const loadColumns = async (tableName: string) => {
setLoadingColumns(true);
try {
const response = await tableManagementApi.getColumnList(tableName);
if (response.success && response.data) {
const rawColumns = response.data?.columns || response.data;
// 날짜 타입 컬럼만 필터링
const dateColumns = (rawColumns as any[]).filter((col: any) => {
const inputType = col.inputType || col.input_type || "";
const dataType = (col.dataType || col.data_type || "").toLowerCase();
return (
inputType === "date" ||
inputType === "datetime" ||
dataType.includes("date") ||
dataType.includes("timestamp")
);
});
setColumns(
dateColumns.map((col: any) => ({
columnName: col.columnName || col.column_name,
displayName: col.displayName || col.column_label || col.columnName || col.column_name,
dataType: col.dataType || col.data_type || "",
inputType: col.inputType || col.input_type || "",
}))
);
}
} catch (error) {
console.error("컬럼 목록 로드 실패:", error);
} finally {
setLoadingColumns(false);
}
};
// 선택된 테이블/컬럼 라벨
const selectedTableLabel = useMemo(() => {
const found = tables.find((t) => t.tableName === sourceTableName);
return found ? `${found.displayName} (${found.tableName})` : "";
}, [tables, sourceTableName]);
const selectedColumnLabel = useMemo(() => {
const found = columns.find((c) => c.columnName === sourceColumnName);
return found ? `${found.displayName} (${found.columnName})` : "";
}, [columns, sourceColumnName]);
return (
<div className="space-y-3 sm:space-y-4">
{/* 날짜 형식 선택 */}
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Select
value={config.dateFormat || "YYYYMMDD"}
onValueChange={(value) => onChange({ ...config, dateFormat: value })}
disabled={isPreview}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{DATE_FORMAT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value} className="text-xs sm:text-sm">
{option.label} ({option.example})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
{useColumnValue
? "선택한 컬럼의 날짜 값이 이 형식으로 변환됩니다"
: "현재 날짜가 자동으로 입력됩니다"}
</p>
</div>
{/* 컬럼 값 기준 생성 체크박스 */}
<div className="flex items-start gap-2">
<Checkbox
id="useColumnValue"
checked={useColumnValue}
onCheckedChange={(checked) => {
onChange({
...config,
useColumnValue: checked,
// 체크 해제 시 테이블/컬럼 초기화
...(checked ? {} : { sourceTableName: "", sourceColumnName: "" }),
});
}}
disabled={isPreview}
className="mt-0.5"
/>
<div className="flex-1">
<Label
htmlFor="useColumnValue"
className="cursor-pointer text-xs font-medium sm:text-sm"
>
</Label>
<p className="text-[10px] text-muted-foreground sm:text-xs">
</p>
</div>
</div>
{/* 테이블 선택 (체크 시 표시) */}
{useColumnValue && (
<>
<div>
<Label className="text-xs font-medium sm:text-sm"></Label>
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableComboboxOpen}
disabled={isPreview || loadingTables}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
>
{loadingTables
? "로딩 중..."
: sourceTableName
? selectedTableLabel
: "테이블 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm">
</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.displayName} ${table.tableName}`}
onSelect={() => {
onChange({
...config,
sourceTableName: table.tableName,
sourceColumnName: "", // 테이블 변경 시 컬럼 초기화
});
setTableComboboxOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
sourceTableName === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{table.displayName}</span>
<span className="text-[10px] text-gray-500">{table.tableName}</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 컬럼 선택 */}
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Popover open={columnComboboxOpen} onOpenChange={setColumnComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={columnComboboxOpen}
disabled={isPreview || loadingColumns || !sourceTableName}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
>
{loadingColumns
? "로딩 중..."
: !sourceTableName
? "테이블을 먼저 선택하세요"
: sourceColumnName
? selectedColumnLabel
: columns.length === 0
? "날짜 컬럼이 없습니다"
: "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm">
</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.columnName}
value={`${column.displayName} ${column.columnName}`}
onSelect={() => {
onChange({ ...config, sourceColumnName: column.columnName });
setColumnComboboxOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
sourceColumnName === column.columnName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{column.displayName}</span>
<span className="text-[10px] text-gray-500">
{column.columnName} ({column.inputType || column.dataType})
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{sourceTableName && columns.length === 0 && !loadingColumns && (
<p className="mt-1 text-[10px] text-amber-600 sm:text-xs">
</p>
)}
</div>
</>
)}
</div>
);
};

View File

@ -479,18 +479,6 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
</p>
</div>
{/* 세 번째 줄: 자동 감지된 테이블 정보 표시 */}
{currentTableName && (
<div className="space-y-2">
<Label className="text-sm font-medium"> </Label>
<div className="border-input bg-muted text-muted-foreground flex h-9 items-center rounded-md border px-3 text-sm">
{currentTableName}
</div>
<p className="text-muted-foreground text-xs">
({currentTableName})
</p>
</div>
)}
</div>
<div className="flex-1 overflow-y-auto">

View File

@ -44,6 +44,22 @@ export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
// 3. 날짜
case "date": {
const format = autoConfig.dateFormat || "YYYYMMDD";
// 컬럼 기준 생성인 경우 placeholder 표시
if (autoConfig.useColumnValue && autoConfig.sourceColumnName) {
// 형식에 맞는 placeholder 반환
switch (format) {
case "YYYY": return "[YYYY]";
case "YY": return "[YY]";
case "YYYYMM": return "[YYYYMM]";
case "YYMM": return "[YYMM]";
case "YYYYMMDD": return "[YYYYMMDD]";
case "YYMMDD": return "[YYMMDD]";
default: return "[DATE]";
}
}
// 현재 날짜 기준 생성
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");

View File

@ -2,7 +2,7 @@
/**
* UnifiedInput
*
*
*
* - text: 텍스트
* - number:
@ -12,12 +12,14 @@
* - button: 버튼 ( )
*/
import React, { forwardRef, useCallback, useMemo, useState } from "react";
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Input } from "@/components/ui/input";
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { UnifiedInputProps, UnifiedInputType, UnifiedInputFormat } from "@/types/unified-components";
import { UnifiedInputProps, UnifiedInputConfig, UnifiedInputFormat } from "@/types/unified-components";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
import { AutoGenerationConfig } from "@/types/screen";
// 형식별 입력 마스크 및 검증 패턴
const FORMAT_PATTERNS: Record<UnifiedInputFormat, { pattern: RegExp; placeholder: string }> = {
@ -56,46 +58,55 @@ function formatTel(value: string): string {
/**
*
*/
const TextInput = forwardRef<HTMLInputElement, {
value?: string | number;
onChange?: (value: string) => void;
format?: UnifiedInputFormat;
mask?: string;
placeholder?: string;
readonly?: boolean;
disabled?: boolean;
className?: string;
}>(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => {
const TextInput = forwardRef<
HTMLInputElement,
{
value?: string | number;
onChange?: (value: string) => void;
format?: UnifiedInputFormat;
mask?: string;
placeholder?: string;
readonly?: boolean;
disabled?: boolean;
className?: string;
}
>(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => {
// 형식에 따른 값 포맷팅
const formatValue = useCallback((val: string): string => {
switch (format) {
case "currency":
return formatCurrency(val);
case "biz_no":
return formatBizNo(val);
case "tel":
return formatTel(val);
default:
return val;
}
}, [format]);
const formatValue = useCallback(
(val: string): string => {
switch (format) {
case "currency":
return formatCurrency(val);
case "biz_no":
return formatBizNo(val);
case "tel":
return formatTel(val);
default:
return val;
}
},
[format],
);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
let newValue = e.target.value;
// 형식에 따른 자동 포맷팅
if (format === "currency") {
// 숫자와 쉼표만 허용
newValue = newValue.replace(/[^\d,]/g, "");
newValue = formatCurrency(newValue);
} else if (format === "biz_no") {
newValue = formatBizNo(newValue);
} else if (format === "tel") {
newValue = formatTel(newValue);
}
onChange?.(newValue);
}, [format, onChange]);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
let newValue = e.target.value;
// 형식에 따른 자동 포맷팅
if (format === "currency") {
// 숫자와 쉼표만 허용
newValue = newValue.replace(/[^\d,]/g, "");
newValue = formatCurrency(newValue);
} else if (format === "biz_no") {
newValue = formatBizNo(newValue);
} else if (format === "tel") {
newValue = formatTel(newValue);
}
onChange?.(newValue);
},
[format, onChange],
);
const displayValue = useMemo(() => {
if (value === undefined || value === null) return "";
@ -122,32 +133,38 @@ TextInput.displayName = "TextInput";
/**
*
*/
const NumberInput = forwardRef<HTMLInputElement, {
value?: number;
onChange?: (value: number | undefined) => void;
min?: number;
max?: number;
step?: number;
placeholder?: string;
readonly?: boolean;
disabled?: boolean;
className?: string;
}>(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className }, ref) => {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
if (val === "") {
onChange?.(undefined);
return;
}
let num = parseFloat(val);
// 범위 제한
if (min !== undefined && num < min) num = min;
if (max !== undefined && num > max) num = max;
onChange?.(num);
}, [min, max, onChange]);
const NumberInput = forwardRef<
HTMLInputElement,
{
value?: number;
onChange?: (value: number | undefined) => void;
min?: number;
max?: number;
step?: number;
placeholder?: string;
readonly?: boolean;
disabled?: boolean;
className?: string;
}
>(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className }, ref) => {
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
if (val === "") {
onChange?.(undefined);
return;
}
let num = parseFloat(val);
// 범위 제한
if (min !== undefined && num < min) num = min;
if (max !== undefined && num > max) num = max;
onChange?.(num);
},
[min, max, onChange],
);
return (
<Input
@ -170,14 +187,17 @@ NumberInput.displayName = "NumberInput";
/**
*
*/
const PasswordInput = forwardRef<HTMLInputElement, {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
readonly?: boolean;
disabled?: boolean;
className?: string;
}>(({ value, onChange, placeholder, readonly, disabled, className }, ref) => {
const PasswordInput = forwardRef<
HTMLInputElement,
{
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
readonly?: boolean;
disabled?: boolean;
className?: string;
}
>(({ value, onChange, placeholder, readonly, disabled, className }, ref) => {
const [showPassword, setShowPassword] = useState(false);
return (
@ -195,7 +215,7 @@ const PasswordInput = forwardRef<HTMLInputElement, {
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-xs"
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2 text-xs"
>
{showPassword ? "숨김" : "보기"}
</button>
@ -207,15 +227,18 @@ PasswordInput.displayName = "PasswordInput";
/**
*
*/
const SliderInput = forwardRef<HTMLDivElement, {
value?: number;
onChange?: (value: number) => void;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
className?: string;
}>(({ value, onChange, min = 0, max = 100, step = 1, disabled, className }, ref) => {
const SliderInput = forwardRef<
HTMLDivElement,
{
value?: number;
onChange?: (value: number) => void;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
className?: string;
}
>(({ value, onChange, min = 0, max = 100, step = 1, disabled, className }, ref) => {
return (
<div ref={ref} className={cn("flex items-center gap-4", className)}>
<Slider
@ -227,7 +250,7 @@ const SliderInput = forwardRef<HTMLDivElement, {
disabled={disabled}
className="flex-1"
/>
<span className="text-sm font-medium w-12 text-right">{value ?? min}</span>
<span className="w-12 text-right text-sm font-medium">{value ?? min}</span>
</div>
);
});
@ -236,12 +259,15 @@ SliderInput.displayName = "SliderInput";
/**
*
*/
const ColorInput = forwardRef<HTMLInputElement, {
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
className?: string;
}>(({ value, onChange, disabled, className }, ref) => {
const ColorInput = forwardRef<
HTMLInputElement,
{
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
className?: string;
}
>(({ value, onChange, disabled, className }, ref) => {
return (
<div className={cn("flex items-center gap-2", className)}>
<Input
@ -250,7 +276,7 @@ const ColorInput = forwardRef<HTMLInputElement, {
value={value || "#000000"}
onChange={(e) => onChange?.(e.target.value)}
disabled={disabled}
className="w-12 h-full p-1 cursor-pointer"
className="h-full w-12 cursor-pointer p-1"
/>
<Input
type="text"
@ -268,15 +294,18 @@ ColorInput.displayName = "ColorInput";
/**
*
*/
const TextareaInput = forwardRef<HTMLTextAreaElement, {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
rows?: number;
readonly?: boolean;
disabled?: boolean;
className?: string;
}>(({ value = "", onChange, placeholder, rows = 3, readonly, disabled, className }, ref) => {
const TextareaInput = forwardRef<
HTMLTextAreaElement,
{
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
rows?: number;
readonly?: boolean;
disabled?: boolean;
className?: string;
}
>(({ value = "", onChange, placeholder, rows = 3, readonly, disabled, className }, ref) => {
return (
<textarea
ref={ref}
@ -287,8 +316,8 @@ const TextareaInput = forwardRef<HTMLTextAreaElement, {
readOnly={readonly}
disabled={disabled}
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
/>
);
@ -298,155 +327,251 @@ TextareaInput.displayName = "TextareaInput";
/**
* UnifiedInput
*/
export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
(props, ref) => {
const {
id,
label,
required,
readonly,
disabled,
style,
size,
config: configProp,
value,
onChange,
} = props;
export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>((props, ref) => {
const { id, label, required, readonly, disabled, style, size, config: configProp, value, onChange } = props;
// config가 없으면 기본값 사용
const config: UnifiedInputConfig = configProp || { type: "text" };
// formData 추출 (채번규칙 날짜 컬럼 기준 생성 시 사용)
const formData = (props as any).formData || {};
const columnName = (props as any).columnName;
// 조건부 렌더링 체크
// TODO: conditional 처리 로직 추가
// 타입별 입력 컴포넌트 렌더링
const renderInput = () => {
const inputType = config.type || "text";
switch (inputType) {
case "text":
return (
<TextInput
value={value}
onChange={(v) => onChange?.(v)}
format={config.format}
mask={config.mask}
placeholder={config.placeholder}
readonly={readonly}
disabled={disabled}
/>
);
// config가 없으면 기본값 사용
const config = (configProp || { type: "text" }) as UnifiedInputConfig & {
inputType?: string;
rows?: number;
autoGeneration?: AutoGenerationConfig;
};
case "number":
return (
<NumberInput
value={typeof value === "number" ? value : undefined}
onChange={(v) => onChange?.(v ?? 0)}
min={config.min}
max={config.max}
step={config.step}
placeholder={config.placeholder}
readonly={readonly}
disabled={disabled}
/>
);
// 자동생성 설정 추출
const autoGeneration: AutoGenerationConfig = (props as any).autoGeneration ||
(config as any).autoGeneration || {
type: "none",
enabled: false,
};
case "password":
return (
<PasswordInput
value={typeof value === "string" ? value : ""}
onChange={(v) => onChange?.(v)}
placeholder={config.placeholder}
readonly={readonly}
disabled={disabled}
/>
);
// 자동생성 상태 관리
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string | null>(null);
const isGeneratingRef = useRef(false);
const hasGeneratedRef = useRef(false);
const lastFormDataRef = useRef<string>(""); // 마지막 formData 추적 (채번 규칙용)
case "slider":
return (
<SliderInput
value={typeof value === "number" ? value : config.min ?? 0}
onChange={(v) => onChange?.(v)}
min={config.min}
max={config.max}
step={config.step}
disabled={disabled}
/>
);
// 수정 모드 여부 확인
const originalData = (props as any).originalData || (props as any)._originalData;
const isEditMode = originalData && Object.keys(originalData).length > 0;
case "color":
return (
<ColorInput
value={typeof value === "string" ? value : "#000000"}
onChange={(v) => onChange?.(v)}
disabled={disabled}
/>
);
// 채번 규칙인 경우 formData 변경 감지 (자기 자신 필드 제외)
const formDataForNumbering = useMemo(() => {
if (autoGeneration.type !== "numbering_rule") return "";
// 자기 자신의 값은 제외 (무한 루프 방지)
const { [columnName]: _, ...rest } = formData;
return JSON.stringify(rest);
}, [autoGeneration.type, formData, columnName]);
case "textarea":
return (
<TextareaInput
value={value}
onChange={(v) => onChange?.(v)}
placeholder={config.placeholder}
rows={config.rows}
readonly={readonly}
disabled={disabled}
/>
);
// 자동생성 로직
useEffect(() => {
const generateValue = async () => {
// 자동생성 비활성화 또는 생성 중
if (!autoGeneration.enabled || isGeneratingRef.current) {
return;
}
default:
return (
<TextInput
value={value}
onChange={(v) => onChange?.(v)}
placeholder={config.placeholder}
readonly={readonly}
disabled={disabled}
/>
);
// 수정 모드에서는 자동생성 안함
if (isEditMode) {
return;
}
// 채번 규칙인 경우: formData가 변경되었는지 확인
const isNumberingRule = autoGeneration.type === "numbering_rule";
const formDataChanged =
isNumberingRule && formDataForNumbering !== lastFormDataRef.current && lastFormDataRef.current !== "";
// 이미 생성되었고, formData 변경이 아닌 경우 스킵
if (hasGeneratedRef.current && !formDataChanged) {
return;
}
// 첫 생성 시: 값이 이미 있으면 스킵 (formData 변경 시에는 강제 재생성)
if (!formDataChanged && value !== undefined && value !== null && value !== "") {
return;
}
isGeneratingRef.current = true;
try {
// formData를 전달하여 날짜 컬럼 기준 생성 지원
const generatedValue = await AutoGenerationUtils.generateValue(autoGeneration, columnName, formData);
if (generatedValue !== null && generatedValue !== undefined) {
setAutoGeneratedValue(generatedValue);
onChange?.(generatedValue);
hasGeneratedRef.current = true;
// formData 기록
if (isNumberingRule) {
lastFormDataRef.current = formDataForNumbering;
}
}
} catch (error) {
console.error("자동생성 실패:", error);
} finally {
isGeneratingRef.current = false;
}
};
// 라벨이 표시될 때 입력 필드가 차지할 높이 계산
const showLabel = label && style?.labelDisplay !== false;
// size에서 우선 가져오고, 없으면 style에서 가져옴
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
generateValue();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoGeneration.enabled, autoGeneration.type, isEditMode, formDataForNumbering]);
return (
<div
ref={ref}
id={id}
className="flex flex-col"
style={{
width: componentWidth,
height: componentHeight,
}}
>
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
// 실제 표시할 값 (자동생성 값 또는 props value)
const displayValue = autoGeneratedValue ?? value;
// 조건부 렌더링 체크
// TODO: conditional 처리 로직 추가
// 타입별 입력 컴포넌트 렌더링
const renderInput = () => {
const inputType = config.inputType || config.type || "text";
switch (inputType) {
case "text":
return (
<TextInput
value={displayValue}
onChange={(v) => {
setAutoGeneratedValue(null); // 사용자 입력 시 자동생성 값 초기화
onChange?.(v);
}}
className="text-sm font-medium flex-shrink-0"
>
{label}
{required && <span className="text-orange-500 ml-0.5">*</span>}
</Label>
)}
<div className="flex-1 min-h-0">
{renderInput()}
</div>
</div>
);
}
);
format={config.format}
mask={config.mask}
placeholder={config.placeholder}
readonly={readonly || (autoGeneration.enabled && hasGeneratedRef.current)}
disabled={disabled}
/>
);
case "number":
return (
<NumberInput
value={typeof displayValue === "number" ? displayValue : undefined}
onChange={(v) => {
setAutoGeneratedValue(null);
onChange?.(v ?? 0);
}}
min={config.min}
max={config.max}
step={config.step}
placeholder={config.placeholder}
readonly={readonly}
disabled={disabled}
/>
);
case "password":
return (
<PasswordInput
value={typeof displayValue === "string" ? displayValue : ""}
onChange={(v) => {
setAutoGeneratedValue(null);
onChange?.(v);
}}
placeholder={config.placeholder}
readonly={readonly}
disabled={disabled}
/>
);
case "slider":
return (
<SliderInput
value={typeof displayValue === "number" ? displayValue : (config.min ?? 0)}
onChange={(v) => {
setAutoGeneratedValue(null);
onChange?.(v);
}}
min={config.min}
max={config.max}
step={config.step}
disabled={disabled}
/>
);
case "color":
return (
<ColorInput
value={typeof displayValue === "string" ? displayValue : "#000000"}
onChange={(v) => {
setAutoGeneratedValue(null);
onChange?.(v);
}}
disabled={disabled}
/>
);
case "textarea":
return (
<TextareaInput
value={displayValue as string}
onChange={(v) => {
setAutoGeneratedValue(null);
onChange?.(v);
}}
placeholder={config.placeholder}
rows={config.rows}
readonly={readonly}
disabled={disabled}
/>
);
default:
return (
<TextInput
value={displayValue}
onChange={(v) => {
setAutoGeneratedValue(null);
onChange?.(v);
}}
placeholder={config.placeholder}
readonly={readonly}
disabled={disabled}
/>
);
}
};
// 라벨이 표시될 때 입력 필드가 차지할 높이 계산
const showLabel = label && style?.labelDisplay !== false;
// size에서 우선 가져오고, 없으면 style에서 가져옴
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
return (
<div
ref={ref}
id={id}
className="flex flex-col"
style={{
width: componentWidth,
height: componentHeight,
}}
>
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
}}
className="flex-shrink-0 text-sm font-medium"
>
{label}
{required && <span className="ml-0.5 text-orange-500">*</span>}
</Label>
)}
<div className="min-h-0 flex-1">{renderInput()}</div>
</div>
);
});
UnifiedInput.displayName = "UnifiedInput";
export default UnifiedInput;

View File

@ -5,23 +5,99 @@
* .
*/
import React from "react";
import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
import { getAvailableNumberingRules } from "@/lib/api/numberingRule";
import { NumberingRuleConfig } from "@/types/numbering-rule";
interface UnifiedInputConfigPanelProps {
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
menuObjid?: number; // 메뉴 OBJID (채번 규칙 필터링용)
}
export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = ({ config, onChange }) => {
export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = ({ config, onChange, menuObjid }) => {
// 채번 규칙 목록 상태
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [loadingRules, setLoadingRules] = useState(false);
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
const [parentMenus, setParentMenus] = useState<any[]>([]);
const [loadingMenus, setLoadingMenus] = useState(false);
// 선택된 메뉴 OBJID
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
return config.autoGeneration?.selectedMenuObjid || menuObjid;
});
// 설정 업데이트 핸들러
const updateConfig = (field: string, value: any) => {
onChange({ ...config, [field]: value });
};
// 부모 메뉴 목록 로드 (사용자 메뉴의 레벨 2만)
useEffect(() => {
const loadMenus = async () => {
setLoadingMenus(true);
try {
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get("/admin/menus");
if (response.data.success && response.data.data) {
const allMenus = response.data.data;
// 사용자 메뉴(menu_type='1')의 레벨 2만 필터링
const level2UserMenus = allMenus.filter((menu: any) =>
menu.menu_type === '1' && menu.lev === 2
);
setParentMenus(level2UserMenus);
}
} catch (error) {
console.error("부모 메뉴 로드 실패:", error);
} finally {
setLoadingMenus(false);
}
};
loadMenus();
}, []);
// 채번 규칙 목록 로드 (선택된 메뉴 기준)
useEffect(() => {
const loadRules = async () => {
if (config.autoGeneration?.type !== "numbering_rule") {
return;
}
if (!selectedMenuObjid) {
setNumberingRules([]);
return;
}
setLoadingRules(true);
try {
const response = await getAvailableNumberingRules(selectedMenuObjid);
if (response.success && response.data) {
setNumberingRules(response.data);
}
} catch (error) {
console.error("채번 규칙 목록 로드 실패:", error);
setNumberingRules([]);
} finally {
setLoadingRules(false);
}
};
loadRules();
}, [selectedMenuObjid, config.autoGeneration?.type]);
return (
<div className="space-y-4">
{/* 입력 타입 */}
@ -143,6 +219,229 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
/>
<p className="text-muted-foreground text-[10px]"># = , A = , * = </p>
</div>
<Separator />
{/* 자동생성 기능 */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="autoGenerationEnabled"
checked={config.autoGeneration?.enabled || false}
onCheckedChange={(checked) => {
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
updateConfig("autoGeneration", {
...currentConfig,
enabled: checked as boolean,
});
}}
/>
<Label htmlFor="autoGenerationEnabled" className="text-xs font-medium cursor-pointer">
</Label>
</div>
{/* 자동생성 타입 선택 */}
{config.autoGeneration?.enabled && (
<div className="space-y-3 pl-6">
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select
value={config.autoGeneration?.type || "none"}
onValueChange={(value: AutoGenerationType) => {
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
updateConfig("autoGeneration", {
...currentConfig,
type: value,
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="자동생성 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
<SelectItem value="uuid">UUID </SelectItem>
<SelectItem value="current_user"> ID</SelectItem>
<SelectItem value="current_time"> </SelectItem>
<SelectItem value="sequence"> </SelectItem>
<SelectItem value="numbering_rule"> </SelectItem>
<SelectItem value="random_string"> </SelectItem>
<SelectItem value="random_number"> </SelectItem>
<SelectItem value="company_code"> </SelectItem>
<SelectItem value="department"> </SelectItem>
</SelectContent>
</Select>
{/* 선택된 타입 설명 */}
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
<p className="text-muted-foreground text-[10px]">
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
</p>
)}
</div>
{/* 채번 규칙 선택 */}
{config.autoGeneration?.type === "numbering_rule" && (
<>
{/* 부모 메뉴 선택 */}
<div className="space-y-2">
<Label className="text-xs font-medium">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedMenuObjid?.toString() || ""}
onValueChange={(value) => {
const menuId = parseInt(value);
setSelectedMenuObjid(menuId);
updateConfig("autoGeneration", {
...config.autoGeneration,
selectedMenuObjid: menuId,
});
}}
disabled={loadingMenus}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={loadingMenus ? "메뉴 로딩 중..." : "채번규칙을 사용할 메뉴 선택"} />
</SelectTrigger>
<SelectContent>
{parentMenus.length === 0 ? (
<SelectItem value="no-menus" disabled>
</SelectItem>
) : (
parentMenus.map((menu) => (
<SelectItem key={menu.objid} value={menu.objid.toString()}>
{menu.menu_name_kor}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
{/* 채번 규칙 선택 */}
{selectedMenuObjid ? (
<div className="space-y-2">
<Label className="text-xs font-medium">
<span className="text-destructive">*</span>
</Label>
<Select
value={config.autoGeneration?.options?.numberingRuleId || ""}
onValueChange={(value) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
numberingRuleId: value,
},
});
}}
disabled={loadingRules}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
</SelectTrigger>
<SelectContent>
{numberingRules.length === 0 ? (
<SelectItem value="no-rules" disabled>
</SelectItem>
) : (
numberingRules.map((rule) => (
<SelectItem key={rule.ruleId} value={rule.ruleId}>
{rule.ruleName}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
) : (
<div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-xs text-amber-800">
</div>
)}
</>
)}
{/* 자동생성 옵션 (랜덤/순차용) */}
{config.autoGeneration?.type &&
["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && (
<div className="space-y-2">
{/* 길이 설정 */}
{["random_string", "random_number"].includes(config.autoGeneration.type) && (
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<Input
type="number"
min="1"
max="50"
value={config.autoGeneration?.options?.length || 8}
onChange={(e) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
length: parseInt(e.target.value) || 8,
},
});
}}
className="h-8 text-xs"
/>
</div>
)}
{/* 접두사 */}
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<Input
value={config.autoGeneration?.options?.prefix || ""}
onChange={(e) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
prefix: e.target.value,
},
});
}}
placeholder="예: INV-"
className="h-8 text-xs"
/>
</div>
{/* 접미사 */}
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<Input
value={config.autoGeneration?.options?.suffix || ""}
onChange={(e) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
suffix: e.target.value,
},
});
}}
className="h-8 text-xs"
/>
</div>
{/* 미리보기 */}
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<div className="rounded border bg-muted p-2 text-xs font-mono">
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
</div>
</div>
</div>
)}
</div>
)}
</div>
</div>
);
};

View File

@ -143,3 +143,4 @@ export const useActiveTabOptional = () => {

View File

@ -200,3 +200,4 @@ export function applyAutoFillToFormData(

View File

@ -244,7 +244,11 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
step: config.step,
buttonText: config.buttonText,
buttonVariant: config.buttonVariant,
autoGeneration: config.autoGeneration,
}}
autoGeneration={config.autoGeneration}
formData={props.formData}
originalData={props.originalData}
/>
);

View File

@ -146,10 +146,34 @@ export class AutoGenerationUtils {
}
/**
*
* API
*/
static generateValue(config: AutoGenerationConfig, columnName?: string): string | null {
console.log("🔧 AutoGenerationUtils.generateValue 호출:", {
static async generateNumberingRuleCode(ruleId: string, formData?: Record<string, any>): Promise<string | null> {
try {
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`, {
formData: formData || {},
});
if (response.data.success && response.data.data) {
// API 응답에서 생성된 코드 추출
const generatedCode = response.data.data.generatedCode || response.data.data;
console.log("채번 규칙 코드 생성 성공:", generatedCode);
return generatedCode;
}
console.error("채번 규칙 코드 생성 실패:", response.data.message);
return null;
} catch (error) {
console.error("채번 규칙 API 호출 실패:", error);
return null;
}
}
/**
* ()
*/
static async generateValue(config: AutoGenerationConfig, columnName?: string, formData?: Record<string, any>): Promise<string | null> {
console.log("AutoGenerationUtils.generateValue 호출:", {
config,
columnName,
enabled: config.enabled,
@ -157,7 +181,7 @@ export class AutoGenerationUtils {
});
if (!config.enabled || config.type === "none") {
console.log("⚠️ AutoGenerationUtils.generateValue 스킵:", {
console.log("AutoGenerationUtils.generateValue 스킵:", {
enabled: config.enabled,
type: config.type,
});
@ -174,17 +198,25 @@ export class AutoGenerationUtils {
return this.getCurrentUserId();
case "current_time":
console.log("🕒 AutoGenerationUtils.generateCurrentTime 호출:", {
console.log("AutoGenerationUtils.generateCurrentTime 호출:", {
format: options.format,
options,
});
const timeValue = this.generateCurrentTime(options.format);
console.log("🕒 AutoGenerationUtils.generateCurrentTime 결과:", timeValue);
console.log("AutoGenerationUtils.generateCurrentTime 결과:", timeValue);
return timeValue;
case "sequence":
return this.generateSequence(columnName || "default", options.startValue || 1, options.prefix, options.suffix);
case "numbering_rule":
// 채번 규칙 ID가 있으면 API 호출
if (options.numberingRuleId) {
return await this.generateNumberingRuleCode(options.numberingRuleId, formData);
}
console.warn("numbering_rule 타입인데 numberingRuleId가 없습니다");
return null;
case "random_string":
return this.generateRandomString(options.length || 8, options.prefix, options.suffix);

View File

@ -51,6 +51,9 @@ export interface NumberingRulePart {
// 날짜용
dateFormat?: DateFormat; // 날짜 형식
useColumnValue?: boolean; // 컬럼 값 기준 생성 여부
sourceTableName?: string; // 소스 테이블명
sourceColumnName?: string; // 소스 컬럼명 (날짜 컬럼)
// 문자용
textValue?: string; // 텍스트 값 (예: "PRJ", "CODE")

View File

@ -123,12 +123,25 @@ export interface AreaComponent extends ContainerComponent {
}
/**
* @deprecated
*
*/
export type AutoGenerationType = "table" | "form" | "mixed";
export type AutoGenerationType =
| "none"
| "uuid"
| "current_user"
| "current_time"
| "sequence"
| "numbering_rule"
| "random_string"
| "random_number"
| "company_code"
| "department"
| "table" // deprecated
| "form" // deprecated
| "mixed"; // deprecated
/**
* @deprecated
*
*/
export interface AutoGenerationConfig {
type: AutoGenerationType;
@ -143,5 +156,6 @@ export interface AutoGenerationConfig {
format?: string; // 시간 형식 (current_time용)
startValue?: number; // 시퀀스 시작값
numberingRuleId?: string; // 채번 규칙 ID (numbering_rule 타입용)
sourceColumnName?: string; // 날짜 컬럼명 (채번 규칙에서 날짜 기반 생성 시)
};
}

View File

@ -1692,3 +1692,4 @@ const 출고등록_설정: ScreenSplitPanel = {

View File

@ -539,3 +539,4 @@ const { data: config } = await getScreenSplitPanel(screenId);

View File

@ -526,3 +526,4 @@ function ScreenViewPage() {