"use client"; /** * V2Input 설정 패널 * 토스식 단계별 UX: 기본 설정 -> 타입별 설정 -> 고급 설정(접힘) */ 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 { Switch } from "@/components/ui/switch"; import { Separator } from "@/components/ui/separator"; import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Settings, ChevronDown, Loader2, Type, Hash, Lock, AlignLeft, SlidersHorizontal, Palette, ListOrdered, } from "lucide-react"; import { cn } from "@/lib/utils"; 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 V2InputConfigPanelProps { config: Record; onChange: (config: Record) => void; menuObjid?: number; allComponents?: any[]; } export const V2InputConfigPanel: React.FC = ({ config, onChange, menuObjid, allComponents = [], }) => { const [numberingRules, setNumberingRules] = useState([]); const [loadingRules, setLoadingRules] = useState(false); const [parentMenus, setParentMenus] = useState([]); const [loadingMenus, setLoadingMenus] = useState(false); const [selectedMenuObjid, setSelectedMenuObjid] = useState(() => { return config.autoGeneration?.selectedMenuObjid || menuObjid; }); const [advancedOpen, setAdvancedOpen] = useState(false); const updateConfig = (field: string, value: any) => { onChange({ ...config, [field]: value }); }; 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; const userMenus = allMenus.filter((menu: any) => { const menuType = menu.menu_type || menu.menuType; const level = menu.level || menu.lev || menu.LEVEL; return menuType === "1" && (level === 2 || level === 3 || level === "2" || level === "3"); }); setParentMenus(userMenus); } } catch (error) { console.error("부모 메뉴 로드 실패:", error); } finally { setLoadingMenus(false); } }; loadMenus(); }, []); const inputType = config.inputType || config.type || "text"; useEffect(() => { const loadRules = async () => { const isNumbering = inputType === "numbering" || config.autoGeneration?.type === "numbering_rule"; if (!isNumbering) 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, inputType]); return (
{/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */}

입력 타입

입력 필드의 종류를 선택해요

{[ { value: "text", icon: Type, label: "텍스트", desc: "일반 텍스트 입력" }, { value: "number", icon: Hash, label: "숫자", desc: "숫자만 입력" }, { value: "password", icon: Lock, label: "비밀번호", desc: "마스킹 처리" }, { value: "textarea", icon: AlignLeft, label: "여러 줄", desc: "긴 텍스트 입력" }, { value: "slider", icon: SlidersHorizontal, label: "슬라이더", desc: "범위 선택" }, { value: "color", icon: Palette, label: "색상", desc: "색상 선택기" }, { value: "numbering", icon: ListOrdered, label: "채번", desc: "자동 번호 생성" }, ].map((item) => ( ))}
{/* ─── 채번 타입 전용 설정 ─── */} {inputType === "numbering" && (
채번 규칙

적용할 메뉴

{menuObjid && selectedMenuObjid === menuObjid ? (

현재 화면 메뉴 사용 중

{parentMenus.find((m: any) => m.objid === menuObjid)?.menu_name_kor || parentMenus.find((m: any) => m.objid === menuObjid)?.translated_name || `메뉴 #${menuObjid}`}

) : loadingMenus ? (
메뉴 목록 로딩 중...
) : ( )}
{selectedMenuObjid && (

채번 규칙

{loadingRules ? (
채번 규칙 로딩 중...
) : numberingRules.length > 0 ? ( ) : (

선택한 메뉴에 등록된 채번 규칙이 없어요

)}
)}

읽기전용

채번 필드는 자동 생성되므로 읽기전용을 권장해요

updateConfig("readonly", checked)} />
)} {/* ─── 채번 타입이 아닌 경우: 기본 설정 ─── */} {inputType !== "numbering" && ( <> {/* 기본 설정 영역 */}
{/* 안내 텍스트 (placeholder) */}
안내 텍스트 updateConfig("placeholder", e.target.value)} placeholder="입력 안내" className="h-7 w-[160px] text-xs" />
{/* 입력 형식 - 텍스트 타입 전용 */} {(inputType === "text" || !config.inputType) && (
입력 형식
)} {/* 입력 마스크 */}
입력 마스크

# = 숫자, A = 문자, * = 모두

updateConfig("mask", e.target.value)} placeholder="###-####-####" className="h-7 w-[160px] text-xs" />
{/* 숫자/슬라이더: 범위 설정 */} {(inputType === "number" || inputType === "slider") && (

값 범위

updateConfig("min", e.target.value ? Number(e.target.value) : undefined)} placeholder="0" className="h-7 text-xs" />
updateConfig("max", e.target.value ? Number(e.target.value) : undefined)} placeholder="100" className="h-7 text-xs" />
updateConfig("step", e.target.value ? Number(e.target.value) : undefined)} placeholder="1" className="h-7 text-xs" />
)} {/* 여러 줄 텍스트: 줄 수 */} {inputType === "textarea" && (
줄 수 updateConfig("rows", parseInt(e.target.value) || 3)} min={2} max={20} className="h-7 w-[160px] text-xs" />
)}
{/* ─── 고급 설정: 자동 생성 (Collapsible) ─── */}
{/* 자동 생성 토글 */}

자동 생성

값이 자동으로 채워져요

{ 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 ? (

채번 규칙 *

{loadingRules ? (
규칙 로딩 중...
) : ( )}
) : (
먼저 대상 메뉴를 선택하세요
)}
)} {/* 랜덤/순차 옵션 */} {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-7 w-[120px] text-xs" />
)}
접두사 { updateConfig("autoGeneration", { ...config.autoGeneration, options: { ...config.autoGeneration?.options, prefix: e.target.value, }, }); }} placeholder="예: INV-" className="h-7 w-[120px] text-xs" />
접미사 { updateConfig("autoGeneration", { ...config.autoGeneration, options: { ...config.autoGeneration?.options, suffix: e.target.value, }, }); }} className="h-7 w-[120px] text-xs" />
미리보기
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
)}
)}
)} {/* 데이터 바인딩 설정 */}
); }; V2InputConfigPanel.displayName = "V2InputConfigPanel"; /** * 데이터 바인딩 설정 섹션 * 같은 화면의 v2-table-list 컴포넌트를 자동 감지하여 드롭다운으로 표시 */ function DataBindingSection({ config, onChange, allComponents, }: { config: Record; onChange: (config: Record) => void; allComponents: any[]; }) { const [tableColumns, setTableColumns] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); // 같은 화면의 v2-table-list 컴포넌트만 필터링 const tableListComponents = React.useMemo(() => { return allComponents.filter((comp) => { const type = comp.componentType || comp.widgetType || comp.componentConfig?.type || (comp.url && comp.url.split("/").pop()); return type === "v2-table-list"; }); }, [allComponents]); // 선택된 테이블 컴포넌트의 테이블명 추출 const selectedTableComponent = React.useMemo(() => { if (!config.dataBinding?.sourceComponentId) return null; return tableListComponents.find((comp) => comp.id === config.dataBinding.sourceComponentId); }, [tableListComponents, config.dataBinding?.sourceComponentId]); const selectedTableName = React.useMemo(() => { if (!selectedTableComponent) return null; return selectedTableComponent.componentConfig?.selectedTable || selectedTableComponent.selectedTable || null; }, [selectedTableComponent]); // 선택된 테이블의 컬럼 목록 로드 useEffect(() => { if (!selectedTableName) { setTableColumns([]); return; } const loadColumns = async () => { setLoadingColumns(true); try { const { tableTypeApi } = await import("@/lib/api/screen"); const response = await tableTypeApi.getTableTypeColumns(selectedTableName); if (response.success && response.data) { const cols = response.data.map((col: any) => col.column_name).filter(Boolean); setTableColumns(cols); } } catch { // 컬럼 정보를 못 가져오면 테이블 컴포넌트의 columns에서 추출 const configColumns = selectedTableComponent?.componentConfig?.columns; if (Array.isArray(configColumns)) { setTableColumns(configColumns.map((c: any) => c.columnName).filter(Boolean)); } } finally { setLoadingColumns(false); } }; loadColumns(); }, [selectedTableName, selectedTableComponent]); const updateConfig = (field: string, value: any) => { onChange({ ...config, [field]: value }); }; return (
{ if (checked) { const firstTable = tableListComponents[0]; updateConfig("dataBinding", { sourceComponentId: firstTable?.id || "", sourceColumn: "", }); } else { updateConfig("dataBinding", undefined); } }} />
{config.dataBinding && (

테이블에서 행 선택 시 해당 컬럼 값이 자동으로 채워집니다

{/* 소스 테이블 컴포넌트 선택 */}
{tableListComponents.length === 0 ? (

이 화면에 v2-table-list 컴포넌트가 없습니다

) : ( )}
{/* 소스 컬럼 선택 */} {config.dataBinding?.sourceComponentId && (
{loadingColumns ? (

컬럼 로딩 중...

) : tableColumns.length === 0 ? ( <> { updateConfig("dataBinding", { ...config.dataBinding, sourceColumn: e.target.value, }); }} placeholder="컬럼명 직접 입력" className="h-7 text-xs" />

컬럼 정보를 불러올 수 없어 직접 입력

) : ( )}
)}
)}
); } export default V2InputConfigPanel;