summaryrefslogtreecommitdiff
path: root/lib/shi-signature
diff options
context:
space:
mode:
Diffstat (limited to 'lib/shi-signature')
-rw-r--r--lib/shi-signature/buyer-signature.ts186
-rw-r--r--lib/shi-signature/signature-list.tsx149
-rw-r--r--lib/shi-signature/upload-form.tsx115
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