화면 다국어 처리
This commit is contained in:
@@ -27,6 +27,7 @@ import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
|
||||
import { applyMappingRules } from "@/lib/utils/dataMapping";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
|
||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||
config?: ButtonPrimaryConfig;
|
||||
@@ -107,6 +108,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const screenContext = useScreenContextOptional(); // 화면 컨텍스트
|
||||
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
|
||||
const { getTranslatedText } = useScreenMultiLang(); // 다국어 컨텍스트
|
||||
// 🆕 ScreenContext에서 splitPanelPosition 가져오기 (중첩 화면에서도 작동)
|
||||
const splitPanelPosition = screenContext?.splitPanelPosition;
|
||||
|
||||
@@ -1285,7 +1287,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
...userStyle,
|
||||
};
|
||||
|
||||
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
||||
// 다국어 적용: componentConfig.langKey가 있으면 번역 텍스트 사용
|
||||
const langKey = (component as any).componentConfig?.langKey;
|
||||
const originalButtonText = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
||||
const buttonContent = getTranslatedText(langKey, originalButtonText);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ColumnConfig } from "./types";
|
||||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
|
||||
interface SingleTableWithStickyProps {
|
||||
visibleColumns?: ColumnConfig[];
|
||||
@@ -74,281 +75,298 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
currentSearchIndex = 0,
|
||||
searchTerm = "",
|
||||
}) => {
|
||||
const { getTranslatedText } = useScreenMultiLang();
|
||||
const checkboxConfig = tableConfig?.checkbox || {};
|
||||
const actualColumns = visibleColumns || columns || [];
|
||||
const sortHandler = onSort || handleSort || (() => {});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex flex-col bg-background shadow-sm"
|
||||
className="bg-background relative flex flex-col shadow-sm"
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<div className="relative overflow-x-auto">
|
||||
<Table
|
||||
className="w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
tableLayout: "auto", // 테이블 크기 자동 조정
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<TableHeader
|
||||
className={cn(
|
||||
"border-b bg-background",
|
||||
tableConfig?.stickyHeader && "sticky top-0 z-30 shadow-sm"
|
||||
)}
|
||||
<Table
|
||||
className="w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
tableLayout: "auto", // 테이블 크기 자동 조정
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<TableRow className="border-b">
|
||||
{actualColumns.map((column, colIndex) => {
|
||||
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
||||
const leftFixedWidth = actualColumns
|
||||
.slice(0, colIndex)
|
||||
.filter((col) => col.fixed === "left")
|
||||
.reduce((sum, col) => sum + getColumnWidth(col), 0);
|
||||
<TableHeader
|
||||
className={cn("bg-background border-b", tableConfig?.stickyHeader && "sticky top-0 z-30 shadow-sm")}
|
||||
>
|
||||
<TableRow className="border-b">
|
||||
{actualColumns.map((column, colIndex) => {
|
||||
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
||||
const leftFixedWidth = actualColumns
|
||||
.slice(0, colIndex)
|
||||
.filter((col) => col.fixed === "left")
|
||||
.reduce((sum, col) => sum + getColumnWidth(col), 0);
|
||||
|
||||
// 오른쪽 고정 컬럼들의 누적 너비 계산
|
||||
const rightFixedColumns = actualColumns.filter((col) => col.fixed === "right");
|
||||
const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName);
|
||||
const rightFixedWidth =
|
||||
rightFixedIndex >= 0
|
||||
? rightFixedColumns.slice(rightFixedIndex + 1).reduce((sum, col) => sum + getColumnWidth(col), 0)
|
||||
: 0;
|
||||
// 오른쪽 고정 컬럼들의 누적 너비 계산
|
||||
const rightFixedColumns = actualColumns.filter((col) => col.fixed === "right");
|
||||
const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName);
|
||||
const rightFixedWidth =
|
||||
rightFixedIndex >= 0
|
||||
? rightFixedColumns.slice(rightFixedIndex + 1).reduce((sum, col) => sum + getColumnWidth(col), 0)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<TableHead
|
||||
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 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-40 border-r border-border bg-background shadow-sm",
|
||||
column.fixed === "right" &&
|
||||
"sticky z-40 border-l border-border bg-background shadow-sm",
|
||||
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
||||
isDesignMode && column.hidden && "bg-muted/50 opacity-40",
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth(column),
|
||||
minWidth: "100px", // 최소 너비 보장
|
||||
maxWidth: "300px", // 최대 너비 제한
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
}}
|
||||
onClick={() => column.sortable && sortHandler(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
checkboxConfig.selectAll && (
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
aria-label="전체 선택"
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 truncate">
|
||||
{columnLabels[column.columnName] || column.displayName || column.columnName}
|
||||
</span>
|
||||
{column.sortable && sortColumn === column.columnName && (
|
||||
<span className="ml-1 flex h-4 w-4 items-center justify-center rounded-md bg-background/50 shadow-sm sm:ml-2 sm:h-5 sm:w-5">
|
||||
{sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-2.5 w-2.5 text-primary sm:h-3.5 sm:w-3.5" />
|
||||
) : (
|
||||
<ArrowDown className="h-2.5 w-2.5 text-primary sm:h-3.5 sm:w-3.5" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
return (
|
||||
<TableHead
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
column.columnName === "__checkbox__"
|
||||
? "bg-background h-10 border-0 px-3 py-2 text-center align-middle sm:h-12 sm:px-6 sm:py-3"
|
||||
: "text-foreground hover:text-foreground bg-background h-10 cursor-pointer border-0 px-3 py-2 text-left align-middle text-xs font-semibold whitespace-nowrap transition-all duration-200 select-none sm:h-12 sm:px-6 sm:py-3 sm:text-sm",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-primary/10",
|
||||
// 고정 컬럼 스타일
|
||||
column.fixed === "left" && "border-border bg-background sticky z-40 border-r shadow-sm",
|
||||
column.fixed === "right" && "border-border bg-background sticky z-40 border-l shadow-sm",
|
||||
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
||||
isDesignMode && column.hidden && "bg-muted/50 opacity-40",
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={visibleColumns.length} className="py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center space-y-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
||||
<svg className="h-6 w-6 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted-foreground">데이터가 없습니다</span>
|
||||
<span className="rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground">
|
||||
조건을 변경하여 다시 검색해보세요
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
style={{
|
||||
width: getColumnWidth(column),
|
||||
minWidth: "100px", // 최소 너비 보장
|
||||
maxWidth: "300px", // 최대 너비 제한
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
}}
|
||||
onClick={() => column.sortable && sortHandler(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
checkboxConfig.selectAll && (
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
aria-label="전체 선택"
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 truncate">
|
||||
{/* langKey가 있으면 다국어 번역 사용, 없으면 기존 라벨 */}
|
||||
{(column as any).langKey
|
||||
? getTranslatedText(
|
||||
(column as any).langKey,
|
||||
columnLabels[column.columnName] || column.displayName || column.columnName,
|
||||
)
|
||||
: columnLabels[column.columnName] || column.displayName || column.columnName}
|
||||
</span>
|
||||
{column.sortable && sortColumn === column.columnName && (
|
||||
<span className="bg-background/50 ml-1 flex h-4 w-4 items-center justify-center rounded-md shadow-sm sm:ml-2 sm:h-5 sm:w-5">
|
||||
{sortDirection === "asc" ? (
|
||||
<ArrowUp className="text-primary h-2.5 w-2.5 sm:h-3.5 sm:w-3.5" />
|
||||
) : (
|
||||
<ArrowDown className="text-primary h-2.5 w-2.5 sm:h-3.5 sm:w-3.5" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={`row-${index}`}
|
||||
className={cn(
|
||||
"h-14 cursor-pointer border-b transition-colors bg-background sm:h-16",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-muted/50",
|
||||
)}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{visibleColumns.map((column, colIndex) => {
|
||||
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
||||
const leftFixedWidth = visibleColumns
|
||||
.slice(0, colIndex)
|
||||
.filter((col) => col.fixed === "left")
|
||||
.reduce((sum, col) => sum + getColumnWidth(col), 0);
|
||||
</TableHeader>
|
||||
|
||||
// 오른쪽 고정 컬럼들의 누적 너비 계산
|
||||
const rightFixedColumns = visibleColumns.filter((col) => col.fixed === "right");
|
||||
const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName);
|
||||
const rightFixedWidth =
|
||||
rightFixedIndex >= 0
|
||||
? rightFixedColumns.slice(rightFixedIndex + 1).reduce((sum, col) => sum + getColumnWidth(col), 0)
|
||||
: 0;
|
||||
<TableBody>
|
||||
{data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={visibleColumns.length} className="py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center space-y-3">
|
||||
<div className="bg-muted flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<svg
|
||||
className="text-muted-foreground h-6 w-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-muted-foreground text-sm font-medium">데이터가 없습니다</span>
|
||||
<span className="bg-muted text-muted-foreground rounded-full px-3 py-1 text-xs">
|
||||
조건을 변경하여 다시 검색해보세요
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={`row-${index}`}
|
||||
className={cn(
|
||||
"bg-background h-14 cursor-pointer border-b transition-colors sm:h-16",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-muted/50",
|
||||
)}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{visibleColumns.map((column, colIndex) => {
|
||||
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
||||
const leftFixedWidth = visibleColumns
|
||||
.slice(0, colIndex)
|
||||
.filter((col) => col.fixed === "left")
|
||||
.reduce((sum, col) => sum + getColumnWidth(col), 0);
|
||||
|
||||
// 현재 셀이 편집 중인지 확인
|
||||
const isEditing = editingCell?.rowIndex === index && editingCell?.colIndex === colIndex;
|
||||
// 오른쪽 고정 컬럼들의 누적 너비 계산
|
||||
const rightFixedColumns = visibleColumns.filter((col) => col.fixed === "right");
|
||||
const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName);
|
||||
const rightFixedWidth =
|
||||
rightFixedIndex >= 0
|
||||
? rightFixedColumns
|
||||
.slice(rightFixedIndex + 1)
|
||||
.reduce((sum, col) => sum + getColumnWidth(col), 0)
|
||||
: 0;
|
||||
|
||||
// 검색 하이라이트 확인 - 실제 셀 값에 검색어가 포함되어 있는지도 확인
|
||||
const cellKey = `${index}-${colIndex}`;
|
||||
const cellValue = String(row[column.columnName] ?? "").toLowerCase();
|
||||
const hasSearchTerm = searchTerm ? cellValue.includes(searchTerm.toLowerCase()) : false;
|
||||
|
||||
// 인덱스 기반 하이라이트 + 실제 값 검증
|
||||
const isHighlighted = column.columnName !== "__checkbox__" &&
|
||||
hasSearchTerm &&
|
||||
(searchHighlights?.has(cellKey) ?? false);
|
||||
|
||||
// 현재 검색 결과인지 확인 (currentSearchIndex가 -1이면 현재 페이지에 없음)
|
||||
const highlightArray = searchHighlights ? Array.from(searchHighlights) : [];
|
||||
const isCurrentSearchResult = isHighlighted &&
|
||||
currentSearchIndex >= 0 &&
|
||||
currentSearchIndex < highlightArray.length &&
|
||||
highlightArray[currentSearchIndex] === cellKey;
|
||||
// 현재 셀이 편집 중인지 확인
|
||||
const isEditing = editingCell?.rowIndex === index && editingCell?.colIndex === colIndex;
|
||||
|
||||
// 셀 값에서 검색어 하이라이트 렌더링
|
||||
const renderCellContent = () => {
|
||||
const cellValue = formatCellValue(row[column.columnName], column.format, column.columnName, row) || "\u00A0";
|
||||
|
||||
if (!isHighlighted || !searchTerm || column.columnName === "__checkbox__") {
|
||||
return cellValue;
|
||||
}
|
||||
// 검색 하이라이트 확인 - 실제 셀 값에 검색어가 포함되어 있는지도 확인
|
||||
const cellKey = `${index}-${colIndex}`;
|
||||
const cellValue = String(row[column.columnName] ?? "").toLowerCase();
|
||||
const hasSearchTerm = searchTerm ? cellValue.includes(searchTerm.toLowerCase()) : false;
|
||||
|
||||
// 검색어 하이라이트 처리
|
||||
const lowerValue = String(cellValue).toLowerCase();
|
||||
const lowerTerm = searchTerm.toLowerCase();
|
||||
const startIndex = lowerValue.indexOf(lowerTerm);
|
||||
|
||||
if (startIndex === -1) return cellValue;
|
||||
// 인덱스 기반 하이라이트 + 실제 값 검증
|
||||
const isHighlighted =
|
||||
column.columnName !== "__checkbox__" &&
|
||||
hasSearchTerm &&
|
||||
(searchHighlights?.has(cellKey) ?? false);
|
||||
|
||||
const before = String(cellValue).slice(0, startIndex);
|
||||
const match = String(cellValue).slice(startIndex, startIndex + searchTerm.length);
|
||||
const after = String(cellValue).slice(startIndex + searchTerm.length);
|
||||
// 현재 검색 결과인지 확인 (currentSearchIndex가 -1이면 현재 페이지에 없음)
|
||||
const highlightArray = searchHighlights ? Array.from(searchHighlights) : [];
|
||||
const isCurrentSearchResult =
|
||||
isHighlighted &&
|
||||
currentSearchIndex >= 0 &&
|
||||
currentSearchIndex < highlightArray.length &&
|
||||
highlightArray[currentSearchIndex] === cellKey;
|
||||
|
||||
// 셀 값에서 검색어 하이라이트 렌더링
|
||||
const renderCellContent = () => {
|
||||
const cellValue =
|
||||
formatCellValue(row[column.columnName], column.format, column.columnName, row) || "\u00A0";
|
||||
|
||||
if (!isHighlighted || !searchTerm || column.columnName === "__checkbox__") {
|
||||
return cellValue;
|
||||
}
|
||||
|
||||
// 검색어 하이라이트 처리
|
||||
const lowerValue = String(cellValue).toLowerCase();
|
||||
const lowerTerm = searchTerm.toLowerCase();
|
||||
const startIndex = lowerValue.indexOf(lowerTerm);
|
||||
|
||||
if (startIndex === -1) return cellValue;
|
||||
|
||||
const before = String(cellValue).slice(0, startIndex);
|
||||
const match = String(cellValue).slice(startIndex, startIndex + searchTerm.length);
|
||||
const after = String(cellValue).slice(startIndex + searchTerm.length);
|
||||
|
||||
return (
|
||||
<>
|
||||
{before}
|
||||
<mark
|
||||
className={cn(
|
||||
"rounded px-0.5",
|
||||
isCurrentSearchResult
|
||||
? "bg-orange-400 font-semibold text-white"
|
||||
: "bg-yellow-200 text-yellow-900",
|
||||
)}
|
||||
>
|
||||
{match}
|
||||
</mark>
|
||||
{after}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{before}
|
||||
<mark className={cn(
|
||||
"rounded px-0.5",
|
||||
isCurrentSearchResult
|
||||
? "bg-orange-400 text-white font-semibold"
|
||||
: "bg-yellow-200 text-yellow-900"
|
||||
)}>
|
||||
{match}
|
||||
</mark>
|
||||
{after}
|
||||
</>
|
||||
<TableCell
|
||||
key={`cell-${column.columnName}`}
|
||||
id={isCurrentSearchResult ? "current-search-result" : undefined}
|
||||
className={cn(
|
||||
"text-foreground h-14 px-3 py-2 align-middle text-xs whitespace-nowrap transition-colors sm:h-16 sm:px-6 sm:py-3 sm:text-sm",
|
||||
`text-${column.align}`,
|
||||
// 고정 컬럼 스타일
|
||||
column.fixed === "left" &&
|
||||
"border-border bg-background/90 sticky z-10 border-r backdrop-blur-sm",
|
||||
column.fixed === "right" &&
|
||||
"border-border bg-background/90 sticky z-10 border-l backdrop-blur-sm",
|
||||
// 편집 가능 셀 스타일
|
||||
onCellDoubleClick && column.columnName !== "__checkbox__" && "cursor-text",
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth(column),
|
||||
minWidth: "100px", // 최소 너비 보장
|
||||
maxWidth: "300px", // 최대 너비 제한
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
if (onCellDoubleClick && column.columnName !== "__checkbox__") {
|
||||
e.stopPropagation();
|
||||
onCellDoubleClick(index, colIndex, column.columnName, row[column.columnName]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
renderCheckboxCell(row, index)
|
||||
) : isEditing ? (
|
||||
// 인라인 편집 입력 필드
|
||||
<input
|
||||
ref={editInputRef}
|
||||
type="text"
|
||||
value={editingValue ?? ""}
|
||||
onChange={(e) => onEditingValueChange?.(e.target.value)}
|
||||
onKeyDown={onEditKeyDown}
|
||||
onBlur={() => {
|
||||
// blur 시 저장 (Enter와 동일)
|
||||
if (onEditKeyDown) {
|
||||
const fakeEvent = {
|
||||
key: "Enter",
|
||||
preventDefault: () => {},
|
||||
} as React.KeyboardEvent<HTMLInputElement>;
|
||||
onEditKeyDown(fakeEvent);
|
||||
}
|
||||
}}
|
||||
className="border-primary bg-background focus:ring-primary h-8 w-full rounded border px-2 text-xs focus:ring-2 focus:outline-none sm:text-sm"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
renderCellContent()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={`cell-${column.columnName}`}
|
||||
id={isCurrentSearchResult ? "current-search-result" : undefined}
|
||||
className={cn(
|
||||
"h-14 px-3 py-2 align-middle text-xs whitespace-nowrap text-foreground transition-colors sm:h-16 sm:px-6 sm:py-3 sm:text-sm",
|
||||
`text-${column.align}`,
|
||||
// 고정 컬럼 스타일
|
||||
column.fixed === "left" &&
|
||||
"sticky z-10 border-r border-border bg-background/90 backdrop-blur-sm",
|
||||
column.fixed === "right" &&
|
||||
"sticky z-10 border-l border-border bg-background/90 backdrop-blur-sm",
|
||||
// 편집 가능 셀 스타일
|
||||
onCellDoubleClick && column.columnName !== "__checkbox__" && "cursor-text",
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth(column),
|
||||
minWidth: "100px", // 최소 너비 보장
|
||||
maxWidth: "300px", // 최대 너비 제한
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
if (onCellDoubleClick && column.columnName !== "__checkbox__") {
|
||||
e.stopPropagation();
|
||||
onCellDoubleClick(index, colIndex, column.columnName, row[column.columnName]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
renderCheckboxCell(row, index)
|
||||
) : isEditing ? (
|
||||
// 인라인 편집 입력 필드
|
||||
<input
|
||||
ref={editInputRef}
|
||||
type="text"
|
||||
value={editingValue ?? ""}
|
||||
onChange={(e) => onEditingValueChange?.(e.target.value)}
|
||||
onKeyDown={onEditKeyDown}
|
||||
onBlur={() => {
|
||||
// blur 시 저장 (Enter와 동일)
|
||||
if (onEditKeyDown) {
|
||||
const fakeEvent = { key: "Enter", preventDefault: () => {} } as React.KeyboardEvent<HTMLInputElement>;
|
||||
onEditKeyDown(fakeEvent);
|
||||
}
|
||||
}}
|
||||
className="h-8 w-full rounded border border-primary bg-background px-2 text-xs focus:outline-none focus:ring-2 focus:ring-primary sm:text-sm"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
renderCellContent()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -67,6 +67,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
|
||||
import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer";
|
||||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
|
||||
// ========================================
|
||||
// 인터페이스
|
||||
@@ -243,6 +244,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
parentTabsComponentId,
|
||||
companyCode,
|
||||
}) => {
|
||||
// ========================================
|
||||
// 다국어 번역 훅
|
||||
// ========================================
|
||||
const { getTranslatedText } = useScreenMultiLang();
|
||||
|
||||
// ========================================
|
||||
// 설정 및 스타일
|
||||
// ========================================
|
||||
@@ -5821,7 +5827,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
rowSpan={2}
|
||||
className="border-primary/10 border-r px-2 py-1 text-center text-xs font-semibold sm:px-4 sm:text-sm"
|
||||
>
|
||||
{columnLabels[column.columnName] || column.columnName}
|
||||
{/* langKey가 있으면 다국어 번역 사용 */}
|
||||
{(column as any).langKey
|
||||
? getTranslatedText((column as any).langKey, columnLabels[column.columnName] || column.columnName)
|
||||
: columnLabels[column.columnName] || column.columnName}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
@@ -5917,7 +5926,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
<Lock className="text-muted-foreground h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||
<span>
|
||||
{/* langKey가 있으면 다국어 번역 사용 */}
|
||||
{(column as any).langKey
|
||||
? getTranslatedText((column as any).langKey, columnLabels[column.columnName] || column.displayName || column.columnName)
|
||||
: columnLabels[column.columnName] || column.displayName}
|
||||
</span>
|
||||
{column.sortable !== false && sortColumn === column.columnName && (
|
||||
<span>{sortDirection === "asc" ? "↑" : "↓"}</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user