summaryrefslogtreecommitdiff
path: root/app/api
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-12-09 05:35:23 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-12-09 05:35:23 +0000
commitea8aed1e1d62fb9fa6716347de73e4ef13040929 (patch)
tree133eea9c6be513670b7bb9b40e984543e5bdb4b9 /app/api
parent3462d754574e2558c791c7958d3e5da013a7a573 (diff)
(임수민) 공동인증서 개발
Diffstat (limited to 'app/api')
-rw-r--r--app/api/basic-contract/[contractId]/internal-sign-complete/route.ts124
-rw-r--r--app/api/basic-contract/[contractId]/sign-source/route.ts118
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) }
+ );
+ }
+}