diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts
index ab7114a5..f61bd4e4 100644
--- a/backend-node/src/controllers/numberingRuleController.ts
+++ b/backend-node/src/controllers/numberingRuleController.ts
@@ -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) {
diff --git a/backend-node/src/routes/cascadingAutoFillRoutes.ts b/backend-node/src/routes/cascadingAutoFillRoutes.ts
index b479147b..81eef3e9 100644
--- a/backend-node/src/routes/cascadingAutoFillRoutes.ts
+++ b/backend-node/src/routes/cascadingAutoFillRoutes.ts
@@ -58,3 +58,4 @@ export default router;
+
diff --git a/backend-node/src/routes/cascadingConditionRoutes.ts b/backend-node/src/routes/cascadingConditionRoutes.ts
index 43c2b80c..57d533e4 100644
--- a/backend-node/src/routes/cascadingConditionRoutes.ts
+++ b/backend-node/src/routes/cascadingConditionRoutes.ts
@@ -54,3 +54,4 @@ export default router;
+
diff --git a/backend-node/src/routes/cascadingHierarchyRoutes.ts b/backend-node/src/routes/cascadingHierarchyRoutes.ts
index be46e0fc..934034d0 100644
--- a/backend-node/src/routes/cascadingHierarchyRoutes.ts
+++ b/backend-node/src/routes/cascadingHierarchyRoutes.ts
@@ -70,3 +70,4 @@ export default router;
+
diff --git a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts
index 842a61d6..4a3f5543 100644
--- a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts
+++ b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts
@@ -58,3 +58,4 @@ export default router;
+
diff --git a/backend-node/src/services/masterDetailExcelService.ts b/backend-node/src/services/masterDetailExcelService.ts
index 44cc42b1..623fb228 100644
--- a/backend-node/src/services/masterDetailExcelService.ts
+++ b/backend-node/src/services/masterDetailExcelService.ts
@@ -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
): Promise {
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}`);
diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts
index b5237f0b..eadddf9f 100644
--- a/backend-node/src/services/nodeFlowExecutionService.ts
+++ b/backend-node/src/services/nodeFlowExecutionService.ts
@@ -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})`
diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts
index 8208ecc5..db37d4b5 100644
--- a/backend-node/src/services/numberingRuleService.ts
+++ b/backend-node/src/services/numberingRuleService.ts
@@ -937,8 +937,15 @@ class NumberingRuleService {
/**
* 코드 할당 (저장 시점에 실제 순번 증가)
+ * @param ruleId 채번 규칙 ID
+ * @param companyCode 회사 코드
+ * @param formData 폼 데이터 (날짜 컬럼 기준 생성 시 사용)
*/
- async allocateCode(ruleId: string, companyCode: string): Promise {
+ async allocateCode(
+ ruleId: string,
+ companyCode: string,
+ formData?: Record
+ ): Promise {
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": {
diff --git a/docs/노드플로우_개선사항.md b/docs/노드플로우_개선사항.md
index 3ceb0818..1c688d74 100644
--- a/docs/노드플로우_개선사항.md
+++ b/docs/노드플로우_개선사항.md
@@ -590,3 +590,4 @@ const result = await executeNodeFlow(flowId, {
+
diff --git a/docs/다국어_관리_시스템_개선_계획서.md b/docs/다국어_관리_시스템_개선_계획서.md
index 5d1c1155..0b4e7135 100644
--- a/docs/다국어_관리_시스템_개선_계획서.md
+++ b/docs/다국어_관리_시스템_개선_계획서.md
@@ -596,3 +596,4 @@ POST /multilang/keys/123/override
+
diff --git a/docs/메일발송_기능_사용_가이드.md b/docs/메일발송_기능_사용_가이드.md
index 3567f6f1..699d1b66 100644
--- a/docs/메일발송_기능_사용_가이드.md
+++ b/docs/메일발송_기능_사용_가이드.md
@@ -363,3 +363,4 @@
+
diff --git a/docs/즉시저장_버튼_액션_구현_계획서.md b/docs/즉시저장_버튼_액션_구현_계획서.md
index 39a7c7ac..e6a63d83 100644
--- a/docs/즉시저장_버튼_액션_구현_계획서.md
+++ b/docs/즉시저장_버튼_액션_구현_계획서.md
@@ -349,3 +349,4 @@ const getComponentValue = (componentId: string) => {
+
diff --git a/docs/집계위젯_개발진행상황.md b/docs/집계위젯_개발진행상황.md
index b01bd380..79e2abd5 100644
--- a/docs/집계위젯_개발진행상황.md
+++ b/docs/집계위젯_개발진행상황.md
@@ -208,3 +208,4 @@ console.log("[AggregationWidget] selectableComponents:", filtered);
- `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` - `allComponents` 전달
- `frontend/components/screen/ScreenDesigner.tsx` - `layout.components` 전달
+
diff --git a/frontend/components/numbering-rule/AutoConfigPanel.tsx b/frontend/components/numbering-rule/AutoConfigPanel.tsx
index 8f05e2e3..5fd28ed5 100644
--- a/frontend/components/numbering-rule/AutoConfigPanel.tsx
+++ b/frontend/components/numbering-rule/AutoConfigPanel.tsx
@@ -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 = ({
partType,
config = {},
@@ -104,28 +123,11 @@ export const AutoConfigPanel: React.FC = ({
// 3. 날짜
if (partType === "date") {
return (
-
-
-
-
- 현재 날짜가 자동으로 입력됩니다
-
-
+
);
}
@@ -150,3 +152,314 @@ export const AutoConfigPanel: React.FC = ({
return null;
};
+
+/**
+ * 날짜 타입 전용 설정 패널
+ * - 날짜 형식 선택
+ * - 컬럼 값 기준 생성 옵션
+ */
+interface DateConfigPanelProps {
+ config?: any;
+ onChange: (config: any) => void;
+ isPreview?: boolean;
+}
+
+const DateConfigPanel: React.FC = ({
+ config = {},
+ onChange,
+ isPreview = false,
+}) => {
+ // 테이블 목록
+ const [tables, setTables] = useState([]);
+ const [loadingTables, setLoadingTables] = useState(false);
+ const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
+
+ // 컬럼 목록
+ const [columns, setColumns] = useState([]);
+ 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 (
+
+ {/* 날짜 형식 선택 */}
+
+
+
+
+ {useColumnValue
+ ? "선택한 컬럼의 날짜 값이 이 형식으로 변환됩니다"
+ : "현재 날짜가 자동으로 입력됩니다"}
+
+
+
+ {/* 컬럼 값 기준 생성 체크박스 */}
+
+
{
+ onChange({
+ ...config,
+ useColumnValue: checked,
+ // 체크 해제 시 테이블/컬럼 초기화
+ ...(checked ? {} : { sourceTableName: "", sourceColumnName: "" }),
+ });
+ }}
+ disabled={isPreview}
+ className="mt-0.5"
+ />
+
+
+
+ 폼에 입력된 날짜 값으로 코드를 생성합니다
+
+
+
+
+ {/* 테이블 선택 (체크 시 표시) */}
+ {useColumnValue && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ 테이블을 찾을 수 없습니다
+
+
+ {tables.map((table) => (
+ {
+ onChange({
+ ...config,
+ sourceTableName: table.tableName,
+ sourceColumnName: "", // 테이블 변경 시 컬럼 초기화
+ });
+ setTableComboboxOpen(false);
+ }}
+ className="text-xs sm:text-sm"
+ >
+
+
+ {table.displayName}
+ {table.tableName}
+
+
+ ))}
+
+
+
+
+
+
+
+ {/* 컬럼 선택 */}
+
+
+
+
+
+
+
+
+
+
+
+ 날짜 컬럼을 찾을 수 없습니다
+
+
+ {columns.map((column) => (
+ {
+ onChange({ ...config, sourceColumnName: column.columnName });
+ setColumnComboboxOpen(false);
+ }}
+ className="text-xs sm:text-sm"
+ >
+
+
+ {column.displayName}
+
+ {column.columnName} ({column.inputType || column.dataType})
+
+
+
+ ))}
+
+
+
+
+
+ {sourceTableName && columns.length === 0 && !loadingColumns && (
+
+ 이 테이블에 날짜 타입 컬럼이 없습니다
+
+ )}
+
+ >
+ )}
+
+ );
+};
diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
index 3a20b883..60eda2eb 100644
--- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
+++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
@@ -479,18 +479,6 @@ export const NumberingRuleDesigner: React.FC = ({
- {/* 세 번째 줄: 자동 감지된 테이블 정보 표시 */}
- {currentTableName && (
-
-
-
- {currentTableName}
-
-
- 이 규칙은 현재 화면의 테이블({currentTableName})에 자동으로 적용됩니다
-
-
- )}
diff --git a/frontend/components/numbering-rule/NumberingRulePreview.tsx b/frontend/components/numbering-rule/NumberingRulePreview.tsx
index 63e39e04..a9179959 100644
--- a/frontend/components/numbering-rule/NumberingRulePreview.tsx
+++ b/frontend/components/numbering-rule/NumberingRulePreview.tsx
@@ -44,6 +44,22 @@ export const NumberingRulePreview: React.FC
= ({
// 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");
diff --git a/frontend/components/unified/UnifiedInput.tsx b/frontend/components/unified/UnifiedInput.tsx
index ed8a7147..6e4b9927 100644
--- a/frontend/components/unified/UnifiedInput.tsx
+++ b/frontend/components/unified/UnifiedInput.tsx
@@ -2,7 +2,7 @@
/**
* UnifiedInput
- *
+ *
* 통합 입력 컴포넌트
* - text: 텍스트 입력
* - number: 숫자 입력
@@ -12,12 +12,14 @@
* - button: 버튼 (입력이 아닌 액션)
*/
-import React, { forwardRef, useCallback, useMemo, useState } from "react";
+import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Input } from "@/components/ui/input";
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
-import { UnifiedInputProps, UnifiedInputType, UnifiedInputFormat } from "@/types/unified-components";
+import { UnifiedInputProps, UnifiedInputConfig, UnifiedInputFormat } from "@/types/unified-components";
+import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
+import { AutoGenerationConfig } from "@/types/screen";
// 형식별 입력 마스크 및 검증 패턴
const FORMAT_PATTERNS: Record = {
@@ -56,46 +58,55 @@ function formatTel(value: string): string {
/**
* 텍스트 입력 컴포넌트
*/
-const TextInput = forwardRef void;
- format?: UnifiedInputFormat;
- mask?: string;
- placeholder?: string;
- readonly?: boolean;
- disabled?: boolean;
- className?: string;
-}>(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => {
+const TextInput = forwardRef<
+ HTMLInputElement,
+ {
+ value?: string | number;
+ onChange?: (value: string) => void;
+ format?: UnifiedInputFormat;
+ mask?: string;
+ placeholder?: string;
+ readonly?: boolean;
+ disabled?: boolean;
+ className?: string;
+ }
+>(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => {
// 형식에 따른 값 포맷팅
- const formatValue = useCallback((val: string): string => {
- switch (format) {
- case "currency":
- return formatCurrency(val);
- case "biz_no":
- return formatBizNo(val);
- case "tel":
- return formatTel(val);
- default:
- return val;
- }
- }, [format]);
+ const formatValue = useCallback(
+ (val: string): string => {
+ switch (format) {
+ case "currency":
+ return formatCurrency(val);
+ case "biz_no":
+ return formatBizNo(val);
+ case "tel":
+ return formatTel(val);
+ default:
+ return val;
+ }
+ },
+ [format],
+ );
- const handleChange = useCallback((e: React.ChangeEvent) => {
- let newValue = e.target.value;
-
- // 형식에 따른 자동 포맷팅
- if (format === "currency") {
- // 숫자와 쉼표만 허용
- newValue = newValue.replace(/[^\d,]/g, "");
- newValue = formatCurrency(newValue);
- } else if (format === "biz_no") {
- newValue = formatBizNo(newValue);
- } else if (format === "tel") {
- newValue = formatTel(newValue);
- }
-
- onChange?.(newValue);
- }, [format, onChange]);
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ let newValue = e.target.value;
+
+ // 형식에 따른 자동 포맷팅
+ if (format === "currency") {
+ // 숫자와 쉼표만 허용
+ newValue = newValue.replace(/[^\d,]/g, "");
+ newValue = formatCurrency(newValue);
+ } else if (format === "biz_no") {
+ newValue = formatBizNo(newValue);
+ } else if (format === "tel") {
+ newValue = formatTel(newValue);
+ }
+
+ onChange?.(newValue);
+ },
+ [format, onChange],
+ );
const displayValue = useMemo(() => {
if (value === undefined || value === null) return "";
@@ -122,32 +133,38 @@ TextInput.displayName = "TextInput";
/**
* 숫자 입력 컴포넌트
*/
-const NumberInput = forwardRef void;
- min?: number;
- max?: number;
- step?: number;
- placeholder?: string;
- readonly?: boolean;
- disabled?: boolean;
- className?: string;
-}>(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className }, ref) => {
- const handleChange = useCallback((e: React.ChangeEvent) => {
- const val = e.target.value;
- if (val === "") {
- onChange?.(undefined);
- return;
- }
-
- let num = parseFloat(val);
-
- // 범위 제한
- if (min !== undefined && num < min) num = min;
- if (max !== undefined && num > max) num = max;
-
- onChange?.(num);
- }, [min, max, onChange]);
+const NumberInput = forwardRef<
+ HTMLInputElement,
+ {
+ value?: number;
+ onChange?: (value: number | undefined) => void;
+ min?: number;
+ max?: number;
+ step?: number;
+ placeholder?: string;
+ readonly?: boolean;
+ disabled?: boolean;
+ className?: string;
+ }
+>(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className }, ref) => {
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const val = e.target.value;
+ if (val === "") {
+ onChange?.(undefined);
+ return;
+ }
+
+ let num = parseFloat(val);
+
+ // 범위 제한
+ if (min !== undefined && num < min) num = min;
+ if (max !== undefined && num > max) num = max;
+
+ onChange?.(num);
+ },
+ [min, max, onChange],
+ );
return (
void;
- placeholder?: string;
- readonly?: boolean;
- disabled?: boolean;
- className?: string;
-}>(({ value, onChange, placeholder, readonly, disabled, className }, ref) => {
+const PasswordInput = forwardRef<
+ HTMLInputElement,
+ {
+ value?: string;
+ onChange?: (value: string) => void;
+ placeholder?: string;
+ readonly?: boolean;
+ disabled?: boolean;
+ className?: string;
+ }
+>(({ value, onChange, placeholder, readonly, disabled, className }, ref) => {
const [showPassword, setShowPassword] = useState(false);
return (
@@ -195,7 +215,7 @@ const PasswordInput = forwardRef 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 ? "숨김" : "보기"}
@@ -207,15 +227,18 @@ PasswordInput.displayName = "PasswordInput";
/**
* 슬라이더 입력 컴포넌트
*/
-const SliderInput = forwardRef void;
- min?: number;
- max?: number;
- step?: number;
- disabled?: boolean;
- className?: string;
-}>(({ value, onChange, min = 0, max = 100, step = 1, disabled, className }, ref) => {
+const SliderInput = forwardRef<
+ HTMLDivElement,
+ {
+ value?: number;
+ onChange?: (value: number) => void;
+ min?: number;
+ max?: number;
+ step?: number;
+ disabled?: boolean;
+ className?: string;
+ }
+>(({ value, onChange, min = 0, max = 100, step = 1, disabled, className }, ref) => {
return (
- {value ?? min}
+ {value ?? min}
);
});
@@ -236,12 +259,15 @@ SliderInput.displayName = "SliderInput";
/**
* 색상 선택 컴포넌트
*/
-const ColorInput = forwardRef void;
- disabled?: boolean;
- className?: string;
-}>(({ value, onChange, disabled, className }, ref) => {
+const ColorInput = forwardRef<
+ HTMLInputElement,
+ {
+ value?: string;
+ onChange?: (value: string) => void;
+ disabled?: boolean;
+ className?: string;
+ }
+>(({ value, onChange, disabled, className }, ref) => {
return (
onChange?.(e.target.value)}
disabled={disabled}
- className="w-12 h-full p-1 cursor-pointer"
+ className="h-full w-12 cursor-pointer p-1"
/>
void;
- placeholder?: string;
- rows?: number;
- readonly?: boolean;
- disabled?: boolean;
- className?: string;
-}>(({ value = "", onChange, placeholder, rows = 3, readonly, disabled, className }, ref) => {
+const TextareaInput = forwardRef<
+ HTMLTextAreaElement,
+ {
+ value?: string;
+ onChange?: (value: string) => void;
+ placeholder?: string;
+ rows?: number;
+ readonly?: boolean;
+ disabled?: boolean;
+ className?: string;
+ }
+>(({ value = "", onChange, placeholder, rows = 3, readonly, disabled, className }, ref) => {
return (
);
@@ -298,155 +327,251 @@ TextareaInput.displayName = "TextareaInput";
/**
* 메인 UnifiedInput 컴포넌트
*/
-export const UnifiedInput = forwardRef
(
- (props, ref) => {
- const {
- id,
- label,
- required,
- readonly,
- disabled,
- style,
- size,
- config: configProp,
- value,
- onChange,
- } = props;
+export const UnifiedInput = forwardRef((props, ref) => {
+ const { id, label, required, readonly, disabled, style, size, config: configProp, value, onChange } = props;
- // config가 없으면 기본값 사용
- const config: UnifiedInputConfig = configProp || { type: "text" };
+ // formData 추출 (채번규칙 날짜 컬럼 기준 생성 시 사용)
+ const formData = (props as any).formData || {};
+ const columnName = (props as any).columnName;
- // 조건부 렌더링 체크
- // TODO: conditional 처리 로직 추가
-
- // 타입별 입력 컴포넌트 렌더링
- const renderInput = () => {
- const inputType = config.type || "text";
- switch (inputType) {
- case "text":
- return (
- onChange?.(v)}
- format={config.format}
- mask={config.mask}
- placeholder={config.placeholder}
- readonly={readonly}
- disabled={disabled}
- />
- );
+ // config가 없으면 기본값 사용
+ const config = (configProp || { type: "text" }) as UnifiedInputConfig & {
+ inputType?: string;
+ rows?: number;
+ autoGeneration?: AutoGenerationConfig;
+ };
- case "number":
- return (
- onChange?.(v ?? 0)}
- min={config.min}
- max={config.max}
- step={config.step}
- placeholder={config.placeholder}
- readonly={readonly}
- disabled={disabled}
- />
- );
+ // 자동생성 설정 추출
+ const autoGeneration: AutoGenerationConfig = (props as any).autoGeneration ||
+ (config as any).autoGeneration || {
+ type: "none",
+ enabled: false,
+ };
- case "password":
- return (
- onChange?.(v)}
- placeholder={config.placeholder}
- readonly={readonly}
- disabled={disabled}
- />
- );
+ // 자동생성 상태 관리
+ const [autoGeneratedValue, setAutoGeneratedValue] = useState(null);
+ const isGeneratingRef = useRef(false);
+ const hasGeneratedRef = useRef(false);
+ const lastFormDataRef = useRef(""); // 마지막 formData 추적 (채번 규칙용)
- case "slider":
- return (
- onChange?.(v)}
- min={config.min}
- max={config.max}
- step={config.step}
- disabled={disabled}
- />
- );
+ // 수정 모드 여부 확인
+ const originalData = (props as any).originalData || (props as any)._originalData;
+ const isEditMode = originalData && Object.keys(originalData).length > 0;
- case "color":
- return (
- onChange?.(v)}
- disabled={disabled}
- />
- );
+ // 채번 규칙인 경우 formData 변경 감지 (자기 자신 필드 제외)
+ const formDataForNumbering = useMemo(() => {
+ if (autoGeneration.type !== "numbering_rule") return "";
+ // 자기 자신의 값은 제외 (무한 루프 방지)
+ const { [columnName]: _, ...rest } = formData;
+ return JSON.stringify(rest);
+ }, [autoGeneration.type, formData, columnName]);
- case "textarea":
- return (
- onChange?.(v)}
- placeholder={config.placeholder}
- rows={config.rows}
- readonly={readonly}
- disabled={disabled}
- />
- );
+ // 자동생성 로직
+ useEffect(() => {
+ const generateValue = async () => {
+ // 자동생성 비활성화 또는 생성 중
+ if (!autoGeneration.enabled || isGeneratingRef.current) {
+ return;
+ }
- default:
- return (
- onChange?.(v)}
- placeholder={config.placeholder}
- readonly={readonly}
- disabled={disabled}
- />
- );
+ // 수정 모드에서는 자동생성 안함
+ if (isEditMode) {
+ return;
+ }
+
+ // 채번 규칙인 경우: formData가 변경되었는지 확인
+ const isNumberingRule = autoGeneration.type === "numbering_rule";
+ const formDataChanged =
+ isNumberingRule && formDataForNumbering !== lastFormDataRef.current && lastFormDataRef.current !== "";
+
+ // 이미 생성되었고, formData 변경이 아닌 경우 스킵
+ if (hasGeneratedRef.current && !formDataChanged) {
+ return;
+ }
+
+ // 첫 생성 시: 값이 이미 있으면 스킵 (formData 변경 시에는 강제 재생성)
+ if (!formDataChanged && value !== undefined && value !== null && value !== "") {
+ return;
+ }
+
+ isGeneratingRef.current = true;
+
+ try {
+ // formData를 전달하여 날짜 컬럼 기준 생성 지원
+ const generatedValue = await AutoGenerationUtils.generateValue(autoGeneration, columnName, formData);
+
+ if (generatedValue !== null && generatedValue !== undefined) {
+ setAutoGeneratedValue(generatedValue);
+ onChange?.(generatedValue);
+ hasGeneratedRef.current = true;
+
+ // formData 기록
+ if (isNumberingRule) {
+ lastFormDataRef.current = formDataForNumbering;
+ }
+ }
+ } catch (error) {
+ console.error("자동생성 실패:", error);
+ } finally {
+ isGeneratingRef.current = false;
}
};
- // 라벨이 표시될 때 입력 필드가 차지할 높이 계산
- const showLabel = label && style?.labelDisplay !== false;
- // size에서 우선 가져오고, 없으면 style에서 가져옴
- const componentWidth = size?.width || style?.width;
- const componentHeight = size?.height || style?.height;
+ generateValue();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [autoGeneration.enabled, autoGeneration.type, isEditMode, formDataForNumbering]);
- return (
-
- {showLabel && (
-
- )}
-
- {renderInput()}
-
-
- );
- }
-);
+ format={config.format}
+ mask={config.mask}
+ placeholder={config.placeholder}
+ readonly={readonly || (autoGeneration.enabled && hasGeneratedRef.current)}
+ disabled={disabled}
+ />
+ );
+
+ case "number":
+ return (
+ {
+ setAutoGeneratedValue(null);
+ onChange?.(v ?? 0);
+ }}
+ min={config.min}
+ max={config.max}
+ step={config.step}
+ placeholder={config.placeholder}
+ readonly={readonly}
+ disabled={disabled}
+ />
+ );
+
+ case "password":
+ return (
+ {
+ setAutoGeneratedValue(null);
+ onChange?.(v);
+ }}
+ placeholder={config.placeholder}
+ readonly={readonly}
+ disabled={disabled}
+ />
+ );
+
+ case "slider":
+ return (
+ {
+ setAutoGeneratedValue(null);
+ onChange?.(v);
+ }}
+ min={config.min}
+ max={config.max}
+ step={config.step}
+ disabled={disabled}
+ />
+ );
+
+ case "color":
+ return (
+ {
+ setAutoGeneratedValue(null);
+ onChange?.(v);
+ }}
+ disabled={disabled}
+ />
+ );
+
+ case "textarea":
+ return (
+ {
+ setAutoGeneratedValue(null);
+ onChange?.(v);
+ }}
+ placeholder={config.placeholder}
+ rows={config.rows}
+ readonly={readonly}
+ disabled={disabled}
+ />
+ );
+
+ default:
+ return (
+ {
+ setAutoGeneratedValue(null);
+ onChange?.(v);
+ }}
+ placeholder={config.placeholder}
+ readonly={readonly}
+ disabled={disabled}
+ />
+ );
+ }
+ };
+
+ // 라벨이 표시될 때 입력 필드가 차지할 높이 계산
+ const showLabel = label && style?.labelDisplay !== false;
+ // size에서 우선 가져오고, 없으면 style에서 가져옴
+ const componentWidth = size?.width || style?.width;
+ const componentHeight = size?.height || style?.height;
+
+ return (
+
+ {showLabel && (
+
+ )}
+
{renderInput()}
+
+ );
+});
UnifiedInput.displayName = "UnifiedInput";
export default UnifiedInput;
-
diff --git a/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx
index c0059a55..e76548e8 100644
--- a/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx
+++ b/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx
@@ -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;
onChange: (config: Record) => void;
+ menuObjid?: number; // 메뉴 OBJID (채번 규칙 필터링용)
}
-export const UnifiedInputConfigPanel: React.FC = ({ config, onChange }) => {
+export const UnifiedInputConfigPanel: React.FC = ({ config, onChange, menuObjid }) => {
+ // 채번 규칙 목록 상태
+ const [numberingRules, setNumberingRules] = useState([]);
+ const [loadingRules, setLoadingRules] = useState(false);
+
+ // 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
+ const [parentMenus, setParentMenus] = useState([]);
+ const [loadingMenus, setLoadingMenus] = useState(false);
+
+ // 선택된 메뉴 OBJID
+ const [selectedMenuObjid, setSelectedMenuObjid] = useState(() => {
+ 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 (
{/* 입력 타입 */}
@@ -143,6 +219,229 @@ export const UnifiedInputConfigPanel: React.FC
= (
/>
# = 숫자, A = 문자, * = 모든 문자
+
+
+
+ {/* 자동생성 기능 */}
+
+
+ {
+ const currentConfig = config.autoGeneration || { type: "none", enabled: false };
+ updateConfig("autoGeneration", {
+ ...currentConfig,
+ enabled: checked as boolean,
+ });
+ }}
+ />
+
+
+
+ {/* 자동생성 타입 선택 */}
+ {config.autoGeneration?.enabled && (
+
+
+
+
+
+ {/* 선택된 타입 설명 */}
+ {config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
+
+ {AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
+
+ )}
+
+
+ {/* 채번 규칙 선택 */}
+ {config.autoGeneration?.type === "numbering_rule" && (
+ <>
+ {/* 부모 메뉴 선택 */}
+
+
+
+
+
+ {/* 채번 규칙 선택 */}
+ {selectedMenuObjid ? (
+
+
+
+
+ ) : (
+
+ 먼저 대상 메뉴를 선택하세요
+
+ )}
+ >
+ )}
+
+ {/* 자동생성 옵션 (랜덤/순차용) */}
+ {config.autoGeneration?.type &&
+ ["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && (
+
+ {/* 길이 설정 */}
+ {["random_string", "random_number"].includes(config.autoGeneration.type) && (
+
+
+ {
+ updateConfig("autoGeneration", {
+ ...config.autoGeneration,
+ options: {
+ ...config.autoGeneration?.options,
+ length: parseInt(e.target.value) || 8,
+ },
+ });
+ }}
+ className="h-8 text-xs"
+ />
+
+ )}
+
+ {/* 접두사 */}
+
+
+ {
+ updateConfig("autoGeneration", {
+ ...config.autoGeneration,
+ options: {
+ ...config.autoGeneration?.options,
+ prefix: e.target.value,
+ },
+ });
+ }}
+ placeholder="예: INV-"
+ className="h-8 text-xs"
+ />
+
+
+ {/* 접미사 */}
+
+
+ {
+ updateConfig("autoGeneration", {
+ ...config.autoGeneration,
+ options: {
+ ...config.autoGeneration?.options,
+ suffix: e.target.value,
+ },
+ });
+ }}
+ className="h-8 text-xs"
+ />
+
+
+ {/* 미리보기 */}
+
+
+
+ {AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
+
+
+
+ )}
+
+ )}
+
);
};
diff --git a/frontend/contexts/ActiveTabContext.tsx b/frontend/contexts/ActiveTabContext.tsx
index 53e4bbd9..ef31be62 100644
--- a/frontend/contexts/ActiveTabContext.tsx
+++ b/frontend/contexts/ActiveTabContext.tsx
@@ -143,3 +143,4 @@ export const useActiveTabOptional = () => {
+
diff --git a/frontend/hooks/useAutoFill.ts b/frontend/hooks/useAutoFill.ts
index 01ec7c34..4aeb7b38 100644
--- a/frontend/hooks/useAutoFill.ts
+++ b/frontend/hooks/useAutoFill.ts
@@ -200,3 +200,4 @@ export function applyAutoFillToFormData(
+
diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx
index a66a921e..31861599 100644
--- a/frontend/lib/registry/DynamicComponentRenderer.tsx
+++ b/frontend/lib/registry/DynamicComponentRenderer.tsx
@@ -244,7 +244,11 @@ export const DynamicComponentRenderer: React.FC =
step: config.step,
buttonText: config.buttonText,
buttonVariant: config.buttonVariant,
+ autoGeneration: config.autoGeneration,
}}
+ autoGeneration={config.autoGeneration}
+ formData={props.formData}
+ originalData={props.originalData}
/>
);
diff --git a/frontend/lib/utils/autoGeneration.ts b/frontend/lib/utils/autoGeneration.ts
index 0d225cdd..d9124bf9 100644
--- a/frontend/lib/utils/autoGeneration.ts
+++ b/frontend/lib/utils/autoGeneration.ts
@@ -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): Promise {
+ 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): Promise {
+ 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);
diff --git a/frontend/types/numbering-rule.ts b/frontend/types/numbering-rule.ts
index c44e2354..4f086b5e 100644
--- a/frontend/types/numbering-rule.ts
+++ b/frontend/types/numbering-rule.ts
@@ -51,6 +51,9 @@ export interface NumberingRulePart {
// 날짜용
dateFormat?: DateFormat; // 날짜 형식
+ useColumnValue?: boolean; // 컬럼 값 기준 생성 여부
+ sourceTableName?: string; // 소스 테이블명
+ sourceColumnName?: string; // 소스 컬럼명 (날짜 컬럼)
// 문자용
textValue?: string; // 텍스트 값 (예: "PRJ", "CODE")
diff --git a/frontend/types/screen.ts b/frontend/types/screen.ts
index c20167b8..20347005 100644
--- a/frontend/types/screen.ts
+++ b/frontend/types/screen.ts
@@ -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; // 날짜 컬럼명 (채번 규칙에서 날짜 기반 생성 시)
};
}
diff --git a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md
index 7150b942..82dc47d6 100644
--- a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md
+++ b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md
@@ -1692,3 +1692,4 @@ const 출고등록_설정: ScreenSplitPanel = {
+
diff --git a/화면_임베딩_시스템_Phase1-4_구현_완료.md b/화면_임베딩_시스템_Phase1-4_구현_완료.md
index 2d935417..03605468 100644
--- a/화면_임베딩_시스템_Phase1-4_구현_완료.md
+++ b/화면_임베딩_시스템_Phase1-4_구현_완료.md
@@ -539,3 +539,4 @@ const { data: config } = await getScreenSplitPanel(screenId);
+
diff --git a/화면_임베딩_시스템_충돌_분석_보고서.md b/화면_임베딩_시스템_충돌_분석_보고서.md
index 8be3e9cc..c7c676c1 100644
--- a/화면_임베딩_시스템_충돌_분석_보고서.md
+++ b/화면_임베딩_시스템_충돌_분석_보고서.md
@@ -526,3 +526,4 @@ function ScreenViewPage() {
+