feat(테이블타입): DBeaver 식 탭 분리 + 입력 타입 아이콘 lucide 통일 #24
@@ -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
|
||||
|
||||
@@ -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 드롭다운/카드 그리드용) */
|
||||
|
||||
Reference in New Issue
Block a user