This commit is contained in:
parent
cfd7ee9fce
commit
b293d184bb
|
|
@ -76,10 +76,12 @@ export function ColumnDetailPanel({
|
|||
|
||||
if (!column) return null;
|
||||
|
||||
const refTableOpts = referenceTableOptions.length
|
||||
? referenceTableOptions
|
||||
const refTableOpts = useMemo(() => {
|
||||
const hasKorean = (s: string) => /[가-힣]/.test(s);
|
||||
const raw = referenceTableOptions.length
|
||||
? [...referenceTableOptions]
|
||||
: [
|
||||
{ value: "none", label: "선택 안함" },
|
||||
{ value: "none", label: "없음" },
|
||||
...tables.map((t) => ({
|
||||
value: t.tableName,
|
||||
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 (
|
||||
<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]">
|
||||
<CommandEmpty className="py-2 text-center text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{refTableOpts.map((opt) => (
|
||||
{refTableOpts.map((opt) => {
|
||||
const hasKorean = opt.value !== "none" && opt.label !== opt.value && !opt.label.startsWith(opt.value);
|
||||
return (
|
||||
<CommandItem
|
||||
key={opt.value}
|
||||
value={`${opt.label} ${opt.value}`}
|
||||
|
|
@ -197,9 +215,17 @@ export function ColumnDetailPanel({
|
|||
<Check
|
||||
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>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
|
@ -263,13 +289,14 @@ export function ColumnDetailPanel({
|
|||
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{refCol.displayName && refCol.displayName !== refCol.columnName ? (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{refCol.displayName && refCol.displayName !== refCol.columnName
|
||||
? `${refCol.displayName} (${refCol.columnName})`
|
||||
: refCol.columnName}
|
||||
</span>
|
||||
<span className="font-medium">{refCol.displayName}</span>
|
||||
<span className="text-[10px] text-muted-foreground">{refCol.columnName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>{refCol.columnName}</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
|
|
|||
|
|
@ -568,18 +568,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
|
||||
const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : [];
|
||||
|
||||
// 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장
|
||||
useEffect(() => {
|
||||
|
|
@ -603,6 +592,17 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
}
|
||||
}, [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 (
|
||||
<div className="bg-background flex h-screen flex-col">
|
||||
{/* 모바일 헤더 */}
|
||||
|
|
|
|||
|
|
@ -493,8 +493,8 @@ export function TabBar() {
|
|||
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",
|
||||
isActive
|
||||
? "text-foreground z-10 -mb-px h-[30px] bg-white"
|
||||
: "bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-transparent",
|
||||
? "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-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground border-transparent",
|
||||
)}
|
||||
style={{
|
||||
width: TAB_WIDTH,
|
||||
|
|
|
|||
|
|
@ -1552,16 +1552,22 @@ export const V2SplitPanelLayoutConfigPanel: React.FC<
|
|||
/>
|
||||
<SwitchRow
|
||||
label="수정 버튼"
|
||||
checked={config.rightPanel?.showEdit ?? false}
|
||||
checked={(config.rightPanel?.showEdit ?? config.rightPanel?.editButton?.enabled) ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateRightPanel({ showEdit: checked })
|
||||
updateRightPanel({
|
||||
showEdit: checked,
|
||||
editButton: { ...config.rightPanel?.editButton!, enabled: checked },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SwitchRow
|
||||
label="삭제 버튼"
|
||||
checked={config.rightPanel?.showDelete ?? false}
|
||||
checked={(config.rightPanel?.showDelete ?? config.rightPanel?.deleteButton?.enabled) ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateRightPanel({ showDelete: checked })
|
||||
updateRightPanel({
|
||||
showDelete: checked,
|
||||
deleteButton: { ...config.rightPanel?.deleteButton!, enabled: checked },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
Move,
|
||||
FileSpreadsheet,
|
||||
List,
|
||||
LayoutPanelRight,
|
||||
PanelRight,
|
||||
} from "lucide-react";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
|
|
@ -3524,7 +3524,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
{columnsToShow.map((col, idx) => (
|
||||
<th
|
||||
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={{
|
||||
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||
textAlign: col.align || "left",
|
||||
|
|
@ -3534,7 +3534,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</th>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
</tr>
|
||||
|
|
@ -3621,7 +3621,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
{columnsToShow.map((col, idx) => (
|
||||
<th
|
||||
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={{
|
||||
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||
textAlign: col.align || "left",
|
||||
|
|
@ -3631,7 +3631,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</th>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
</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 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 밑줄 인디케이터) */}
|
||||
{(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
|
||||
<div className="flex items-center gap-0">
|
||||
<button
|
||||
onClick={() => handleTabChange(0)}
|
||||
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
|
||||
? "text-primary border-b-2 border-primary font-semibold bg-primary/5"
|
||||
: "text-foreground/70 hover:text-foreground hover:bg-muted/30"
|
||||
? "text-primary border-b-2 border-primary"
|
||||
: "text-muted-foreground border-b-2 border-transparent hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{componentConfig.rightPanel?.title || "기본"}
|
||||
|
|
@ -3992,10 +3992,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
key={tab.tabId || `tab-${index}`}
|
||||
onClick={() => handleTabChange(index + 1)}
|
||||
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
|
||||
? "text-primary border-b-2 border-primary font-semibold bg-primary/5"
|
||||
: "text-foreground/70 hover:text-foreground hover:bg-muted/30"
|
||||
? "text-primary border-b-2 border-primary"
|
||||
: "text-muted-foreground border-b-2 border-transparent hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{tab.label || `탭 ${index + 1}`}
|
||||
|
|
@ -4120,7 +4120,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<th
|
||||
key={col.name}
|
||||
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",
|
||||
canDragTabColumns && "cursor-grab active:cursor-grabbing",
|
||||
isDragging && "opacity-50",
|
||||
|
|
@ -4136,7 +4136,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
);
|
||||
})}
|
||||
{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>
|
||||
</thead>
|
||||
|
|
@ -4157,13 +4157,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<React.Fragment key={tabItemId}>
|
||||
<tr
|
||||
className={cn(
|
||||
"cursor-pointer border-b border-border/40 transition-colors",
|
||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
||||
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
|
||||
)}
|
||||
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
||||
>
|
||||
{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"
|
||||
? renderProgressCell(col, item, selectedLeftItem)
|
||||
: formatCellValue(
|
||||
|
|
@ -4256,7 +4256,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<th
|
||||
key={col.name}
|
||||
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",
|
||||
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
|
||||
isDragging && "opacity-50",
|
||||
|
|
@ -4272,7 +4272,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
);
|
||||
})}
|
||||
{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>
|
||||
</thead>
|
||||
|
|
@ -4292,13 +4292,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<React.Fragment key={tabItemId}>
|
||||
<tr
|
||||
className={cn(
|
||||
"cursor-pointer border-b border-border/40 transition-colors",
|
||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
||||
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
|
||||
)}
|
||||
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
||||
>
|
||||
{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"
|
||||
? renderProgressCell(col, item, selectedLeftItem)
|
||||
: formatCellValue(
|
||||
|
|
@ -4670,7 +4670,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<th
|
||||
key={idx}
|
||||
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",
|
||||
isDraggable && "cursor-grab active:cursor-grabbing",
|
||||
isDragging && "opacity-50",
|
||||
|
|
@ -4689,26 +4689,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</th>
|
||||
);
|
||||
})}
|
||||
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 컬럼 표시 */}
|
||||
{!isDesignMode &&
|
||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold" style={{ width: '80px' }}>
|
||||
{(() => {
|
||||
const rightEditVisible = (componentConfig.rightPanel?.showEdit ?? componentConfig.rightPanel?.editButton?.enabled) !== false;
|
||||
const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
|
||||
return !isDesignMode && (rightEditVisible || rightDeleteVisible) ? (
|
||||
<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>
|
||||
)}
|
||||
) : null;
|
||||
})()}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredData.map((item, 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 (
|
||||
<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) => (
|
||||
<td
|
||||
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" }}
|
||||
>
|
||||
{col.type === "progress"
|
||||
|
|
@ -4722,12 +4725,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</td>
|
||||
))}
|
||||
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
||||
{!isDesignMode &&
|
||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||
<td className="px-3 py-2 text-right text-sm whitespace-nowrap group/action">
|
||||
{!isDesignMode && (rightEditVisible || rightDeleteVisible) && (
|
||||
<td className="bg-card sticky right-0 z-10 px-3 py-2 text-right text-sm whitespace-nowrap group-hover/action:bg-accent">
|
||||
<div className="flex justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
|
||||
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
||||
{rightEditVisible && (
|
||||
<Button
|
||||
variant={
|
||||
componentConfig.rightPanel?.editButton?.buttonVariant || "outline"
|
||||
|
|
@ -4743,7 +4744,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
||||
</Button>
|
||||
)}
|
||||
{(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && (
|
||||
{rightDeleteVisible && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -4801,8 +4802,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
return sum + w;
|
||||
}, 0);
|
||||
|
||||
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
|
||||
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
|
||||
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.showEdit ?? componentConfig.rightPanel?.editButton?.enabled) !== false;
|
||||
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
|
||||
const hasActions = hasEditButton || hasDeleteButton;
|
||||
|
||||
return filteredData.length > 0 ? (
|
||||
|
|
@ -4814,14 +4815,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
{columnsToDisplay.map((col) => (
|
||||
<th
|
||||
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" }}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
{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>
|
||||
</thead>
|
||||
|
|
@ -4849,13 +4850,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<React.Fragment key={itemId}>
|
||||
<tr
|
||||
className={cn(
|
||||
"group/action cursor-pointer border-b border-border/40 transition-colors",
|
||||
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
||||
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
|
||||
)}
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
>
|
||||
{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(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
|
|
@ -4865,7 +4866,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</td>
|
||||
))}
|
||||
{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">
|
||||
{hasEditButton && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
|
|
|
|||
|
|
@ -109,9 +109,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
}}
|
||||
>
|
||||
<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) => {
|
||||
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
||||
const leftFixedWidth = actualColumns
|
||||
|
|
@ -132,10 +133,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
key={column.columnName}
|
||||
className={cn(
|
||||
column.columnName === "__checkbox__"
|
||||
? "bg-background 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",
|
||||
? "h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2"
|
||||
: "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}`,
|
||||
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 === "right" && "border-border bg-background sticky z-40 border-l shadow-sm",
|
||||
|
|
@ -150,7 +151,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
backgroundColor: "hsl(var(--muted) / 0.8)",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
|
|
@ -228,8 +229,9 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
<TableRow
|
||||
key={`row-${index}`}
|
||||
className={cn(
|
||||
"bg-background h-10 cursor-pointer border-b transition-colors",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-muted/50",
|
||||
"cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||
index % 2 === 0 ? "bg-background" : "bg-muted/70",
|
||||
tableConfig.tableStyle?.hoverEffect !== false && "hover:bg-accent",
|
||||
)}
|
||||
onClick={(e) => handleRowClick?.(row, index, e)}
|
||||
>
|
||||
|
|
@ -273,9 +275,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
highlightArray[currentSearchIndex] === cellKey;
|
||||
|
||||
// formatCellValue 결과 (이미지 등 JSX 반환 가능)
|
||||
const rawCellValue =
|
||||
formatCellValue(row[column.columnName], column.format, column.columnName, row) || "\u00A0";
|
||||
// 이미지 등 JSX 반환 여부 확인
|
||||
const formattedValue = formatCellValue(row[column.columnName], column.format, column.columnName, row);
|
||||
const rawCellValue = (formattedValue === null || formattedValue === undefined || formattedValue === "")
|
||||
? <span className="text-muted-foreground/50">-</span>
|
||||
: formattedValue;
|
||||
const isReactElement = typeof rawCellValue === "object" && React.isValidElement(rawCellValue);
|
||||
|
||||
// 셀 값에서 검색어 하이라이트 렌더링
|
||||
|
|
@ -324,7 +327,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
key={`cell-${column.columnName}`}
|
||||
id={isCurrentSearchResult ? "current-search-result" : undefined}
|
||||
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 제외 (이미지 잘림 방지)
|
||||
!isReactElement && "whitespace-nowrap",
|
||||
`text-${column.align}`,
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ import {
|
|||
CheckSquare,
|
||||
Trash2,
|
||||
Lock,
|
||||
GripVertical,
|
||||
} from "lucide-react";
|
||||
import * as XLSX from "xlsx";
|
||||
import { FileText, ChevronRightIcon } from "lucide-react";
|
||||
|
|
@ -5676,7 +5677,41 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
)}
|
||||
</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) && (
|
||||
<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">
|
||||
|
|
@ -5826,9 +5861,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
</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={{
|
||||
backgroundColor: "hsl(var(--muted))",
|
||||
backgroundColor: "hsl(var(--muted) / 0.8)",
|
||||
}}
|
||||
>
|
||||
{visibleColumns.map((column, columnIndex) => {
|
||||
|
|
@ -5856,11 +5891,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
key={column.columnName}
|
||||
ref={(el) => (columnRefs.current[column.columnName] = el)}
|
||||
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",
|
||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
||||
"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-3 py-2",
|
||||
column.sortable !== false &&
|
||||
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)]",
|
||||
// 🆕 Column Reordering 스타일
|
||||
isColumnDragEnabled &&
|
||||
|
|
@ -5880,7 +5916,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))",
|
||||
backgroundColor: "hsl(var(--muted) / 0.8)",
|
||||
...(isFrozen && { left: `${leftPosition}px` }),
|
||||
}}
|
||||
// 🆕 Column Reordering 이벤트
|
||||
|
|
@ -5900,9 +5936,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
renderCheckboxHeader()
|
||||
) : (
|
||||
<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>
|
||||
{column.sortable !== false && sortColumn === column.columnName && (
|
||||
<span>{sortDirection === "asc" ? "↑" : "↓"}</span>
|
||||
<span className="text-primary">{sortDirection === "asc" ? "↑" : "↓"}</span>
|
||||
)}
|
||||
{/* 🆕 헤더 필터 버튼 */}
|
||||
{tableConfig.headerFilter !== false &&
|
||||
|
|
@ -6127,7 +6166,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<tr
|
||||
key={index}
|
||||
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)}
|
||||
>
|
||||
|
|
@ -6158,13 +6198,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<td
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
"text-foreground text-xs font-normal sm:text-sm",
|
||||
// 이미지 컬럼은 overflow/ellipsis 제외 (이미지 잘림 방지)
|
||||
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap",
|
||||
"text-foreground text-[11px] font-normal",
|
||||
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]",
|
||||
column.columnName === "__checkbox__"
|
||||
? "px-0 py-1"
|
||||
: "px-2 py-1 sm:px-4 sm:py-1.5",
|
||||
? "px-0 py-[7px]"
|
||||
: "px-3 py-[7px]",
|
||||
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={{
|
||||
textAlign:
|
||||
|
|
@ -6264,10 +6305,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<tr
|
||||
key={index}
|
||||
className={cn(
|
||||
"bg-background hover:bg-muted/50 cursor-pointer border-b transition-colors",
|
||||
isRowSelected && "bg-primary/10 hover:bg-primary/15",
|
||||
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||
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",
|
||||
// 🆕 Drag & Drop 스타일
|
||||
isDragEnabled && "cursor-grab active:cursor-grabbing",
|
||||
isDragging && "bg-muted opacity-50",
|
||||
isDropTarget && "border-t-primary border-t-2",
|
||||
|
|
@ -6327,23 +6369,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
data-row={index}
|
||||
data-col={colIndex}
|
||||
className={cn(
|
||||
"text-foreground text-xs font-normal sm:text-sm",
|
||||
// 이미지 컬럼은 overflow/ellipsis 제외 (이미지 잘림 방지)
|
||||
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap",
|
||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-1.5",
|
||||
"text-foreground text-[11px] font-normal",
|
||||
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]",
|
||||
column.columnName === "__checkbox__" ? "px-0 py-[7px]" : "px-3 py-[7px]",
|
||||
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",
|
||||
// 🆕 편집 중인 셀 스타일
|
||||
editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0",
|
||||
// 🆕 배치 편집: 수정된 셀 스타일 (노란 배경)
|
||||
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",
|
||||
// 🆕 검색 하이라이트 스타일 (노란 배경)
|
||||
isSearchHighlighted && !isCellFocused && "bg-yellow-200 dark:bg-yellow-700/50",
|
||||
// 🆕 편집 불가 컬럼 스타일 (연한 회색 배경)
|
||||
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}
|
||||
|
|
@ -6465,6 +6504,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
})()
|
||||
: column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: (cellValue === null || cellValue === undefined || cellValue === "")
|
||||
? <span className="text-muted-foreground/50">-</span>
|
||||
: formatCellValue(cellValue, column, row)}
|
||||
</td>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ const TabsDesignEditor: React.FC<{
|
|||
return cn(
|
||||
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
|
||||
isActive
|
||||
? "bg-primary/10 border-b-2 border-primary text-primary font-semibold"
|
||||
: "text-foreground/70 hover:text-foreground hover:bg-muted/50"
|
||||
? "bg-primary/20 dark:bg-primary/25 border-b-2 border-primary text-primary font-semibold"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue