diff --git a/CLAUDE.md b/CLAUDE.md index 54b32c82..08ded1be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,82 @@ + +# 절대 규칙: 검증 없는 주장 금지 + +내가 출력하는 모든 발언은 근거가 있어야 한다. 근거가 없으면 그 말을 하지 않는다. 위로·추정·일반론·"보통 그렇다"로 채우지 않는다. + +## 위반 사례 (절대 하지 말 것) +- "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 인스턴스가 이 컨벤션을 따라야 합니다. diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index ec166cbc..4118f898 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -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, "컬럼이 없습니다")} ) : ( - <> - - 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"); + + + + + 컬럼 + + + + 참조 + {(() => { + const refCount = columns.filter((c) => + ["entity", "code", "category", "numbering"].includes(c.input_type), + ).length; + return refCount > 0 ? ( + + {refCount} + + ) : null; + })()} + + + + + + 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} + /> + + + + setSelectedColumn((prev) => (prev === c ? null : c))} + /> + + )} )} diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx index 8b365576..d1556a11 100644 --- a/frontend/components/admin/table-type/ColumnDetailPanel.tsx +++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx @@ -183,12 +183,12 @@ export function ColumnDetailPanel({ isLegacy && "cursor-not-allowed", )} > - - {conf.iconChar} - +
라벨 · 컬럼명 - 참조/설정 타입 PK / NN / IDX / UQ @@ -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({
- {/* 참조/설정 칩 */} -
- {column.input_type === "entity" && column.reference_table && column.reference_table !== "none" && ( - <> - { - 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} - - - { - 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 || "—"} - - - )} - {column.input_type === "code" && ( - - {column.code_info ?? "—"} · {column.default_value ?? ""} - - )} - {column.input_type === "numbering" && column.numbering_rule_id && ( - - {column.numbering_rule_id} - - )} - {column.input_type !== "entity" && - column.input_type !== "code" && - column.input_type !== "numbering" && - (column.default_value ? ( - {column.default_value} - ) : ( - - ))} -
- {/* 타입 뱃지 */}
diff --git a/frontend/components/admin/table-type/ReferenceListView.tsx b/frontend/components/admin/table-type/ReferenceListView.tsx new file mode 100644 index 00000000..5bca69e5 --- /dev/null +++ b/frontend/components/admin/table-type/ReferenceListView.tsx @@ -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; + 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 = { + 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 ( +
+
+ + 이 테이블에는 참조 컬럼이 없어요. +
+
+ ); + } + + return ( +
+ {/* 헤더 */} +
+ + 소스 컬럼 + 참조 종류 + 참조 대상 +
+ + {/* 그룹별 행 */} +
+ {(["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 ( +
+
+ + + {meta.label} + + + {list.length} + +
+ {list.map((column) => { + const typeConf = INPUT_TYPE_COLORS[column.input_type || "text"] || INPUT_TYPE_COLORS.text; + const isSelected = selectedColumn === column.column_name; + return ( +
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" }} + > + {/* 색상바 */} +
+ + {/* 소스 컬럼명 */} +
+
+ {column.display_name && column.display_name !== column.column_name + ? `${column.display_name} (${column.column_name})` + : column.column_name} +
+
+ + {/* 참조 종류 칩 */} +
+ + {meta.label} +
+ + {/* 참조 대상 */} +
+ {kind === "entity" && column.reference_table && column.reference_table !== "none" ? ( + <> + { + 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} + + + { + 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 || "—"} + + + ) : kind === "code" ? ( + column.code_info ? ( + + 코드: {column.code_info} + + ) : ( + — (코드 그룹 미지정) + ) + ) : kind === "category" ? ( + column.category_ref ? ( + + 카테고리: {column.category_ref} + + ) : column.category_menus && column.category_menus.length > 0 ? ( + + 카테고리 메뉴 {column.category_menus.length}개 + + ) : ( + — (카테고리 미지정) + ) + ) : kind === "numbering" ? ( + column.numbering_rule_id ? ( + + 채번: {column.numbering_rule_id} + + ) : ( + — (채번 규칙 미지정) + ) + ) : ( + + )} +
+
+ ); + })} +
+ ); + })} +
+
+ ); +} diff --git a/frontend/components/admin/table-type/TypeOverviewStrip.tsx b/frontend/components/admin/table-type/TypeOverviewStrip.tsx index c63ed1f3..19474c0e 100644 --- a/frontend/components/admin/table-type/TypeOverviewStrip.tsx +++ b/frontend/components/admin/table-type/TypeOverviewStrip.tsx @@ -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 (