summaryrefslogtreecommitdiff
path: root/lib/vendor-document/service.ts
blob: d81e703c986e1cca37dd7c434031a8d7d4d40944 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
"use server"

import { eq, SQL } from "drizzle-orm"
import db from "@/db/db"
import { documentAttachments, documents, issueStages, revisions, vendorDocumentsView } from "@/db/schema/vendorDocu"
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"
import { contractItems } from "@/db/schema"

/**
 * 특정 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 []; // 오류 발생시 빈 배열 반환
  }
}


// Define the return types
export interface Document {
  id: number;
  docNumber: string;
  title: string;
}

export interface IssueStage {
  id: number;
  stageName: string;
}

export interface Revision {
  revision: string;
}

// Server Action: Fetch documents by packageId (contractItems.id)
export async function fetchDocumentsByPackageId(packageId: number): Promise<Document[]> {
  try {
    // First, find the contractId from contractItems where id = packageId
    const contractItemResult = await db.select({ contractId: contractItems.contractId })
      .from(contractItems)
      .where(eq(contractItems.id, packageId))
      .limit(1);

    if (!contractItemResult.length) {
      return [];
    }

    const contractId = contractItemResult[0].contractId;

    // Then, get documents associated with this contractId
    const docsResult = await db.select({
      id: documents.id,
      docNumber: documents.docNumber,
      title: documents.title,
    })
      .from(documents)
      .where(eq(documents.contractId, contractId))
      .orderBy(documents.docNumber);

    return docsResult;
  } catch (error) {
    console.error("Error fetching documents:", error);
    return [];
  }
}

// Server Action: Fetch stages by documentId
export async function fetchStagesByDocumentId(documentId: number): Promise<IssueStage[]> {
  try {
    const stagesResult = await db.select({
      id: issueStages.id,
      stageName: issueStages.stageName,
    })
      .from(issueStages)
      .where(eq(issueStages.documentId, documentId))
      .orderBy(issueStages.stageName);

    return stagesResult;
  } catch (error) {
    console.error("Error fetching stages:", error);
    return [];
  }
}

// Server Action: Fetch revisions by documentId and stageName
export async function fetchRevisionsByStageParams(
  documentId: number, 
  stageName: string
): Promise<Revision[]> {
  try {
    // First, find the issueStageId
    const stageResult = await db.select({ id: issueStages.id })
      .from(issueStages)
      .where(
        and(
          eq(issueStages.documentId, documentId),
          eq(issueStages.stageName, stageName)
        )
      )
      .limit(1);

    if (!stageResult.length) {
      return [];
    }

    const issueStageId = stageResult[0].id;

    // Then, get revisions for this stage
    const revisionsResult = await db.select({
      revision: revisions.revision,
    })
      .from(revisions)
      .where(eq(revisions.issueStageId, issueStageId))
      .orderBy(revisions.revision);

    return revisionsResult;
  } catch (error) {
    console.error("Error fetching revisions:", error);
    return [];
  }
}