summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 17:29:43 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 17:29:43 +0000
commitbc7d627f61a4d055b19d0679b3a4c128b7afcfda (patch)
tree84c765b0334c39246444c0a67916c5174b6e2cc7 /app
parent4bad21ef79fdda5f016e2012ba673d6ee6abb5fc (diff)
(대표님) admin / api / components
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/admin/page.tsx132
-rw-r--r--app/api/admin/clear-test-data/route.ts60
-rw-r--r--app/api/cron/form-tags/start/route.ts11
-rw-r--r--app/api/cron/tags/start/route.ts2
-rw-r--r--app/api/revision-upload/route.ts345
-rw-r--r--app/api/vendor-investigations/[investigationId]/attachments/route.ts8
6 files changed, 385 insertions, 173 deletions
diff --git a/app/[lng]/admin/page.tsx b/app/[lng]/admin/page.tsx
new file mode 100644
index 00000000..04679342
--- /dev/null
+++ b/app/[lng]/admin/page.tsx
@@ -0,0 +1,132 @@
+// app/admin/page.tsx
+'use client'
+
+import { useState } from 'react'
+
+export default function AdminPage() {
+ const [isLoading, setIsLoading] = useState(false)
+ const [lastResult, setLastResult] = useState<string | null>(null)
+
+
+ const clearTestData = async () => {
+ const confirmation = window.confirm(
+ '⚠️ 정말로 모든 테스트 데이터를 삭제하시겠습니까?\n\n삭제될 데이터:\n- Forms\n- Form Metas\n- Form Entries\n- Tags\n\n이 작업은 되돌릴 수 없습니다!'
+ )
+
+ if (!confirmation) return
+
+ setIsLoading(true)
+ setLastResult(null)
+
+ try {
+ const response = await fetch('/api/admin/clear-test-data', {
+ method: 'DELETE',
+ })
+
+ const result = await response.json()
+
+ if (result.success) {
+ setLastResult(`✅ 성공: ${result.message}`)
+ console.log('Deleted counts:', result.deleted)
+ } else {
+ setLastResult(`❌ 실패: ${result.error}`)
+ console.error('Error details:', result.details)
+ }
+ } catch (error) {
+ setLastResult(`❌ 네트워크 오류: ${error}`)
+ console.error('Network error:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const checkApiStatus = async () => {
+ try {
+ const response = await fetch('/api/admin/clear-test-data')
+ const result = await response.json()
+ setLastResult(`ℹ️ API 상태: ${result.message}`)
+ } catch (error) {
+ setLastResult(`❌ API 체크 실패: ${error}`)
+ }
+ }
+
+ return (
+ <div className="min-h-screen bg-gray-50 py-8">
+ <div className="max-w-2xl mx-auto px-4">
+ <div className="bg-white rounded-lg shadow-md p-6">
+ {/* 헤더 */}
+ <div className="border-b pb-4 mb-6">
+ <h1 className="text-2xl font-bold text-gray-900">
+ 🔧 개발 관리자 패널
+ </h1>
+ <p className="text-gray-600 mt-2">
+ 테스트 데이터 관리 및 개발 도구
+ </p>
+ <div className="mt-2 inline-block bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm">
+ Development Mode Only
+ </div>
+ </div>
+
+ {/* 테스트 데이터 삭제 섹션 */}
+ <div className="mb-8">
+ <h2 className="text-lg font-semibold mb-4 text-gray-800">
+ 🗑️ 테스트 데이터 삭제
+ </h2>
+
+ <div className="bg-red-50 border border-red-200 rounded-md p-4 mb-4">
+ <h3 className="font-medium text-red-800 mb-2">삭제될 테이블:</h3>
+ <ul className="text-sm text-red-700 space-y-1">
+ <li>• forms (양식 정보)</li>
+ <li>• form_metas (양식 메타데이터)</li>
+ <li>• form_entries (양식 입력 데이터)</li>
+ <li>• tags (태그 정보)</li>
+ </ul>
+ </div>
+
+ <div className="flex gap-3">
+ <button
+ onClick={clearTestData}
+ disabled={isLoading}
+ className={`px-4 py-2 rounded-md font-medium transition-colors ${
+ isLoading
+ ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
+ : 'bg-red-600 text-white hover:bg-red-700'
+ }`}
+ >
+ {isLoading ? '삭제 중...' : '🗑️ 전체 데이터 삭제'}
+ </button>
+
+ <button
+ onClick={checkApiStatus}
+ disabled={isLoading}
+ className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
+ >
+ 🔍 API 상태 확인
+ </button>
+ </div>
+ </div>
+
+ {/* 결과 표시 */}
+ {lastResult && (
+ <div className="bg-gray-50 border rounded-md p-4">
+ <h3 className="font-medium text-gray-800 mb-2">실행 결과:</h3>
+ <p className="text-sm font-mono whitespace-pre-wrap">
+ {lastResult}
+ </p>
+ </div>
+ )}
+
+ {/* 추가 정보 */}
+ <div className="mt-8 pt-6 border-t">
+ <h3 className="font-medium text-gray-800 mb-2">📋 사용법:</h3>
+ <div className="text-sm text-gray-600 space-y-1">
+ <p>• 이 페이지는 개발 환경에서만 접근 가능합니다</p>
+ <p>• 삭제 전 반드시 확인 창이 표시됩니다</p>
+ <p>• API 엔드포인트: <code className="bg-gray-100 px-1 rounded">/api/admin/clear-test-data</code></p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/app/api/admin/clear-test-data/route.ts b/app/api/admin/clear-test-data/route.ts
new file mode 100644
index 00000000..9a5c8458
--- /dev/null
+++ b/app/api/admin/clear-test-data/route.ts
@@ -0,0 +1,60 @@
+// app/api/admin/clear-test-data/route.ts
+import db from '@/db/db'
+import { formEntries, forms, formMetas, tags } from '@/db/schema/vendorData'
+import { NextRequest } from 'next/server'
+
+export async function DELETE(request: NextRequest) {
+
+ try {
+ // 외래키 참조 순서를 고려하여 삭제 (자식 테이블부터)
+ console.log('Clearing test data...')
+
+ // 1. form_entries 삭제
+ const deletedEntries = await db.delete(formEntries)
+ console.log('Deleted form entries')
+
+ // 2. tags 삭제
+ const deletedTags = await db.delete(tags)
+ console.log('Deleted tags')
+
+ // 3. forms 삭제
+ const deletedForms = await db.delete(forms)
+ console.log('Deleted forms')
+
+ // 4. form_metas 삭제
+ const deletedMetas = await db.delete(formMetas)
+ console.log('Deleted form metas')
+
+ return Response.json({
+ success: true,
+ message: 'All test data cleared successfully',
+ deleted: {
+ formEntries: deletedEntries,
+ tags: deletedTags,
+ forms: deletedForms,
+ formMetas: deletedMetas
+ }
+ })
+ } catch (error) {
+ console.error('Error clearing test data:', error)
+ return Response.json({
+ error: 'Failed to clear data',
+ details: error instanceof Error ? error.message : 'Unknown error'
+ }, { status: 500 })
+ }
+}
+
+// GET 요청도 지원 (브라우저에서 직접 접근 시)
+export async function GET() {
+ if (process.env.NODE_ENV !== 'development') {
+ return Response.json({
+ error: 'Not allowed in production'
+ }, { status: 403 })
+ }
+
+ return Response.json({
+ message: 'Use DELETE method to clear test data',
+ endpoint: '/api/admin/clear-test-data',
+ method: 'DELETE'
+ })
+} \ 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
index 6a029c4c..67fbd73b 100644
--- a/app/api/cron/form-tags/start/route.ts
+++ b/app/api/cron/form-tags/start/route.ts
@@ -1,4 +1,4 @@
-// app/api/cron/tags/start/route.ts
+// app/api/cron/form-tags/start/route.ts
import { NextRequest } from 'next/server';
import { v4 as uuidv4 } from 'uuid';
import { revalidateTag } from 'next/cache';
@@ -15,6 +15,7 @@ const syncJobs = new Map<string, {
projectCode?: string;
formCode?: string;
packageId?: number;
+ mode?: string
}>();
export async function POST(request: NextRequest) {
@@ -23,12 +24,14 @@ export async function POST(request: NextRequest) {
let projectCode: string | undefined;
let formCode: string | undefined;
let packageId: number | undefined;
+ let mode: string | undefined;
const body = await request.json();
projectCode = body.projectCode;
formCode = body.formCode;
packageId = body.contractItemId;
+ mode = body.mode; // 모드 정보 추출
// 고유 ID 생성
@@ -40,7 +43,8 @@ export async function POST(request: NextRequest) {
startTime: new Date(),
formCode,
projectCode,
- packageId
+ packageId,
+ mode
});
@@ -78,6 +82,7 @@ async function processTagImport(syncId: string) {
const formCode = jobInfo.formCode;
const projectCode = jobInfo.projectCode;
const packageId = jobInfo.packageId || 0;
+ const mode = jobInfo.mode || 0;
// 상태 업데이트: 처리 중
syncJobs.set(syncId, {
@@ -105,7 +110,7 @@ async function processTagImport(syncId: string) {
const result = await importTagsFromSEDP(formCode, projectCode, packageId, updateProgress);
// 명시적으로 캐시 무효화
- revalidateTag(`forms-${packageId}`);
+ revalidateTag(`forms-${packageId}-${mode}`);
// 상태 업데이트: 완료
syncJobs.set(syncId, {
diff --git a/app/api/cron/tags/start/route.ts b/app/api/cron/tags/start/route.ts
index 3312aad8..f97d36c5 100644
--- a/app/api/cron/tags/start/route.ts
+++ b/app/api/cron/tags/start/route.ts
@@ -110,7 +110,7 @@ async function processTagImport(syncId: string) {
// 명시적으로 캐시 무효화
revalidateTag(`tags-${packageId}`);
- revalidateTag(`forms-${packageId}`);
+ revalidateTag(`forms-${packageId}-${mode}`);
// 상태 업데이트: 완료
syncJobs.set(syncId, {
diff --git a/app/api/revision-upload/route.ts b/app/api/revision-upload/route.ts
index 2138d674..35344b4b 100644
--- a/app/api/revision-upload/route.ts
+++ b/app/api/revision-upload/route.ts
@@ -1,213 +1,224 @@
-// app/api/revision-upload/route.ts
-import { NextRequest, NextResponse } from 'next/server'
-import { writeFile } from 'fs/promises'
-import { join } from 'path'
-import { v4 as uuidv4 } from 'uuid'
-import path from 'path'
-import db from '@/db/db'
-import { documents, issueStages, revisions, documentAttachments } from '@/db/schema/vendorDocu'
-import { and, eq } from 'drizzle-orm'
+import { NextRequest, NextResponse } from "next/server"
+import { writeFile } from "fs/promises"
+import { join } from "path"
+import { v4 as uuidv4 } from "uuid"
+import path from "path"
+import { revalidateTag } from "next/cache" // ✅ 추가
+
+import db from "@/db/db"
+import {
+ documents,
+ issueStages,
+ revisions,
+ documentAttachments,
+} from "@/db/schema/vendorDocu"
+import { and, eq } from "drizzle-orm"
+
+/* ① change log 유틸 */
+import {
+ logRevisionChange,
+ logAttachmentChange,
+} from "@/lib/vendor-document-list/sync-service"
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
-
- // FormData에서 데이터 추출
- const stage = formData.get("stage") as string | null
- const revision = formData.get("revision") as string | null
- const docIdStr = formData.get("documentId") as string
- const docId = parseInt(docIdStr, 10)
- const uploaderName = formData.get("uploaderName") as string | null
- const comment = formData.get("comment") as string | null
- const mode = formData.get("mode") as string || "new" // 'new' | 'append'
-
- // 파일들 추출
- const attachmentFiles = formData.getAll("attachments") as File[]
-
- // 유효성 검증
- if (!docId || Number.isNaN(docId)) {
- return NextResponse.json(
- { error: "Invalid or missing documentId" },
- { status: 400 }
- )
- }
-
- if (!stage || !revision) {
- return NextResponse.json(
- { error: "Missing stage or revision" },
- { status: 400 }
- )
- }
-
- if (attachmentFiles.length === 0) {
- return NextResponse.json(
- { error: "No files provided" },
- { status: 400 }
- )
- }
- // 파일 크기 검증 (각 파일 최대 3GB)
- const maxFileSize = 3 * 1024 * 1024 * 1024 // 3GB
- for (const file of attachmentFiles) {
- if (file.size > maxFileSize) {
+ /* ------- 파라미터 파싱 ------- */
+ const stage = formData.get("stage") as string | null
+ const revision = formData.get("revision") as string | null
+ const docId = Number(formData.get("documentId"))
+ const uploaderName = formData.get("uploaderName") as string | null
+ const comment = formData.get("comment") as string | null
+ const mode = (formData.get("mode") || "new") as string // 'new'|'append'
+ const targetSystem = (formData.get("targetSystem") as string | null) ?? "DOLCE"
+ const attachmentFiles = formData.getAll("attachments") as File[]
+
+ /* ------- 검증 ------- */
+ if (!docId || Number.isNaN(docId))
+ return NextResponse.json({ error: "Invalid documentId" }, { status: 400 })
+ if (!stage || !revision)
+ return NextResponse.json({ error: "Missing stage or revision" }, { status: 400 })
+ if (!attachmentFiles.length)
+ return NextResponse.json({ error: "No files provided" }, { status: 400 })
+
+ const MAX = 3 * 1024 * 1024 * 1024 // 3 GB
+ for (const f of attachmentFiles)
+ if (f.size > MAX)
return NextResponse.json(
- { error: `파일 ${file.name}이 너무 큽니다. 최대 3GB까지 허용됩니다.` },
+ { error: `${f.name} > 3 GB` },
{ status: 400 }
)
- }
- }
- // 트랜잭션 실행
+ /* ------- 계약 ID 확보 ------- */
+ const [{ contractId }] = await db
+ .select({ contractId: documents.contractId })
+ .from(documents)
+ .where(eq(documents.id, docId))
+ .limit(1)
+
+ /* ------- 트랜잭션 ------- */
const result = await db.transaction(async (tx) => {
- // (1) issueStageId 찾기 또는 생성
+ /* 1) Stage */
let issueStageId: number
- const stageRecord = await tx
- .select()
+ const [stageRow] = await tx
+ .select({ id: issueStages.id })
.from(issueStages)
- .where(and(eq(issueStages.stageName, stage), eq(issueStages.documentId, docId)))
+ .where(and(
+ eq(issueStages.stageName, stage),
+ eq(issueStages.documentId, docId)
+ ))
.limit(1)
-
- if (!stageRecord.length) {
- // Stage가 없으면 새로 생성
- const [newStage] = await tx
- .insert(issueStages)
- .values({
- documentId: docId,
- stageName: stage,
- updatedAt: new Date(),
- })
- .returning()
-
- issueStageId = newStage.id
- } else {
- issueStageId = stageRecord[0].id
- }
-
- // (2) Revision 찾기 또는 생성
+
+ if (!stageRow) {
+ const [s] = await tx.insert(issueStages)
+ .values({ documentId: docId, stageName: stage, updatedAt: new Date() })
+ .returning({ id: issueStages.id })
+ issueStageId = s.id
+ } else issueStageId = stageRow.id
+
+ /* 2) Revision */
+ const today = new Date().toISOString().slice(0, 10)
let revisionId: number
- const revisionRecord = await tx
+ const [revRow] = await tx
.select()
.from(revisions)
- .where(and(eq(revisions.issueStageId, issueStageId), eq(revisions.revision, revision)))
+ .where(and(
+ eq(revisions.issueStageId, issueStageId),
+ eq(revisions.revision, revision)
+ ))
.limit(1)
-
- const currentDate = new Date().toISOString().split('T')[0] // YYYY-MM-DD 형식
-
- if (!revisionRecord.length || mode === 'new') {
- // 새 리비전 생성
- const [newRevision] = await tx
- .insert(revisions)
+
+ if (!revRow || mode === "new") {
+ /* --- CREATE --- */
+ const [newRev] = await tx.insert(revisions)
.values({
issueStageId,
revision,
- uploaderType: "vendor", // 항상 vendor로 고정
- uploaderName: uploaderName || undefined,
- revisionStatus: "SUBMITTED", // 기본 상태
- submittedDate: currentDate, // 제출일 설정
- comment: comment || undefined,
+ uploaderType: "vendor",
+ uploaderName: uploaderName ?? undefined,
+ revisionStatus: "UPLOADED",
+ uploadedAt: today,
+ comment: comment ?? undefined,
updatedAt: new Date(),
})
.returning()
-
- revisionId = newRevision.id
- console.log("✅ 새 리비전 생성:", { revisionId, revision })
+ revisionId = newRev.id
+
+ // change_logs: CREATE
+ await logRevisionChange(
+ contractId,
+ revisionId,
+ "CREATE",
+ newRev,
+ undefined,
+ undefined,
+ uploaderName ?? undefined,
+ [targetSystem]
+ )
} else {
- // 기존 리비전에 파일 추가 (append 모드)
- revisionId = revisionRecord[0].id
-
- // 기존 리비전 정보 업데이트 (코멘트나 업로더명 변경 가능)
- await tx
- .update(revisions)
+ /* --- UPDATE --- */
+ await tx.update(revisions)
.set({
- uploaderName: uploaderName || revisionRecord[0].uploaderName,
- comment: comment || revisionRecord[0].comment,
+ uploaderName: uploaderName ?? revRow.uploaderName,
+ comment: comment ?? revRow.comment,
updatedAt: new Date(),
})
- .where(eq(revisions.id, revisionId))
-
- console.log("✅ 기존 리비전에 파일 추가:", { revisionId, revision })
+ .where(eq(revisions.id, revRow.id))
+
+ const [updated] = await tx
+ .select()
+ .from(revisions)
+ .where(eq(revisions.id, revRow.id))
+
+ revisionId = revRow.id
+
+ await logRevisionChange(
+ contractId,
+ revisionId,
+ "UPDATE",
+ updated,
+ revRow,
+ undefined,
+ uploaderName ?? undefined,
+ [targetSystem]
+ )
}
-
- // (3) 파일들 저장 및 DB 기록
- const uploadedFiles = []
+
+ /* 3) Attachments */
+ const uploadedFiles: any[] = []
const baseDir = join(process.cwd(), "public", "documents")
-
+
for (const file of attachmentFiles) {
- if (file.size > 0) {
- const originalName = file.name
- const ext = path.extname(originalName)
- const uniqueName = uuidv4() + ext
- const savePath = join(baseDir, uniqueName)
-
- // 파일 저장
- const arrayBuffer = await file.arrayBuffer()
- const buffer = Buffer.from(arrayBuffer)
- await writeFile(savePath, buffer)
-
- // DB에 첨부파일 정보 저장
- const [attachmentRecord] = await tx
- .insert(documentAttachments)
- .values({
- revisionId,
- fileName: originalName,
- filePath: "/documents/" + uniqueName,
- fileSize: file.size,
- fileType: ext.replace('.', '').toLowerCase() || undefined,
- updatedAt: new Date(),
- })
- .returning()
-
- uploadedFiles.push({
- id: attachmentRecord.id,
- fileName: originalName,
+ const ext = path.extname(file.name)
+ const fname = uuidv4() + ext
+ const dest = join(baseDir, fname)
+
+ await writeFile(dest, Buffer.from(await file.arrayBuffer()))
+
+ const [att] = await tx.insert(documentAttachments)
+ .values({
+ revisionId,
+ fileName: file.name,
+ filePath: "/documents/" + fname,
fileSize: file.size,
- filePath: attachmentRecord.filePath,
+ fileType: ext.slice(1).toLowerCase() || undefined,
+ updatedAt: new Date(),
})
-
- console.log("✅ 파일 저장 완료:", originalName)
- }
+ .returning()
+
+ uploadedFiles.push({
+ id: att.id,
+ fileName: file.name,
+ fileSize: file.size,
+ filePath: att.filePath,
+ })
+
+ // change_logs: attachment CREATE
+ await logAttachmentChange(
+ contractId,
+ att.id,
+ "CREATE",
+ att,
+ undefined,
+ undefined,
+ uploaderName ?? undefined,
+ [targetSystem]
+ )
}
-
- // (4) Documents 테이블의 updatedAt 갱신
- await tx
- .update(documents)
+
+ /* 4) documents.updatedAt */
+ await tx.update(documents)
.set({ updatedAt: new Date() })
.where(eq(documents.id, docId))
-
- return {
- revisionId,
- stage,
- revision,
- uploaderName,
- comment,
- uploadedFiles,
- mode,
- }
- })
- console.log("✅ 리비전 업로드 완료:", {
- documentId: docId,
- stage,
- revision,
- filesCount: result.uploadedFiles.length,
- mode
+ return { revisionId, stage, revision, uploadedFiles, mode, contractId }
})
+ // ✅ 캐시 무효화 - 트랜잭션 완료 후에 실행
+ try {
+ // enhanced documents 캐시 무효화
+ revalidateTag(`enhanced-documents-${result.contractId}`)
+
+ // sync status 관련 캐시도 무효화 (필요시)
+ revalidateTag(`sync-status-${result.contractId}`)
+
+ console.log(`✅ Cache invalidated for contract ${result.contractId}`)
+ } catch (cacheError) {
+ console.warn('⚠️ Cache invalidation failed:', cacheError)
+ // 캐시 무효화 실패해도 업로드는 성공으로 처리
+ }
+
return NextResponse.json({
success: true,
- message: `${result.uploadedFiles.length}개 파일이 성공적으로 업로드되었습니다.`,
+ message: `${result.uploadedFiles.length}개 파일 업로드 완료`,
data: result,
})
-
- } catch (error) {
- console.error('❌ 리비전 업로드 오류:', error)
-
+ } catch (e) {
+ console.error("revision-upload error:", e)
return NextResponse.json(
- {
- error: 'Failed to upload revision',
- details: error instanceof Error ? error.message : String(error),
- },
- { status: 500 }
+ { error: "Failed to upload revision", details: String(e) },
+ { status: 500 },
)
}
} \ No newline at end of file
diff --git a/app/api/vendor-investigations/[investigationId]/attachments/route.ts b/app/api/vendor-investigations/[investigationId]/attachments/route.ts
index 80513e28..6787de7e 100644
--- a/app/api/vendor-investigations/[investigationId]/attachments/route.ts
+++ b/app/api/vendor-investigations/[investigationId]/attachments/route.ts
@@ -7,12 +7,16 @@ import db from "@/db/db"
import { vendorInvestigationAttachments } from "@/db/schema"
import { eq } from "drizzle-orm";
+type Ctx = { params: Promise<{ investigationId: string }> };
+
export async function POST(
req: NextRequest,
- { params }: { params: { investigationId: string } }
+ context: Ctx // ① 두 번째 인자를 통째로 받는다
) {
try {
- const investigationId = parseInt(params.investigationId)
+ const { investigationId: idParam } = await context.params; // ② 여기서 await
+ const investigationId = Number(idParam);
+
if (!investigationId) {
return NextResponse.json({ error: "Invalid investigation ID" }, { status: 400 })
}