diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-28 00:42:08 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-28 00:42:08 +0000 |
| commit | b8e8328b1ffffb80bf4ebb776a4a24e5680fc5bc (patch) | |
| tree | bbb4d82cee5f3fbf107e0648dea9a8f66e2710c4 /components | |
| parent | 34bbeb86c1a8d24b5f526710889b5e54d699cfd0 (diff) | |
테이블 칼럼 리사이즈 및 핀 충돌 해결
Diffstat (limited to 'components')
| -rw-r--r-- | components/data-table/data-table-column-header.tsx | 15 | ||||
| -rw-r--r-- | components/data-table/data-table-pin-right.tsx | 173 | ||||
| -rw-r--r-- | components/data-table/data-table.tsx | 30 | ||||
| -rw-r--r-- | components/documents/view-document-dialog.tsx | 244 | ||||
| -rw-r--r-- | components/form-data/form-data-table.tsx | 4 |
5 files changed, 182 insertions, 284 deletions
diff --git a/components/data-table/data-table-column-header.tsx b/components/data-table/data-table-column-header.tsx index aa0c754b..795531c8 100644 --- a/components/data-table/data-table-column-header.tsx +++ b/components/data-table/data-table-column-header.tsx @@ -24,15 +24,18 @@ export function DataTableColumnHeader<TData, TValue>({ className, }: DataTableColumnHeaderProps<TData, TValue>) { if (!column.getCanSort() && !column.getCanHide()) { - return <div className={cn(className)}>{title}</div> + return <div className={cn("w-full", className)}>{title}</div> } const ascValue = `${column.id}-asc` const descValue = `${column.id}-desc` const hideValue = `${column.id}-hide` + // 현재 컬럼 pinned 상태 + const isPinned = column.getIsPinned(); + return ( - <div className={cn("flex items-center gap-2", className)}> + <div className={cn("flex items-center gap-2 w-full", className)}> <Select value={ column.getIsSorted() === "desc" @@ -55,7 +58,11 @@ export function DataTableColumnHeader<TData, TValue>({ ? "Sorted ascending. Click to sort descending." : "Not sorted. Click to sort ascending." } - className="-ml-3 h-8 w-fit border-none text-xs hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent [&>svg:last-child]:hidden" + className={cn( + "-ml-3 h-8 w-full border-none text-xs hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent [&>svg:last-child]:hidden", + // 고정된 상태일 때 추가 스타일 + isPinned && "sticky-content" + )} > {title} <SelectIcon asChild> @@ -106,4 +113,4 @@ export function DataTableColumnHeader<TData, TValue>({ </Select> </div> ) -} +}
\ No newline at end of file diff --git a/components/data-table/data-table-pin-right.tsx b/components/data-table/data-table-pin-right.tsx index 051dd985..3ed42402 100644 --- a/components/data-table/data-table-pin-right.tsx +++ b/components/data-table/data-table-pin-right.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { type Table } from "@tanstack/react-table" +import { type Column, type Table } from "@tanstack/react-table" import { Check, ChevronsUpDown, MoveRight } from "lucide-react" import { cn, toSentenceCase } from "@/lib/utils" @@ -13,6 +13,7 @@ import { CommandInput, CommandItem, CommandList, + CommandSeparator, } from "@/components/ui/command" import { Popover, @@ -21,12 +22,99 @@ import { } from "@/components/ui/popover" /** - * “Pin Right” Popover. Similar to PinLeftButton, but pins columns to "right". + * Helper function to check if a column is a parent column (has subcolumns) + */ +function isParentColumn<TData>(column: Column<TData>): boolean { + return column.columns && column.columns.length > 0 +} + +/** + * Helper function to pin all subcolumns of a parent column + */ +function pinSubColumns<TData>( + column: Column<TData>, + pinType: false | "left" | "right" +): void { + // If this is a parent column, pin all its subcolumns + if (isParentColumn(column)) { + column.columns.forEach((subColumn) => { + // Recursively handle nested columns + pinSubColumns(subColumn, pinType) + }) + } else { + // For leaf columns, apply the pin if possible + if (column.getCanPin?.()) { + column.pin?.(pinType) + } + } +} + +/** + * Checks if all subcolumns of a parent column are pinned to the specified side + */ +function areAllSubColumnsPinned<TData>( + column: Column<TData>, + pinType: "left" | "right" +): boolean { + if (isParentColumn(column)) { + // Check if all subcolumns are pinned + return column.columns.every((subColumn) => + areAllSubColumnsPinned(subColumn, pinType) + ) + } else { + // For leaf columns, check if it's pinned to the specified side + return column.getIsPinned?.() === pinType + } +} + +/** + * "Pin Right" Popover. Supports pinning both individual columns and header groups. */ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { const [open, setOpen] = React.useState(false) const triggerRef = React.useRef<HTMLButtonElement>(null) + // Get all columns that can be pinned, including parent columns + const pinnableColumns = React.useMemo(() => { + return table.getAllColumns().filter((column) => { + // If it's a leaf column, check if it can be pinned + if (!isParentColumn(column)) { + return column.getCanPin?.() + } + + // If it's a parent column, check if at least one subcolumn can be pinned + return column.columns.some((subCol) => { + if (isParentColumn(subCol)) { + // Recursively check nested columns + return subCol.columns.some(c => c.getCanPin?.()) + } + return subCol.getCanPin?.() + }) + }) + }, [table]) + + // Handle column pinning + const handleColumnPin = React.useCallback((column: Column<TData>) => { + // For parent columns, pin/unpin all subcolumns + if (isParentColumn(column)) { + const allPinned = areAllSubColumnsPinned(column, "right") + pinSubColumns(column, allPinned ? false : "right") + } else { + // For leaf columns, toggle pin state + const isPinned = column.getIsPinned?.() === "right" + column.pin?.(isPinned ? false : "right") + } + }, []) + + // Check if a column or its subcolumns are pinned right + const isColumnPinned = React.useCallback((column: Column<TData>): boolean => { + if (isParentColumn(column)) { + return areAllSubColumnsPinned(column, "right") + } else { + return column.getIsPinned?.() === "right" + } + }, []) + return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> @@ -37,17 +125,17 @@ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { className="h-8 gap-2" > <MoveRight className="size-4" /> - + <span className="hidden sm:inline"> Right </span> <ChevronsUpDown className="ml-1 size-4 opacity-50 hidden sm:inline" /> </Button> </PopoverTrigger> - + <PopoverContent align="end" - className="w-44 p-0" + className="w-56 p-0" onCloseAutoFocus={() => triggerRef.current?.focus()} > <Command> @@ -55,30 +143,57 @@ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { <CommandList> <CommandEmpty>No columns found.</CommandEmpty> <CommandGroup> - {table - .getAllLeafColumns() - .filter((col) => col.getCanPin?.()) - .map((column) => { - const pinned = column.getIsPinned?.() - return ( - <CommandItem - key={column.id} - onSelect={() => { - column.pin?.(pinned === "right" ? false : "right") - }} - > - <span className="truncate"> - {toSentenceCase(column.id)} - </span> - <Check - className={cn( - "ml-auto size-4 shrink-0", - pinned === "right" ? "opacity-100" : "opacity-0" - )} - /> - </CommandItem> - ) - })} + {/* Header Columns (Parent Columns) */} + {pinnableColumns + .filter(isParentColumn) + .map((column) => ( + <CommandItem + key={column.id} + onSelect={() => { + handleColumnPin(column) + }} + className="font-medium" + > + <span className="truncate"> + {column.id === "Basic Info" || column.id === "Metadata" + ? column.id // Use column ID directly for common groups + : toSentenceCase(column.id)} + </span> + <Check + className={cn( + "ml-auto size-4 shrink-0", + isColumnPinned(column) ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + + {pinnableColumns.some(isParentColumn) && + pinnableColumns.some(col => !isParentColumn(col)) && ( + <CommandSeparator /> + )} + + {/* Leaf Columns (individual columns) */} + {pinnableColumns + .filter(col => !isParentColumn(col)) + .map((column) => ( + <CommandItem + key={column.id} + onSelect={() => { + handleColumnPin(column) + }} + > + <span className="truncate"> + {toSentenceCase(column.id)} + </span> + <Check + className={cn( + "ml-auto size-4 shrink-0", + isColumnPinned(column) ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} </CommandGroup> </CommandList> </Command> diff --git a/components/data-table/data-table.tsx b/components/data-table/data-table.tsx index 3d01994a..b1027cc0 100644 --- a/components/data-table/data-table.tsx +++ b/components/data-table/data-table.tsx @@ -41,8 +41,8 @@ export function DataTable<TData>({ return ( <div className={cn("w-full space-y-2.5 overflow-auto", className)} {...props}> {children} - <div className="max-w-[100vw] overflow-auto" style={{maxHeight:'36.1rem'}}> - <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed"> + <div className="max-w-[100vw] overflow-auto" style={{ maxHeight: '36.1rem' }}> + <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed"> {/* ------------------------------- Table Header → 그룹핑된 컬럼의 헤더는 숨김 처리 @@ -60,24 +60,26 @@ export function DataTable<TData>({ <TableHead key={header.id} colSpan={header.colSpan} - data-column-id={header.column.id} + data-column-id={header.column.id} style={{ ...getCommonPinningStyles({ column: header.column }), width: header.getSize(), // 리사이징을 위한 너비 설정 - position: "relative" // 리사이저를 위한 포지셔닝 + // position: "relative" // 리사이저를 위한 포지셔닝 }} > - {header.isPlaceholder - ? null - : flexRender( + <div style={{ position: "relative" }}> + {header.isPlaceholder + ? null + : flexRender( header.column.columnDef.header, header.getContext() )} - - {/* 리사이즈 핸들 - 별도의 컴포넌트로 분리 */} - {header.column.getCanResize() && ( - <DataTableResizer header={header} /> - )} + + {/* 리사이즈 핸들 - 별도의 컴포넌트로 분리 */} + {header.column.getCanResize() && ( + <DataTableResizer header={header} /> + )} + </div> </TableHead> ) })} @@ -115,7 +117,7 @@ export function DataTable<TData>({ data-state={row.getIsExpanded() && "expanded"} > {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */} - <TableCell colSpan={table.getVisibleFlatColumns().length}> + <TableCell colSpan={table.getVisibleFlatColumns().length}> {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} {row.getCanExpand() && ( <button @@ -164,7 +166,7 @@ export function DataTable<TData>({ return ( <TableCell key={cell.id} - data-column-id={cell.column.id} + data-column-id={cell.column.id} style={{ ...getCommonPinningStyles({ column: cell.column }), width: cell.column.getSize(), // 리사이징을 위한 너비 설정 diff --git a/components/documents/view-document-dialog.tsx b/components/documents/view-document-dialog.tsx index 7603fdc0..6daa806b 100644 --- a/components/documents/view-document-dialog.tsx +++ b/components/documents/view-document-dialog.tsx @@ -1,80 +1,3 @@ -<<<<<<< HEAD -"use client"; - -import * as React from "react"; -import { WebViewerInstance } from "@pdftron/webviewer"; -import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@/components/ui/dialog"; -import { Building2, FileIcon, Loader2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import fs from "fs"; - -// 인터페이스 -interface Attachment { - id: number; - fileName: string; - filePath: string; - fileType?: string; -} - -interface Version { - id: number; - stage: string; - revision: string; - uploaderType: string; - uploaderName: string | null; - comment: string | null; - status: string | null; - planDate: string | null; - actualDate: string | null; - approvedDate: string | null; - DocumentSubmitDate: Date; - attachments: Attachment[]; - selected: boolean; -} - -type ViewDocumentDialogProps = { - versions: Version[]; -}; - -export function ViewDocumentDialog({ versions }: ViewDocumentDialogProps) { - const [open, setOpen] = React.useState(false); - - return ( - <> - <Button - size="sm" - className="border-blue-200" - variant="outline" - onClick={() => setOpen((prev) => !prev)} - > - 문서 보기 - </Button> - {open && ( - <DocumentViewer open={open} setOpen={setOpen} versions={versions} /> - )} - </> - ); -} - -const DocumentViewer: React.FC<{ - open: boolean; - setOpen: React.Dispatch<React.SetStateAction<boolean>>; - versions: Version[]; -}> = ({ open, setOpen, versions }) => { - const [instance, setInstance] = React.useState<null | WebViewerInstance>( - null - ); - const [viwerLoading, setViewerLoading] = React.useState<boolean>(true); - const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true); -======= "use client" import * as React from "react" @@ -85,7 +8,14 @@ import { } from "@/components/ui/dialog" import { Building2, FileIcon, Loader2 } from "lucide-react" import { Button } from "@/components/ui/button" -import fs from "fs" + +interface Attachment { + id: number; + fileName: string; + filePath: string; + fileType?: string; +} + interface Version { id: number @@ -135,34 +65,22 @@ function DocumentViewer({open, setOpen, versions}){ const [instance, setInstance] = React.useState<null | WebViewerInstance>(null) const [viwerLoading, setViewerLoading] = React.useState<boolean>(true) const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true) ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d const viewer = React.useRef<HTMLDivElement>(null); const initialized = React.useRef(false); const isCancelled = React.useRef(false); // 초기화 중단용 flag const cleanupHtmlStyle = () => { const htmlElement = document.documentElement; -<<<<<<< HEAD - - // 기존 style 속성 가져오기 - const originalStyle = htmlElement.getAttribute("style") || ""; - -======= // 기존 style 속성 가져오기 const originalStyle = htmlElement.getAttribute("style") || ""; ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d // "color-scheme: light" 또는 "color-scheme: dark" 찾기 const colorSchemeStyle = originalStyle .split(";") .map((s) => s.trim()) .find((s) => s.startsWith("color-scheme:")); -<<<<<<< HEAD - -======= ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d // 새로운 스타일 적용 (color-scheme만 유지) if (colorSchemeStyle) { htmlElement.setAttribute("style", colorSchemeStyle + ";"); @@ -170,46 +88,13 @@ function DocumentViewer({open, setOpen, versions}){ htmlElement.removeAttribute("style"); // color-scheme도 없으면 style 속성 자체 삭제 } -<<<<<<< HEAD - console.log("html style 삭제"); -======= console.log("html style 삭제") ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }; React.useEffect(() => { if (open && !initialized.current) { initialized.current = true; isCancelled.current = false; // 다시 열릴 때는 false로 리셋 -<<<<<<< HEAD - - requestAnimationFrame(() => { - if (viewer.current) { - import("@pdftron/webviewer").then(({ default: WebViewer }) => { - console.log(isCancelled.current); - if (isCancelled.current) { - console.log("📛 WebViewer 초기화 취소됨 (Dialog 닫힘)"); - - return; - } - - WebViewer( - { - path: "/pdftronWeb", - licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, - fullAPI: true, - css: "/globals.css", - }, - viewer.current as HTMLDivElement - ).then(async (instance: WebViewerInstance) => { - setInstance(instance); - instance.UI.enableFeatures([instance.UI.Feature.MultiTab]); - instance.UI.disableElements([ - "addTabButton", - "multiTabsEmptyPage", - ]); - setViewerLoading(false); -======= requestAnimationFrame(() => { if (viewer.current) { @@ -237,21 +122,11 @@ function DocumentViewer({open, setOpen, versions}){ instance.UI.disableElements(["addTabButton", "multiTabsEmptyPage"]); setViewerLoading(false); ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }); }); } }); } -<<<<<<< HEAD - - return () => { - // cleanup 시에는 중단 flag 세움 - if (instance) { - instance.UI.dispose(); - } - setTimeout(() => cleanupHtmlStyle(), 500); -======= return async () => { // cleanup 시에는 중단 flag 세움 @@ -259,78 +134,10 @@ function DocumentViewer({open, setOpen, versions}){ await instance.UI.dispose() } await setTimeout(() => cleanupHtmlStyle(), 500) ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }; }, [open]); React.useEffect(() => { -<<<<<<< HEAD - const loadDocument = async () => { - if (instance && versions.length > 0) { - const { UI } = instance; - - const optionsArray: any[] = []; - - versions.forEach((c) => { - const { attachments } = c; - attachments.forEach((c2) => { - const { fileName, filePath, fileType } = c2; - - const fileTypeCur = fileType ?? ""; - - const options = { - filename: fileName, - ...(fileTypeCur.includes("xlsx") && { - officeOptions: { - formatOptions: { - applyPageBreaksToSheet: true, - }, - }, - }), - }; - - optionsArray.push({ - filePath, - options, - }); - }); - }); - - const tabIds = []; - - for (const option of optionsArray) { - const { filePath, options } = option; - const response = await fetch(filePath); - const blob = await response.blob(); - - const tab = await UI.TabManager.addTab(blob, options); - tabIds.push(tab); // 탭 ID 저장 - } - - if (tabIds.length > 0) { - await UI.TabManager.setActiveTab(tabIds[0]); - } - - setFileSetLoading(false); - } - }; - loadDocument(); - }, [instance, versions]); - - return ( - <Dialog - open={open} - onOpenChange={async (val) => { - console.log({ val, fileSetLoading }); - if (!val && fileSetLoading) { - return; - } - - if (instance) { - try { - await instance.UI.dispose(); - setInstance(null); // 상태도 초기화 -======= const loadDocument = async () => { if(instance && versions.length > 0){ @@ -395,42 +202,10 @@ function DocumentViewer({open, setOpen, versions}){ await instance.UI.dispose(); setInstance(null); // 상태도 초기화 ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d } catch (e) { console.warn("dispose error", e); } } -<<<<<<< HEAD - - // cleanupHtmlStyle() - setViewerLoading(false); - setOpen((prev) => !prev); - await setTimeout(() => cleanupHtmlStyle(), 1000); - }} - > - <DialogContent className="w-[70vw] h-[90vh]" style={{ maxWidth: "none" }}> - <DialogHeader className="h-[38px]"> - <DialogTitle>문서 미리보기</DialogTitle> - <DialogDescription>첨부파일 미리보기</DialogDescription> - </DialogHeader> - <div - ref={viewer} - style={{ height: "calc(90vh - 20px - 38px - 1rem - 48px)" }} - > - {viwerLoading && ( - <div className="flex flex-col items-center justify-center py-12"> - <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> - <p className="text-sm text-muted-foreground"> - 문서 뷰어 로딩 중... - </p> - </div> - )} - </div> - </DialogContent> - </Dialog> - ); -}; -======= // cleanupHtmlStyle() setViewerLoading(false); @@ -455,5 +230,4 @@ function DocumentViewer({open, setOpen, versions}){ </DialogContent> </Dialog> ); -} ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d +}
\ No newline at end of file diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index 9feaf3b2..25a005d1 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -531,14 +531,14 @@ export default function DynamicTable({ size="sm" onClick={() => setBatchDownDialog(true)} > - Report Batch + Batch </Button> <Button variant="default" size="sm" onClick={() => setTempUpDialog(true)} > - Temp Upload + Template Upload </Button> <Button variant="default" |
