diff --git a/backend-spring/src/main/java/com/erp/service/DdlService.java b/backend-spring/src/main/java/com/erp/service/DdlService.java index 182174d5..543983c2 100644 --- a/backend-spring/src/main/java/com/erp/service/DdlService.java +++ b/backend-spring/src/main/java/com/erp/service/DdlService.java @@ -39,6 +39,12 @@ public class DdlService extends BaseService { "id", "created_date", "updated_date", "company_code" ); + /** 사용자가 신규 추가하는 컬럼에 허용되는 INPUT_TYPE 8종 (백엔드 백스톱) */ + private static final Set USER_SELECTABLE_INPUT_TYPES = Set.of( + "text", "number", "date", "code", "entity", + "numbering", "file", "image" + ); + public DdlService(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) { this.jdbcTemplate = jdbcTemplate; this.transactionTemplate = new TransactionTemplate(transactionManager); @@ -140,6 +146,12 @@ public class DdlService extends BaseService { transactionTemplate.execute(status -> { jdbcTemplate.execute(ddlQuery); String inputType = convertToInputType(column); + if (!USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { + throw new IllegalArgumentException( + "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES + + " (받은 값: " + inputType + ")" + ); + } String detailSettings = column.containsKey("detail_settings") ? column.get("detail_settings").toString() : "{}"; Integer maxOrder = jdbcTemplate.queryForObject( @@ -408,10 +420,17 @@ public class DdlService extends BaseService { // 사용자 정의 컬럼 for (int i = 0; i < columns.size(); i++) { Map col = columns.get(i); + String inputType = convertToInputType(col); + if (!USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { + throw new IllegalArgumentException( + "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES + + " (받은 값: " + inputType + ")" + ); + } String detailSettings = col.containsKey("detail_settings") ? col.get("detail_settings").toString() : "{}"; saveColumnMeta(tableName, (String) col.get("name"), companyCode, - convertToInputType(col), detailSettings, i); + inputType, detailSettings, i); } } diff --git a/backend-spring/src/main/java/com/erp/service/TableManagementService.java b/backend-spring/src/main/java/com/erp/service/TableManagementService.java index eb8d0d1e..832e3d4a 100644 --- a/backend-spring/src/main/java/com/erp/service/TableManagementService.java +++ b/backend-spring/src/main/java/com/erp/service/TableManagementService.java @@ -26,6 +26,12 @@ public class TableManagementService extends BaseService { private static final String NS = "tableManagement."; + /** 사용자가 직접 선택 가능한 INPUT_TYPE 8종 (INSERT/UPDATE-type 검증용) */ + private static final Set USER_SELECTABLE_INPUT_TYPES = Set.of( + "text", "number", "date", "code", "entity", + "numbering", "file", "image" + ); + // ────────────────────────────────────────────────── // 테이블 목록 // ────────────────────────────────────────────────── @@ -145,7 +151,9 @@ public class TableManagementService extends BaseService { Map settings, String companyCode) { ensureTableInLabels(tableName); - String inputType = normalizeInputType((String) settings.get("input_type")); + boolean inputTypeChanged = settings.containsKey("input_type"); + String ctx = inputTypeChanged ? "user-update-type" : "user-update-other"; + String inputType = normalizeInputType((String) settings.get("input_type"), ctx); Map params = new HashMap<>(); params.put("table_name", tableName); params.put("column_name", columnName); @@ -202,7 +210,7 @@ public class TableManagementService extends BaseService { public void updateColumnInputType(String tableName, String columnName, String inputType, String companyCode, Map detailSettings) { - String finalType = normalizeInputType(inputType); + String finalType = normalizeInputType(inputType, "user-update-type"); Map params = new HashMap<>(); params.put("table_name", tableName); params.put("column_name", columnName); @@ -853,7 +861,7 @@ public class TableManagementService extends BaseService { return name.replaceAll("[^a-zA-Z0-9_]", ""); } - /** "direct" / "auto" → "text" 변환 */ + /** "direct" / "auto" → "text" 변환 (legacy 호출처 보호 — system-normalize 동작) */ private String normalizeInputType(String inputType) { if ("direct".equals(inputType) || "auto".equals(inputType)) { log.warn("잘못된 inputType 값 감지: {} → 'text'로 변환", inputType); @@ -862,6 +870,24 @@ public class TableManagementService extends BaseService { return inputType != null ? inputType : "text"; } + /** + * context 에 따라 INPUT_TYPE 정규화 및 검증. + * @param context "user-insert" | "user-update-type" | "user-update-other" | "system-normalize" + */ + private String normalizeInputType(String value, String context) { + if ("user-insert".equals(context) || "user-update-type".equals(context)) { + if (value == null || !USER_SELECTABLE_INPUT_TYPES.contains(value)) { + throw new IllegalArgumentException( + "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES + + " (받은 값: " + value + ")" + ); + } + return value; + } + // user-update-other / system-normalize: 기존 동작 그대로 + return normalizeInputType(value); + } + private String toJsonString(Object obj) { if (obj == null) return "{}"; if (obj instanceof String s) return s.isBlank() ? "{}" : s; diff --git a/docker/deploy/frontend.Dockerfile b/docker/deploy/frontend.Dockerfile index 46490573..6c0736f5 100644 --- a/docker/deploy/frontend.Dockerfile +++ b/docker/deploy/frontend.Dockerfile @@ -34,6 +34,7 @@ RUN echo "Build SHA: $GIT_SHA" # Build the application ENV DISABLE_ESLINT_PLUGIN=true +ENV NODE_OPTIONS=--max-old-space-size=4096 RUN npm run build # Production image, copy all the files and run next diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index bea0953f..cc8cb6d3 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -28,7 +28,7 @@ import { showErrorToast } from "@/lib/utils/toastUtils"; import { useMultiLang } from "@/hooks/useMultiLang"; import { useAuth } from "@/hooks/useAuth"; import { TABLE_MANAGEMENT_KEYS } from "@/constants/tableManagement"; -import { INPUT_TYPE_OPTIONS } from "@/types/input-types"; +import { INPUT_TYPE_OPTIONS, USER_SELECTABLE_INPUT_TYPE_ORDER } from "@/types/input-types"; import { apiClient } from "@/lib/api/client"; import { commonCodeApi } from "@/lib/api/commonCode"; import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin"; @@ -203,12 +203,14 @@ export default function TableManagementPage() { [], // 의존성 배열에서 referenceTableColumns 제거 ); - // 입력 타입 옵션 (8개 핵심 타입) - const inputTypeOptions = INPUT_TYPE_OPTIONS.map((option) => ({ - value: option.value, - label: option.label, - description: option.description, - })); + // 입력 타입 옵션 (8개 사용자 선택 가능 타입 — Layer 2) + const inputTypeOptions = INPUT_TYPE_OPTIONS + .filter((o) => USER_SELECTABLE_INPUT_TYPE_ORDER.includes(o.value as any)) + .map((option) => ({ + value: option.value, + label: option.label, + description: option.description, + })); // 메모이제이션된 입력타입 옵션 const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []); diff --git a/frontend/components/admin/AddColumnModal.tsx b/frontend/components/admin/AddColumnModal.tsx index bf603d86..e5aedceb 100644 --- a/frontend/components/admin/AddColumnModal.tsx +++ b/frontend/components/admin/AddColumnModal.tsx @@ -31,7 +31,7 @@ import { RESERVED_WORDS, RESERVED_COLUMNS, } from "../../types/ddl"; -import { INPUT_TYPE_OPTIONS } from "../../types/input-types"; +import { INPUT_TYPE_OPTIONS, USER_SELECTABLE_INPUT_TYPE_ORDER } from "../../types/input-types"; export function AddColumnModal({ isOpen, onClose, table_name, onSuccess }: AddColumnModalProps) { const [column, setColumn] = useState({ @@ -247,16 +247,18 @@ export function AddColumnModal({ isOpen, onClose, table_name, onSuccess }: AddCo - {INPUT_TYPE_OPTIONS.map((option) => ( - -
-
{option.label}
- {option.description && ( -
{option.description}
- )} -
-
- ))} + {INPUT_TYPE_OPTIONS + .filter((o) => USER_SELECTABLE_INPUT_TYPE_ORDER.includes(o.value as any)) + .map((option) => ( + +
+
{option.label}
+ {option.description && ( +
{option.description}
+ )} +
+
+ ))}
diff --git a/frontend/components/admin/ColumnDefinitionTable.tsx b/frontend/components/admin/ColumnDefinitionTable.tsx index dee7761a..60f5daed 100644 --- a/frontend/components/admin/ColumnDefinitionTable.tsx +++ b/frontend/components/admin/ColumnDefinitionTable.tsx @@ -21,7 +21,7 @@ import { RESERVED_WORDS, RESERVED_COLUMNS, } from "../../types/ddl"; -import { INPUT_TYPE_OPTIONS } from "../../types/input-types"; +import { INPUT_TYPE_OPTIONS, USER_SELECTABLE_INPUT_TYPE_ORDER } from "../../types/input-types"; export function ColumnDefinitionTable({ columns, onChange, disabled = false }: ColumnDefinitionTableProps) { const [validationErrors, setValidationErrors] = useState>({}); @@ -228,16 +228,18 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C - {INPUT_TYPE_OPTIONS.map((option) => ( - -
-
{option.label}
- {option.description && ( -
{option.description}
- )} -
-
- ))} + {INPUT_TYPE_OPTIONS + .filter((o) => USER_SELECTABLE_INPUT_TYPE_ORDER.includes(o.value as any)) + .map((option) => ( + +
+
{option.label}
+ {option.description && ( +
{option.description}
+ )} +
+
+ ))}
diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx index fc8da16b..5e400918 100644 --- a/frontend/components/admin/table-type/ColumnDetailPanel.tsx +++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx @@ -17,10 +17,12 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Badge } from "@/components/ui/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { Check, ChevronsUpDown } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Check, ChevronsUpDown, AlertTriangle } from "lucide-react"; import { cn } from "@/lib/utils"; import type { ColumnTypeInfo, TableInfo, SecondLevelMenu } from "./types"; -import { INPUT_TYPE_COLORS } from "./types"; +import { INPUT_TYPE_COLORS, USER_SELECTABLE_INPUT_TYPE_COLORS } from "./types"; +import { USER_SELECTABLE_INPUT_TYPE_ORDER, isUserSelectableInputType } from "@/types/input-types"; import type { ReferenceTableColumn } from "@/lib/api/entityJoin"; export interface ColumnDetailPanelProps { @@ -54,6 +56,7 @@ export function ColumnDetailPanel({ const [entityColumnOpen, setEntityColumnOpen] = React.useState(false); const typeConf = column ? INPUT_TYPE_COLORS[column.input_type || "text"] : null; + const isLegacy = column ? !isUserSelectableInputType(column.input_type || "text") : false; const refColumns = column?.reference_table ? referenceTableColumns[column.reference_table] ?? [] : []; @@ -131,19 +134,43 @@ export function ColumnDetailPanel({

이 필드는 어떤 유형인가요?

유형에 따라 입력 방식이 바뀌어요

-
- {Object.entries(INPUT_TYPE_COLORS).map(([type, conf]) => { + {isLegacy && ( + + + Legacy 입력 타입 + + 이 컬럼은 legacy 타입 {column.input_type} 입니다. + 변경하려면 컬럼을 삭제 후 재생성하세요. 다른 속성(길이/필수 등)은 정상 편집 가능합니다. + + + )} +
+ {USER_SELECTABLE_INPUT_TYPE_ORDER.map((type) => { + const conf = USER_SELECTABLE_INPUT_TYPE_COLORS[type]; + if (!conf) return null; const isSelected = (column.input_type || "text") === type; return (
- {/* 타입 칩 목록 (클릭 시 필터 토글) */} + {/* 타입 칩 목록 (8개 사용자 선택 가능 타입 한정, 클릭 시 필터 토글) */}
- {Object.entries(counts) - .sort((a, b) => (b[1] ?? 0) - (a[1] ?? 0)) - .map(([type]) => { + {(USER_SELECTABLE_INPUT_TYPE_ORDER as readonly string[]) + .filter((type) => (counts[type] || 0) > 0) + .sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0)) + .map((type) => { const conf = INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted", label: type }; const isActive = activeFilter === null || activeFilter === type; return ( diff --git a/frontend/components/admin/table-type/types.ts b/frontend/components/admin/table-type/types.ts index cb3f58e6..d7837aad 100644 --- a/frontend/components/admin/table-type/types.ts +++ b/frontend/components/admin/table-type/types.ts @@ -3,6 +3,8 @@ * page.tsx에서 추출한 인터페이스 및 타입별 색상/그룹 유틸 */ +import { USER_SELECTABLE_INPUT_TYPE_ORDER } from "@/types/input-types"; + export interface TableInfo { table_name: string; display_name: string; @@ -70,6 +72,15 @@ export const INPUT_TYPE_COLORS: Record = { image: { color: "text-sky-600", bgColor: "bg-sky-50", barColor: "bg-sky-500", label: "이미지", desc: "이미지 표시", iconChar: "🖼" }, }; +/** v3.2 — 사용자 선택 가능한 8개 입력 타입 색상 맵 (T2 드롭다운/카드 그리드용) */ +export const USER_SELECTABLE_INPUT_TYPE_COLORS = USER_SELECTABLE_INPUT_TYPE_ORDER.reduce( + (acc, key) => { + if (INPUT_TYPE_COLORS[key]) acc[key] = INPUT_TYPE_COLORS[key]; + return acc; + }, + {} as Record, +); + /** 컬럼 그룹 판별 */ export function getColumnGroup(col: ColumnTypeInfo): ColumnGroup { const metaCols = ["id", "created_date", "updated_date", "writer", "company_code"]; diff --git a/frontend/components/screen/TableSettingModal.tsx b/frontend/components/screen/TableSettingModal.tsx index 3ce154dc..62e18d34 100644 --- a/frontend/components/screen/TableSettingModal.tsx +++ b/frontend/components/screen/TableSettingModal.tsx @@ -80,7 +80,7 @@ import { } from "@/lib/api/screenGroup"; import { tableManagementApi, ColumnTypeInfo, TableInfo, ColumnSettings } from "@/lib/api/tableManagement"; import { screenApi } from "@/lib/api/screen"; -import { INPUT_TYPE_OPTIONS } from "@/types/input-types"; +import { INPUT_TYPE_OPTIONS, USER_SELECTABLE_INPUT_TYPE_ORDER } from "@/types/input-types"; import TableManagementPage from "@/app/(main)/admin/systemMng/tableMngList/page"; // ============================================================ @@ -621,13 +621,15 @@ export function TableSettingModal({ [tables] ); - // 입력 타입 옵션 + // 입력 타입 옵션 (8개 사용자 선택 가능 — 박창현 Q4: 일관성) const inputTypeOptions = useMemo( () => - INPUT_TYPE_OPTIONS.map((opt) => ({ - value: opt.value, - label: opt.label, - })), + INPUT_TYPE_OPTIONS + .filter((opt) => USER_SELECTABLE_INPUT_TYPE_ORDER.includes(opt.value as any)) + .map((opt) => ({ + value: opt.value, + label: opt.label, + })), [] ); diff --git a/frontend/lib/utils/getDetailType.ts b/frontend/lib/utils/getDetailType.ts new file mode 100644 index 00000000..dc3b2f08 --- /dev/null +++ b/frontend/lib/utils/getDetailType.ts @@ -0,0 +1,18 @@ +import { + INPUT_TYPE_DETAIL_TYPES, + type UserSelectableInputType, + type WidgetVariantOption, + isUserSelectableInputType, +} from "@/types/input-type-mapping"; + +/** base 가 8개 안에 있을 때만 variant 목록 반환 */ +export function getWidgetVariants(baseInputType: string): WidgetVariantOption[] { + if (!isUserSelectableInputType(baseInputType)) return []; + return INPUT_TYPE_DETAIL_TYPES[baseInputType as UserSelectableInputType] ?? []; +} + +/** base 의 기본 variant 반환 (variant 목록의 첫 번째) */ +export function getDefaultWidgetVariant(baseInputType: string): string { + const variants = getWidgetVariants(baseInputType); + return variants[0]?.value ?? baseInputType; +} diff --git a/frontend/types/input-type-mapping.ts b/frontend/types/input-type-mapping.ts index fb2fd5d9..a38fb56b 100644 --- a/frontend/types/input-type-mapping.ts +++ b/frontend/types/input-type-mapping.ts @@ -1,194 +1,186 @@ -/** - * 입력 타입(Input Type)과 세부 타입(Detail Type) 매핑 정의 - * - * 테이블 타입 관리의 8개 핵심 입력 타입을 기반으로 - * 화면 관리에서 선택 가능한 세부 타입들을 정의합니다. - */ +/** 박창현 image 2 의 8개 — 사용자가 테이블 타입 관리에서 직접 고를 수 있는 base */ +export type UserSelectableInputType = + | "text" | "number" | "date" + | "code" | "entity" + | "numbering" | "file" | "image"; -import { WebType } from "./v2-core"; - -/** - * 핵심 입력 타입 - */ -export type BaseInputType = - | "text" // 텍스트 - | "textarea" // 텍스트 에리어 (여러 줄) - | "number" // 숫자 - | "date" // 날짜 - | "code" // 코드 - | "entity" // 엔티티 - | "select" // 선택박스 - | "checkbox" // 체크박스 - | "radio" // 라디오버튼 - | "image"; // 이미지 - -/** - * 세부 타입 옵션 인터페이스 - */ -export interface DetailTypeOption { - value: WebType; +/** 화면관리 widget 의 세부 variant */ +export interface WidgetVariantOption { + value: string; label: string; description: string; } /** - * 입력 타입별 세부 타입 매핑 + * 8개 base 별 widget variant 매핑. + * vexplor_rps `input-type-mapping.ts:37-102` 의 variant 그대로 포팅. + * 박창현 결정 (Q3): select/checkbox/radio base 의 variant 는 모두 code base 로 흡수. */ -export const INPUT_TYPE_DETAIL_TYPES: Record = { - // 텍스트 → text, email, tel, url, password +export const INPUT_TYPE_DETAIL_TYPES: Record = { text: [ { value: "text", label: "일반 텍스트", description: "기본 텍스트 입력" }, { value: "email", label: "이메일", description: "이메일 주소 입력" }, { value: "tel", label: "전화번호", description: "전화번호 입력" }, { value: "url", label: "URL", description: "웹사이트 주소 입력" }, - { value: "password", label: "비밀번호", description: "비밀번호 입력 (마스킹)" }, + { value: "password", label: "비밀번호", description: "마스킹 입력" }, + { value: "textarea", label: "여러 줄 텍스트", description: "긴 텍스트 영역" }, ], - - // 텍스트 에리어 → textarea - textarea: [{ value: "textarea", label: "텍스트 에리어", description: "여러 줄 텍스트 입력" }], - - // 숫자 → number, decimal, currency, percentage number: [ { value: "number", label: "정수", description: "정수 숫자 입력" }, - { value: "decimal", label: "소수", description: "소수점 포함 숫자 입력" }, - { value: "currency", label: "통화", description: "통화 형식 (₩ 1,000)" }, - { value: "percentage", label: "퍼센트", description: "퍼센트 형식 (50%)" }, + { value: "decimal", label: "소수", description: "소수점 포함" }, + { value: "currency", label: "통화", description: "₩ 1,000 형식" }, + { value: "percentage", label: "퍼센트", description: "50% 형식" }, ], - - // 날짜 → date, datetime, time, daterange, month, year date: [ - { value: "date", label: "날짜", description: "날짜 선택 (YYYY-MM-DD)" }, - { value: "datetime", label: "날짜+시간", description: "날짜와 시간 선택" }, - { value: "time", label: "시간", description: "시간 선택 (HH:mm)" }, + { value: "date", label: "날짜", description: "YYYY-MM-DD" }, + { value: "datetime", label: "날짜+시간", description: "날짜와 시간" }, + { value: "time", label: "시간", description: "HH:mm" }, { value: "daterange", label: "기간", description: "시작일 ~ 종료일" }, - { value: "month", label: "월", description: "년/월 선택 (YYYY-MM)" }, - { value: "year", label: "년", description: "년도 선택 (YYYY)" }, + { value: "month", label: "월", description: "YYYY-MM" }, + { value: "year", label: "년", description: "YYYY" }, ], - - // 코드 → code, code-autocomplete, code-radio code: [ { value: "code", label: "코드 선택박스", description: "드롭다운으로 코드 선택" }, { value: "code-autocomplete", label: "코드 자동완성", description: "코드/코드명 검색" }, - { value: "code-radio", label: "코드 라디오", description: "라디오 버튼으로 선택" }, + { value: "code-radio", label: "코드 라디오", description: "라디오 버튼 그룹" }, + { value: "code-radio-horizontal", label: "가로 라디오", description: "가로 배치 라디오" }, + { value: "code-radio-vertical", label: "세로 라디오", description: "세로 배치 라디오" }, + { value: "dropdown", label: "검색 선택박스", description: "검색 기능 포함 (vexplor_rps select base)" }, + { value: "multiselect", label: "다중 선택", description: "여러 항목 선택 (vexplor_rps select base)" }, + { value: "autocomplete", label: "자동완성", description: "입력하면 자동완성 제안 (vexplor_rps select base)" }, + { value: "checkbox", label: "체크박스", description: "단일 체크박스 (vexplor_rps checkbox base)" }, + { value: "checkbox-group", label: "체크박스 그룹", description: "여러 체크박스 (vexplor_rps checkbox base)" }, + { value: "boolean", label: "On/Off 스위치", description: "boolean 스위치 (vexplor_rps checkbox base)" }, ], - - // 엔티티 → entity (세부 타입 없음, 참조 테이블만 선택) - entity: [{ value: "entity", label: "엔티티 참조", description: "다른 테이블 데이터 참조" }], - - // 선택박스 → select, dropdown, multiselect, autocomplete - select: [ - { value: "select", label: "선택박스", description: "드롭다운 선택 (단일)" }, - { value: "dropdown", label: "검색 선택박스", description: "검색 기능 포함" }, - { value: "multiselect", label: "다중 선택", description: "여러 항목 선택 (태그)" }, - { value: "autocomplete", label: "자동완성", description: "입력하면 자동완성 제안" }, + entity: [ + { value: "entity", label: "엔티티 참조", description: "다른 테이블 데이터 참조" }, + { value: "entity-autocomplete", label: "엔티티 자동완성", description: "검색 결과 inline 표시" }, + { value: "entity-popup", label: "엔티티 팝업", description: "별도 검색 팝업" }, ], - - // 체크박스 → checkbox, boolean, checkbox-group - checkbox: [ - { value: "checkbox", label: "체크박스", description: "단일 체크박스" }, - { value: "boolean", label: "스위치", description: "On/Off 스위치" }, - { value: "checkbox-group", label: "체크박스 그룹", description: "여러 체크박스" }, + file: [ + { value: "file", label: "단일 파일", description: "파일 1개 첨부" }, + { value: "file-list", label: "다중 파일", description: "여러 파일 첨부" }, + { value: "file-drop", label: "드래그 앤 드롭", description: "drop zone 형태" }, ], - - // 라디오버튼 → radio, radio-horizontal, radio-vertical - radio: [ - { value: "radio", label: "라디오버튼", description: "기본 라디오 버튼" }, - { value: "radio-horizontal", label: "가로 라디오", description: "가로 배치" }, - { value: "radio-vertical", label: "세로 라디오", description: "세로 배치" }, + image: [ + { value: "image", label: "이미지", description: "이미지 1장 표시/업로드" }, + { value: "image-gallery", label: "이미지 갤러리", description: "여러 장 grid 표시" }, + { value: "image-upload", label: "이미지 업로드", description: "크롭/리사이즈 포함" }, + ], + numbering: [ + { value: "numbering", label: "자동 채번", description: "옵션설정의 채번 규칙으로 자동 생성" }, ], - - // 이미지 → image - image: [{ value: "image", label: "이미지", description: "이미지 URL 표시" }], }; -/** - * 웹타입에서 기본 입력 타입 추출 - */ -export function getBaseInputType(webType: WebType): BaseInputType { - // textarea (별도 타입으로 분리) - if (webType === "textarea") { - return "textarea"; - } +/** 8개 base 의 한글 라벨 */ +export const USER_SELECTABLE_INPUT_TYPE_LABELS: Record = { + text: "텍스트", + number: "숫자", + date: "날짜", + code: "코드", + entity: "테이블참조", + numbering: "채번", + file: "파일", + image: "이미지", +}; - // text 계열 - if (["text", "email", "tel", "url", "password"].includes(webType)) { - return "text"; - } +/** 8개 base 의 순서 (드롭다운/카드 표시 순서) */ +export const USER_SELECTABLE_INPUT_TYPE_ORDER: UserSelectableInputType[] = [ + "text", "number", "date", + "code", "entity", + "numbering", "file", "image", +]; - // number 계열 - if (["number", "decimal", "currency", "percentage"].includes(webType)) { - return "number"; - } - - // date 계열 - if (["date", "datetime", "time", "daterange", "month", "year"].includes(webType)) { - return "date"; - } - - // code 계열 - if (["code", "code-autocomplete", "code-radio"].includes(webType)) { - return "code"; - } - - // select 계열 - if (["select", "dropdown", "multiselect", "autocomplete"].includes(webType)) { - return "select"; - } - - // checkbox 계열 - if (["checkbox", "boolean", "checkbox-group"].includes(webType)) { - return "checkbox"; - } - - // radio 계열 - if (["radio", "radio-horizontal", "radio-vertical"].includes(webType)) { - return "radio"; - } - - // entity - if (webType === "entity") return "entity"; - - // image - if (webType === "image") return "image"; - - // 기본값: text - return "text"; +export function isUserSelectableInputType(value: string): value is UserSelectableInputType { + return USER_SELECTABLE_INPUT_TYPE_ORDER.includes(value as UserSelectableInputType); } -/** - * 입력 타입에 해당하는 세부 타입 목록 가져오기 - */ -export function getDetailTypes(baseInputType: BaseInputType): DetailTypeOption[] { - return INPUT_TYPE_DETAIL_TYPES[baseInputType] || []; -} +// ───────────────────────────────────────────────────────────────────────── +// Backward Shim — 사용처 3곳 (V2PropertiesPanel/PropertiesPanel/DetailSettingsPanel) 임시 호환 +// ───────────────────────────────────────────────────────────────────────── /** - * 입력 타입에 해당하는 기본 세부 타입 가져오기 + * @deprecated v3.2: BaseInputType 은 UserSelectableInputType (8개) 로 대체. + * 이 export 는 backward shim — 사용처 3곳 임시 호환. 후속 PR 에서 strangle. */ -export function getDefaultDetailType(baseInputType: BaseInputType): WebType { - const detailTypes = getDetailTypes(baseInputType); - return detailTypes[0]?.value || "text"; -} +export type BaseInputType = + | UserSelectableInputType + | "textarea" + | "select" + | "checkbox" + | "radio"; /** - * 입력 타입 옵션 (PropertiesPanel에서 사용) + * @deprecated v3.2: WidgetVariantOption 로 대체. + * 사용처 3곳의 DetailTypeOption import 호환용. + */ +export type DetailTypeOption = WidgetVariantOption; + +/** + * @deprecated v3.2: UserSelectableInputType 의 8개로 사용 권장. + * 사용처 3곳의 BASE_INPUT_TYPE_OPTIONS import 호환용. */ export const BASE_INPUT_TYPE_OPTIONS: Array<{ value: BaseInputType; label: string; description: string }> = [ { value: "text", label: "텍스트", description: "텍스트 입력 필드" }, - { value: "textarea", label: "텍스트 에리어", description: "여러 줄 텍스트 입력" }, + { value: "textarea", label: "텍스트 에리어", description: "여러 줄 텍스트 입력 (legacy — text base 의 variant)" }, { value: "number", label: "숫자", description: "숫자 입력 필드" }, { value: "date", label: "날짜", description: "날짜/시간 선택" }, { value: "code", label: "코드", description: "공통 코드 선택" }, { value: "entity", label: "엔티티", description: "다른 테이블 참조" }, - { value: "select", label: "선택박스", description: "드롭다운 선택" }, - { value: "checkbox", label: "체크박스", description: "체크박스/스위치" }, - { value: "radio", label: "라디오버튼", description: "라디오 버튼 그룹" }, + { value: "select", label: "선택박스", description: "드롭다운 선택 (legacy — code base 의 variant)" }, + { value: "checkbox", label: "체크박스", description: "체크박스/스위치 (legacy — code base 의 variant)" }, + { value: "radio", label: "라디오버튼", description: "라디오 버튼 그룹 (legacy — code base 의 variant)" }, { value: "image", label: "이미지", description: "이미지 표시" }, + { value: "numbering", label: "자동 채번", description: "옵션설정 기반 자동 번호" }, + { value: "file", label: "파일", description: "파일 업로드" }, ]; /** - * 입력 타입 검증 + * @deprecated v3.2: webType → UserSelectableInputType 로 매핑 권장 (`getUserSelectableInputType`). + * 사용처 3곳의 getBaseInputType 호환용. */ -export function isValidBaseInputType(value: string): value is BaseInputType { - return BASE_INPUT_TYPE_OPTIONS.some((opt) => opt.value === value); +export function getBaseInputType(webType: string): BaseInputType { + if (["text", "email", "tel", "url", "password"].includes(webType)) return "text"; + if (webType === "textarea") return "textarea"; + if (["number", "decimal", "currency", "percentage"].includes(webType)) return "number"; + if (["date", "datetime", "time", "daterange", "month", "year"].includes(webType)) return "date"; + if (["code", "code-autocomplete", "code-radio", "code-radio-horizontal", "code-radio-vertical"].includes(webType)) return "code"; + if (["select", "dropdown", "multiselect", "autocomplete"].includes(webType)) return "select"; + if (["checkbox", "boolean", "checkbox-group"].includes(webType)) return "checkbox"; + if (["radio", "radio-horizontal", "radio-vertical"].includes(webType)) return "radio"; + if (webType === "entity" || webType.startsWith("entity-")) return "entity"; + if (webType === "image" || webType.startsWith("image-")) return "image"; + if (webType === "file" || webType.startsWith("file-")) return "file"; + if (webType === "numbering") return "numbering"; + return "text"; +} + +/** 신규 권장 함수 — UserSelectableInputType 직접 반환 */ +export function getUserSelectableInputType(webType: string): UserSelectableInputType { + const base = getBaseInputType(webType); + if (base === "textarea") return "text"; + if (base === "select" || base === "checkbox" || base === "radio") return "code"; + return base as UserSelectableInputType; +} + +/** + * @deprecated v3.2: getWidgetVariants (lib/utils/getDetailType.ts) 권장. + * 사용처의 getDetailTypes import 호환용. + */ +export function getDetailTypes(baseInputType: BaseInputType): DetailTypeOption[] { + // legacy base → UserSelectableInputType 매핑 후 variant 조회 + let key: UserSelectableInputType; + if (baseInputType === "textarea") key = "text"; + else if (baseInputType === "select" || baseInputType === "checkbox" || baseInputType === "radio") key = "code"; + else key = baseInputType; + return INPUT_TYPE_DETAIL_TYPES[key] ?? []; +} + +/** + * @deprecated v3.2: getDefaultWidgetVariant (lib/utils/getDetailType.ts) 권장. + * 사용처의 getDefaultDetailType import 호환용. + */ +export function getDefaultDetailType(baseInputType: BaseInputType): string { + const variants = getDetailTypes(baseInputType); + return variants[0]?.value ?? baseInputType; } diff --git a/frontend/types/input-types.ts b/frontend/types/input-types.ts index e748efe7..22c4a4a5 100644 --- a/frontend/types/input-types.ts +++ b/frontend/types/input-types.ts @@ -311,3 +311,14 @@ export const INPUT_TYPE_VALIDATION_RULES: Record> autoGenerate: true, }, }; + +// ───────────────────────────────────────────────────────────────────────── +// v3.2 — Layer 2 사용자 선택 가능한 8개 (박창현 image 2) +// ───────────────────────────────────────────────────────────────────────── + +export { + USER_SELECTABLE_INPUT_TYPE_ORDER, + USER_SELECTABLE_INPUT_TYPE_LABELS, + isUserSelectableInputType, + type UserSelectableInputType, +} from "./input-type-mapping";