diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
| commit | cf8dac0c6490469dab88a560004b0c07dbd48612 (patch) | |
| tree | b9e76061e80d868331e6b4277deecb9086f845f3 /lib/shi-signature | |
| parent | e5745fc0268bbb5770bc14a55fd58a0ec30b466e (diff) | |
(대표님) rfq, 계약, 서명 등
Diffstat (limited to 'lib/shi-signature')
| -rw-r--r-- | lib/shi-signature/buyer-signature.ts | 186 | ||||
| -rw-r--r-- | lib/shi-signature/signature-list.tsx | 149 | ||||
| -rw-r--r-- | lib/shi-signature/upload-form.tsx | 115 |
3 files changed, 450 insertions, 0 deletions
diff --git a/lib/shi-signature/buyer-signature.ts b/lib/shi-signature/buyer-signature.ts new file mode 100644 index 00000000..d464ae54 --- /dev/null +++ b/lib/shi-signature/buyer-signature.ts @@ -0,0 +1,186 @@ +'use server'; + +import db from '@/db/db'; +import { buyerSignatures } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; +import { writeFile, mkdir } from 'fs/promises'; +import path from 'path'; +import { v4 as uuidv4 } from 'uuid'; + +export async function uploadBuyerSignature(formData: FormData) { + try { + const file = formData.get('file') as File; + if (!file) { + return { success: false, error: '파일이 없습니다.' }; + } + + // 파일 크기 체크 (5MB) + if (file.size > 5 * 1024 * 1024) { + return { success: false, error: '파일 크기는 5MB 이하여야 합니다.' }; + } + + // 파일 타입 체크 + if (!file.type.startsWith('image/')) { + return { success: false, error: '이미지 파일만 업로드 가능합니다.' }; + } + + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + + // Base64 변환 + const base64 = `data:${file.type};base64,${buffer.toString('base64')}`; + + // 파일 저장 경로 + const fileName = `${uuidv4()}-${file.name}`; + const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'signatures'); + + // 디렉토리 생성 + await mkdir(uploadDir, { recursive: true }); + + const filePath = path.join(uploadDir, fileName); + await writeFile(filePath, buffer); + + // 기존 활성 서명 비활성화 + await db.update(buyerSignatures) + .set({ isActive: false }) + .where(eq(buyerSignatures.isActive, true)); + + // 새 서명 저장 + const [newSignature] = await db.insert(buyerSignatures) + .values({ + name: '삼성중공업', + imageUrl: `/uploads/signatures/${fileName}`, + dataUrl: base64, + mimeType: file.type, + fileSize: file.size, + isActive: true, + }) + .returning(); + + revalidatePath('/admin/buyer-signature'); + + return { success: true, signature: newSignature }; + } catch (error) { + console.error('서명 업로드 실패:', error); + return { success: false, error: '서명 업로드에 실패했습니다.' }; + } +} + +export async function getActiveSignature() { + try { + const [signature] = await db + .select() + .from(buyerSignatures) + .where(eq(buyerSignatures.isActive, true)) + .limit(1); + + return signature; + } catch (error) { + console.error('활성 서명 조회 실패:', error); + return null; + } +} + +export async function getAllSignatures() { + try { + const signatures = await db + .select() + .from(buyerSignatures) + .orderBy(buyerSignatures.createdAt); + + return signatures; + } catch (error) { + console.error('서명 목록 조회 실패:', error); + return []; + } +} + +export async function setActiveSignature(id: number) { + try { + // 모든 서명 비활성화 + await db.update(buyerSignatures) + .set({ isActive: false }) + .where(eq(buyerSignatures.isActive, true)); + + // 선택한 서명 활성화 + await db.update(buyerSignatures) + .set({ isActive: true }) + .where(eq(buyerSignatures.id, id)); + + revalidatePath('/admin/buyer-signature'); + + return { success: true }; + } catch (error) { + console.error('활성 서명 설정 실패:', error); + return { success: false, error: '활성 서명 설정에 실패했습니다.' }; + } +} + +export async function deleteSignature(id: number) { + try { + await db.delete(buyerSignatures) + .where(eq(buyerSignatures.id, id)); + + revalidatePath('/admin/buyer-signature'); + + return { success: true }; + } catch (error) { + console.error('서명 삭제 실패:', error); + return { success: false, error: '서명 삭제에 실패했습니다.' }; + } +} + + +// 클라이언트에서 직접 호출할 수 있는 서버 액션 +export async function getBuyerSignatureFile() { + try { + const [signature] = await db + .select() + .from(buyerSignatures) + .where(eq(buyerSignatures.isActive, true)) + .limit(1); + + if (!signature || !signature.dataUrl) { + console.log('활성화된 구매자 서명이 없습니다.'); + return null; + } + + return { + data: { + dataUrl: signature.dataUrl, + imageUrl: signature.imageUrl, + mimeType: signature.mimeType + } + }; + } catch (error) { + console.error('구매자 서명 조회 실패:', error); + return null; + } +} + +// 대체 서명이나 기본 서명이 필요한 경우 +export async function getBuyerSignatureFileWithFallback() { + try { + // 먼저 DB에서 활성 서명 조회 + const signature = await getBuyerSignatureFile(); + + if (signature) { + return signature; + } + + // DB에 서명이 없으면 기본 서명 반환 (선택사항) + const defaultSignature = { + data: { + dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', // 1x1 투명 픽셀 또는 실제 기본 서명 + imageUrl: '/images/default-buyer-signature.png', + mimeType: 'image/png' + } + }; + + return defaultSignature; + } catch (error) { + console.error('서명 조회 실패 (fallback 포함):', error); + return null; + } +}
\ No newline at end of file diff --git a/lib/shi-signature/signature-list.tsx b/lib/shi-signature/signature-list.tsx new file mode 100644 index 00000000..93cd3dbe --- /dev/null +++ b/lib/shi-signature/signature-list.tsx @@ -0,0 +1,149 @@ +'use client'; + +import { useState } from 'react'; +import { BuyerSignature } from '@/db/schemae'; +import { setActiveSignature, deleteSignature } from './buyer-signature'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Trash2, CheckCircle, Circle,Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; + +interface SignatureListProps { + signatures: BuyerSignature[]; +} + +export function SignatureList({ signatures }: SignatureListProps) { + const [isUpdating, setIsUpdating] = useState<number | null>(null); + + const handleSetActive = async (id: number) => { + setIsUpdating(id); + try { + const result = await setActiveSignature(id); + if (result.success) { + toast.success('활성 서명이 변경되었습니다.'); + } else { + toast.error(result.error || '변경에 실패했습니다.'); + } + } catch (error) { + toast.error('오류가 발생했습니다.'); + } finally { + setIsUpdating(null); + } + }; + + const handleDelete = async (id: number) => { + try { + const result = await deleteSignature(id); + if (result.success) { + toast.success('서명이 삭제되었습니다.'); + } else { + toast.error(result.error || '삭제에 실패했습니다.'); + } + } catch (error) { + toast.error('오류가 발생했습니다.'); + } + }; + + if (signatures.length === 0) { + return ( + <Card> + <CardContent className="py-8 text-center text-muted-foreground"> + 아직 업로드된 서명이 없습니다. + </CardContent> + </Card> + ); + } + + return ( + <Card> + <CardHeader> + <CardTitle>서명 목록</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {signatures.map((signature) => ( + <div + key={signature.id} + className="flex items-center justify-between p-4 border rounded-lg" + > + <div className="flex items-center space-x-4"> + <img + src={signature.imageUrl} + alt="서명" + className="h-12 object-contain border rounded p-1" + /> + <div> + <div className="flex items-center space-x-2"> + <span className="font-medium">{signature.name}</span> + {signature.isActive && ( + <Badge variant="default" className="text-xs"> + 활성 + </Badge> + )} + </div> + <p className="text-sm text-muted-foreground"> + {new Date(signature.createdAt).toLocaleDateString()} + </p> + </div> + </div> + + <div className="flex items-center space-x-2"> + {!signature.isActive && ( + <Button + variant="outline" + size="sm" + onClick={() => handleSetActive(signature.id)} + disabled={isUpdating === signature.id} + > + {isUpdating === signature.id ? ( + <Loader2 className="h-4 w-4 animate-spin" /> + ) : ( + <Circle className="h-4 w-4" /> + )} + <span className="ml-2">활성화</span> + </Button> + )} + + <AlertDialog> + <AlertDialogTrigger asChild> + <Button + variant="outline" + size="sm" + disabled={signature.isActive} + > + <Trash2 className="h-4 w-4" /> + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>서명 삭제</AlertDialogTitle> + <AlertDialogDescription> + 이 서명을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction onClick={() => handleDelete(signature.id)}> + 삭제 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + </div> + ))} + </CardContent> + </Card> + ); +}
\ No newline at end of file diff --git a/lib/shi-signature/upload-form.tsx b/lib/shi-signature/upload-form.tsx new file mode 100644 index 00000000..642cd1a5 --- /dev/null +++ b/lib/shi-signature/upload-form.tsx @@ -0,0 +1,115 @@ +'use client'; + +import { useState } from 'react'; +import { uploadBuyerSignature } from './buyer-signature'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Upload, Loader2, CheckCircle } from 'lucide-react'; +import { toast } from 'sonner'; + +export function BuyerSignatureUploadForm() { + const [isUploading, setIsUploading] = useState(false); + const [preview, setPreview] = useState<string | null>(null); + + const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + setIsUploading(true); + + const formData = new FormData(e.currentTarget); + + try { + const result = await uploadBuyerSignature(formData); + + if (result.success) { + toast.success('서명이 성공적으로 업로드되었습니다.'); + setPreview(null); + (e.target as HTMLFormElement).reset(); + } else { + toast.error(result.error || '업로드에 실패했습니다.'); + } + } catch (error) { + toast.error('업로드 중 오류가 발생했습니다.'); + } finally { + setIsUploading(false); + } + }; + + return ( + <Card> + <CardHeader> + <CardTitle>구매자 서명 업로드</CardTitle> + <CardDescription> + 삼성중공업 서명 이미지를 업로드하세요. 이 서명은 계약서에 자동으로 적용됩니다. + </CardDescription> + </CardHeader> + <CardContent> + <form onSubmit={handleSubmit} className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="file">서명 이미지</Label> + <Input + id="file" + name="file" + type="file" + accept="image/*" + onChange={handleFileChange} + required + disabled={isUploading} + /> + <p className="text-sm text-muted-foreground"> + PNG, JPG, JPEG 형식 (최대 5MB) + </p> + </div> + + {preview && ( + <div className="border rounded-lg p-4 bg-gray-50"> + <Label className="text-sm font-medium mb-2 block">미리보기</Label> + <img + src={preview} + alt="서명 미리보기" + className="max-h-32 object-contain" + /> + </div> + )} + + <Alert> + <AlertDescription> + 업로드한 서명은 즉시 활성화되며, 새로운 계약서에 자동으로 적용됩니다. + </AlertDescription> + </Alert> + + <Button + type="submit" + disabled={isUploading || !preview} + className="w-full" + > + {isUploading ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + 업로드 중... + </> + ) : ( + <> + <Upload className="mr-2 h-4 w-4" /> + 서명 업로드 + </> + )} + </Button> + </form> + </CardContent> + </Card> + ); +}
\ No newline at end of file |
