코드 할당 요청 시 폼 데이터 추가: numberingRuleController에서 코드 할당 요청 시 폼 데이터를 포함하도록 수정하였습니다. 이를 통해 날짜 컬럼 기준 생성 시 필요한 정보를 전달할 수 있도록 개선하였습니다.
This commit is contained in:
parent
95da69ec70
commit
d3701cfe1e
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -58,3 +58,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -54,3 +54,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -70,3 +70,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,3 +58,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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})`
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -590,3 +590,4 @@ const result = await executeNodeFlow(flowId, {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -596,3 +596,4 @@ POST /multilang/keys/123/override
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -363,3 +363,4 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -349,3 +349,4 @@ const getComponentValue = (componentId: string) => {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -208,3 +208,4 @@ console.log("[AggregationWidget] selectableComponents:", filtered);
|
|||
- `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` - `allComponents` 전달
|
||||
- `frontend/components/screen/ScreenDesigner.tsx` - `layout.components` 전달
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,7 +58,9 @@ function formatTel(value: string): string {
|
|||
/**
|
||||
* 텍스트 입력 컴포넌트
|
||||
*/
|
||||
const TextInput = forwardRef<HTMLInputElement, {
|
||||
const TextInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
{
|
||||
value?: string | number;
|
||||
onChange?: (value: string) => void;
|
||||
format?: UnifiedInputFormat;
|
||||
|
|
@ -65,9 +69,11 @@ const TextInput = forwardRef<HTMLInputElement, {
|
|||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => {
|
||||
}
|
||||
>(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => {
|
||||
// 형식에 따른 값 포맷팅
|
||||
const formatValue = useCallback((val: string): string => {
|
||||
const formatValue = useCallback(
|
||||
(val: string): string => {
|
||||
switch (format) {
|
||||
case "currency":
|
||||
return formatCurrency(val);
|
||||
|
|
@ -78,9 +84,12 @@ const TextInput = forwardRef<HTMLInputElement, {
|
|||
default:
|
||||
return val;
|
||||
}
|
||||
}, [format]);
|
||||
},
|
||||
[format],
|
||||
);
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue = e.target.value;
|
||||
|
||||
// 형식에 따른 자동 포맷팅
|
||||
|
|
@ -95,7 +104,9 @@ const TextInput = forwardRef<HTMLInputElement, {
|
|||
}
|
||||
|
||||
onChange?.(newValue);
|
||||
}, [format, onChange]);
|
||||
},
|
||||
[format, onChange],
|
||||
);
|
||||
|
||||
const displayValue = useMemo(() => {
|
||||
if (value === undefined || value === null) return "";
|
||||
|
|
@ -122,7 +133,9 @@ TextInput.displayName = "TextInput";
|
|||
/**
|
||||
* 숫자 입력 컴포넌트
|
||||
*/
|
||||
const NumberInput = forwardRef<HTMLInputElement, {
|
||||
const NumberInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
{
|
||||
value?: number;
|
||||
onChange?: (value: number | undefined) => void;
|
||||
min?: number;
|
||||
|
|
@ -132,8 +145,10 @@ const NumberInput = forwardRef<HTMLInputElement, {
|
|||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className }, ref) => {
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
}
|
||||
>(({ 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);
|
||||
|
|
@ -147,7 +162,9 @@ const NumberInput = forwardRef<HTMLInputElement, {
|
|||
if (max !== undefined && num > max) num = max;
|
||||
|
||||
onChange?.(num);
|
||||
}, [min, max, onChange]);
|
||||
},
|
||||
[min, max, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Input
|
||||
|
|
@ -170,14 +187,17 @@ NumberInput.displayName = "NumberInput";
|
|||
/**
|
||||
* 비밀번호 입력 컴포넌트
|
||||
*/
|
||||
const PasswordInput = forwardRef<HTMLInputElement, {
|
||||
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) => {
|
||||
}
|
||||
>(({ 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,7 +227,9 @@ PasswordInput.displayName = "PasswordInput";
|
|||
/**
|
||||
* 슬라이더 입력 컴포넌트
|
||||
*/
|
||||
const SliderInput = forwardRef<HTMLDivElement, {
|
||||
const SliderInput = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
min?: number;
|
||||
|
|
@ -215,7 +237,8 @@ const SliderInput = forwardRef<HTMLDivElement, {
|
|||
step?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ value, onChange, min = 0, max = 100, step = 1, disabled, className }, ref) => {
|
||||
}
|
||||
>(({ 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, {
|
||||
const ColorInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
{
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ value, onChange, disabled, className }, ref) => {
|
||||
}
|
||||
>(({ 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,7 +294,9 @@ ColorInput.displayName = "ColorInput";
|
|||
/**
|
||||
* 여러 줄 텍스트 입력 컴포넌트
|
||||
*/
|
||||
const TextareaInput = forwardRef<HTMLTextAreaElement, {
|
||||
const TextareaInput = forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
{
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
|
|
@ -276,7 +304,8 @@ const TextareaInput = forwardRef<HTMLTextAreaElement, {
|
|||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ value = "", onChange, placeholder, rows = 3, readonly, disabled, className }, ref) => {
|
||||
}
|
||||
>(({ 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,40 +327,122 @@ 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;
|
||||
|
||||
// formData 추출 (채번규칙 날짜 컬럼 기준 생성 시 사용)
|
||||
const formData = (props as any).formData || {};
|
||||
const columnName = (props as any).columnName;
|
||||
|
||||
// config가 없으면 기본값 사용
|
||||
const config: UnifiedInputConfig = configProp || { type: "text" };
|
||||
const config = (configProp || { type: "text" }) as UnifiedInputConfig & {
|
||||
inputType?: string;
|
||||
rows?: number;
|
||||
autoGeneration?: AutoGenerationConfig;
|
||||
};
|
||||
|
||||
// 자동생성 설정 추출
|
||||
const autoGeneration: AutoGenerationConfig = (props as any).autoGeneration ||
|
||||
(config as any).autoGeneration || {
|
||||
type: "none",
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
// 자동생성 상태 관리
|
||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string | null>(null);
|
||||
const isGeneratingRef = useRef(false);
|
||||
const hasGeneratedRef = useRef(false);
|
||||
const lastFormDataRef = useRef<string>(""); // 마지막 formData 추적 (채번 규칙용)
|
||||
|
||||
// 수정 모드 여부 확인
|
||||
const originalData = (props as any).originalData || (props as any)._originalData;
|
||||
const isEditMode = originalData && Object.keys(originalData).length > 0;
|
||||
|
||||
// 채번 규칙인 경우 formData 변경 감지 (자기 자신 필드 제외)
|
||||
const formDataForNumbering = useMemo(() => {
|
||||
if (autoGeneration.type !== "numbering_rule") return "";
|
||||
// 자기 자신의 값은 제외 (무한 루프 방지)
|
||||
const { [columnName]: _, ...rest } = formData;
|
||||
return JSON.stringify(rest);
|
||||
}, [autoGeneration.type, formData, columnName]);
|
||||
|
||||
// 자동생성 로직
|
||||
useEffect(() => {
|
||||
const generateValue = async () => {
|
||||
// 자동생성 비활성화 또는 생성 중
|
||||
if (!autoGeneration.enabled || isGeneratingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 수정 모드에서는 자동생성 안함
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
generateValue();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoGeneration.enabled, autoGeneration.type, isEditMode, formDataForNumbering]);
|
||||
|
||||
// 실제 표시할 값 (자동생성 값 또는 props value)
|
||||
const displayValue = autoGeneratedValue ?? value;
|
||||
|
||||
// 조건부 렌더링 체크
|
||||
// TODO: conditional 처리 로직 추가
|
||||
|
||||
// 타입별 입력 컴포넌트 렌더링
|
||||
const renderInput = () => {
|
||||
const inputType = config.type || "text";
|
||||
const inputType = config.inputType || config.type || "text";
|
||||
switch (inputType) {
|
||||
case "text":
|
||||
return (
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
value={displayValue}
|
||||
onChange={(v) => {
|
||||
setAutoGeneratedValue(null); // 사용자 입력 시 자동생성 값 초기화
|
||||
onChange?.(v);
|
||||
}}
|
||||
format={config.format}
|
||||
mask={config.mask}
|
||||
placeholder={config.placeholder}
|
||||
readonly={readonly}
|
||||
readonly={readonly || (autoGeneration.enabled && hasGeneratedRef.current)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
|
@ -339,8 +450,11 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
|
|||
case "number":
|
||||
return (
|
||||
<NumberInput
|
||||
value={typeof value === "number" ? value : undefined}
|
||||
onChange={(v) => onChange?.(v ?? 0)}
|
||||
value={typeof displayValue === "number" ? displayValue : undefined}
|
||||
onChange={(v) => {
|
||||
setAutoGeneratedValue(null);
|
||||
onChange?.(v ?? 0);
|
||||
}}
|
||||
min={config.min}
|
||||
max={config.max}
|
||||
step={config.step}
|
||||
|
|
@ -353,8 +467,11 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
|
|||
case "password":
|
||||
return (
|
||||
<PasswordInput
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
value={typeof displayValue === "string" ? displayValue : ""}
|
||||
onChange={(v) => {
|
||||
setAutoGeneratedValue(null);
|
||||
onChange?.(v);
|
||||
}}
|
||||
placeholder={config.placeholder}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
|
|
@ -364,8 +481,11 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
|
|||
case "slider":
|
||||
return (
|
||||
<SliderInput
|
||||
value={typeof value === "number" ? value : config.min ?? 0}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
value={typeof displayValue === "number" ? displayValue : (config.min ?? 0)}
|
||||
onChange={(v) => {
|
||||
setAutoGeneratedValue(null);
|
||||
onChange?.(v);
|
||||
}}
|
||||
min={config.min}
|
||||
max={config.max}
|
||||
step={config.step}
|
||||
|
|
@ -376,8 +496,11 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
|
|||
case "color":
|
||||
return (
|
||||
<ColorInput
|
||||
value={typeof value === "string" ? value : "#000000"}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
value={typeof displayValue === "string" ? displayValue : "#000000"}
|
||||
onChange={(v) => {
|
||||
setAutoGeneratedValue(null);
|
||||
onChange?.(v);
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
|
@ -385,8 +508,11 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
|
|||
case "textarea":
|
||||
return (
|
||||
<TextareaInput
|
||||
value={value}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
value={displayValue as string}
|
||||
onChange={(v) => {
|
||||
setAutoGeneratedValue(null);
|
||||
onChange?.(v);
|
||||
}}
|
||||
placeholder={config.placeholder}
|
||||
rows={config.rows}
|
||||
readonly={readonly}
|
||||
|
|
@ -397,8 +523,11 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
|
|||
default:
|
||||
return (
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
value={displayValue}
|
||||
onChange={(v) => {
|
||||
setAutoGeneratedValue(null);
|
||||
onChange?.(v);
|
||||
}}
|
||||
placeholder={config.placeholder}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
|
|
@ -432,21 +561,17 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>(
|
|||
fontWeight: style?.labelFontWeight,
|
||||
marginBottom: style?.labelMarginBottom,
|
||||
}}
|
||||
className="text-sm font-medium flex-shrink-0"
|
||||
className="flex-shrink-0 text-sm font-medium"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||
</Label>
|
||||
)}
|
||||
<div className="flex-1 min-h-0">
|
||||
{renderInput()}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">{renderInput()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
UnifiedInput.displayName = "UnifiedInput";
|
||||
|
||||
export default UnifiedInput;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -143,3 +143,4 @@ export const useActiveTabOptional = () => {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -200,3 +200,4 @@ export function applyAutoFillToFormData(
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ export interface NumberingRulePart {
|
|||
|
||||
// 날짜용
|
||||
dateFormat?: DateFormat; // 날짜 형식
|
||||
useColumnValue?: boolean; // 컬럼 값 기준 생성 여부
|
||||
sourceTableName?: string; // 소스 테이블명
|
||||
sourceColumnName?: string; // 소스 컬럼명 (날짜 컬럼)
|
||||
|
||||
// 문자용
|
||||
textValue?: string; // 텍스트 값 (예: "PRJ", "CODE")
|
||||
|
|
|
|||
|
|
@ -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; // 날짜 컬럼명 (채번 규칙에서 날짜 기반 생성 시)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1692,3 +1692,4 @@ const 출고등록_설정: ScreenSplitPanel = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -539,3 +539,4 @@ const { data: config } = await getScreenSplitPanel(screenId);
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -526,3 +526,4 @@ function ScreenViewPage() {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue