summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/form-data/form-data-table.tsx6
-rw-r--r--components/form-data/spreadJS-dialog.tsx63
-rw-r--r--components/ship-vendor-document/new-revision-dialog.tsx237
-rw-r--r--components/ship-vendor-document/user-vendor-document-table-container.tsx168
4 files changed, 386 insertions, 88 deletions
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 591ba66a..3d8b1438 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
-import { useParams, useRouter } from "next/navigation";
+import { useParams, useRouter, usePathname } from "next/navigation";
import { useTranslation } from "@/i18n/client";
import { ClientDataTable } from "../client-data-table/data-table";
@@ -99,6 +99,7 @@ export default function DynamicTable({
const router = useRouter();
const lng = (params?.lng as string) || "ko";
const { t } = useTranslation(lng, "engineering");
+ const pathname = usePathname();
const [rowAction, setRowAction] =
React.useState<DataTableRowAction<GenericData> | null>(null);
@@ -114,6 +115,7 @@ export default function DynamicTable({
const [formStats, setFormStats] = React.useState<FormStatusByVendor | null>(null);
const [isLoadingStats, setIsLoadingStats] = React.useState(true);
+ const isEVCPPath = pathname.includes('evcp');
React.useEffect(() => {
const fetchFormStats = async () => {
@@ -672,6 +674,7 @@ export default function DynamicTable({
return (
<>
+ {!isEVCPPath && (
<div className="mb-6">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-6">
{/* Tag Count */}
@@ -807,6 +810,7 @@ export default function DynamicTable({
</Card>
</div>
</div>
+ )}
<ClientDataTable
data={tableData}
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 91d5672c..af1a3dca 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -197,7 +197,38 @@ export function TemplateViewDialog({
}, []);
React.useEffect(() => {
- if (!templateData) return;
+ // 템플릿 데이터가 없거나 빈 배열인 경우 기본 GRD_LIST 템플릿 생성
+ if (!templateData || (Array.isArray(templateData) && templateData.length === 0)) {
+ // columnsJSON이 있으면 기본 GRD_LIST 템플릿 생성
+ if (columnsJSON && columnsJSON.length > 0) {
+ const defaultGrdTemplate: TemplateItem = {
+ TMPL_ID: 'DEFAULT_GRD_LIST',
+ NAME: 'Default Grid View',
+ TMPL_TYPE: 'GRD_LIST',
+ SPR_LST_SETUP: {
+ ACT_SHEET: '',
+ HIDN_SHEETS: [],
+ DATA_SHEETS: []
+ },
+ GRD_LST_SETUP: {
+ REG_TYPE_ID: 'DEFAULT',
+ SPR_ITM_IDS: [],
+ ATTS: []
+ },
+ SPR_ITM_LST_SETUP: {
+ ACT_SHEET: '',
+ HIDN_SHEETS: [],
+ DATA_SHEETS: []
+ }
+ };
+
+ setAvailableTemplates([defaultGrdTemplate]);
+ setSelectedTemplateId('DEFAULT_GRD_LIST');
+ setTemplateType('GRD_LIST');
+ console.log('📋 Created default GRD_LIST template');
+ }
+ return;
+ }
let templates: TemplateItem[];
if (Array.isArray(templateData)) {
@@ -207,6 +238,34 @@ export function TemplateViewDialog({
}
const validTemplates = templates.filter(isValidTemplate);
+
+ // 유효한 템플릿이 없지만 columnsJSON이 있으면 기본 GRD_LIST 추가
+ if (validTemplates.length === 0 && columnsJSON && columnsJSON.length > 0) {
+ const defaultGrdTemplate: TemplateItem = {
+ TMPL_ID: 'DEFAULT_GRD_LIST',
+ NAME: 'Default Grid View',
+ TMPL_TYPE: 'GRD_LIST',
+ SPR_LST_SETUP: {
+ ACT_SHEET: '',
+ HIDN_SHEETS: [],
+ DATA_SHEETS: []
+ },
+ GRD_LST_SETUP: {
+ REG_TYPE_ID: 'DEFAULT',
+ SPR_ITM_IDS: [],
+ ATTS: []
+ },
+ SPR_ITM_LST_SETUP: {
+ ACT_SHEET: '',
+ HIDN_SHEETS: [],
+ DATA_SHEETS: []
+ }
+ };
+
+ validTemplates.push(defaultGrdTemplate);
+ console.log('📋 Added default GRD_LIST template to empty template list');
+ }
+
setAvailableTemplates(validTemplates);
if (validTemplates.length > 0 && !selectedTemplateId) {
@@ -215,7 +274,7 @@ export function TemplateViewDialog({
setSelectedTemplateId(firstTemplate.TMPL_ID);
setTemplateType(templateTypeToSet);
}
- }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType]);
+ }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType, columnsJSON]);
const handleTemplateChange = (templateId: string) => {
const template = availableTemplates.find(t => t.TMPL_ID === templateId);
diff --git a/components/ship-vendor-document/new-revision-dialog.tsx b/components/ship-vendor-document/new-revision-dialog.tsx
index 1ffcf630..3ec58d1d 100644
--- a/components/ship-vendor-document/new-revision-dialog.tsx
+++ b/components/ship-vendor-document/new-revision-dialog.tsx
@@ -9,7 +9,8 @@ import {
DialogContent,
DialogDescription,
DialogHeader,
- DialogTitle,DialogFooter
+ DialogTitle,
+ DialogFooter
} from "@/components/ui/dialog"
import {
Form,
@@ -35,10 +36,12 @@ import {
FileText,
X,
Loader2,
- CheckCircle
+ CheckCircle,
+ Info
} from "lucide-react"
import { toast } from "sonner"
import { useSession } from "next-auth/react"
+import { Alert, AlertDescription } from "@/components/ui/alert"
// 기존 메인 컴포넌트에서 추가할 import
// import { NewRevisionDialog } from "./new-revision-dialog"
@@ -50,12 +53,26 @@ import { useSession } from "next-auth/react"
// 파일 검증 스키마
const MAX_FILE_SIZE = 1024 * 1024 * 1024 // 1GB
+// B3 리비전 검증 함수
+const validateB3Revision = (value: string) => {
+ // B3 리비전 패턴: 단일 알파벳(A-Z) 또는 R01-R99
+ const alphabetPattern = /^[A-Z]$/
+ const numericPattern = /^R(0[1-9]|[1-9][0-9])$/
+
+ return alphabetPattern.test(value) || numericPattern.test(value)
+}
+
+// B4 리비전 검증 함수
+const validateB4Revision = (value: string) => {
+ // B4 리비전 패턴: R01-R99
+ const numericPattern = /^R(0[1-9]|[1-9][0-9])$/
+ return numericPattern.test(value)
+}
// drawingKind에 따른 동적 스키마 생성
const createRevisionUploadSchema = (drawingKind: string) => {
const baseSchema = {
usage: z.string().min(1, "Please select a usage"),
- revision: z.string().min(1, "Please enter a revision").max(50, "Revision must be 50 characters or less"),
comment: z.string().optional(),
attachments: z
.array(z.instanceof(File))
@@ -63,22 +80,37 @@ const createRevisionUploadSchema = (drawingKind: string) => {
.refine(
(files) => files.every((file) => file.size <= MAX_FILE_SIZE),
"File size must be 50MB or less"
- )
- // .refine(
- // (files) => files.every((file) => ACCEPTED_FILE_TYPES.includes(file.type)),
- // "Unsupported file format"
- // ),
+ ),
}
+ // B3와 B4에 따른 리비전 검증 추가
+ const revisionField = drawingKind === 'B3'
+ ? z.string()
+ .min(1, "Please enter a revision")
+ .max(3, "Revision must be 3 characters or less")
+ .refine(
+ validateB3Revision,
+ "Invalid format. Use A-Z or R01-R99"
+ )
+ : z.string()
+ .min(1, "Please enter a revision")
+ .max(3, "Revision must be 3 characters or less")
+ .refine(
+ validateB4Revision,
+ "Invalid format. Use R01-R99"
+ )
+
// B3인 경우에만 usageType 필드 추가
if (drawingKind === 'B3') {
return z.object({
...baseSchema,
+ revision: revisionField,
usageType: z.string().min(1, "Please select a usage type"),
})
} else {
return z.object({
...baseSchema,
+ revision: revisionField,
usageType: z.string().optional(),
})
}
@@ -118,7 +150,6 @@ const getUsageTypeOptions = (usage: string) => {
return [
{ value: "Full", label: "Full" },
{ value: "Partial", label: "Partial" },
-
]
case 'Working':
return [
@@ -128,7 +159,6 @@ const getUsageTypeOptions = (usage: string) => {
case 'Comments':
return [
{ value: "Comments", label: "Comments" },
-
]
default:
return []
@@ -136,8 +166,49 @@ const getUsageTypeOptions = (usage: string) => {
}
// 리비전 형식 가이드 생성
-const getRevisionGuide = () => {
- return "Enter in R01, R02, R03... format"
+const getRevisionGuide = (drawingKind: string) => {
+ if (drawingKind === 'B3') {
+ return {
+ placeholder: "e.g., A, B, C or R01, R02",
+ helpText: "Use single letter (A-Z) or R01-R99 format",
+ examples: [
+ "A, B, C, ... Z (alphabetic revisions)",
+ "R01, R02, ... R99 (numeric revisions)"
+ ]
+ }
+ }
+ return {
+ placeholder: "e.g., R01, R02, R03",
+ helpText: "Enter in R01, R02, R03... format",
+ examples: ["R01, R02, R03, ... R99"]
+ }
+}
+
+// B3 리비전 자동 포맷팅 함수
+const formatB3RevisionInput = (value: string): string => {
+ // 입력값을 대문자로 변환
+ const upperValue = value.toUpperCase()
+
+ // 단일 알파벳인 경우
+ if (/^[A-Z]$/.test(upperValue)) {
+ return upperValue
+ }
+
+ // R로 시작하는 경우
+ if (upperValue.startsWith('R')) {
+ // R 뒤의 숫자 추출
+ const numPart = upperValue.slice(1).replace(/\D/g, '')
+ if (numPart) {
+ const num = parseInt(numPart, 10)
+ // 1-99 범위 체크
+ if (num >= 1 && num <= 99) {
+ // 01-09는 0을 붙이고, 10-99는 그대로
+ return `R${num.toString().padStart(2, '0')}`
+ }
+ }
+ }
+
+ return upperValue
}
interface NewRevisionDialogProps {
@@ -146,7 +217,7 @@ interface NewRevisionDialogProps {
documentId: number
documentTitle?: string
drawingKind: string
- onSuccess?: (result?: any) => void // ✅ result 파라미터 추가
+ onSuccess?: (result?: any) => void
}
/* -------------------------------------------------------------------------------------------------
@@ -221,7 +292,7 @@ function FileUploadArea({
</div>
{files.length > 0 && (
- <div className="space-y-2 max-h-40 overflow-y-auto overscroll-contain pr-2">
+ <div className="space-y-2 max-h-40 overflow-y-auto overscroll-contain pr-2">
<p className="text-sm font-medium">Selected Files ({files.length})</p>
<div className="max-h-40 overflow-y-auto space-y-2">
{files.map((file, index) => (
@@ -259,6 +330,56 @@ function FileUploadArea({
}
/* -------------------------------------------------------------------------------------------------
+ * Revision Input Component for B3
+ * -----------------------------------------------------------------------------------------------*/
+function B3RevisionInput({
+ value,
+ onChange,
+ error
+}: {
+ value: string
+ onChange: (value: string) => void
+ error?: string
+}) {
+ const [inputValue, setInputValue] = React.useState(value)
+
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const rawValue = e.target.value
+ const formattedValue = formatB3RevisionInput(rawValue)
+
+ // 길이 제한 (알파벳은 1자, R숫자는 3자)
+ if (rawValue.length <= 3) {
+ setInputValue(formattedValue)
+ onChange(formattedValue)
+ }
+ }
+
+ const revisionGuide = getRevisionGuide('B3')
+
+ return (
+ <div className="space-y-2">
+ <Input
+ value={inputValue}
+ onChange={handleInputChange}
+ placeholder={revisionGuide.placeholder}
+ className={error ? "border-red-500" : ""}
+ />
+ <Alert className="bg-blue-50 border-blue-200">
+ <Info className="h-4 w-4 text-blue-600" />
+ <AlertDescription className="text-xs space-y-1">
+ <div className="font-medium text-blue-900">{revisionGuide.helpText}</div>
+ <div className="text-blue-700">
+ {revisionGuide.examples.map((example, idx) => (
+ <div key={idx}>• {example}</div>
+ ))}
+ </div>
+ </AlertDescription>
+ </Alert>
+ </div>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
* Main Dialog Component
* -----------------------------------------------------------------------------------------------*/
export function NewRevisionDialog({
@@ -272,6 +393,34 @@ export function NewRevisionDialog({
const [isUploading, setIsUploading] = React.useState(false)
const [uploadProgress, setUploadProgress] = React.useState(0)
const { data: session } = useSession()
+ const [nextSerialNo, setNextSerialNo] = React.useState<string>("1")
+ const [isLoadingSerialNo, setIsLoadingSerialNo] = React.useState(false)
+
+ // Serial No 조회
+ const fetchNextSerialNo = React.useCallback(async () => {
+ setIsLoadingSerialNo(true)
+ try {
+ const response = await fetch(`/api/revisions/max-serial-no?documentId=${documentId}`)
+ if (response.ok) {
+ const data = await response.json()
+ setNextSerialNo(String(data.nextSerialNo))
+ }
+ } catch (error) {
+ console.error('Failed to fetch serial no:', error)
+ // 에러 시 기본값 1 사용
+ setNextSerialNo("1")
+ } finally {
+ setIsLoadingSerialNo(false)
+ }
+ }, [documentId])
+
+ // Dialog 열릴 때 Serial No 조회
+ React.useEffect(() => {
+ if (open && documentId) {
+ fetchNextSerialNo()
+ }
+ }, [open, documentId, fetchNextSerialNo])
+
const userName = React.useMemo(() => {
return session?.user?.name ? session.user.name : null;
@@ -319,8 +468,8 @@ export function NewRevisionDialog({
// 리비전 가이드 텍스트
const revisionGuide = React.useMemo(() => {
- return getRevisionGuide()
- }, [])
+ return getRevisionGuide(drawingKind)
+ }, [drawingKind])
const handleDialogClose = () => {
if (!isUploading) {
@@ -337,6 +486,7 @@ export function NewRevisionDialog({
try {
const formData = new FormData()
formData.append("documentId", String(documentId))
+ formData.append("serialNo", nextSerialNo) // 추가
formData.append("usage", data.usage)
formData.append("revision", data.revision)
formData.append("uploaderName", userName || "evcp")
@@ -365,7 +515,7 @@ export function NewRevisionDialog({
setUploadProgress(progress)
}, 300)
- const response = await fetch('/api/revision-upload-ship', { // ✅ 올바른 API 엔드포인트 사용
+ const response = await fetch('/api/revision-upload-ship', {
method: 'POST',
body: formData,
})
@@ -389,7 +539,7 @@ export function NewRevisionDialog({
setTimeout(() => {
handleDialogClose()
- onSuccess?.(result) // ✅ API 응답 결과를 콜백에 전달
+ onSuccess?.(result)
}, 1000)
} catch (error) {
@@ -400,22 +550,22 @@ export function NewRevisionDialog({
if (error instanceof Error) {
const message = error.message.toLowerCase()
- // 파일명 관련 에러 (보안상 허용)
+ // 파일명 관련 에러
if (message.includes("안전하지 않은 파일명") || message.includes("unsafe filename") ||
message.includes("filename") && message.includes("invalid")) {
userMessage = "File name contains invalid characters. Please avoid using < > : \" ' | ? * in file names. filename can't start with '..'."
}
- // 파일명 길이 에러 (보안상 허용)
+ // 파일명 길이 에러
else if (message.includes("파일명이 너무 깁니다") || message.includes("filename too long") ||
message.includes("파일명") && message.includes("길이")) {
userMessage = "File name is too long. Please use a shorter name (max 255 characters)."
}
- // 파일 크기 에러 (보안상 허용)
+ // 파일 크기 에러
else if (message.includes("파일 크기가 너무 큽니다") || message.includes("file size") ||
message.includes("1gb limit") || message.includes("exceeds") && message.includes("limit")) {
userMessage = "File size is too large. Please use files smaller than 1GB."
}
- // 클라이언트측 네트워크 에러 (기존과 같이 처리)
+ // 클라이언트측 네트워크 에러
else if (message.includes("network") || message.includes("fetch") ||
message.includes("connection") || message.includes("timeout")) {
userMessage = "Network error occurred. Please check your connection and try again."
@@ -426,7 +576,6 @@ export function NewRevisionDialog({
message.includes("security") || message.includes("validation")) {
userMessage = "Please try again later. If the problem persists, please contact the administrator."
}
- // 그 외는 일반적인 메시지
else {
userMessage = "Please try again later. If the problem persists, please contact the administrator."
}
@@ -441,7 +590,7 @@ export function NewRevisionDialog({
return (
<Dialog open={open} onOpenChange={handleDialogClose}>
- <DialogContent className="max-w-2xl h-[90vh] flex flex-col overflow-hidden" style={{maxHeight:'90vh'}}>
+ <DialogContent className="max-w-2xl h-[90vh] flex flex-col overflow-hidden" style={{maxHeight:'90vh'}}>
{/* 고정 헤더 */}
<DialogHeader className="flex-shrink-0 pb-4 border-b">
<DialogTitle className="flex items-center gap-2">
@@ -451,6 +600,12 @@ export function NewRevisionDialog({
{documentTitle && (
<DialogDescription className="text-sm space-y-1">
<div>Document: {documentTitle}</div>
+ <div className="text-xs text-muted-foreground">
+ Drawing Type: {drawingKind} | Serial No: {nextSerialNo}
+ {isLoadingSerialNo && (
+ <Loader2 className="inline-block ml-2 h-3 w-3 animate-spin" />
+ )}
+ </div>
</DialogDescription>
)}
</DialogHeader>
@@ -513,7 +668,7 @@ export function NewRevisionDialog({
/>
)}
- {/* 리비전 */}
+ {/* 리비전 입력 */}
<FormField
control={form.control}
name="revision"
@@ -521,14 +676,30 @@ export function NewRevisionDialog({
<FormItem>
<FormLabel className="required">Revision</FormLabel>
<FormControl>
- <Input
- placeholder={revisionGuide}
- {...field}
- />
+ {drawingKind === 'B3' ? (
+ <B3RevisionInput
+ value={field.value}
+ onChange={field.onChange}
+ error={form.formState.errors.revision?.message}
+ />
+ ) : (
+ <>
+ <Input
+ placeholder={revisionGuide.placeholder}
+ {...field}
+ onChange={(e) => {
+ const upperValue = e.target.value.toUpperCase()
+ if (upperValue.length <= 3) {
+ field.onChange(upperValue)
+ }
+ }}
+ />
+ <div className="text-xs text-muted-foreground mt-1">
+ {revisionGuide.helpText}
+ </div>
+ </>
+ )}
</FormControl>
- <div className="text-xs text-muted-foreground mt-1">
- {revisionGuide}
- </div>
<FormMessage />
</FormItem>
)}
@@ -617,7 +788,7 @@ export function NewRevisionDialog({
</>
)}
</Button>
- </DialogFooter>
+ </DialogFooter>
</form>
</Form>
</DialogContent>
diff --git a/components/ship-vendor-document/user-vendor-document-table-container.tsx b/components/ship-vendor-document/user-vendor-document-table-container.tsx
index 7fac34a9..775dac47 100644
--- a/components/ship-vendor-document/user-vendor-document-table-container.tsx
+++ b/components/ship-vendor-document/user-vendor-document-table-container.tsx
@@ -40,6 +40,16 @@ import { useRouter } from 'next/navigation'
import { AddAttachmentDialog } from "./add-attachment-dialog" // ✅ import 추가
import { EditRevisionDialog } from "./edit-revision-dialog" // ✅ 추가
import { downloadFile } from "@/lib/file-download" // ✅ 공용 다운로드 함수 import
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
/* -------------------------------------------------------------------------------------------------
* Types & Constants
@@ -172,12 +182,12 @@ function RevisionTable({
// ✅ 리비전 수정 가능 여부 확인 함수
const canEditRevision = React.useCallback((revision: RevisionInfo) => {
// 첨부파일이 없으면 수정 가능
- if ((!revision.attachments || revision.attachments.length === 0)&&revision.uploaderType ==="vendor") {
+ if ((!revision.attachments || revision.attachments.length === 0) && revision.uploaderType === "vendor") {
return true
}
// 모든 첨부파일의 dolceFilePath가 null이거나 빈값이어야 수정 가능
- return revision.attachments.every(attachment =>
+ return revision.attachments.every(attachment =>
!attachment.dolceFilePath || attachment.dolceFilePath.trim() === ''
)
}, [])
@@ -188,7 +198,7 @@ function RevisionTable({
return 'no-files'
}
- const processedCount = revision.attachments.filter(attachment =>
+ const processedCount = revision.attachments.filter(attachment =>
attachment.dolceFilePath && attachment.dolceFilePath.trim() !== ''
).length
@@ -241,7 +251,7 @@ function RevisionTable({
{revisions.map((revision) => {
const canEdit = canEditRevision(revision)
const processStatus = getRevisionProcessStatus(revision)
-
+
return (
<TableRow
key={revision.id}
@@ -264,14 +274,14 @@ function RevisionTable({
{revision.revision}
{/* ✅ 처리 상태 인디케이터 */}
{processStatus === 'fully-processed' && (
- <div
- className="w-2 h-2 bg-blue-500 rounded-full"
+ <div
+ className="w-2 h-2 bg-blue-500 rounded-full"
title="All files processed"
/>
)}
{processStatus === 'partially-processed' && (
- <div
- className="w-2 h-2 bg-yellow-500 rounded-full"
+ <div
+ className="w-2 h-2 bg-yellow-500 rounded-full"
title="Some files processed"
/>
)}
@@ -333,7 +343,7 @@ function RevisionTable({
{/* ✅ 처리된 파일 수 표시 */}
{processStatus === 'partially-processed' && (
<span className="text-xs text-muted-foreground">
- ({revision.attachments.filter(att =>
+ ({revision.attachments.filter(att =>
att.dolceFilePath && att.dolceFilePath.trim() !== ''
).length} processed)
</span>
@@ -354,21 +364,20 @@ function RevisionTable({
<Eye className="h-4 w-4" />
</Button>
)}
-
+
{/* ✅ 수정 버튼 */}
<Button
variant="ghost"
size="sm"
onClick={() => onEditRevision(revision)}
- className={`h-8 px-2 ${
- canEdit
- ? 'text-blue-600 hover:text-blue-700 hover:bg-blue-50'
+ className={`h-8 px-2 ${canEdit
+ ? 'text-blue-600 hover:text-blue-700 hover:bg-blue-50'
: 'text-muted-foreground cursor-not-allowed'
- }`}
+ }`}
disabled={!canEdit}
title={
- canEdit
- ? 'Edit revision'
+ canEdit
+ ? 'Edit revision'
: 'Cannot edit - some files have been processed'
}
>
@@ -390,17 +399,23 @@ function RevisionTable({
function AttachmentTable({
attachments,
onDownloadFile,
- onDeleteFile, // ✅ 삭제 함수 prop 추가
+ onDeleteFile,
}: {
attachments: AttachmentInfo[]
onDownloadFile: (attachment: AttachmentInfo) => void
- onDeleteFile: (attachment: AttachmentInfo) => Promise<void> // ✅ 삭제 함수 추가
+ onDeleteFile: (attachment: AttachmentInfo) => Promise<void>
}) {
const { selectedRevisionId, allData, setAllData } = React.useContext(DocumentSelectionContext)
const [addAttachmentDialogOpen, setAddAttachmentDialogOpen] = React.useState(false)
- const [deletingFileId, setDeletingFileId] = React.useState<number | null>(null) // ✅ 삭제 중인 파일 ID
+ const [deletingFileId, setDeletingFileId] = React.useState<number | null>(null)
const router = useRouter()
+ // ✅ AlertDialog 상태 추가
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false)
+ const [fileToDelete, setFileToDelete] = React.useState<AttachmentInfo | null>(null)
+ const [errorAlertOpen, setErrorAlertOpen] = React.useState(false)
+ const [errorMessage, setErrorMessage] = React.useState('')
+
// 선택된 리비전 정보 가져오기
const selectedRevisionInfo = React.useMemo(() => {
if (!selectedRevisionId || !allData) return null
@@ -425,34 +440,48 @@ function AttachmentTable({
// ✅ 삭제 가능 여부 확인 함수
const canDeleteFile = React.useCallback((attachment: AttachmentInfo) => {
+ // rejected 상태의 리비전에 속한 첨부파일은 무조건 삭제 가능
+ if (selectedRevisionInfo &&
+ selectedRevisionInfo.revisionStatus &&
+ selectedRevisionInfo.revisionStatus.toLowerCase() === 'rejected') {
+ return true
+ }
+
+ // 그 외의 경우는 기존 로직대로: dolceFilePath가 없거나 빈값인 경우만 삭제 가능
return !attachment.dolceFilePath || attachment.dolceFilePath.trim() === ''
- }, [])
+ }, [selectedRevisionInfo])
- // ✅ 파일 삭제 핸들러
- const handleDeleteFile = React.useCallback(async (attachment: AttachmentInfo) => {
+ // ✅ 삭제 요청 핸들러 (확인 다이얼로그 표시)
+ const handleDeleteRequest = React.useCallback((attachment: AttachmentInfo) => {
if (!canDeleteFile(attachment)) {
- alert('This file cannot be deleted because it has been processed by the system.')
+ setErrorMessage('This file cannot be deleted because it has been processed by the system.')
+ setErrorAlertOpen(true)
return
}
- const confirmDelete = window.confirm(
- `Are you sure you want to delete "${attachment.fileName}"?\nThis action cannot be undone.`
- )
-
- if (!confirmDelete) return
+ setFileToDelete(attachment)
+ setDeleteConfirmOpen(true)
+ }, [canDeleteFile])
+
+ // ✅ 실제 삭제 수행 핸들러
+ const handleConfirmDelete = React.useCallback(async () => {
+ if (!fileToDelete) return
try {
- setDeletingFileId(attachment.id)
- await onDeleteFile(attachment)
+ setDeletingFileId(fileToDelete.id)
+ setDeleteConfirmOpen(false)
+ await onDeleteFile(fileToDelete)
} catch (error) {
console.error('Delete file error:', error)
- alert(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`)
+ setErrorMessage(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`)
+ setErrorAlertOpen(true)
} finally {
setDeletingFileId(null)
+ setFileToDelete(null)
}
- }, [canDeleteFile, onDeleteFile])
+ }, [fileToDelete, onDeleteFile])
- // 첨부파일 업로드 성공 핸들러
+ // 첨부파일 업로드 성공 핸들러 (기존 코드 유지)
const handleAttachmentUploadSuccess = React.useCallback((uploadResult?: any) => {
if (!selectedRevisionId || !allData || !uploadResult?.data) {
console.log('🔄 Full refresh')
@@ -467,7 +496,7 @@ function AttachmentTable({
revisionId: selectedRevisionId,
fileName: file.fileName,
filePath: file.filePath,
- dolceFilePath: null, // ✅ 새 파일은 dolceFilePath가 없음
+ dolceFilePath: null,
fileSize: file.fileSize,
fileType: file.fileType || null,
createdAt: new Date(),
@@ -484,7 +513,6 @@ function AttachmentTable({
for (const stage of stages) {
const revisionIndex = stage.revisions.findIndex(r => r.id === selectedRevisionId)
if (revisionIndex !== -1) {
- // 해당 리비전의 첨부파일 배열에 새 파일들 추가
stage.revisions[revisionIndex] = {
...stage.revisions[revisionIndex],
attachments: [...stage.revisions[revisionIndex].attachments, ...newAttachments]
@@ -501,7 +529,6 @@ function AttachmentTable({
setAllData(updatedData)
console.log('✅ AttachmentTable update complete')
- // 메인 테이블도 업데이트 (약간의 지연 후)
setTimeout(() => {
router.refresh()
}, 1500)
@@ -518,7 +545,6 @@ function AttachmentTable({
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Attachments</CardTitle>
- {/* + 버튼 */}
{selectedRevisionId && selectedRevisionInfo && (
<Button
onClick={handleAddAttachment}
@@ -551,7 +577,6 @@ function AttachmentTable({
? 'Please select a revision'
: 'No attached files'}
</span>
- {/* 리비전이 선택된 경우 추가 버튼 표시 */}
{selectedRevisionId && selectedRevisionInfo && (
<Button
onClick={handleAddAttachment}
@@ -581,7 +606,6 @@ function AttachmentTable({
: `${(file.fileSize / 1024).toFixed(1)}KB`
: '-'}
</div>
- {/* ✅ dolceFilePath 상태 표시 */}
{file.dolceFilePath && file.dolceFilePath.trim() !== '' && (
<div className="text-xs text-blue-600 font-medium">
Processed
@@ -591,7 +615,6 @@ function AttachmentTable({
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
- {/* 다운로드 버튼 */}
<Button
variant="ghost"
size="sm"
@@ -601,21 +624,21 @@ function AttachmentTable({
>
<Download className="h-4 w-4" />
</Button>
-
- {/* ✅ 삭제 버튼 */}
+
<Button
variant="ghost"
size="sm"
- onClick={() => handleDeleteFile(file)}
- className={`h-8 px-2 ${
- canDeleteFile(file)
- ? 'text-red-600 hover:text-red-700 hover:bg-red-50'
+ onClick={() => handleDeleteRequest(file)}
+ className={`h-8 px-2 ${canDeleteFile(file)
+ ? 'text-red-600 hover:text-red-700 hover:bg-red-50'
: 'text-muted-foreground cursor-not-allowed'
- }`}
+ }`}
disabled={!canDeleteFile(file) || deletingFileId === file.id}
title={
- canDeleteFile(file)
- ? 'Delete file'
+ canDeleteFile(file)
+ ? selectedRevisionInfo?.revisionStatus?.toLowerCase() === 'rejected'
+ ? 'Delete file (rejected revision)'
+ : 'Delete file'
: 'Cannot delete processed file'
}
>
@@ -635,6 +658,47 @@ function AttachmentTable({
</CardContent>
</Card>
+ {/* ✅ 삭제 확인 다이얼로그 */}
+ <AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Delete File</AlertDialogTitle>
+ <AlertDialogDescription>
+ Are you sure you want to delete "{fileToDelete?.fileName}"?
+ This action cannot be undone.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel onClick={() => setFileToDelete(null)}>
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleConfirmDelete}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ Delete
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+
+ {/* ✅ 에러 메시지 다이얼로그 */}
+ <AlertDialog open={errorAlertOpen} onOpenChange={setErrorAlertOpen}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Error</AlertDialogTitle>
+ <AlertDialogDescription>
+ {errorMessage}
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogAction onClick={() => setErrorMessage('')}>
+ OK
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+
{/* AddAttachmentDialog */}
{selectedRevisionInfo && (
<AddAttachmentDialog
@@ -666,7 +730,7 @@ function SubTables() {
const isCancelled = React.useRef(false)
const [newRevisionDialogOpen, setNewRevisionDialogOpen] = React.useState(false)
-
+
// ✅ 리비전 수정 다이얼로그 상태
const [editRevisionDialogOpen, setEditRevisionDialogOpen] = React.useState(false)
const [editingRevision, setEditingRevision] = React.useState<RevisionInfo | null>(null)
@@ -770,7 +834,7 @@ function SubTables() {
try {
// 파일 경로 처리
let downloadPath = attachment.filePath
-
+
// 공용 다운로드 함수 사용 (보안 검증, 파일 체크 모두 포함)
const result = await downloadFile(downloadPath, attachment.fileName, {
action: 'download',
@@ -784,7 +848,7 @@ function SubTables() {
} catch (error) {
console.error('File download error:', error)
-
+
// fallback: API 엔드포인트를 통한 다운로드 시도
try {
const queryParam = attachment.id