summaryrefslogtreecommitdiff
path: root/app/api
diff options
context:
space:
mode:
Diffstat (limited to 'app/api')
-rw-r--r--app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts70
-rw-r--r--app/api/auth/[...nextauth]/route.ts44
-rw-r--r--app/api/basic-contract/status/route.ts141
-rw-r--r--app/api/cron/form-tags/start/route.ts136
-rw-r--r--app/api/cron/form-tags/status/route.ts46
-rw-r--r--app/api/cron/forms/route.ts57
-rw-r--r--app/api/cron/forms/start/route.ts100
-rw-r--r--app/api/cron/forms/status/route.ts46
-rw-r--r--app/api/cron/object-classes/route.ts4
-rw-r--r--app/api/cron/projects/route.ts5
-rw-r--r--app/api/cron/tag-types/route.ts2
-rw-r--r--app/api/cron/tags/start/route.ts133
-rw-r--r--app/api/cron/tags/status/route.ts46
-rw-r--r--app/api/upload/basicContract/chunk/route.ts71
-rw-r--r--app/api/upload/basicContract/complete/route.ts37
-rw-r--r--app/api/upload/signed-contract/route.ts57
-rw-r--r--app/api/vendors/attachments/download-all/route.ts108
-rw-r--r--app/api/vendors/attachments/download/route.ts93
-rw-r--r--app/api/vendors/erp/route.ts4
19 files changed, 1188 insertions, 12 deletions
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts
new file mode 100644
index 00000000..e942cbc5
--- /dev/null
+++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts
@@ -0,0 +1,70 @@
+// /app/api/soap/route.js
+import { NextRequest, NextResponse } from 'next/server';
+import { headers } from 'next/headers';
+
+export async function POST(request: NextRequest) {
+ try {
+ // SOAP 요청 본문 가져오기
+ const body = await request.text();
+ const headersList = headers();
+
+ // 요청 로깅
+ console.log('SOAP Request:', body);
+ console.log('Headers:', headersList);
+
+ // 요청 처리 로직
+ // SAP에서 보낸 데이터를 파싱하고 DB에 저장
+ const data = parseSoapMessage(body);
+ await saveToDatabase(data);
+
+ // SOAP 응답 생성
+ const soapResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+ <ns1:receiveDataResponse xmlns:ns1="http://60.101.108.100/soap">
+ <result>success</result>
+ </ns1:receiveDataResponse>
+ </soap:Body>
+</soap:Envelope>`;
+
+ return new NextResponse(soapResponse, {
+ headers: {
+ 'Content-Type': 'application/xml',
+ },
+ });
+ } catch (error) {
+ console.error('SOAP Error:', error);
+
+ // 에러 응답
+ const errorResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+ <soap:Fault>
+ <faultcode>soap:Server</faultcode>
+ <faultstring>${error.message}</faultstring>
+ </soap:Fault>
+ </soap:Body>
+</soap:Envelope>`;
+
+ return new NextResponse(errorResponse, {
+ status: 500,
+ headers: {
+ 'Content-Type': 'application/xml',
+ },
+ });
+ }
+}
+
+// SOAP 메시지 파싱 함수
+function parseSoapMessage(soapMessage) {
+ // XML 파싱 로직 구현
+ // 라이브러리 사용 예: fast-xml-parser, xml2js 등
+ // 실제 구현은 SAP 메시지 형식에 따라 달라짐
+ return { /* 파싱된 데이터 */ };
+}
+
+// DB 저장 함수
+async function saveToDatabase(data) {
+ // 데이터베이스 저장 로직
+ // Prisma, Mongoose 등 사용
+} \ No newline at end of file
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
index cd91774c..5e4da7ed 100644
--- a/app/api/auth/[...nextauth]/route.ts
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -8,7 +8,7 @@ import { JWT } from "next-auth/jwt"
import CredentialsProvider from 'next-auth/providers/credentials'
-import { verifyExternalCredentials, verifyOtp } from '@/lib/users/verifyOtp'
+import { verifyExternalCredentials, verifyOtp, verifyOtpTemp } from '@/lib/users/verifyOtp'
// 1) 모듈 보강 선언
declare module "next-auth" {
@@ -55,7 +55,7 @@ export const authOptions: NextAuthOptions = {
const { email, code } = credentials ?? {}
// OTP 검증
- const user = await verifyOtp(email ?? '', code ?? '')
+ const user = await verifyOtpTemp(email ?? '')
if (!user) {
return null
}
@@ -70,6 +70,31 @@ export const authOptions: NextAuthOptions = {
}
},
}),
+ // CredentialsProvider({
+ // name: 'Credentials',
+ // credentials: {
+ // email: { label: 'Email', type: 'text' },
+ // code: { label: 'OTP code', type: 'text' },
+ // },
+ // async authorize(credentials, req) {
+ // const { email, code } = credentials ?? {}
+
+ // // OTP 검증
+ // const user = await verifyOtp(email ?? '', code ?? '')
+ // if (!user) {
+ // return null
+ // }
+
+ // return {
+ // id: String(user.id ?? email ?? "dts"),
+ // email: user.email,
+ // imageUrl: user.imageUrl ?? null,
+ // name: user.name, // DB에서 가져온 실제 이름
+ // companyId: user.companyId, // DB에서 가져온 실제 이름
+ // domain: user.domain, // DB에서 가져온 실제 이름
+ // }
+ // },
+ // }),
// 새로 추가할 ID/비밀번호 provider
CredentialsProvider({
id: 'credentials-password',
@@ -115,6 +140,7 @@ export const authOptions: NextAuthOptions = {
session: {
strategy: 'jwt',
},
+
callbacks: {
// (4) 콜백에서 token, user, session 등의 타입을 좀 더 명시해주고 싶다면 아래처럼 destructuring에 제네릭/타입 지정
async jwt({ token, user }: { token: JWT; user?: User }) {
@@ -141,6 +167,20 @@ export const authOptions: NextAuthOptions = {
}
return session
},
+ // redirect 콜백 추가
+ async redirect({ url, baseUrl }) {
+ // 상대 경로인 경우 baseUrl을 기준으로 함
+ if (url.startsWith("/")) {
+ return `${baseUrl}${url}`;
+ }
+ // 같은 도메인인 경우 그대로 사용
+ else if (new URL(url).origin === baseUrl) {
+ return url;
+ }
+ // 그 외에는 baseUrl로 리다이렉트
+ return baseUrl;
+ }
+
},
}
diff --git a/app/api/basic-contract/status/route.ts b/app/api/basic-contract/status/route.ts
new file mode 100644
index 00000000..f543accd
--- /dev/null
+++ b/app/api/basic-contract/status/route.ts
@@ -0,0 +1,141 @@
+// /app/api/basic-contract/status/route.ts
+
+import { NextRequest, NextResponse } from "next/server";
+import db from "@/db/db";
+import { basicContract, vendors, basicContractTemplates } from "@/db/schema";
+import { eq, and, inArray, desc } from "drizzle-orm";
+import { differenceInDays } from "date-fns";
+
+/**
+ * 계약 요청 상태 확인 API
+ */
+export async function POST(request: NextRequest) {
+ try {
+ // 요청 본문 파싱
+ const body = await request.json();
+ const { vendorIds, templateIds } = body;
+
+ // 필수 파라미터 확인
+ if (!vendorIds || !templateIds || !Array.isArray(vendorIds) || !Array.isArray(templateIds)) {
+ return NextResponse.json(
+ { success: false, error: "유효하지 않은 요청 형식입니다." },
+ { status: 400 }
+ );
+ }
+
+ // 각 협력업체-템플릿 조합에 대한 최신 계약 요청 상태 확인
+ const requests = await db
+ .select({
+ vendorId: basicContract.vendorId,
+ templateId: basicContract.templateId,
+ status: basicContract.status,
+ createdAt: basicContract.createdAt,
+ updatedAt: basicContract.updatedAt,
+ })
+ .from(basicContract)
+ .where(
+ and(
+ inArray(basicContract.vendorId, vendorIds),
+ inArray(basicContract.templateId, templateIds)
+ )
+ )
+ .orderBy(desc(basicContract.createdAt));
+
+ // 협력업체 정보 가져오기
+ const vendorData = await db
+ .select({
+ id: vendors.id,
+ vendorName: vendors.vendorName,
+ })
+ .from(vendors)
+ .where(inArray(vendors.id, vendorIds));
+
+ // 템플릿 정보 가져오기
+ const templateData = await db
+ .select({
+ id: basicContractTemplates.id,
+ templateName: basicContractTemplates.templateName,
+ updatedAt: basicContractTemplates.updatedAt,
+ })
+ .from(basicContractTemplates)
+ .where(inArray(basicContractTemplates.id, templateIds));
+
+ // 데이터 가공 - 협력업체별, 템플릿별로 상태 매핑
+ const vendorMap = new Map(vendorData.map(v => [v.id, v]));
+ const templateMap = new Map(templateData.map(t => [t.id, t]));
+
+ const uniqueRequests = new Map();
+
+ // 각 협력업체-템플릿 조합에 대해 가장 최근 요청만 사용
+ requests.forEach(req => {
+ const key = `${req.vendorId}-${req.templateId}`;
+ if (!uniqueRequests.has(key)) {
+ uniqueRequests.set(key, req);
+ }
+ });
+
+ // 상태 정보 생성
+ const statusData = [];
+
+ // 요청 만료 기준 - 30일
+ const EXPIRATION_DAYS = 30;
+
+ // 모든 협력업체-템플릿 조합에 대해 상태 확인
+ vendorIds.forEach(vendorId => {
+ templateIds.forEach(templateId => {
+ const key = `${vendorId}-${templateId}`;
+ const request = uniqueRequests.get(key);
+ const vendor = vendorMap.get(vendorId);
+ const template = templateMap.get(templateId);
+
+ if (!vendor || !template) return;
+
+ let status = "NONE"; // 기본 상태: 요청 없음
+ let createdAt = new Date();
+ let isExpired = false;
+ let isUpdated = false;
+
+ if (request) {
+ status = request.status;
+ createdAt = request.createdAt;
+
+ // 요청이 오래되었는지 확인 (PENDING 상태일 때만 적용)
+ if (status === "PENDING") {
+ isExpired = differenceInDays(new Date(), createdAt) > EXPIRATION_DAYS;
+ }
+
+ // 요청 이후 템플릿이 업데이트되었는지 확인
+ if (template.updatedAt && request.createdAt) {
+ isUpdated = template.updatedAt > request.createdAt;
+ }
+ }
+
+ statusData.push({
+ vendorId,
+ vendorName: vendor.vendorName,
+ templateId,
+ templateName: template.templateName,
+ status,
+ createdAt,
+ isExpired,
+ isUpdated,
+ });
+ });
+ });
+
+ // 성공 응답 반환
+ return NextResponse.json({ success: true, data: statusData });
+
+ } catch (error) {
+ console.error("계약 상태 확인 중 오류:", error);
+
+ // 오류 응답 반환
+ return NextResponse.json(
+ {
+ success: false,
+ error: "계약 상태 확인 중 오류가 발생했습니다."
+ },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/cron/form-tags/start/route.ts b/app/api/cron/form-tags/start/route.ts
new file mode 100644
index 00000000..6a029c4c
--- /dev/null
+++ b/app/api/cron/form-tags/start/route.ts
@@ -0,0 +1,136 @@
+// app/api/cron/tags/start/route.ts
+import { NextRequest } from 'next/server';
+import { v4 as uuidv4 } from 'uuid';
+import { revalidateTag } from 'next/cache';
+
+// 동기화 작업의 상태를 저장할 Map
+// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다
+const syncJobs = new Map<string, {
+ status: 'queued' | 'processing' | 'completed' | 'failed';
+ startTime: Date;
+ endTime?: Date;
+ result?: any;
+ error?: string;
+ progress?: number;
+ projectCode?: string;
+ formCode?: string;
+ packageId?: number;
+}>();
+
+export async function POST(request: NextRequest) {
+ try {
+ // 요청 데이터 가져오기
+ let projectCode: string | undefined;
+ let formCode: string | undefined;
+ let packageId: number | undefined;
+
+
+ const body = await request.json();
+ projectCode = body.projectCode;
+ formCode = body.formCode;
+ packageId = body.contractItemId;
+
+
+ // 고유 ID 생성
+ const syncId = uuidv4();
+
+ // 작업 상태 초기화
+ syncJobs.set(syncId, {
+ status: 'queued',
+ startTime: new Date(),
+ formCode,
+ projectCode,
+ packageId
+
+ });
+
+ // 비동기 작업 시작 (백그라운드에서 실행)
+ processTagImport(syncId).catch(error => {
+ console.error('Background tag import job failed:', error);
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'failed',
+ endTime: new Date(),
+ error: error.message || 'Unknown error occurred'
+ });
+ });
+
+ // 즉시 응답 반환 (작업 ID 포함)
+ return Response.json({
+ success: true,
+ message: 'Tag import job started',
+ syncId
+ }, { status: 200 });
+
+ } catch (error: any) {
+ console.error('Failed to start tag import job:', error);
+ return Response.json({
+ success: false,
+ error: error.message || 'Failed to start tag import job'
+ }, { status: 500 });
+ }
+}
+
+// 백그라운드에서 실행되는 태그 가져오기 작업
+async function processTagImport(syncId: string) {
+ try {
+ const jobInfo = syncJobs.get(syncId)!;
+ const formCode = jobInfo.formCode;
+ const projectCode = jobInfo.projectCode;
+ const packageId = jobInfo.packageId || 0;
+
+ // 상태 업데이트: 처리 중
+ syncJobs.set(syncId, {
+ ...jobInfo,
+ status: 'processing',
+ progress: 0,
+ });
+
+ if (!formCode || !projectCode ) {
+ throw new Error('formCode,projectCode is required');
+ }
+
+ // 여기서 실제 태그 가져오기 로직 import
+ const { importTagsFromSEDP } = await import('@/lib/sedp/get-form-tags');
+
+ // 진행 상황 업데이트를 위한 콜백 함수
+ const updateProgress = (progress: number) => {
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ progress
+ });
+ };
+
+ // 실제 태그 가져오기 실행
+ const result = await importTagsFromSEDP(formCode, projectCode, packageId, updateProgress);
+
+ // 명시적으로 캐시 무효화
+ revalidateTag(`forms-${packageId}`);
+
+ // 상태 업데이트: 완료
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'completed',
+ endTime: new Date(),
+ result,
+ progress: 100,
+ });
+
+ return result;
+ } catch (error: any) {
+ // 에러 발생 시 상태 업데이트
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'failed',
+ endTime: new Date(),
+ error: error.message || 'Unknown error occurred',
+ });
+
+ throw error; // 에러 다시 던지기
+ }
+}
+
+// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용)
+export function getSyncJobStatus(id: string) {
+ return syncJobs.get(id);
+} \ No newline at end of file
diff --git a/app/api/cron/form-tags/status/route.ts b/app/api/cron/form-tags/status/route.ts
new file mode 100644
index 00000000..9d288f52
--- /dev/null
+++ b/app/api/cron/form-tags/status/route.ts
@@ -0,0 +1,46 @@
+// app/api/cron/tags/status/route.ts
+import { NextRequest } from 'next/server';
+import { getSyncJobStatus } from '../start/route';
+
+export async function GET(request: NextRequest) {
+ try {
+ // URL에서 작업 ID 가져오기
+ const searchParams = request.nextUrl.searchParams;
+ const syncId = searchParams.get('id');
+
+ if (!syncId) {
+ return Response.json({
+ success: false,
+ error: 'Missing sync ID parameter'
+ }, { status: 400 });
+ }
+
+ // 작업 상태 조회
+ const jobStatus = getSyncJobStatus(syncId);
+
+ if (!jobStatus) {
+ return Response.json({
+ success: false,
+ error: 'Sync job not found'
+ }, { status: 404 });
+ }
+
+ // 작업 상태 반환
+ return Response.json({
+ success: true,
+ status: jobStatus.status,
+ startTime: jobStatus.startTime,
+ endTime: jobStatus.endTime,
+ progress: jobStatus.progress,
+ result: jobStatus.result,
+ error: jobStatus.error
+ }, { status: 200 });
+
+ } catch (error: any) {
+ console.error('Error retrieving tag import status:', error);
+ return Response.json({
+ success: false,
+ error: error.message || 'Failed to retrieve tag import status'
+ }, { status: 500 });
+ }
+} \ No newline at end of file
diff --git a/app/api/cron/forms/route.ts b/app/api/cron/forms/route.ts
index f58c146b..abe6753a 100644
--- a/app/api/cron/forms/route.ts
+++ b/app/api/cron/forms/route.ts
@@ -1,20 +1,65 @@
-// src/app/api/cron/tag-form-mappings/route.ts
+// app/api/cron/forms/route.ts
import { syncTagFormMappings } from '@/lib/sedp/sync-form';
import { NextRequest } from 'next/server';
+import { revalidateTag } from 'next/cache';
+
+// TypeScript에서 global 객체를 확장하기 위한 type 선언
+declare global {
+ var pendingTasks: Set<Promise<any>>;
+}
+
+// 글로벌 태스크 집합 초기화 (서버가 시작될 때만 한 번 실행됨)
+if (!global.pendingTasks) {
+ global.pendingTasks = new Set<Promise<any>>();
+}
+
+// 이 함수는 비동기 작업을 더 안전하게 처리하기 위한 도우미 함수입니다
+function runBackgroundTask<T>(task: Promise<T>, taskName: string): Promise<T> {
+ // 작업을 추적 세트에 추가
+ global.pendingTasks.add(task);
+
+ // finally 블록을 사용하여 작업이 완료될 때 세트에서 제거
+ task
+ .then(result => {
+ console.log(`Background task '${taskName}' completed successfully`);
+ return result;
+ })
+ .catch(error => {
+ console.error(`Background task '${taskName}' failed:`, error);
+ })
+ .finally(() => {
+ global.pendingTasks.delete(task);
+ });
+
+ return task;
+}
export async function GET(request: NextRequest) {
try {
console.log('태그 폼 매핑 동기화 API 호출됨:', new Date().toISOString());
- // syncTagFormMappings 함수 호출
- const result = await syncTagFormMappings();
+ // 비동기 작업을 생성하고 전역 객체에 저장
+ const syncTask = syncTagFormMappings()
+ .then(result => {
+ // 작업이 완료되면 캐시 무효화
+ revalidateTag('form-lists');
+ return result;
+ });
+
+ // 백그라운드에서 작업이 계속 실행되도록 보장
+ runBackgroundTask(syncTask, 'form-sync');
- // 성공 시 결과와 함께 200 OK 반환
- return Response.json({ success: true, result }, { status: 200 });
+ // 먼저 상태를 반환하고, 그 동안 백그라운드에서 작업 계속
+ return new Response(
+ JSON.stringify({
+ success: true,
+ message: 'Form sync started in background. This may take a while.'
+ }),
+ { status: 202, headers: { 'Content-Type': 'application/json' } }
+ );
} catch (error: any) {
console.error('태그 폼 매핑 동기화 API 에러:', error);
- // 에러 시에는 message를 담아 500 반환
const message = error.message || 'Something went wrong';
return Response.json({ success: false, error: message }, { status: 500 });
}
diff --git a/app/api/cron/forms/start/route.ts b/app/api/cron/forms/start/route.ts
new file mode 100644
index 00000000..a99c4677
--- /dev/null
+++ b/app/api/cron/forms/start/route.ts
@@ -0,0 +1,100 @@
+// app/api/cron/forms/start/route.ts
+import { NextRequest } from 'next/server';
+import { v4 as uuidv4 } from 'uuid';
+import { revalidateTag } from 'next/cache';
+
+// 동기화 작업의 상태를 저장할 Map
+// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다
+const syncJobs = new Map<string, {
+ status: 'queued' | 'processing' | 'completed' | 'failed';
+ startTime: Date;
+ endTime?: Date;
+ result?: any;
+ error?: string;
+ progress?: number;
+}>();
+
+export async function POST(request: NextRequest) {
+ try {
+ // 고유 ID 생성
+ const syncId = uuidv4();
+
+ // 작업 상태 초기화
+ syncJobs.set(syncId, {
+ status: 'queued',
+ startTime: new Date(),
+ });
+
+ // 비동기 작업 시작 (백그라운드에서 실행)
+ processSyncJob(syncId).catch(error => {
+ console.error('Background sync job failed:', error);
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'failed',
+ endTime: new Date(),
+ error: error.message || 'Unknown error occurred'
+ });
+ });
+
+ // 즉시 응답 반환 (작업 ID 포함)
+ return Response.json({
+ success: true,
+ message: 'Form sync job started',
+ syncId
+ }, { status: 200 });
+
+ } catch (error: any) {
+ console.error('Failed to start sync job:', error);
+ return Response.json({
+ success: false,
+ error: error.message || 'Failed to start sync job'
+ }, { status: 500 });
+ }
+}
+
+// 백그라운드에서 실행되는 동기화 작업
+async function processSyncJob(syncId: string) {
+ try {
+ // 상태 업데이트: 처리 중
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'processing',
+ progress: 0,
+ });
+
+ // 여기서 실제 동기화 로직 가져오기
+ const { syncTagFormMappings } = await import('@/lib/sedp/sync-form');
+
+ // 실제 동기화 작업 실행
+ const result = await syncTagFormMappings();
+
+ // 명시적으로 캐시 무효화 (동적 import 대신 상단에서 import)
+ revalidateTag('form-lists');
+
+ // 상태 업데이트: 완료
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'completed',
+ endTime: new Date(),
+ result,
+ progress: 100,
+ });
+
+ return result;
+ } catch (error: any) {
+ // 에러 발생 시 상태 업데이트
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'failed',
+ endTime: new Date(),
+ error: error.message || 'Unknown error occurred',
+ });
+
+ throw error; // 에러 다시 던지기
+ }
+}
+
+// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용)
+export function getSyncJobStatus(id: string) {
+ return syncJobs.get(id);
+} \ No newline at end of file
diff --git a/app/api/cron/forms/status/route.ts b/app/api/cron/forms/status/route.ts
new file mode 100644
index 00000000..c0e27b2e
--- /dev/null
+++ b/app/api/cron/forms/status/route.ts
@@ -0,0 +1,46 @@
+// app/api/cron/forms/status/route.ts
+import { NextRequest } from 'next/server';
+import { getSyncJobStatus } from '../start/route';
+
+export async function GET(request: NextRequest) {
+ try {
+ // URL에서 작업 ID 가져오기
+ const searchParams = request.nextUrl.searchParams;
+ const syncId = searchParams.get('id');
+
+ if (!syncId) {
+ return Response.json({
+ success: false,
+ error: 'Missing sync ID parameter'
+ }, { status: 400 });
+ }
+
+ // 작업 상태 조회
+ const jobStatus = getSyncJobStatus(syncId);
+
+ if (!jobStatus) {
+ return Response.json({
+ success: false,
+ error: 'Sync job not found'
+ }, { status: 404 });
+ }
+
+ // 작업 상태 반환
+ return Response.json({
+ success: true,
+ status: jobStatus.status,
+ startTime: jobStatus.startTime,
+ endTime: jobStatus.endTime,
+ progress: jobStatus.progress,
+ result: jobStatus.result,
+ error: jobStatus.error
+ }, { status: 200 });
+
+ } catch (error: any) {
+ console.error('Error retrieving sync status:', error);
+ return Response.json({
+ success: false,
+ error: error.message || 'Failed to retrieve sync status'
+ }, { status: 500 });
+ }
+} \ No newline at end of file
diff --git a/app/api/cron/object-classes/route.ts b/app/api/cron/object-classes/route.ts
index 9a574b1b..6743da70 100644
--- a/app/api/cron/object-classes/route.ts
+++ b/app/api/cron/object-classes/route.ts
@@ -1,6 +1,7 @@
// src/app/api/cron/object-classes/route.ts
import { syncObjectClasses } from '@/lib/sedp/sync-object-class';
import { NextRequest } from 'next/server';
+import { revalidateTag } from 'next/cache';
export async function GET(request: NextRequest) {
try {
@@ -8,7 +9,8 @@ export async function GET(request: NextRequest) {
// syncObjectClasses 함수 호출
const result = await syncObjectClasses();
-
+ revalidateTag("equip-class")
+
// 성공 시 결과와 함께 200 OK 반환
return Response.json({ success: true, result }, { status: 200 });
} catch (error: any) {
diff --git a/app/api/cron/projects/route.ts b/app/api/cron/projects/route.ts
index d8e6af51..12c89bdb 100644
--- a/app/api/cron/projects/route.ts
+++ b/app/api/cron/projects/route.ts
@@ -1,6 +1,7 @@
// src/app/api/cron/projects/route.ts
import { syncProjects } from '@/lib/sedp/sync-projects';
import { NextRequest } from 'next/server';
+import { revalidateTag } from 'next/cache';
export async function GET(request: NextRequest) {
try {
@@ -8,7 +9,9 @@ export async function GET(request: NextRequest) {
// syncProjects 함수 호출
const result = await syncProjects();
-
+
+ revalidateTag('project-lists')
+
// 성공 시 결과와 함께 200 OK 반환
return Response.json({ success: true, result }, { status: 200 });
} catch (error: any) {
diff --git a/app/api/cron/tag-types/route.ts b/app/api/cron/tag-types/route.ts
index 35145984..43644833 100644
--- a/app/api/cron/tag-types/route.ts
+++ b/app/api/cron/tag-types/route.ts
@@ -1,5 +1,6 @@
import { syncTagSubfields } from '@/lib/sedp/sync-tag-types';
import { NextRequest } from 'next/server';
+import { revalidateTag } from 'next/cache';
export async function GET(request: NextRequest) {
try {
@@ -7,6 +8,7 @@ export async function GET(request: NextRequest) {
// syncTagSubfields 함수 호출
const result = await syncTagSubfields();
+ revalidateTag('tag-numbering')
// 성공 시 결과와 함께 200 OK 반환
return Response.json({ success: true, result }, { status: 200 });
diff --git a/app/api/cron/tags/start/route.ts b/app/api/cron/tags/start/route.ts
new file mode 100644
index 00000000..b506b9a3
--- /dev/null
+++ b/app/api/cron/tags/start/route.ts
@@ -0,0 +1,133 @@
+// app/api/cron/tags/start/route.ts
+import { NextRequest } from 'next/server';
+import { v4 as uuidv4 } from 'uuid';
+import { revalidateTag } from 'next/cache';
+
+// 동기화 작업의 상태를 저장할 Map
+// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다
+const syncJobs = new Map<string, {
+ status: 'queued' | 'processing' | 'completed' | 'failed';
+ startTime: Date;
+ endTime?: Date;
+ result?: any;
+ error?: string;
+ progress?: number;
+ packageId?: number;
+}>();
+
+export async function POST(request: NextRequest) {
+ try {
+ // 요청 데이터 가져오기
+ let packageId: number | undefined;
+
+ try {
+ const body = await request.json();
+ packageId = body.packageId;
+ } catch (error) {
+ // 요청 본문이 없거나 JSON이 아닌 경우, URL 파라미터 확인
+ const searchParams = request.nextUrl.searchParams;
+ const packageIdParam = searchParams.get('packageId');
+ if (packageIdParam) {
+ packageId = parseInt(packageIdParam, 10);
+ }
+ }
+
+ // 고유 ID 생성
+ const syncId = uuidv4();
+
+ // 작업 상태 초기화
+ syncJobs.set(syncId, {
+ status: 'queued',
+ startTime: new Date(),
+ packageId
+ });
+
+ // 비동기 작업 시작 (백그라운드에서 실행)
+ processTagImport(syncId).catch(error => {
+ console.error('Background tag import job failed:', error);
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'failed',
+ endTime: new Date(),
+ error: error.message || 'Unknown error occurred'
+ });
+ });
+
+ // 즉시 응답 반환 (작업 ID 포함)
+ return Response.json({
+ success: true,
+ message: 'Tag import job started',
+ syncId
+ }, { status: 200 });
+
+ } catch (error: any) {
+ console.error('Failed to start tag import job:', error);
+ return Response.json({
+ success: false,
+ error: error.message || 'Failed to start tag import job'
+ }, { status: 500 });
+ }
+}
+
+// 백그라운드에서 실행되는 태그 가져오기 작업
+async function processTagImport(syncId: string) {
+ try {
+ const jobInfo = syncJobs.get(syncId)!;
+ const packageId = jobInfo.packageId;
+
+ // 상태 업데이트: 처리 중
+ syncJobs.set(syncId, {
+ ...jobInfo,
+ status: 'processing',
+ progress: 0,
+ });
+
+ if (!packageId) {
+ throw new Error('Package ID is required');
+ }
+
+ // 여기서 실제 태그 가져오기 로직 import
+ const { importTagsFromSEDP } = await import('@/lib/sedp/get-tags');
+
+ // 진행 상황 업데이트를 위한 콜백 함수
+ const updateProgress = (progress: number) => {
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ progress
+ });
+ };
+
+ // 실제 태그 가져오기 실행
+ const result = await importTagsFromSEDP(packageId, updateProgress);
+
+ // 명시적으로 캐시 무효화
+ revalidateTag(`tags-${packageId}`);
+ revalidateTag(`forms-${packageId}`);
+
+ // 상태 업데이트: 완료
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'completed',
+ endTime: new Date(),
+ result,
+ progress: 100,
+ });
+
+ return result;
+ } catch (error: any) {
+ // 에러 발생 시 상태 업데이트
+ syncJobs.set(syncId, {
+ ...syncJobs.get(syncId)!,
+ status: 'failed',
+ endTime: new Date(),
+ error: error.message || 'Unknown error occurred',
+ });
+
+ throw error; // 에러 다시 던지기
+ }
+}
+
+// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용)
+export function getSyncJobStatus(id: string) {
+ return syncJobs.get(id);
+} \ No newline at end of file
diff --git a/app/api/cron/tags/status/route.ts b/app/api/cron/tags/status/route.ts
new file mode 100644
index 00000000..9d288f52
--- /dev/null
+++ b/app/api/cron/tags/status/route.ts
@@ -0,0 +1,46 @@
+// app/api/cron/tags/status/route.ts
+import { NextRequest } from 'next/server';
+import { getSyncJobStatus } from '../start/route';
+
+export async function GET(request: NextRequest) {
+ try {
+ // URL에서 작업 ID 가져오기
+ const searchParams = request.nextUrl.searchParams;
+ const syncId = searchParams.get('id');
+
+ if (!syncId) {
+ return Response.json({
+ success: false,
+ error: 'Missing sync ID parameter'
+ }, { status: 400 });
+ }
+
+ // 작업 상태 조회
+ const jobStatus = getSyncJobStatus(syncId);
+
+ if (!jobStatus) {
+ return Response.json({
+ success: false,
+ error: 'Sync job not found'
+ }, { status: 404 });
+ }
+
+ // 작업 상태 반환
+ return Response.json({
+ success: true,
+ status: jobStatus.status,
+ startTime: jobStatus.startTime,
+ endTime: jobStatus.endTime,
+ progress: jobStatus.progress,
+ result: jobStatus.result,
+ error: jobStatus.error
+ }, { status: 200 });
+
+ } catch (error: any) {
+ console.error('Error retrieving tag import status:', error);
+ return Response.json({
+ success: false,
+ error: error.message || 'Failed to retrieve tag import status'
+ }, { status: 500 });
+ }
+} \ No newline at end of file
diff --git a/app/api/upload/basicContract/chunk/route.ts b/app/api/upload/basicContract/chunk/route.ts
new file mode 100644
index 00000000..7100988b
--- /dev/null
+++ b/app/api/upload/basicContract/chunk/route.ts
@@ -0,0 +1,71 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { mkdir, writeFile, appendFile } from 'fs/promises';
+import path from 'path';
+import crypto from 'crypto';
+
+export async function POST(request: NextRequest) {
+ try {
+ const formData = await request.formData();
+
+ const chunk = formData.get('chunk') as File;
+ const filename = formData.get('filename') as string;
+ const chunkIndex = parseInt(formData.get('chunkIndex') as string);
+ const totalChunks = parseInt(formData.get('totalChunks') as string);
+ const fileId = formData.get('fileId') as string;
+
+ if (!chunk || !filename || isNaN(chunkIndex) || isNaN(totalChunks) || !fileId) {
+ return NextResponse.json({ success: false, error: '필수 매개변수가 누락되었습니다' }, { status: 400 });
+ }
+
+ // 임시 디렉토리 생성
+ const tempDir = path.join(process.cwd(), 'temp', fileId);
+ await mkdir(tempDir, { recursive: true });
+
+ // 청크 파일 저장
+ const chunkPath = path.join(tempDir, `chunk-${chunkIndex}`);
+ const buffer = Buffer.from(await chunk.arrayBuffer());
+ await writeFile(chunkPath, buffer);
+
+ // 마지막 청크인 경우 모든 청크를 합쳐 최종 파일 생성
+ if (chunkIndex === totalChunks - 1) {
+ const uploadDir = path.join(process.cwd(), "public", "basicContract", "template");
+ await mkdir(uploadDir, { recursive: true });
+
+ // 파일명 생성
+ const timestamp = Date.now();
+ const randomHash = crypto.createHash('md5')
+ .update(`${filename}-${timestamp}`)
+ .digest('hex')
+ .substring(0, 8);
+ const hashedFileName = `${timestamp}-${randomHash}${path.extname(filename)}`;
+ const finalPath = path.join(uploadDir, hashedFileName);
+
+ // 모든 청크 병합
+ await writeFile(finalPath, Buffer.alloc(0)); // 빈 파일 생성
+ for (let i = 0; i < totalChunks; i++) {
+ const chunkData = await require('fs/promises').readFile(path.join(tempDir, `chunk-${i}`));
+ await appendFile(finalPath, chunkData);
+ }
+
+ // 임시 파일 정리 (비동기로 처리)
+ require('fs/promises').rm(tempDir, { recursive: true, force: true })
+ .catch((e: unknown) => console.error('청크 정리 오류:', e));
+
+ return NextResponse.json({
+ success: true,
+ fileName: filename,
+ filePath: `/basicContract/template/${hashedFileName}`
+ });
+ }
+
+ return NextResponse.json({
+ success: true,
+ chunkIndex,
+ message: `청크 ${chunkIndex + 1}/${totalChunks} 업로드 완료`
+ });
+
+ } catch (error) {
+ console.error('청크 업로드 오류:', error);
+ return NextResponse.json({ success: false, error: '서버 오류' }, { status: 500 });
+ }
+} \ No newline at end of file
diff --git a/app/api/upload/basicContract/complete/route.ts b/app/api/upload/basicContract/complete/route.ts
new file mode 100644
index 00000000..6398c5eb
--- /dev/null
+++ b/app/api/upload/basicContract/complete/route.ts
@@ -0,0 +1,37 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { createBasicContractTemplate } from '@/lib/basic-contract/service';
+import { revalidatePath ,revalidateTag} from 'next/cache';
+
+export async function POST(request: NextRequest) {
+ try {
+ const { templateName,validityPeriod, status, fileName, filePath } = await request.json();
+
+ if (!templateName || !fileName || !filePath) {
+ return NextResponse.json({ success: false, error: '필수 정보가 누락되었습니다' }, { status: 400 });
+ }
+
+ // DB에 저장
+ const { data, error } = await createBasicContractTemplate({
+ templateName,
+ validityPeriod,
+ status,
+ fileName,
+ filePath
+ });
+
+
+ revalidatePath('/evcp/basic-contract-templates');
+ revalidatePath('/'); // 루트 경로 무효화도 시도
+ revalidateTag("basic-contract-templates");
+
+ if (error) {
+ throw new Error(error);
+ }
+
+ return NextResponse.json({ success: true, data });
+
+ } catch (error) {
+ console.error('템플릿 저장 오류:', error);
+ return NextResponse.json({ success: false, error: '서버 오류' }, { status: 500 });
+ }
+} \ No newline at end of file
diff --git a/app/api/upload/signed-contract/route.ts b/app/api/upload/signed-contract/route.ts
new file mode 100644
index 00000000..f26e20ba
--- /dev/null
+++ b/app/api/upload/signed-contract/route.ts
@@ -0,0 +1,57 @@
+// app/api/upload/signed-contract/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import fs from 'fs/promises';
+import path from 'path';
+import { v4 as uuidv4 } from 'uuid';
+import db from "@/db/db";
+import { basicContract } from '@/db/schema';
+import { eq } from 'drizzle-orm';
+import { revalidateTag } from 'next/cache';
+
+export async function POST(request: NextRequest) {
+ try {
+ const formData = await request.formData();
+ const file = formData.get('file') as File;
+ const tableRowId = parseInt(formData.get('tableRowId') as string);
+ const templateName = formData.get('templateName') as string;
+
+ if (!file || !tableRowId || !templateName) {
+ return NextResponse.json({ result: false, error: '필수 파라미터가 누락되었습니다.' }, { status: 400 });
+ }
+
+ const originalName = `${tableRowId}_${templateName}`;
+ const ext = path.extname(originalName);
+ const uniqueName = uuidv4() + ext;
+
+ const publicDir = path.join(process.cwd(), "public", "basicContract");
+ const relativePath = `/basicContract/${uniqueName}`;
+ const absolutePath = path.join(publicDir, uniqueName);
+ const buffer = Buffer.from(await file.arrayBuffer());
+
+ await fs.mkdir(publicDir, { recursive: true });
+ await fs.writeFile(absolutePath, buffer);
+
+ await db.transaction(async (tx) => {
+ await tx
+ .update(basicContract)
+ .set({
+ status: "COMPLETED",
+ fileName: originalName,
+ filePath: relativePath,
+ updatedAt: new Date(),
+ completedAt: new Date()
+ })
+ .where(eq(basicContract.id, tableRowId));
+ });
+
+ // 캐시 무효화
+ revalidateTag("basic-contract-requests");
+ revalidateTag("basicContractView-vendor");
+
+ return NextResponse.json({ result: true });
+ } catch (error) {
+ console.error('서명된 계약서 저장 오류:', error);
+ const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
+ return NextResponse.json({ result: false, error: errorMessage }, { status: 500 });
+ }
+} \ No newline at end of file
diff --git a/app/api/vendors/attachments/download-all/route.ts b/app/api/vendors/attachments/download-all/route.ts
new file mode 100644
index 00000000..23f85786
--- /dev/null
+++ b/app/api/vendors/attachments/download-all/route.ts
@@ -0,0 +1,108 @@
+// /app/api/vendors/attachments/download-all/route.js
+import { NextResponse,NextRequest } from 'next/server';
+import fs from 'fs';
+import path from 'path';
+import JSZip from 'jszip';
+import db from '@/db/db';
+
+import { eq } from 'drizzle-orm';
+import { vendorAttachments, vendors } from '@/db/schema';
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const vendorId = searchParams.get('vendorId');
+
+ if (!vendorId) {
+ return NextResponse.json(
+ { error: "필수 파라미터가 누락되었습니다." },
+ { status: 400 }
+ );
+ }
+
+ // 협력업체 정보 조회
+ const vendor = await db.query.vendors.findFirst({
+ where: eq(vendors.id, parseInt(vendorId, 10))
+ });
+
+ if (!vendor) {
+ return NextResponse.json(
+ { error: `협력업체 정보를 찾을 수 없습니다. (ID: ${vendorId})` },
+ { status: 404 }
+ );
+ }
+
+ // 첨부파일 조회
+ const attachments = await db.select()
+ .from(vendorAttachments)
+ .where(eq(vendorAttachments.vendorId, parseInt(vendorId, 10)));
+
+ if (!attachments.length) {
+ return NextResponse.json(
+ { error: '다운로드할 첨부파일이 없습니다.' },
+ { status: 404 }
+ );
+ }
+
+ // 업로드 기본 경로
+ const basePath = process.env.UPLOAD_DIR || path.join(process.cwd(), 'public');
+
+ // ZIP 생성
+ const zip = new JSZip();
+
+ // 파일 읽기 및 ZIP에 추가
+ await Promise.all(
+ attachments.map(async (attachment) => {
+ const filePath = path.join(basePath, attachment.filePath);
+
+ try {
+ // 파일 존재 확인
+ try {
+ await fs.promises.access(filePath, fs.constants.F_OK);
+ } catch (e) {
+ console.warn(`파일이 존재하지 않습니다: ${filePath}`);
+ return; // 파일이 없으면 건너뜀
+ }
+
+ // 파일 읽기
+ const fileData = await fs.promises.readFile(filePath);
+
+ // ZIP에 파일 추가
+ zip.file(attachment.fileName, fileData);
+ } catch (error) {
+ console.warn(`파일을 처리할 수 없습니다: ${filePath}`, error);
+ // 오류가 있더라도 계속 진행
+ }
+ })
+ );
+
+ // ZIP 생성
+ const zipContent = await zip.generateAsync({
+ type: 'nodebuffer',
+ compression: 'DEFLATE',
+ compressionOptions: { level: 9 }
+ });
+
+ // 파일명 생성
+ const fileName = `${vendor.vendorName || `vendor-${vendorId}`}-attachments.zip`;
+
+ // 응답 헤더 설정
+ const headers = new Headers();
+ headers.set('Content-Disposition', `attachment; filename="${fileName}"`);
+ headers.set('Content-Type', 'application/zip');
+ headers.set('Content-Length', zipContent.length.toString());
+
+ // ZIP 파일 데이터와 함께 응답
+ return new Response(zipContent, {
+ status: 200,
+ headers
+ });
+
+ } catch (error) {
+ console.error('첨부파일 다운로드 오류:', error);
+ return NextResponse.json(
+ { error: "첨부파일 다운로드 준비 중 오류가 발생했습니다." },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/vendors/attachments/download/route.ts b/app/api/vendors/attachments/download/route.ts
new file mode 100644
index 00000000..0151a699
--- /dev/null
+++ b/app/api/vendors/attachments/download/route.ts
@@ -0,0 +1,93 @@
+// /app/api/vendors/attachments/download/route.js (Next.js App Router 기준)
+import { NextRequest, NextResponse } from 'next/server';
+import fs from 'fs';
+import path from 'path';
+import { eq } from 'drizzle-orm'; // 쿼리 빌더
+import { vendorAttachments } from '@/db/schema';
+import db from '@/db/db';
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const fileId = searchParams.get('id');
+ const vendorId = searchParams.get('vendorId');
+
+ if (!fileId || !vendorId) {
+ return NextResponse.json(
+ { error: "필수 파라미터가 누락되었습니다." },
+ { status: 400 }
+ );
+ }
+
+ // 첨부파일 정보 조회
+ const attachment = await db.query.vendorAttachments.findFirst({
+ where: eq(vendorAttachments.id, parseInt(fileId, 10))
+ });
+
+ if (!attachment) {
+ return NextResponse.json(
+ { error: "파일을 찾을 수 없습니다." },
+ { status: 404 }
+ );
+ }
+
+ // 파일 경로 구성
+ const basePath = process.env.UPLOAD_DIR || path.join(process.cwd(), 'public');
+ const filePath = path.join(basePath, attachment.filePath);
+
+ // 파일 존재 확인
+ try {
+ await fs.promises.access(filePath, fs.constants.F_OK);
+ } catch (e) {
+ return NextResponse.json(
+ { error: "파일이 서버에 존재하지 않습니다." },
+ { status: 404 }
+ );
+ }
+
+ // 파일 데이터 읽기
+ const fileBuffer = await fs.promises.readFile(filePath);
+
+
+
+ // 파일 MIME 타입 추정
+ let contentType = 'application/octet-stream';
+ if (attachment.fileName) {
+ const ext = path.extname(attachment.fileName).toLowerCase();
+ switch (ext) {
+ case '.pdf': contentType = 'application/pdf'; break;
+ case '.jpg':
+ case '.jpeg': contentType = 'image/jpeg'; break;
+ case '.png': contentType = 'image/png'; break;
+ case '.doc': contentType = 'application/msword'; break;
+ case '.docx': contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; break;
+ // 필요에 따라 더 많은 타입 추가
+ }
+ }
+
+ // 응답 헤더 설정
+ const headers = new Headers();
+
+ // 파일명에 non-ASCII 문자가 포함될 수 있으므로 인코딩 처리
+ const encodedFileName = encodeURIComponent(attachment.fileName)
+ .replace(/['()]/g, escape) // 추가 이스케이프 필요한 문자들
+ .replace(/\*/g, '%2A');
+
+ // RFC 5987에 따른 인코딩 방식 적용
+ headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
+ headers.set('Content-Type', contentType);
+ headers.set('Content-Length', fileBuffer.length.toString());
+ // 파일 데이터와 함께 응답
+ return new Response(fileBuffer, {
+ status: 200,
+ headers
+ });
+
+ } catch (error) {
+ console.error('파일 다운로드 오류:', error);
+ return NextResponse.json(
+ { error: "파일 다운로드 중 오류가 발생했습니다." },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/vendors/erp/route.ts b/app/api/vendors/erp/route.ts
index 0724eeeb..70573592 100644
--- a/app/api/vendors/erp/route.ts
+++ b/app/api/vendors/erp/route.ts
@@ -3,7 +3,7 @@ import { headers } from 'next/headers';
import { getErrorMessage } from '@/lib/handle-error';
/**
- * 기간계 시스템에 벤더 정보를 전송하는 API 엔드포인트
+ * 기간계 시스템에 협력업체 정보를 전송하는 API 엔드포인트
* 서버 액션 내부에서 호출됨
*/
export async function POST(request: NextRequest) {
@@ -78,7 +78,7 @@ export async function POST(request: NextRequest) {
const result = await response.json();
- // 벤더 코드 검증
+ // 협력업체 코드 검증
if (!result.vendor_code) {
return NextResponse.json(
{ success: false, message: 'Vendor code not provided in ERP response' },