autofill 기능 구현

This commit is contained in:
dohyeons 2025-11-04 14:33:39 +09:00
parent 4dde008c6d
commit 39080dff59
11 changed files with 966 additions and 52 deletions

View File

@ -12,6 +12,7 @@ import {
ColumnListResponse,
ColumnSettingsResponse,
} from "../types/tableManagement";
import { query } from "../database/db"; // 🆕 query 함수 import
/**
*
@ -506,7 +507,91 @@ export async function updateColumnInputType(
}
/**
* ( + )
* ( )
*/
export async function getTableRecord(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { filterColumn, filterValue, displayColumn } = req.body;
logger.info(`=== 단일 레코드 조회 시작: ${tableName} ===`);
logger.info(`필터: ${filterColumn} = ${filterValue}`);
logger.info(`표시 컬럼: ${displayColumn}`);
if (!tableName || !filterColumn || !filterValue || !displayColumn) {
const response: ApiResponse<null> = {
success: false,
message: "필수 파라미터가 누락되었습니다.",
error: {
code: "MISSING_PARAMETERS",
details:
"tableName, filterColumn, filterValue, displayColumn이 필요합니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
// 단일 레코드 조회 (WHERE filterColumn = filterValue)
const result = await tableManagementService.getTableData(tableName, {
page: 1,
size: 1,
search: {
[filterColumn]: filterValue,
},
});
if (!result.data || result.data.length === 0) {
const response: ApiResponse<null> = {
success: false,
message: "데이터를 찾을 수 없습니다.",
error: {
code: "NOT_FOUND",
details: `${filterColumn} = ${filterValue}에 해당하는 데이터가 없습니다.`,
},
};
res.status(404).json(response);
return;
}
const record = result.data[0];
const displayValue = record[displayColumn];
logger.info(`레코드 조회 완료: ${displayColumn} = ${displayValue}`);
const response: ApiResponse<{ value: any; record: any }> = {
success: true,
message: "레코드를 성공적으로 조회했습니다.",
data: {
value: displayValue,
record: record,
},
};
res.status(200).json(response);
} catch (error) {
logger.error("레코드 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "레코드 조회 중 오류가 발생했습니다.",
error: {
code: "RECORD_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
* ( + + )
*/
export async function getTableData(
req: AuthenticatedRequest,
@ -520,12 +605,14 @@ export async function getTableData(
search = {},
sortBy,
sortOrder = "asc",
autoFilter, // 🆕 자동 필터 설정 추가 (컴포넌트에서 직접 전달)
} = req.body;
logger.info(`=== 테이블 데이터 조회 시작: ${tableName} ===`);
logger.info(`페이징: page=${page}, size=${size}`);
logger.info(`검색 조건:`, search);
logger.info(`정렬: ${sortBy} ${sortOrder}`);
logger.info(`자동 필터:`, autoFilter); // 🆕
if (!tableName) {
const response: ApiResponse<null> = {
@ -542,11 +629,35 @@ export async function getTableData(
const tableManagementService = new TableManagementService();
// 🆕 현재 사용자 필터 적용
let enhancedSearch = { ...search };
if (autoFilter?.enabled && req.user) {
const filterColumn = autoFilter.filterColumn || "company_code";
const userField = autoFilter.userField || "companyCode";
const userValue = (req.user as any)[userField];
if (userValue) {
enhancedSearch[filterColumn] = userValue;
logger.info("🔍 현재 사용자 필터 적용:", {
filterColumn,
userField,
userValue,
tableName,
});
} else {
logger.warn("⚠️ 사용자 정보 필드 값 없음:", {
userField,
user: req.user,
});
}
}
// 데이터 조회
const result = await tableManagementService.getTableData(tableName, {
page: parseInt(page),
size: parseInt(size),
search,
search: enhancedSearch, // 🆕 필터가 적용된 search 사용
sortBy,
sortOrder,
});
@ -1216,9 +1327,7 @@ export async function getLogData(
originalId: originalId as string,
});
logger.info(
`로그 데이터 조회 완료: ${tableName}_log, ${result.total}`
);
logger.info(`로그 데이터 조회 완료: ${tableName}_log, ${result.total}`);
const response: ApiResponse<typeof result> = {
success: true,
@ -1254,7 +1363,9 @@ export async function toggleLogTable(
const { tableName } = req.params;
const { isActive } = req.body;
logger.info(`=== 로그 테이블 토글: ${tableName}, isActive: ${isActive} ===`);
logger.info(
`=== 로그 테이블 토글: ${tableName}, isActive: ${isActive} ===`
);
if (!tableName) {
const response: ApiResponse<null> = {
@ -1288,9 +1399,7 @@ export async function toggleLogTable(
isActive === "Y" || isActive === true
);
logger.info(
`로그 테이블 토글 완료: ${tableName}, isActive: ${isActive}`
);
logger.info(`로그 테이블 토글 완료: ${tableName}, isActive: ${isActive}`);
const response: ApiResponse<null> = {
success: true,

View File

@ -11,6 +11,7 @@ import {
updateColumnInputType,
updateTableLabel,
getTableData,
getTableRecord, // 🆕 단일 레코드 조회
addTableData,
editTableData,
deleteTableData,
@ -134,6 +135,12 @@ router.get("/health", checkDatabaseConnection);
*/
router.post("/tables/:tableName/data", getTableData);
/**
* ( )
* POST /api/table-management/tables/:tableName/record
*/
router.post("/tables/:tableName/record", getTableRecord);
/**
*
* POST /api/table-management/tables/:tableName/add

View File

@ -147,6 +147,57 @@ export default function ScreenViewPage() {
}
}, [screenId]);
// 🆕 autoFill 자동 입력 초기화
useEffect(() => {
const initAutoFill = async () => {
if (!layout || !layout.components || !user) {
return;
}
for (const comp of layout.components) {
// type: "component" 또는 type: "widget" 모두 처리
if (comp.type === 'widget' || comp.type === 'component') {
const widget = comp as any;
const fieldName = widget.columnName || widget.id;
// autoFill 처리
if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) {
const autoFillConfig = widget.autoFill || (comp as any).autoFill;
const currentValue = formData[fieldName];
if (currentValue === undefined || currentValue === '') {
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
// 사용자 정보에서 필터 값 가져오기
const userValue = user?.[userField as keyof typeof user];
if (userValue && sourceTable && filterColumn && displayColumn) {
try {
const { tableTypeApi } = await import("@/lib/api/screen");
const result = await tableTypeApi.getTableRecord(
sourceTable,
filterColumn,
userValue,
displayColumn
);
setFormData((prev) => ({
...prev,
[fieldName]: result.value,
}));
} catch (error) {
console.error(`autoFill 조회 실패: ${fieldName}`, error);
}
}
}
}
}
}
};
initAutoFill();
}, [layout, user]);
// 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) - 모바일에서는 비활성화
useEffect(() => {
// 모바일 환경에서는 스케일 조정 비활성화 (반응형만 작동)

View File

@ -485,6 +485,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
page,
size: pageSize,
search: searchParams,
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
});
setData(result.data);
@ -576,7 +577,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
setLoading(false);
}
},
[component.tableName, pageSize],
[component.tableName, pageSize, component.autoFilter], // 🆕 autoFilter 추가
);
// 현재 사용자 정보 로드

View File

@ -36,7 +36,7 @@ import { InteractiveDataTable } from "./InteractiveDataTable";
import { FileUpload } from "./widgets/FileUpload";
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
import { useParams } from "next/navigation";
import { screenApi } from "@/lib/api/screen";
import { screenApi, tableTypeApi } from "@/lib/api/screen";
import { DynamicWebTypeRenderer } from "@/lib/registry/DynamicWebTypeRenderer";
import { enhancedFormService } from "@/lib/services/enhancedFormService";
import { FormValidationIndicator } from "@/components/common/FormValidationIndicator";
@ -237,14 +237,46 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
// 자동입력 필드들의 값을 formData에 초기 설정
React.useEffect(() => {
// console.log("🚀 자동입력 초기화 useEffect 실행 - allComponents 개수:", allComponents.length);
const initAutoInputFields = () => {
const initAutoInputFields = async () => {
// console.log("🔧 initAutoInputFields 실행 시작");
allComponents.forEach(comp => {
if (comp.type === 'widget') {
for (const comp of allComponents) {
// 🆕 type: "component" 또는 type: "widget" 모두 처리
if (comp.type === 'widget' || comp.type === 'component') {
const widget = comp as WidgetComponent;
const fieldName = widget.columnName || widget.id;
// 텍스트 타입 위젯의 자동입력 처리
// 🆕 autoFill 처리 (테이블 조회 기반 자동 입력)
if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) {
const autoFillConfig = widget.autoFill || (comp as any).autoFill;
const currentValue = formData[fieldName];
if (currentValue === undefined || currentValue === '') {
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
// 사용자 정보에서 필터 값 가져오기
const userValue = user?.[userField];
if (userValue && sourceTable && filterColumn && displayColumn) {
try {
const result = await tableTypeApi.getTableRecord(
sourceTable,
filterColumn,
userValue,
displayColumn
);
updateFormData(fieldName, result.value);
} catch (error) {
console.error(`autoFill 조회 실패: ${fieldName}`, error);
}
}
}
continue; // autoFill이 활성화되면 일반 자동입력은 건너뜀
}
// 기존 widget 타입 전용 로직은 widget인 경우만
if (comp.type !== 'widget') continue;
// 텍스트 타입 위젯의 자동입력 처리 (기존 로직)
if ((widget.widgetType === 'text' || widget.widgetType === 'email' || widget.widgetType === 'tel') &&
widget.webTypeConfig) {
const config = widget.webTypeConfig as TextTypeConfig;
@ -278,12 +310,12 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
}
}
}
});
}
};
// 초기 로드 시 자동입력 필드들 설정
initAutoInputFields();
}, [allComponents, generateAutoValue]); // formData는 의존성에서 제외 (무한 루프 방지)
}, [allComponents, generateAutoValue, user]); // formData는 의존성에서 제외 (무한 루프 방지)
// 날짜 값 업데이트
const updateDateValue = (fieldName: string, date: Date | undefined) => {

View File

@ -81,6 +81,21 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
// formData 결정 (외부에서 전달받은 것이 있으면 우선 사용)
const formData = externalFormData || localFormData;
// formData 업데이트 함수
const updateFormData = useCallback(
(fieldName: string, value: any) => {
if (onFormDataChange) {
onFormDataChange(fieldName, value);
} else {
setLocalFormData((prev) => ({
...prev,
[fieldName]: value,
}));
}
},
[onFormDataChange],
);
// 자동값 생성 함수
const generateAutoValue = useCallback(
(autoValueType: string): string => {
@ -105,6 +120,50 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
[userName],
);
// 🆕 autoFill 자동 입력 초기화
React.useEffect(() => {
const initAutoInputFields = async () => {
for (const comp of allComponents) {
// type: "component" 또는 type: "widget" 모두 처리
if (comp.type === 'widget' || comp.type === 'component') {
const widget = comp as any;
const fieldName = widget.columnName || widget.id;
// autoFill 처리 (테이블 조회 기반 자동 입력)
if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) {
const autoFillConfig = widget.autoFill || (comp as any).autoFill;
const currentValue = formData[fieldName];
if (currentValue === undefined || currentValue === '') {
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
// 사용자 정보에서 필터 값 가져오기
const userValue = user?.[userField];
if (userValue && sourceTable && filterColumn && displayColumn) {
try {
const { tableTypeApi } = await import("@/lib/api/screen");
const result = await tableTypeApi.getTableRecord(
sourceTable,
filterColumn,
userValue,
displayColumn
);
updateFormData(fieldName, result.value);
} catch (error) {
console.error(`autoFill 조회 실패: ${fieldName}`, error);
}
}
}
}
}
}
};
initAutoInputFields();
}, [allComponents, user]);
// 팝업 화면 레이아웃 로드
React.useEffect(() => {
if (popupScreen?.screenId) {

View File

@ -2198,6 +2198,90 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 🆕 자동 필터 설정 */}
<div className="space-y-4">
<h4 className="text-sm font-medium flex items-center gap-2">
<Filter className="h-4 w-4" />
</h4>
<div className="space-y-3 rounded-md border border-gray-200 p-3">
<div className="flex items-center space-x-2">
<Checkbox
id="auto-filter-enabled"
checked={component.autoFilter?.enabled || false}
onCheckedChange={(checked) => {
onUpdateComponent({
autoFilter: {
enabled: checked as boolean,
filterColumn: component.autoFilter?.filterColumn || 'company_code',
userField: component.autoFilter?.userField || 'companyCode',
},
});
}}
/>
<Label htmlFor="auto-filter-enabled" className="font-normal">
</Label>
</div>
{component.autoFilter?.enabled && (
<div className="space-y-3 pt-2">
<div className="space-y-2">
<Label htmlFor="filter-column" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="filter-column"
value={component.autoFilter?.filterColumn || ''}
onChange={(e) => {
onUpdateComponent({
autoFilter: {
...component.autoFilter!,
filterColumn: e.target.value,
},
});
}}
placeholder="company_code"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">
: company_code, dept_code, user_id
</p>
</div>
<div className="space-y-2">
<Label htmlFor="user-field" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={component.autoFilter?.userField || 'companyCode'}
onValueChange={(value: any) => {
onUpdateComponent({
autoFilter: {
...component.autoFilter!,
userField: value,
},
});
}}
>
<SelectTrigger id="user-field" className="text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode"> </SelectItem>
<SelectItem value="userId"> ID</SelectItem>
<SelectItem value="deptCode"> </SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
</div>
)}
</div>
</div>
{/* 페이지네이션 설정 */}
<div className="space-y-4">
<h4 className="text-sm font-medium"> </h4>

View File

@ -1,8 +1,12 @@
"use client";
import React, { useState, useEffect } from "react";
import { Settings } from "lucide-react";
import { Settings, Database } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Separator } from "@/components/ui/separator";
import { useWebTypes } from "@/hooks/admin/useWebTypes";
import { getConfigPanelComponent } from "@/lib/utils/getConfigPanelComponent";
import {
@ -1125,6 +1129,136 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
});
}}
/>
{/* 🆕 테이블 데이터 자동 입력 섹션 (component 타입용) */}
<div className="space-y-4 rounded-md border border-gray-200 p-4">
<h4 className="flex items-center gap-2 text-sm font-medium">
<Database className="h-4 w-4" />
</h4>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="auto-fill-enabled-component"
checked={selectedComponent.autoFill?.enabled || false}
onCheckedChange={(checked) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
enabled: checked as boolean,
sourceTable: selectedComponent.autoFill?.sourceTable || "",
filterColumn: selectedComponent.autoFill?.filterColumn || "company_code",
userField: selectedComponent.autoFill?.userField || "companyCode",
displayColumn: selectedComponent.autoFill?.displayColumn || "",
});
}}
/>
<Label htmlFor="auto-fill-enabled-component" className="text-xs font-normal">
</Label>
</div>
{selectedComponent.autoFill?.enabled && (
<div className="space-y-3 pt-2">
<div className="space-y-2">
<Label htmlFor="source-table-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.sourceTable || ""}
onValueChange={(value) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
sourceTable: value,
});
}}
>
<SelectTrigger id="source-table-component" className="text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<div className="space-y-2">
<Label htmlFor="filter-column-autofill-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="filter-column-autofill-component"
value={selectedComponent.autoFill?.filterColumn || ""}
onChange={(e) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
filterColumn: e.target.value,
});
}}
placeholder="company_code"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">: company_code, dept_code, user_id</p>
</div>
<div className="space-y-2">
<Label htmlFor="user-field-autofill-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.userField || "companyCode"}
onValueChange={(value: any) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
userField: value,
});
}}
>
<SelectTrigger id="user-field-autofill-component" className="text-xs">
<SelectValue placeholder="사용자 정보 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode" className="text-xs">
</SelectItem>
<SelectItem value="userId" className="text-xs">
ID
</SelectItem>
<SelectItem value="deptCode" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<div className="space-y-2">
<Label htmlFor="display-column-autofill-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="display-column-autofill-component"
value={selectedComponent.autoFill?.displayColumn || ""}
onChange={(e) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
displayColumn: e.target.value,
});
}}
placeholder="company_name"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">
(: company_name)
</p>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
@ -1202,7 +1336,144 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
</div>
{/* 상세 설정 영역 */}
<div className="flex-1 overflow-y-auto p-4">{renderWebTypeConfig(widget)}</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-6">
{console.log("🔍 [DetailSettingsPanel] widget 타입:", selectedComponent?.type, "autoFill:", widget.autoFill)}
{/* 🆕 자동 입력 섹션 */}
<div className="space-y-4 rounded-md border border-red-500 bg-yellow-50 p-4">
<h4 className="text-sm font-medium flex items-center gap-2">
<Database className="h-4 w-4" />
🔥 ()
</h4>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="auto-fill-enabled"
checked={widget.autoFill?.enabled || false}
onCheckedChange={(checked) => {
onUpdateProperty(widget.id, "autoFill", {
enabled: checked as boolean,
sourceTable: widget.autoFill?.sourceTable || '',
filterColumn: widget.autoFill?.filterColumn || 'company_code',
userField: widget.autoFill?.userField || 'companyCode',
displayColumn: widget.autoFill?.displayColumn || '',
});
}}
/>
<Label htmlFor="auto-fill-enabled" className="font-normal text-xs">
</Label>
</div>
{widget.autoFill?.enabled && (
<div className="space-y-3 pt-2">
<div className="space-y-2">
<Label htmlFor="source-table" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.sourceTable || ''}
onValueChange={(value) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
sourceTable: value,
});
}}
>
<SelectTrigger id="source-table" className="text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="filter-column-autofill" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="filter-column-autofill"
value={widget.autoFill?.filterColumn || ''}
onChange={(e) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
filterColumn: e.target.value,
});
}}
placeholder="company_code"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">
: company_code, dept_code, user_id
</p>
</div>
<div className="space-y-2">
<Label htmlFor="user-field-autofill" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.userField || 'companyCode'}
onValueChange={(value: any) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
userField: value,
});
}}
>
<SelectTrigger id="user-field-autofill" className="text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode"> </SelectItem>
<SelectItem value="userId"> ID</SelectItem>
<SelectItem value="deptCode"> </SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="display-column" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="display-column"
value={widget.autoFill?.displayColumn || ''}
onChange={(e) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
displayColumn: e.target.value,
});
}}
placeholder="company_name"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">
Input에 (: company_name, dept_name)
</p>
</div>
</div>
)}
</div>
</div>
{/* 웹타입 설정 */}
<Separator />
{renderWebTypeConfig(widget)}
</div>
</div>
</div>
);
};

View File

@ -118,9 +118,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
{/* 안내 메시지 */}
<Separator className="my-4" />
<div className="flex flex-col items-center justify-center py-8 text-center">
<Settings className="mb-2 h-8 w-8 text-muted-foreground/30" />
<p className="text-[10px] text-muted-foreground"> </p>
<p className="text-[10px] text-muted-foreground"> </p>
<Settings className="text-muted-foreground/30 mb-2 h-8 w-8" />
<p className="text-muted-foreground text-[10px]"> </p>
<p className="text-muted-foreground text-[10px]"> </p>
</div>
</div>
</div>
@ -412,8 +412,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
const renderDetailTab = () => {
console.log("🔍 [renderDetailTab] selectedComponent.type:", selectedComponent.type);
// 1. DataTable 컴포넌트
if (selectedComponent.type === "datatable") {
console.log("✅ [renderDetailTab] DataTable 컴포넌트");
return (
<DataTableConfigPanel
component={selectedComponent as DataTableComponent}
@ -470,6 +473,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
// 5. 새로운 컴포넌트 시스템 (type: "component")
if (selectedComponent.type === "component") {
console.log("✅ [renderDetailTab] Component 타입");
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
const webType = selectedComponent.componentConfig?.webType;
@ -479,7 +483,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
if (!componentId) {
return (
<div className="flex h-full items-center justify-center p-8 text-center">
<p className="text-sm text-muted-foreground"> ID가 </p>
<p className="text-muted-foreground text-sm"> ID가 </p>
</div>
);
}
@ -511,7 +515,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<SelectItem key={option.value} value={option.value}>
<div>
<div className="font-medium">{option.label}</div>
<div className="text-xs text-muted-foreground">{option.description}</div>
<div className="text-muted-foreground text-xs">{option.description}</div>
</div>
</SelectItem>
))}
@ -535,45 +539,154 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
});
}}
/>
{/* 🆕 테이블 데이터 자동 입력 (component 타입용) */}
<Separator />
<div className="space-y-3">
<div className="flex items-center gap-2">
<Database className="text-primary h-4 w-4" />
<h4 className="text-xs font-semibold"> </h4>
</div>
{/* 활성화 체크박스 */}
<div className="flex items-center space-x-2">
<Checkbox
id="autoFill-enabled-component"
checked={selectedComponent.autoFill?.enabled || false}
onCheckedChange={(checked) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: Boolean(checked),
});
}}
/>
<Label htmlFor="autoFill-enabled-component" className="cursor-pointer text-xs">
</Label>
</div>
{selectedComponent.autoFill?.enabled && (
<>
{/* 조회할 테이블 */}
<div className="space-y-1">
<Label htmlFor="autoFill-sourceTable-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.sourceTable || ""}
onValueChange={(value) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
sourceTable: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName} className="text-xs">
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 필터링할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-filterColumn-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-filterColumn-component"
value={selectedComponent.autoFill?.filterColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
filterColumn: e.target.value,
});
}}
placeholder="예: company_code"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
{/* 사용자 정보 필드 */}
<div className="space-y-1">
<Label htmlFor="autoFill-userField-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.userField || ""}
onValueChange={(value: "companyCode" | "userId" | "deptCode") => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
userField: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="사용자 정보 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode" className="text-xs">
</SelectItem>
<SelectItem value="userId" className="text-xs">
ID
</SelectItem>
<SelectItem value="deptCode" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 표시할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-displayColumn-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-displayColumn-component"
value={selectedComponent.autoFill?.displayColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
displayColumn: e.target.value,
});
}}
placeholder="예: company_name"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
</>
)}
</div>
</div>
);
}
// 6. Widget 컴포넌트
if (selectedComponent.type === "widget") {
console.log("✅ [renderDetailTab] Widget 타입");
const widget = selectedComponent as WidgetComponent;
console.log("🔍 [renderDetailTab] widget.widgetType:", widget.widgetType);
// Widget에 webType이 있는 경우
if (widget.webType) {
return (
<div className="space-y-4">
{/* WebType 선택 */}
<div>
<Label> </Label>
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{webTypes.map((wt) => (
<SelectItem key={wt.web_type} value={wt.web_type}>
{wt.web_type_name_kor || wt.web_type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
}
// 새로운 컴포넌트 시스템 (widgetType이 button, card 등)
// 새로운 컴포넌트 시스템 (widgetType이 button, card 등) - 먼저 체크
if (
widget.widgetType &&
["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes(
widget.widgetType,
)
) {
console.log("✅ [renderDetailTab] DynamicComponent 반환 (widgetType)");
return (
<DynamicComponentConfigPanel
componentId={widget.widgetType}
@ -589,12 +702,168 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
/>
);
}
// 일반 위젯 (webType 기반)
console.log("✅ [renderDetailTab] 일반 위젯 렌더링 시작");
return (
<div className="space-y-4">
{console.log("🔍 [UnifiedPropertiesPanel] widget.webType:", widget.webType, "widget:", widget)}
{/* WebType 선택 (있는 경우만) */}
{widget.webType && (
<div>
<Label> </Label>
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{webTypes.map((wt) => (
<SelectItem key={wt.web_type} value={wt.web_type}>
{wt.web_type_name_kor || wt.web_type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 🆕 테이블 데이터 자동 입력 (모든 widget 컴포넌트) */}
<Separator />
<div className="space-y-3 border-4 border-red-500 bg-yellow-100 p-4">
<div className="flex items-center gap-2">
<Database className="text-primary h-4 w-4" />
<h4 className="text-xs font-semibold"> </h4>
</div>
{/* 활성화 체크박스 */}
<div className="flex items-center space-x-2">
<Checkbox
id="autoFill-enabled"
checked={widget.autoFill?.enabled || false}
onCheckedChange={(checked) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: Boolean(checked),
});
}}
/>
<Label htmlFor="autoFill-enabled" className="cursor-pointer text-xs">
</Label>
</div>
{widget.autoFill?.enabled && (
<>
{/* 조회할 테이블 */}
<div className="space-y-1">
<Label htmlFor="autoFill-sourceTable" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.sourceTable || ""}
onValueChange={(value) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
sourceTable: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName} className="text-xs">
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 필터링할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-filterColumn" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-filterColumn"
value={widget.autoFill?.filterColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
filterColumn: e.target.value,
});
}}
placeholder="예: company_code"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
{/* 사용자 정보 필드 */}
<div className="space-y-1">
<Label htmlFor="autoFill-userField" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.userField || ""}
onValueChange={(value: "companyCode" | "userId" | "deptCode") => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
userField: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="사용자 정보 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode" className="text-xs">
</SelectItem>
<SelectItem value="userId" className="text-xs">
ID
</SelectItem>
<SelectItem value="deptCode" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 표시할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-displayColumn" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-displayColumn"
value={widget.autoFill?.displayColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
displayColumn: e.target.value,
});
}}
placeholder="예: company_name"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
</>
)}
</div>
</div>
);
}
// 기본 메시지
return (
<div className="flex h-full items-center justify-center p-8 text-center">
<p className="text-sm text-muted-foreground"> </p>
<p className="text-muted-foreground text-sm"> </p>
</div>
);
};
@ -602,9 +871,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
return (
<div className="flex h-full flex-col bg-white">
{/* 헤더 - 간소화 */}
<div className="border-b border-border px-3 py-2">
<div className="border-border border-b px-3 py-2">
{selectedComponent.type === "widget" && (
<div className="truncate text-[10px] text-muted-foreground">
<div className="text-muted-foreground truncate text-[10px]">
{(selectedComponent as WidgetComponent).label || selectedComponent.id}
</div>
)}

View File

@ -313,6 +313,21 @@ export const tableTypeApi = {
deleteTableData: async (tableName: string, data: Record<string, any>[] | { ids: string[] }): Promise<void> => {
await apiClient.delete(`/table-management/tables/${tableName}/delete`, { data });
},
// 🆕 단일 레코드 조회 (자동 입력용)
getTableRecord: async (
tableName: string,
filterColumn: string,
filterValue: any,
displayColumn: string,
): Promise<{ value: any; record: Record<string, any> }> => {
const response = await apiClient.post(`/table-management/tables/${tableName}/record`, {
filterColumn,
filterValue,
displayColumn,
});
return response.data.data;
},
};
// 메뉴-화면 할당 관련 API

View File

@ -84,6 +84,15 @@ export interface WidgetComponent extends BaseComponent {
entityConfig?: EntityTypeConfig;
buttonConfig?: ButtonTypeConfig;
arrayConfig?: ArrayTypeConfig;
// 🆕 자동 입력 설정 (테이블 조회 기반)
autoFill?: {
enabled: boolean; // 자동 입력 활성화
sourceTable: string; // 조회할 테이블 (예: company_mng)
filterColumn: string; // 필터링할 컬럼 (예: company_code)
userField: 'companyCode' | 'userId' | 'deptCode'; // 사용자 정보 필드
displayColumn: string; // 표시할 컬럼 (예: company_name)
};
}
/**
@ -121,6 +130,13 @@ export interface DataTableComponent extends BaseComponent {
searchable?: boolean;
sortable?: boolean;
filters?: DataTableFilter[];
// 🆕 현재 사용자 정보로 자동 필터링
autoFilter?: {
enabled: boolean; // 자동 필터 활성화 여부
filterColumn: string; // 필터링할 테이블 컬럼 (예: company_code, dept_code)
userField: 'companyCode' | 'userId' | 'deptCode'; // 사용자 정보에서 가져올 필드
};
}
/**