From d3701cfe1e73d66a13acd8f34a03b391e7643cad Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 19 Jan 2026 18:21:30 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=95=A0=EB=8B=B9=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=ED=8F=BC=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80:=20numberingRuleController?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=BD=94=EB=93=9C=20=ED=95=A0=EB=8B=B9=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=ED=8F=BC=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=ED=8F=AC=ED=95=A8=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=98=EC=98=80=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20=EC=9D=B4=EB=A5=BC=20=ED=86=B5=ED=95=B4?= =?UTF-8?q?=20=EB=82=A0=EC=A7=9C=20=EC=BB=AC=EB=9F=BC=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=A0=84=EB=8B=AC=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/numberingRuleController.ts | 5 +- .../src/routes/cascadingAutoFillRoutes.ts | 1 + .../src/routes/cascadingConditionRoutes.ts | 1 + .../src/routes/cascadingHierarchyRoutes.ts | 1 + .../routes/cascadingMutualExclusionRoutes.ts | 1 + .../src/services/masterDetailExcelService.ts | 9 +- .../src/services/nodeFlowExecutionService.ts | 4 +- .../src/services/numberingRuleService.ts | 47 +- docs/노드플로우_개선사항.md | 1 + docs/다국어_관리_시스템_개선_계획서.md | 1 + docs/메일발송_기능_사용_가이드.md | 1 + docs/즉시저장_버튼_액션_구현_계획서.md | 1 + docs/집계위젯_개발진행상황.md | 1 + .../numbering-rule/AutoConfigPanel.tsx | 359 ++++++++++- .../numbering-rule/NumberingRuleDesigner.tsx | 12 - .../numbering-rule/NumberingRulePreview.tsx | 16 + frontend/components/unified/UnifiedInput.tsx | 603 +++++++++++------- .../config-panels/UnifiedInputConfigPanel.tsx | 303 ++++++++- frontend/contexts/ActiveTabContext.tsx | 1 + frontend/hooks/useAutoFill.ts | 1 + .../lib/registry/DynamicComponentRenderer.tsx | 4 + frontend/lib/utils/autoGeneration.ts | 44 +- frontend/types/numbering-rule.ts | 3 + frontend/types/screen.ts | 20 +- ..._임베딩_및_데이터_전달_시스템_구현_계획서.md | 1 + 화면_임베딩_시스템_Phase1-4_구현_완료.md | 1 + 화면_임베딩_시스템_충돌_분석_보고서.md | 1 + 27 files changed, 1148 insertions(+), 295 deletions(-) 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 (