summaryrefslogtreecommitdiff
path: root/lib/items-tech
diff options
context:
space:
mode:
Diffstat (limited to 'lib/items-tech')
-rw-r--r--lib/items-tech/repository.ts248
-rw-r--r--lib/items-tech/service.ts34
-rw-r--r--lib/items-tech/table/delete-items-dialog.tsx388
-rw-r--r--lib/items-tech/table/feature-flags.tsx192
-rw-r--r--lib/items-tech/table/hull/import-item-handler.tsx254
-rw-r--r--lib/items-tech/table/hull/item-excel-template.tsx210
-rw-r--r--lib/items-tech/table/import-excel-button.tsx606
-rw-r--r--lib/items-tech/table/ship/import-item-handler.tsx266
-rw-r--r--lib/items-tech/table/ship/item-excel-template.tsx220
-rw-r--r--lib/items-tech/table/ship/items-table-toolbar-actions.tsx352
-rw-r--r--lib/items-tech/table/top/import-item-handler.tsx271
-rw-r--r--lib/items-tech/table/top/item-excel-template.tsx218
12 files changed, 1634 insertions, 1625 deletions
diff --git a/lib/items-tech/repository.ts b/lib/items-tech/repository.ts
index 1f4f7933..10ae2dab 100644
--- a/lib/items-tech/repository.ts
+++ b/lib/items-tech/repository.ts
@@ -1,124 +1,124 @@
-// src/lib/items/repository.ts
-import db from "@/db/db";
-import { Item, ItemOffshoreTop, ItemOffshoreHull, itemOffshoreHull, itemOffshoreTop, items } from "@/db/schema/items";
-import {
- eq,
- inArray,
- asc,
- desc,
- count,
-} from "drizzle-orm";
-import { PgTransaction } from "drizzle-orm/pg-core";
-export type NewItem = typeof items.$inferInsert
-
-/**
- * 단건/복수 조회 시 공통으로 사용 가능한 SELECT 함수 예시
- * - 트랜잭션(tx)을 받아서 사용하도록 구현
- */
-export async function selectItems(
- tx: PgTransaction<any, any, any>,
- params: {
- where?: any; // drizzle-orm의 조건식 (and, eq...) 등
- orderBy?: (ReturnType<typeof asc> | ReturnType<typeof desc>)[];
- offset?: number;
- limit?: number;
- }
-) {
- const { where, orderBy, offset = 0, limit = 10 } = params;
-
- return tx
- .select()
- .from(items)
- .where(where)
- .orderBy(...(orderBy ?? []))
- .offset(offset)
- .limit(limit);
-}
-/** 총 개수 count */
-export async function countItems(
- tx: PgTransaction<any, any, any>,
- where?: any
-) {
- const res = await tx.select({ count: count() }).from(items).where(where);
- return res[0]?.count ?? 0;
-}
-
-/** 단건 Insert 예시 */
-export async function insertItem(
- tx: PgTransaction<any, any, any>,
- data: NewItem // DB와 동일한 insert 가능한 타입
-) {
- // returning() 사용 시 배열로 돌아오므로 [0]만 리턴
- return tx
- .insert(items)
- .values(data)
- .returning({ id: items.id, createdAt: items.createdAt });
-}
-
-/** 복수 Insert 예시 */
-export async function insertItems(
- tx: PgTransaction<any, any, any>,
- data: Item[]
-) {
- return tx.insert(items).values(data).onConflictDoNothing();
-}
-
-
-
-/** 단건 삭제 */
-export async function deleteItemById(
- tx: PgTransaction<any, any, any>,
- itemId: number
-) {
- return tx.delete(items).where(eq(items.id, itemId));
-}
-
-/** 복수 삭제 */
-export async function deleteItemsByIds(
- tx: PgTransaction<any, any, any>,
- ids: number[]
-) {
- return tx.delete(items).where(inArray(items.id, ids));
-}
-
-/** 전체 삭제 */
-export async function deleteAllItems(
- tx: PgTransaction<any, any, any>,
-) {
- return tx.delete(items);
-}
-
-/** 단건 업데이트 */
-export async function updateItem(
- tx: PgTransaction<any, any, any>,
- itemId: number,
- data: Partial<Item>
-) {
- return tx
- .update(items)
- .set(data)
- .where(eq(items.id, itemId))
- .returning({ id: items.id, createdAt: items.createdAt });
-}
-
-/** 복수 업데이트 */
-export async function updateItems(
- tx: PgTransaction<any, any, any>,
- ids: number[],
- data: Partial<Item>
-) {
- return tx
- .update(items)
- .set(data)
- .where(inArray(items.id, ids))
- .returning({ id: items.id, createdAt: items.createdAt });
-}
-
-export async function findAllItems(): Promise<Item[]> {
- return db.select().from(items).orderBy(asc(items.itemCode));
-}
-export async function findAllOffshoreItems(): Promise<(ItemOffshoreHull | ItemOffshoreTop)[]> {
- const hullItems = await db.select().from(itemOffshoreHull);
- const topItems = await db.select().from(itemOffshoreTop);
- return [...hullItems, ...topItems];
-}
+// src/lib/items/repository.ts
+import db from "@/db/db";
+import { Item, ItemOffshoreTop, ItemOffshoreHull, itemOffshoreHull, itemOffshoreTop, items } from "@/db/schema/items";
+import {
+ eq,
+ inArray,
+ asc,
+ desc,
+ count,
+} from "drizzle-orm";
+import { PgTransaction } from "drizzle-orm/pg-core";
+export type NewItem = typeof items.$inferInsert
+
+/**
+ * 단건/복수 조회 시 공통으로 사용 가능한 SELECT 함수 예시
+ * - 트랜잭션(tx)을 받아서 사용하도록 구현
+ */
+export async function selectItems(
+ tx: PgTransaction<any, any, any>,
+ params: {
+ where?: any; // drizzle-orm의 조건식 (and, eq...) 등
+ orderBy?: (ReturnType<typeof asc> | ReturnType<typeof desc>)[];
+ offset?: number;
+ limit?: number;
+ }
+) {
+ const { where, orderBy, offset = 0, limit = 10 } = params;
+
+ return tx
+ .select()
+ .from(items)
+ .where(where)
+ .orderBy(...(orderBy ?? []))
+ .offset(offset)
+ .limit(limit);
+}
+/** 총 개수 count */
+export async function countItems(
+ tx: PgTransaction<any, any, any>,
+ where?: any
+) {
+ const res = await tx.select({ count: count() }).from(items).where(where);
+ return res[0]?.count ?? 0;
+}
+
+/** 단건 Insert 예시 */
+export async function insertItem(
+ tx: PgTransaction<any, any, any>,
+ data: NewItem // DB와 동일한 insert 가능한 타입
+) {
+ // returning() 사용 시 배열로 돌아오므로 [0]만 리턴
+ return tx
+ .insert(items)
+ .values(data)
+ .returning({ id: items.id, createdAt: items.createdAt });
+}
+
+/** 복수 Insert 예시 */
+export async function insertItems(
+ tx: PgTransaction<any, any, any>,
+ data: Item[]
+) {
+ return tx.insert(items).values(data).onConflictDoNothing();
+}
+
+
+
+/** 단건 삭제 */
+export async function deleteItemById(
+ tx: PgTransaction<any, any, any>,
+ itemId: number
+) {
+ return tx.delete(items).where(eq(items.id, itemId));
+}
+
+/** 복수 삭제 */
+export async function deleteItemsByIds(
+ tx: PgTransaction<any, any, any>,
+ ids: number[]
+) {
+ return tx.delete(items).where(inArray(items.id, ids));
+}
+
+/** 전체 삭제 */
+export async function deleteAllItems(
+ tx: PgTransaction<any, any, any>,
+) {
+ return tx.delete(items);
+}
+
+/** 단건 업데이트 */
+export async function updateItem(
+ tx: PgTransaction<any, any, any>,
+ itemId: number,
+ data: Partial<Item>
+) {
+ return tx
+ .update(items)
+ .set(data)
+ .where(eq(items.id, itemId))
+ .returning({ id: items.id, createdAt: items.createdAt });
+}
+
+/** 복수 업데이트 */
+export async function updateItems(
+ tx: PgTransaction<any, any, any>,
+ ids: number[],
+ data: Partial<Item>
+) {
+ return tx
+ .update(items)
+ .set(data)
+ .where(inArray(items.id, ids))
+ .returning({ id: items.id, createdAt: items.createdAt });
+}
+
+export async function findAllItems(): Promise<Item[]> {
+ return db.select().from(items).orderBy(asc(items.itemCode));
+}
+export async function findAllOffshoreItems(): Promise<(ItemOffshoreHull | ItemOffshoreTop)[]> {
+ const hullItems = await db.select().from(itemOffshoreHull);
+ const topItems = await db.select().from(itemOffshoreTop);
+ return [...hullItems, ...topItems];
+}
diff --git a/lib/items-tech/service.ts b/lib/items-tech/service.ts
index bf2684d7..d93c5f96 100644
--- a/lib/items-tech/service.ts
+++ b/lib/items-tech/service.ts
@@ -405,7 +405,14 @@ export async function createShipbuildingItem(input: TypedItemCreateData) {
unstable_noStore()
try {
- // itemCode는 nullable하게 변경
+ if (!input.itemCode) {
+ return {
+ success: false,
+ message: "아이템 코드는 필수입니다",
+ data: null,
+ error: "필수 필드 누락"
+ }
+ }
const shipData = input as ShipbuildingItemCreateData;
const result = await db.insert(itemShipbuilding).values({
@@ -459,7 +466,14 @@ export async function createShipbuildingImportItem(input: {
unstable_noStore();
try {
- // itemCode는 nullable하게 변경
+ if (!input.itemCode) {
+ return {
+ success: false,
+ message: "아이템 코드는 필수입니다",
+ data: null,
+ error: "필수 필드 누락"
+ }
+ }
// 기존 아이템 및 선종 확인 (itemCode가 있을 경우에만)
if (input.itemCode) {
@@ -525,6 +539,14 @@ export async function createOffshoreTopItem(data: OffshoreTopItemCreateData) {
unstable_noStore();
try {
+ if (!data.itemCode) {
+ return {
+ success: false,
+ message: "아이템 코드는 필수입니다",
+ data: null,
+ error: "필수 필드 누락"
+ }
+ }
// itemCode가 있는 경우 중복 체크
if (data.itemCode && data.itemCode.trim() !== "") {
const existingItem = await db
@@ -586,6 +608,14 @@ export async function createOffshoreHullItem(data: OffshoreHullItemCreateData) {
unstable_noStore();
try {
+ if (!data.itemCode) {
+ return {
+ success: false,
+ message: "아이템 코드는 필수입니다",
+ data: null,
+ error: "필수 필드 누락"
+ }
+ }
// itemCode가 있는 경우 중복 체크
if (data.itemCode && data.itemCode.trim() !== "") {
const existingItem = await db
diff --git a/lib/items-tech/table/delete-items-dialog.tsx b/lib/items-tech/table/delete-items-dialog.tsx
index b94a2333..6ec4b4c7 100644
--- a/lib/items-tech/table/delete-items-dialog.tsx
+++ b/lib/items-tech/table/delete-items-dialog.tsx
@@ -1,194 +1,194 @@
-"use client"
-
-import * as React from "react"
-import { type Row } from "@tanstack/react-table"
-import { Loader, Trash } from "lucide-react"
-import { toast } from "sonner"
-
-import { useMediaQuery } from "@/hooks/use-media-query"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "@/components/ui/drawer"
-
-import { Item } from "@/db/schema/items"
-import {
- removeShipbuildingItems,
- removeOffshoreTopItems,
- removeOffshoreHullItems
-} from "../service"
-
-export type ItemType = 'shipbuilding' | 'offshoreTop' | 'offshoreHull';
-
-interface DeleteItemsDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- items: Row<Item>["original"][]
- showTrigger?: boolean
- onSuccess?: () => void
- itemType: ItemType
-}
-
-export function DeleteItemsDialog({
- items,
- showTrigger = true,
- onSuccess,
- itemType,
- ...props
-}: DeleteItemsDialogProps) {
- const [isDeletePending, startDeleteTransition] = React.useTransition()
- const isDesktop = useMediaQuery("(min-width: 640px)")
-
- const getItemTypeLabel = () => {
- switch (itemType) {
- case 'shipbuilding':
- return '조선 아이템';
- case 'offshoreTop':
- return '해양 TOP 아이템';
- case 'offshoreHull':
- return '해양 HULL 아이템';
- default:
- return '아이템';
- }
- }
-
- async function onDelete() {
- try {
- startDeleteTransition(async () => {
- let result;
-
- switch (itemType) {
- case 'shipbuilding':
- result = await removeShipbuildingItems({
- ids: items.map((item) => item.id),
- });
- break;
- case 'offshoreTop':
- result = await removeOffshoreTopItems({
- ids: items.map((item) => item.id),
- });
- break;
- case 'offshoreHull':
- result = await removeOffshoreHullItems({
- ids: items.map((item) => item.id),
- });
- break;
- default:
- toast.error("지원하지 않는 아이템 타입입니다");
- return;
- }
-
- if (result.error) {
- toast.error(result.error)
- return
- }
-
- props.onOpenChange?.(false)
- toast.success("아이템 삭제 완료")
- onSuccess?.()
- })
- } catch (error) {
- toast.error("오류가 발생했습니다.")
- console.error(error)
- }
- }
-
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({items.length})
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
- <DialogDescription>
- 이 작업은 되돌릴 수 없습니다. 선택한{" "}
- <span className="font-medium">{items.length}</span>
- 개의 {getItemTypeLabel()}이(가) 영구적으로 삭제됩니다.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="gap-2 sm:space-x-0">
- <DialogClose asChild>
- <Button variant="outline">취소</Button>
- </DialogClose>
- <Button
- aria-label="삭제 확인"
- variant="destructive"
- onClick={onDelete}
- disabled={isDeletePending}
- >
- {isDeletePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- 삭제
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
-
- return (
- <Drawer {...props}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({items.length})
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
- <DrawerDescription>
- 이 작업은 되돌릴 수 없습니다. 선택한{" "}
- <span className="font-medium">{items.length}</span>
- 개의 {getItemTypeLabel()}이(가) 영구적으로 삭제됩니다.
- </DrawerDescription>
- </DrawerHeader>
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">취소</Button>
- </DrawerClose>
- <Button
- aria-label="삭제 확인"
- variant="destructive"
- onClick={onDelete}
- disabled={isDeletePending}
- >
- {isDeletePending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- 삭제
- </Button>
- </DrawerFooter>
- </DrawerContent>
- </Drawer>
- )
-}
+"use client"
+
+import * as React from "react"
+import { type Row } from "@tanstack/react-table"
+import { Loader, Trash } from "lucide-react"
+import { toast } from "sonner"
+
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer"
+
+import { Item } from "@/db/schema/items"
+import {
+ removeShipbuildingItems,
+ removeOffshoreTopItems,
+ removeOffshoreHullItems
+} from "../service"
+
+export type ItemType = 'shipbuilding' | 'offshoreTop' | 'offshoreHull';
+
+interface DeleteItemsDialogProps
+ extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ items: Row<Item>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+ itemType: ItemType
+}
+
+export function DeleteItemsDialog({
+ items,
+ showTrigger = true,
+ onSuccess,
+ itemType,
+ ...props
+}: DeleteItemsDialogProps) {
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+
+ const getItemTypeLabel = () => {
+ switch (itemType) {
+ case 'shipbuilding':
+ return '조선 아이템';
+ case 'offshoreTop':
+ return '해양 TOP 아이템';
+ case 'offshoreHull':
+ return '해양 HULL 아이템';
+ default:
+ return '아이템';
+ }
+ }
+
+ async function onDelete() {
+ try {
+ startDeleteTransition(async () => {
+ let result;
+
+ switch (itemType) {
+ case 'shipbuilding':
+ result = await removeShipbuildingItems({
+ ids: items.map((item) => item.id),
+ });
+ break;
+ case 'offshoreTop':
+ result = await removeOffshoreTopItems({
+ ids: items.map((item) => item.id),
+ });
+ break;
+ case 'offshoreHull':
+ result = await removeOffshoreHullItems({
+ ids: items.map((item) => item.id),
+ });
+ break;
+ default:
+ toast.error("지원하지 않는 아이템 타입입니다");
+ return;
+ }
+
+ if (result.error) {
+ toast.error(result.error)
+ return
+ }
+
+ props.onOpenChange?.(false)
+ toast.success("아이템 삭제 완료")
+ onSuccess?.()
+ })
+ } catch (error) {
+ toast.error("오류가 발생했습니다.")
+ console.error(error)
+ }
+ }
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ 삭제 ({items.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
+ <DialogDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택한{" "}
+ <span className="font-medium">{items.length}</span>
+ 개의 {getItemTypeLabel()}이(가) 영구적으로 삭제됩니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">취소</Button>
+ </DialogClose>
+ <Button
+ aria-label="삭제 확인"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 삭제
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ 삭제 ({items.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
+ <DrawerDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택한{" "}
+ <span className="font-medium">{items.length}</span>
+ 개의 {getItemTypeLabel()}이(가) 영구적으로 삭제됩니다.
+ </DrawerDescription>
+ </DrawerHeader>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">취소</Button>
+ </DrawerClose>
+ <Button
+ aria-label="삭제 확인"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ 삭제
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+}
diff --git a/lib/items-tech/table/feature-flags.tsx b/lib/items-tech/table/feature-flags.tsx
index aaae6af2..cc5093ca 100644
--- a/lib/items-tech/table/feature-flags.tsx
+++ b/lib/items-tech/table/feature-flags.tsx
@@ -1,96 +1,96 @@
-"use client"
-
-import * as React from "react"
-import { useQueryState } from "nuqs"
-
-import { dataTableConfig, type DataTableConfig } from "@/config/data-table"
-import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip"
-
-type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"]
-
-interface TasksTableContextProps {
- featureFlags: FeatureFlagValue[]
- setFeatureFlags: (value: FeatureFlagValue[]) => void
-}
-
-const TasksTableContext = React.createContext<TasksTableContextProps>({
- featureFlags: [],
- setFeatureFlags: () => {},
-})
-
-export function useTasksTable() {
- const context = React.useContext(TasksTableContext)
- if (!context) {
- throw new Error("useTasksTable must be used within a TasksTableProvider")
- }
- return context
-}
-
-export function TasksTableProvider({ children }: React.PropsWithChildren) {
- const [featureFlags, setFeatureFlags] = useQueryState<FeatureFlagValue[]>(
- "featureFlags",
- {
- defaultValue: [],
- parse: (value) => value.split(",") as FeatureFlagValue[],
- serialize: (value) => value.join(","),
- eq: (a, b) =>
- a.length === b.length && a.every((value, index) => value === b[index]),
- clearOnDefault: true,
- }
- )
-
- return (
- <TasksTableContext.Provider
- value={{
- featureFlags,
- setFeatureFlags: (value) => void setFeatureFlags(value),
- }}
- >
- <div className="w-full overflow-x-auto">
- <ToggleGroup
- type="multiple"
- variant="outline"
- size="sm"
- value={featureFlags}
- onValueChange={(value: FeatureFlagValue[]) => setFeatureFlags(value)}
- className="w-fit"
- >
- {dataTableConfig.featureFlags.map((flag) => (
- <Tooltip key={flag.value}>
- <ToggleGroupItem
- value={flag.value}
- className="whitespace-nowrap px-3 text-xs"
- asChild
- >
- <TooltipTrigger>
- <flag.icon
- className="mr-2 size-3.5 shrink-0"
- aria-hidden="true"
- />
- {flag.label}
- </TooltipTrigger>
- </ToggleGroupItem>
- <TooltipContent
- align="start"
- side="bottom"
- sideOffset={6}
- className="flex max-w-60 flex-col space-y-1.5 border bg-background py-2 font-semibold text-foreground"
- >
- <div>{flag.tooltipTitle}</div>
- <div className="text-xs text-muted-foreground">
- {flag.tooltipDescription}
- </div>
- </TooltipContent>
- </Tooltip>
- ))}
- </ToggleGroup>
- </div>
- {children}
- </TasksTableContext.Provider>
- )
-}
+"use client"
+
+import * as React from "react"
+import { useQueryState } from "nuqs"
+
+import { dataTableConfig, type DataTableConfig } from "@/config/data-table"
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"]
+
+interface TasksTableContextProps {
+ featureFlags: FeatureFlagValue[]
+ setFeatureFlags: (value: FeatureFlagValue[]) => void
+}
+
+const TasksTableContext = React.createContext<TasksTableContextProps>({
+ featureFlags: [],
+ setFeatureFlags: () => {},
+})
+
+export function useTasksTable() {
+ const context = React.useContext(TasksTableContext)
+ if (!context) {
+ throw new Error("useTasksTable must be used within a TasksTableProvider")
+ }
+ return context
+}
+
+export function TasksTableProvider({ children }: React.PropsWithChildren) {
+ const [featureFlags, setFeatureFlags] = useQueryState<FeatureFlagValue[]>(
+ "featureFlags",
+ {
+ defaultValue: [],
+ parse: (value) => value.split(",") as FeatureFlagValue[],
+ serialize: (value) => value.join(","),
+ eq: (a, b) =>
+ a.length === b.length && a.every((value, index) => value === b[index]),
+ clearOnDefault: true,
+ }
+ )
+
+ return (
+ <TasksTableContext.Provider
+ value={{
+ featureFlags,
+ setFeatureFlags: (value) => void setFeatureFlags(value),
+ }}
+ >
+ <div className="w-full overflow-x-auto">
+ <ToggleGroup
+ type="multiple"
+ variant="outline"
+ size="sm"
+ value={featureFlags}
+ onValueChange={(value: FeatureFlagValue[]) => setFeatureFlags(value)}
+ className="w-fit"
+ >
+ {dataTableConfig.featureFlags.map((flag) => (
+ <Tooltip key={flag.value}>
+ <ToggleGroupItem
+ value={flag.value}
+ className="whitespace-nowrap px-3 text-xs"
+ asChild
+ >
+ <TooltipTrigger>
+ <flag.icon
+ className="mr-2 size-3.5 shrink-0"
+ aria-hidden="true"
+ />
+ {flag.label}
+ </TooltipTrigger>
+ </ToggleGroupItem>
+ <TooltipContent
+ align="start"
+ side="bottom"
+ sideOffset={6}
+ className="flex max-w-60 flex-col space-y-1.5 border bg-background py-2 font-semibold text-foreground"
+ >
+ <div>{flag.tooltipTitle}</div>
+ <div className="text-xs text-muted-foreground">
+ {flag.tooltipDescription}
+ </div>
+ </TooltipContent>
+ </Tooltip>
+ ))}
+ </ToggleGroup>
+ </div>
+ {children}
+ </TasksTableContext.Provider>
+ )
+}
diff --git a/lib/items-tech/table/hull/import-item-handler.tsx b/lib/items-tech/table/hull/import-item-handler.tsx
index 8c8fc57d..9090dab1 100644
--- a/lib/items-tech/table/hull/import-item-handler.tsx
+++ b/lib/items-tech/table/hull/import-item-handler.tsx
@@ -1,127 +1,127 @@
-"use client"
-
-import { z } from "zod"
-import { createOffshoreHullItem } from "../../service"
-
-// 해양 HULL 기능(공종) 유형 enum
-const HULL_WORK_TYPES = ["HA", "HE", "HH", "HM", "HO", "HP", "NC"] as const;
-
-// 아이템 데이터 검증을 위한 Zod 스키마
-const itemSchema = z.object({
- itemCode: z.string().optional(),
- workType: z.enum(HULL_WORK_TYPES, {
- required_error: "기능(공종)은 필수입니다",
- }),
- itemList: z.string().nullable().optional(),
- subItemList: z.string().nullable().optional(),
-});
-
-interface ProcessResult {
- successCount: number;
- errorCount: number;
- errors: Array<{ row: number; message: string; itemCode?: string; workType?: string }>;
-}
-
-/**
- * Excel 파일에서 가져온 해양 HULL 아이템 데이터 처리하는 함수
- */
-export async function processHullFileImport(
- jsonData: Record<string, unknown>[],
- progressCallback?: (current: number, total: number) => void
-): Promise<ProcessResult> {
- // 결과 카운터 초기화
- let successCount = 0;
- let errorCount = 0;
- const errors: Array<{ row: number; message: string }> = [];
-
- // 빈 행 등 필터링
- const dataRows = jsonData.filter(row => {
- // 빈 행 건너뛰기
- if (Object.values(row).every(val => !val)) {
- return false;
- }
- return true;
- });
-
- // 데이터 행이 없으면 빈 결과 반환
- if (dataRows.length === 0) {
- return { successCount: 0, errorCount: 0, errors: [] };
- }
-
- // 각 행에 대해 처리
- for (let i = 0; i < dataRows.length; i++) {
- const row = dataRows[i];
- const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작
-
- // 진행 상황 콜백 호출
- if (progressCallback) {
- progressCallback(i + 1, dataRows.length);
- }
-
- try {
- // 필드 매핑 (한글/영문 필드명 모두 지원)
- const itemCode = row["자재 그룹"] || row["itemCode"] || "";
- const workType = row["기능(공종)"] || row["workType"] || "";
- const itemList = row["자재명"] || row["itemList"] || null;
- const subItemList = row["자재명(상세)"] || row["subItemList"] || null;
-
- // 데이터 정제
- const cleanedRow = {
- itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
- workType: typeof workType === 'string' ? workType.trim() : String(workType).trim(),
- itemList: itemList ? (typeof itemList === 'string' ? itemList : String(itemList)) : null,
- subItemList: subItemList ? (typeof subItemList === 'string' ? subItemList : String(subItemList)) : null,
- };
-
- // 데이터 유효성 검사
- const validationResult = itemSchema.safeParse(cleanedRow);
-
- if (!validationResult.success) {
- const errorMessage = validationResult.error.errors.map(
- err => `${err.path.join('.')}: ${err.message}`
- ).join(', ');
-
- errors.push({ row: rowIndex, message: errorMessage });
- errorCount++;
- continue;
- }
-
- // 해양 HULL 아이템 생성
- const result = await createOffshoreHullItem({
- itemCode: cleanedRow.itemCode,
- workType: cleanedRow.workType as "HA" | "HE" | "HH" | "HM" | "HO" | "HP" | "NC",
- itemList: cleanedRow.itemList,
- subItemList: cleanedRow.subItemList,
- });
-
- if (result.success) {
- successCount++;
- } else {
- errors.push({
- row: rowIndex,
- message: result.message || result.error || "알 수 없는 오류"
- });
- errorCount++;
- }
- } catch (error) {
- console.error(`${rowIndex}행 처리 오류:`, error);
- errors.push({
- row: rowIndex,
- message: error instanceof Error ? error.message : "알 수 없는 오류"
- });
- errorCount++;
- }
-
- // 비동기 작업 쓰로틀링
- if (i % 5 === 0) {
- await new Promise(resolve => setTimeout(resolve, 10));
- }
- }
-
- // 처리 결과 반환
- return {
- successCount,
- errorCount,
- errors: errors.length > 0 ? errors : undefined
- };
-}
+"use client"
+
+import { z } from "zod"
+import { createOffshoreHullItem } from "../../service"
+
+// 해양 HULL 기능(공종) 유형 enum
+const HULL_WORK_TYPES = ["HA", "HE", "HH", "HM", "HO", "HP", "NC"] as const;
+
+// 아이템 데이터 검증을 위한 Zod 스키마
+const itemSchema = z.object({
+ itemCode: z.string().optional(),
+ workType: z.enum(HULL_WORK_TYPES, {
+ required_error: "기능(공종)은 필수입니다",
+ }),
+ itemList: z.string().nullable().optional(),
+ subItemList: z.string().nullable().optional(),
+});
+
+interface ProcessResult {
+ successCount: number;
+ errorCount: number;
+ errors: Array<{ row: number; message: string }>;
+}
+
+/**
+ * Excel 파일에서 가져온 해양 HULL 아이템 데이터 처리하는 함수
+ */
+export async function processHullFileImport(
+ jsonData: Record<string, unknown>[],
+ progressCallback?: (current: number, total: number) => void
+): Promise<ProcessResult> {
+ // 결과 카운터 초기화
+ let successCount = 0;
+ let errorCount = 0;
+ const errors: Array<{ row: number; message: string }> = [];
+
+ // 빈 행 등 필터링
+ const dataRows = jsonData.filter(row => {
+ // 빈 행 건너뛰기
+ if (Object.values(row).every(val => !val)) {
+ return false;
+ }
+ return true;
+ });
+
+ // 데이터 행이 없으면 빈 결과 반환
+ if (dataRows.length === 0) {
+ return { successCount: 0, errorCount: 0, errors: [] };
+ }
+
+ // 각 행에 대해 처리
+ for (let i = 0; i < dataRows.length; i++) {
+ const row = dataRows[i];
+ const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작
+
+ // 진행 상황 콜백 호출
+ if (progressCallback) {
+ progressCallback(i + 1, dataRows.length);
+ }
+
+ try {
+ // 필드 매핑 (한글/영문 필드명 모두 지원)
+ const itemCode = row["자재 그룹"] || row["itemCode"] || "";
+ const workType = row["기능(공종)"] || row["workType"] || "";
+ const itemList = row["자재명"] || row["itemList"] || null;
+ const subItemList = row["자재명(상세)"] || row["subItemList"] || null;
+
+ // 데이터 정제
+ const cleanedRow = {
+ itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
+ workType: typeof workType === 'string' ? workType.trim() : String(workType).trim(),
+ itemList: itemList ? (typeof itemList === 'string' ? itemList : String(itemList)) : null,
+ subItemList: subItemList ? (typeof subItemList === 'string' ? subItemList : String(subItemList)) : null,
+ };
+
+ // 데이터 유효성 검사
+ const validationResult = itemSchema.safeParse(cleanedRow);
+
+ if (!validationResult.success) {
+ const errorMessage = validationResult.error.errors.map(
+ err => `${err.path.join('.')}: ${err.message}`
+ ).join(', ');
+
+ errors.push({ row: rowIndex, message: errorMessage });
+ errorCount++;
+ continue;
+ }
+
+ // 해양 HULL 아이템 생성
+ const result = await createOffshoreHullItem({
+ itemCode: cleanedRow.itemCode,
+ workType: cleanedRow.workType as "HA" | "HE" | "HH" | "HM" | "HO" | "HP" | "NC",
+ itemList: cleanedRow.itemList,
+ subItemList: cleanedRow.subItemList,
+ });
+
+ if (result.success) {
+ successCount++;
+ } else {
+ errors.push({
+ row: rowIndex,
+ message: result.message || result.error || "알 수 없는 오류"
+ });
+ errorCount++;
+ }
+ } catch (error) {
+ console.error(`${rowIndex}행 처리 오류:`, error);
+ errors.push({
+ row: rowIndex,
+ message: error instanceof Error ? error.message : "알 수 없는 오류"
+ });
+ errorCount++;
+ }
+
+ // 비동기 작업 쓰로틀링
+ if (i % 5 === 0) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+ }
+
+ // 처리 결과 반환
+ return {
+ successCount,
+ errorCount,
+ errors: errors.length > 0 ? errors : []
+ };
+}
diff --git a/lib/items-tech/table/hull/item-excel-template.tsx b/lib/items-tech/table/hull/item-excel-template.tsx
index 79512b9b..2e5196e1 100644
--- a/lib/items-tech/table/hull/item-excel-template.tsx
+++ b/lib/items-tech/table/hull/item-excel-template.tsx
@@ -1,105 +1,105 @@
-import * as ExcelJS from 'exceljs';
-import { saveAs } from "file-saver";
-
-/**
- * 해양 HULL 아이템 데이터 가져오기를 위한 Excel 템플릿 파일 생성 및 다운로드
- */
-export async function exportHullItemTemplate() {
- // 워크북 생성
- const workbook = new ExcelJS.Workbook();
- workbook.creator = 'Offshore HULL Item Management System';
- workbook.created = new Date();
-
- // 워크시트 생성
- const worksheet = workbook.addWorksheet('해양 HULL 아이템');
-
- // 컬럼 헤더 정의 및 스타일 적용
- worksheet.columns = [
- { header: '자재 그룹', key: 'itemCode', width: 15 },
- { header: '기능(공종)', key: 'workType', width: 15 },
- { header: '자재명', key: 'itemList', width: 20 },
- { header: '자재명(상세)', key: 'subItemList', width: 20 },
- ];
-
- // 헤더 스타일 적용
- const headerRow = worksheet.getRow(1);
- headerRow.font = { bold: true };
- headerRow.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFE0E0E0' }
- };
- headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
-
- // 테두리 스타일 적용
- headerRow.eachCell((cell) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
-
- // 샘플 데이터 추가
- const sampleData = [
- {
- itemCode: 'HULL001',
- workType: 'HA',
- itemList: '항목1 샘플 데이터',
- subItemList: '항목2 샘플 데이터',
- },
- {
- itemCode: 'HULL002',
- workType: 'HE',
- itemList: '항목1 샘플 데이터',
- subItemList: '항목2 샘플 데이터',
- }
- ];
-
- // 데이터 행 추가
- sampleData.forEach(item => {
- worksheet.addRow(item);
- });
-
- // 데이터 행 스타일 적용
- worksheet.eachRow((row, rowNumber) => {
- if (rowNumber > 1) { // 헤더를 제외한 데이터 행
- row.eachCell((cell) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
- }
- });
-
- // 워크시트 보호 (선택적)
- worksheet.protect('', {
- selectLockedCells: true,
- selectUnlockedCells: true,
- formatColumns: true,
- formatRows: true,
- insertColumns: false,
- insertRows: true,
- insertHyperlinks: false,
- deleteColumns: false,
- deleteRows: true,
- sort: true,
- autoFilter: true,
- pivotTables: false
- });
-
- try {
- // 워크북을 Blob으로 변환
- const buffer = await workbook.xlsx.writeBuffer();
- const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
- saveAs(blob, 'offshore-hull-item-template.xlsx');
- return true;
- } catch (error) {
- console.error('Excel 템플릿 생성 오류:', error);
- throw error;
- }
-}
+import * as ExcelJS from 'exceljs';
+import { saveAs } from "file-saver";
+
+/**
+ * 해양 HULL 아이템 데이터 가져오기를 위한 Excel 템플릿 파일 생성 및 다운로드
+ */
+export async function exportHullItemTemplate() {
+ // 워크북 생성
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = 'Offshore HULL Item Management System';
+ workbook.created = new Date();
+
+ // 워크시트 생성
+ const worksheet = workbook.addWorksheet('해양 HULL 아이템');
+
+ // 컬럼 헤더 정의 및 스타일 적용
+ worksheet.columns = [
+ { header: '자재 그룹', key: 'itemCode', width: 15 },
+ { header: '기능(공종)', key: 'workType', width: 15 },
+ { header: '자재명', key: 'itemList', width: 20 },
+ { header: '자재명(상세)', key: 'subItemList', width: 20 },
+ ];
+
+ // 헤더 스타일 적용
+ const headerRow = worksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ };
+ headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
+
+ // 테두리 스타일 적용
+ headerRow.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+
+ // 샘플 데이터 추가
+ const sampleData = [
+ {
+ itemCode: 'HULL001',
+ workType: 'HA',
+ itemList: '항목1 샘플 데이터',
+ subItemList: '항목2 샘플 데이터',
+ },
+ {
+ itemCode: 'HULL002',
+ workType: 'HE',
+ itemList: '항목1 샘플 데이터',
+ subItemList: '항목2 샘플 데이터',
+ }
+ ];
+
+ // 데이터 행 추가
+ sampleData.forEach(item => {
+ worksheet.addRow(item);
+ });
+
+ // 데이터 행 스타일 적용
+ worksheet.eachRow((row, rowNumber) => {
+ if (rowNumber > 1) { // 헤더를 제외한 데이터 행
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+ }
+ });
+
+ // 워크시트 보호 (선택적)
+ worksheet.protect('', {
+ selectLockedCells: true,
+ selectUnlockedCells: true,
+ formatColumns: true,
+ formatRows: true,
+ insertColumns: false,
+ insertRows: true,
+ insertHyperlinks: false,
+ deleteColumns: false,
+ deleteRows: true,
+ sort: true,
+ autoFilter: true,
+ pivotTables: false
+ });
+
+ try {
+ // 워크북을 Blob으로 변환
+ const buffer = await workbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+ saveAs(blob, 'offshore-hull-item-template.xlsx');
+ return true;
+ } catch (error) {
+ console.error('Excel 템플릿 생성 오류:', error);
+ throw error;
+ }
+}
diff --git a/lib/items-tech/table/import-excel-button.tsx b/lib/items-tech/table/import-excel-button.tsx
index 4565c365..f8ba9f6d 100644
--- a/lib/items-tech/table/import-excel-button.tsx
+++ b/lib/items-tech/table/import-excel-button.tsx
@@ -1,304 +1,304 @@
-"use client"
-
-import * as React from "react"
-import { Upload } from "lucide-react"
-import { toast } from "sonner"
-import * as ExcelJS from 'exceljs'
-
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { Progress } from "@/components/ui/progress"
-import { processFileImport } from "./ship/import-item-handler"
-import { processTopFileImport } from "./top/import-item-handler"
-import { processHullFileImport } from "./hull/import-item-handler"
-import { decryptWithServerAction } from "@/components/drm/drmUtils"
-
-
-// 선박 아이템 타입
-type ItemType = "ship" | "top" | "hull";
-
-const ITEM_TYPE_NAMES = {
- ship: "조선 아이템",
- top: "해양 TOP 아이템",
- hull: "해양 HULL 아이템",
-};
-
-interface ImportItemButtonProps {
- itemType: ItemType;
- onSuccess?: () => void;
-}
-
-export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) {
- const [open, setOpen] = React.useState(false);
- const [file, setFile] = React.useState<File | null>(null);
- const [isUploading, setIsUploading] = React.useState(false);
- const [progress, setProgress] = React.useState(0);
- const [error, setError] = React.useState<string | null>(null);
-
- const fileInputRef = React.useRef<HTMLInputElement>(null);
-
- // 파일 선택 처리
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const selectedFile = e.target.files?.[0];
- if (!selectedFile) return;
-
- if (!selectedFile.name.endsWith('.xlsx') && !selectedFile.name.endsWith('.xls')) {
- setError("Excel 파일(.xlsx 또는 .xls)만 가능합니다.");
- return;
- }
-
- setFile(selectedFile);
- setError(null);
- };
-
-
- // 데이터 가져오기 처리
- const handleImport = async () => {
- if (!file) {
- setError("가져올 파일을 선택해주세요.");
- return;
- }
-
- try {
- setIsUploading(true);
- setProgress(0);
- setError(null);
-
- // DRM 복호화 처리 - 서버 액션 직접 호출
- let arrayBuffer: ArrayBuffer;
- try {
- setProgress(10);
- toast.info("파일 복호화 중...");
- arrayBuffer = await decryptWithServerAction(file);
- setProgress(30);
- } catch (decryptError) {
- console.error("파일 복호화 실패, 원본 파일 사용:", decryptError);
- toast.warning("파일 복호화에 실패하여 원본 파일을 사용합니다.");
- // 복호화 실패 시 원본 파일 사용
- arrayBuffer = await file.arrayBuffer();
- }
- // ExcelJS 워크북 로드
- const workbook = new ExcelJS.Workbook();
- await workbook.xlsx.load(arrayBuffer);
-
- // 첫 번째 워크시트 가져오기
- const worksheet = workbook.worksheets[0];
- if (!worksheet) {
- throw new Error("Excel 파일에 워크시트가 없습니다.");
- }
- // 헤더 행 찾기
- let headerRowIndex = 1;
- let headerRow: ExcelJS.Row | undefined;
- let headerValues: (string | null)[] = [];
-
-
- worksheet.eachRow((row, rowNumber) => {
- const values = row.values as (string | null)[];
- if (!headerRow && values.some(v => v === "자재 그룹" || v === "자재 코드" || v === "itemCode" || v === "item_code")) {
- headerRowIndex = rowNumber;
- headerRow = row;
- headerValues = [...values];
- }
- });
-
- if (!headerRow) {
- throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다.");
- }
-
- // 헤더를 기반으로 인덱스 매핑 생성
- const headerMapping: Record<string, number> = {};
- headerValues.forEach((value, index) => {
- if (typeof value === 'string') {
- headerMapping[value] = index;
- }
- });
-
- // 필수 헤더 확인 (타입별 구분)
- const requiredHeaders: string[] = ["자재 그룹", "기능(공종)"];
-
- const alternativeHeaders = {
- "자재 그룹": ["itemCode", "item_code"],
- "기능(공종)": ["workType"],
- "자재명": ["itemList"],
- "자재명(상세)": ["subItemList"]
- };
-
- // 헤더 매핑 확인 (대체 이름 포함)
- const missingHeaders = requiredHeaders.filter(header => {
- const alternatives = alternativeHeaders[header as keyof typeof alternativeHeaders] || [];
- return !(header in headerMapping) &&
- !alternatives.some(alt => alt in headerMapping);
- });
-
- if (missingHeaders.length > 0) {
- throw new Error(`다음 필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`);
- }
-
- // 데이터 행 추출 (헤더 이후 행부터)
- const dataRows: Record<string, any>[] = [];
-
- worksheet.eachRow((row, rowNumber) => {
- if (rowNumber > headerRowIndex) {
- const rowData: Record<string, any> = {};
- const values = row.values as (string | null | undefined)[];
-
- // 헤더 매핑에 따라 데이터 추출
- Object.entries(headerMapping).forEach(([header, index]) => {
- rowData[header] = values[index] || "";
- });
-
- // 빈 행이 아닌 경우만 추가
- if (Object.values(rowData).some(value => value && value.toString().trim() !== "")) {
- dataRows.push(rowData);
- }
- }
- });
-
- if (dataRows.length === 0) {
- throw new Error("Excel 파일에 가져올 데이터가 없습니다.");
- }
-
- // 진행 상황 업데이트를 위한 콜백
- const updateProgress = (current: number, total: number) => {
- const percentage = Math.round((current / total) * 100);
- setProgress(percentage);
- };
-
- // 선택된 타입에 따라 적절한 프로세스 함수 호출
- let result: { successCount: number; errorCount: number; errors?: Array<{ row: number; message: string; itemCode?: string; workType?: string }> };
- if (itemType === "top") {
- result = await processTopFileImport(dataRows, updateProgress);
- } else if (itemType === "hull") {
- result = await processHullFileImport(dataRows, updateProgress);
- } else {
- result = await processFileImport(dataRows, updateProgress);
- }
-
- toast.success(`${result.successCount}개의 ${ITEM_TYPE_NAMES[itemType]}이(가) 성공적으로 가져와졌습니다.`);
-
- if (result.errorCount > 0) {
- const errorDetails = result.errors?.map((error: { row: number; message: string; itemCode?: string; workType?: string }) =>
- `행 ${error.row}: ${error.itemCode || '알 수 없음'} (${error.workType || '알 수 없음'}) - ${error.message}`
- ).join('\n') || '오류 정보를 가져올 수 없습니다.';
-
- console.error('Import 오류 상세:', errorDetails);
- toast.error(`${result.errorCount}개의 항목 처리 실패. 콘솔에서 상세 내용을 확인하세요.`);
- }
-
- // 상태 초기화 및 다이얼로그 닫기
- setFile(null);
- setOpen(false);
-
- // 성공 콜백 호출
- if (onSuccess) {
- onSuccess();
- }
- } catch (error) {
- console.error("Excel 파일 처리 중 오류 발생:", error);
- setError(error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다.");
- } finally {
- setIsUploading(false);
- }
- };
-
-
-
- // 다이얼로그 열기/닫기 핸들러
- const handleOpenChange = (newOpen: boolean) => {
- if (!newOpen) {
- // 닫을 때 상태 초기화
- setFile(null);
- setError(null);
- setProgress(0);
- if (fileInputRef.current) {
- fileInputRef.current.value = "";
- }
- }
- setOpen(newOpen);
- };
-
- return (
- <>
- <Button
- variant="outline"
- size="sm"
- className="gap-2"
- onClick={() => setOpen(true)}
- disabled={isUploading}
- >
- <Upload className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Import</span>
- </Button>
-
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="sm:max-w-[500px]">
- <DialogHeader>
- <DialogTitle>{ITEM_TYPE_NAMES[itemType]} 가져오기</DialogTitle>
- <DialogDescription>
- {ITEM_TYPE_NAMES[itemType]}을 Excel 파일에서 가져옵니다.
- <br />
- 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요.
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4 py-4">
- <div className="flex items-center gap-4">
- <input
- type="file"
- ref={fileInputRef}
- className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-foreground file:font-medium"
- accept=".xlsx,.xls"
- onChange={handleFileChange}
- disabled={isUploading}
- />
- </div>
-
- {file && (
- <div className="text-sm text-muted-foreground">
- 선택된 파일: <span className="font-medium">{file.name}</span> ({(file.size / 1024).toFixed(1)} KB)
- </div>
- )}
-
- {isUploading && (
- <div className="space-y-2">
- <Progress value={progress} />
- <p className="text-sm text-muted-foreground text-center">
- {progress}% 완료
- </p>
- </div>
- )}
-
- {error && (
- <div className="text-sm font-medium text-destructive">
- {error}
- </div>
- )}
- </div>
-
- <DialogFooter>
- <Button
- variant="outline"
- onClick={() => setOpen(false)}
- disabled={isUploading}
- >
- 취소
- </Button>
- <Button
- onClick={handleImport}
- disabled={!file || isUploading}
- >
- {isUploading ? "처리 중..." : "가져오기"}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </>
- );
+"use client"
+
+import * as React from "react"
+import { Upload } from "lucide-react"
+import { toast } from "sonner"
+import * as ExcelJS from 'exceljs'
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Progress } from "@/components/ui/progress"
+import { processFileImport } from "./ship/import-item-handler"
+import { processTopFileImport } from "./top/import-item-handler"
+import { processHullFileImport } from "./hull/import-item-handler"
+import { decryptWithServerAction } from "@/components/drm/drmUtils"
+
+
+// 선박 아이템 타입
+type ItemType = "ship" | "top" | "hull";
+
+const ITEM_TYPE_NAMES = {
+ ship: "조선 아이템",
+ top: "해양 TOP 아이템",
+ hull: "해양 HULL 아이템",
+};
+
+interface ImportItemButtonProps {
+ itemType: ItemType;
+ onSuccess?: () => void;
+}
+
+export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) {
+ const [open, setOpen] = React.useState(false);
+ const [file, setFile] = React.useState<File | null>(null);
+ const [isUploading, setIsUploading] = React.useState(false);
+ const [progress, setProgress] = React.useState(0);
+ const [error, setError] = React.useState<string | null>(null);
+
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
+
+ // 파일 선택 처리
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const selectedFile = e.target.files?.[0];
+ if (!selectedFile) return;
+
+ if (!selectedFile.name.endsWith('.xlsx') && !selectedFile.name.endsWith('.xls')) {
+ setError("Excel 파일(.xlsx 또는 .xls)만 가능합니다.");
+ return;
+ }
+
+ setFile(selectedFile);
+ setError(null);
+ };
+
+
+ // 데이터 가져오기 처리
+ const handleImport = async () => {
+ if (!file) {
+ setError("가져올 파일을 선택해주세요.");
+ return;
+ }
+
+ try {
+ setIsUploading(true);
+ setProgress(0);
+ setError(null);
+
+ // DRM 복호화 처리 - 서버 액션 직접 호출
+ let arrayBuffer: ArrayBuffer;
+ try {
+ setProgress(10);
+ toast.info("파일 복호화 중...");
+ arrayBuffer = await decryptWithServerAction(file);
+ setProgress(30);
+ } catch (decryptError) {
+ console.error("파일 복호화 실패, 원본 파일 사용:", decryptError);
+ toast.warning("파일 복호화에 실패하여 원본 파일을 사용합니다.");
+ // 복호화 실패 시 원본 파일 사용
+ arrayBuffer = await file.arrayBuffer();
+ }
+ // ExcelJS 워크북 로드
+ const workbook = new ExcelJS.Workbook();
+ await workbook.xlsx.load(arrayBuffer);
+
+ // 첫 번째 워크시트 가져오기
+ const worksheet = workbook.worksheets[0];
+ if (!worksheet) {
+ throw new Error("Excel 파일에 워크시트가 없습니다.");
+ }
+ // 헤더 행 찾기
+ let headerRowIndex = 1;
+ let headerRow: ExcelJS.Row | undefined;
+ let headerValues: (string | null)[] = [];
+
+
+ worksheet.eachRow((row, rowNumber) => {
+ const values = row.values as (string | null)[];
+ if (!headerRow && values.some(v => v === "자재 그룹" || v === "자재 코드" || v === "itemCode" || v === "item_code")) {
+ headerRowIndex = rowNumber;
+ headerRow = row;
+ headerValues = [...values];
+ }
+ });
+
+ if (!headerRow) {
+ throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다.");
+ }
+
+ // 헤더를 기반으로 인덱스 매핑 생성
+ const headerMapping: Record<string, number> = {};
+ headerValues.forEach((value, index) => {
+ if (typeof value === 'string') {
+ headerMapping[value] = index;
+ }
+ });
+
+ // 필수 헤더 확인 (타입별 구분)
+ const requiredHeaders: string[] = ["자재 그룹", "기능(공종)"];
+
+ const alternativeHeaders = {
+ "자재 그룹": ["itemCode", "item_code"],
+ "기능(공종)": ["workType"],
+ "자재명": ["itemList"],
+ "자재명(상세)": ["subItemList"]
+ };
+
+ // 헤더 매핑 확인 (대체 이름 포함)
+ const missingHeaders = requiredHeaders.filter(header => {
+ const alternatives = alternativeHeaders[header as keyof typeof alternativeHeaders] || [];
+ return !(header in headerMapping) &&
+ !alternatives.some(alt => alt in headerMapping);
+ });
+
+ if (missingHeaders.length > 0) {
+ throw new Error(`다음 필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`);
+ }
+
+ // 데이터 행 추출 (헤더 이후 행부터)
+ const dataRows: Record<string, any>[] = [];
+
+ worksheet.eachRow((row, rowNumber) => {
+ if (rowNumber > headerRowIndex) {
+ const rowData: Record<string, any> = {};
+ const values = row.values as (string | null | undefined)[];
+
+ // 헤더 매핑에 따라 데이터 추출
+ Object.entries(headerMapping).forEach(([header, index]) => {
+ rowData[header] = values[index] || "";
+ });
+
+ // 빈 행이 아닌 경우만 추가
+ if (Object.values(rowData).some(value => value && value.toString().trim() !== "")) {
+ dataRows.push(rowData);
+ }
+ }
+ });
+
+ if (dataRows.length === 0) {
+ throw new Error("Excel 파일에 가져올 데이터가 없습니다.");
+ }
+
+ // 진행 상황 업데이트를 위한 콜백
+ const updateProgress = (current: number, total: number) => {
+ const percentage = Math.round((current / total) * 100);
+ setProgress(percentage);
+ };
+
+ // 선택된 타입에 따라 적절한 프로세스 함수 호출
+ let result: { successCount: number; errorCount: number; errors?: Array<{ row: number; message: string }> };
+ if (itemType === "top") {
+ result = await processTopFileImport(dataRows, updateProgress);
+ } else if (itemType === "hull") {
+ result = await processHullFileImport(dataRows, updateProgress);
+ } else {
+ result = await processFileImport(dataRows, updateProgress);
+ }
+
+ toast.success(`${result.successCount}개의 ${ITEM_TYPE_NAMES[itemType]}이(가) 성공적으로 가져와졌습니다.`);
+
+ if (result.errorCount > 0) {
+ const errorDetails = result.errors?.map((error: { row: number; message: string; itemCode?: string; workType?: string }) =>
+ `행 ${error.row}: ${error.itemCode || '알 수 없음'} (${error.workType || '알 수 없음'}) - ${error.message}`
+ ).join('\n') || '오류 정보를 가져올 수 없습니다.';
+
+ console.error('Import 오류 상세:', errorDetails);
+ toast.error(`${result.errorCount}개의 항목 처리 실패. 콘솔에서 상세 내용을 확인하세요.`);
+ }
+
+ // 상태 초기화 및 다이얼로그 닫기
+ setFile(null);
+ setOpen(false);
+
+ // 성공 콜백 호출
+ if (onSuccess) {
+ onSuccess();
+ }
+ } catch (error) {
+ console.error("Excel 파일 처리 중 오류 발생:", error);
+ setError(error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다.");
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+
+
+ // 다이얼로그 열기/닫기 핸들러
+ const handleOpenChange = (newOpen: boolean) => {
+ if (!newOpen) {
+ // 닫을 때 상태 초기화
+ setFile(null);
+ setError(null);
+ setProgress(0);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ }
+ setOpen(newOpen);
+ };
+
+ return (
+ <>
+ <Button
+ variant="outline"
+ size="sm"
+ className="gap-2"
+ onClick={() => setOpen(true)}
+ disabled={isUploading}
+ >
+ <Upload className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Import</span>
+ </Button>
+
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <DialogHeader>
+ <DialogTitle>{ITEM_TYPE_NAMES[itemType]} 가져오기</DialogTitle>
+ <DialogDescription>
+ {ITEM_TYPE_NAMES[itemType]}을 Excel 파일에서 가져옵니다.
+ <br />
+ 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4 py-4">
+ <div className="flex items-center gap-4">
+ <input
+ type="file"
+ ref={fileInputRef}
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-foreground file:font-medium"
+ accept=".xlsx,.xls"
+ onChange={handleFileChange}
+ disabled={isUploading}
+ />
+ </div>
+
+ {file && (
+ <div className="text-sm text-muted-foreground">
+ 선택된 파일: <span className="font-medium">{file.name}</span> ({(file.size / 1024).toFixed(1)} KB)
+ </div>
+ )}
+
+ {isUploading && (
+ <div className="space-y-2">
+ <Progress value={progress} />
+ <p className="text-sm text-muted-foreground text-center">
+ {progress}% 완료
+ </p>
+ </div>
+ )}
+
+ {error && (
+ <div className="text-sm font-medium text-destructive">
+ {error}
+ </div>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => setOpen(false)}
+ disabled={isUploading}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleImport}
+ disabled={!file || isUploading}
+ >
+ {isUploading ? "처리 중..." : "가져오기"}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </>
+ );
} \ No newline at end of file
diff --git a/lib/items-tech/table/ship/import-item-handler.tsx b/lib/items-tech/table/ship/import-item-handler.tsx
index 57546cc6..b0f475ff 100644
--- a/lib/items-tech/table/ship/import-item-handler.tsx
+++ b/lib/items-tech/table/ship/import-item-handler.tsx
@@ -1,139 +1,129 @@
-"use client"
-
-import { z } from "zod"
-import { createShipbuildingImportItem } from "../../service" // 아이템 생성 서버 액션
-
-// 아이템 데이터 검증을 위한 Zod 스키마
-const itemSchema = z.object({
- itemCode: z.string().optional(),
- workType: z.enum(["기장", "전장", "선실", "배관", "철의", "선체"], {
- required_error: "기능(공종)은 필수입니다",
- }),
- shipTypes: z.string().nullable().optional(),
- itemList: z.string().nullable().optional(),
-});
-
-interface ProcessResult {
- successCount: number;
- errorCount: number;
- errors: Array<{ row: number; message: string; itemCode?: string; workType?: string }>;
-}
-
-/**
- * Excel 파일에서 가져온 조선 아이템 데이터 처리하는 함수
- */
-export async function processFileImport(
- jsonData: Record<string, unknown>[],
- progressCallback?: (current: number, total: number) => void
-): Promise<ProcessResult> {
- // 결과 카운터 초기화
- let successCount = 0;
- let errorCount = 0;
- const errors: Array<{ row: number; message: string }> = [];
-
- // 빈 행 등 필터링
- const dataRows = jsonData.filter(row => {
- // 빈 행 건너뛰기
- if (Object.values(row).every(val => !val)) {
- return false;
- }
- return true;
- });
-
- // 데이터 행이 없으면 빈 결과 반환
- if (dataRows.length === 0) {
- return { successCount: 0, errorCount: 0, errors: [] };
- }
-
- // 각 행에 대해 처리
- for (let i = 0; i < dataRows.length; i++) {
- const row = dataRows[i];
- const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작
-
- // 진행 상황 콜백 호출
- if (progressCallback) {
- progressCallback(i + 1, dataRows.length);
- }
-
- try {
- // 필드 매핑 (한글/영문 필드명 모두 지원)
- const itemCode = row["자재 그룹"] || row["itemCode"] || "";
- const workType = row["기능(공종)"] || row["workType"] || "";
- const shipTypes = row["선종"] || row["shipTypes"] || null;
- const itemList = row["자재명"] || row["itemList"] || null;
-
- // 데이터 정제
- const cleanedRow = {
- itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
- workType: typeof workType === 'string' ? workType.trim() : String(workType).trim(),
- shipTypes: shipTypes ? (typeof shipTypes === 'string' ? shipTypes.trim() : String(shipTypes).trim()) : null,
- itemList: itemList ? (typeof itemList === 'string' ? itemList : String(itemList)) : null,
- };
-
- // 데이터 유효성 검사
- const validationResult = itemSchema.safeParse(cleanedRow);
-
- if (!validationResult.success) {
- const errorMessage = validationResult.error.errors.map(
- err => `${err.path.join('.')}: ${err.message}`
- ).join(', ');
-
- errors.push({
- row: rowIndex,
- message: errorMessage,
- itemCode: cleanedRow.itemCode,
- workType: cleanedRow.workType
- });
- errorCount++;
- continue;
- }
-
- // 아이템 생성
- const result = await createShipbuildingImportItem({
- itemCode: cleanedRow.itemCode,
- workType: cleanedRow.workType as "기장" | "전장" | "선실" | "배관" | "철의" | "선체",
- shipTypes: cleanedRow.shipTypes,
- itemList: cleanedRow.itemList,
- });
-
- if (result.success || !result.error) {
- successCount++;
- } else {
- errors.push({
- row: rowIndex,
- message: result.message || result.error || "알 수 없는 오류",
- itemCode: cleanedRow.itemCode,
- workType: cleanedRow.workType
- });
- errorCount++;
- }
-
- } catch (error) {
- console.error(`${rowIndex}행 처리 오류:`, error);
-
- // cleanedRow가 정의되지 않은 경우를 처리
- const itemCode = row["자재 그룹"] || row["itemCode"] || "";
- const workType = row["기능(공종)"] || row["workType"] || "";
-
- errors.push({
- row: rowIndex,
- message: error instanceof Error ? error.message : "알 수 없는 오류",
- itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
- workType: typeof workType === 'string' ? workType.trim() : String(workType).trim()
- });
- errorCount++;
- }
-
- // 비동기 작업 쓰로틀링
- if (i % 5 === 0) {
- await new Promise(resolve => setTimeout(resolve, 10));
- }
- }
-
- // 처리 결과 반환
- return {
- successCount,
- errorCount,
- errors
- };
+"use client"
+
+import { z } from "zod"
+import { createShipbuildingImportItem } from "../../service" // 아이템 생성 서버 액션
+
+// 아이템 데이터 검증을 위한 Zod 스키마
+const itemSchema = z.object({
+ itemCode: z.string().optional(),
+ workType: z.enum(["기장", "전장", "선실", "배관", "철의", "선체"], {
+ required_error: "기능(공종)은 필수입니다",
+ }),
+ shipTypes: z.string().nullable().optional(),
+ itemList: z.string().nullable().optional(),
+});
+
+interface ProcessResult {
+ successCount: number;
+ errorCount: number;
+ errors: Array<{ row: number; message: string }>;
+}
+
+/**
+ * Excel 파일에서 가져온 조선 아이템 데이터 처리하는 함수
+ */
+export async function processFileImport(
+ jsonData: Record<string, unknown>[],
+ progressCallback?: (current: number, total: number) => void
+): Promise<ProcessResult> {
+ // 결과 카운터 초기화
+ let successCount = 0;
+ let errorCount = 0;
+ const errors: Array<{ row: number; message: string }> = [];
+
+ // 빈 행 등 필터링
+ const dataRows = jsonData.filter(row => {
+ // 빈 행 건너뛰기
+ if (Object.values(row).every(val => !val)) {
+ return false;
+ }
+ return true;
+ });
+
+ // 데이터 행이 없으면 빈 결과 반환
+ if (dataRows.length === 0) {
+ return { successCount: 0, errorCount: 0, errors: [] };
+ }
+
+ // 각 행에 대해 처리
+ for (let i = 0; i < dataRows.length; i++) {
+ const row = dataRows[i];
+ const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작
+
+ // 진행 상황 콜백 호출
+ if (progressCallback) {
+ progressCallback(i + 1, dataRows.length);
+ }
+
+ try {
+ // 필드 매핑 (한글/영문 필드명 모두 지원)
+ const itemCode = row["자재 그룹"] || row["itemCode"] || "";
+ const workType = row["기능(공종)"] || row["workType"] || "";
+ const shipTypes = row["선종"] || row["shipTypes"] || null;
+ const itemList = row["자재명"] || row["itemList"] || null;
+
+ // 데이터 정제
+ const cleanedRow = {
+ itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
+ workType: typeof workType === 'string' ? workType.trim() : String(workType).trim(),
+ shipTypes: shipTypes ? (typeof shipTypes === 'string' ? shipTypes.trim() : String(shipTypes).trim()) : null,
+ itemList: itemList ? (typeof itemList === 'string' ? itemList : String(itemList)) : null,
+ };
+
+ // 데이터 유효성 검사
+ const validationResult = itemSchema.safeParse(cleanedRow);
+
+ if (!validationResult.success) {
+ const errorMessage = validationResult.error.errors.map(
+ err => `${err.path.join('.')}: ${err.message}`
+ ).join(', ');
+
+ errors.push({
+ row: rowIndex,
+ message: errorMessage,
+ });
+ errorCount++;
+ continue;
+ }
+
+ // 아이템 생성
+ const result = await createShipbuildingImportItem({
+ itemCode: cleanedRow.itemCode,
+ workType: cleanedRow.workType as "기장" | "전장" | "선실" | "배관" | "철의" | "선체",
+ shipTypes: cleanedRow.shipTypes,
+ itemList: cleanedRow.itemList,
+ });
+
+ if (result.success || !result.error) {
+ successCount++;
+ } else {
+ errors.push({
+ row: rowIndex,
+ message: result.message || result.error || "알 수 없는 오류",
+ });
+ errorCount++;
+ }
+
+ } catch (error) {
+ console.error(`${rowIndex}행 처리 오류:`, error);
+
+ errors.push({
+ row: rowIndex,
+ message: error instanceof Error ? error.message : "알 수 없는 오류",
+ });
+ errorCount++;
+ }
+
+ // 비동기 작업 쓰로틀링
+ if (i % 5 === 0) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+ }
+
+ // 처리 결과 반환
+ return {
+ successCount,
+ errorCount,
+ errors
+ };
} \ No newline at end of file
diff --git a/lib/items-tech/table/ship/item-excel-template.tsx b/lib/items-tech/table/ship/item-excel-template.tsx
index 401fb911..fdff0de0 100644
--- a/lib/items-tech/table/ship/item-excel-template.tsx
+++ b/lib/items-tech/table/ship/item-excel-template.tsx
@@ -1,111 +1,111 @@
-import * as ExcelJS from 'exceljs';
-import { saveAs } from "file-saver";
-
-/**
- * 조선 아이템 데이터 가져오기를 위한 Excel 템플릿 파일 생성 및 다운로드
- */
-export async function exportItemTemplate() {
- // 워크북 생성
- const workbook = new ExcelJS.Workbook();
- workbook.creator = 'Shipbuilding Item Management System';
- workbook.created = new Date();
-
- // 워크시트 생성
- const worksheet = workbook.addWorksheet('조선 아이템');
-
- // 컬럼 헤더 정의 및 스타일 적용
- worksheet.columns = [
- { header: '자재 그룹', key: 'itemCode', width: 15 },
- { header: '기능(공종)', key: 'workType', width: 15 },
- { header: '선종', key: 'shipTypes', width: 15 },
- { header: '자재명', key: 'itemList', width: 30 },
- ];
-
- // 헤더 스타일 적용
- const headerRow = worksheet.getRow(1);
- headerRow.font = { bold: true };
- headerRow.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFE0E0E0' }
- };
- headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
-
- // 테두리 스타일 적용
- headerRow.eachCell((cell) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
-
- // 샘플 데이터 추가
- const sampleData = [
- {
- itemCode: 'BG0001',
- workType: '기장',
- shipTypes: 'A-MAX',
- itemList: '자재명',
- },
- {
- itemCode: 'BG0002',
- workType: '전장',
- shipTypes: 'LNGC',
- itemList: '자재명',
- },
- {
- itemCode: 'BG0003',
- workType: '선실',
- shipTypes: 'VLCC',
- itemList: '자재명',
- }
- ];
-
- // 데이터 행 추가
- sampleData.forEach(item => {
- worksheet.addRow(item);
- });
-
- // 데이터 행 스타일 적용
- worksheet.eachRow((row, rowNumber) => {
- if (rowNumber > 1) { // 헤더를 제외한 데이터 행
- row.eachCell((cell) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
- }
- });
-
- // 워크시트 보호 (선택적)
- worksheet.protect('', {
- selectLockedCells: true,
- selectUnlockedCells: true,
- formatColumns: true,
- formatRows: true,
- insertColumns: false,
- insertRows: true,
- insertHyperlinks: false,
- deleteColumns: false,
- deleteRows: true,
- sort: true,
- autoFilter: true,
- pivotTables: false
- });
-
- try {
- // 워크북을 Blob으로 변환
- const buffer = await workbook.xlsx.writeBuffer();
- const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
- saveAs(blob, 'shipbuilding-item-template.xlsx');
- return true;
- } catch (error) {
- console.error('Excel 템플릿 생성 오류:', error);
- throw error;
- }
+import * as ExcelJS from 'exceljs';
+import { saveAs } from "file-saver";
+
+/**
+ * 조선 아이템 데이터 가져오기를 위한 Excel 템플릿 파일 생성 및 다운로드
+ */
+export async function exportItemTemplate() {
+ // 워크북 생성
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = 'Shipbuilding Item Management System';
+ workbook.created = new Date();
+
+ // 워크시트 생성
+ const worksheet = workbook.addWorksheet('조선 아이템');
+
+ // 컬럼 헤더 정의 및 스타일 적용
+ worksheet.columns = [
+ { header: '자재 그룹', key: 'itemCode', width: 15 },
+ { header: '기능(공종)', key: 'workType', width: 15 },
+ { header: '선종', key: 'shipTypes', width: 15 },
+ { header: '자재명', key: 'itemList', width: 30 },
+ ];
+
+ // 헤더 스타일 적용
+ const headerRow = worksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ };
+ headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
+
+ // 테두리 스타일 적용
+ headerRow.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+
+ // 샘플 데이터 추가
+ const sampleData = [
+ {
+ itemCode: 'BG0001',
+ workType: '기장',
+ shipTypes: 'A-MAX',
+ itemList: '자재명',
+ },
+ {
+ itemCode: 'BG0002',
+ workType: '전장',
+ shipTypes: 'LNGC',
+ itemList: '자재명',
+ },
+ {
+ itemCode: 'BG0003',
+ workType: '선실',
+ shipTypes: 'VLCC',
+ itemList: '자재명',
+ }
+ ];
+
+ // 데이터 행 추가
+ sampleData.forEach(item => {
+ worksheet.addRow(item);
+ });
+
+ // 데이터 행 스타일 적용
+ worksheet.eachRow((row, rowNumber) => {
+ if (rowNumber > 1) { // 헤더를 제외한 데이터 행
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+ }
+ });
+
+ // 워크시트 보호 (선택적)
+ worksheet.protect('', {
+ selectLockedCells: true,
+ selectUnlockedCells: true,
+ formatColumns: true,
+ formatRows: true,
+ insertColumns: false,
+ insertRows: true,
+ insertHyperlinks: false,
+ deleteColumns: false,
+ deleteRows: true,
+ sort: true,
+ autoFilter: true,
+ pivotTables: false
+ });
+
+ try {
+ // 워크북을 Blob으로 변환
+ const buffer = await workbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+ saveAs(blob, 'shipbuilding-item-template.xlsx');
+ return true;
+ } catch (error) {
+ console.error('Excel 템플릿 생성 오류:', error);
+ throw error;
+ }
} \ No newline at end of file
diff --git a/lib/items-tech/table/ship/items-table-toolbar-actions.tsx b/lib/items-tech/table/ship/items-table-toolbar-actions.tsx
index 29995327..82ceb298 100644
--- a/lib/items-tech/table/ship/items-table-toolbar-actions.tsx
+++ b/lib/items-tech/table/ship/items-table-toolbar-actions.tsx
@@ -1,177 +1,177 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, FileDown } from "lucide-react"
-import * as ExcelJS from 'exceljs'
-import { saveAs } from "file-saver"
-
-import { Button } from "@/components/ui/button"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-
-import { DeleteItemsDialog } from "../delete-items-dialog"
-import { AddItemDialog } from "../add-items-dialog"
-import { exportItemTemplate } from "./item-excel-template"
-import { ImportItemButton } from "../import-excel-button"
-
-// 조선 아이템 타입 정의
-interface ShipbuildingItem {
- id: number;
- itemId: number;
- workType: "기장" | "전장" | "선실" | "배관" | "철의" | "선체";
- shipTypes: string;
- itemCode: string;
- itemName: string;
- itemList: string | null;
- description: string | null;
- createdAt: Date;
- updatedAt: Date;
-}
-
-interface ItemsTableToolbarActionsProps {
- table: Table<ShipbuildingItem>
-}
-
-export function ItemsTableToolbarActions({ table }: ItemsTableToolbarActionsProps) {
- const [refreshKey, setRefreshKey] = React.useState(0)
-
- // 가져오기 성공 후 테이블 갱신
- const handleImportSuccess = () => {
- setRefreshKey(prev => prev + 1)
- }
-
- // Excel 내보내기 함수
- const exportTableToExcel = async (
- table: Table<ShipbuildingItem>,
- options: {
- filename: string;
- excludeColumns?: string[];
- sheetName?: string;
- }
- ) => {
- const { filename, excludeColumns = [], sheetName = "조선 아이템 목록" } = options;
-
- // 워크북 생성
- const workbook = new ExcelJS.Workbook();
- workbook.creator = 'Shipbuilding Item Management System';
- workbook.created = new Date();
-
- // 워크시트 생성
- const worksheet = workbook.addWorksheet(sheetName);
-
- // 테이블 데이터 가져오기
- const data = table.getFilteredRowModel().rows.map(row => row.original);
- console.log("내보내기 데이터:", data);
-
- // 필요한 헤더 직접 정의 (필터링 문제 해결)
- const headers = [
- { key: 'itemCode', header: '자재 그룹' },
- { key: 'workType', header: '기능(공종)' },
- { key: 'shipTypes', header: '선종' },
- { key: 'itemList', header: '자재명' },
- { key: 'subItemList', header: '자재명(상세)' },
- ].filter(header => !excludeColumns.includes(header.key));
-
- console.log("내보내기 헤더:", headers);
- // 컬럼 정의
- worksheet.columns = headers.map(header => ({
- header: header.header,
- key: header.key,
- width: 20 // 기본 너비
- }));
-
- // 스타일 적용
- const headerRow = worksheet.getRow(1);
- headerRow.font = { bold: true };
- headerRow.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFE0E0E0' }
- };
- headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
-
- // 데이터 행 추가
- data.forEach(item => {
- const row: Record<string, any> = {};
- headers.forEach(header => {
- row[header.key] = item[header.key as keyof ShipbuildingItem];
- });
- worksheet.addRow(row);
- });
-
- // 전체 셀에 테두리 추가
- worksheet.eachRow((row) => {
- row.eachCell((cell) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
- });
-
- try {
- // 워크북을 Blob으로 변환
- const buffer = await workbook.xlsx.writeBuffer();
- const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
- saveAs(blob, `${filename}.xlsx`);
- return true;
- } catch (error) {
- console.error("Excel 내보내기 오류:", error);
- return false;
- }
- }
-
- return (
- <div className="flex items-center gap-2">
- {/* 선택된 로우가 있으면 삭제 다이얼로그 */}
- {table.getFilteredSelectedRowModel().rows.length > 0 ? (
- <DeleteItemsDialog
- items={table.getFilteredSelectedRowModel().rows.map((row) => row.original) as any}
- onSuccess={() => table.toggleAllRowsSelected(false)}
- itemType="shipbuilding"
- />
- ) : null}
-
- {/* 새 아이템 추가 다이얼로그 */}
- <AddItemDialog itemType="shipbuilding" />
-
- {/* Import 버튼 */}
- <ImportItemButton itemType="ship" onSuccess={handleImportSuccess} />
-
- {/* Export 드롭다운 메뉴 */}
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="outline" size="sm" className="gap-2">
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem
- onClick={() =>
- exportTableToExcel(table, {
- filename: "shipbuilding_items",
- excludeColumns: ["select", "actions"],
- sheetName: "조선 아이템 목록"
- })
- }
- >
- <FileDown className="mr-2 h-4 w-4" />
- <span>현재 데이터 내보내기</span>
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => exportItemTemplate()}>
- <FileDown className="mr-2 h-4 w-4" />
- <span>템플릿 다운로드</span>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- )
+"use client"
+
+import * as React from "react"
+import { type Table } from "@tanstack/react-table"
+import { Download, FileDown } from "lucide-react"
+import * as ExcelJS from 'exceljs'
+import { saveAs } from "file-saver"
+
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+import { DeleteItemsDialog } from "../delete-items-dialog"
+import { AddItemDialog } from "../add-items-dialog"
+import { exportItemTemplate } from "./item-excel-template"
+import { ImportItemButton } from "../import-excel-button"
+
+// 조선 아이템 타입 정의
+interface ShipbuildingItem {
+ id: number;
+ itemId: number;
+ workType: "기장" | "전장" | "선실" | "배관" | "철의" | "선체";
+ shipTypes: string;
+ itemCode: string;
+ itemName: string;
+ itemList: string | null;
+ description: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+interface ItemsTableToolbarActionsProps {
+ table: Table<ShipbuildingItem>
+}
+
+export function ItemsTableToolbarActions({ table }: ItemsTableToolbarActionsProps) {
+ const [refreshKey, setRefreshKey] = React.useState(0)
+
+ // 가져오기 성공 후 테이블 갱신
+ const handleImportSuccess = () => {
+ setRefreshKey(prev => prev + 1)
+ }
+
+ // Excel 내보내기 함수
+ const exportTableToExcel = async (
+ table: Table<ShipbuildingItem>,
+ options: {
+ filename: string;
+ excludeColumns?: string[];
+ sheetName?: string;
+ }
+ ) => {
+ const { filename, excludeColumns = [], sheetName = "조선 아이템 목록" } = options;
+
+ // 워크북 생성
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = 'Shipbuilding Item Management System';
+ workbook.created = new Date();
+
+ // 워크시트 생성
+ const worksheet = workbook.addWorksheet(sheetName);
+
+ // 테이블 데이터 가져오기
+ const data = table.getFilteredRowModel().rows.map(row => row.original);
+ console.log("내보내기 데이터:", data);
+
+ // 필요한 헤더 직접 정의 (필터링 문제 해결)
+ const headers = [
+ { key: 'itemCode', header: '자재 그룹' },
+ { key: 'workType', header: '기능(공종)' },
+ { key: 'shipTypes', header: '선종' },
+ { key: 'itemList', header: '자재명' },
+ { key: 'subItemList', header: '자재명(상세)' },
+ ].filter(header => !excludeColumns.includes(header.key));
+
+ console.log("내보내기 헤더:", headers);
+ // 컬럼 정의
+ worksheet.columns = headers.map(header => ({
+ header: header.header,
+ key: header.key,
+ width: 20 // 기본 너비
+ }));
+
+ // 스타일 적용
+ const headerRow = worksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ };
+ headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
+
+ // 데이터 행 추가
+ data.forEach(item => {
+ const row: Record<string, any> = {};
+ headers.forEach(header => {
+ row[header.key] = item[header.key as keyof ShipbuildingItem];
+ });
+ worksheet.addRow(row);
+ });
+
+ // 전체 셀에 테두리 추가
+ worksheet.eachRow((row) => {
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+ });
+
+ try {
+ // 워크북을 Blob으로 변환
+ const buffer = await workbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+ saveAs(blob, `${filename}.xlsx`);
+ return true;
+ } catch (error) {
+ console.error("Excel 내보내기 오류:", error);
+ return false;
+ }
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ {/* 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <DeleteItemsDialog
+ items={table.getFilteredSelectedRowModel().rows.map((row) => row.original) as any}
+ onSuccess={() => table.toggleAllRowsSelected(false)}
+ itemType="shipbuilding"
+ />
+ ) : null}
+
+ {/* 새 아이템 추가 다이얼로그 */}
+ <AddItemDialog itemType="shipbuilding" />
+
+ {/* Import 버튼 */}
+ <ImportItemButton itemType="ship" onSuccess={handleImportSuccess} />
+
+ {/* Export 드롭다운 메뉴 */}
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="outline" size="sm" className="gap-2">
+ <Download className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Export</span>
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ <DropdownMenuItem
+ onClick={() =>
+ exportTableToExcel(table, {
+ filename: "shipbuilding_items",
+ excludeColumns: ["select", "actions"],
+ sheetName: "조선 아이템 목록"
+ })
+ }
+ >
+ <FileDown className="mr-2 h-4 w-4" />
+ <span>현재 데이터 내보내기</span>
+ </DropdownMenuItem>
+ <DropdownMenuItem onClick={() => exportItemTemplate()}>
+ <FileDown className="mr-2 h-4 w-4" />
+ <span>템플릿 다운로드</span>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+ )
} \ No newline at end of file
diff --git a/lib/items-tech/table/top/import-item-handler.tsx b/lib/items-tech/table/top/import-item-handler.tsx
index 0a163791..4f34cff2 100644
--- a/lib/items-tech/table/top/import-item-handler.tsx
+++ b/lib/items-tech/table/top/import-item-handler.tsx
@@ -1,141 +1,130 @@
-"use client"
-
-import { z } from "zod"
-import { createOffshoreTopItem } from "../../service"
-
-// 해양 TOP 기능(공종) 유형 enum
-const TOP_WORK_TYPES = ["TM", "TS", "TE", "TP"] as const;
-
-// 아이템 데이터 검증을 위한 Zod 스키마
-const itemSchema = z.object({
- itemCode: z.string().optional(),
- workType: z.enum(TOP_WORK_TYPES, {
- required_error: "기능(공종)은 필수입니다",
- }),
- itemList: z.string().nullable().optional(),
- subItemList: z.string().nullable().optional(),
-});
-
-interface ProcessResult {
- successCount: number;
- errorCount: number;
- errors: Array<{ row: number; message: string; itemCode?: string; workType?: string }>;
-}
-
-/**
- * Excel 파일에서 가져온 해양 TOP 아이템 데이터 처리하는 함수
- */
-export async function processTopFileImport(
- jsonData: Record<string, unknown>[],
- progressCallback?: (current: number, total: number) => void
-): Promise<ProcessResult> {
- // 결과 카운터 초기화
- let successCount = 0;
- let errorCount = 0;
- const errors: Array<{ row: number; message: string }> = [];
-
- // 빈 행 등 필터링
- const dataRows = jsonData.filter(row => {
- // 빈 행 건너뛰기
- if (Object.values(row).every(val => !val)) {
- return false;
- }
- return true;
- });
-
- // 데이터 행이 없으면 빈 결과 반환
- if (dataRows.length === 0) {
- return { successCount: 0, errorCount: 0, errors: [] };
- }
-
- // 각 행에 대해 처리
- for (let i = 0; i < dataRows.length; i++) {
- const row = dataRows[i];
- const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작
-
- // 진행 상황 콜백 호출
- if (progressCallback) {
- progressCallback(i + 1, dataRows.length);
- }
-
- try {
- // 필드 매핑 (한글/영문 필드명 모두 지원)
- const itemCode = row["자재 그룹"] || row["itemCode"] || "";
- const workType = row["기능(공종)"] || row["workType"] || "";
- const itemList = row["자재명"] || row["itemList"] || null;
- const subItemList = row["자재명(상세)"] || row["subItemList"] || null;
-
- // 데이터 정제
- const cleanedRow = {
- itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
- workType: typeof workType === 'string' ? workType.trim() : String(workType).trim(),
- itemList: itemList ? (typeof itemList === 'string' ? itemList : String(itemList)) : null,
- subItemList: subItemList ? (typeof subItemList === 'string' ? subItemList : String(subItemList)) : null,
- };
-
- // 데이터 유효성 검사
- const validationResult = itemSchema.safeParse(cleanedRow);
-
- if (!validationResult.success) {
- const errorMessage = validationResult.error.errors.map(
- err => `${err.path.join('.')}: ${err.message}`
- ).join(', ');
-
- errors.push({
- row: rowIndex,
- message: errorMessage,
- itemCode: cleanedRow.itemCode,
- workType: cleanedRow.workType
- });
- errorCount++;
- continue;
- }
-
- // 해양 TOP 아이템 생성
- const result = await createOffshoreTopItem({
- itemCode: cleanedRow.itemCode,
- workType: cleanedRow.workType as "TM" | "TS" | "TE" | "TP",
- itemList: cleanedRow.itemList,
- subItemList: cleanedRow.subItemList,
- });
-
- if (result.success) {
- successCount++;
- } else {
- errors.push({
- row: rowIndex,
- message: result.message || result.error || "알 수 없는 오류",
- itemCode: cleanedRow.itemCode,
- workType: cleanedRow.workType
- });
- errorCount++;
- }
- } catch (error) {
- console.error(`${rowIndex}행 처리 오류:`, error);
-
- // cleanedRow가 정의되지 않은 경우를 처리
- const itemCode = row["자재 그룹"] || row["itemCode"] || "";
- const workType = row["기능(공종)"] || row["workType"] || "";
-
- errors.push({
- row: rowIndex,
- message: error instanceof Error ? error.message : "알 수 없는 오류",
- itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
- workType: typeof workType === 'string' ? workType.trim() : String(workType).trim()
- });
- errorCount++;
- }
-
- // 비동기 작업 쓰로틀링
- if (i % 5 === 0) {
- await new Promise(resolve => setTimeout(resolve, 10));
- }
- }
-
- // 처리 결과 반환
- return {
- successCount,
- errorCount,
- errors
- };
-}
+"use client"
+
+import { z } from "zod"
+import { createOffshoreTopItem } from "../../service"
+
+// 해양 TOP 기능(공종) 유형 enum
+const TOP_WORK_TYPES = ["TM", "TS", "TE", "TP"] as const;
+
+// 아이템 데이터 검증을 위한 Zod 스키마
+const itemSchema = z.object({
+ itemCode: z.string().optional(),
+ workType: z.enum(TOP_WORK_TYPES, {
+ required_error: "기능(공종)은 필수입니다",
+ }),
+ itemList: z.string().nullable().optional(),
+ subItemList: z.string().nullable().optional(),
+});
+
+interface ProcessResult {
+ successCount: number;
+ errorCount: number;
+ errors: Array<{ row: number; message: string }>;
+}
+
+/**
+ * Excel 파일에서 가져온 해양 TOP 아이템 데이터 처리하는 함수
+ */
+export async function processTopFileImport(
+ jsonData: Record<string, unknown>[],
+ progressCallback?: (current: number, total: number) => void
+): Promise<ProcessResult> {
+ // 결과 카운터 초기화
+ let successCount = 0;
+ let errorCount = 0;
+ const errors: Array<{ row: number; message: string }> = [];
+
+ // 빈 행 등 필터링
+ const dataRows = jsonData.filter(row => {
+ // 빈 행 건너뛰기
+ if (Object.values(row).every(val => !val)) {
+ return false;
+ }
+ return true;
+ });
+
+ // 데이터 행이 없으면 빈 결과 반환
+ if (dataRows.length === 0) {
+ return { successCount: 0, errorCount: 0, errors: [] };
+ }
+
+ // 각 행에 대해 처리
+ for (let i = 0; i < dataRows.length; i++) {
+ const row = dataRows[i];
+ const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작
+
+ // 진행 상황 콜백 호출
+ if (progressCallback) {
+ progressCallback(i + 1, dataRows.length);
+ }
+
+ try {
+ // 필드 매핑 (한글/영문 필드명 모두 지원)
+ const itemCode = row["자재 그룹"] || row["itemCode"] || "";
+ const workType = row["기능(공종)"] || row["workType"] || "";
+ const itemList = row["자재명"] || row["itemList"] || null;
+ const subItemList = row["자재명(상세)"] || row["subItemList"] || null;
+
+ // 데이터 정제
+ const cleanedRow = {
+ itemCode: typeof itemCode === 'string' ? itemCode.trim() : String(itemCode).trim(),
+ workType: typeof workType === 'string' ? workType.trim() : String(workType).trim(),
+ itemList: itemList ? (typeof itemList === 'string' ? itemList : String(itemList)) : null,
+ subItemList: subItemList ? (typeof subItemList === 'string' ? subItemList : String(subItemList)) : null,
+ };
+
+ // 데이터 유효성 검사
+ const validationResult = itemSchema.safeParse(cleanedRow);
+
+ if (!validationResult.success) {
+ const errorMessage = validationResult.error.errors.map(
+ err => `${err.path.join('.')}: ${err.message}`
+ ).join(', ');
+
+ errors.push({
+ row: rowIndex,
+ message: errorMessage,
+ });
+ errorCount++;
+ continue;
+ }
+
+ // 해양 TOP 아이템 생성
+ const result = await createOffshoreTopItem({
+ itemCode: cleanedRow.itemCode,
+ workType: cleanedRow.workType as "TM" | "TS" | "TE" | "TP",
+ itemList: cleanedRow.itemList,
+ subItemList: cleanedRow.subItemList,
+ });
+
+ if (result.success) {
+ successCount++;
+ } else {
+ errors.push({
+ row: rowIndex,
+ message: result.message || result.error || "알 수 없는 오류",
+ });
+ errorCount++;
+ }
+ } catch (error) {
+ console.error(`${rowIndex}행 처리 오류:`, error);
+ errors.push({
+ row: rowIndex,
+ message: error instanceof Error ? error.message : "알 수 없는 오류",
+ });
+ errorCount++;
+ }
+
+ // 비동기 작업 쓰로틀링
+ if (i % 5 === 0) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+ }
+
+ // 처리 결과 반환
+ return {
+ successCount,
+ errorCount,
+ errors
+ };
+}
diff --git a/lib/items-tech/table/top/item-excel-template.tsx b/lib/items-tech/table/top/item-excel-template.tsx
index b67d91be..9121d70f 100644
--- a/lib/items-tech/table/top/item-excel-template.tsx
+++ b/lib/items-tech/table/top/item-excel-template.tsx
@@ -1,109 +1,109 @@
-import * as ExcelJS from 'exceljs';
-import { saveAs } from "file-saver";
-
-
-/**
- * 해양 TOP 아이템 데이터 가져오기를 위한 Excel 템플릿 파일 생성 및 다운로드
- */
-export async function exportTopItemTemplate() {
- // 워크북 생성
- const workbook = new ExcelJS.Workbook();
- workbook.creator = 'Offshore TOP Item Management System';
- workbook.created = new Date();
-
- // 워크시트 생성
- const worksheet = workbook.addWorksheet('해양 TOP 아이템');
-
- // 컬럼 헤더 정의 및 스타일 적용
- worksheet.columns = [
- { header: '자재 그룹', key: 'itemCode', width: 15 },
- { header: '기능(공종)', key: 'workType', width: 15 },
- { header: '자재명', key: 'itemList', width: 20 },
- { header: '자재명(상세)', key: 'subItemList', width: 20 },
-
- ];
-
- // 헤더 스타일 적용
- const headerRow = worksheet.getRow(1);
- headerRow.font = { bold: true };
- headerRow.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFE0E0E0' }
- };
- headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
-
- // 테두리 스타일 적용
- headerRow.eachCell((cell) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
-
- // 샘플 데이터 추가
- const sampleData = [
- {
- itemCode: 'TOP001',
- workType: 'TM',
- itemList: '항목1 샘플 데이터',
- subItemList: '항목2 샘플 데이터',
- },
- {
- itemCode: 'TOP002',
- workType: 'TS',
- itemList: '항목1 샘플 데이터',
- subItemList: '항목2 샘플 데이터',
- }
-
- ];
-
- // 데이터 행 추가
- sampleData.forEach(item => {
- worksheet.addRow(item);
- });
-
- // 데이터 행 스타일 적용
- worksheet.eachRow((row, rowNumber) => {
- if (rowNumber > 1) { // 헤더를 제외한 데이터 행
- row.eachCell((cell) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
- }
- });
-
-
- // 워크시트 보호 (선택적)
- worksheet.protect('', {
- selectLockedCells: true,
- selectUnlockedCells: true,
- formatColumns: true,
- formatRows: true,
- insertColumns: false,
- insertRows: true,
- insertHyperlinks: false,
- deleteColumns: false,
- deleteRows: true,
- sort: true,
- autoFilter: true,
- pivotTables: false
- });
-
- try {
- // 워크북을 Blob으로 변환
- const buffer = await workbook.xlsx.writeBuffer();
- const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
- saveAs(blob, 'offshore-top-item-template.xlsx');
- return true;
- } catch (error) {
- console.error('Excel 템플릿 생성 오류:', error);
- throw error;
- }
-}
+import * as ExcelJS from 'exceljs';
+import { saveAs } from "file-saver";
+
+
+/**
+ * 해양 TOP 아이템 데이터 가져오기를 위한 Excel 템플릿 파일 생성 및 다운로드
+ */
+export async function exportTopItemTemplate() {
+ // 워크북 생성
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = 'Offshore TOP Item Management System';
+ workbook.created = new Date();
+
+ // 워크시트 생성
+ const worksheet = workbook.addWorksheet('해양 TOP 아이템');
+
+ // 컬럼 헤더 정의 및 스타일 적용
+ worksheet.columns = [
+ { header: '자재 그룹', key: 'itemCode', width: 15 },
+ { header: '기능(공종)', key: 'workType', width: 15 },
+ { header: '자재명', key: 'itemList', width: 20 },
+ { header: '자재명(상세)', key: 'subItemList', width: 20 },
+
+ ];
+
+ // 헤더 스타일 적용
+ const headerRow = worksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ };
+ headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
+
+ // 테두리 스타일 적용
+ headerRow.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+
+ // 샘플 데이터 추가
+ const sampleData = [
+ {
+ itemCode: 'TOP001',
+ workType: 'TM',
+ itemList: '항목1 샘플 데이터',
+ subItemList: '항목2 샘플 데이터',
+ },
+ {
+ itemCode: 'TOP002',
+ workType: 'TS',
+ itemList: '항목1 샘플 데이터',
+ subItemList: '항목2 샘플 데이터',
+ }
+
+ ];
+
+ // 데이터 행 추가
+ sampleData.forEach(item => {
+ worksheet.addRow(item);
+ });
+
+ // 데이터 행 스타일 적용
+ worksheet.eachRow((row, rowNumber) => {
+ if (rowNumber > 1) { // 헤더를 제외한 데이터 행
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+ }
+ });
+
+
+ // 워크시트 보호 (선택적)
+ worksheet.protect('', {
+ selectLockedCells: true,
+ selectUnlockedCells: true,
+ formatColumns: true,
+ formatRows: true,
+ insertColumns: false,
+ insertRows: true,
+ insertHyperlinks: false,
+ deleteColumns: false,
+ deleteRows: true,
+ sort: true,
+ autoFilter: true,
+ pivotTables: false
+ });
+
+ try {
+ // 워크북을 Blob으로 변환
+ const buffer = await workbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+ saveAs(blob, 'offshore-top-item-template.xlsx');
+ return true;
+ } catch (error) {
+ console.error('Excel 템플릿 생성 오류:', error);
+ throw error;
+ }
+}