diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 09:02:00 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 09:02:00 +0000 |
| commit | cbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (patch) | |
| tree | 0a26712f7685e4f6511e637b9a81269d90a47c8f /components/form-data | |
| parent | eb654f88214095f71be142b989e620fd28db3f69 (diff) | |
(대표님) 입찰, EDP 변경사항 대응, spreadJS 오류 수정, 벤더실사 수정
Diffstat (limited to 'components/form-data')
| -rw-r--r-- | components/form-data/add-formTag-dialog.tsx | 58 | ||||
| -rw-r--r-- | components/form-data/delete-form-data-dialog.tsx | 67 | ||||
| -rw-r--r-- | components/form-data/form-data-report-batch-dialog.tsx | 57 | ||||
| -rw-r--r-- | components/form-data/form-data-report-dialog.tsx | 26 | ||||
| -rw-r--r-- | components/form-data/form-data-report-temp-upload-dialog.tsx | 116 | ||||
| -rw-r--r-- | components/form-data/form-data-report-temp-upload-tab.tsx | 53 | ||||
| -rw-r--r-- | components/form-data/form-data-report-temp-uploaded-list-tab.tsx | 37 | ||||
| -rw-r--r-- | components/form-data/form-data-table-columns.tsx | 108 | ||||
| -rw-r--r-- | components/form-data/form-data-table.tsx | 33 | ||||
| -rw-r--r-- | components/form-data/sedp-compare-dialog.tsx | 233 | ||||
| -rw-r--r-- | components/form-data/sedp-components.tsx | 50 | ||||
| -rw-r--r-- | components/form-data/sedp-excel-download.tsx | 43 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 736 | ||||
| -rw-r--r-- | components/form-data/update-form-sheet.tsx | 55 | ||||
| -rw-r--r-- | components/form-data/var-list-download-btn.tsx | 28 |
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 |
