테스트용 채번규칙 API 추가: numberingRuleController에 테이블+컬럼 기반 채번규칙 조회 및 테스트 테이블에 채번규칙 저장 기능을 추가하였습니다. 이를 통해 개발 및 테스트 환경에서 채번규칙을 보다 쉽게 관리할 수 있도록 개선하였습니다.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 || "테스트 규칙 저장 실패",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user