테스트용 채번규칙 API 추가: numberingRuleController에 테이블+컬럼 기반 채번규칙 조회 및 테스트 테이블에 채번규칙 저장 기능을 추가하였습니다. 이를 통해 개발 및 테스트 환경에서 채번규칙을 보다 쉽게 관리할 수 있도록 개선하였습니다.

This commit is contained in:
kjs
2026-01-21 13:54:14 +09:00
parent 4781a17b71
commit 16cb1ea1af
8 changed files with 664 additions and 5 deletions

View File

@@ -258,4 +258,67 @@ router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedReques
}
});
// ====== 테스트용 API (menu_objid 없는 방식) ======
// [테스트] 테이블+컬럼 기반 채번규칙 조회
router.get("/test/by-column", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { tableName, columnName } = req.query;
try {
if (!tableName || typeof tableName !== "string") {
return res.status(400).json({ success: false, error: "tableName is required" });
}
if (!columnName || typeof columnName !== "string") {
return res.status(400).json({ success: false, error: "columnName is required" });
}
const rule = await numberingRuleService.getNumberingRuleByColumn(
companyCode,
tableName,
columnName
);
if (!rule) {
return res.status(404).json({ success: false, error: "규칙을 찾을 수 없습니다" });
}
return res.json({ success: true, data: rule });
} catch (error: any) {
logger.error("테이블+컬럼 기반 채번규칙 조회 실패", {
error: error.message,
companyCode,
tableName,
columnName,
});
return res.status(500).json({ success: false, error: error.message });
}
});
// [테스트] 테스트 테이블에 채번규칙 저장
router.post("/test/save", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const config = req.body;
try {
if (!config.ruleId || !config.ruleName) {
return res.status(400).json({ success: false, error: "ruleId and ruleName are required" });
}
if (!config.tableName || !config.columnName) {
return res.status(400).json({ success: false, error: "tableName and columnName are required" });
}
const savedRule = await numberingRuleService.saveRuleToTest(config, companyCode, userId);
return res.json({ success: true, data: savedRule });
} catch (error: any) {
logger.error("테스트 테이블에 채번규칙 저장 실패", {
error: error.message,
companyCode,
ruleId: config.ruleId,
});
return res.status(500).json({ success: false, error: error.message });
}
});
export default router;

View File

@@ -1099,6 +1099,216 @@ class NumberingRuleService {
);
logger.info("시퀀스 초기화 완료", { ruleId, companyCode });
}
/**
* [테스트] 테이블명 + 컬럼명 기반으로 채번규칙 조회 (menu_objid 없이)
* numbering_rules_test 테이블 사용
*/
async getNumberingRuleByColumn(
companyCode: string,
tableName: string,
columnName: string
): Promise<NumberingRuleConfig | null> {
try {
logger.info("테이블+컬럼 기반 채번 규칙 조회 시작 (테스트)", {
companyCode,
tableName,
columnName,
});
const pool = getPool();
const query = `
SELECT
rule_id AS "ruleId",
rule_name AS "ruleName",
description,
separator,
reset_period AS "resetPeriod",
current_sequence AS "currentSequence",
table_name AS "tableName",
column_name AS "columnName",
company_code AS "companyCode",
created_at AS "createdAt",
updated_at AS "updatedAt",
created_by AS "createdBy"
FROM numbering_rules_test
WHERE company_code = $1
AND table_name = $2
AND column_name = $3
LIMIT 1
`;
const params = [companyCode, tableName, columnName];
const result = await pool.query(query, params);
if (result.rows.length === 0) {
logger.info("테이블+컬럼 기반 채번 규칙을 찾을 수 없음", {
companyCode,
tableName,
columnName,
});
return null;
}
const rule = result.rows[0];
// 파트 정보 조회 (테스트 테이블)
const partsQuery = `
SELECT
id,
part_order AS "order",
part_type AS "partType",
generation_method AS "generationMethod",
auto_config AS "autoConfig",
manual_config AS "manualConfig"
FROM numbering_rule_parts_test
WHERE rule_id = $1 AND company_code = $2
ORDER BY part_order
`;
const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]);
rule.parts = partsResult.rows;
logger.info("테이블+컬럼 기반 채번 규칙 조회 성공 (테스트)", {
ruleId: rule.ruleId,
ruleName: rule.ruleName,
});
return rule;
} catch (error: any) {
logger.error("테이블+컬럼 기반 채번 규칙 조회 실패 (테스트)", {
error: error.message,
stack: error.stack,
companyCode,
tableName,
columnName,
});
throw error;
}
}
/**
* [테스트] 테스트 테이블에 채번규칙 저장
* numbering_rules_test 테이블 사용
*/
async saveRuleToTest(
config: NumberingRuleConfig,
companyCode: string,
createdBy: string
): Promise<NumberingRuleConfig> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
logger.info("테스트 테이블에 채번 규칙 저장 시작", {
ruleId: config.ruleId,
ruleName: config.ruleName,
tableName: config.tableName,
columnName: config.columnName,
companyCode,
});
// 기존 규칙 확인
const existingQuery = `
SELECT rule_id FROM numbering_rules_test
WHERE rule_id = $1 AND company_code = $2
`;
const existingResult = await client.query(existingQuery, [config.ruleId, companyCode]);
if (existingResult.rows.length > 0) {
// 업데이트
const updateQuery = `
UPDATE numbering_rules_test SET
rule_name = $1,
description = $2,
separator = $3,
reset_period = $4,
table_name = $5,
column_name = $6,
updated_at = NOW()
WHERE rule_id = $7 AND company_code = $8
`;
await client.query(updateQuery, [
config.ruleName,
config.description || "",
config.separator || "-",
config.resetPeriod || "none",
config.tableName || "",
config.columnName || "",
config.ruleId,
companyCode,
]);
// 기존 파트 삭제
await client.query(
"DELETE FROM numbering_rule_parts_test WHERE rule_id = $1 AND company_code = $2",
[config.ruleId, companyCode]
);
} else {
// 신규 등록
const insertQuery = `
INSERT INTO numbering_rules_test (
rule_id, rule_name, description, separator, reset_period,
current_sequence, table_name, column_name, company_code,
created_at, updated_at, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), $10)
`;
await client.query(insertQuery, [
config.ruleId,
config.ruleName,
config.description || "",
config.separator || "-",
config.resetPeriod || "none",
config.currentSequence || 1,
config.tableName || "",
config.columnName || "",
companyCode,
createdBy,
]);
}
// 파트 저장
if (config.parts && config.parts.length > 0) {
for (const part of config.parts) {
const partInsertQuery = `
INSERT INTO numbering_rule_parts_test (
rule_id, part_order, part_type, generation_method,
auto_config, manual_config, company_code, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
`;
await client.query(partInsertQuery, [
config.ruleId,
part.order,
part.partType,
part.generationMethod,
JSON.stringify(part.autoConfig || {}),
JSON.stringify(part.manualConfig || {}),
companyCode,
]);
}
}
await client.query("COMMIT");
logger.info("테스트 테이블에 채번 규칙 저장 완료", {
ruleId: config.ruleId,
companyCode,
});
return config;
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("테스트 테이블에 채번 규칙 저장 실패", {
error: error.message,
stack: error.stack,
ruleId: config.ruleId,
companyCode,
});
throw error;
} finally {
client.release();
}
}
}
export const numberingRuleService = new NumberingRuleService();

View File

@@ -21,6 +21,8 @@ import { commonCodeApi } from "@/lib/api/commonCode";
import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
import { ddlApi } from "@/lib/api/ddl";
import { getSecondLevelMenus, createColumnMapping, deleteColumnMappingsByColumn } from "@/lib/api/tableCategoryValue";
import { getNumberingRules, saveNumberingRuleToTest } from "@/lib/api/numberingRule";
import { NumberingRuleConfig } from "@/types/numbering-rule";
import { CreateTableModal } from "@/components/admin/CreateTableModal";
import { AddColumnModal } from "@/components/admin/AddColumnModal";
import { DDLLogViewer } from "@/components/admin/DDLLogViewer";
@@ -60,6 +62,7 @@ interface ColumnTypeInfo {
displayColumn?: string; // 🎯 Entity 조인에서 표시할 컬럼명
categoryMenus?: number[]; // 🆕 Category 타입: 선택된 2레벨 메뉴 OBJID 배열
hierarchyRole?: "large" | "medium" | "small"; // 🆕 계층구조 역할
numberingRuleId?: string; // 🆕 Numbering 타입: 채번규칙 ID
}
interface SecondLevelMenu {
@@ -112,6 +115,11 @@ export default function TableManagementPage() {
// 🆕 Category 타입용: 2레벨 메뉴 목록
const [secondLevelMenus, setSecondLevelMenus] = useState<SecondLevelMenu[]>([]);
// 🆕 Numbering 타입용: 채번규칙 목록
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [numberingRulesLoading, setNumberingRulesLoading] = useState(false);
const [numberingComboboxOpen, setNumberingComboboxOpen] = useState<Record<string, boolean>>({});
// 로그 뷰어 상태
const [logViewerOpen, setLogViewerOpen] = useState(false);
const [logViewerTableName, setLogViewerTableName] = useState<string>("");
@@ -263,6 +271,25 @@ export default function TableManagementPage() {
}
};
// 🆕 채번규칙 목록 로드
const loadNumberingRules = async () => {
setNumberingRulesLoading(true);
try {
const response = await getNumberingRules();
if (response.success && response.data) {
setNumberingRules(response.data);
} else {
console.warn("⚠️ 채번규칙 로드 실패:", response);
setNumberingRules([]);
}
} catch (error) {
console.error("❌ 채번규칙 로드 에러:", error);
setNumberingRules([]);
} finally {
setNumberingRulesLoading(false);
}
};
// 테이블 목록 로드
const loadTables = async () => {
setLoading(true);
@@ -304,14 +331,18 @@ export default function TableManagementPage() {
// 컬럼 데이터에 기본값 설정
const processedColumns = (data.columns || data).map((col: any) => {
// detailSettings에서 hierarchyRole 추출
// detailSettings에서 hierarchyRole, numberingRuleId 추출
let hierarchyRole: "large" | "medium" | "small" | undefined = undefined;
let numberingRuleId: string | undefined = undefined;
if (col.detailSettings && typeof col.detailSettings === "string") {
try {
const parsed = JSON.parse(col.detailSettings);
if (parsed.hierarchyRole === "large" || parsed.hierarchyRole === "medium" || parsed.hierarchyRole === "small") {
hierarchyRole = parsed.hierarchyRole;
}
if (parsed.numberingRuleId) {
numberingRuleId = parsed.numberingRuleId;
}
} catch {
// JSON 파싱 실패 시 무시
}
@@ -320,6 +351,7 @@ export default function TableManagementPage() {
return {
...col,
inputType: col.inputType || "text", // 기본값: text
numberingRuleId, // 🆕 채번규칙 ID
categoryMenus: col.categoryMenus || [], // 카테고리 메뉴 매핑 정보
hierarchyRole, // 계층구조 역할
};
@@ -557,6 +589,38 @@ export default function TableManagementPage() {
console.log("🔧 Code 계층 역할 설정 JSON 생성:", codeSettings);
}
// 🆕 Numbering 타입인 경우 numberingRuleId를 detailSettings에 포함
console.log("🔍 Numbering 저장 체크:", {
inputType: column.inputType,
numberingRuleId: column.numberingRuleId,
hasNumberingRuleId: !!column.numberingRuleId,
});
if (column.inputType === "numbering") {
let existingSettings: Record<string, unknown> = {};
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
try {
existingSettings = JSON.parse(finalDetailSettings);
} catch {
existingSettings = {};
}
}
// numberingRuleId가 있으면 저장, 없으면 제거
if (column.numberingRuleId) {
const numberingSettings = {
...existingSettings,
numberingRuleId: column.numberingRuleId,
};
finalDetailSettings = JSON.stringify(numberingSettings);
console.log("🔧 Numbering 설정 JSON 생성:", numberingSettings);
} else {
// numberingRuleId가 없으면 빈 객체
finalDetailSettings = JSON.stringify(existingSettings);
console.log("🔧 Numbering 규칙 없이 저장:", existingSettings);
}
}
const columnSetting = {
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
columnLabel: column.displayName, // 사용자가 입력한 표시명
@@ -826,6 +890,7 @@ export default function TableManagementPage() {
loadTables();
loadCommonCodeCategories();
loadSecondLevelMenus();
loadNumberingRules();
}, []);
// 🎯 컬럼 로드 후 이미 설정된 참조 테이블들의 컬럼 정보 로드
@@ -1675,6 +1740,116 @@ export default function TableManagementPage() {
)}
</>
)}
{/* 입력 타입이 'numbering'인 경우 채번규칙 선택 */}
{column.inputType === "numbering" && (
<div className="w-64">
<label className="text-muted-foreground mb-1 block text-xs"></label>
<Popover
open={numberingComboboxOpen[column.columnName] || false}
onOpenChange={(open) =>
setNumberingComboboxOpen((prev) => ({
...prev,
[column.columnName]: open,
}))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={numberingComboboxOpen[column.columnName] || false}
disabled={numberingRulesLoading}
className="bg-background h-8 w-full justify-between text-xs"
>
<span className="truncate">
{numberingRulesLoading
? "로딩 중..."
: column.numberingRuleId
? numberingRules.find((r) => r.ruleId === column.numberingRuleId)?.ruleName ||
column.numberingRuleId
: "채번규칙 선택..."}
</span>
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="규칙 검색..." className="h-8 text-xs" />
<CommandList className="max-h-[200px]">
<CommandEmpty className="py-2 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
<CommandItem
value="none"
onSelect={async () => {
const columnIndex = columns.findIndex((c) => c.columnName === column.columnName);
handleColumnChange(columnIndex, "numberingRuleId", undefined);
setNumberingComboboxOpen((prev) => ({
...prev,
[column.columnName]: false,
}));
// 🆕 자동 저장 (선택 해제)
const updatedColumn = { ...column, numberingRuleId: undefined };
await handleSaveColumn(updatedColumn);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!column.numberingRuleId ? "opacity-100" : "opacity-0"
)}
/>
-- --
</CommandItem>
{numberingRules.map((rule) => (
<CommandItem
key={rule.ruleId}
value={`${rule.ruleName} ${rule.ruleId}`}
onSelect={async () => {
const columnIndex = columns.findIndex((c) => c.columnName === column.columnName);
// 상태 업데이트
handleColumnChange(columnIndex, "numberingRuleId", rule.ruleId);
setNumberingComboboxOpen((prev) => ({
...prev,
[column.columnName]: false,
}));
// 🆕 자동 저장
const updatedColumn = { ...column, numberingRuleId: rule.ruleId };
await handleSaveColumn(updatedColumn);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
column.numberingRuleId === rule.ruleId ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{rule.ruleName}</span>
{rule.tableName && (
<span className="text-muted-foreground text-[10px]">
{rule.tableName}.{rule.columnName}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{column.numberingRuleId && (
<div className="bg-primary/10 text-primary mt-1 flex items-center gap-1 rounded px-2 py-0.5 text-[10px]">
<Check className="h-2.5 w-2.5" />
<span> </span>
</div>
)}
</div>
)}
</div>
</div>
<div className="pl-4">

View File

@@ -20,6 +20,7 @@ import { cn } from "@/lib/utils";
import { UnifiedInputProps, UnifiedInputConfig, UnifiedInputFormat } from "@/types/unified-components";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
import { AutoGenerationConfig } from "@/types/screen";
import { previewNumberingCode } from "@/lib/api/numberingRule";
// 형식별 입력 마스크 및 검증 패턴
const FORMAT_PATTERNS: Record<UnifiedInputFormat, { pattern: RegExp; placeholder: string }> = {
@@ -354,6 +355,13 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>((props
const hasGeneratedRef = useRef(false);
const lastFormDataRef = useRef<string>(""); // 마지막 formData 추적 (채번 규칙용)
// 채번 타입 자동생성 상태
const [isGeneratingNumbering, setIsGeneratingNumbering] = useState(false);
const hasGeneratedNumberingRef = useRef(false);
// tableName 추출 (props에서 전달받거나 config에서)
const tableName = (props as any).tableName || (config as any).tableName;
// 수정 모드 여부 확인
const originalData = (props as any).originalData || (props as any)._originalData;
const isEditMode = originalData && Object.keys(originalData).length > 0;
@@ -421,6 +429,96 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>((props
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoGeneration.enabled, autoGeneration.type, isEditMode, formDataForNumbering]);
// 채번 타입 자동생성 로직 (테이블 관리에서 설정된 numberingRuleId 사용)
useEffect(() => {
const generateNumberingCode = async () => {
const inputType = config.inputType || config.type || "text";
// numbering 타입이 아니면 스킵
if (inputType !== "numbering") {
return;
}
// 이미 생성되었거나 생성 중이면 스킵
if (hasGeneratedNumberingRef.current || isGeneratingNumbering) {
return;
}
// 수정 모드에서는 자동생성 안함
if (isEditMode) {
return;
}
// 이미 값이 있으면 스킵
if (value !== undefined && value !== null && value !== "") {
return;
}
// tableName과 columnName이 필요
if (!tableName || !columnName) {
console.warn("채번 타입: tableName 또는 columnName이 없습니다", { tableName, columnName });
return;
}
setIsGeneratingNumbering(true);
try {
// 테이블 설정에서 numberingRuleId 조회
const { getTableColumns } = await import("@/lib/api/tableManagement");
const columnsResponse = await getTableColumns(tableName);
if (!columnsResponse.success || !columnsResponse.data) {
console.warn("테이블 컬럼 정보 조회 실패:", columnsResponse);
return;
}
const columns = columnsResponse.data.columns || columnsResponse.data;
const targetColumn = columns.find((col: any) => col.columnName === columnName);
if (!targetColumn) {
console.warn("컬럼 정보를 찾을 수 없습니다:", columnName);
return;
}
// detailSettings에서 numberingRuleId 추출
let numberingRuleId: string | undefined;
if (targetColumn.detailSettings && typeof targetColumn.detailSettings === "string") {
try {
const parsed = JSON.parse(targetColumn.detailSettings);
numberingRuleId = parsed.numberingRuleId;
} catch {
// JSON 파싱 실패
}
}
if (!numberingRuleId) {
console.warn("채번 규칙 ID가 설정되지 않았습니다. 테이블 관리에서 설정하세요.", { tableName, columnName });
return;
}
// 채번 코드 생성 (미리보기)
const previewResponse = await previewNumberingCode(numberingRuleId);
if (previewResponse.success && previewResponse.data?.generatedCode) {
const generatedCode = previewResponse.data.generatedCode;
setAutoGeneratedValue(generatedCode);
onChange?.(generatedCode);
hasGeneratedNumberingRef.current = true;
console.log("채번 코드 생성 성공:", generatedCode);
} else {
console.warn("채번 코드 생성 실패:", previewResponse);
}
} catch (error) {
console.error("채번 자동생성 오류:", error);
} finally {
setIsGeneratingNumbering(false);
}
};
generateNumberingCode();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tableName, columnName, isEditMode, value]);
// 실제 표시할 값 (자동생성 값 또는 props value)
const displayValue = autoGeneratedValue ?? value;
@@ -520,6 +618,18 @@ export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>((props
/>
);
case "numbering":
// 채번 타입: 읽기 전용 텍스트 필드로 표시 (자동 생성)
return (
<TextInput
value={displayValue || ""}
onChange={() => {}}
placeholder={isGeneratingNumbering ? "생성 중..." : "자동 생성됩니다"}
readonly={true}
disabled={disabled || isGeneratingNumbering}
/>
);
default:
return (
<TextInput

View File

@@ -117,14 +117,50 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
<SelectItem value="textarea"> </SelectItem>
<SelectItem value="slider"></SelectItem>
<SelectItem value="color"> </SelectItem>
<SelectItem value="numbering"> ()</SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
{/* 채번 타입 전용 설정 */}
{config.inputType === "numbering" && (
<div className="space-y-3">
<Separator />
<div className="rounded-md border border-blue-200 bg-blue-50 p-3">
<p className="text-xs font-medium text-blue-800"> </p>
<p className="mt-1 text-[10px] text-blue-700">
<strong> </strong> .
<br />
.
</p>
</div>
{/* 채번 필드는 기본적으로 읽기전용 */}
<div className="flex items-center space-x-2">
<Checkbox
id="numberingReadonly"
checked={config.readonly !== false}
onCheckedChange={(checked) => {
updateConfig("readonly", checked);
}}
/>
<Label htmlFor="numberingReadonly" className="text-xs font-medium cursor-pointer">
()
</Label>
</div>
<p className="text-muted-foreground text-[10px] pl-6">
</p>
</div>
)}
{/* 형식 (텍스트/숫자용) */}
{(config.inputType === "text" || !config.inputType) && (
{/* 채번 타입이 아닌 경우에만 추가 설정 표시 */}
{config.inputType !== "numbering" && (
<>
<Separator />
{/* 형식 (텍스트/숫자용) */}
{(config.inputType === "text" || !config.inputType) && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
@@ -442,6 +478,8 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
</div>
)}
</div>
</>
)}
</div>
);
};

View File

@@ -167,5 +167,44 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
}
}
// ====== 테스트용 API (menu_objid 없는 방식) ======
/**
* [테스트] 테이블+컬럼 기반 채번규칙 조회
* numbering_rules_test 테이블 사용
*/
export async function getNumberingRuleByColumn(
tableName: string,
columnName: string
): Promise<ApiResponse<NumberingRuleConfig>> {
try {
const response = await apiClient.get("/numbering-rules/test/by-column", {
params: { tableName, columnName },
});
return response.data;
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || error.message || "테이블+컬럼 기반 규칙 조회 실패",
};
}
}
/**
* [테스트] 테스트 테이블에 채번규칙 저장
* numbering_rules_test 테이블 사용
*/
export async function saveNumberingRuleToTest(
config: NumberingRuleConfig
): Promise<ApiResponse<NumberingRuleConfig>> {
try {
const response = await apiClient.post("/numbering-rules/test/save", config);
return response.data;
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || error.message || "테스트 규칙 저장 실패",
};
}
}

View File

@@ -268,6 +268,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
{...commonProps}
config={{
type: config.inputType || config.type || "text",
inputType: config.inputType || config.type || "text", // 🆕 inputType 명시적 전달
format: config.format,
placeholder: config.placeholder,
mask: config.mask,
@@ -277,6 +278,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
buttonText: config.buttonText,
buttonVariant: config.buttonVariant,
autoGeneration: config.autoGeneration,
tableName: (component as any).tableName || props.tableName, // 🆕 채번용 테이블명
}}
autoGeneration={config.autoGeneration}
formData={props.formData}

View File

@@ -17,7 +17,8 @@ export type InputType =
| "checkbox" // 체크박스
| "radio" // 라디오버튼
| "image" // 이미지
| "file"; // 파일
| "file" // 파일
| "numbering"; // 채번 (자동번호 생성)
// 입력 타입 옵션 정의
export interface InputTypeOption {
@@ -113,6 +114,13 @@ export const INPUT_TYPE_OPTIONS: InputTypeOption[] = [
category: "basic",
icon: "File",
},
{
value: "numbering",
label: "채번",
description: "자동 번호 생성 (테이블 설정 기반)",
category: "basic",
icon: "Hash",
},
];
// 카테고리별 입력 타입 그룹화
@@ -180,6 +188,11 @@ export const INPUT_TYPE_DEFAULT_CONFIGS: Record<InputType, Record<string, any>>
accept: "*/*",
maxSize: 10485760, // 10MB
},
numbering: {
placeholder: "자동 생성됩니다",
readOnly: true,
autoGenerate: true,
},
};
// 레거시 웹 타입 → 입력 타입 매핑
@@ -217,6 +230,9 @@ export const WEB_TYPE_TO_INPUT_TYPE: Record<string, InputType> = {
file: "file",
image: "image",
// 채번
numbering: "numbering",
// 기타 (기본값: text)
button: "text",
};
@@ -234,6 +250,7 @@ export const INPUT_TYPE_TO_WEB_TYPE: Record<InputType, string> = {
radio: "radio",
image: "image",
file: "file",
numbering: "numbering",
};
// 입력 타입 변환 함수
@@ -288,4 +305,9 @@ export const INPUT_TYPE_VALIDATION_RULES: Record<InputType, Record<string, any>>
type: "string",
required: false,
},
numbering: {
type: "string",
required: false,
autoGenerate: true,
},
};