diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-09 05:35:23 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-09 05:35:23 +0000 |
| commit | ea8aed1e1d62fb9fa6716347de73e4ef13040929 (patch) | |
| tree | 133eea9c6be513670b7bb9b40e984543e5bdb4b9 /app/api | |
| parent | 3462d754574e2558c791c7958d3e5da013a7a573 (diff) | |
(임수민) 공동인증서 개발
Diffstat (limited to 'app/api')
| -rw-r--r-- | app/api/basic-contract/[contractId]/internal-sign-complete/route.ts | 124 | ||||
| -rw-r--r-- | app/api/basic-contract/[contractId]/sign-source/route.ts | 118 |
2 files changed, 242 insertions, 0 deletions
diff --git a/app/api/basic-contract/[contractId]/internal-sign-complete/route.ts b/app/api/basic-contract/[contractId]/internal-sign-complete/route.ts new file mode 100644 index 00000000..9ee46563 --- /dev/null +++ b/app/api/basic-contract/[contractId]/internal-sign-complete/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from "next/server"; +import db from "@/db/db"; +import { basicContract, vendors } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +// CORS 헤더 (credentials 사용 시 특정 origin 필요) +function getCorsHeaders(request: NextRequest) { + const origin = request.headers.get('origin') || '*'; + return { + 'Access-Control-Allow-Origin': origin === '*' ? '*' : origin, + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Allow-Credentials': 'true', + }; +} + +export async function OPTIONS(request: NextRequest) { + return NextResponse.json({}, { headers: getCorsHeaders(request) }); +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ contractId: string }> } +) { + try { + const { contractId: contractIdStr } = await params; + const contractId = parseInt(contractIdStr, 10); + + if (isNaN(contractId)) { + return NextResponse.json( + { success: false, error: "유효하지 않은 계약서 ID입니다." }, + { status: 400, headers: getCorsHeaders(request) } + ); + } + + const body = await request.json().catch(() => ({})); + const { pkcs7Data, signedBy } = body; + + if (!pkcs7Data) { + return NextResponse.json( + { success: false, error: "서명 데이터가 없습니다." }, + { status: 400, headers: getCorsHeaders(request) } + ); + } + + // 계약서 조회 (벤더 정보 확인용) + const [contract] = await db + .select({ + id: basicContract.id, + vendorId: basicContract.vendorId, + taxId: vendors.taxId, // 협력업체 사업자등록번호 + }) + .from(basicContract) + .leftJoin(vendors, eq(basicContract.vendorId, vendors.id)) + .where(eq(basicContract.id, contractId)) + .limit(1); + + if (!contract) { + return NextResponse.json( + { success: false, error: "계약서를 찾을 수 없습니다." }, + { status: 404, headers: getCorsHeaders(request) } + ); + } + + // TODO: PKCS7 검증 + // 1. PKCS7에서 인증서 추출 + // 2. 인증서 주체(Subject)에서 사업자등록번호 확인 + // 3. contract.taxId와 일치하는지 검증 + // 현재는 signedBy로 전달받은 값과 비교 (임시) + + if (signedBy && contract.taxId && signedBy !== contract.taxId) { + console.error('❌ 사업자등록번호 불일치:', { + expected: contract.taxId, + received: signedBy, + contractId + }); + return NextResponse.json( + { + success: false, + error: "인증서의 사업자등록번호가 계약서와 일치하지 않습니다." + }, + { status: 403, headers: getCorsHeaders(request) } + ); + } + + console.log('✅ 사업자등록번호 확인:', { + taxId: contract.taxId, + signedBy, + contractId + }); + + // 상태 업데이트: VENDOR_SIGNED + console.log('📝 DB 업데이트 시작:', { + contractId, + oldStatus: 'PENDING', + newStatus: 'VENDOR_SIGNED' + }); + + const updateResult = await db.update(basicContract) + .set({ + status: "VENDOR_SIGNED", + vendorSignedAt: new Date(), + updatedAt: new Date() + }) + .where(eq(basicContract.id, contractId)); + + console.log('✅ DB 업데이트 완료:', { + contractId, + status: 'VENDOR_SIGNED', + timestamp: new Date().toISOString() + }); + + return NextResponse.json({ success: true }, { headers: getCorsHeaders(request) }); + } catch (error) { + console.error("Internal sign complete error:", error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : "서명 완료 처리 중 오류가 발생했습니다." + }, + { status: 500, headers: getCorsHeaders(request) } + ); + } +} diff --git a/app/api/basic-contract/[contractId]/sign-source/route.ts b/app/api/basic-contract/[contractId]/sign-source/route.ts new file mode 100644 index 00000000..9f51f47a --- /dev/null +++ b/app/api/basic-contract/[contractId]/sign-source/route.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from "next/server"; +import db from "@/db/db"; +import { basicContract, basicContractTemplates } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { readFile } from "fs/promises"; +import path from "path"; + +// CORS 헤더 (credentials 사용 시 특정 origin 필요) +function getCorsHeaders(request: NextRequest) { + const origin = request.headers.get('origin') || '*'; + return { + 'Access-Control-Allow-Origin': origin === '*' ? '*' : origin, + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Allow-Credentials': 'true', + }; +} + +export async function OPTIONS(request: NextRequest) { + return NextResponse.json({}, { headers: getCorsHeaders(request) }); +} + +/** + * 계약서 파일을 읽어서 서명 원문으로 사용할 수 있는 형태로 반환 + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ contractId: string }> } +) { + console.log('🔵 [sign-source] API 호출됨:', request.url); + try { + const { contractId: contractIdStr } = await params; + console.log('🔵 [sign-source] contractId:', contractIdStr); + const contractId = parseInt(contractIdStr, 10); + + if (isNaN(contractId)) { + return NextResponse.json( + { success: false, error: "유효하지 않은 계약서 ID입니다." }, + { status: 400, headers: getCorsHeaders(request) } + ); + } + + // 계약서 정보 조회 + const [contract] = await db + .select({ + id: basicContract.id, + filePath: basicContract.filePath, + fileName: basicContract.fileName, + templateId: basicContract.templateId, + templateFilePath: basicContractTemplates.filePath, + templateFileName: basicContractTemplates.fileName, + }) + .from(basicContract) + .leftJoin( + basicContractTemplates, + eq(basicContract.templateId, basicContractTemplates.id) + ) + .where(eq(basicContract.id, contractId)) + .limit(1); + + if (!contract) { + return NextResponse.json( + { success: false, error: "계약서를 찾을 수 없습니다." }, + { status: 404, headers: getCorsHeaders(request) } + ); + } + + // 계약서 파일 경로 결정 (서명된 파일이 있으면 그것을, 없으면 템플릿 파일 사용) + const filePath = contract.filePath || contract.templateFilePath; + const fileName = contract.fileName || contract.templateFileName || "contract.pdf"; + + if (!filePath) { + return NextResponse.json( + { success: false, error: "계약서 파일을 찾을 수 없습니다." }, + { status: 404, headers: getCorsHeaders(request) } + ); + } + + // 파일 읽기 + const nasPath = process.env.NAS_PATH || "/evcp_nas"; + let fullPath: string; + + if (process.env.NODE_ENV === "production") { + fullPath = path.join(nasPath, filePath); + } else { + fullPath = path.join(process.cwd(), "public", filePath); + } + + const fileBuffer = await readFile(fullPath); + + // 파일 내용을 Base64로 인코딩 + const base64Content = fileBuffer.toString("base64"); + + // 서명 원문 형식: <html><title>contract file</title><body>파일내용|파일명</body></html> + const oriMsg = `<html><title>contract file</title><body>${base64Content}|${fileName}</body></html>`; + + return NextResponse.json({ + success: true, + data: { + signSrc: oriMsg, + fileName: fileName, + fileSize: fileBuffer.length, + }, + }, { headers: getCorsHeaders(request) }); + } catch (error) { + console.error("계약서 서명 원문 조회 실패:", error); + return NextResponse.json( + { + success: false, + error: + error instanceof Error + ? error.message + : "계약서 파일을 읽는 중 오류가 발생했습니다.", + }, + { status: 500, headers: getCorsHeaders(request) } + ); + } +} |
