feat(테이블타입): DBeaver 식 탭 분리 + 입력 타입 아이콘 lucide 통일 #24

Merged
johngreen merged 3 commits from johngreen into main 2026-05-21 10:22:55 +00:00
7 changed files with 430 additions and 132 deletions
+79
View File
@@ -1,3 +1,82 @@
<!-- User customizations -->
# 절대 규칙: 검증 없는 주장 금지
내가 출력하는 모든 발언은 근거가 있어야 한다. 근거가 없으면 그 말을 하지 않는다. 위로·추정·일반론·"보통 그렇다"로 채우지 않는다.
## 위반 사례 (절대 하지 말 것)
- "100명 중 5명도 안 된다" 같은 통계를 출처 없이 만들어내기
- "통과 확률 70~80%" 같은 수치를 추정으로 제시하기
- "보통", "일반적으로", "대부분" 으로 시작하는 일반론
- 본인이 검증 안 한 SDK/API 동작을 단정적으로 설명하기
- 위로·격려를 위해 사실이 아닌 것을 끼워넣기
## 발화 전 자기 검증
한 문장이라도 출력하기 전에 다음을 확인:
1. **출처가 있는가?** — 코드(파일:라인), 명령 결과, 공식 문서, 사용자가 준 정보, 도구 호출 결과 중 하나
2. **출처가 없다면 추정인가?** — 추정이면 명시적으로 "추정이지만…" 또는 "확인 안 됐지만…" 으로 시작
3. **추정도 근거가 없으면?** — 말하지 않는다. "모릅니다" 또는 "확인이 필요합니다" 라고 한다
## 모를 때의 정답
- 검색·문서 조회·코드 읽기로 확인 가능하면 확인부터 한다
- 확인이 불가능하면 "모릅니다" 가 정답. 그럴듯한 답을 만들지 않는다
- 사용자 의사결정에 영향을 주는 사실일수록 더 엄격하게 적용
## 어겼을 때
사용자가 "그 근거 뭐야" 라고 묻거나 잘못된 사실을 지적하면:
- 즉시 인정. "맞습니다. 그 수치 제가 지어냈습니다." 같이 명시적으로 시인
- 변명·재포장 금지
- 무엇이 검증된 사실이고 무엇이 추정/날조였는지 다시 분리해서 제시
# 💬 사용자에게 설명할 때 — 그림으로 (★ 중요)
UI 변경 제안, 디자인 토론, 코드 구조 설명 등을 할 때는 **반드시 변경 전/후를 ASCII 표나 도식으로 그려서** 보여준다. 글로만 설명하면 사용자가 이해 못 한다.
## 원칙
1. **변경 제안은 무조건 Before / After 두 그림**
2. **코드 인용 (file:line, 변수명, CSS class) 최소화** — 결론과 시각적 영향 위주
3. **평어, 한국어, 짧은 문장**
4. **영문/SQL/전문용어 풀어쓰기** — "grid template" 대신 "표 컬럼 배치", "stopPropagation" 대신 "클릭이 위로 새는 거 막기"
5. **3줄 패턴 권장** — 무슨 일 / 사용자한테 보이는 영향 / 어떻게 고치는지
## 나쁜 예시 ❌
> "ColumnGrid.tsx:93-103 의 `grid-cols-[4px_140px_1fr_100px_160px_40px]` 를 5컬럼으로 축소하고, 라벨 셀에 sub-line 을 추가하면 entity/code/numbering 의 메타가 inline 으로..."
(사용자: "뭐라는지 모르겠어")
## 좋은 예시 ⭕
> **지금 모양:**
> ```
> 라벨·컬럼명 │ 참조/설정 │ 타입
> 거래처명 │ — │ 텍스트 ← 빈 칸
> 거래처ID │ customer_mng → ... │ 테이블참조
> ```
>
> **바꿔서:**
> ```
> 라벨·컬럼명 │ 타입
> 거래처명 │ 텍스트
> 거래처ID │ 테이블참조
> → customer_mng.id ← 정보 있을 때만 작게 밑에
> ```
## 옵션 제시할 땐 표로
```
| 옵션 | 핵심 | 단점 |
| A안 | 이름만 바꾸기 | 가장 가벼움 |
| B안 | 그룹을 잘게 쪼개기 | 그룹 수 늘어남 |
```
## 우선 순위
- 첫 시도에 글만 쓰지 말 것. 그림부터 그리고 글은 짧게 보충.
- 사용자가 "무슨 말인지 모르겠어" 하면 → 더 분해해서 다시 그림 그리기. 글 길어지면 더 헷갈림.
---
# INVYONE — Claude 작업 컨벤션
이 파일은 git 에 올라가는 **프로젝트 공용** Claude 가이드입니다. 모든 머신/팀원의 Claude Code 인스턴스가 이 컨벤션을 따라야 합니다.
@@ -21,7 +21,10 @@ import {
ChevronsUpDown,
Loader2,
Pencil,
Columns3,
Link2,
} from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { toast } from "sonner";
@@ -56,6 +59,7 @@ import type { TableInfo, ColumnTypeInfo, SecondLevelMenu } from "@/components/ad
import { TypeOverviewStrip } from "@/components/admin/table-type/TypeOverviewStrip";
import { ColumnGrid } from "@/components/admin/table-type/ColumnGrid";
import { ColumnDetailPanel } from "@/components/admin/table-type/ColumnDetailPanel";
import { ReferenceListView } from "@/components/admin/table-type/ReferenceListView";
export default function TableManagementPage() {
const { userLang, getText } = useMultiLang({ companyCode: "*" });
@@ -1690,46 +1694,79 @@ export default function TableManagementPage() {
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
</div>
) : (
<>
<TypeOverviewStrip
columns={columns}
activeFilter={typeFilter}
onFilterChange={setTypeFilter}
/>
<ColumnGrid
columns={columns}
selectedColumn={selectedColumn}
onSelectColumn={(c) => setSelectedColumn((prev) => (prev === c ? null : c))}
onColumnChange={(columnName, field, value) => {
if (field === "is_unique") {
const currentColumn = columns.find((c) => c.column_name === columnName);
if (currentColumn) {
handleUniqueToggle(columnName, currentColumn.is_unique || "NO");
<Tabs defaultValue="columns" className="flex min-h-0 flex-1 flex-col">
<TabsList className="h-8 w-fit shrink-0 self-start mx-4 mt-1">
<TabsTrigger value="columns" className="flex items-center gap-1.5 text-xs">
<Columns3 className="h-3.5 w-3.5" />
</TabsTrigger>
<TabsTrigger value="references" className="flex items-center gap-1.5 text-xs">
<Link2 className="h-3.5 w-3.5" />
{(() => {
const refCount = columns.filter((c) =>
["entity", "code", "category", "numbering"].includes(c.input_type),
).length;
return refCount > 0 ? (
<Badge variant="secondary" className="ml-1 h-4 px-1.5 text-[10px]">
{refCount}
</Badge>
) : null;
})()}
</TabsTrigger>
</TabsList>
<TabsContent value="columns" className="mt-0 flex min-h-0 flex-1 flex-col">
<TypeOverviewStrip
columns={columns}
activeFilter={typeFilter}
onFilterChange={setTypeFilter}
/>
<ColumnGrid
columns={columns}
selectedColumn={selectedColumn}
onSelectColumn={(c) => setSelectedColumn((prev) => (prev === c ? null : c))}
onColumnChange={(columnName, field, value) => {
if (field === "is_unique") {
const currentColumn = columns.find((c) => c.column_name === columnName);
if (currentColumn) {
handleUniqueToggle(columnName, currentColumn.is_unique || "NO");
}
return;
}
return;
}
if (field === "is_nullable") {
const currentColumn = columns.find((c) => c.column_name === columnName);
if (currentColumn) {
handleNullableToggle(columnName, currentColumn.is_nullable || "YES");
if (field === "is_nullable") {
const currentColumn = columns.find((c) => c.column_name === columnName);
if (currentColumn) {
handleNullableToggle(columnName, currentColumn.is_nullable || "YES");
}
return;
}
return;
const idx = columns.findIndex((c) => c.column_name === columnName);
if (idx >= 0) handleColumnChange(idx, field, value);
}}
constraints={constraints}
typeFilter={typeFilter}
getColumnIndexState={getColumnIndexState}
onPkToggle={handlePkToggle}
onIndexToggle={(columnName, checked) =>
handleIndexToggle(columnName, "index", checked)
}
const idx = columns.findIndex((c) => c.column_name === columnName);
if (idx >= 0) handleColumnChange(idx, field, value);
}}
constraints={constraints}
typeFilter={typeFilter}
getColumnIndexState={getColumnIndexState}
onPkToggle={handlePkToggle}
onIndexToggle={(columnName, checked) =>
handleIndexToggle(columnName, "index", checked)
}
onDeleteColumn={handleDeleteColumnClick}
tables={tables}
referenceTableColumns={referenceTableColumns}
/>
</>
onDeleteColumn={handleDeleteColumnClick}
tables={tables}
referenceTableColumns={referenceTableColumns}
/>
</TabsContent>
<TabsContent value="references" className="mt-0 flex min-h-0 flex-1 flex-col">
<ReferenceListView
columns={columns}
tables={tables}
referenceTableColumns={referenceTableColumns}
selectedColumn={selectedColumn}
onSelectColumn={(c) => setSelectedColumn((prev) => (prev === c ? null : c))}
/>
</TabsContent>
</Tabs>
)}
</>
)}
@@ -183,12 +183,12 @@ export function ColumnDetailPanel({
isLegacy && "cursor-not-allowed",
)}
>
<span className={cn(
"text-base font-bold leading-none",
isSelected ? "text-primary" : conf.color,
)}>
{conf.iconChar}
</span>
<conf.Icon
className={cn(
"h-4 w-4",
isSelected ? "text-primary" : conf.color,
)}
/>
<span className={cn(
"text-[16px] font-semibold leading-tight",
isSelected ? "text-primary" : "text-foreground",
@@ -92,11 +92,10 @@ export function ColumnGrid({
<div className="flex flex-1 flex-col overflow-hidden">
<div
className="grid flex-shrink-0 items-center border-b bg-muted/50 px-4 py-2 text-xs font-semibold text-foreground"
style={{ gridTemplateColumns: "4px 140px 1fr 100px 160px 40px" }}
style={{ gridTemplateColumns: "4px 1fr 100px 160px 40px" }}
>
<span />
<span> · </span>
<span>/</span>
<span></span>
<span className="text-center">PK / NN / IDX / UQ</span>
<span />
@@ -142,7 +141,7 @@ export function ColumnGrid({
}}
className={cn(
"grid min-h-12 cursor-pointer items-center gap-2 rounded-md border px-4 py-2 transition-colors",
"grid-cols-[4px_140px_1fr_100px_160px_40px]",
"grid-cols-[4px_1fr_100px_160px_40px]",
"bg-card border-transparent hover:border-border hover:shadow-sm",
isSelected && "border-primary/30 bg-primary/5 shadow-sm",
)}
@@ -159,66 +158,6 @@ export function ColumnGrid({
</div>
</div>
{/* 참조/설정 칩 */}
<div className="flex min-w-0 flex-wrap gap-1">
{column.input_type === "entity" && column.reference_table && column.reference_table !== "none" && (
<>
<Badge
variant="outline"
className="text-xs font-normal"
title={
tables
? (() => {
const t = tables.find((tb) => tb.table_name === column.reference_table);
return t?.display_name && t.display_name !== t.table_name
? `${t.display_name} (${column.reference_table})`
: column.reference_table;
})()
: column.reference_table
}
>
{column.reference_table}
</Badge>
<span className="text-muted-foreground text-xs"></span>
<Badge
variant="outline"
className="text-xs font-normal"
title={
referenceTableColumns?.[column.reference_table]
? (() => {
const refCols = referenceTableColumns[column.reference_table];
const c = refCols.find((rc) => rc.column_name === (column.reference_column ?? ""));
return c?.display_name && c.display_name !== c.column_name
? `${c.display_name} (${column.reference_column})`
: column.reference_column ?? "—";
})()
: column.reference_column ?? "—"
}
>
{column.reference_column || "—"}
</Badge>
</>
)}
{column.input_type === "code" && (
<span className="text-muted-foreground truncate text-xs">
{column.code_info ?? "—"} · {column.default_value ?? ""}
</span>
)}
{column.input_type === "numbering" && column.numbering_rule_id && (
<Badge variant="outline" className="text-xs font-normal">
{column.numbering_rule_id}
</Badge>
)}
{column.input_type !== "entity" &&
column.input_type !== "code" &&
column.input_type !== "numbering" &&
(column.default_value ? (
<span className="text-muted-foreground truncate text-xs">{column.default_value}</span>
) : (
<span className="text-muted-foreground/60 text-xs"></span>
))}
</div>
{/* 타입 뱃지 */}
<div className={cn("rounded-md border px-2 py-0.5 text-xs", typeConf.bgColor, typeConf.color)}>
<span className="mr-1 inline-block h-1.5 w-1.5 rounded-full bg-current opacity-70" />
@@ -0,0 +1,223 @@
"use client";
import React, { useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { Database, FolderTree, Hash, Link2, FileCode2 } from "lucide-react";
import type { ColumnTypeInfo, TableInfo } from "./types";
import { INPUT_TYPE_COLORS } from "./types";
import type { ReferenceTableColumn } from "@/lib/api/entityJoin";
export interface ReferenceListViewProps {
columns: ColumnTypeInfo[];
tables?: TableInfo[];
referenceTableColumns?: Record<string, ReferenceTableColumn[]>;
onSelectColumn?: (columnName: string) => void;
selectedColumn?: string | null;
}
type RefKind = "entity" | "code" | "category" | "numbering";
const KIND_META: Record<
RefKind,
{ icon: React.FC<{ className?: string }>; label: string; color: string; bgColor: string }
> = {
entity: { icon: Link2, label: "테이블 참조", color: "text-violet-600", bgColor: "bg-violet-50" },
code: { icon: FileCode2, label: "공통코드", color: "text-emerald-600", bgColor: "bg-emerald-50" },
category: { icon: FolderTree, label: "카테고리", color: "text-teal-600", bgColor: "bg-teal-50" },
numbering: { icon: Hash, label: "채번", color: "text-orange-600", bgColor: "bg-orange-50" },
};
function getRefKind(col: ColumnTypeInfo): RefKind | null {
const t = col.input_type;
if (t === "entity" || t === "code" || t === "category" || t === "numbering") return t;
return null;
}
export function ReferenceListView({
columns,
tables,
referenceTableColumns,
onSelectColumn,
selectedColumn = null,
}: ReferenceListViewProps) {
const grouped = useMemo(() => {
const groups: Record<RefKind, ColumnTypeInfo[]> = {
entity: [],
code: [],
category: [],
numbering: [],
};
for (const col of columns) {
const kind = getRefKind(col);
if (kind) groups[kind].push(col);
}
return groups;
}, [columns]);
const totalRefs =
grouped.entity.length + grouped.code.length + grouped.category.length + grouped.numbering.length;
if (totalRefs === 0) {
return (
<div className="flex flex-1 items-center justify-center py-12 text-sm text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<Database className="h-8 w-8 text-muted-foreground/50" />
<span> .</span>
</div>
</div>
);
}
return (
<div className="flex flex-1 flex-col overflow-hidden">
{/* 헤더 */}
<div
className="grid flex-shrink-0 items-center border-b bg-muted/50 px-4 py-2 text-xs font-semibold text-foreground"
style={{ gridTemplateColumns: "4px 220px 110px 1fr" }}
>
<span />
<span> </span>
<span> </span>
<span> </span>
</div>
{/* 그룹별 행 */}
<div className="flex-1 overflow-y-auto">
{(["entity", "code", "category", "numbering"] as const).map((kind) => {
const list = grouped[kind];
if (list.length === 0) return null;
const meta = KIND_META[kind];
const KindIcon = meta.icon;
return (
<div key={kind} className="space-y-1 py-2">
<div className="flex items-center gap-2 border-b border-border/60 px-4 pb-1.5">
<KindIcon className={cn("h-4 w-4", meta.color)} />
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{meta.label}
</span>
<Badge variant="secondary" className="text-xs">
{list.length}
</Badge>
</div>
{list.map((column) => {
const typeConf = INPUT_TYPE_COLORS[column.input_type || "text"] || INPUT_TYPE_COLORS.text;
const isSelected = selectedColumn === column.column_name;
return (
<div
key={column.column_name}
role="button"
tabIndex={0}
onClick={() => onSelectColumn?.(column.column_name)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelectColumn?.(column.column_name);
}
}}
className={cn(
"grid min-h-10 cursor-pointer items-center gap-2 rounded-md border px-4 py-2 transition-colors",
"bg-card border-transparent hover:border-border hover:shadow-sm",
isSelected && "border-primary/30 bg-primary/5 shadow-sm",
)}
style={{ gridTemplateColumns: "4px 220px 110px 1fr" }}
>
{/* 색상바 */}
<div className={cn("h-full min-h-8 w-1 rounded-full", typeConf.barColor)} />
{/* 소스 컬럼명 */}
<div className="min-w-0">
<div className="truncate text-xs font-medium">
{column.display_name && column.display_name !== column.column_name
? `${column.display_name} (${column.column_name})`
: column.column_name}
</div>
</div>
{/* 참조 종류 칩 */}
<div className={cn("inline-flex w-fit items-center gap-1 rounded-md border px-2 py-0.5 text-xs", meta.bgColor, meta.color)}>
<span className="h-1.5 w-1.5 rounded-full bg-current opacity-70" />
{meta.label}
</div>
{/* 참조 대상 */}
<div className="flex min-w-0 flex-wrap items-center gap-1">
{kind === "entity" && column.reference_table && column.reference_table !== "none" ? (
<>
<Badge
variant="outline"
className="text-xs font-normal"
title={
tables
? (() => {
const t = tables.find((tb) => tb.table_name === column.reference_table);
return t?.display_name && t.display_name !== t.table_name
? `${t.display_name} (${column.reference_table})`
: column.reference_table;
})()
: column.reference_table
}
>
{column.reference_table}
</Badge>
<span className="text-muted-foreground text-xs"></span>
<Badge
variant="outline"
className="text-xs font-normal"
title={
referenceTableColumns?.[column.reference_table]
? (() => {
const refCols = referenceTableColumns[column.reference_table];
const c = refCols.find((rc) => rc.column_name === (column.reference_column ?? ""));
return c?.display_name && c.display_name !== c.column_name
? `${c.display_name} (${column.reference_column})`
: column.reference_column ?? "—";
})()
: column.reference_column ?? "—"
}
>
{column.reference_column || "—"}
</Badge>
</>
) : kind === "code" ? (
column.code_info ? (
<Badge variant="outline" className="text-xs font-normal">
: {column.code_info}
</Badge>
) : (
<span className="text-muted-foreground/60 text-xs"> ( )</span>
)
) : kind === "category" ? (
column.category_ref ? (
<Badge variant="outline" className="text-xs font-normal">
: {column.category_ref}
</Badge>
) : column.category_menus && column.category_menus.length > 0 ? (
<Badge variant="outline" className="text-xs font-normal">
{column.category_menus.length}
</Badge>
) : (
<span className="text-muted-foreground/60 text-xs"> ( )</span>
)
) : kind === "numbering" ? (
column.numbering_rule_id ? (
<Badge variant="outline" className="text-xs font-normal">
: {column.numbering_rule_id}
</Badge>
) : (
<span className="text-muted-foreground/60 text-xs"> ( )</span>
)
) : (
<span className="text-muted-foreground/60 text-xs"></span>
)}
</div>
</div>
);
})}
</div>
);
})}
</div>
</div>
);
}
@@ -3,7 +3,7 @@
import React, { useMemo } from "react";
import { cn } from "@/lib/utils";
import type { ColumnTypeInfo } from "./types";
import { INPUT_TYPE_COLORS } from "./types";
import { INPUT_TYPE_COLORS, FALLBACK_TYPE_CONFIG } from "./types";
import { USER_SELECTABLE_INPUT_TYPE_ORDER } from "@/types/input-types";
export interface TypeOverviewStripProps {
@@ -57,20 +57,13 @@ export function TypeOverviewStrip({
/** stroke-dasharray: 비율만큼 둘레에 할당 (둘레 100 기준) */
const circumference = 100;
let offset = 0;
const LEGACY_CONF = {
color: "text-amber-600",
bgColor: "bg-amber-50",
barColor: "bg-amber-400",
label: "Legacy",
desc: "구버전 타입",
iconChar: "?",
};
const LEGACY_CONF = { ...FALLBACK_TYPE_CONFIG, color: "text-amber-600", bgColor: "bg-amber-50", barColor: "bg-amber-400" };
const segmentPaths = segments.map(({ type, ratio, isLegacy }) => {
const length = ratio * circumference;
const dashArray = `${length} ${circumference - length}`;
const dashOffset = -offset;
offset += length;
const conf = isLegacy ? LEGACY_CONF : (INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted" });
const conf = isLegacy ? LEGACY_CONF : (INPUT_TYPE_COLORS[type] || FALLBACK_TYPE_CONFIG);
return {
type,
dashArray,
@@ -112,7 +105,7 @@ export function TypeOverviewStrip({
.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 conf = INPUT_TYPE_COLORS[type] || { ...FALLBACK_TYPE_CONFIG, label: type };
const isActive = activeFilter === null || activeFilter === type;
return (
<button
+42 -15
View File
@@ -3,6 +3,23 @@
* page.tsx에서 추출한 인터페이스 및 타입별 색상/그룹 유틸
*/
import {
AlignLeft,
Braces,
Calendar,
CheckSquare,
ChevronDown,
CircleDot,
FolderTree,
Hash,
HelpCircle,
Image as ImageIcon,
Link2,
ListOrdered,
Paperclip,
Type,
type LucideIcon,
} from "lucide-react";
import { USER_SELECTABLE_INPUT_TYPE_ORDER } from "@/types/input-types";
export interface TableInfo {
@@ -52,24 +69,34 @@ export interface TypeColorConfig {
barColor: string;
label: string;
desc: string;
iconChar: string;
Icon: LucideIcon;
}
/** 입력 타입별 색상 맵 - iconChar는 카드 선택용 시각 아이콘 */
/** Legacy/알 수 없는 타입용 fallback config */
export const FALLBACK_TYPE_CONFIG: TypeColorConfig = {
color: "text-muted-foreground",
bgColor: "bg-muted",
barColor: "bg-muted",
label: "Legacy",
desc: "구버전 타입",
Icon: HelpCircle,
};
/** 입력 타입별 색상 맵 - Icon 은 lucide 컴포넌트로 통일 (letter/symbol/emoji 혼재 방지) */
export const INPUT_TYPE_COLORS: Record<string, TypeColorConfig> = {
text: { color: "text-slate-600", bgColor: "bg-slate-50", barColor: "bg-slate-400", label: "텍스트", desc: "일반 텍스트 입력", iconChar: "T" },
number: { color: "text-indigo-600", bgColor: "bg-indigo-50", barColor: "bg-indigo-500", label: "숫자", desc: "숫자만 입력", iconChar: "#" },
date: { color: "text-amber-600", bgColor: "bg-amber-50", barColor: "bg-amber-500", label: "날짜", desc: "날짜 선택", iconChar: "D" },
code: { color: "text-emerald-600", bgColor: "bg-emerald-50", barColor: "bg-emerald-500", label: "코드", desc: "공통코드 선택", iconChar: "{}" },
entity: { color: "text-violet-600", bgColor: "bg-violet-50", barColor: "bg-violet-500", label: "테이블 참조", desc: "다른 테이블 연결", iconChar: "⊞" },
select: { color: "text-cyan-600", bgColor: "bg-cyan-50", barColor: "bg-cyan-500", label: "셀렉트", desc: "직접 옵션 선택", iconChar: "☰" },
checkbox: { color: "text-pink-600", bgColor: "bg-pink-50", barColor: "bg-pink-500", label: "체크박스", desc: "예/아니오 선택", iconChar: "☑" },
numbering: { color: "text-orange-600", bgColor: "bg-orange-50", barColor: "bg-orange-500", label: "채번", desc: "자동 번호 생성", iconChar: "≡" },
category: { color: "text-teal-600", bgColor: "bg-teal-50", barColor: "bg-teal-500", label: "카테고리", desc: "등록된 선택지", iconChar: "⊟" },
textarea: { color: "text-indigo-600", bgColor: "bg-indigo-50", barColor: "bg-indigo-400", label: "여러 줄", desc: "긴 텍스트 입력", iconChar: "≡" },
radio: { color: "text-rose-600", bgColor: "bg-rose-50", barColor: "bg-rose-500", label: "라디오", desc: "하나만 선택", iconChar: "◉" },
file: { color: "text-amber-600", bgColor: "bg-amber-50", barColor: "bg-amber-500", label: "파일", desc: "파일 업로드", iconChar: "📎" },
image: { color: "text-sky-600", bgColor: "bg-sky-50", barColor: "bg-sky-500", label: "이미지", desc: "이미지 표시", iconChar: "🖼" },
text: { color: "text-slate-600", bgColor: "bg-slate-50", barColor: "bg-slate-400", label: "텍스트", desc: "일반 텍스트 입력", Icon: Type },
number: { color: "text-indigo-600", bgColor: "bg-indigo-50", barColor: "bg-indigo-500", label: "숫자", desc: "숫자만 입력", Icon: Hash },
date: { color: "text-amber-600", bgColor: "bg-amber-50", barColor: "bg-amber-500", label: "날짜", desc: "날짜 선택", Icon: Calendar },
code: { color: "text-emerald-600", bgColor: "bg-emerald-50", barColor: "bg-emerald-500", label: "코드", desc: "공통코드 선택", Icon: Braces },
entity: { color: "text-violet-600", bgColor: "bg-violet-50", barColor: "bg-violet-500", label: "테이블 참조", desc: "다른 테이블 연결", Icon: Link2 },
select: { color: "text-cyan-600", bgColor: "bg-cyan-50", barColor: "bg-cyan-500", label: "셀렉트", desc: "직접 옵션 선택", Icon: ChevronDown },
checkbox: { color: "text-pink-600", bgColor: "bg-pink-50", barColor: "bg-pink-500", label: "체크박스", desc: "예/아니오 선택", Icon: CheckSquare },
numbering: { color: "text-orange-600", bgColor: "bg-orange-50", barColor: "bg-orange-500", label: "채번", desc: "자동 번호 생성", Icon: ListOrdered },
category: { color: "text-teal-600", bgColor: "bg-teal-50", barColor: "bg-teal-500", label: "카테고리", desc: "등록된 선택지", Icon: FolderTree },
textarea: { color: "text-indigo-600", bgColor: "bg-indigo-50", barColor: "bg-indigo-400", label: "여러 줄", desc: "긴 텍스트 입력", Icon: AlignLeft },
radio: { color: "text-rose-600", bgColor: "bg-rose-50", barColor: "bg-rose-500", label: "라디오", desc: "하나만 선택", Icon: CircleDot },
file: { color: "text-amber-600", bgColor: "bg-amber-50", barColor: "bg-amber-500", label: "파일", desc: "파일 업로드", Icon: Paperclip },
image: { color: "text-sky-600", bgColor: "bg-sky-50", barColor: "bg-sky-500", label: "이미지", desc: "이미지 표시", Icon: ImageIcon },
};
/** v3.2 — 사용자 선택 가능한 8개 입력 타입 색상 맵 (T2 드롭다운/카드 그리드용) */