feat(pop-search): 모달 뷰 전면 개선 - 아이콘 뷰, 가나다/ABC 필터 탭, 컬럼 라벨
모달 타입 통합 (modal-table/card/icon-grid -> modal 1종): - normalizeInputType()으로 레거시 저장값 호환 - 캔버스 모달 모드 완전 제거 (ModalMode, modalCanvasId, returnEvent) - SearchInputType 9종으로 정리 모달 뷰 실제 구현: - TableView / IconView 분리 렌더링 (displayStyle 반영) - 아이콘 뷰: 이름 첫 글자 컬러 카드 + 초성 그룹 헤더 - getIconColor() 결정적 해시 색상 (16색 팔레트) 가나다/ABC 필터 탭: - ModalFilterTab 타입 + getGroupKey() 한글 초성 추출 - 쌍자음 합침 (ㄲ->ㄱ, ㄸ->ㄷ 등) - 모달 상단 토글 버튼으로 초성/알파벳 섹션 그룹화 디자이너 설정 개선: - 컬럼 헤더 라벨 커스터마이징 (columnLabels) - 필터 탭 활성화 체크박스 (가나다/ABC) - card 스타일 제거, 정렬 옵션 제거 - 검색 방식 (포함/시작/같음) 유지 시나리오 A 모달 선택 필터링: - ConnectionEditor 필터 컬럼에 DB 전체 컬럼 표시 - pop-string-list 복수 필터 AND 지원 - useConnectionResolver 페이로드 구조 정규화 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ArrowRight, Link2, Unlink2, Plus, Trash2, Pencil, X } from "lucide-react";
|
||||
import { ArrowRight, Link2, Unlink2, Plus, Trash2, Pencil, X, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
PopComponentRegistry,
|
||||
type ComponentConnectionMeta,
|
||||
} from "@/lib/registry/PopComponentRegistry";
|
||||
import { getTableColumns } from "@/lib/api/tableManagement";
|
||||
|
||||
// ========================================
|
||||
// Props
|
||||
@@ -101,10 +102,11 @@ export default function ConnectionEditor({
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 대상 컴포넌트의 컬럼 목록 추출
|
||||
// 대상 컴포넌트에서 정보 추출
|
||||
// ========================================
|
||||
|
||||
function extractTargetColumns(comp: PopComponentDefinitionV5 | undefined): string[] {
|
||||
/** 화면에 표시 중인 컬럼만 추출 */
|
||||
function extractDisplayColumns(comp: PopComponentDefinitionV5 | undefined): string[] {
|
||||
if (!comp?.config) return [];
|
||||
const cfg = comp.config as Record<string, unknown>;
|
||||
const cols: string[] = [];
|
||||
@@ -124,6 +126,14 @@ function extractTargetColumns(comp: PopComponentDefinitionV5 | undefined): strin
|
||||
return cols;
|
||||
}
|
||||
|
||||
/** 대상 컴포넌트의 데이터소스 테이블명 추출 */
|
||||
function extractTableName(comp: PopComponentDefinitionV5 | undefined): string {
|
||||
if (!comp?.config) return "";
|
||||
const cfg = comp.config as Record<string, unknown>;
|
||||
const ds = cfg.dataSource as { tableName?: string } | undefined;
|
||||
return ds?.tableName || "";
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 보내기 섹션
|
||||
// ========================================
|
||||
@@ -262,11 +272,47 @@ function ConnectionForm({
|
||||
? PopComponentRegistry.getComponent(targetComp.type)?.connectionMeta
|
||||
: null;
|
||||
|
||||
const targetColumns = React.useMemo(
|
||||
() => extractTargetColumns(targetComp || undefined),
|
||||
// 화면에 표시 중인 컬럼
|
||||
const displayColumns = React.useMemo(
|
||||
() => extractDisplayColumns(targetComp || undefined),
|
||||
[targetComp]
|
||||
);
|
||||
|
||||
// DB 테이블 전체 컬럼 (비동기 조회)
|
||||
const tableName = React.useMemo(
|
||||
() => extractTableName(targetComp || undefined),
|
||||
[targetComp]
|
||||
);
|
||||
const [allDbColumns, setAllDbColumns] = React.useState<string[]>([]);
|
||||
const [dbColumnsLoading, setDbColumnsLoading] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!tableName) {
|
||||
setAllDbColumns([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setDbColumnsLoading(true);
|
||||
getTableColumns(tableName).then((res) => {
|
||||
if (cancelled) return;
|
||||
if (res.success && res.data?.columns) {
|
||||
setAllDbColumns(res.data.columns.map((c) => c.columnName));
|
||||
} else {
|
||||
setAllDbColumns([]);
|
||||
}
|
||||
setDbColumnsLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [tableName]);
|
||||
|
||||
// 표시 컬럼과 데이터 전용 컬럼 분리
|
||||
const displaySet = React.useMemo(() => new Set(displayColumns), [displayColumns]);
|
||||
const dataOnlyColumns = React.useMemo(
|
||||
() => allDbColumns.filter((c) => !displaySet.has(c)),
|
||||
[allDbColumns, displaySet]
|
||||
);
|
||||
const hasAnyColumns = displayColumns.length > 0 || dataOnlyColumns.length > 0;
|
||||
|
||||
const toggleColumn = (col: string) => {
|
||||
setFilterColumns((prev) =>
|
||||
prev.includes(col) ? prev.filter((c) => c !== col) : [...prev, col]
|
||||
@@ -384,25 +430,61 @@ function ConnectionForm({
|
||||
{/* 필터 설정 */}
|
||||
{selectedTargetInput && (
|
||||
<div className="space-y-2 rounded bg-gray-50 p-2">
|
||||
{/* 컬럼 선택 (복수) */}
|
||||
<p className="text-[10px] font-medium text-muted-foreground">필터할 컬럼</p>
|
||||
{targetColumns.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
{targetColumns.map((col) => (
|
||||
<div key={col} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`col-${col}-${initial?.id || "new"}`}
|
||||
checked={filterColumns.includes(col)}
|
||||
onCheckedChange={() => toggleColumn(col)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`col-${col}-${initial?.id || "new"}`}
|
||||
className="cursor-pointer text-xs"
|
||||
>
|
||||
{col}
|
||||
</label>
|
||||
|
||||
{dbColumnsLoading ? (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
<span className="text-[10px] text-muted-foreground">컬럼 조회 중...</span>
|
||||
</div>
|
||||
) : hasAnyColumns ? (
|
||||
<div className="space-y-2">
|
||||
{/* 표시 컬럼 그룹 */}
|
||||
{displayColumns.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[9px] font-medium text-green-600">화면 표시 컬럼</p>
|
||||
{displayColumns.map((col) => (
|
||||
<div key={col} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`col-${col}-${initial?.id || "new"}`}
|
||||
checked={filterColumns.includes(col)}
|
||||
onCheckedChange={() => toggleColumn(col)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`col-${col}-${initial?.id || "new"}`}
|
||||
className="cursor-pointer text-xs"
|
||||
>
|
||||
{col}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
{/* 데이터 전용 컬럼 그룹 */}
|
||||
{dataOnlyColumns.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{displayColumns.length > 0 && (
|
||||
<div className="my-1 h-px bg-gray-200" />
|
||||
)}
|
||||
<p className="text-[9px] font-medium text-amber-600">데이터 전용 컬럼</p>
|
||||
{dataOnlyColumns.map((col) => (
|
||||
<div key={col} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`col-${col}-${initial?.id || "new"}`}
|
||||
checked={filterColumns.includes(col)}
|
||||
onCheckedChange={() => toggleColumn(col)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`col-${col}-${initial?.id || "new"}`}
|
||||
className="cursor-pointer text-xs text-muted-foreground"
|
||||
>
|
||||
{col}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user