summaryrefslogtreecommitdiff
path: root/components/ship-vendor-document/new-revision-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-01 10:31:23 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-01 10:31:23 +0000
commit74843fe598702a9a55f914f2d2d291368a5abb13 (patch)
treea88abdaf039f51dd843e0416321f08877b17ea75 /components/ship-vendor-document/new-revision-dialog.tsx
parent33e8452331c301430191b3506825ebaf3edac93a (diff)
(대표님) dolce 수정, spreadjs 수정 등
Diffstat (limited to 'components/ship-vendor-document/new-revision-dialog.tsx')
-rw-r--r--components/ship-vendor-document/new-revision-dialog.tsx237
1 files changed, 204 insertions, 33 deletions
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>