summaryrefslogtreecommitdiff
path: root/components/form-data
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:02:00 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:02:00 +0000
commitcbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (patch)
tree0a26712f7685e4f6511e637b9a81269d90a47c8f /components/form-data
parenteb654f88214095f71be142b989e620fd28db3f69 (diff)
(대표님) 입찰, EDP 변경사항 대응, spreadJS 오류 수정, 벤더실사 수정
Diffstat (limited to 'components/form-data')
-rw-r--r--components/form-data/add-formTag-dialog.tsx58
-rw-r--r--components/form-data/delete-form-data-dialog.tsx67
-rw-r--r--components/form-data/form-data-report-batch-dialog.tsx57
-rw-r--r--components/form-data/form-data-report-dialog.tsx26
-rw-r--r--components/form-data/form-data-report-temp-upload-dialog.tsx116
-rw-r--r--components/form-data/form-data-report-temp-upload-tab.tsx53
-rw-r--r--components/form-data/form-data-report-temp-uploaded-list-tab.tsx37
-rw-r--r--components/form-data/form-data-table-columns.tsx108
-rw-r--r--components/form-data/form-data-table.tsx33
-rw-r--r--components/form-data/sedp-compare-dialog.tsx233
-rw-r--r--components/form-data/sedp-components.tsx50
-rw-r--r--components/form-data/sedp-excel-download.tsx43
-rw-r--r--components/form-data/spreadJS-dialog.tsx736
-rw-r--r--components/form-data/update-form-sheet.tsx55
-rw-r--r--components/form-data/var-list-download-btn.tsx28
15 files changed, 1042 insertions, 658 deletions
diff --git a/components/form-data/add-formTag-dialog.tsx b/components/form-data/add-formTag-dialog.tsx
index 2cd336a0..9d80de8c 100644
--- a/components/form-data/add-formTag-dialog.tsx
+++ b/components/form-data/add-formTag-dialog.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { useRouter } from "next/navigation"
+import { useParams, useRouter } from "next/navigation";
import { useForm, useFieldArray } from "react-hook-form"
import { toast } from "sonner"
import { Loader2, ChevronsUpDown, Check, Plus, Trash2, Copy } from "lucide-react"
@@ -57,6 +57,7 @@ import {
getTagTypeByDescription,
getSubfieldsByTagTypeForForm
} from "@/lib/forms/services"
+import { useTranslation } from "@/i18n/client";
// Form-specific tag mapping interface
interface FormTagMapping {
@@ -107,6 +108,9 @@ export function AddFormTagDialog({
onOpenChange: externalOnOpenChange
}: AddFormTagDialogProps) {
const router = useRouter()
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
// Use external control if provided, otherwise use internal state
const [internalOpen, setInternalOpen] = React.useState(false);
@@ -439,7 +443,7 @@ export function AddFormTagDialog({
// ---------------
// Render Class field
// ---------------
- function renderClassField(field: any) {
+ function renderClassField(field: any) {
const [popoverOpen, setPopoverOpen] = React.useState(false)
const buttonId = React.useMemo(
@@ -460,7 +464,7 @@ export function AddFormTagDialog({
return (
<FormItem className="w-1/2">
- <FormLabel>Class</FormLabel>
+ <FormLabel>{t("labels.class")}</FormLabel>
<FormControl>
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild>
@@ -473,13 +477,13 @@ export function AddFormTagDialog({
>
{isLoadingClasses ? (
<>
- <span>클래스 로딩 중...</span>
+ <span>{t("messages.loadingClasses")}</span>
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
</>
) : (
<>
<span className="truncate mr-1 flex-grow text-left">
- {field.value || "클래스 선택..."}
+ {field.value || t("placeholders.selectClass")}
</span>
<ChevronsUpDown className="h-4 w-4 opacity-50 flex-shrink-0" />
</>
@@ -490,10 +494,10 @@ export function AddFormTagDialog({
<Command key={commandId}>
<CommandInput
key={`${commandId}-input`}
- placeholder="클래스 검색..."
+ placeholder={t("placeholders.searchClass")}
/>
<CommandList key={`${commandId}-list`} className="max-h-[300px]">
- <CommandEmpty key={`${commandId}-empty`}>검색 결과가 없습니다.</CommandEmpty>
+ <CommandEmpty key={`${commandId}-empty`}>{t("messages.noSearchResults")}</CommandEmpty>
<CommandGroup key={`${commandId}-group`}>
{classOptions.map((className, optIndex) => {
if (!classOptionIdsRef.current[className]) {
@@ -553,7 +557,7 @@ export function AddFormTagDialog({
return (
<FormItem className="w-1/2">
- <FormLabel>Tag Type</FormLabel>
+ <FormLabel>{t("labels.tagType")}</FormLabel>
<FormControl>
{isReadOnly ? (
<div className="relative">
@@ -569,7 +573,7 @@ export function AddFormTagDialog({
key={`tag-type-placeholder-${inputId}`}
{...field}
readOnly
- placeholder="클래스 선택시 자동으로 결정됩니다"
+ placeholder={t("placeholders.autoSetByClass")}
className="h-9 bg-muted"
/>
)}
@@ -587,7 +591,7 @@ export function AddFormTagDialog({
return (
<div className="flex justify-center items-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
- <div className="ml-3 text-muted-foreground">필드 로딩 중...</div>
+ <div className="ml-3 text-muted-foreground">{t("messages.loadingFields")}</div>
</div>
)
}
@@ -595,7 +599,7 @@ export function AddFormTagDialog({
if (subFields.length === 0 && selectedTagTypeCode) {
return (
<div className="py-4 text-center text-muted-foreground">
- 이 태그 유형에 대한 필드가 없습니다.
+ {t("messages.noFieldsForTagType")}
</div>
)
}
@@ -603,7 +607,7 @@ export function AddFormTagDialog({
if (subFields.length === 0) {
return (
<div className="py-4 text-center text-muted-foreground">
- 태그 데이터를 입력하려면 먼저 상단에서 클래스를 선택하세요.
+ {t("messages.selectClassFirst")}
</div>
)
}
@@ -612,10 +616,10 @@ export function AddFormTagDialog({
<div className="space-y-4">
{/* 헤더 */}
<div className="flex justify-between items-center">
- <h3 className="text-sm font-medium">태그 항목 ({fields.length}개)</h3>
+ <h3 className="text-sm font-medium">{t("sections.tagItems")} ({fields.length}개)</h3>
{!areAllTagNosValid && (
<Badge variant="destructive" className="ml-2">
- 유효하지 않은 태그 존재
+ {t("messages.invalidTagsExist")}
</Badge>
)}
</div>
@@ -628,10 +632,10 @@ export function AddFormTagDialog({
<TableRow>
<TableHead className="w-10 text-center">#</TableHead>
<TableHead className="w-[120px]">
- <div className="font-medium">Tag No</div>
+ <div className="font-medium">{t("labels.tagNo")}</div>
</TableHead>
<TableHead className="w-[180px]">
- <div className="font-medium">Description</div>
+ <div className="font-medium">{t("labels.description")}</div>
</TableHead>
{/* Subfields */}
@@ -653,7 +657,7 @@ export function AddFormTagDialog({
</TableHead>
))}
- <TableHead className="w-[100px] text-center sticky right-0 bg-muted">Actions</TableHead>
+ <TableHead className="w-[100px] text-center sticky right-0 bg-muted">{t("labels.actions")}</TableHead>
</TableRow>
</TableHeader>
@@ -711,7 +715,7 @@ export function AddFormTagDialog({
<Input
{...field}
className="h-8 w-full"
- placeholder="항목 이름 입력"
+ placeholder={t("placeholders.enterDescription")}
title={field.value || ""}
/>
</FormControl>
@@ -793,7 +797,7 @@ export function AddFormTagDialog({
</Button>
</TooltipTrigger>
<TooltipContent side="left">
- <p>행 복제</p>
+ <p>{t("tooltips.duplicateRow")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -816,7 +820,7 @@ export function AddFormTagDialog({
</Button>
</TooltipTrigger>
<TooltipContent side="left">
- <p>행 삭제</p>
+ <p>{t("tooltips.deleteRow")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -837,7 +841,7 @@ export function AddFormTagDialog({
disabled={!selectedTagTypeCode || isLoadingSubFields}
>
<Plus className="h-4 w-4 mr-2" />
- 새 행 추가
+ {t("buttons.addRow")}
</Button>
</div>
</div>
@@ -877,16 +881,16 @@ export function AddFormTagDialog({
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Plus className="mr-2 size-4" />
- 태그 추가
+ {t("buttons.addTags")}
</Button>
</DialogTrigger>
)}
<DialogContent className="max-h-[90vh] max-w-[95vw]" style={{ width: 1500 }}>
<DialogHeader>
- <DialogTitle>폼 태그 추가 - {formName || formCode}</DialogTitle>
+ <DialogTitle>{t("dialogs.addFormTag")} - {formName || formCode}</DialogTitle>
<DialogDescription>
- 클래스를 선택하여 태그 유형과 하위 필드를 로드한 다음, 여러 행을 추가하여 여러 태그를 생성하세요.
+ {t("dialogs.selectClassToLoadFields")}
</DialogDescription>
</DialogHeader>
@@ -934,7 +938,7 @@ export function AddFormTagDialog({
}}
disabled={isSubmitting}
>
- 취소
+ {t("buttons.cancel")}
</Button>
<Button
type="submit"
@@ -943,10 +947,10 @@ export function AddFormTagDialog({
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
- 처리 중...
+ {t("messages.processing")}
</>
) : (
- `${fields.length}개 태그 생성`
+ `${fields.length} ${t("buttons.create")}`
)}
</Button>
</div>
diff --git a/components/form-data/delete-form-data-dialog.tsx b/components/form-data/delete-form-data-dialog.tsx
index ca2f8729..9298b43b 100644
--- a/components/form-data/delete-form-data-dialog.tsx
+++ b/components/form-data/delete-form-data-dialog.tsx
@@ -3,6 +3,8 @@
import * as React from "react"
import { Loader, Trash } from "lucide-react"
import { toast } from "sonner"
+import { useParams } from "next/navigation"
+import { useTranslation } from "@/i18n/client"
import { useMediaQuery } from "@/hooks/use-media-query"
import { Button } from "@/components/ui/button"
@@ -55,22 +57,26 @@ export function DeleteFormDataDialog({
}: DeleteFormDataDialogProps) {
const [isDeletePending, startDeleteTransition] = React.useTransition()
const isDesktop = useMediaQuery("(min-width: 640px)")
+
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
// TAG_NO가 있는 항목들만 필터링
- const validItems = formData.filter(item => item.TAG_NO?.trim())
- const tagNos = validItems.map(item => item.TAG_NO).filter(Boolean) as string[]
+ const validItems = formData.filter(item => item.TAG_IDX?.trim())
+ const tagIdxs = validItems.map(item => item.TAG_IDX).filter(Boolean) as string[]
function onDelete() {
startDeleteTransition(async () => {
- if (tagNos.length === 0) {
- toast.error("No valid items to delete")
+ if (tagIdxs.length === 0) {
+ toast.error(t("delete.noValidItems"))
return
}
const result = await deleteFormDataByTags({
formCode,
contractItemId,
- tagNos,
+ tagIdxs,
})
if (result.error) {
@@ -88,12 +94,15 @@ export function DeleteFormDataDialog({
// 데이터 불일치 경고
console.warn(`Data inconsistency: FormEntries deleted: ${deletedCount}, Tags deleted: ${deletedTagsCount}`)
toast.error(
- `Deleted ${deletedCount} form entries and ${deletedTagsCount} tags (data inconsistency detected)`
+ t("delete.dataInconsistency", { deletedCount, deletedTagsCount })
)
} else {
// 정상적인 삭제 완료
toast.success(
- `Successfully deleted ${deletedCount} item${deletedCount === 1 ? "" : "s"}`
+ t("delete.successMessage", {
+ count: deletedCount,
+ items: deletedCount === 1 ? t("delete.item") : t("delete.items")
+ })
)
}
@@ -101,7 +110,7 @@ export function DeleteFormDataDialog({
})
}
- const itemCount = tagNos.length
+ const itemCount = tagIdxs.length
const hasValidItems = itemCount > 0
if (isDesktop) {
@@ -115,24 +124,25 @@ export function DeleteFormDataDialog({
disabled={!hasValidItems}
>
<Trash className="mr-2 size-4" aria-hidden="true" />
- Delete ({itemCount})
+ {t("buttons.delete")} ({itemCount})
</Button>
</DialogTrigger>
) : null}
<DialogContent>
<DialogHeader>
- <DialogTitle>Are you absolutely sure?</DialogTitle>
+ <DialogTitle>{t("delete.confirmTitle")}</DialogTitle>
<DialogDescription>
- This action cannot be undone. This will permanently delete{" "}
- <span className="font-medium">{itemCount}</span>
- {itemCount === 1 ? " item" : " items"} and related tag records from the database.
+ {t("delete.confirmDescription", {
+ count: itemCount,
+ items: itemCount === 1 ? t("delete.item") : t("delete.items")
+ })}
{itemCount > 0 && (
<>
<br />
<br />
<span className="text-sm text-muted-foreground">
- TAG_NO(s): {tagNos.slice(0, 3).join(", ")}
- {tagNos.length > 3 && ` and ${tagNos.length - 3} more...`}
+ {t("delete.tagNumbers")}: {tagIdxs.slice(0, 3).join(", ")}
+ {tagIdxs.length > 3 && t("delete.andMore", { count: tagIdxs.length - 3 })}
</span>
</>
)}
@@ -140,10 +150,10 @@ export function DeleteFormDataDialog({
</DialogHeader>
<DialogFooter className="gap-2 sm:space-x-0">
<DialogClose asChild>
- <Button variant="outline">Cancel</Button>
+ <Button variant="outline">{t("buttons.cancel")}</Button>
</DialogClose>
<Button
- aria-label="Delete selected entries"
+ aria-label={t("delete.deleteButtonLabel")}
variant="destructive"
onClick={onDelete}
disabled={isDeletePending || !hasValidItems}
@@ -154,7 +164,7 @@ export function DeleteFormDataDialog({
aria-hidden="true"
/>
)}
- Delete
+ {t("buttons.delete")}
</Button>
</DialogFooter>
</DialogContent>
@@ -172,24 +182,25 @@ export function DeleteFormDataDialog({
disabled={!hasValidItems}
>
<Trash className="mr-2 size-4" aria-hidden="true" />
- Delete ({itemCount})
+ {t("buttons.delete")} ({itemCount})
</Button>
</DrawerTrigger>
) : null}
<DrawerContent>
<DrawerHeader>
- <DrawerTitle>Are you absolutely sure?</DrawerTitle>
+ <DrawerTitle>{t("delete.confirmTitle")}</DrawerTitle>
<DrawerDescription>
- This action cannot be undone. This will permanently delete{" "}
- <span className="font-medium">{itemCount}</span>
- {itemCount === 1 ? " item" : " items"} and related tag records from the database.
+ {t("delete.confirmDescription", {
+ count: itemCount,
+ items: itemCount === 1 ? t("delete.item") : t("delete.items")
+ })}
{itemCount > 0 && (
<>
<br />
<br />
<span className="text-sm text-muted-foreground">
- TAG_NO(s): {tagNos.slice(0, 3).join(", ")}
- {tagNos.length > 3 && ` and ${tagNos.length - 3} more...`}
+ {t("delete.tagNumbers")}: {tagIdxs.slice(0, 3).join(", ")}
+ {tagIdxs.length > 3 && t("delete.andMore", { count: tagIdxs.length - 3 })}
</span>
</>
)}
@@ -197,10 +208,10 @@ export function DeleteFormDataDialog({
</DrawerHeader>
<DrawerFooter className="gap-2 sm:space-x-0">
<DrawerClose asChild>
- <Button variant="outline">Cancel</Button>
+ <Button variant="outline">{t("buttons.cancel")}</Button>
</DrawerClose>
<Button
- aria-label="Delete selected entries"
+ aria-label={t("delete.deleteButtonLabel")}
variant="destructive"
onClick={onDelete}
disabled={isDeletePending || !hasValidItems}
@@ -208,7 +219,7 @@ export function DeleteFormDataDialog({
{isDeletePending && (
<Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
)}
- Delete
+ {t("buttons.delete")}
</Button>
</DrawerFooter>
</DrawerContent>
diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx
index 53f8c489..fdd36c80 100644
--- a/components/form-data/form-data-report-batch-dialog.tsx
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -7,6 +7,8 @@ import React, {
useState,
useEffect,
} from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
import { useToast } from "@/hooks/use-toast";
import { toast as toastMessage } from "sonner";
import prettyBytes from "pretty-bytes";
@@ -84,6 +86,10 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
formCode,
}) => {
const { toast } = useToast();
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
+
const [tempList, setTempList] = useState<tempFile[]>([]);
const [selectTemp, setSelectTemp] = useState<string>("");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
@@ -115,9 +121,9 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
fileRejections.forEach((rejection) => {
toast({
variant: "destructive",
- title: "File Error",
+ title: t("batchReport.fileError"),
description: `${rejection.file.name}: ${
- rejection.errors[0]?.message || "Upload failed"
+ rejection.errors[0]?.message || t("batchReport.uploadFailed")
}`,
});
});
@@ -164,7 +170,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
if (requestCreateReport.ok) {
const blob = await requestCreateReport.blob();
saveAs(blob, `${formCode}.pdf`);
- toastMessage.success("Report 다운로드 완료!");
+ toastMessage.success(t("batchReport.downloadComplete"));
} else {
const err = await requestCreateReport.json();
console.error("에러:", err);
@@ -173,8 +179,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
} catch (err) {
console.error(err);
toast({
- title: "Error",
- description: "Report 생성 중 오류가 발생했습니다.",
+ title: t("batchReport.error"),
+ description: t("batchReport.reportGenerationError"),
variant: "destructive",
});
} finally {
@@ -219,7 +225,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
const blob = await requestCreateReport.blob();
setGeneratedFileBlob(blob);
setPublishDialogOpen(true);
- toastMessage.success("문서가 생성되었습니다. 발행 정보를 입력해주세요.");
+ toastMessage.success(t("batchReport.documentGenerated"));
} else {
const err = await requestCreateReport.json();
console.error("에러:", err);
@@ -228,8 +234,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
} catch (err) {
console.error(err);
toast({
- title: "Error",
- description: "문서 생성 중 오류가 발생했습니다.",
+ title: t("batchReport.error"),
+ description: t("batchReport.documentGenerationError"),
variant: "destructive",
});
} finally {
@@ -242,17 +248,16 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
<DialogHeader>
- <DialogTitle>Vendor Document Create</DialogTitle>
+ <DialogTitle>{t("batchReport.dialogTitle")}</DialogTitle>
<DialogDescription>
- Vendor Document Template을 선택하신 후 갑지를 업로드하여 주시기
- 바랍니다.
+ {t("batchReport.dialogDescription")}
</DialogDescription>
</DialogHeader>
<div className="h-[60px]">
- <Label>Vendor Document Template Select</Label>
+ <Label>{t("batchReport.templateSelectLabel")}</Label>
<Select value={selectTemp} onValueChange={setSelectTemp}>
<SelectTrigger className="w-[100%]">
- <SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." />
+ <SelectValue placeholder={t("batchReport.templateSelectPlaceholder")} />
</SelectTrigger>
<SelectContent>
{tempList.map((c) => {
@@ -268,7 +273,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
</Select>
</div>
<div>
- <Label>Vendor Document Cover Page Upload(.docx)</Label>
+ <Label>{t("batchReport.coverPageUploadLabel")}</Label>
<Dropzone
maxSize={MAX_FILE_SIZE}
multiple={false}
@@ -284,16 +289,17 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<div className="flex items-center gap-6">
<DropzoneUploadIcon />
<div className="grid gap-0.5">
- <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
+ <DropzoneTitle>{t("batchReport.dropFileHere")}</DropzoneTitle>
<DropzoneDescription>
- 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
- {maxSize ? prettyBytes(maxSize) : "무제한"}
+ {t("batchReport.orClickToSelect", {
+ maxSize: maxSize ? prettyBytes(maxSize) : t("batchReport.unlimited")
+ })}
</DropzoneDescription>
</div>
</div>
</DropzoneZone>
<Label className="text-xs text-muted-foreground">
- 여러 파일을 선택할 수 있습니다.
+ {t("batchReport.multipleFilesAllowed")}
</Label>
</>
)}
@@ -304,15 +310,18 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<div className="grid gap-2">
<div className="flex items-center justify-between">
<h6 className="text-sm font-semibold">
- 선택된 파일 ({selectedFiles.length})
+ {t("batchReport.selectedFiles", { count: selectedFiles.length })}
</h6>
- <Badge variant="secondary">{selectedFiles.length}개 파일</Badge>
+ <Badge variant="secondary">
+ {t("batchReport.fileCount", { count: selectedFiles.length })}
+ </Badge>
</div>
<ScrollArea>
<UploadFileItem
selectedFiles={selectedFiles}
removeFile={removeFile}
isUploading={isUploading}
+ t={t}
/>
</ScrollArea>
</div>
@@ -331,7 +340,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
className="mr-2"
>
{isUploading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- Publish
+ {t("batchReport.publish")}
</Button>
<Button
disabled={
@@ -342,7 +351,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
onClick={submitData}
>
{isUploading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- Create Vendor Document
+ {t("batchReport.createDocument")}
</Button>
</DialogFooter>
</DialogContent>
@@ -364,12 +373,14 @@ interface UploadFileItemProps {
selectedFiles: File[];
removeFile: (index: number) => void;
isUploading: boolean;
+ t: (key: string, options?: any) => string;
}
const UploadFileItem: FC<UploadFileItemProps> = ({
selectedFiles,
removeFile,
isUploading,
+ t,
}) => {
return (
<FileList className="max-h-[200px] gap-3">
@@ -388,7 +399,7 @@ const UploadFileItem: FC<UploadFileItemProps> = ({
disabled={isUploading}
>
<X className="h-4 w-4" />
- <span className="sr-only">Remove</span>
+ <span className="sr-only">{t("batchReport.remove")}</span>
</FileListAction>
</FileListHeader>
</FileListItem>
diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx
index 3cfbbeb3..c990f019 100644
--- a/components/form-data/form-data-report-dialog.tsx
+++ b/components/form-data/form-data-report-dialog.tsx
@@ -8,6 +8,8 @@ import React, {
useEffect,
useRef,
} from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
import { WebViewerInstance } from "@pdftron/webviewer";
import { Loader2 } from "lucide-react";
import { saveAs } from "file-saver";
@@ -60,6 +62,9 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
formId,
formCode,
}) => {
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
const [tempList, setTempList] = useState<tempFile[]>([]);
const [selectTemp, setSelectTemp] = useState<string>("");
@@ -100,7 +105,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
saveAs(new Blob([fileData]), fileName);
- toast.success("Report 다운로드 완료!");
+ toast.success(t("singleReport.downloadComplete"));
}
};
@@ -120,7 +125,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
setPublishDialogOpen(true);
} catch (error) {
console.error("Error preparing file for publishing:", error);
- toast.error("Failed to prepare document for publishing");
+ toast.error(t("singleReport.publishPreparationFailed"));
}
}
};
@@ -130,20 +135,20 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
<Dialog open={reportData.length > 0} onOpenChange={onClose}>
<DialogContent className="w-[70vw]" style={{ maxWidth: "none" }}>
<DialogHeader>
- <DialogTitle>Create Vendor Document</DialogTitle>
+ <DialogTitle>{t("singleReport.dialogTitle")}</DialogTitle>
<DialogDescription>
- 사용하시고자 하는 Vendor Document Template를 선택하여 주시기 바랍니다.
+ {t("singleReport.dialogDescription")}
</DialogDescription>
</DialogHeader>
<div className="h-[60px]">
- <Label>Vendor Document Template Select</Label>
+ <Label>{t("singleReport.templateSelectLabel")}</Label>
<Select
value={selectTemp}
onValueChange={setSelectTemp}
disabled={instance === null}
>
<SelectTrigger className="w-[100%]">
- <SelectValue placeholder="사용하시고자하는 Vendor Document Template을 선택하여 주시기 바랍니다." />
+ <SelectValue placeholder={t("singleReport.templateSelectPlaceholder")} />
</SelectTrigger>
<SelectContent>
{tempList.map((c) => {
@@ -167,6 +172,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
setInstance={setInstance}
setFileLoading={setFileLoading}
formCode={formCode}
+ t={t}
/>
</div>
@@ -178,10 +184,10 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
variant="outline"
className="mr-2"
>
- Publish
+ {t("singleReport.publish")}
</Button>
<Button onClick={downloadFileData} disabled={selectTemp.length === 0}>
- Create Vendor Document
+ {t("singleReport.createDocument")}
</Button>
</DialogFooter>
</DialogContent>
@@ -208,6 +214,7 @@ interface ReportWebViewerProps {
setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
setFileLoading: Dispatch<SetStateAction<boolean>>;
formCode: string;
+ t: (key: string, options?: any) => string;
}
const ReportWebViewer: FC<ReportWebViewerProps> = ({
@@ -218,6 +225,7 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
setInstance,
setFileLoading,
formCode,
+ t,
}) => {
const [viwerLoading, setViewerLoading] = useState<boolean>(true);
const viewer = useRef<HTMLDivElement>(null);
@@ -280,7 +288,7 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
{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>
+ <p className="text-sm text-muted-foreground">{t("singleReport.documentViewerLoading")}</p>
</div>
)}
</div>
diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx
index 78663d64..59ea6ade 100644
--- a/components/form-data/form-data-report-temp-upload-dialog.tsx
+++ b/components/form-data/form-data-report-temp-upload-dialog.tsx
@@ -1,6 +1,8 @@
"use client";
import React, { FC, Dispatch, SetStateAction, useState } from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
import {
Dialog,
DialogContent,
@@ -38,60 +40,62 @@ export const FormDataReportTempUploadDialog: FC<
formCode,
uploaderType,
}) => {
- const [tabValue, setTabValue] = useState<"upload" | "uploaded">("upload");
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
- <DialogHeader className="gap-2">
- <DialogTitle>Vendor Document Template</DialogTitle>
- <DialogDescription className="flex justify-around gap-[16px] ">
- {/* 사용하시고자 하는 Vendor Document Template(.docx)를 업로드
- 하여주시기 바랍니다. */}
- <FileActionsDropdown
- filePath={"/vendorFormReportSample/sample_template_file.docx"}
- fileName={"sample_template_file.docx"}
- variant="ghost"
- size="icon"
- description="Sample File"
- />
- <VarListDownloadBtn columnsJSON={columnsJSON} formCode={formCode} />
- </DialogDescription>
- </DialogHeader>
- <Tabs value={tabValue}>
- <div className="flex justify-between items-center">
- <TabsList className="w-full">
- <TabsTrigger
- value="upload"
- onClick={() => setTabValue("upload")}
- className="flex-1"
- >
- Upload Template File
- </TabsTrigger>
- <TabsTrigger
- value="uploaded"
- onClick={() => setTabValue("uploaded")}
- className="flex-1"
- >
- Uploaded Template File List
- </TabsTrigger>
- </TabsList>
- </div>
- <TabsContent value="upload">
- <FormDataReportTempUploadTab
- packageId={packageId}
- formId={formId}
- uploaderType={uploaderType}
- />
- </TabsContent>
- <TabsContent value="uploaded">
- <FormDataReportTempUploadedListTab
- packageId={packageId}
- formId={formId}
- />
- </TabsContent>
- </Tabs>
- </DialogContent>
- </Dialog>
- );
- }; \ No newline at end of file
+ const [tabValue, setTabValue] = useState<"upload" | "uploaded">("upload");
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
+ <DialogHeader className="gap-2">
+ <DialogTitle>{t("templateUpload.dialogTitle")}</DialogTitle>
+ <DialogDescription className="flex justify-around gap-[16px] ">
+ <FileActionsDropdown
+ filePath={"/vendorFormReportSample/sample_template_file.docx"}
+ fileName={"sample_template_file.docx"}
+ variant="ghost"
+ size="icon"
+ description={t("templateUpload.sampleFile")}
+ />
+ <VarListDownloadBtn columnsJSON={columnsJSON} formCode={formCode} />
+ </DialogDescription>
+ </DialogHeader>
+ <Tabs value={tabValue}>
+ <div className="flex justify-between items-center">
+ <TabsList className="w-full">
+ <TabsTrigger
+ value="upload"
+ onClick={() => setTabValue("upload")}
+ className="flex-1"
+ >
+ {t("templateUpload.uploadTab")}
+ </TabsTrigger>
+ <TabsTrigger
+ value="uploaded"
+ onClick={() => setTabValue("uploaded")}
+ className="flex-1"
+ >
+ {t("templateUpload.uploadedListTab")}
+ </TabsTrigger>
+ </TabsList>
+ </div>
+ <TabsContent value="upload">
+ <FormDataReportTempUploadTab
+ packageId={packageId}
+ formId={formId}
+ uploaderType={uploaderType}
+ />
+ </TabsContent>
+ <TabsContent value="uploaded">
+ <FormDataReportTempUploadedListTab
+ packageId={packageId}
+ formId={formId}
+ />
+ </TabsContent>
+ </Tabs>
+ </DialogContent>
+ </Dialog>
+ );
+}; \ No newline at end of file
diff --git a/components/form-data/form-data-report-temp-upload-tab.tsx b/components/form-data/form-data-report-temp-upload-tab.tsx
index 32161e49..39d895a1 100644
--- a/components/form-data/form-data-report-temp-upload-tab.tsx
+++ b/components/form-data/form-data-report-temp-upload-tab.tsx
@@ -1,6 +1,8 @@
"use client";
import React, { FC, useState } from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
import { useToast } from "@/hooks/use-toast";
import { toast as toastMessage } from "sonner";
import prettyBytes from "pretty-bytes";
@@ -43,6 +45,10 @@ export const FormDataReportTempUploadTab: FC<
FormDataReportTempUploadTabProps
> = ({ packageId, formId, uploaderType }) => {
const { toast } = useToast();
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
+
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
@@ -58,9 +64,9 @@ export const FormDataReportTempUploadTab: FC<
fileRejections.forEach((rejection) => {
toast({
variant: "destructive",
- title: "File Error",
+ title: t("templateUploadTab.fileError"),
description: `${rejection.file.name}: ${
- rejection.errors[0]?.message || "Upload failed"
+ rejection.errors[0]?.message || t("templateUploadTab.uploadFailed")
}`,
});
});
@@ -93,12 +99,12 @@ export const FormDataReportTempUploadTab: FC<
successCount++;
setUploadProgress(Math.round((successCount / totalFiles) * 100));
}
- toastMessage.success("Template File 업로드 완료!");
+ toastMessage.success(t("templateUploadTab.uploadComplete"));
} catch (err) {
console.error(err);
toast({
- title: "Error",
- description: "파일 업로드 중 오류가 발생했습니다.",
+ title: t("templateUploadTab.error"),
+ description: t("templateUploadTab.uploadError"),
variant: "destructive",
});
} finally {
@@ -111,7 +117,7 @@ export const FormDataReportTempUploadTab: FC<
return (
<div className='flex flex-col gap-4'>
<div>
- <Label>Vendor Document Template File Upload(.docx)</Label>
+ <Label>{t("templateUploadTab.uploadLabel")}</Label>
<Dropzone
maxSize={MAX_FILE_SIZE}
multiple={true}
@@ -127,16 +133,17 @@ export const FormDataReportTempUploadTab: FC<
<div className="flex items-center gap-6">
<DropzoneUploadIcon />
<div className="grid gap-0.5">
- <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
+ <DropzoneTitle>{t("templateUploadTab.dropFileHere")}</DropzoneTitle>
<DropzoneDescription>
- 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
- {maxSize ? prettyBytes(maxSize) : "무제한"}
+ {t("templateUploadTab.orClickToSelect", {
+ maxSize: maxSize ? prettyBytes(maxSize) : t("templateUploadTab.unlimited")
+ })}
</DropzoneDescription>
</div>
</div>
</DropzoneZone>
<Label className="text-xs text-muted-foreground">
- 여러 파일을 선택할 수 있습니다.
+ {t("templateUploadTab.multipleFilesAllowed")}
</Label>
</>
)}
@@ -147,24 +154,27 @@ export const FormDataReportTempUploadTab: FC<
<div className="grid gap-2">
<div className="flex items-center justify-between">
<h6 className="text-sm font-semibold">
- 선택된 파일 ({selectedFiles.length})
+ {t("templateUploadTab.selectedFiles", { count: selectedFiles.length })}
</h6>
- <Badge variant="secondary">{selectedFiles.length}개 파일</Badge>
+ <Badge variant="secondary">
+ {t("templateUploadTab.fileCount", { count: selectedFiles.length })}
+ </Badge>
</div>
<ScrollArea>
<UploadFileItem
selectedFiles={selectedFiles}
removeFile={removeFile}
isUploading={isUploading}
+ t={t}
/>
</ScrollArea>
</div>
)}
- {isUploading && <UploadProgressBox uploadProgress={uploadProgress} />}
+ {isUploading && <UploadProgressBox uploadProgress={uploadProgress} t={t} />}
<DialogFooter>
<Button disabled={selectedFiles.length === 0} onClick={submitData}>
- 업로드
+ {t("templateUploadTab.upload")}
</Button>
</DialogFooter>
</div>
@@ -175,12 +185,14 @@ interface UploadFileItemProps {
selectedFiles: File[];
removeFile: (index: number) => void;
isUploading: boolean;
+ t: (key: string, options?: any) => string;
}
const UploadFileItem: FC<UploadFileItemProps> = ({
selectedFiles,
removeFile,
isUploading,
+ t,
}) => {
return (
<FileList className="max-h-[150px] gap-3">
@@ -199,7 +211,7 @@ const UploadFileItem: FC<UploadFileItemProps> = ({
disabled={isUploading}
>
<X className="h-4 w-4" />
- <span className="sr-only">Remove</span>
+ <span className="sr-only">{t("templateUploadTab.remove")}</span>
</FileListAction>
</FileListHeader>
</FileListItem>
@@ -208,14 +220,17 @@ const UploadFileItem: FC<UploadFileItemProps> = ({
);
};
-const UploadProgressBox: FC<{ uploadProgress: number }> = ({
- uploadProgress,
-}) => {
+const UploadProgressBox: FC<{
+ uploadProgress: number;
+ t: (key: string, options?: any) => string;
+}> = ({ uploadProgress, t }) => {
return (
<div className="flex flex-col gap-1 mt-2">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
- <span className="text-sm">{uploadProgress}% 업로드 중...</span>
+ <span className="text-sm">
+ {t("templateUploadTab.uploadingProgress", { progress: uploadProgress })}
+ </span>
</div>
<div className="h-2 w-full bg-muted rounded-full overflow-hidden">
<div
diff --git a/components/form-data/form-data-report-temp-uploaded-list-tab.tsx b/components/form-data/form-data-report-temp-uploaded-list-tab.tsx
index a5c3c7a5..7bd5eeef 100644
--- a/components/form-data/form-data-report-temp-uploaded-list-tab.tsx
+++ b/components/form-data/form-data-report-temp-uploaded-list-tab.tsx
@@ -7,6 +7,8 @@ import React, {
useState,
useEffect,
} from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
import { useToast } from "@/hooks/use-toast";
import { toast as toastMessage } from "sonner";
import { Download, Trash2 } from "lucide-react";
@@ -44,6 +46,10 @@ interface FormDataReportTempUploadedListTabProps {
export const FormDataReportTempUploadedListTab: FC<
FormDataReportTempUploadedListTabProps
> = ({ packageId, formId }) => {
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
+
const [prevReportTemp, setPrevReportTemp] = useState<VendorDataReportTemps[]>(
[]
);
@@ -60,13 +66,14 @@ export const FormDataReportTempUploadedListTab: FC<
return (
<div>
- <Label>Uploaded Template File List</Label>
+ <Label>{t("templateUploadedList.listLabel")}</Label>
<UploadedTempFiles
prevReportTemp={prevReportTemp}
updateReportTempList={() =>
updateReportTempList(packageId, formId, setPrevReportTemp)
}
isLoading={isLoading}
+ t={t}
/>
</div>
);
@@ -91,12 +98,14 @@ interface UploadedTempFiles {
prevReportTemp: VendorDataReportTemps[];
updateReportTempList: () => void;
isLoading: boolean;
+ t: (key: string, options?: any) => string;
}
const UploadedTempFiles: FC<UploadedTempFiles> = ({
prevReportTemp,
updateReportTempList,
isLoading,
+ t,
}) => {
const { toast } = useToast();
@@ -109,19 +118,17 @@ const UploadedTempFiles: FC<UploadedTempFiles> = ({
saveAs(blob, fileName);
- toastMessage.success("Report 다운로드 완료!");
+ toastMessage.success(t("templateUploadedList.downloadComplete"));
} else {
const err = await getTempFile.json();
console.error("에러:", err);
throw new Error(err.message);
}
-
- toastMessage.success("Template File 다운로드 완료!");
} catch (err) {
console.error(err);
toast({
- title: "Error",
- description: "Template File 다운로드 중 오류가 발생했습니다.",
+ title: t("templateUploadedList.error"),
+ description: t("templateUploadedList.downloadError"),
variant: "destructive",
});
}
@@ -133,14 +140,14 @@ const UploadedTempFiles: FC<UploadedTempFiles> = ({
if (result) {
updateReportTempList();
- toastMessage.success("Template File 삭제 완료!");
+ toastMessage.success(t("templateUploadedList.deleteComplete"));
} else {
throw new Error(error);
}
} catch (err) {
toast({
- title: "Error",
- description: "Template File 삭제 중 오류가 발생했습니다.",
+ title: t("templateUploadedList.error"),
+ description: t("templateUploadedList.deleteError"),
variant: "destructive",
});
}
@@ -149,7 +156,7 @@ const UploadedTempFiles: FC<UploadedTempFiles> = ({
if (isLoading) {
return (
<div className="min-h-[157px]">
- <Label>로딩 중...</Label>
+ <Label>{t("templateUploadedList.loading")}</Label>
</div>
);
}
@@ -174,29 +181,29 @@ const UploadedTempFiles: FC<UploadedTempFiles> = ({
}}
>
<Download className="h-4 w-4" />
- <span className="sr-only">Download</span>
+ <span className="sr-only">{t("templateUploadedList.download")}</span>
</FileListAction>
<AlertDialogTrigger asChild>
<FileListAction>
<Trash2 className="h-4 w-4" />
- <span className="sr-only">Delete</span>
+ <span className="sr-only">{t("templateUploadedList.delete")}</span>
</FileListAction>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
- Report Templete File({fileName})을 삭제하시겠습니까?
+ {t("templateUploadedList.deleteConfirmTitle", { fileName })}
</AlertDialogTitle>
<AlertDialogDescription />
</AlertDialogHeader>
<AlertDialogFooter>
- <AlertDialogCancel>취소</AlertDialogCancel>
+ <AlertDialogCancel>{t("templateUploadedList.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
deleteTempFile(id);
}}
>
- 삭제
+ {t("templateUploadedList.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx
index 2a065d1b..2f623bdb 100644
--- a/components/form-data/form-data-table-columns.tsx
+++ b/components/form-data/form-data-table-columns.tsx
@@ -63,7 +63,93 @@ interface GetColumnsProps<TData> {
// 체크박스 선택 관련 props
selectedRows?: Record<string, boolean>;
onRowSelectionChange?: (updater: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void;
- // editableFieldsMap 제거됨
+ // 새로 추가: templateData
+ templateData?: any;
+}
+
+/**
+ * 셀 주소(예: "A1", "B1", "AA1")에서 컬럼 순서를 추출하는 함수
+ * A=0, B=1, C=2, ..., Z=25, AA=26, AB=27, ...
+ */
+function getColumnOrderFromCellAddress(cellAddress: string): number {
+ if (!cellAddress || typeof cellAddress !== 'string') {
+ return 999999; // 유효하지 않은 경우 맨 뒤로
+ }
+
+ // 셀 주소에서 알파벳 부분만 추출 (예: "A1" -> "A", "AA1" -> "AA")
+ const match = cellAddress.match(/^([A-Z]+)/);
+ if (!match) {
+ return 999999;
+ }
+
+ const columnLetters = match[1];
+ let result = 0;
+
+ // 알파벳을 숫자로 변환 (26진법과 유사하지만 0이 없는 체계)
+ for (let i = 0; i < columnLetters.length; i++) {
+ const charCode = columnLetters.charCodeAt(i) - 65 + 1; // A=1, B=2, ..., Z=26
+ result = result * 26 + charCode;
+ }
+
+ return result - 1; // 0부터 시작하도록 조정
+}
+
+/**
+ * templateData에서 SPREAD_LIST의 컬럼 순서 정보를 추출하여 seq를 업데이트하는 함수
+ */
+function updateSeqFromTemplate(columnsJSON: DataTableColumnJSON[], templateData: any): DataTableColumnJSON[] {
+ if (!templateData) {
+ return columnsJSON; // templateData가 없으면 원본 그대로 반환
+ }
+
+ // templateData가 배열인지 단일 객체인지 확인
+ let templates: any[];
+ if (Array.isArray(templateData)) {
+ templates = templateData;
+ } else {
+ templates = [templateData];
+ }
+
+ // SPREAD_LIST 타입의 템플릿 찾기
+ const spreadListTemplate = templates.find(template =>
+ template.TMPL_TYPE === 'SPREAD_LIST' &&
+ template.SPR_LST_SETUP?.DATA_SHEETS
+ );
+
+ if (!spreadListTemplate) {
+ return columnsJSON; // SPREAD_LIST 템플릿이 없으면 원본 그대로 반환
+ }
+
+ // MAP_CELL_ATT에서 ATT_ID와 IN 매핑 정보 추출
+ const cellMappings = new Map<string, string>(); // key: ATT_ID, value: IN (셀 주소)
+
+ spreadListTemplate.SPR_LST_SETUP.DATA_SHEETS.forEach((dataSheet: any) => {
+ if (dataSheet.MAP_CELL_ATT) {
+ dataSheet.MAP_CELL_ATT.forEach((mapping: any) => {
+ if (mapping.ATT_ID && mapping.IN) {
+ cellMappings.set(mapping.ATT_ID, mapping.IN);
+ }
+ });
+ }
+ });
+
+ // columnsJSON을 복사하여 seq 값 업데이트
+ const updatedColumns = columnsJSON.map(column => {
+ const cellAddress = cellMappings.get(column.key);
+ if (cellAddress) {
+ // 셀 주소에서 컬럼 순서 추출
+ const newSeq = getColumnOrderFromCellAddress(cellAddress);
+ console.log(`🔄 Updating seq for ${column.key}: ${column.seq} -> ${newSeq} (from ${cellAddress})`);
+
+ return {
+ ...column,
+ seq: newSeq
+ };
+ }
+ return column; // 매핑이 없으면 원본 그대로
+ });
+
+ return updatedColumns;
}
/**
@@ -271,12 +357,15 @@ export function getColumns<TData extends object>({
tempCount,
selectedRows = {},
onRowSelectionChange,
- // editableFieldsMap 매개변수 제거됨
+ templateData, // 새로 추가된 매개변수
}: GetColumnsProps<TData>): ColumnDef<TData>[] {
const columns: ColumnDef<TData>[] = [];
- // (0) 컬럼 필터링 및 정렬
- const visibleColumns = columnsJSON
+ // (0) templateData에서 SPREAD_LIST인 경우 seq 값 업데이트
+ const processedColumnsJSON = updateSeqFromTemplate(columnsJSON, templateData);
+
+ // (1) 컬럼 필터링 및 정렬
+ const visibleColumns = processedColumnsJSON
.filter(col => col.hidden !== true) // hidden이 true가 아닌 것들만
.sort((a, b) => {
// seq가 없는 경우 999999로 처리하여 맨 뒤로 보냄
@@ -285,7 +374,10 @@ export function getColumns<TData extends object>({
return seqA - seqB;
});
- // (1) 체크박스 컬럼 (항상 표시)
+ console.log('📊 Final column order after template processing:',
+ visibleColumns.map(c => `${c.key}(seq:${c.seq})`));
+
+ // (2) 체크박스 컬럼 (항상 표시)
const selectColumn: ColumnDef<TData> = {
id: "select",
header: ({ table }) => (
@@ -335,11 +427,11 @@ export function getColumns<TData extends object>({
};
columns.push(selectColumn);
- // (2) 기본 컬럼들 (seq 순서를 유지하면서 head에 따라 그룹핑 처리)
+ // (3) 기본 컬럼들 (seq 순서를 유지하면서 head에 따라 그룹핑 처리)
const groupedColumns = groupColumnsByHead(visibleColumns);
columns.push(...groupedColumns);
- // (3) 액션 칼럼 - update 버튼 예시
+ // (4) 액션 칼럼 - update 버튼 예시
const actionColumn: ColumnDef<TData> = {
id: "update",
header: "",
@@ -392,6 +484,6 @@ export function getColumns<TData extends object>({
columns.push(actionColumn);
- // (4) 최종 반환
+ // (5) 최종 반환
return columns;
} \ 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 9936e870..be37de7a 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -172,7 +172,7 @@ export default function DynamicTable({
const params = useParams();
const router = useRouter();
const lng = (params?.lng as string) || "ko";
- const { t } = useTranslation(lng, "translation");
+ const { t } = useTranslation(lng, "engineering");
const [rowAction, setRowAction] =
React.useState<DataTableRowAction<GenericData> | null>(null);
@@ -719,7 +719,7 @@ export default function DynamicTable({
onClick={handleBatchDelete}
>
<Trash2 className="mr-2 size-4" />
- Delete ({selectedRowCount})
+ {t("buttons.delete")} ({selectedRowCount})
</Button>
)}
@@ -733,7 +733,7 @@ export default function DynamicTable({
<Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
)}
<TagsIcon className="size-4" />
- Tag Operations
+ {t("buttons.tagOperations")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
@@ -741,12 +741,12 @@ export default function DynamicTable({
{mode === "IM" ? (
<DropdownMenuItem onClick={handleSyncTags} disabled={isAnyOperationPending}>
<Tag className="mr-2 h-4 w-4" />
- Sync Tags
+ {t("buttons.syncTags")}
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={handleGetTags} disabled={isAnyOperationPending}>
<RefreshCcw className="mr-2 h-4 w-4" />
- Get Tags
+ {t("buttons.getTags")}
</DropdownMenuItem>
)}
<DropdownMenuItem
@@ -754,7 +754,7 @@ export default function DynamicTable({
disabled={isAnyOperationPending || isAddTagDisabled}
>
<Plus className="mr-2 h-4 w-4" />
- Add Tags
+ {t("buttons.addTags")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -764,17 +764,17 @@ export default function DynamicTable({
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={isAnyOperationPending}>
<Clipboard className="size-4" />
- Report Operations
+ {t("buttons.reportOperations")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTempUpDialog(true)} disabled={isAnyOperationPending}>
<Upload className="mr-2 h-4 w-4" />
- Upload Template
+ {t("buttons.uploadTemplate")}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleBatchDocument} disabled={isAnyOperationPending}>
<FileOutput className="mr-2 h-4 w-4" />
- Batch Document
+ {t("buttons.batchDocument")}
{selectedRowCount > 0 && (
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded">
{selectedRowCount}
@@ -792,7 +792,7 @@ export default function DynamicTable({
) : (
<Upload className="size-4" />
)}
- Import
+ {t("buttons.import")}
<input
type="file"
accept=".xlsx,.xls"
@@ -815,10 +815,10 @@ export default function DynamicTable({
) : (
<Download className="mr-2 size-4" />
)}
- Export
+ {t("buttons.export")}
</Button>
- {/* 새로 추가된 Template 보기 버튼 */}
+ {/* Template 보기 버튼 */}
<Button
variant="outline"
size="sm"
@@ -830,10 +830,9 @@ export default function DynamicTable({
) : (
<Eye className="mr-2 size-4" />
)}
- View Template
+ {t("buttons.viewTemplate")}
</Button>
-
{/* COMPARE WITH SEDP 버튼 */}
<Button
variant="outline"
@@ -842,7 +841,7 @@ export default function DynamicTable({
disabled={isAnyOperationPending}
>
<GitCompareIcon className="mr-2 size-4" />
- Compare with SEDP
+ {t("buttons.compareWithSEDP")}
</Button>
{/* SEDP 전송 버튼 */}
@@ -855,12 +854,12 @@ export default function DynamicTable({
{isSendingSEDP ? (
<>
<Loader className="mr-2 size-4 animate-spin" />
- SEDP 전송 중...
+ {t("messages.sendingSEDP")}
</>
) : (
<>
<Send className="size-4" />
- Send to SHI
+ {t("buttons.sendToSHI")}
</>
)}
</Button>
diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx
index 647f2810..1a9938bd 100644
--- a/components/form-data/sedp-compare-dialog.tsx
+++ b/components/form-data/sedp-compare-dialog.tsx
@@ -11,6 +11,8 @@ import { DataTableColumnJSON } from "./form-data-table-columns";
import { ExcelDownload } from "./sedp-excel-download";
import { Switch } from "../ui/switch";
import { Card, CardContent } from "@/components/ui/card";
+import { useTranslation } from "@/i18n/client"
+import { useParams } from "next/navigation"
interface SEDPCompareDialogProps {
isOpen: boolean;
@@ -56,85 +58,6 @@ const DisplayValue = ({ value, uom, isSedp = false }: { value: any; uom?: string
);
};
-// 범례 컴포넌트
-const ColorLegend = () => {
- return (
- <div className="flex items-center gap-4 text-sm p-2 bg-muted/20 rounded">
- <div className="flex items-center gap-1.5">
- <Info className="h-4 w-4 text-muted-foreground" />
- <span className="font-medium">범례:</span>
- </div>
- <div className="flex items-center gap-3">
- <div className="flex items-center gap-1.5">
- <div className="h-3 w-3 rounded-full bg-red-500"></div>
- <span className="line-through text-red-500">로컬 값</span>
- </div>
- <div className="flex items-center gap-1.5">
- <div className="h-3 w-3 rounded-full bg-green-500"></div>
- <span className="text-green-500">SEDP 값</span>
- </div>
- </div>
- </div>
- );
-};
-
-// 확장 가능한 차이점 표시 컴포넌트
-const DifferencesCard = ({
- attributes,
- columnLabelMap,
- showOnlyDifferences
-}: {
- attributes: ComparisonResult['attributes'];
- columnLabelMap: Record<string, string>;
- showOnlyDifferences: boolean;
-}) => {
- const attributesToShow = showOnlyDifferences
- ? attributes.filter(attr => !attr.isMatching)
- : attributes;
-
- if (attributesToShow.length === 0) {
- return (
- <div className="text-center text-muted-foreground py-4">
- 모든 속성이 일치합니다
- </div>
- );
- }
-
- return (
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 p-4">
- {attributesToShow.map((attr) => (
- <Card key={attr.key} className={`${!attr.isMatching ? 'border-red-200' : 'border-green-200'}`}>
- <CardContent className="p-3">
- <div className="font-medium text-sm mb-2 truncate" title={attr.label}>
- {attr.label}
- {attr.uom && <span className="text-xs text-muted-foreground ml-1">({attr.uom})</span>}
- </div>
- {attr.isMatching ? (
- <div className="text-sm">
- <DisplayValue value={attr.localValue} uom={attr.uom} />
- </div>
- ) : (
- <div className="space-y-2">
- <div className="flex items-center gap-2 text-sm">
- <span className="text-xs text-muted-foreground min-w-[35px]">로컬:</span>
- <span className="line-through text-red-500 flex-1">
- <DisplayValue value={attr.localValue} uom={attr.uom} isSedp={false} />
- </span>
- </div>
- <div className="flex items-center gap-2 text-sm">
- <span className="text-xs text-muted-foreground min-w-[35px]">SEDP:</span>
- <span className="text-green-500 flex-1">
- <DisplayValue value={attr.sedpValue} uom={attr.uom} isSedp={true} />
- </span>
- </div>
- </div>
- )}
- </CardContent>
- </Card>
- ))}
- </div>
- );
-};
export function SEDPCompareDialog({
isOpen,
@@ -145,6 +68,92 @@ export function SEDPCompareDialog({
formCode,
fetchTagDataFromSEDP,
}: SEDPCompareDialogProps) {
+
+ const params = useParams() || {}
+ const lng = params.lng ? String(params.lng) : "ko"
+ const { t } = useTranslation(lng, "engineering")
+
+ // 범례 컴포넌트
+ const ColorLegend = () => {
+ return (
+ <div className="flex items-center gap-4 text-sm p-2 bg-muted/20 rounded">
+ <div className="flex items-center gap-1.5">
+ <Info className="h-4 w-4 text-muted-foreground" />
+ <span className="font-medium">{t("labels.legend")}:</span>
+ </div>
+ <div className="flex items-center gap-3">
+ <div className="flex items-center gap-1.5">
+ <div className="h-3 w-3 rounded-full bg-red-500"></div>
+ <span className="line-through text-red-500">{t("labels.localValue")}</span>
+ </div>
+ <div className="flex items-center gap-1.5">
+ <div className="h-3 w-3 rounded-full bg-green-500"></div>
+ <span className="text-green-500">{t("labels.sedpValue")}</span>
+ </div>
+ </div>
+ </div>
+ );
+ };
+
+ // 확장 가능한 차이점 표시 컴포넌트
+ const DifferencesCard = ({
+ attributes,
+ columnLabelMap,
+ showOnlyDifferences
+ }: {
+ attributes: ComparisonResult['attributes'];
+ columnLabelMap: Record<string, string>;
+ showOnlyDifferences: boolean;
+ }) => {
+ const attributesToShow = showOnlyDifferences
+ ? attributes.filter(attr => !attr.isMatching)
+ : attributes;
+
+ if (attributesToShow.length === 0) {
+ return (
+ <div className="text-center text-muted-foreground py-4">
+ {t("messages.allAttributesMatch")}
+ </div>
+ );
+ }
+
+ return (
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 p-4">
+ {attributesToShow.map((attr) => (
+ <Card key={attr.key} className={`${!attr.isMatching ? 'border-red-200' : 'border-green-200'}`}>
+ <CardContent className="p-3">
+ <div className="font-medium text-sm mb-2 truncate" title={attr.label}>
+ {attr.label}
+ {attr.uom && <span className="text-xs text-muted-foreground ml-1">({attr.uom})</span>}
+ </div>
+ {attr.isMatching ? (
+ <div className="text-sm">
+ <DisplayValue value={attr.localValue} uom={attr.uom} />
+ </div>
+ ) : (
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm">
+ <span className="text-xs text-muted-foreground min-w-[35px]">로컬:</span>
+ <span className="line-through text-red-500 flex-1">
+ <DisplayValue value={attr.localValue} uom={attr.uom} isSedp={false} />
+ </span>
+ </div>
+ <div className="flex items-center gap-2 text-sm">
+ <span className="text-xs text-muted-foreground min-w-[35px]">SEDP:</span>
+ <span className="text-green-500 flex-1">
+ <DisplayValue value={attr.sedpValue} uom={attr.uom} isSedp={true} />
+ </span>
+ </div>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ );
+ };
+
+
const [isLoading, setIsLoading] = React.useState(false);
const [comparisonResults, setComparisonResults] = React.useState<ComparisonResult[]>([]);
const [activeTab, setActiveTab] = React.useState("all");
@@ -181,7 +190,7 @@ export function SEDPCompareDialog({
// Filter and search results
const filteredResults = React.useMemo(() => {
let results = comparisonResults;
-
+
// Filter by tab
switch (activeTab) {
case "matching":
@@ -198,8 +207,8 @@ export function SEDPCompareDialog({
// Apply search filter
if (searchTerm.trim()) {
const search = searchTerm.toLowerCase();
- results = results.filter(r =>
- r.tagNo.toLowerCase().includes(search) ||
+ results = results.filter(r =>
+ r.tagNo.toLowerCase().includes(search) ||
r.tagDesc.toLowerCase().includes(search)
);
}
@@ -291,7 +300,7 @@ export function SEDPCompareDialog({
// Compare attributes
const attributeComparisons = columnsJSON
- .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC"&& col.key !== "status")
+ .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC" && col.key !== "status")
.map(col => {
const localValue = localItem[col.key];
const sedpValue = sedpItem.attributes.get(col.key);
@@ -370,7 +379,7 @@ export function SEDPCompareDialog({
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
- <DialogTitle className="mb-2">SEDP 데이터 비교</DialogTitle>
+ <DialogTitle className="mb-2">{t("dialogs.sedpDataComparison")}</DialogTitle>
<div className="flex items-center justify-between gap-2 pr-8">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
@@ -380,22 +389,22 @@ export function SEDPCompareDialog({
id="show-differences"
/>
<label htmlFor="show-differences" className="text-sm cursor-pointer">
- 차이가 있는 항목만 표시
+ {t("switches.showOnlyDifferences")}
</label>
</div>
-
+
{/* 검색 입력 */}
<div className="relative">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
- placeholder="태그 번호 또는 설명 검색..."
+ placeholder={t("placeholders.searchTagOrDesc")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 w-64"
/>
</div>
</div>
-
+
<div className="flex items-center gap-2">
<Badge variant={matchingTags === totalTags && totalMissingTags === 0 ? "default" : "destructive"}>
{matchingTags} / {totalTags} 일치 {totalMissingTags > 0 ? `(${totalMissingTags} 누락)` : ''}
@@ -411,7 +420,7 @@ export function SEDPCompareDialog({
) : (
<RefreshCw className="h-4 w-4" />
)}
- <span className="ml-2">새로고침</span>
+ <span className="ml-2">{t("buttons.refresh")}</span>
</Button>
</div>
</div>
@@ -424,11 +433,11 @@ export function SEDPCompareDialog({
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
<TabsList>
- <TabsTrigger value="all">전체 태그 ({totalTags})</TabsTrigger>
- <TabsTrigger value="differences">차이 있음 ({nonMatchingTags})</TabsTrigger>
- <TabsTrigger value="matching">일치함 ({matchingTags})</TabsTrigger>
+ <TabsTrigger value="all">{t("tabs.allTags")} ({totalTags})</TabsTrigger>
+ <TabsTrigger value="differences">{t("tabs.differences")} ({nonMatchingTags})</TabsTrigger>
+ <TabsTrigger value="matching">{t("tabs.matching")} ({matchingTags})</TabsTrigger>
<TabsTrigger value="missing" className={totalMissingTags > 0 ? "text-red-500" : ""}>
- 누락된 태그 ({totalMissingTags})
+ {t("tabs.missingTags")} ({totalMissingTags})
</TabsTrigger>
</TabsList>
@@ -436,19 +445,19 @@ export function SEDPCompareDialog({
{isLoading ? (
<div className="flex items-center justify-center h-full">
<Loader className="h-8 w-8 animate-spin mr-2" />
- <span>데이터 비교 중...</span>
+ <span>{t("messages.dataComparing")}</span>
</div>
) : activeTab === "missing" ? (
// Missing tags tab content
<div className="space-y-6">
{missingTags.localOnly.length > 0 && (
<div>
- <h3 className="text-sm font-medium mb-2">로컬에만 있는 태그 ({missingTags.localOnly.length})</h3>
+ <h3 className="text-sm font-medium mb-2">{t("sections.localOnlyTags")} ({missingTags.localOnly.length})</h3>
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
- <TableHead className="w-[180px]">Tag Number</TableHead>
- <TableHead>Tag Description</TableHead>
+ <TableHead className="w-[180px]">{t("labels.tagNo")}</TableHead>
+ <TableHead>{t("labels.description")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -465,12 +474,12 @@ export function SEDPCompareDialog({
{missingTags.sedpOnly.length > 0 && (
<div>
- <h3 className="text-sm font-medium mb-2">SEDP에만 있는 태그 ({missingTags.sedpOnly.length})</h3>
+ <h3 className="text-sm font-medium mb-2">{t("sections.sedpOnlyTags")} ({missingTags.sedpOnly.length})</h3>
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
- <TableHead className="w-[180px]">Tag Number</TableHead>
- <TableHead>Tag Description</TableHead>
+ <TableHead className="w-[180px]">{t("labels.tagNo")}</TableHead>
+ <TableHead>{t("labels.description")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -487,7 +496,7 @@ export function SEDPCompareDialog({
{totalMissingTags === 0 && (
<div className="flex items-center justify-center h-full text-muted-foreground">
- 모든 태그가 양쪽 시스템에 존재합니다
+ {t("messages.allTagsExistInBothSystems")}
</div>
)}
</div>
@@ -498,9 +507,9 @@ export function SEDPCompareDialog({
<TableHeader className="sticky top-0 bg-muted/50 z-10">
<TableRow>
<TableHead className="w-12"></TableHead>
- <TableHead className="w-[180px]">Tag Number</TableHead>
- <TableHead className="w-[250px]">Tag Description</TableHead>
- <TableHead className="w-[120px]">상태</TableHead>
+ <TableHead className="w-[180px]">{t("labels.tagNo")}</TableHead>
+ <TableHead className="w-[250px]">{t("labels.description")}</TableHead>
+ <TableHead className="w-[120px]">{t("labels.status")}</TableHead>
<TableHead>차이점 개수</TableHead>
</TableRow>
</TableHeader>
@@ -508,7 +517,7 @@ export function SEDPCompareDialog({
{filteredResults.map((result) => (
<React.Fragment key={result.tagNo}>
{/* 메인 행 */}
- <TableRow
+ <TableRow
className={`hover:bg-muted/20 cursor-pointer ${!result.isMatching ? "bg-muted/10" : ""}`}
onClick={() => toggleRowExpansion(result.tagNo)}
>
@@ -533,29 +542,29 @@ export function SEDPCompareDialog({
{result.isMatching ? (
<Badge variant="default" className="flex items-center gap-1">
<CheckCircle className="h-3 w-3" />
- <span>일치</span>
+ <span>{t("labels.matching")}</span>
</Badge>
) : (
<Badge variant="destructive" className="flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
- <span>차이</span>
+ <span>{t("labels.different")}</span>
</Badge>
)}
</TableCell>
<TableCell>
{!result.isMatching && (
<span className="text-sm text-muted-foreground">
- {result.attributes.filter(attr => !attr.isMatching).length}개 속성이 다름
+ {result.attributes.filter(attr => !attr.isMatching).length}{t("sections.differenceCount")}
</span>
)}
</TableCell>
</TableRow>
-
+
{/* 확장된 차이점 표시 행 */}
{expandedRows.has(result.tagNo) && (
<TableRow>
<TableCell colSpan={5} className="p-0 bg-muted/5">
- <DifferencesCard
+ <DifferencesCard
attributes={result.attributes}
columnLabelMap={columnLabelMap}
showOnlyDifferences={showOnlyDifferences}
@@ -570,7 +579,7 @@ export function SEDPCompareDialog({
</div>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">
- {searchTerm ? "검색 결과가 없습니다" : "현재 필터에 맞는 태그가 없습니다"}
+ {searchTerm ? t("messages.noSearchResults") : t("messages.noMatchingTags")}
</div>
)}
</TabsContent>
@@ -583,7 +592,7 @@ export function SEDPCompareDialog({
formCode={formCode}
disabled={isLoading || (nonMatchingTags === 0 && totalMissingTags === 0)}
/>
- <Button onClick={onClose}>닫기</Button>
+ <Button onClick={onClose}>{t("buttons.close")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
diff --git a/components/form-data/sedp-components.tsx b/components/form-data/sedp-components.tsx
index 4865e23d..869f730c 100644
--- a/components/form-data/sedp-components.tsx
+++ b/components/form-data/sedp-components.tsx
@@ -1,6 +1,8 @@
"use client";
import * as React from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -30,36 +32,40 @@ export function SEDPConfirmationDialog({
tagCount: number;
isLoading: boolean;
}) {
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
+
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
- <DialogTitle>Send Data to SEDP</DialogTitle>
+ <DialogTitle>{t("sedp.sendDataTitle")}</DialogTitle>
<DialogDescription>
- You are about to send form data to the Samsung Engineering Design Platform (SEDP).
+ {t("sedp.sendDataDescription")}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="grid grid-cols-2 gap-4 mb-4">
- <div className="text-muted-foreground">Form Name:</div>
+ <div className="text-muted-foreground">{t("sedp.formName")}:</div>
<div className="font-medium">{formName}</div>
- <div className="text-muted-foreground">Total Tags:</div>
+ <div className="text-muted-foreground">{t("sedp.totalTags")}:</div>
<div className="font-medium">{tagCount}</div>
</div>
<div className="bg-amber-50 p-3 rounded-md border border-amber-200 flex items-start gap-2">
<AlertTriangle className="h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="text-sm text-amber-800">
- Data sent to SEDP cannot be easily reverted. Please ensure all information is correct before proceeding.
+ {t("sedp.warningMessage")}
</div>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={onClose} disabled={isLoading}>
- Cancel
+ {t("buttons.cancel")}
</Button>
<Button
variant="samsung"
@@ -70,12 +76,12 @@ export function SEDPConfirmationDialog({
{isLoading ? (
<>
<Loader className="h-4 w-4 animate-spin" />
- Sending...
+ {t("sedp.sending")}
</>
) : (
<>
<Send className="h-4 w-4" />
- Send to SEDP
+ {t("sedp.sendToSEDP")}
</>
)}
</Button>
@@ -103,17 +109,31 @@ export function SEDPStatusDialog({
errorCount: number;
totalCount: number;
}) {
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
+
// Calculate percentage for the progress bar
const percentage = Math.round((successCount / totalCount) * 100);
+ const getStatusTitle = () => {
+ switch (status) {
+ case 'success':
+ return t("sedp.dataSentSuccessfully");
+ case 'partial':
+ return t("sedp.partiallySuccessful");
+ case 'error':
+ default:
+ return t("sedp.failedToSendData");
+ }
+ };
+
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>
- {status === 'success' ? 'Data Sent Successfully' :
- status === 'partial' ? 'Partially Successful' :
- 'Failed to Send Data'}
+ {getStatusTitle()}
</DialogTitle>
</DialogHeader>
@@ -141,20 +161,20 @@ export function SEDPStatusDialog({
{/* Progress Stats */}
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm">
- <span>Progress</span>
+ <span>{t("sedp.progress")}</span>
<span>{percentage}%</span>
</div>
<Progress value={percentage} className="h-2" />
<div className="flex justify-between text-sm pt-1">
<div>
<Badge variant="outline" className="bg-green-50 text-green-700 hover:bg-green-50">
- {successCount} Successful
+ {t("sedp.successfulCount", { count: successCount })}
</Badge>
</div>
{errorCount > 0 && (
<div>
<Badge variant="outline" className="bg-red-50 text-red-700 hover:bg-red-50">
- {errorCount} Failed
+ {t("sedp.failedCount", { count: errorCount })}
</Badge>
</div>
)}
@@ -164,7 +184,7 @@ export function SEDPStatusDialog({
<DialogFooter>
<Button onClick={onClose}>
- Close
+ {t("buttons.close")}
</Button>
</DialogFooter>
</DialogContent>
diff --git a/components/form-data/sedp-excel-download.tsx b/components/form-data/sedp-excel-download.tsx
index 24e1003d..36be4847 100644
--- a/components/form-data/sedp-excel-download.tsx
+++ b/components/form-data/sedp-excel-download.tsx
@@ -1,4 +1,6 @@
import * as React from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
import { Button } from "@/components/ui/button";
import { FileDown, Loader } from "lucide-react";
import { toast } from "sonner";
@@ -28,6 +30,9 @@ interface ExcelDownloadProps {
export function ExcelDownload({ comparisonResults, missingTags, formCode, disabled }: ExcelDownloadProps) {
const [isExporting, setIsExporting] = React.useState(false);
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
// Function to generate and download Excel file with differences
const handleExportDifferences = async () => {
@@ -39,7 +44,7 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
const hasMissingTags = missingTags.localOnly.length > 0 || missingTags.sedpOnly.length > 0;
if (itemsWithDifferences.length === 0 && !hasMissingTags) {
- toast.info("차이가 없어 다운로드할 내용이 없습니다");
+ toast.info(t("excelDownload.noDifferencesToDownload"));
return;
}
@@ -50,15 +55,15 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
// Add a worksheet for attribute differences
if (itemsWithDifferences.length > 0) {
- const worksheet = workbook.addWorksheet('속성 차이');
+ const worksheet = workbook.addWorksheet(t("excelDownload.attributeDifferencesSheet"));
// Add headers
worksheet.columns = [
- { header: 'Tag Number', key: 'tagNo', width: 20 },
- { header: 'Tag Description', key: 'tagDesc', width: 30 },
- { header: 'Attribute', key: 'attribute', width: 25 },
- { header: 'Local Value', key: 'localValue', width: 20 },
- { header: 'SEDP Value', key: 'sedpValue', width: 20 }
+ { header: t("excelDownload.tagNumber"), key: 'tagNo', width: 20 },
+ { header: t("excelDownload.tagDescription"), key: 'tagDesc', width: 30 },
+ { header: t("excelDownload.attribute"), key: 'attribute', width: 25 },
+ { header: t("excelDownload.localValue"), key: 'localValue', width: 20 },
+ { header: t("excelDownload.sedpValue"), key: 'sedpValue', width: 20 }
];
// Style the header row
@@ -90,12 +95,12 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
// Format local value with UOM
const localDisplay = diff.localValue === null || diff.localValue === undefined || diff.localValue === ''
- ? "(empty)"
+ ? t("excelDownload.emptyValue")
: diff.uom ? `${diff.localValue} ${diff.uom}` : diff.localValue;
// SEDP value is displayed as-is
const sedpDisplay = diff.sedpValue === null || diff.sedpValue === undefined || diff.sedpValue === ''
- ? "(empty)"
+ ? t("excelDownload.emptyValue")
: diff.sedpValue;
// Set cell values
@@ -127,13 +132,13 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
// Add a worksheet for missing tags if there are any
if (hasMissingTags) {
- const missingWorksheet = workbook.addWorksheet('누락된 태그');
+ const missingWorksheet = workbook.addWorksheet(t("excelDownload.missingTagsSheet"));
// Add headers
missingWorksheet.columns = [
- { header: 'Tag Number', key: 'tagNo', width: 20 },
- { header: 'Tag Description', key: 'tagDesc', width: 30 },
- { header: 'Status', key: 'status', width: 20 }
+ { header: t("excelDownload.tagNumber"), key: 'tagNo', width: 20 },
+ { header: t("excelDownload.tagDescription"), key: 'tagDesc', width: 30 },
+ { header: t("excelDownload.status"), key: 'status', width: 20 }
];
// Style the header row
@@ -160,7 +165,7 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
row.getCell('tagNo').value = tag.tagNo;
row.getCell('tagDesc').value = tag.tagDesc;
- row.getCell('status').value = '로컬에만 존재';
+ row.getCell('status').value = t("excelDownload.localOnlyStatus");
// Style the status cell
row.getCell('status').font = { color: { argb: 'FFFF8C00' } }; // Orange for local-only
@@ -187,7 +192,7 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
row.getCell('tagNo').value = tag.tagNo;
row.getCell('tagDesc').value = tag.tagDesc;
- row.getCell('status').value = 'SEDP에만 존재';
+ row.getCell('status').value = t("excelDownload.sedpOnlyStatus");
// Style the status cell
row.getCell('status').font = { color: { argb: 'FF0000FF' } }; // Blue for SEDP-only
@@ -214,7 +219,7 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
- a.download = `SEDP_차이점_${formCode}_${new Date().toISOString().slice(0, 10)}.xlsx`;
+ a.download = `${t("excelDownload.fileNamePrefix")}_${formCode}_${new Date().toISOString().slice(0, 10)}.xlsx`;
document.body.appendChild(a);
a.click();
@@ -222,10 +227,10 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
- toast.success("Excel 다운로드 완료");
+ toast.success(t("excelDownload.downloadComplete"));
} catch (error) {
console.error("Error exporting to Excel:", error);
- toast.error("Excel 다운로드 실패");
+ toast.error(t("excelDownload.downloadFailed"));
} finally {
setIsExporting(false);
}
@@ -248,7 +253,7 @@ export function ExcelDownload({ comparisonResults, missingTags, formCode, disabl
) : (
<FileDown className="h-4 w-4" />
)}
- 차이점 Excel로 다운로드
+ {t("excelDownload.downloadButtonText")}
</Button>
);
} \ No newline at end of file
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 1d0796fe..11d37911 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -119,7 +119,7 @@ export function TemplateViewDialog({
const [currentSpread, setCurrentSpread] = React.useState<any>(null);
const [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]);
const [isClient, setIsClient] = React.useState(false);
- const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null);
+ const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null>(null);
const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]);
const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]);
@@ -140,21 +140,35 @@ export function TemplateViewDialog({
templates = [templateData as TemplateItem];
}
- // CONTENT가 있는 템플릿들 필터링
+ // 유효한 템플릿들 필터링
const validTemplates = templates.filter(template => {
const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT;
const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT;
- const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM";
+ const hasGrdListSetup = template.GRD_LST_SETUP && columnsJSON.length > 0; // GRD_LIST 조건: GRD_LST_SETUP 존재 + columnsJSON 있음
+
+ const isValidType = template.TMPL_TYPE === "SPREAD_LIST" ||
+ template.TMPL_TYPE === "SPREAD_ITEM" ||
+ template.TMPL_TYPE === "GRD_LIST"; // GRD_LIST 타입 추가
- return isValidType && (hasSpreadListContent || hasSpreadItemContent);
+ return isValidType && (hasSpreadListContent || hasSpreadItemContent || hasGrdListSetup);
});
setAvailableTemplates(validTemplates);
// 첫 번째 유효한 템플릿을 기본으로 선택
if (validTemplates.length > 0 && !selectedTemplateId) {
- setSelectedTemplateId(validTemplates[0].TMPL_ID);
- setTemplateType(validTemplates[0].TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
+ const firstTemplate = validTemplates[0];
+ setSelectedTemplateId(firstTemplate.TMPL_ID);
+
+ // 템플릿 타입 결정
+ let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST';
+ if (firstTemplate.GRD_LST_SETUP && columnsJSON.length > 0) {
+ templateTypeToSet = 'GRD_LIST';
+ } else {
+ templateTypeToSet = firstTemplate.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM';
+ }
+
+ setTemplateType(templateTypeToSet);
}
}, [templateData, selectedTemplateId]);
@@ -163,7 +177,16 @@ export function TemplateViewDialog({
const template = availableTemplates.find(t => t.TMPL_ID === templateId);
if (template) {
setSelectedTemplateId(templateId);
- setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
+
+ // 템플릿 타입 결정
+ let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST';
+ if (template.GRD_LST_SETUP && columnsJSON.length > 0) {
+ templateTypeToSet = 'GRD_LIST';
+ } else {
+ templateTypeToSet = template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM';
+ }
+
+ setTemplateType(templateTypeToSet);
setHasChanges(false);
setValidationErrors([]);
@@ -192,8 +215,8 @@ export function TemplateViewDialog({
return editableFieldsMap.get(selectedRow.TAG_NO) || [];
}
- // SPREAD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리
- if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
+ // SPREAD_LIST 또는 GRD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리
+ if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) {
const firstRowTagNo = tableData[0]?.TAG_NO;
if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) {
return editableFieldsMap.get(firstRowTagNo) || [];
@@ -221,8 +244,8 @@ export function TemplateViewDialog({
return editableFields.includes(attId);
}
- // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려
- if (templateType === 'SPREAD_LIST') {
+ // SPREAD_LIST 또는 GRD_LIST인 경우: 개별 행의 편집 가능성도 고려
+ if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') {
// 기본적으로 editableFields에 포함되어야 함
if (!editableFields.includes(attId)) {
return false;
@@ -236,8 +259,6 @@ export function TemplateViewDialog({
return true;
}
- // 기본적으로는 editableFields 체크
- // return editableFields.includes(attId);
return true;
}, [templateType, columnsJSON, editableFields]);
@@ -266,6 +287,17 @@ export function TemplateViewDialog({
return { row, col };
};
+ // 행과 열을 셀 주소로 변환하는 함수 (GRD_LIST용)
+ const getCellAddress = (row: number, col: number): string => {
+ let colStr = '';
+ let colNum = col;
+ while (colNum >= 0) {
+ colStr = String.fromCharCode((colNum % 26) + 65) + colStr;
+ colNum = Math.floor(colNum / 26) - 1;
+ }
+ return colStr + (row + 1);
+ };
+
// 데이터 타입 검증 함수
const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => {
if (value === undefined || value === null || value === "") {
@@ -322,7 +354,7 @@ export function TemplateViewDialog({
message: errorMessage
});
}
- } else if (templateType === 'SPREAD_LIST') {
+ } else if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') {
// 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴
const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
@@ -359,164 +391,282 @@ export function TemplateViewDialog({
return style;
}, []);
- // 📋 최적화된 LIST 드롭다운 설정
-const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
- try {
- console.log(`🎯 Setting up DataValidation dropdown for ${rowCount} rows with options:`, options);
-
- // 🚨 성능 임계점 확인
- if (rowCount > 100) {
- console.warn(`⚡ Large dataset (${rowCount} rows): Using simple validation only`);
- setupSimpleValidation(activeSheet, cellPos, options, rowCount);
- return;
- }
-
- // ✅ 1단계: options 철저하게 정규화 (이것이 에러 방지의 핵심!)
- let safeOptions;
+ // 🎯 드롭다운 설정
+ const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
try {
- safeOptions = options
- .filter(opt => opt !== null && opt !== undefined && opt !== '') // null, undefined, 빈값 제거
- .map(opt => {
- // 모든 값을 안전한 문자열로 변환
- let str = String(opt);
- // 특수 문자나 문제 있는 문자 처리
- str = str.replace(/[\r\n\t]/g, ' '); // 줄바꿈, 탭을 공백으로
- str = str.replace(/[^\x20-\x7E\u00A1-\uFFFF]/g, ''); // 제어 문자 제거
- return str.trim();
- })
- .filter(opt => opt.length > 0 && opt.length < 255) // 빈값과 너무 긴 값 제거
- .filter((opt, index, arr) => arr.indexOf(opt) === index) // 중복 제거
- .slice(0, 100); // 최대 100개로 제한
-
- console.log(`📋 Original options:`, options);
- console.log(`📋 Safe options:`, safeOptions);
- } catch (filterError) {
- console.error('❌ Options filtering failed:', filterError);
- safeOptions = ['Option1', 'Option2']; // 안전한 폴백 옵션
- }
+ console.log(`🎯 Setting up dropdown for ${rowCount} rows with options:`, options);
+
+ // ✅ options 정규화
+ const safeOptions = options
+ .filter(opt => opt !== null && opt !== undefined && opt !== '')
+ .map(opt => String(opt).trim())
+ .filter(opt => opt.length > 0)
+ .filter((opt, index, arr) => arr.indexOf(opt) === index)
+ .slice(0, 20);
+
+ if (safeOptions.length === 0) {
+ console.warn(`⚠️ No valid options found, skipping`);
+ return;
+ }
- if (safeOptions.length === 0) {
- console.warn(`⚠️ No valid options found, using fallback`);
- safeOptions = ['Please Select'];
- }
+ console.log(`📋 Safe options:`, safeOptions);
- // ✅ 2단계: DataValidation 생성 (엑셀 스타일 드롭다운)
- let validator;
- try {
- validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions);
- console.log(`✅ DataValidation validator created successfully`);
- } catch (validatorError) {
- console.error('❌ Failed to create validator:', validatorError);
- return;
- }
+ // ✅ DataValidation용 문자열 준비
+ const optionsString = safeOptions.join(',');
- // ✅ 3단계: 셀/범위에 적용
- try {
- if (rowCount > 1) {
- // 범위에 적용
- const range = activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1);
- range.dataValidator(validator);
- console.log(`✅ DataValidation applied to range [${cellPos.row}, ${cellPos.col}, ${rowCount}, 1]`);
- } else {
- // 단일 셀에 적용
- activeSheet.setDataValidator(cellPos.row, cellPos.col, validator);
- console.log(`✅ DataValidation applied to single cell [${cellPos.row}, ${cellPos.col}]`);
- }
- } catch (applicationError) {
- console.error('❌ Failed to apply DataValidation:', applicationError);
-
- // 폴백: 개별 셀에 하나씩 적용
- console.log('🔄 Trying individual cell application...');
- for (let i = 0; i < Math.min(rowCount, 50); i++) {
+ // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성!
+ for (let i = 0; i < rowCount; i++) {
try {
- const individualValidator = GC.Spread.Sheets.DataValidation.createListValidator([...safeOptions]);
- activeSheet.setDataValidator(cellPos.row + i, cellPos.col, individualValidator);
- console.log(`✅ Individual DataValidation set for row ${cellPos.row + i}`);
- } catch (individualError) {
- console.warn(`⚠️ Failed individual cell ${cellPos.row + i}:`, individualError);
+ const targetRow = cellPos.row + i;
+
+ // ✅ 각 셀마다 새로운 ComboBox 인스턴스 생성
+ const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox();
+ comboBoxCellType.items(safeOptions); // 배열로 전달
+ comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
+
+ // ✅ 각 셀마다 새로운 DataValidation 인스턴스 생성
+ const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString);
+
+ // ComboBox + DataValidation 둘 다 적용
+ activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType);
+ activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator);
+
+ // 셀 잠금 해제
+ const cell = activeSheet.getCell(targetRow, cellPos.col);
+ cell.locked(false);
+
+ console.log(`✅ Individual dropdown applied to [${targetRow}, ${cellPos.col}]`);
+
+ } catch (cellError) {
+ console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError);
}
}
- }
- console.log(`✅ DataValidation dropdown setup completed`);
+ console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`);
- } catch (error) {
- console.error('❌ DataValidation setup failed completely:', error);
- console.error('Error stack:', error.stack);
- console.log('🔄 Falling back to no validation');
- }
-}, []);
+ } catch (error) {
+ console.error('❌ Dropdown setup failed:', error);
+ }
+ }, []);
+
+ // 🚀 행 용량 확보
+ const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => {
+ const currentRowCount = activeSheet.getRowCount();
+ if (requiredRowCount > currentRowCount) {
+ activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가
+ console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`);
+ }
+ }, []);
-// ⚡ 단순 검증 설정 (드롭다운 없이 검증만)
-const setupSimpleValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
- try {
- console.log(`⚡ Setting up simple validation (no dropdown UI) for ${rowCount} rows`);
+ // 🆕 GRD_LIST용 동적 테이블 생성 함수
+ const createGrdListTable = React.useCallback((activeSheet: any, template: TemplateItem) => {
+ console.log('🏗️ Creating GRD_LIST table');
- const safeOptions = options
- .filter(opt => opt != null && opt !== '')
- .map(opt => String(opt).trim())
- .filter(opt => opt.length > 0);
+ // columnsJSON의 visible 컬럼들을 seq 순서로 정렬하여 사용
+ const visibleColumns = columnsJSON
+ .filter(col => col.hidden !== true) // hidden이 true가 아닌 것들만
+ .sort((a, b) => {
+ // seq가 없는 경우 999999로 처리하여 맨 뒤로 보냄
+ const seqA = a.seq !== undefined ? a.seq : 999999;
+ const seqB = b.seq !== undefined ? b.seq : 999999;
+ return seqA - seqB;
+ });
- if (safeOptions.length === 0) return;
+ console.log('📊 Using columns:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`));
- const validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions);
+ if (visibleColumns.length === 0) {
+ console.warn('❌ No visible columns found in columnsJSON');
+ return [];
+ }
+
+ // 테이블 생성 시작
+ const mappings: CellMapping[] = [];
+ const startCol = 1; // A열 제외하고 B열부터 시작
- // 범위로 적용 시도
- try {
- activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1).dataValidator(validator);
- console.log(`✅ Simple validation applied to range`);
- } catch (rangeError) {
- console.warn('Range validation failed, trying individual cells');
- // 폴백: 개별 적용
- for (let i = 0; i < Math.min(rowCount, 100); i++) {
- try {
- activeSheet.setDataValidator(cellPos.row + i, cellPos.col, validator);
- } catch (individualError) {
- // 개별 실패해도 계속
+ // 🔍 그룹 헤더 분석
+ const groupInfo = analyzeColumnGroups(visibleColumns);
+ const hasGroups = groupInfo.groups.length > 0;
+
+ // 헤더 행 계산: 그룹이 있으면 2행, 없으면 1행
+ const groupHeaderRow = 0;
+ const columnHeaderRow = hasGroups ? 1 : 0;
+ const dataStartRow = hasGroups ? 2 : 1;
+
+ // 🎨 헤더 스타일 생성
+ const groupHeaderStyle = new GC.Spread.Sheets.Style();
+ groupHeaderStyle.backColor = "#1e40af"; // 더 진한 파란색
+ groupHeaderStyle.foreColor = "#ffffff";
+ groupHeaderStyle.font = "bold 13px Arial";
+ groupHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center;
+ groupHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center;
+
+ const columnHeaderStyle = new GC.Spread.Sheets.Style();
+ columnHeaderStyle.backColor = "#3b82f6"; // 기본 파란색
+ columnHeaderStyle.foreColor = "#ffffff";
+ columnHeaderStyle.font = "bold 12px Arial";
+ columnHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center;
+ columnHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center;
+
+ let currentCol = startCol;
+
+ // 🏗️ 그룹 헤더 및 컬럼 헤더 생성
+ if (hasGroups) {
+ // 그룹 헤더가 있는 경우
+ groupInfo.groups.forEach(group => {
+ if (group.isGroup) {
+ // 그룹 헤더 생성 및 병합
+ const groupStartCol = currentCol;
+ const groupEndCol = currentCol + group.columns.length - 1;
+
+ // 그룹 헤더 셀 설정
+ const groupHeaderCell = activeSheet.getCell(groupHeaderRow, groupStartCol);
+ groupHeaderCell.value(group.head);
+
+ // 그룹 헤더 병합
+ if (group.columns.length > 1) {
+ activeSheet.addSpan(groupHeaderRow, groupStartCol, 1, group.columns.length);
+ }
+
+ // 그룹 헤더 스타일 적용
+ for (let col = groupStartCol; col <= groupEndCol; col++) {
+ activeSheet.setStyle(groupHeaderRow, col, groupHeaderStyle);
+ activeSheet.getCell(groupHeaderRow, col).locked(true);
+ }
+
+ console.log(`📝 Group Header [${groupHeaderRow}, ${groupStartCol}-${groupEndCol}]: ${group.head}`);
+
+ // 그룹 내 개별 컬럼 헤더 생성
+ group.columns.forEach((column, index) => {
+ const colIndex = groupStartCol + index;
+ const columnHeaderCell = activeSheet.getCell(columnHeaderRow, colIndex);
+ columnHeaderCell.value(column.label);
+ activeSheet.setStyle(columnHeaderRow, colIndex, columnHeaderStyle);
+ columnHeaderCell.locked(true);
+
+ console.log(`📝 Column Header [${columnHeaderRow}, ${colIndex}]: ${column.label}`);
+ });
+
+ currentCol += group.columns.length;
+ } else {
+ // 그룹이 아닌 단일 컬럼
+ const column = group.columns[0];
+
+ // 그룹 헤더 행에는 빈 셀 (개별 컬럼이므로)
+ const groupHeaderCell = activeSheet.getCell(groupHeaderRow, currentCol);
+ groupHeaderCell.value("");
+ activeSheet.setStyle(groupHeaderRow, currentCol, groupHeaderStyle);
+ groupHeaderCell.locked(true);
+
+ // 컬럼 헤더 생성
+ const columnHeaderCell = activeSheet.getCell(columnHeaderRow, currentCol);
+ columnHeaderCell.value(column.label);
+ activeSheet.setStyle(columnHeaderRow, currentCol, columnHeaderStyle);
+ columnHeaderCell.locked(true);
+
+ console.log(`📝 Single Column [${columnHeaderRow}, ${currentCol}]: ${column.label}`);
+ currentCol++;
}
- }
+ });
+ } else {
+ // 그룹이 없는 경우 - 기존 로직
+ visibleColumns.forEach((column, colIndex) => {
+ const headerCol = startCol + colIndex;
+ const headerCell = activeSheet.getCell(columnHeaderRow, headerCol);
+ headerCell.value(column.label);
+ activeSheet.setStyle(columnHeaderRow, headerCol, columnHeaderStyle);
+ headerCell.locked(true);
+
+ console.log(`📝 Header [${columnHeaderRow}, ${headerCol}]: ${column.label}`);
+ });
}
-
- } catch (error) {
- console.error('❌ Simple validation failed:', error);
- }
-}, []);
-
-// ═══════════════════════════════════════════════════════════════════════════════
-// 🔍 디버깅용: 에러 발생 원인 추적
-// ═══════════════════════════════════════════════════════════════════════════════
-
-const debugDropdownError = (options: any[], attId: string) => {
- console.group(`🔍 Debugging dropdown for ${attId}`);
-
- console.log('Original options type:', typeof options);
- console.log('Is array:', Array.isArray(options));
- console.log('Length:', options?.length);
- console.log('Raw options:', options);
-
- if (Array.isArray(options)) {
- options.forEach((opt, index) => {
- console.log(`[${index}] Type: ${typeof opt}, Value: "${opt}", String: "${String(opt)}"`);
+
+ // 데이터 행 생성
+ const dataRowCount = tableData.length;
+ ensureRowCapacity(activeSheet, dataStartRow + dataRowCount);
+
+ tableData.forEach((rowData, rowIndex) => {
+ const targetRow = dataStartRow + rowIndex;
- // 문제 있는 값 체크
- if (opt === null) console.warn(` ⚠️ NULL value at index ${index}`);
- if (opt === undefined) console.warn(` ⚠️ UNDEFINED value at index ${index}`);
- if (typeof opt === 'object') console.warn(` ⚠️ OBJECT value at index ${index}:`, opt);
- if (typeof opt === 'string' && opt.includes('\n')) console.warn(` ⚠️ NEWLINE in string at index ${index}`);
- if (typeof opt === 'string' && opt.length === 0) console.warn(` ⚠️ EMPTY STRING at index ${index}`);
+ visibleColumns.forEach((column, colIndex) => {
+ const targetCol = startCol + colIndex;
+ const cellAddress = getCellAddress(targetRow, targetCol);
+ const isEditable = isFieldEditable(column.key, rowData);
+
+ // 매핑 정보 추가
+ mappings.push({
+ attId: column.key,
+ cellAddress: cellAddress,
+ isEditable: isEditable,
+ dataRowIndex: rowIndex
+ });
+
+ // 셀 값 설정
+ const cell = activeSheet.getCell(targetRow, targetCol);
+ const value = rowData[column.key];
+ cell.value(value ?? null);
+
+ // 스타일 적용
+ const style = createCellStyle(isEditable);
+ activeSheet.setStyle(targetRow, targetCol, style);
+ cell.locked(!isEditable);
+
+ console.log(`📝 Data [${targetRow}, ${targetCol}]: ${column.key} = "${value}" (${isEditable ? 'Editable' : 'ReadOnly'})`);
+
+ // LIST 타입 드롭다운 설정
+ if (column.type === "LIST" && column.options && isEditable) {
+ setupOptimizedListValidation(activeSheet, { row: targetRow, col: targetCol }, column.options, 1);
+ }
+ });
});
- }
-
- console.groupEnd();
-};
- // 🚀 행 용량 확보
- const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => {
- const currentRowCount = activeSheet.getRowCount();
- if (requiredRowCount > currentRowCount) {
- activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가
- console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`);
+ console.log(`🏗️ GRD_LIST table created with ${mappings.length} mappings, hasGroups: ${hasGroups}`);
+ return mappings;
+ }, [tableData, columnsJSON, isFieldEditable, createCellStyle, ensureRowCapacity, setupOptimizedListValidation]);
+
+ // 🔍 컬럼 그룹 분석 함수
+ const analyzeColumnGroups = React.useCallback((columns: DataTableColumnJSON[]) => {
+ const groups: Array<{
+ head: string;
+ isGroup: boolean;
+ columns: DataTableColumnJSON[];
+ }> = [];
+
+ let i = 0;
+ while (i < columns.length) {
+ const currentCol = columns[i];
+
+ // head가 없거나 빈 문자열인 경우 단일 컬럼으로 처리
+ if (!currentCol.head || !currentCol.head.trim()) {
+ groups.push({
+ head: '',
+ isGroup: false,
+ columns: [currentCol]
+ });
+ i++;
+ continue;
+ }
+
+ // 같은 head를 가진 연속된 컬럼들을 찾기
+ const groupHead = currentCol.head.trim();
+ const groupColumns: DataTableColumnJSON[] = [currentCol];
+ let j = i + 1;
+
+ while (j < columns.length && columns[j].head && columns[j].head.trim() === groupHead) {
+ groupColumns.push(columns[j]);
+ j++;
+ }
+
+ // 그룹 추가
+ groups.push({
+ head: groupHead,
+ isGroup: groupColumns.length > 1,
+ columns: groupColumns
+ });
+
+ i = j; // 다음 그룹으로 이동
}
+
+ return { groups };
}, []);
// 🛡️ 시트 보호 및 이벤트 설정
@@ -574,8 +724,8 @@ const debugDropdownError = (options: any[], attId: string) => {
return;
}
- // SPREAD_LIST 개별 행 SHI 확인
- if (templateType === 'SPREAD_LIST' && exactMapping.dataRowIndex !== undefined) {
+ // SPREAD_LIST 또는 GRD_LIST 개별 행 SHI 확인
+ if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && exactMapping.dataRowIndex !== undefined) {
const dataRowIndex = exactMapping.dataRowIndex;
console.log(`🔍 Checking SHI for data row ${dataRowIndex}`);
@@ -663,149 +813,167 @@ const debugDropdownError = (options: any[], attId: string) => {
setHasChanges(false);
setValidationErrors([]);
- // 📋 템플릿 콘텐츠 및 데이터 시트 추출
- let contentJson = null;
- let dataSheets = null;
-
- // SPR_LST_SETUP.CONTENT 우선 사용
- if (workingTemplate.SPR_LST_SETUP?.CONTENT) {
- contentJson = workingTemplate.SPR_LST_SETUP.CONTENT;
- dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS;
- console.log('✅ Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
- }
- // SPR_ITM_LST_SETUP.CONTENT 대안 사용
- else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) {
- contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT;
- dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS;
- console.log('✅ Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
- }
-
- if (!contentJson) {
- console.warn('❌ No CONTENT found in template:', workingTemplate.NAME);
- return;
- }
-
- // 🏗️ SpreadSheets 초기화
- const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
-
// 성능을 위한 렌더링 일시 중단
spread.suspendPaint();
try {
- // 템플릿 구조 로드
- spread.fromJSON(jsonData);
const activeSheet = spread.getActiveSheet();
// 시트 보호 해제 (편집을 위해)
activeSheet.options.isProtected = false;
- // 📊 셀 매핑 및 데이터 처리
- if (dataSheets && dataSheets.length > 0) {
- const mappings: CellMapping[] = [];
+ let mappings: CellMapping[] = [];
+
+ // 🆕 GRD_LIST 처리
+ if (templateType === 'GRD_LIST' && workingTemplate.GRD_LST_SETUP) {
+ console.log('🏗️ Processing GRD_LIST template');
- // 🔄 각 데이터 시트의 매핑 정보 처리
- dataSheets.forEach(dataSheet => {
- if (dataSheet.MAP_CELL_ATT) {
- dataSheet.MAP_CELL_ATT.forEach(mapping => {
- const { ATT_ID, IN } = mapping;
-
- if (IN && IN.trim() !== "") {
- const cellPos = parseCellAddress(IN);
- if (cellPos) {
- const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
-
- // 🎯 템플릿 타입별 데이터 처리
- if (templateType === 'SPREAD_ITEM' && selectedRow) {
- // 📝 단일 행 처리 (SPREAD_ITEM)
- const isEditable = isFieldEditable(ATT_ID);
-
- // 매핑 정보 저장
- mappings.push({
- attId: ATT_ID,
- cellAddress: IN,
- isEditable: isEditable,
- dataRowIndex: 0
- });
-
- const cell = activeSheet.getCell(cellPos.row, cellPos.col);
- const value = selectedRow[ATT_ID];
-
- // 값 설정
- cell.value(value ?? null);
-
- // 🎨 스타일 및 편집 권한 설정
- cell.locked(!isEditable);
- const style = createCellStyle(isEditable);
- activeSheet.setStyle(cellPos.row, cellPos.col, style);
-
- // 📋 LIST 타입 드롭다운 설정
- if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
- setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1);
- }
+ // 기본 워크북 설정
+ spread.clearSheets();
+ spread.addSheet(0);
+ const sheet = spread.getSheet(0);
+ sheet.name('Data');
+ spread.setActiveSheet('Data');
+
+ // 동적 테이블 생성
+ mappings = createGrdListTable(sheet, workingTemplate);
+
+ } else {
+ // 기존 SPREAD_LIST 및 SPREAD_ITEM 처리
+ let contentJson = null;
+ let dataSheets = null;
+
+ // SPR_LST_SETUP.CONTENT 우선 사용
+ if (workingTemplate.SPR_LST_SETUP?.CONTENT) {
+ contentJson = workingTemplate.SPR_LST_SETUP.CONTENT;
+ dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS;
+ console.log('✅ Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
+ }
+ // SPR_ITM_LST_SETUP.CONTENT 대안 사용
+ else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) {
+ contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT;
+ dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS;
+ console.log('✅ Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
+ }
- } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
- // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨
- console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`);
-
- // 🚀 행 확장 (필요시)
- ensureRowCapacity(activeSheet, cellPos.row + tableData.length);
-
- // 📋 각 행마다 개별 매핑 생성
- tableData.forEach((rowData, index) => {
- const targetRow = cellPos.row + index;
- const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`;
- const cellEditable = isFieldEditable(ATT_ID, rowData);
+ if (!contentJson) {
+ console.warn('❌ No CONTENT found in template:', workingTemplate.NAME);
+ return;
+ }
+
+ // 🏗️ SpreadSheets 초기화
+ const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
+
+ // 템플릿 구조 로드
+ spread.fromJSON(jsonData);
+
+ // 📊 셀 매핑 및 데이터 처리
+ if (dataSheets && dataSheets.length > 0) {
+
+ // 🔄 각 데이터 시트의 매핑 정보 처리
+ dataSheets.forEach(dataSheet => {
+ if (dataSheet.MAP_CELL_ATT) {
+ dataSheet.MAP_CELL_ATT.forEach(mapping => {
+ const { ATT_ID, IN } = mapping;
+
+ if (IN && IN.trim() !== "") {
+ const cellPos = parseCellAddress(IN);
+ if (cellPos) {
+ const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
+
+ // 🎯 템플릿 타입별 데이터 처리
+ if (templateType === 'SPREAD_ITEM' && selectedRow) {
+ // 📝 단일 행 처리 (SPREAD_ITEM)
+ const isEditable = isFieldEditable(ATT_ID);
- // 개별 매핑 추가
+ // 매핑 정보 저장
mappings.push({
attId: ATT_ID,
- cellAddress: targetCellAddress, // 각 행마다 다른 주소
- isEditable: cellEditable,
- dataRowIndex: index // 원본 데이터 인덱스
+ cellAddress: IN,
+ isEditable: isEditable,
+ dataRowIndex: 0
});
-
- console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`);
- });
-
- // 📋 LIST 타입 드롭다운 설정 (조건부)
- if (columnConfig?.type === "LIST" && columnConfig.options) {
- // 편집 가능한 행이 하나라도 있으면 드롭다운 설정
- const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData));
- if (hasEditableRows) {
- setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
- }
- }
- // 🎨 개별 셀 데이터 및 스타일 설정
- tableData.forEach((rowData, index) => {
- const targetRow = cellPos.row + index;
- const cell = activeSheet.getCell(targetRow, cellPos.col);
- const value = rowData[ATT_ID];
+ const cell = activeSheet.getCell(cellPos.row, cellPos.col);
+ const value = selectedRow[ATT_ID];
// 값 설정
cell.value(value ?? null);
- console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`);
+
+ // 🎨 스타일 및 편집 권한 설정
+ cell.locked(!isEditable);
+ const style = createCellStyle(isEditable);
+ activeSheet.setStyle(cellPos.row, cellPos.col, style);
+
+ // 📋 LIST 타입 드롭다운 설정
+ if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
+ setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1);
+ }
+
+ } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
+ // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨
+ console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`);
- // 편집 권한 및 스타일 설정
- const cellEditable = isFieldEditable(ATT_ID, rowData);
- cell.locked(!cellEditable);
- const style = createCellStyle(cellEditable);
- activeSheet.setStyle(targetRow, cellPos.col, style);
- });
- }
+ // 🚀 행 확장 (필요시)
+ ensureRowCapacity(activeSheet, cellPos.row + tableData.length);
+
+ // 📋 각 행마다 개별 매핑 생성
+ tableData.forEach((rowData, index) => {
+ const targetRow = cellPos.row + index;
+ const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`;
+ const cellEditable = isFieldEditable(ATT_ID, rowData);
+
+ // 개별 매핑 추가
+ mappings.push({
+ attId: ATT_ID,
+ cellAddress: targetCellAddress, // 각 행마다 다른 주소
+ isEditable: cellEditable,
+ dataRowIndex: index // 원본 데이터 인덱스
+ });
+
+ console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`);
+ });
- console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`);
- }
- }
- });
- }
- });
+ // 📋 LIST 타입 드롭다운 설정 (조건부)
+ if (columnConfig?.type === "LIST" && columnConfig.options) {
+ // 편집 가능한 행이 하나라도 있으면 드롭다운 설정
+ const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData));
+ if (hasEditableRows) {
+ setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
+ }
+ }
- // 💾 매핑 정보 저장 및 이벤트 설정
- setCellMappings(mappings);
- setupSheetProtectionAndEvents(activeSheet, mappings);
+ // 🎨 개별 셀 데이터 및 스타일 설정
+ tableData.forEach((rowData, index) => {
+ const targetRow = cellPos.row + index;
+ const cell = activeSheet.getCell(targetRow, cellPos.col);
+ const value = rowData[ATT_ID];
+
+ // 값 설정
+ cell.value(value ?? null);
+ // console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`);
+
+ // 편집 권한 및 스타일 설정
+ const cellEditable = isFieldEditable(ATT_ID, rowData);
+ cell.locked(!cellEditable);
+ const style = createCellStyle(cellEditable);
+ activeSheet.setStyle(targetRow, cellPos.col, style);
+ });
+ }
+
+ // console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`);
+ }
+ }
+ });
+ }
+ });
+ }
}
+ // 💾 매핑 정보 저장 및 이벤트 설정
+ setCellMappings(mappings);
+ setupSheetProtectionAndEvents(activeSheet, mappings);
+
} finally {
// 렌더링 재개
spread.resumePaint();
@@ -818,7 +986,7 @@ const debugDropdownError = (options: any[], attId: string) => {
spread.resumePaint();
}
}
- }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]);
+ }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents, createGrdListTable]);
// 변경사항 저장 함수
const handleSaveChanges = React.useCallback(async () => {
@@ -869,8 +1037,8 @@ const debugDropdownError = (options: any[], attId: string) => {
toast.success("Changes saved successfully!");
onUpdateSuccess?.(dataToSave);
- } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
- // 복수 행 저장
+ } else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) {
+ // 복수 행 저장 (SPREAD_LIST와 GRD_LIST 동일 처리)
const updatedRows: GenericData[] = [];
let saveCount = 0;
@@ -966,7 +1134,11 @@ const debugDropdownError = (options: any[], attId: string) => {
<SelectContent>
{availableTemplates.map(template => (
<SelectItem key={template.TMPL_ID} value={template.TMPL_ID}>
- {template.NAME} ({template.TMPL_TYPE})
+ {template.NAME} ({
+ template.GRD_LST_SETUP && columnsJSON.length > 0
+ ? 'GRD_LIST'
+ : template.TMPL_TYPE
+ })
</SelectItem>
))}
</SelectContent>
@@ -978,12 +1150,16 @@ const debugDropdownError = (options: any[], attId: string) => {
{selectedTemplate && (
<div className="flex items-center gap-4 text-sm">
<span className="font-medium text-blue-600">
- Template Type: {selectedTemplate.TMPL_TYPE === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : 'Item View (SPREAD_ITEM)'}
+ Template Type: {
+ templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' :
+ templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' :
+ 'Grid List View (GRD_LIST)'
+ }
</span>
{templateType === 'SPREAD_ITEM' && selectedRow && (
<span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span>
)}
- {templateType === 'SPREAD_LIST' && (
+ {(templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && (
<span>• {dataCount} rows</span>
)}
{hasChanges && (
@@ -1028,7 +1204,7 @@ const debugDropdownError = (options: any[], attId: string) => {
<div className="flex-1 overflow-hidden">
{selectedTemplate && isClient && isDataValid ? (
<SpreadSheets
- key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
+ key={`${templateType}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
workbookInitialized={initSpread}
hostStyle={hostStyle}
/>
diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx
index 5666a116..91cb7a07 100644
--- a/components/form-data/update-form-sheet.tsx
+++ b/components/form-data/update-form-sheet.tsx
@@ -45,6 +45,8 @@ import {
import { DataTableColumnJSON } from "./form-data-table-columns";
import { updateFormDataInDB } from "@/lib/forms/services";
import { cn } from "@/lib/utils";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
/** =============================================================
* 🔄 UpdateTagSheet with grouped fields by `head` property
@@ -86,6 +88,10 @@ export function UpdateTagSheet({
const [isPending, startTransition] = React.useTransition();
const router = useRouter();
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
+
/* ----------------------------------------------------------------
* 1️⃣ Editable‑field helpers (unchanged)
* --------------------------------------------------------------*/
@@ -105,17 +111,17 @@ export function UpdateTagSheet({
const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => !isFieldEditable(column), [isFieldEditable]);
const getReadOnlyReason = React.useCallback((column: DataTableColumnJSON) => {
- if (column.shi) return "SHI‑only field (managed by SHI system)";
+ if (column.shi) return t("updateTagSheet.readOnlyReasons.shiOnly");
if (column.key !== "TAG_NO" && column.key !== "TAG_DESC") {
if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) {
- return "No editable fields information for this TAG";
+ return t("updateTagSheet.readOnlyReasons.noEditableFields");
}
if (!editableFields.includes(column.key)) {
- return "Not editable for this TAG class";
+ return t("updateTagSheet.readOnlyReasons.notEditableForTag");
}
}
- return "Read‑only field";
- }, [rowData?.TAG_NO, editableFieldsMap, editableFields]);
+ return t("updateTagSheet.readOnlyReasons.readOnly");
+ }, [rowData?.TAG_NO, editableFieldsMap, editableFields, t]);
/* ----------------------------------------------------------------
* 2️⃣ Zod dynamic schema & form state (unchanged)
@@ -220,7 +226,7 @@ export function UpdateTagSheet({
return;
}
- toast.success("Updated successfully!");
+ toast.success(t("updateTagSheet.messages.updateSuccess"));
const updatedData = { ...rowData, ...finalValues, TAG_NO: rowData?.TAG_NO };
onUpdateSuccess?.(updatedData);
@@ -228,7 +234,7 @@ export function UpdateTagSheet({
onOpenChange(false);
} catch (error) {
console.error("Error updating form data:", error);
- toast.error("An unexpected error occurred while updating");
+ toast.error(t("updateTagSheet.messages.updateError"));
}
});
}
@@ -243,15 +249,18 @@ export function UpdateTagSheet({
<SheetContent className="sm:max-w-xl md:max-w-3xl lg:max-w-4xl xl:max-w-5xl flex flex-col">
<SheetHeader className="text-left">
<SheetTitle>
- Update Row – {rowData?.TAG_NO || "Unknown TAG"}
+ {t("updateTagSheet.title")} – {rowData?.TAG_NO || t("updateTagSheet.unknownTag")}
</SheetTitle>
<SheetDescription>
- Modify the fields below and save changes. Fields with
- <LockIcon className="inline h-3 w-3 mx-1" /> are read‑only.
+ {t("updateTagSheet.description")}
+ <LockIcon className="inline h-3 w-3 mx-1" />
+ {t("updateTagSheet.readOnlyIndicator")}
<br />
<span className="text-sm text-green-600">
- {editableFieldCount} of {columns.length} fields are editable for
- this TAG.
+ {t("updateTagSheet.editableFieldsCount", {
+ editableCount: editableFieldCount,
+ totalCount: columns.length
+ })}
</span>
</SheetDescription>
</SheetHeader>
@@ -303,7 +312,7 @@ export function UpdateTagSheet({
value={field.value ?? ""}
className={cn(
isReadOnly &&
- "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300",
+ "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300",
)}
/>
</FormControl>
@@ -337,17 +346,20 @@ export function UpdateTagSheet({
"w-full justify-between",
!field.value && "text-muted-foreground",
isReadOnly &&
- "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300",
+ "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300",
)}
>
- {field.value ? col.options?.find((o) => o === field.value) : "Select an option"}
+ {field.value ?
+ col.options?.find((o) => o === field.value) :
+ t("updateTagSheet.selectOption")
+ }
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
- <CommandInput placeholder="Search options..." />
- <CommandEmpty>No option found.</CommandEmpty>
+ <CommandInput placeholder={t("updateTagSheet.searchOptions")} />
+ <CommandEmpty>{t("updateTagSheet.noOptionFound")}</CommandEmpty>
<CommandList>
<CommandGroup>
{col.options?.map((opt) => (
@@ -391,7 +403,7 @@ export function UpdateTagSheet({
{...field}
className={cn(
isReadOnly &&
- "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300",
+ "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300",
)}
/>
</FormControl>
@@ -415,11 +427,12 @@ export function UpdateTagSheet({
<SheetFooter className="gap-2 pt-2">
<SheetClose asChild>
<Button type="button" variant="outline">
- Cancel
+ {t("buttons.cancel")}
</Button>
</SheetClose>
<Button type="submit" disabled={isPending}>
- {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />}Save
+ {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />}
+ {t("buttons.save")}
</Button>
</SheetFooter>
</form>
@@ -427,4 +440,4 @@ export function UpdateTagSheet({
</SheetContent>
</Sheet>
);
-}
+} \ No newline at end of file
diff --git a/components/form-data/var-list-download-btn.tsx b/components/form-data/var-list-download-btn.tsx
index bbadf893..9d09ab8c 100644
--- a/components/form-data/var-list-download-btn.tsx
+++ b/components/form-data/var-list-download-btn.tsx
@@ -8,6 +8,8 @@ import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import { Button } from "@/components/ui/button";
import { DataTableColumnJSON } from "./form-data-table-columns";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
interface VarListDownloadBtnProps {
columnsJSON: DataTableColumnJSON[];
@@ -19,6 +21,10 @@ export const VarListDownloadBtn: FC<VarListDownloadBtnProps> = ({
formCode,
}) => {
const { toast } = useToast();
+
+ const params = useParams();
+ const lng = (params?.lng as string) || "ko";
+ const { t } = useTranslation(lng, "engineering");
const downloadReportVarList = async () => {
try {
@@ -33,7 +39,10 @@ export const VarListDownloadBtn: FC<VarListDownloadBtnProps> = ({
validationSheet.state = "hidden"; // 시트 숨김 처리
// 1. 데이터 시트에 헤더 추가
- const headers = ["Table Column Label", "Report Variable"];
+ const headers = [
+ t("varListDownload.headers.tableColumnLabel"),
+ t("varListDownload.headers.reportVariable")
+ ];
worksheet.addRow(headers);
// 헤더 스타일 적용
@@ -50,7 +59,7 @@ export const VarListDownloadBtn: FC<VarListDownloadBtnProps> = ({
// 2. 데이터 행 추가
columnsJSON.forEach((row) => {
- console.log(row)
+ console.log(row);
const { displayLabel, key } = row;
// const labelConvert = label.replaceAll(" ", "_");
@@ -81,13 +90,14 @@ export const VarListDownloadBtn: FC<VarListDownloadBtnProps> = ({
});
const buffer = await workbook.xlsx.writeBuffer();
- saveAs(new Blob([buffer]), `${formCode}_report_varible_list.xlsx`);
- toastMessage.success("Report Varible List File 다운로드 완료!");
+ const fileName = `${formCode}${t("varListDownload.fileNameSuffix")}`;
+ saveAs(new Blob([buffer]), fileName);
+ toastMessage.success(t("varListDownload.messages.downloadComplete"));
} catch (err) {
console.log(err);
toast({
- title: "Error",
- description: "Variable List 파일을 찾을 수가 없습니다.",
+ title: t("varListDownload.messages.errorTitle"),
+ description: t("varListDownload.messages.errorDescription"),
variant: "destructive",
});
}
@@ -97,16 +107,16 @@ export const VarListDownloadBtn: FC<VarListDownloadBtnProps> = ({
<Button
variant="outline"
className="relative px-[8px] py-[6px] flex-1"
- aria-label="Variable List Download"
+ aria-label={t("varListDownload.buttonAriaLabel")}
onClick={downloadReportVarList}
>
<Image
src="/icons/var_list_icon.svg"
- alt="Template Sample Download Icon"
+ alt={t("varListDownload.iconAltText")}
width={16}
height={16}
/>
- <div className='text-[12px]'>Variable List Download</div>
+ <div className="text-[12px]">{t("varListDownload.buttonText")}</div>
</Button>
);
}; \ No newline at end of file