리사이징, 체크박스,엔터치면 다음 칸으로 이동, 표수정, 컬럼에서 이미지 넣는거 등등

This commit is contained in:
leeheejin
2025-11-06 12:11:49 +09:00
parent 0b676098a5
commit 0839f7f603
38 changed files with 1285 additions and 260 deletions

View File

@@ -20,17 +20,26 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
// 모든 hooks를 먼저 호출 (조건부 return 이전에)
const { webTypes } = useWebTypes({ active: "Y" });
// 디버깅: 전달받은 웹타입과 props 정보 로깅
if (webType === "button") {
console.log("🔘 DynamicWebTypeRenderer 버튼 호출:", {
webType,
component: props.component,
position: props.component?.position,
size: props.component?.size,
style: props.component?.style,
config,
});
}
// 디버깅: 이미지 타입만 로깅
// if (webType === "image" || webType === "img" || webType === "picture" || webType === "photo") {
// console.log(`🖼️ DynamicWebTypeRenderer 이미지 호출: webType="${webType}"`, {
// component: props.component,
// readonly: props.readonly,
// value: props.value,
// widgetType: props.component?.widgetType,
// });
// }
// if (webType === "button") {
// console.log("🔘 DynamicWebTypeRenderer 버튼 호출:", {
// webType,
// component: props.component,
// position: props.component?.position,
// size: props.component?.size,
// style: props.component?.style,
// config,
// });
// }
const webTypeDefinition = useMemo(() => {
return WebTypeRegistry.getWebType(webType);
@@ -64,23 +73,35 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
if (webType === "file" || props.component?.type === "file") {
try {
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
console.log(`🎯 최우선: 파일 컴포넌트 → FileUploadComponent 사용`);
// console.log(`🎯 최우선: 파일 컴포넌트 → FileUploadComponent 사용`);
return <FileUploadComponent {...props} {...finalProps} />;
} catch (error) {
console.error("FileUploadComponent 로드 실패:", error);
}
}
// 이미지 컴포넌트 강제 처리
if (webType === "image" || webType === "img" || webType === "picture" || webType === "photo") {
try {
// console.log(`🎯 이미지 컴포넌트 감지! webType: ${webType}`, { props, finalProps });
const { ImageWidget } = require("@/components/screen/widgets/types/ImageWidget");
// console.log(`✅ ImageWidget 로드 성공`);
return <ImageWidget {...props} {...finalProps} />;
} catch (error) {
console.error("❌ ImageWidget 로드 실패:", error);
}
}
// 1순위: DB에서 지정된 컴포넌트 사용 (항상 우선)
if (dbWebType?.component_name) {
try {
console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`);
console.log("DB 웹타입 정보:", dbWebType);
// console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`);
// console.log("DB 웹타입 정보:", dbWebType);
// FileWidget의 경우 FileUploadComponent 직접 사용
if (dbWebType.component_name === "FileWidget" || webType === "file") {
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
console.log("✅ FileWidget → FileUploadComponent 사용");
// console.log("✅ FileWidget → FileUploadComponent 사용");
return <FileUploadComponent {...props} {...finalProps} />;
}
@@ -88,7 +109,7 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
// const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
// console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
// return <ComponentByName {...props} {...finalProps} />;
console.warn(`DB 지정 컴포넌트 "${dbWebType.component_name}" 기능 임시 비활성화 (FileWidget 제외)`);
// console.warn(`DB 지정 컴포넌트 "${dbWebType.component_name}" 기능 임시 비활성화 (FileWidget 제외)`);
// 로딩 중 메시지 대신 레지스트리로 폴백
// return <div>컴포넌트 로딩 중...</div>;
@@ -99,18 +120,18 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
// 2순위: 레지스트리에 등록된 웹타입 사용
if (webTypeDefinition) {
console.log(`웹타입 "${webType}" → 레지스트리 컴포넌트 사용`);
// console.log(`웹타입 "${webType}" → 레지스트리 컴포넌트 사용`);
// 파일 웹타입의 경우 FileUploadComponent 직접 사용
if (webType === "file") {
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
console.log("✅ 파일 웹타입 → FileUploadComponent 사용");
// console.log("✅ 파일 웹타입 → FileUploadComponent 사용");
return <FileUploadComponent {...props} {...finalProps} />;
}
// 웹타입이 비활성화된 경우
if (!webTypeDefinition.isActive) {
console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
// console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
return (
<div className="rounded-md border border-dashed border-yellow-500/30 bg-yellow-50 p-3 dark:bg-yellow-950/20">
<div className="flex items-center gap-2 text-yellow-700 dark:text-yellow-400">
@@ -138,35 +159,35 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
// 파일 웹타입의 경우 FileUploadComponent 직접 사용 (최종 폴백)
if (webType === "file") {
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
console.log("✅ 폴백: 파일 웹타입 → FileUploadComponent 사용");
// console.log("✅ 폴백: 파일 웹타입 → FileUploadComponent 사용");
return <FileUploadComponent {...props} {...finalProps} />;
}
// 텍스트 입력 웹타입들
if (["text", "email", "password", "tel"].includes(webType)) {
const { TextInputComponent } = require("@/lib/registry/components/text-input/TextInputComponent");
console.log(`✅ 폴백: ${webType} 웹타입 → TextInputComponent 사용`);
// console.log(`✅ 폴백: ${webType} 웹타입 → TextInputComponent 사용`);
return <TextInputComponent {...props} {...finalProps} />;
}
// 숫자 입력 웹타입들
if (["number", "decimal"].includes(webType)) {
const { NumberInputComponent } = require("@/lib/registry/components/number-input/NumberInputComponent");
console.log(`✅ 폴백: ${webType} 웹타입 → NumberInputComponent 사용`);
// console.log(`✅ 폴백: ${webType} 웹타입 → NumberInputComponent 사용`);
return <NumberInputComponent {...props} {...finalProps} />;
}
// 날짜 입력 웹타입들
if (["date", "datetime", "time"].includes(webType)) {
const { DateInputComponent } = require("@/lib/registry/components/date-input/DateInputComponent");
console.log(`✅ 폴백: ${webType} 웹타입 → DateInputComponent 사용`);
// console.log(`✅ 폴백: ${webType} 웹타입 → DateInputComponent 사용`);
return <DateInputComponent {...props} {...finalProps} />;
}
// 기본 폴백: Input 컴포넌트 사용
const { Input } = require("@/components/ui/input");
const { filterDOMProps } = require("@/lib/utils/domPropsFilter");
console.log(`✅ 폴백: ${webType} 웹타입 → 기본 Input 사용`);
// console.log(`✅ 폴백: ${webType} 웹타입 → 기본 Input 사용`);
const safeFallbackProps = filterDOMProps(props);
return <Input placeholder={`${webType}`} disabled={props.readonly} className="w-full" {...safeFallbackProps} />;
} catch (error) {

View File

@@ -35,12 +35,35 @@ const WidgetRenderer: ComponentRenderer = ({ component, ...props }) => {
// 동적 웹타입 렌더링 사용
if (widgetType) {
try {
// 파일 위젯의 경우 인터랙션 허용 (pointer-events-none 제거)
// 파일 위젯만 디자인 모드에서 인터랙션 허용
// 이미지 위젯은 실행 모드(모달)에서만 업로드 가능하도록 함
const isFileWidget = widgetType === "file";
const isImageWidget = widgetType === "image" || widgetType === "img" || widgetType === "picture" || widgetType === "photo";
const allowInteraction = isFileWidget;
// 이미지 위젯은 래퍼 없이 직접 렌더링 (크기 문제 해결)
if (isImageWidget) {
return (
<div className="pointer-events-none h-full w-full">
<DynamicWebTypeRenderer
webType={widgetType}
props={{
...commonProps,
component: widget,
value: undefined, // 미리보기이므로 값은 없음
readonly: readonly,
isDesignMode: true, // 디자인 모드임을 명시
...props, // 모든 추가 props 전달 (sortBy, sortOrder 등)
}}
config={widget.webTypeConfig}
/>
</div>
);
}
return (
<div className="flex h-full flex-col">
<div className={isFileWidget ? "flex-1" : "pointer-events-none flex-1"}>
<div className={allowInteraction ? "flex-1" : "pointer-events-none flex-1"}>
<DynamicWebTypeRenderer
webType={widgetType}
props={{

View File

@@ -0,0 +1,69 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
interface ImageWidgetConfigPanelProps {
config: any;
onConfigChange: (config: any) => void;
}
/**
* 이미지 위젯 설정 패널
*/
export function ImageWidgetConfigPanel({ config, onConfigChange }: ImageWidgetConfigPanelProps) {
const handleChange = (key: string, value: any) => {
onConfigChange({
...config,
[key]: value,
});
};
return (
<Card>
<CardHeader>
<CardTitle className="text-sm"> </CardTitle>
<CardDescription className="text-xs"> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="maxSize" className="text-xs">
(MB)
</Label>
<Input
id="maxSize"
type="number"
min="1"
max="10"
value={(config.maxSize || 5 * 1024 * 1024) / (1024 * 1024)}
onChange={(e) => handleChange("maxSize", parseInt(e.target.value) * 1024 * 1024)}
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<Label htmlFor="placeholder" className="text-xs">
</Label>
<Input
id="placeholder"
type="text"
value={config.placeholder || "이미지를 업로드하세요"}
onChange={(e) => handleChange("placeholder", e.target.value)}
className="h-8 text-xs"
/>
</div>
<div className="rounded-md bg-muted p-3 text-xs text-muted-foreground">
<p className="mb-1 font-medium"> :</p>
<p>JPG, PNG, GIF, WebP</p>
</div>
</CardContent>
</Card>
);
}
export default ImageWidgetConfigPanel;

View File

@@ -0,0 +1,57 @@
"use client";
import React from "react";
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
import { ImageWidgetDefinition } from "./index";
import { ImageWidget } from "@/components/screen/widgets/types/ImageWidget";
/**
* ImageWidget 렌더러
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
*/
export class ImageWidgetRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = ImageWidgetDefinition;
render(): React.ReactElement {
return <ImageWidget {...this.props} renderer={this} />;
}
/**
* 컴포넌트별 특화 메서드들
*/
// image 타입 특화 속성 처리
protected getImageWidgetProps() {
const baseProps = this.getWebTypeProps();
// image 타입에 특화된 추가 속성들
return {
...baseProps,
// 여기에 image 타입 특화 속성들 추가
};
}
// 값 변경 처리
protected handleValueChange = (value: any) => {
this.updateComponent({ value });
};
// 포커스 처리
protected handleFocus = () => {
// 포커스 로직
};
// 블러 처리
protected handleBlur = () => {
// 블러 로직
};
}
// 자동 등록 실행
ImageWidgetRenderer.registerSelf();
// Hot Reload 지원 (개발 모드)
if (process.env.NODE_ENV === "development") {
ImageWidgetRenderer.enableHotReload();
}

View File

@@ -0,0 +1,40 @@
"use client";
import React from "react";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import type { WebType } from "@/types/screen";
import { ImageWidget } from "@/components/screen/widgets/types/ImageWidget";
import { ImageWidgetConfigPanel } from "./ImageWidgetConfigPanel";
/**
* ImageWidget 컴포넌트 정의
* image-widget 컴포넌트입니다
*/
export const ImageWidgetDefinition = createComponentDefinition({
id: "image-widget",
name: "이미지 위젯",
nameEng: "Image Widget",
description: "이미지 표시 및 업로드",
category: ComponentCategory.INPUT,
webType: "image",
component: ImageWidget,
defaultConfig: {
type: "image-widget",
webType: "image",
maxSize: 5 * 1024 * 1024, // 5MB
acceptedFormats: ["image/jpeg", "image/png", "image/gif", "image/webp"],
},
defaultSize: { width: 200, height: 200 },
configPanel: ImageWidgetConfigPanel,
icon: "Image",
tags: ["image", "upload", "media", "picture", "photo"],
version: "1.0.0",
author: "개발팀",
documentation: "https://docs.example.com/components/image-widget",
});
// 컴포넌트 내보내기
export { ImageWidget } from "@/components/screen/widgets/types/ImageWidget";
export { ImageWidgetRenderer } from "./ImageWidgetRenderer";

View File

@@ -28,6 +28,7 @@ import "./date-input/DateInputRenderer";
// import "./label-basic/LabelBasicRenderer"; // 제거됨 - text-display로 교체
import "./text-display/TextDisplayRenderer";
import "./file-upload/FileUploadRenderer";
import "./image-widget/ImageWidgetRenderer";
import "./slider-basic/SliderBasicRenderer";
import "./toggle-switch/ToggleSwitchRenderer";
import "./image-display/ImageDisplayRenderer";

View File

@@ -58,14 +58,14 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
return (
<div
className="relative h-full overflow-x-auto overflow-y-auto bg-background shadow-sm backdrop-blur-sm"
className="relative flex h-full flex-col overflow-hidden bg-background shadow-sm"
style={{
width: "100%",
maxWidth: "100%",
maxHeight: "100%", // 최대 높이 제한으로 스크롤 활성화
boxSizing: "border-box",
}}
>
<div className="relative flex-1 overflow-x-auto overflow-y-auto">
<Table
className="w-full"
style={{
@@ -78,9 +78,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
<TableHeader
className={
tableConfig.stickyHeader
? "sticky top-0 z-20 border-b bg-background backdrop-blur-sm"
: "border-b bg-background backdrop-blur-sm"
? "sticky top-0 border-b shadow-md"
: "border-b"
}
style={{
position: "sticky",
top: 0,
zIndex: 50,
backgroundColor: "hsl(var(--background))",
}}
>
<TableRow className="border-b">
{actualColumns.map((column, colIndex) => {
@@ -103,15 +109,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
key={column.columnName}
className={cn(
column.columnName === "__checkbox__"
? "h-10 border-0 px-3 py-2 text-center align-middle sm:h-12 sm:px-6 sm:py-3"
: "h-10 cursor-pointer border-0 px-3 py-2 text-left align-middle font-semibold whitespace-nowrap text-xs text-foreground transition-all duration-200 select-none hover:text-foreground sm:h-12 sm:px-6 sm:py-3 sm:text-sm",
? "h-10 border-0 px-3 py-2 text-center align-middle sm:h-12 sm:px-6 sm:py-3 bg-background"
: "h-10 cursor-pointer border-0 px-3 py-2 text-left align-middle font-semibold whitespace-nowrap text-xs text-foreground transition-all duration-200 select-none hover:text-foreground sm:h-12 sm:px-6 sm:py-3 sm:text-sm bg-background",
`text-${column.align}`,
column.sortable && "hover:bg-primary/10",
// 고정 컬럼 스타일
column.fixed === "left" &&
"sticky z-10 border-r border-border bg-background shadow-sm",
"sticky z-40 border-r border-border bg-background shadow-sm",
column.fixed === "right" &&
"sticky z-10 border-l border-border bg-background shadow-sm",
"sticky z-40 border-l border-border bg-background shadow-sm",
// 숨김 컬럼 스타일 (디자인 모드에서만)
isDesignMode && column.hidden && "bg-muted/50 opacity-40",
)}
@@ -123,6 +129,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
backgroundColor: "hsl(var(--background))",
// sticky 위치 설정
...(column.fixed === "left" && { left: leftFixedWidth }),
...(column.fixed === "right" && { right: rightFixedWidth }),
@@ -245,6 +252,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
)}
</TableBody>
</Table>
</div>
</div>
);
};

View File

@@ -7,6 +7,7 @@ import { tableTypeApi } from "@/lib/api/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { codeCache } from "@/lib/caching/codeCache";
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
import { getFullImageUrl } from "@/lib/api/client";
import { Button } from "@/components/ui/button";
import {
ChevronLeft,
@@ -649,11 +650,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
fetchTableDataDebounced();
};
const handleClearAdvancedFilters = () => {
const handleClearAdvancedFilters = useCallback(() => {
console.log("🔄 필터 초기화 시작", { 이전searchValues: searchValues });
// 상태를 초기화하고 useEffect로 데이터 새로고침
setSearchValues({});
setCurrentPage(1);
fetchTableDataDebounced();
};
// 강제로 데이터 새로고침 트리거
setRefreshTrigger((prev) => prev + 1);
}, [searchValues]);
const handleRefresh = () => {
fetchTableDataDebounced();
@@ -922,6 +928,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
const inputType = meta?.inputType || column.inputType;
// 🖼️ 이미지 타입: 작은 썸네일 표시
if (inputType === "image" && value && typeof value === "string") {
const imageUrl = getFullImageUrl(value);
return (
<img
src={imageUrl}
alt="이미지"
className="h-10 w-10 object-cover rounded"
onError={(e) => {
e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' fill='%23f3f4f6'/%3E%3C/svg%3E";
}}
/>
);
}
// 코드 타입: 코드 값 → 코드명 변환
if (inputType === "code" && meta?.codeCategory && value) {
@@ -1229,7 +1250,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortColumn,
sortDirection,
searchTerm,
searchValues, // 필터 값 변경 시에도 데이터 새로고침
refreshKey,
refreshTrigger, // 강제 새로고침 트리거
isDesignMode,
fetchTableDataDebounced,
]);
@@ -1386,15 +1409,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return (
<div {...domProps}>
{tableConfig.filter?.enabled && (
<div className="px-4 py-3 border-b border-border sm:px-6 sm:py-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
<div className="px-4 py-2 border-b border-border sm:px-6 sm:py-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
<div className="flex-1">
<AdvancedSearchFilters
filters={activeFilters}
searchValues={searchValues}
onSearchValueChange={handleSearchValueChange}
onSearch={handleAdvancedSearch}
onClear={handleClearAdvancedFilters}
onClearFilters={handleClearAdvancedFilters}
/>
</div>
<div className="flex gap-2">
@@ -1432,7 +1455,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
{/* 그룹 표시 배지 */}
{groupByColumns.length > 0 && (
<div className="px-4 py-2 border-b border-border bg-muted/30 sm:px-6">
<div className="px-4 py-1.5 border-b border-border bg-muted/30 sm:px-6">
<div className="flex items-center gap-2 text-xs sm:text-sm">
<span className="text-muted-foreground">:</span>
<div className="flex flex-wrap items-center gap-2">
@@ -1456,7 +1479,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</div>
)}
<div style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 40}px`, flex: 1, overflow: "hidden" }}>
<div style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px`, flex: 1, overflow: "hidden" }}>
<SingleTableWithSticky
data={data}
columns={visibleColumns}
@@ -1501,7 +1524,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
searchValues={searchValues}
onSearchValueChange={handleSearchValueChange}
onSearch={handleAdvancedSearch}
onClear={handleClearAdvancedFilters}
onClearFilters={handleClearAdvancedFilters}
/>
</div>
<div className="flex gap-2">
@@ -1566,11 +1589,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
{/* 테이블 컨테이너 */}
<div
className="flex-1 flex flex-col overflow-hidden w-full max-w-full"
style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 40}px` }}
style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px` }}
>
{/* 스크롤 영역 */}
<div
className="w-full max-w-full h-[400px] overflow-y-scroll overflow-x-auto bg-background sm:h-[500px]"
style={{ position: "relative" }}
>
{/* 테이블 */}
<table
@@ -1586,9 +1610,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
>
{/* 헤더 (sticky) */}
<thead
className="sticky top-0 z-10"
className="sticky z-50"
style={{
position: "sticky",
top: "-2px",
zIndex: 50,
backgroundColor: "hsl(var(--background))",
}}
>
<tr className="h-10 border-b-2 border-primary/20 bg-gradient-to-b from-muted/50 to-muted sm:h-12">
<tr
className="h-10 border-b-2 border-primary/20 bg-muted sm:h-12"
style={{
backgroundColor: "hsl(var(--muted))",
}}
>
{visibleColumns.map((column, columnIndex) => {
const columnWidth = columnWidths[column.columnName];
const isFrozen = frozenColumns.includes(column.columnName);
@@ -1612,7 +1647,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
"relative h-8 text-xs font-bold text-foreground/90 overflow-hidden text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-sm",
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
(column.sortable !== false && column.columnName !== "__checkbox__") && "cursor-pointer hover:bg-muted/70 transition-colors",
isFrozen && "sticky z-20 bg-muted/80 backdrop-blur-sm shadow-[2px_0_4px_rgba(0,0,0,0.1)]"
isFrozen && "sticky z-60 shadow-[2px_0_4px_rgba(0,0,0,0.1)]"
)}
style={{
textAlign: column.columnName === "__checkbox__" ? "center" : "center",
@@ -1620,6 +1655,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
minWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
maxWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
userSelect: 'none',
backgroundColor: "hsl(var(--muted))",
...(isFrozen && { left: `${leftPosition}px` })
}}
onClick={() => {
@@ -1705,7 +1741,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</thead>
{/* 바디 (스크롤) */}
<tbody>
<tbody style={{ position: "relative" }}>
{loading ? (
<tr>
<td colSpan={visibleColumns.length} className="p-12 text-center">

View File

@@ -718,7 +718,13 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
: componentConfig.placeholder || defaultPlaceholder
}
pattern={validationPattern}
title={webType === "tel" ? "전화번호 형식: 010-1234-5678" : undefined}
title={
webType === "tel"
? "전화번호 형식: 010-1234-5678"
: component.label
? `${component.label}${component.columnName ? ` (${component.columnName})` : ""}`
: component.columnName || undefined
}
disabled={componentConfig.disabled || false}
required={componentConfig.required || false}
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}

View File

@@ -1987,7 +1987,12 @@ export class ButtonActionExecutor {
*/
private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📤 엑셀 업로드 모달 열기:", { config, context });
console.log("📤 엑셀 업로드 모달 열기:", {
config,
context,
userId: context.userId,
tableName: context.tableName,
});
// 동적 import로 모달 컴포넌트 로드
const { ExcelUploadModal } = await import("@/components/common/ExcelUploadModal");
@@ -2004,11 +2009,28 @@ export class ButtonActionExecutor {
document.body.removeChild(modalContainer);
};
// localStorage 디버깅
const modalId = `excel-upload-${context.tableName || ""}`;
const storageKey = `modal_size_${modalId}_${context.userId || "guest"}`;
console.log("🔍 엑셀 업로드 모달 localStorage 확인:", {
modalId,
userId: context.userId,
storageKey,
savedSize: localStorage.getItem(storageKey),
});
root.render(
React.createElement(ExcelUploadModal, {
open: true,
onOpenChange: (open: boolean) => {
if (!open) closeModal();
if (!open) {
// 모달 닫을 때 localStorage 확인
console.log("🔍 모달 닫을 때 localStorage:", {
storageKey,
savedSize: localStorage.getItem(storageKey),
});
closeModal();
}
},
tableName: context.tableName || "",
uploadMode: config.excelUploadMode || "insert",

View File

@@ -60,6 +60,16 @@ export const DB_TYPE_TO_WEB_TYPE: Record<string, WebType> = {
* 컬럼명 기반 스마트 웹 타입 추론 규칙
*/
export const COLUMN_NAME_TO_WEB_TYPE: Record<string, WebType> = {
// 이미지 관련
image: "image",
img: "image",
picture: "image",
photo: "image",
thumbnail: "image",
avatar: "image",
icon: "image",
logo: "image",
// 이메일 관련
email: "email",
mail: "email",

View File

@@ -42,6 +42,12 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record<string, string> = {
// 파일
file: "file-upload",
// 이미지
image: "image-widget",
img: "image-widget",
picture: "image-widget",
photo: "image-widget",
// 버튼
button: "button-primary",