summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/data-table/data-table-column-header.tsx15
-rw-r--r--components/data-table/data-table-pin-right.tsx173
-rw-r--r--components/data-table/data-table.tsx30
-rw-r--r--components/documents/view-document-dialog.tsx244
-rw-r--r--components/form-data/form-data-table.tsx4
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"