This commit is contained in:
parent
cfd7ee9fce
commit
b293d184bb
|
|
@ -76,10 +76,12 @@ export function ColumnDetailPanel({
|
||||||
|
|
||||||
if (!column) return null;
|
if (!column) return null;
|
||||||
|
|
||||||
const refTableOpts = referenceTableOptions.length
|
const refTableOpts = useMemo(() => {
|
||||||
? referenceTableOptions
|
const hasKorean = (s: string) => /[가-힣]/.test(s);
|
||||||
|
const raw = referenceTableOptions.length
|
||||||
|
? [...referenceTableOptions]
|
||||||
: [
|
: [
|
||||||
{ value: "none", label: "선택 안함" },
|
{ value: "none", label: "없음" },
|
||||||
...tables.map((t) => ({
|
...tables.map((t) => ({
|
||||||
value: t.tableName,
|
value: t.tableName,
|
||||||
label:
|
label:
|
||||||
|
|
@ -89,6 +91,20 @@ export function ColumnDetailPanel({
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const noneOpt = raw.find((o) => o.value === "none");
|
||||||
|
const rest = raw.filter((o) => o.value !== "none");
|
||||||
|
|
||||||
|
rest.sort((a, b) => {
|
||||||
|
const aK = hasKorean(a.label);
|
||||||
|
const bK = hasKorean(b.label);
|
||||||
|
if (aK && !bK) return -1;
|
||||||
|
if (!aK && bK) return 1;
|
||||||
|
return a.label.localeCompare(b.label, "ko");
|
||||||
|
});
|
||||||
|
|
||||||
|
return noneOpt ? [noneOpt, ...rest] : rest;
|
||||||
|
}, [referenceTableOptions, tables]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col border-l bg-card">
|
<div className="flex h-full w-full flex-col border-l bg-card">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
|
|
@ -183,7 +199,9 @@ export function ColumnDetailPanel({
|
||||||
<CommandList className="max-h-[200px]">
|
<CommandList className="max-h-[200px]">
|
||||||
<CommandEmpty className="py-2 text-center text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
<CommandEmpty className="py-2 text-center text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{refTableOpts.map((opt) => (
|
{refTableOpts.map((opt) => {
|
||||||
|
const hasKorean = opt.value !== "none" && opt.label !== opt.value && !opt.label.startsWith(opt.value);
|
||||||
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
value={`${opt.label} ${opt.value}`}
|
value={`${opt.label} ${opt.value}`}
|
||||||
|
|
@ -197,9 +215,17 @@ export function ColumnDetailPanel({
|
||||||
<Check
|
<Check
|
||||||
className={cn("mr-2 h-3 w-3", column.referenceTable === opt.value ? "opacity-100" : "opacity-0")}
|
className={cn("mr-2 h-3 w-3", column.referenceTable === opt.value ? "opacity-100" : "opacity-0")}
|
||||||
/>
|
/>
|
||||||
{opt.label}
|
{hasKorean ? (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{opt.label.replace(` (${opt.value})`, "")}</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">{opt.value}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
opt.label
|
||||||
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
|
|
@ -263,13 +289,14 @@ export function ColumnDetailPanel({
|
||||||
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{refCol.displayName && refCol.displayName !== refCol.columnName ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">
|
<span className="font-medium">{refCol.displayName}</span>
|
||||||
{refCol.displayName && refCol.displayName !== refCol.columnName
|
<span className="text-[10px] text-muted-foreground">{refCol.columnName}</span>
|
||||||
? `${refCol.displayName} (${refCol.columnName})`
|
|
||||||
: refCol.columnName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<span>{refCol.columnName}</span>
|
||||||
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|
|
||||||
|
|
@ -568,18 +568,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : [];
|
||||||
return (
|
|
||||||
<div className="flex h-screen items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
|
|
||||||
<p>로딩중...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
|
|
||||||
|
|
||||||
// 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장
|
// 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -603,6 +592,17 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
}
|
}
|
||||||
}, [activeTab, uiMenus, isMenuActive, expandedMenus]);
|
}, [activeTab, uiMenus, isMenuActive, expandedMenus]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
|
||||||
|
<p>로딩중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-screen flex-col">
|
<div className="bg-background flex h-screen flex-col">
|
||||||
{/* 모바일 헤더 */}
|
{/* 모바일 헤더 */}
|
||||||
|
|
|
||||||
|
|
@ -493,8 +493,8 @@ export function TabBar() {
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative flex h-7 shrink-0 cursor-pointer items-center gap-0.5 rounded-t-md border border-b-0 px-3 select-none",
|
"group relative flex h-7 shrink-0 cursor-pointer items-center gap-0.5 rounded-t-md border border-b-0 px-3 select-none",
|
||||||
isActive
|
isActive
|
||||||
? "text-foreground z-10 -mb-px h-[30px] bg-white"
|
? "text-primary z-10 -mb-px h-[30px] bg-primary/15 dark:bg-primary/20 border-primary/40 border-t-[3px] border-t-primary font-semibold"
|
||||||
: "bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-transparent",
|
: "bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground border-transparent",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: TAB_WIDTH,
|
width: TAB_WIDTH,
|
||||||
|
|
|
||||||
|
|
@ -1552,16 +1552,22 @@ export const V2SplitPanelLayoutConfigPanel: React.FC<
|
||||||
/>
|
/>
|
||||||
<SwitchRow
|
<SwitchRow
|
||||||
label="수정 버튼"
|
label="수정 버튼"
|
||||||
checked={config.rightPanel?.showEdit ?? false}
|
checked={(config.rightPanel?.showEdit ?? config.rightPanel?.editButton?.enabled) ?? false}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateRightPanel({ showEdit: checked })
|
updateRightPanel({
|
||||||
|
showEdit: checked,
|
||||||
|
editButton: { ...config.rightPanel?.editButton!, enabled: checked },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SwitchRow
|
<SwitchRow
|
||||||
label="삭제 버튼"
|
label="삭제 버튼"
|
||||||
checked={config.rightPanel?.showDelete ?? false}
|
checked={(config.rightPanel?.showDelete ?? config.rightPanel?.deleteButton?.enabled) ?? false}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateRightPanel({ showDelete: checked })
|
updateRightPanel({
|
||||||
|
showDelete: checked,
|
||||||
|
deleteButton: { ...config.rightPanel?.deleteButton!, enabled: checked },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
Move,
|
Move,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
List,
|
List,
|
||||||
LayoutPanelRight,
|
PanelRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { dataApi } from "@/lib/api/data";
|
import { dataApi } from "@/lib/api/data";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
|
|
@ -3524,7 +3524,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
<th
|
<th
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
|
className="px-3 py-[7px] text-left text-[9px] font-bold tracking-[0.04em] text-muted-foreground uppercase whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
|
|
@ -3534,7 +3534,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{hasGroupedLeftActions && (
|
{hasGroupedLeftActions && (
|
||||||
<th className="bg-muted sticky right-0 z-10 px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
|
<th className="bg-muted sticky right-0 z-10 px-3 py-[7px] text-right text-[9px] font-bold tracking-[0.04em] text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -3621,7 +3621,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
<th
|
<th
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
|
className="px-3 py-[7px] text-left text-[9px] font-bold tracking-[0.04em] text-muted-foreground uppercase whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
|
|
@ -3631,7 +3631,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{hasLeftTableActions && (
|
{hasLeftTableActions && (
|
||||||
<th className="bg-muted sticky right-0 z-10 px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
|
<th className="bg-muted sticky right-0 z-10 px-3 py-[7px] text-right text-[9px] font-bold tracking-[0.04em] text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -3972,17 +3972,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
<LayoutPanelRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<PanelRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
{/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */}
|
{/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */}
|
||||||
{(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
|
{(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
|
||||||
<div className="flex items-center gap-0">
|
<div className="flex items-center gap-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTabChange(0)}
|
onClick={() => handleTabChange(0)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-3 py-1 text-sm font-medium transition-colors",
|
"px-3.5 py-2 text-[10px] font-semibold transition-all -mb-px",
|
||||||
activeTabIndex === 0
|
activeTabIndex === 0
|
||||||
? "text-primary border-b-2 border-primary font-semibold bg-primary/5"
|
? "text-primary border-b-2 border-primary"
|
||||||
: "text-foreground/70 hover:text-foreground hover:bg-muted/30"
|
: "text-muted-foreground border-b-2 border-transparent hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{componentConfig.rightPanel?.title || "기본"}
|
{componentConfig.rightPanel?.title || "기본"}
|
||||||
|
|
@ -3992,10 +3992,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
key={tab.tabId || `tab-${index}`}
|
key={tab.tabId || `tab-${index}`}
|
||||||
onClick={() => handleTabChange(index + 1)}
|
onClick={() => handleTabChange(index + 1)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-3 py-1 text-sm font-medium transition-colors",
|
"px-3.5 py-2 text-[10px] font-semibold transition-all -mb-px",
|
||||||
activeTabIndex === index + 1
|
activeTabIndex === index + 1
|
||||||
? "text-primary border-b-2 border-primary font-semibold bg-primary/5"
|
? "text-primary border-b-2 border-primary"
|
||||||
: "text-foreground/70 hover:text-foreground hover:bg-muted/30"
|
: "text-muted-foreground border-b-2 border-transparent hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tab.label || `탭 ${index + 1}`}
|
{tab.label || `탭 ${index + 1}`}
|
||||||
|
|
@ -4120,7 +4120,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<th
|
<th
|
||||||
key={col.name}
|
key={col.name}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold",
|
"text-muted-foreground px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em]",
|
||||||
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||||
canDragTabColumns && "cursor-grab active:cursor-grabbing",
|
canDragTabColumns && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "opacity-50",
|
isDragging && "opacity-50",
|
||||||
|
|
@ -4136,7 +4136,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{hasTabActions && (
|
{hasTabActions && (
|
||||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
<th className="text-muted-foreground px-3 py-[7px] text-right text-[9px] font-bold uppercase tracking-[0.04em]">작업</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -4157,13 +4157,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<React.Fragment key={tabItemId}>
|
<React.Fragment key={tabItemId}>
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer border-b border-border/40 transition-colors",
|
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
||||||
>
|
>
|
||||||
{tabSummaryColumns.map((col: any) => (
|
{tabSummaryColumns.map((col: any) => (
|
||||||
<td key={col.name} className="px-3 py-2 text-xs" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
<td key={col.name} className="px-3 py-2 text-[11px]" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||||
{col.type === "progress"
|
{col.type === "progress"
|
||||||
? renderProgressCell(col, item, selectedLeftItem)
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
: formatCellValue(
|
: formatCellValue(
|
||||||
|
|
@ -4256,7 +4256,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<th
|
<th
|
||||||
key={col.name}
|
key={col.name}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold",
|
"text-muted-foreground px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em]",
|
||||||
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||||
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
|
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "opacity-50",
|
isDragging && "opacity-50",
|
||||||
|
|
@ -4272,7 +4272,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{hasTabActions && (
|
{hasTabActions && (
|
||||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
<th className="text-muted-foreground px-3 py-[7px] text-right text-[9px] font-bold uppercase tracking-[0.04em]">작업</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -4292,13 +4292,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<React.Fragment key={tabItemId}>
|
<React.Fragment key={tabItemId}>
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer border-b border-border/40 transition-colors",
|
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
||||||
>
|
>
|
||||||
{listSummaryColumns.map((col: any) => (
|
{listSummaryColumns.map((col: any) => (
|
||||||
<td key={col.name} className="px-3 py-2 text-xs" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
<td key={col.name} className="px-3 py-2 text-[11px]" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||||
{col.type === "progress"
|
{col.type === "progress"
|
||||||
? renderProgressCell(col, item, selectedLeftItem)
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
: formatCellValue(
|
: formatCellValue(
|
||||||
|
|
@ -4670,7 +4670,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<th
|
<th
|
||||||
key={idx}
|
key={idx}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap",
|
"text-muted-foreground px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em] whitespace-nowrap",
|
||||||
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||||
isDraggable && "cursor-grab active:cursor-grabbing",
|
isDraggable && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "opacity-50",
|
isDragging && "opacity-50",
|
||||||
|
|
@ -4689,26 +4689,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 컬럼 표시 */}
|
{(() => {
|
||||||
{!isDesignMode &&
|
const rightEditVisible = (componentConfig.rightPanel?.showEdit ?? componentConfig.rightPanel?.editButton?.enabled) !== false;
|
||||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
|
||||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
return !isDesignMode && (rightEditVisible || rightDeleteVisible) ? (
|
||||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold" style={{ width: '80px' }}>
|
<th className="bg-background text-muted-foreground sticky right-0 z-10 px-3 py-[7px] text-right text-[9px] font-bold uppercase tracking-[0.04em]" style={{ width: '80px' }}>
|
||||||
작업
|
작업
|
||||||
</th>
|
</th>
|
||||||
)}
|
) : null;
|
||||||
|
})()}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredData.map((item, idx) => {
|
{filteredData.map((item, idx) => {
|
||||||
const itemId = item.id || item.ID || idx;
|
const itemId = item.id || item.ID || idx;
|
||||||
|
const rightEditVisible = (componentConfig.rightPanel?.showEdit ?? componentConfig.rightPanel?.editButton?.enabled) !== false;
|
||||||
|
const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={itemId} className={cn("group/action border-b border-border/40 transition-colors hover:bg-muted/30", idx % 2 === 1 && "bg-muted/10")}>
|
<tr key={itemId} className={cn("group/action border-b border-border/50 transition-[background] duration-75 hover:bg-accent", idx % 2 === 1 && "bg-muted/50")}>
|
||||||
{columnsToShow.map((col, colIdx) => (
|
{columnsToShow.map((col, colIdx) => (
|
||||||
<td
|
<td
|
||||||
key={colIdx}
|
key={colIdx}
|
||||||
className="px-3 py-2 text-xs"
|
className="px-3 py-2 text-[11px]"
|
||||||
style={{ textAlign: col.align || "left", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
style={{ textAlign: col.align || "left", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||||
>
|
>
|
||||||
{col.type === "progress"
|
{col.type === "progress"
|
||||||
|
|
@ -4722,12 +4725,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
||||||
{!isDesignMode &&
|
{!isDesignMode && (rightEditVisible || rightDeleteVisible) && (
|
||||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
<td className="bg-card sticky right-0 z-10 px-3 py-2 text-right text-sm whitespace-nowrap group-hover/action:bg-accent">
|
||||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
|
||||||
<td className="px-3 py-2 text-right text-sm whitespace-nowrap group/action">
|
|
||||||
<div className="flex justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
|
<div className="flex justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
|
||||||
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
{rightEditVisible && (
|
||||||
<Button
|
<Button
|
||||||
variant={
|
variant={
|
||||||
componentConfig.rightPanel?.editButton?.buttonVariant || "outline"
|
componentConfig.rightPanel?.editButton?.buttonVariant || "outline"
|
||||||
|
|
@ -4743,7 +4744,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && (
|
{rightDeleteVisible && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -4801,8 +4802,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return sum + w;
|
return sum + w;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
|
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.showEdit ?? componentConfig.rightPanel?.editButton?.enabled) !== false;
|
||||||
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
|
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
|
||||||
const hasActions = hasEditButton || hasDeleteButton;
|
const hasActions = hasEditButton || hasDeleteButton;
|
||||||
|
|
||||||
return filteredData.length > 0 ? (
|
return filteredData.length > 0 ? (
|
||||||
|
|
@ -4814,14 +4815,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{columnsToDisplay.map((col) => (
|
{columnsToDisplay.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={col.name}
|
key={col.name}
|
||||||
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
|
className="text-muted-foreground px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em] whitespace-nowrap"
|
||||||
style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}
|
style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{hasActions && (
|
{hasActions && (
|
||||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold" style={{ width: '80px' }}>작업</th>
|
<th className="bg-background text-muted-foreground sticky right-0 z-10 px-3 py-[7px] text-right text-[9px] font-bold uppercase tracking-[0.04em]" style={{ width: '80px' }}>작업</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -4849,13 +4850,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<React.Fragment key={itemId}>
|
<React.Fragment key={itemId}>
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/action cursor-pointer border-b border-border/40 transition-colors",
|
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleRightItemExpansion(itemId)}
|
onClick={() => toggleRightItemExpansion(itemId)}
|
||||||
>
|
>
|
||||||
{columnsToDisplay.map((col) => (
|
{columnsToDisplay.map((col) => (
|
||||||
<td key={col.name} className="px-3 py-2 text-xs" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
<td key={col.name} className="px-3 py-2 text-[11px]" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||||
{formatCellValue(
|
{formatCellValue(
|
||||||
col.name,
|
col.name,
|
||||||
getEntityJoinValue(item, col.name),
|
getEntityJoinValue(item, col.name),
|
||||||
|
|
@ -4865,7 +4866,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasActions && (
|
{hasActions && (
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="bg-card sticky right-0 z-10 px-3 py-2 text-right group-hover/action:bg-accent">
|
||||||
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
|
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
|
||||||
{hasEditButton && (
|
{hasEditButton && (
|
||||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableHeader
|
<TableHeader
|
||||||
className={cn("bg-background border-b", tableConfig?.stickyHeader && "sticky top-0 z-30 shadow-sm")}
|
className={cn("border-b border-border/60", tableConfig?.stickyHeader && "sticky top-0 z-30 shadow-sm")}
|
||||||
|
style={{ backgroundColor: "hsl(var(--muted) / 0.8)" }}
|
||||||
>
|
>
|
||||||
<TableRow className="border-b">
|
<TableRow className="border-b border-border/60">
|
||||||
{actualColumns.map((column, colIndex) => {
|
{actualColumns.map((column, colIndex) => {
|
||||||
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
||||||
const leftFixedWidth = actualColumns
|
const leftFixedWidth = actualColumns
|
||||||
|
|
@ -132,10 +133,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={cn(
|
className={cn(
|
||||||
column.columnName === "__checkbox__"
|
column.columnName === "__checkbox__"
|
||||||
? "bg-background h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2"
|
? "h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2"
|
||||||
: "text-foreground hover:text-foreground bg-background h-9 cursor-pointer border-0 px-3 py-1.5 text-left align-middle text-xs font-semibold whitespace-nowrap transition-all duration-200 select-none sm:px-4 sm:py-2 sm:text-sm",
|
: "text-muted-foreground hover:text-foreground h-9 cursor-pointer border-0 px-3 py-1.5 text-left align-middle text-[10px] font-bold uppercase tracking-[0.04em] whitespace-nowrap transition-all duration-200 select-none sm:px-4 sm:py-2 sm:text-xs",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-primary/10",
|
column.sortable && "hover:bg-muted/70",
|
||||||
// 고정 컬럼 스타일
|
// 고정 컬럼 스타일
|
||||||
column.fixed === "left" && "border-border bg-background sticky z-40 border-r shadow-sm",
|
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",
|
column.fixed === "right" && "border-border bg-background sticky z-40 border-l shadow-sm",
|
||||||
|
|
@ -150,7 +151,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
||||||
backgroundColor: "hsl(var(--background))",
|
backgroundColor: "hsl(var(--muted) / 0.8)",
|
||||||
// sticky 위치 설정
|
// sticky 위치 설정
|
||||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||||
|
|
@ -228,8 +229,9 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
<TableRow
|
<TableRow
|
||||||
key={`row-${index}`}
|
key={`row-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background h-10 cursor-pointer border-b transition-colors",
|
"cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-muted/50",
|
index % 2 === 0 ? "bg-background" : "bg-muted/70",
|
||||||
|
tableConfig.tableStyle?.hoverEffect !== false && "hover:bg-accent",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => handleRowClick?.(row, index, e)}
|
onClick={(e) => handleRowClick?.(row, index, e)}
|
||||||
>
|
>
|
||||||
|
|
@ -273,9 +275,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
highlightArray[currentSearchIndex] === cellKey;
|
highlightArray[currentSearchIndex] === cellKey;
|
||||||
|
|
||||||
// formatCellValue 결과 (이미지 등 JSX 반환 가능)
|
// formatCellValue 결과 (이미지 등 JSX 반환 가능)
|
||||||
const rawCellValue =
|
const formattedValue = formatCellValue(row[column.columnName], column.format, column.columnName, row);
|
||||||
formatCellValue(row[column.columnName], column.format, column.columnName, row) || "\u00A0";
|
const rawCellValue = (formattedValue === null || formattedValue === undefined || formattedValue === "")
|
||||||
// 이미지 등 JSX 반환 여부 확인
|
? <span className="text-muted-foreground/50">-</span>
|
||||||
|
: formattedValue;
|
||||||
const isReactElement = typeof rawCellValue === "object" && React.isValidElement(rawCellValue);
|
const isReactElement = typeof rawCellValue === "object" && React.isValidElement(rawCellValue);
|
||||||
|
|
||||||
// 셀 값에서 검색어 하이라이트 렌더링
|
// 셀 값에서 검색어 하이라이트 렌더링
|
||||||
|
|
@ -324,7 +327,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
key={`cell-${column.columnName}`}
|
key={`cell-${column.columnName}`}
|
||||||
id={isCurrentSearchResult ? "current-search-result" : undefined}
|
id={isCurrentSearchResult ? "current-search-result" : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground h-10 px-3 py-1.5 align-middle text-xs transition-colors sm:px-4 sm:py-2 sm:text-sm",
|
"text-foreground h-10 px-3 py-[7px] align-middle text-[11px] transition-colors",
|
||||||
// 이미지 셀은 overflow/ellipsis 제외 (이미지 잘림 방지)
|
// 이미지 셀은 overflow/ellipsis 제외 (이미지 잘림 방지)
|
||||||
!isReactElement && "whitespace-nowrap",
|
!isReactElement && "whitespace-nowrap",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,7 @@ import {
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
Trash2,
|
Trash2,
|
||||||
Lock,
|
Lock,
|
||||||
|
GripVertical,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx";
|
||||||
import { FileText, ChevronRightIcon } from "lucide-react";
|
import { FileText, ChevronRightIcon } from "lucide-react";
|
||||||
|
|
@ -5676,7 +5677,41 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🆕 배치 편집 툴바 */}
|
{/* 필터 칩 바 */}
|
||||||
|
{filterGroups.length > 0 && filterGroups.some(g => g.conditions.some(c => c.column && c.value)) && (
|
||||||
|
<div className="border-border bg-muted/30 flex items-center gap-2 border-b px-4 py-1.5">
|
||||||
|
{filterGroups.flatMap(group =>
|
||||||
|
group.conditions
|
||||||
|
.filter(c => c.column && c.value)
|
||||||
|
.map(condition => {
|
||||||
|
const label = columnLabels[condition.column] || condition.column;
|
||||||
|
const opLabel = condition.operator === "equals" ? "=" : condition.operator === "contains" ? "⊃" : condition.operator === "notEquals" ? "≠" : condition.operator === "startsWith" ? "^" : condition.operator === "endsWith" ? "$" : condition.operator === "greaterThan" ? ">" : condition.operator === "lessThan" ? "<" : condition.operator;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={condition.id}
|
||||||
|
className="border-border bg-background text-muted-foreground inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-[10px] font-semibold"
|
||||||
|
>
|
||||||
|
{label} {opLabel} {condition.value}
|
||||||
|
<button
|
||||||
|
onClick={() => removeFilterCondition(group.id, condition.id)}
|
||||||
|
className="hover:text-destructive ml-0.5 leading-none transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={clearFilterBuilder}
|
||||||
|
className="text-muted-foreground hover:text-foreground ml-auto text-[9px] font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
전체 초기화
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 배치 편집 툴바 */}
|
||||||
{(editMode === "batch" || pendingChanges.size > 0) && (
|
{(editMode === "batch" || pendingChanges.size > 0) && (
|
||||||
<div className="border-border flex items-center justify-between border-b bg-amber-50 px-4 py-2 sm:px-6 dark:bg-amber-950/30">
|
<div className="border-border flex items-center justify-between border-b bg-amber-50 px-4 py-2 sm:px-6 dark:bg-amber-950/30">
|
||||||
<div className="flex items-center gap-3 text-xs sm:text-sm">
|
<div className="flex items-center gap-3 text-xs sm:text-sm">
|
||||||
|
|
@ -5826,9 +5861,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
<tr
|
<tr
|
||||||
className="border-primary/20 bg-muted h-10 border-b-2 sm:h-12"
|
className="bg-muted/80 h-10 border-b border-border/60 sm:h-12"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "hsl(var(--muted))",
|
backgroundColor: "hsl(var(--muted) / 0.8)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column, columnIndex) => {
|
{visibleColumns.map((column, columnIndex) => {
|
||||||
|
|
@ -5856,11 +5891,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
ref={(el) => (columnRefs.current[column.columnName] = el)}
|
ref={(el) => (columnRefs.current[column.columnName] = el)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground/90 relative h-8 overflow-hidden text-xs font-bold text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-sm",
|
"group text-muted-foreground relative h-8 overflow-hidden text-[10px] font-bold uppercase tracking-[0.04em] text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-xs",
|
||||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2",
|
||||||
column.sortable !== false &&
|
column.sortable !== false &&
|
||||||
column.columnName !== "__checkbox__" &&
|
column.columnName !== "__checkbox__" &&
|
||||||
"hover:bg-muted/70 cursor-pointer transition-colors",
|
"hover:text-foreground hover:bg-muted/70 cursor-pointer transition-colors",
|
||||||
|
sortColumn === column.columnName && "!text-primary",
|
||||||
isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
|
isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
|
||||||
// 🆕 Column Reordering 스타일
|
// 🆕 Column Reordering 스타일
|
||||||
isColumnDragEnabled &&
|
isColumnDragEnabled &&
|
||||||
|
|
@ -5880,7 +5916,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
backgroundColor: "hsl(var(--muted))",
|
backgroundColor: "hsl(var(--muted) / 0.8)",
|
||||||
...(isFrozen && { left: `${leftPosition}px` }),
|
...(isFrozen && { left: `${leftPosition}px` }),
|
||||||
}}
|
}}
|
||||||
// 🆕 Column Reordering 이벤트
|
// 🆕 Column Reordering 이벤트
|
||||||
|
|
@ -5900,9 +5936,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
renderCheckboxHeader()
|
renderCheckboxHeader()
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", justifyContent: "center" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "4px", justifyContent: "center" }}>
|
||||||
|
{isColumnDragEnabled && (
|
||||||
|
<GripVertical className="absolute left-0.5 top-1/2 h-3 w-3 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-40" />
|
||||||
|
)}
|
||||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||||
{column.sortable !== false && sortColumn === column.columnName && (
|
{column.sortable !== false && sortColumn === column.columnName && (
|
||||||
<span>{sortDirection === "asc" ? "↑" : "↓"}</span>
|
<span className="text-primary">{sortDirection === "asc" ? "↑" : "↓"}</span>
|
||||||
)}
|
)}
|
||||||
{/* 🆕 헤더 필터 버튼 */}
|
{/* 🆕 헤더 필터 버튼 */}
|
||||||
{tableConfig.headerFilter !== false &&
|
{tableConfig.headerFilter !== false &&
|
||||||
|
|
@ -6127,7 +6166,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background hover:bg-muted/50 cursor-pointer border-b transition-colors",
|
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
|
index % 2 === 0 ? "bg-background" : "bg-muted/70",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => handleRowClick(row, index, e)}
|
onClick={(e) => handleRowClick(row, index, e)}
|
||||||
>
|
>
|
||||||
|
|
@ -6158,13 +6198,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<td
|
<td
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground text-xs font-normal sm:text-sm",
|
"text-foreground text-[11px] font-normal",
|
||||||
// 이미지 컬럼은 overflow/ellipsis 제외 (이미지 잘림 방지)
|
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]",
|
||||||
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap",
|
|
||||||
column.columnName === "__checkbox__"
|
column.columnName === "__checkbox__"
|
||||||
? "px-0 py-1"
|
? "px-0 py-[7px]"
|
||||||
: "px-2 py-1 sm:px-4 sm:py-1.5",
|
: "px-3 py-[7px]",
|
||||||
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
|
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
|
||||||
|
(inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium",
|
||||||
|
isNumeric && "tabular-nums",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
textAlign:
|
textAlign:
|
||||||
|
|
@ -6264,10 +6305,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background hover:bg-muted/50 cursor-pointer border-b transition-colors",
|
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
isRowSelected && "bg-primary/10 hover:bg-primary/15",
|
index % 2 === 0 ? "bg-background" : "bg-muted/70",
|
||||||
|
isRowSelected && "!bg-primary/15 hover:!bg-primary/20",
|
||||||
|
isRowSelected && "[&_td]:!border-b-primary/30",
|
||||||
isRowFocused && "ring-primary/50 ring-1 ring-inset",
|
isRowFocused && "ring-primary/50 ring-1 ring-inset",
|
||||||
// 🆕 Drag & Drop 스타일
|
|
||||||
isDragEnabled && "cursor-grab active:cursor-grabbing",
|
isDragEnabled && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "bg-muted opacity-50",
|
isDragging && "bg-muted opacity-50",
|
||||||
isDropTarget && "border-t-primary border-t-2",
|
isDropTarget && "border-t-primary border-t-2",
|
||||||
|
|
@ -6327,23 +6369,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
data-row={index}
|
data-row={index}
|
||||||
data-col={colIndex}
|
data-col={colIndex}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground text-xs font-normal sm:text-sm",
|
"text-foreground text-[11px] font-normal",
|
||||||
// 이미지 컬럼은 overflow/ellipsis 제외 (이미지 잘림 방지)
|
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]",
|
||||||
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap",
|
column.columnName === "__checkbox__" ? "px-0 py-[7px]" : "px-3 py-[7px]",
|
||||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-1.5",
|
|
||||||
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
|
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
|
||||||
// 🆕 포커스된 셀 스타일
|
|
||||||
isCellFocused && !editingCell && "ring-primary bg-primary/5 ring-2 ring-inset",
|
isCellFocused && !editingCell && "ring-primary bg-primary/5 ring-2 ring-inset",
|
||||||
// 🆕 편집 중인 셀 스타일
|
|
||||||
editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0",
|
editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0",
|
||||||
// 🆕 배치 편집: 수정된 셀 스타일 (노란 배경)
|
|
||||||
isModified && !cellValidationError && "bg-amber-100 dark:bg-amber-900/40",
|
isModified && !cellValidationError && "bg-amber-100 dark:bg-amber-900/40",
|
||||||
// 🆕 유효성 에러: 빨간 테두리 및 배경
|
|
||||||
cellValidationError && "bg-red-50 ring-2 ring-red-500 ring-inset dark:bg-red-950/40",
|
cellValidationError && "bg-red-50 ring-2 ring-red-500 ring-inset dark:bg-red-950/40",
|
||||||
// 🆕 검색 하이라이트 스타일 (노란 배경)
|
|
||||||
isSearchHighlighted && !isCellFocused && "bg-yellow-200 dark:bg-yellow-700/50",
|
isSearchHighlighted && !isCellFocused && "bg-yellow-200 dark:bg-yellow-700/50",
|
||||||
// 🆕 편집 불가 컬럼 스타일 (연한 회색 배경)
|
|
||||||
column.editable === false && "bg-gray-50 dark:bg-gray-900/30",
|
column.editable === false && "bg-gray-50 dark:bg-gray-900/30",
|
||||||
|
// 코드 컬럼: mono 폰트 + primary 색상
|
||||||
|
(inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium",
|
||||||
|
// 숫자 컬럼: tabular-nums 오른쪽 정렬
|
||||||
|
isNumeric && "tabular-nums",
|
||||||
)}
|
)}
|
||||||
// 🆕 유효성 에러 툴팁
|
// 🆕 유효성 에러 툴팁
|
||||||
title={cellValidationError || undefined}
|
title={cellValidationError || undefined}
|
||||||
|
|
@ -6465,6 +6504,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
})()
|
})()
|
||||||
: column.columnName === "__checkbox__"
|
: column.columnName === "__checkbox__"
|
||||||
? renderCheckboxCell(row, index)
|
? renderCheckboxCell(row, index)
|
||||||
|
: (cellValue === null || cellValue === undefined || cellValue === "")
|
||||||
|
? <span className="text-muted-foreground/50">-</span>
|
||||||
: formatCellValue(cellValue, column, row)}
|
: formatCellValue(cellValue, column, row)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ const TabsDesignEditor: React.FC<{
|
||||||
return cn(
|
return cn(
|
||||||
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
|
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary/10 border-b-2 border-primary text-primary font-semibold"
|
? "bg-primary/20 dark:bg-primary/25 border-b-2 border-primary text-primary font-semibold"
|
||||||
: "text-foreground/70 hover:text-foreground hover:bg-muted/50"
|
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue