feat: DISTINCT 값 조회 API 추가 및 라우터 설정
- 테이블 컬럼의 DISTINCT 값을 조회하는 API를 추가하였습니다. 이 API는 특정 테이블과 컬럼에서 DISTINCT 값을 반환하여 선택박스 옵션으로 사용할 수 있도록 합니다. - API 호출 시 멀티테넌시를 고려하여 회사 코드에 따라 필터링을 적용하였습니다. - 관련된 라우터 설정을 추가하여 API 접근을 가능하게 하였습니다. - 프론트엔드에서 DISTINCT 값을 조회할 수 있도록 UnifiedSelect 컴포넌트를 업데이트하였습니다.
This commit is contained in:
@@ -27,7 +27,7 @@ interface AggregationWidgetConfigPanelProps {
|
||||
onChange: (config: Partial<AggregationWidgetConfig>) => void;
|
||||
screenTableName?: string;
|
||||
// 화면 내 컴포넌트 목록 (컴포넌트 연결용)
|
||||
screenComponents?: Array<{ id: string; componentType: string; label?: string; tableName?: string }>;
|
||||
screenComponents?: Array<{ id: string; componentType: string; label?: string; tableName?: string; columnName?: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,13 +172,14 @@ export function AggregationWidgetConfigPanel({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await tableManagementApi.getColumns(sourceComp.tableName);
|
||||
const cols = (response.data?.columns || response.data || []).map((col: any) => ({
|
||||
const response = await tableManagementApi.getColumnList(sourceComp.tableName);
|
||||
const rawCols = response.data?.columns || (Array.isArray(response.data) ? response.data : []);
|
||||
const cols = rawCols.map((col: any) => ({
|
||||
columnName: col.column_name || col.columnName,
|
||||
label: col.column_label || col.columnLabel || col.display_name || col.column_name || col.columnName,
|
||||
}));
|
||||
|
||||
setSourceComponentColumnsCache(prev => ({
|
||||
setSourceComponentColumnsCache((prev) => ({
|
||||
...prev,
|
||||
[componentId]: cols,
|
||||
}));
|
||||
@@ -290,19 +291,20 @@ export function AggregationWidgetConfigPanel({
|
||||
try {
|
||||
// 카테고리 API 호출
|
||||
const result = await getCategoryValues(targetTableName, col.columnName, false);
|
||||
if (result.success && Array.isArray(result.data)) {
|
||||
if (result.success && "data" in result && Array.isArray(result.data)) {
|
||||
// 중복 제거 (valueCode 기준)
|
||||
const seenCodes = new Set<string>();
|
||||
const uniqueOptions: Array<{ value: string; label: string }> = [];
|
||||
|
||||
for (const item of result.data) {
|
||||
const code = item.valueCode || item.code || item.value || item.id;
|
||||
const itemAny = item as any;
|
||||
const code = item.valueCode || itemAny.code || itemAny.value || itemAny.id;
|
||||
if (!seenCodes.has(code)) {
|
||||
seenCodes.add(code);
|
||||
uniqueOptions.push({
|
||||
value: code,
|
||||
// valueLabel이 실제 표시명
|
||||
label: item.valueLabel || item.valueName || item.name || item.label || item.displayName || code,
|
||||
label: item.valueLabel || itemAny.valueName || itemAny.name || itemAny.label || itemAny.displayName || code,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -418,6 +420,52 @@ export function AggregationWidgetConfigPanel({
|
||||
c.componentType === "table-list"
|
||||
);
|
||||
|
||||
// 폼 필드로 사용 가능한 컴포넌트 (입력 위젯들만)
|
||||
const formFieldComponents = useMemo(() => {
|
||||
// 제외할 컴포넌트 타입 (표시 전용, 레이아웃, 컨테이너 등)
|
||||
const excludeTypes = [
|
||||
"aggregation", "widget", "button", "label", "display", "table-list",
|
||||
"repeat", "container", "layout", "section", "card", "tabs", "modal",
|
||||
"flow", "rack", "map", "chart", "image", "file", "media"
|
||||
];
|
||||
|
||||
const filtered = screenComponents.filter((comp) => {
|
||||
const type = comp.componentType?.toLowerCase() || "";
|
||||
|
||||
// 제외 대상인지 먼저 체크
|
||||
const isExcluded = excludeTypes.some(exclude => type.includes(exclude));
|
||||
if (isExcluded) return false;
|
||||
|
||||
// 입력 가능한 컴포넌트 타입들
|
||||
const isInputType = (
|
||||
type.includes("input") ||
|
||||
type.includes("select") ||
|
||||
type.includes("date") ||
|
||||
type.includes("checkbox") ||
|
||||
type.includes("radio") ||
|
||||
type.includes("textarea") ||
|
||||
type.includes("number") ||
|
||||
// unified-input, unified-select, unified-date 등 (unified-repeater 등은 제외)
|
||||
type === "unified-input" ||
|
||||
type === "unified-select" ||
|
||||
type === "unified-date" ||
|
||||
type === "unified-hierarchy"
|
||||
);
|
||||
|
||||
// columnName이 있으면 입력 필드로 간주 (드래그로 배치된 필드)
|
||||
const hasColumnName = !!comp.columnName;
|
||||
|
||||
return isInputType || hasColumnName;
|
||||
});
|
||||
|
||||
return filtered.map((comp) => ({
|
||||
id: comp.id,
|
||||
label: comp.label || comp.columnName || comp.id,
|
||||
columnName: comp.columnName || comp.id,
|
||||
componentType: comp.componentType,
|
||||
}));
|
||||
}, [screenComponents]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">집계 위젯 설정</div>
|
||||
@@ -444,7 +492,14 @@ export function AggregationWidgetConfigPanel({
|
||||
variant={dataSourceType === "component" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-auto flex-col gap-1 py-2 text-xs"
|
||||
onClick={() => onChange({ dataSourceType: "component" })}
|
||||
onClick={() => {
|
||||
// 컴포넌트 모드로 변경 시 화면의 메인 테이블로 자동 설정
|
||||
onChange({
|
||||
dataSourceType: "component",
|
||||
tableName: screenTableName || config.tableName,
|
||||
useCustomTable: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link2 className="h-4 w-4" />
|
||||
<span>컴포넌트</span>
|
||||
@@ -453,7 +508,14 @@ export function AggregationWidgetConfigPanel({
|
||||
variant={dataSourceType === "selection" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-auto flex-col gap-1 py-2 text-xs"
|
||||
onClick={() => onChange({ dataSourceType: "selection" })}
|
||||
onClick={() => {
|
||||
// 선택 데이터 모드로 변경 시 화면의 메인 테이블로 자동 설정
|
||||
onChange({
|
||||
dataSourceType: "selection",
|
||||
tableName: screenTableName || config.tableName,
|
||||
useCustomTable: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MousePointer className="h-4 w-4" />
|
||||
<span>선택 데이터</span>
|
||||
@@ -797,12 +859,32 @@ export function AggregationWidgetConfigPanel({
|
||||
)
|
||||
)}
|
||||
{filter.valueSourceType === "formField" && (
|
||||
<Input
|
||||
value={filter.formFieldName || ""}
|
||||
onChange={(e) => updateFilter(filter.id, { formFieldName: e.target.value })}
|
||||
placeholder="필드명 입력"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
formFieldComponents.length > 0 ? (
|
||||
<Select
|
||||
value={filter.formFieldName || ""}
|
||||
onValueChange={(value) => updateFilter(filter.id, { formFieldName: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="폼 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{formFieldComponents.map((field) => (
|
||||
<SelectItem key={field.id} value={field.columnName}>
|
||||
{field.label}
|
||||
{field.columnName !== field.label && (
|
||||
<span className="ml-1 text-muted-foreground text-[10px]">
|
||||
({field.columnName})
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground h-7 px-2 border rounded-md bg-slate-50">
|
||||
<span>배치된 입력 필드가 없습니다</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{filter.valueSourceType === "selection" && (
|
||||
<div className="space-y-2 col-span-2">
|
||||
|
||||
Reference in New Issue
Block a user