diff options
Diffstat (limited to 'lib/vendor-document/service.ts')
| -rw-r--r-- | lib/vendor-document/service.ts | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/lib/vendor-document/service.ts b/lib/vendor-document/service.ts new file mode 100644 index 00000000..b14a64e0 --- /dev/null +++ b/lib/vendor-document/service.ts @@ -0,0 +1,346 @@ +"use server" + +import { eq, SQL } from "drizzle-orm" +import db from "@/db/db" +import { documentAttachments, documents, issueStages, revisions, vendorDocumentsView } from "@/db/schema/vendorDocu" +import { contracts } from "@/db/schema/vendorData" +import { GetVendorDcoumentsSchema } from "./validations" +import { unstable_cache } from "@/lib/unstable-cache"; +import { filterColumns } from "@/lib/filter-columns"; +import { getErrorMessage } from "@/lib/handle-error"; +import { asc, desc, ilike, inArray, and, gte, lte, not, or , isNotNull, isNull} from "drizzle-orm"; +import { countVendorDocuments, selectVendorDocuments } from "./repository" +import path from "path"; +import fs from "fs/promises"; +import { v4 as uuidv4 } from "uuid" + +/** + * 특정 vendorId에 속한 문서 목록 조회 + */ +export async function getVendorDocumentLists(input: GetVendorDcoumentsSchema, id: number) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + // advancedTable 모드면 filterColumns()로 where 절 구성 + const advancedWhere = filterColumns({ + table: vendorDocumentsView, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + let globalWhere + if (input.search) { + const s = `%${input.search}%` + globalWhere = or(ilike(vendorDocumentsView.title, s), ilike(vendorDocumentsView.docNumber, s) + ) + // 필요시 여러 칼럼 OR조건 (status, priority, etc) + } + + const finalWhere = and(advancedWhere, globalWhere, eq(vendorDocumentsView.contractId, id)); + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(vendorDocumentsView[item.id]) : asc(vendorDocumentsView[item.id]) + ) + : [asc(vendorDocumentsView.createdAt)]; + + // 트랜잭션 내부에서 Repository 호출 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectVendorDocuments(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }); + const total = await countVendorDocuments(tx, finalWhere); + return { data, total }; + }); + + const pageCount = Math.ceil(total / input.perPage); + + + return { data, pageCount }; + } catch (err) { + // 에러 발생 시 디폴트 + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input), String(id)], // Include id in the cache key + { + revalidate: 3600, + tags: [`vendor-docuemnt-${id}`], + } + )(); +} + + +// getDocumentVersionsByDocId 함수 수정 - 업로더 타입으로 필터링 추가 +export async function getDocumentVersionsByDocId( + docId: number, +) { + // 모든 조건을 배열로 관리 + const conditions: SQL<unknown>[] = [eq(issueStages.documentId, docId)]; + + + + // 쿼리 실행 + const rows = await db + .select({ + // stage 정보 + stageId: issueStages.id, + stageName: issueStages.stageName, + planDate: issueStages.planDate, + actualDate: issueStages.actualDate, + + // revision 정보 + revisionId: revisions.id, + revision: revisions.revision, + uploaderType: revisions.uploaderType, + uploaderName: revisions.uploaderName, + comment: revisions.comment, + status: revisions.status, + approvedDate: revisions.approvedDate, + + // attachment 정보 + attachmentId: documentAttachments.id, + fileName: documentAttachments.fileName, + filePath: documentAttachments.filePath, + fileType: documentAttachments.fileType, + DocumentSubmitDate: revisions.createdAt, + }) + .from(issueStages) + .leftJoin(revisions, eq(issueStages.id, revisions.issueStageId)) + .leftJoin(documentAttachments, eq(revisions.id, documentAttachments.revisionId)) + .where(and(...conditions)) + .orderBy(issueStages.id, revisions.id, documentAttachments.id); + + // 결과를 처리하여 프론트엔드 형식으로 변환 + // 스테이지+리비전별로 그룹화 + const stageRevMap = new Map(); + // 리비전이 있는 스테이지 ID 추적 + const stagesWithRevisions = new Set(); + + for (const row of rows) { + const stageId = row.stageId; + + + // 리비전이 있는 경우 처리 + if (row.revisionId) { + // 리비전이 있는 스테이지 추적 + stagesWithRevisions.add(stageId); + + const key = `${stageId}-${row.revisionId}`; + + if (!stageRevMap.has(key)) { + stageRevMap.set(key, { + id: row.revisionId, + stage: row.stageName, + revision: row.revision, + uploaderType: row.uploaderType, + uploaderName: row.uploaderName || null, + comment: row.comment || null, + status: row.status || null, + planDate: row.planDate, + actualDate: row.actualDate, + approvedDate: row.approvedDate, + DocumentSubmitDate: row.DocumentSubmitDate, + attachments: [] + }); + } + + // attachmentId가 있는 경우에만 첨부파일 추가 + if (row.attachmentId) { + stageRevMap.get(key).attachments.push({ + id: row.attachmentId, + fileName: row.fileName, + filePath: row.filePath, + fileType: row.fileType + }); + } + } + } + + + // 최종 결과 생성 + const result = [ + ...stageRevMap.values() + ]; + + // 스테이지 이름으로 정렬하고, 같은 스테이지 내에서는 리비전이 없는 항목이 먼저 오도록 정렬 + result.sort((a, b) => { + if (a.stage !== b.stage) { + return a.stage.localeCompare(b.stage); + } + + // 같은 스테이지 내에서는 리비전이 없는 항목이 먼저 오도록 + if (a.revision === null) return -1; + if (b.revision === null) return 1; + + // 두 항목 모두 리비전이 있는 경우 리비전 번호로 정렬 + return a.revision - b.revision; + }); + + return result; +} +// createRevisionAction 함수 수정 - 확장된 업로더 타입 지원 +export async function createRevisionAction(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 customFileName = formData.get("customFileName") as string; + + // 업로더 타입 추가 (기본값: "vendor") + const uploaderType = formData.get("uploaderType") as string || "vendor" + const uploaderName = formData.get("uploaderName") as string | null + const comment = formData.get("comment") as string | null + + if (!docId || Number.isNaN(docId)) { + throw new Error("Invalid or missing documentId") + } + if (!stage || !revision) { + throw new Error("Missing stage/revision") + } + + // 업로더 타입 검증 + if (!['vendor', 'client', 'shi'].includes(uploaderType)) { + throw new Error(`Invalid uploaderType: ${uploaderType}. Must be one of: vendor, client, shi`); + } + + // 트랜잭션 시작 + return await db.transaction(async (tx) => { + // (1) issueStageId 찾기 (stageName + documentId) + let issueStageId: number; + const stageRecord = await tx + .select() + .from(issueStages) + .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 찾기 또는 생성 (issueStageId + revision 조합) + let revisionId: number; + const revisionRecord = await tx + .select() + .from(revisions) + .where(and(eq(revisions.issueStageId, issueStageId), eq(revisions.revision, revision))) + .limit(1) + + // 기본 상태값 설정 + let status = 'submitted'; + if (uploaderType === 'client') status = 'reviewed'; + if (uploaderType === 'shi') status = 'official'; + + if (!revisionRecord.length) { + // Revision이 없으면 새로 생성 + const [newRevision] = await tx + .insert(revisions) + .values({ + issueStageId, + revision, + uploaderType, + uploaderName: uploaderName || undefined, + comment: comment || undefined, + status, + updatedAt: new Date(), + }) + .returning() + + revisionId = newRevision.id + } else { + // 이미 존재하는 경우, 업로더 타입이 다르면 업데이트 + if (revisionRecord[0].uploaderType !== uploaderType) { + await tx + .update(revisions) + .set({ + uploaderType, + uploaderName: uploaderName || undefined, + comment: comment || undefined, + status, + updatedAt: new Date(), + }) + .where(eq(revisions.id, revisionRecord[0].id)) + } + revisionId = revisionRecord[0].id + } + + // (3) 파일 처리 + const file = formData.get("attachment") as File | null + let attachmentRecord: typeof documentAttachments.$inferSelect | null = null; + + if (file && file.size > 0) { + const originalName = customFileName + const ext = path.extname(originalName) + const uniqueName = uuidv4() + ext + const baseDir = path.join(process.cwd(), "public", "documents") + const savePath = path.join(baseDir, uniqueName) + + const arrayBuffer = await file.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + await fs.writeFile(savePath, buffer) + + // 파일 정보를 documentAttachments 테이블에 저장 + const result = await tx + .insert(documentAttachments) + .values({ + revisionId, + fileName: originalName, + filePath: "/documents/" + uniqueName, + fileSize: file.size, + fileType: ext.replace('.', '').toLowerCase(), + updatedAt: new Date(), + }) + .returning() + + // 첫 번째 결과만 할당 + attachmentRecord = result[0] + } + + // (4) Documents 테이블의 updatedAt 갱신 (docId가 documents.id) + await tx + .update(documents) + .set({ updatedAt: new Date() }) + .where(eq(documents.id, docId)) + + return attachmentRecord + }) +} + + +export async function getStageNamesByDocumentId(documentId: number) { + try { + if (!documentId || Number.isNaN(documentId)) { + throw new Error("Invalid document ID"); + } + + const stageRecords = await db + .select({ stageName: issueStages.stageName }) + .from(issueStages) + .where(eq(issueStages.documentId, documentId)) + .orderBy(issueStages.stageName); + + // stageName 배열로 변환 + return stageRecords.map(record => record.stageName); + } catch (error) { + console.error("Error fetching stage names:", error); + return []; // 오류 발생시 빈 배열 반환 + } +}
\ No newline at end of file |
