summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list')
-rw-r--r--lib/vendor-document-list/dolce-upload-service.ts606
-rw-r--r--lib/vendor-document-list/enhanced-document-service.ts163
-rw-r--r--lib/vendor-document-list/plant/document-stages-table.tsx39
-rw-r--r--lib/vendor-document-list/ship/send-to-shi-button.tsx18
-rw-r--r--lib/vendor-document-list/sync-service.ts65
-rw-r--r--lib/vendor-document-list/table/enhanced-documents-table.tsx90
6 files changed, 606 insertions, 375 deletions
diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts
index 2d6a83c6..84ae4525 100644
--- a/lib/vendor-document-list/dolce-upload-service.ts
+++ b/lib/vendor-document-list/dolce-upload-service.ts
@@ -5,6 +5,8 @@ import { eq, and, desc, sql, inArray, min } from "drizzle-orm"
import { v4 as uuidv4 } from "uuid"
import path from "path"
import * as crypto from "crypto"
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
export interface DOLCEUploadResult {
success: boolean
@@ -87,7 +89,7 @@ interface DOLCEFileMapping {
function getFileReaderConfig(): FileReaderConfig {
const isProduction = process.env.NODE_ENV === "production";
-
+
if (isProduction) {
return {
baseDir: process.env.NAS_PATH || "/evcp_nas", // NAS 기본 경로
@@ -118,13 +120,13 @@ class DOLCEUploadService {
): Promise<DOLCEUploadResult> {
try {
console.log(`Starting DOLCE upload for contract ${projectId}, revisions: ${revisionIds.join(', ')}`)
-
+
// 1. 계약 정보 조회 (프로젝트 코드, 벤더 코드 등)
const contractInfo = await this.getContractInfo(projectId)
if (!contractInfo) {
throw new Error(`Contract info not found for ID: ${projectId}`)
}
-
+
// 2. 업로드할 리비전 정보 조회
const revisionsToUpload = await this.getRevisionsForUpload(revisionIds)
if (revisionsToUpload.length === 0) {
@@ -134,7 +136,7 @@ class DOLCEUploadService {
uploadedFiles: 0
}
}
-
+
let uploadedDocuments = 0
let uploadedFiles = 0
const errors: string[] = []
@@ -143,19 +145,19 @@ class DOLCEUploadService {
fileResults: [],
mappingResults: []
}
-
+
// 3. 각 리비전별로 처리
for (const revision of revisionsToUpload) {
try {
console.log(`Processing revision ${revision.revision} for document ${revision.documentNo}`)
-
+
// 3-1. UploadId 미리 생성 (파일이 있는 경우에만)
let uploadId: string | undefined
if (revision.attachments && revision.attachments.length > 0) {
uploadId = uuidv4() // 문서 업로드 시 사용할 UploadId 미리 생성
console.log(`Generated UploadId for document upload: ${uploadId}`)
}
-
+
// 3-2. 문서 정보 업로드 (UploadId 포함)
const dolceDoc = this.transformToDoLCEDocument(
revision,
@@ -163,43 +165,43 @@ class DOLCEUploadService {
uploadId, // 미리 생성된 UploadId 사용
contractInfo.vendorCode,
)
-
+
const docResult = await this.uploadDocument([dolceDoc], userId)
if (!docResult.success) {
errors.push(`Document upload failed for ${revision.documentNo}: ${docResult.error}`)
continue // 문서 업로드 실패 시 다음 리비전으로 넘어감
}
-
+
uploadedDocuments++
results.documentResults.push(docResult)
console.log(`✅ Document uploaded successfully: ${revision.documentNo}`)
-
+
// 3-3. 파일 업로드 (이미 생성된 UploadId 사용)
if (uploadId && revision.attachments && revision.attachments.length > 0) {
try {
// 파일 업로드 시 이미 생성된 UploadId 사용
const fileUploadResults = await this.uploadFiles(
- revision.attachments,
- userId,
+ revision.attachments,
+ userId,
uploadId // 이미 생성된 UploadId 전달
)
-
+
} catch (fileError) {
errors.push(`File upload failed for ${revision.documentNo}: ${fileError instanceof Error ? fileError.message : 'Unknown error'}`)
console.error(`❌ File upload failed for ${revision.documentNo}:`, fileError)
}
}
-
+
// 3-5. 성공한 리비전의 상태 업데이트
await this.updateRevisionStatus(revision.id, 'SUBMITTED', uploadId)
-
+
} catch (error) {
const errorMessage = `Failed to process revision ${revision.revision}: ${error instanceof Error ? error.message : 'Unknown error'}`
errors.push(errorMessage)
console.error(errorMessage, error)
}
}
-
+
return {
success: errors.length === 0,
uploadedDocuments,
@@ -207,7 +209,7 @@ class DOLCEUploadService {
errors: errors.length > 0 ? errors : undefined,
results
}
-
+
} catch (error) {
console.error('DOLCE upload failed:', error)
throw error
@@ -216,22 +218,34 @@ class DOLCEUploadService {
/**
* 계약 정보 조회
*/
- private async getContractInfo(projectId: number) {
+ private async getContractInfo(projectId: number): Promise<{
+ projectCode: string;
+ vendorCode: string;
+ } | null> {
+
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.companyId) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+
const [result] = await db
.select({
projectCode: projects.code,
- vendorCode: vendors.vendorCode,
- contractNo: contracts.contractNo
+ vendorCode: vendors.vendorCode
})
.from(contracts)
.innerJoin(projects, eq(contracts.projectId, projects.id))
.innerJoin(vendors, eq(contracts.vendorId, vendors.id))
- .where(eq(contracts.projectId, projectId))
+ .where(and(eq(contracts.projectId, projectId), eq(contracts.vendorId, Number(session.user.companyId))))
.limit(1)
- return result
+ return result?.projectCode && result?.vendorCode
+ ? { projectCode: result.projectCode, vendorCode: result.vendorCode }
+ : null
}
+
/**
* 각 issueStageId별로 첫 번째 revision 정보를 조회
*/
@@ -264,7 +278,7 @@ class DOLCEUploadService {
.select({
// revision 테이블 정보
id: revisions.id,
- registerId:revisions.registerId,
+ registerId: revisions.registerId,
revision: revisions.revision, // revisionNo가 아니라 revision
revisionStatus: revisions.revisionStatus,
uploaderId: revisions.uploaderId,
@@ -341,181 +355,181 @@ class DOLCEUploadService {
return revisionsWithAttachments
}
-/**
- * 파일 업로드 (PWPUploadService.ashx) - 수정된 버전
- * @param attachments 업로드할 첨부파일 목록
- * @param userId 사용자 ID
- * @param uploadId 이미 생성된 UploadId (문서 업로드 시 생성됨)
- */
-private async uploadFiles(
- attachments: any[],
- userId: string,
- uploadId: string // 이미 생성된 UploadId를 매개변수로 받음
-): Promise<Array<{ uploadId: string, fileId: string, filePath: string }>> {
- const uploadResults = []
- const resultDataArray: ResultData[] = []
+ /**
+ * 파일 업로드 (PWPUploadService.ashx) - 수정된 버전
+ * @param attachments 업로드할 첨부파일 목록
+ * @param userId 사용자 ID
+ * @param uploadId 이미 생성된 UploadId (문서 업로드 시 생성됨)
+ */
+ private async uploadFiles(
+ attachments: any[],
+ userId: string,
+ uploadId: string // 이미 생성된 UploadId를 매개변수로 받음
+ ): Promise<Array<{ uploadId: string, fileId: string, filePath: string }>> {
+ const uploadResults = []
+ const resultDataArray: ResultData[] = []
- for (let i = 0; i < attachments.length; i++) {
- const attachment = attachments[i]
- try {
- // FileId만 새로 생성 (UploadId는 이미 생성된 것 사용)
- const fileId = uuidv4()
+ for (let i = 0; i < attachments.length; i++) {
+ const attachment = attachments[i]
+ try {
+ // FileId만 새로 생성 (UploadId는 이미 생성된 것 사용)
+ const fileId = uuidv4()
- console.log(`Uploading file with predefined UploadId: ${uploadId}, FileId: ${fileId}`)
+ console.log(`Uploading file with predefined UploadId: ${uploadId}, FileId: ${fileId}`)
- // 파일 데이터 읽기
- const fileBuffer = await this.getFileBuffer(attachment.filePath)
+ // 파일 데이터 읽기
+ const fileBuffer = await this.getFileBuffer(attachment.filePath)
- const uploadUrl = `${this.UPLOAD_SERVICE_URL}?UploadId=${uploadId}&FileId=${fileId}`
+ const uploadUrl = `${this.UPLOAD_SERVICE_URL}?UploadId=${uploadId}&FileId=${fileId}`
- const response = await fetch(uploadUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/octet-stream',
- },
- body: fileBuffer
- })
+ const response = await fetch(uploadUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ },
+ body: fileBuffer
+ })
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`File upload failed: HTTP ${response.status} - ${errorText}`)
- }
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`File upload failed: HTTP ${response.status} - ${errorText}`)
+ }
- const dolceFilePath = await response.text() // DOLCE에서 반환하는 파일 경로
-
- // 업로드 성공 후 documentAttachments 테이블 업데이트
- await db
- .update(documentAttachments)
- .set({
- uploadId: uploadId, // 이미 생성된 UploadId 사용
- fileId: fileId,
- uploadedBy: userId,
- dolceFilePath: dolceFilePath,
- uploadedAt: new Date(),
- updatedAt: new Date()
+ const dolceFilePath = await response.text() // DOLCE에서 반환하는 파일 경로
+
+ // 업로드 성공 후 documentAttachments 테이블 업데이트
+ await db
+ .update(documentAttachments)
+ .set({
+ uploadId: uploadId, // 이미 생성된 UploadId 사용
+ fileId: fileId,
+ uploadedBy: userId,
+ dolceFilePath: dolceFilePath,
+ uploadedAt: new Date(),
+ updatedAt: new Date()
+ })
+ .where(eq(documentAttachments.id, attachment.id))
+
+ uploadResults.push({
+ uploadId,
+ fileId,
+ filePath: dolceFilePath
})
- .where(eq(documentAttachments.id, attachment.id))
- uploadResults.push({
- uploadId,
- fileId,
- filePath: dolceFilePath
- })
-
- // ResultData 객체 생성 (PWPUploadResultService 호출용)
- const fileStats = await this.getFileStats(attachment.filePath) // 파일 통계 정보 조회
-
- const resultData: ResultData = {
- FileId: fileId,
- UploadId: uploadId,
- FileSeq: i + 1, // 1부터 시작하는 시퀀스
- FileName: attachment.fileName,
- FileRelativePath: dolceFilePath,
- FileSize: fileStats.size,
- FileCreateDT: fileStats.birthtime.toISOString(),
- FileWriteDT: fileStats.mtime.toISOString(),
- OwnerUserId: userId
- }
+ // ResultData 객체 생성 (PWPUploadResultService 호출용)
+ const fileStats = await this.getFileStats(attachment.filePath) // 파일 통계 정보 조회
+
+ const resultData: ResultData = {
+ FileId: fileId,
+ UploadId: uploadId,
+ FileSeq: i + 1, // 1부터 시작하는 시퀀스
+ FileName: attachment.fileName,
+ FileRelativePath: dolceFilePath,
+ FileSize: fileStats.size,
+ FileCreateDT: fileStats.birthtime.toISOString(),
+ FileWriteDT: fileStats.mtime.toISOString(),
+ OwnerUserId: userId
+ }
- resultDataArray.push(resultData)
+ resultDataArray.push(resultData)
- console.log(`✅ File uploaded successfully: ${attachment.fileName} -> ${dolceFilePath}`)
- console.log(`✅ DB updated for attachment ID: ${attachment.id}`)
+ console.log(`✅ File uploaded successfully: ${attachment.fileName} -> ${dolceFilePath}`)
+ console.log(`✅ DB updated for attachment ID: ${attachment.id}`)
- // 🧪 DOLCE 업로드 확인 테스트
- try {
- const testResult = await this.testDOLCEFileDownload(fileId, userId, attachment.fileName)
- if (testResult.success) {
- console.log(`✅ DOLCE 업로드 확인 성공: ${attachment.fileName}`)
- } else {
- console.warn(`⚠️ DOLCE 업로드 확인 실패: ${attachment.fileName} - ${testResult.error}`)
+ // 🧪 DOLCE 업로드 확인 테스트
+ try {
+ const testResult = await this.testDOLCEFileDownload(fileId, userId, attachment.fileName)
+ if (testResult.success) {
+ console.log(`✅ DOLCE 업로드 확인 성공: ${attachment.fileName}`)
+ } else {
+ console.warn(`⚠️ DOLCE 업로드 확인 실패: ${attachment.fileName} - ${testResult.error}`)
+ }
+ } catch (testError) {
+ console.warn(`⚠️ DOLCE 업로드 확인 중 오류: ${attachment.fileName}`, testError)
}
- } catch (testError) {
- console.warn(`⚠️ DOLCE 업로드 확인 중 오류: ${attachment.fileName}`, testError)
- }
- } catch (error) {
- console.error(`❌ File upload failed for ${attachment.fileName}:`, error)
- throw error
+ } catch (error) {
+ console.error(`❌ File upload failed for ${attachment.fileName}:`, error)
+ throw error
+ }
}
- }
- // 모든 파일 업로드가 완료된 후 PWPUploadResultService 호출
- if (resultDataArray.length > 0) {
- try {
- await this.finalizeUploadResult(resultDataArray)
- console.log(`✅ Upload result finalized for UploadId: ${uploadId}`)
- } catch (error) {
- console.error(`❌ Failed to finalize upload result for UploadId: ${uploadId}`, error)
- // 파일 업로드는 성공했지만 결과 저장 실패 - 로그만 남기고 계속 진행
+ // 모든 파일 업로드가 완료된 후 PWPUploadResultService 호출
+ if (resultDataArray.length > 0) {
+ try {
+ await this.finalizeUploadResult(resultDataArray)
+ console.log(`✅ Upload result finalized for UploadId: ${uploadId}`)
+ } catch (error) {
+ console.error(`❌ Failed to finalize upload result for UploadId: ${uploadId}`, error)
+ // 파일 업로드는 성공했지만 결과 저장 실패 - 로그만 남기고 계속 진행
+ }
}
+
+ return uploadResults
}
- return uploadResults
-}
+ private async finalizeUploadResult(resultDataArray: ResultData[]): Promise<void> {
+ const url = `${this.BASE_URL}/PWPUploadResultService.ashx?`
-private async finalizeUploadResult(resultDataArray: ResultData[]): Promise<void> {
- const url = `${this.BASE_URL}/PWPUploadResultService.ashx?`
-
- try {
- const jsonData = JSON.stringify(resultDataArray)
- const dataBuffer = Buffer.from(jsonData, 'utf-8')
+ try {
+ const jsonData = JSON.stringify(resultDataArray)
+ const dataBuffer = Buffer.from(jsonData, 'utf-8')
- console.log(`Calling PWPUploadResultService with ${resultDataArray.length} files`)
- console.log('ResultData:', JSON.stringify(resultDataArray, null, 2))
+ console.log(`Calling PWPUploadResultService with ${resultDataArray.length} files`)
+ console.log('ResultData:', JSON.stringify(resultDataArray, null, 2))
- const response = await fetch(url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: dataBuffer
- })
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: dataBuffer
+ })
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`PWPUploadResultService failed: HTTP ${response.status} - ${errorText}`)
- }
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`PWPUploadResultService failed: HTTP ${response.status} - ${errorText}`)
+ }
- const result = await response.text()
-
- if (result !== 'Success') {
- console.log(result,"돌체 업로드 실패")
- throw new Error(`PWPUploadResultService returned unexpected result: ${result}`)
- }
+ const result = await response.text()
- console.log('✅ PWPUploadResultService call successful')
+ if (result !== 'Success') {
+ console.log(result, "돌체 업로드 실패")
+ throw new Error(`PWPUploadResultService returned unexpected result: ${result}`)
+ }
- } catch (error) {
- console.error('❌ PWPUploadResultService call failed:', error)
- throw error
- }
-}
+ console.log('✅ PWPUploadResultService call successful')
-// 파일 통계 정보 조회 헬퍼 메서드 (파일시스템에서 파일 정보를 가져옴)
-private async getFileStats(filePath: string): Promise<{ size: number, birthtime: Date, mtime: Date }> {
- try {
- // Node.js 환경이라면 fs.stat 사용
- const fs = require('fs').promises
- const stats = await fs.stat(filePath)
-
- return {
- size: stats.size,
- birthtime: stats.birthtime,
- mtime: stats.mtime
+ } catch (error) {
+ console.error('❌ PWPUploadResultService call failed:', error)
+ throw error
}
- } catch (error) {
- console.warn(`Could not get file stats for ${filePath}, using defaults`)
- // 파일 정보를 가져올 수 없는 경우 기본값 사용
- const now = new Date()
- return {
- size: 0,
- birthtime: now,
- mtime: now
+ }
+
+ // 파일 통계 정보 조회 헬퍼 메서드 (파일시스템에서 파일 정보를 가져옴)
+ private async getFileStats(filePath: string): Promise<{ size: number, birthtime: Date, mtime: Date }> {
+ try {
+ // Node.js 환경이라면 fs.stat 사용
+ const fs = require('fs').promises
+ const stats = await fs.stat(filePath)
+
+ return {
+ size: stats.size,
+ birthtime: stats.birthtime,
+ mtime: stats.mtime
+ }
+ } catch (error) {
+ console.warn(`Could not get file stats for ${filePath}, using defaults`)
+ // 파일 정보를 가져올 수 없는 경우 기본값 사용
+ const now = new Date()
+ return {
+ size: 0,
+ birthtime: now,
+ mtime: now
+ }
}
}
-}
/**
* 문서 정보 업로드 (DetailDwgReceiptMgmtEdit)
@@ -604,125 +618,125 @@ private async getFileStats(filePath: string): Promise<{ size: number, birthtime:
/**
* 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용)
*/
-/**
- * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용)
- */
-private transformToDoLCEDocument(
- revision: any,
- contractInfo: any,
- uploadId?: string,
- vendorCode?: string,
-): DOLCEDocument {
- // Mode 결정: registerId가 있으면 MOD, 없으면 ADD
- let mode: "ADD" | "MOD" = "ADD" // 기본값은 ADD
-
- if (revision.registerId) {
- mode = "MOD"
- } else {
- mode = "ADD"
- }
+ /**
+ * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용)
+ */
+ private transformToDoLCEDocument(
+ revision: any,
+ contractInfo: any,
+ uploadId?: string,
+ vendorCode?: string,
+ ): DOLCEDocument {
+ // Mode 결정: registerId가 있으면 MOD, 없으면 ADD
+ let mode: "ADD" | "MOD" = "ADD" // 기본값은 ADD
+
+ if (revision.registerId) {
+ mode = "MOD"
+ } else {
+ mode = "ADD"
+ }
- // RegisterKind 결정: usage와 usageType에 따라 설정
- let registerKind = "APPR" // 기본값
-
- if (revision.usage && revision.usage !== 'DEFAULT') {
- switch (revision.usage) {
- case "APPROVAL":
- if (revision.usageType === "Full") {
- registerKind = "APPR"
- } else if (revision.usageType === "Partial") {
- registerKind = "APPR-P"
- } else {
- registerKind = "APPR" // 기본값
- }
- break
-
- case "WORKING":
- if (revision.usageType === "Full") {
- registerKind = "WORK"
- } else if (revision.usageType === "Partial") {
- registerKind = "WORK-P"
- } else {
- registerKind = "WORK" // 기본값
- }
- break
+ // RegisterKind 결정: usage와 usageType에 따라 설정
+ let registerKind = "APPR" // 기본값
+
+ if (revision.usage && revision.usage !== 'DEFAULT') {
+ switch (revision.usage) {
+ case "APPROVAL":
+ if (revision.usageType === "Full") {
+ registerKind = "APPR"
+ } else if (revision.usageType === "Partial") {
+ registerKind = "APPR-P"
+ } else {
+ registerKind = "APPR" // 기본값
+ }
+ break
+
+ case "WORKING":
+ if (revision.usageType === "Full") {
+ registerKind = "WORK"
+ } else if (revision.usageType === "Partial") {
+ registerKind = "WORK-P"
+ } else {
+ registerKind = "WORK" // 기본값
+ }
+ break
- case "The 1st":
- registerKind = "FMEA-1"
- break
+ case "The 1st":
+ registerKind = "FMEA-1"
+ break
- case "The 2nd":
- registerKind = "FMEA-2"
- break
+ case "The 2nd":
+ registerKind = "FMEA-2"
+ break
- case "Pre":
- registerKind = "RECP"
- break
+ case "Pre":
+ registerKind = "RECP"
+ break
- case "Working":
- registerKind = "RECW"
- break
+ case "Working":
+ registerKind = "RECW"
+ break
- case "Mark-Up":
- registerKind = "CMTM"
- break
+ case "Mark-Up":
+ registerKind = "CMTM"
+ break
- default:
- console.warn(`Unknown usage type: ${revision.usage}, using default APPR`)
- registerKind = "APPR" // 기본값
- break
+ default:
+ console.warn(`Unknown usage type: ${revision.usage}, using default APPR`)
+ registerKind = "APPR" // 기본값
+ break
+ }
+ } else {
+ console.warn(`No usage specified for revision ${revision.revision}, using default APPR`)
}
- } else {
- console.warn(`No usage specified for revision ${revision.revision}, using default APPR`)
- }
- // Serial Number 계산 함수
- const getSerialNumber = (revisionValue: string): number => {
- if (!revisionValue) {
- return 1
- }
+ // Serial Number 계산 함수
+ const getSerialNumber = (revisionValue: string): number => {
+ if (!revisionValue) {
+ return 1
+ }
- // 먼저 숫자인지 확인
- const numericValue = parseInt(revisionValue)
- if (!isNaN(numericValue)) {
- return numericValue
- }
+ // 먼저 숫자인지 확인
+ const numericValue = parseInt(revisionValue)
+ if (!isNaN(numericValue)) {
+ return numericValue
+ }
- // 문자인 경우 (a=1, b=2, c=3, ...)
- if (typeof revisionValue === 'string' && revisionValue.length === 1) {
- const charCode = revisionValue.toLowerCase().charCodeAt(0)
- if (charCode >= 97 && charCode <= 122) { // a-z
- return charCode - 96 // a=1, b=2, c=3, ...
+ // 문자인 경우 (a=1, b=2, c=3, ...)
+ if (typeof revisionValue === 'string' && revisionValue.length === 1) {
+ const charCode = revisionValue.toLowerCase().charCodeAt(0)
+ if (charCode >= 97 && charCode <= 122) { // a-z
+ return charCode - 96 // a=1, b=2, c=3, ...
+ }
}
+
+ // 기본값
+ return 1
}
- // 기본값
- return 1
- }
+ console.log(`Transform to DOLCE: Mode=${mode}, RegisterKind=${registerKind}, Usage=${revision.usage}, UsageType=${revision.usageType}`)
- console.log(`Transform to DOLCE: Mode=${mode}, RegisterKind=${registerKind}, Usage=${revision.usage}, UsageType=${revision.usageType}`)
-
- return {
- Mode: mode,
- Status: revision.revisionStatus || "Standby",
- RegisterId: revision.registerId || 0, // registerId가 없으면 0 (ADD 모드)
- ProjectNo: contractInfo.projectCode,
- Discipline: revision.discipline || "DL",
- DrawingKind: revision.drawingKind || "B3",
- DrawingNo: revision.documentNo,
- DrawingName: revision.documentName,
- RegisterGroupId: revision.registerGroupId || 0,
- RegisterSerialNo: getSerialNumber(revision.revision || "1"),
- RegisterKind: registerKind, // usage/usageType에 따라 동적 설정
- DrawingRevNo: revision.revision || "-",
- Category: revision.category || "TS",
- Receiver: null,
- Manager: revision.managerNo || "202206", // 담당자 번호 사용
- RegisterDesc: revision.comment || "System upload",
- UploadId: uploadId,
- RegCompanyCode: vendorCode || "A0005531" // 벤더 코드
+ return {
+ Mode: mode,
+ Status: revision.revisionStatus || "Standby",
+ RegisterId: revision.registerId || 0, // registerId가 없으면 0 (ADD 모드)
+ ProjectNo: contractInfo.projectCode,
+ Discipline: revision.discipline || "DL",
+ DrawingKind: revision.drawingKind || "B3",
+ DrawingNo: revision.documentNo,
+ DrawingName: revision.documentName,
+ RegisterGroupId: revision.registerGroupId || 0,
+ RegisterSerialNo: getSerialNumber(revision.revision || "1"),
+ RegisterKind: registerKind, // usage/usageType에 따라 동적 설정
+ DrawingRevNo: revision.revision || "-",
+ Category: revision.category || "TS",
+ Receiver: null,
+ Manager: revision.managerNo || "202206", // 담당자 번호 사용
+ RegisterDesc: revision.comment || "System upload",
+ UploadId: uploadId,
+ RegCompanyCode: vendorCode || "A0005531" // 벤더 코드
+ }
}
-}
/**
* 파일 매핑 데이터 변환
*/
@@ -769,28 +783,28 @@ private transformToDoLCEDocument(
private async getFileBuffer(filePath: string): Promise<ArrayBuffer> {
try {
console.log(`📂 파일 읽기 요청: ${filePath}`);
-
+
if (filePath.startsWith('http')) {
// ✅ URL인 경우 직접 다운로드 (기존과 동일)
console.log(`🌐 HTTP URL에서 파일 다운로드: ${filePath}`);
-
+
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`파일 다운로드 실패: ${response.status}`);
}
-
+
const arrayBuffer = await response.arrayBuffer();
console.log(`✅ HTTP 다운로드 완료: ${arrayBuffer.byteLength} bytes`);
-
+
return arrayBuffer;
} else {
// ✅ 로컬/NAS 파일 경로 처리 (환경별 분기)
const fs = await import('fs');
const path = await import('path');
const config = getFileReaderConfig();
-
+
let actualFilePath: string;
-
+
// 경로 형태별 처리
if (filePath.startsWith('/documents/')) {
// ✅ DB에 저장된 경로 형태: "/documents/[uuid].ext"
@@ -798,32 +812,48 @@ private transformToDoLCEDocument(
// 프로덕션: /evcp_nas/documents/[uuid].ext
actualFilePath = path.join(config.baseDir, 'public', filePath.substring(1)); // 앞의 '/' 제거
console.log(`📁 documents 경로 처리: ${filePath} → ${actualFilePath}`);
- }
-
+ }
+ else if (filePath.startsWith('/api/files')) {
+
+ actualFilePath = `${process.env.NEXT_PUBLIC_URL}${filePath}`
+
+
+ const response = await fetch(actualFilePath);
+ if (!response.ok) {
+ throw new Error(`파일 다운로드 실패: ${response.status}`);
+ }
+
+ const arrayBuffer = await response.arrayBuffer();
+ console.log(`✅ HTTP 다운로드 완료: ${arrayBuffer.byteLength} bytes`);
+
+ return arrayBuffer;
+
+ }
+
else {
// ✅ 상대 경로는 현재 디렉토리 기준
actualFilePath = filePath;
console.log(`📂 상대 경로 사용: ${actualFilePath}`);
}
-
+
console.log(`🔍 실제 파일 경로: ${actualFilePath}`);
console.log(`🏠 환경: ${config.isProduction ? 'PRODUCTION (NAS)' : 'DEVELOPMENT (public)'}`);
-
+
// 파일 존재 여부 확인
if (!fs.existsSync(actualFilePath)) {
console.error(`❌ 파일 없음: ${actualFilePath}`);
throw new Error(`파일을 찾을 수 없습니다: ${actualFilePath}`);
}
-
+
// 파일 읽기
const fileBuffer = fs.readFileSync(actualFilePath);
console.log(`✅ 파일 읽기 성공: ${actualFilePath} (${fileBuffer.length} bytes)`);
-
+
// ✅ Buffer를 ArrayBuffer로 정확히 변환
const arrayBuffer = new ArrayBuffer(fileBuffer.length);
const uint8Array = new Uint8Array(arrayBuffer);
uint8Array.set(fileBuffer);
-
+
return arrayBuffer;
}
} catch (error) {
@@ -881,19 +911,19 @@ private transformToDoLCEDocument(
try {
// DES 암호화 (C# DESCryptoServiceProvider 호환)
const DES_KEY = Buffer.from("4fkkdijg", "ascii")
-
+
// 암호화 문자열 생성: FileId↔UserId↔FileName
const encryptString = `${fileId}↔${userId}↔${fileName}`
-
+
// DES 암호화 (createCipheriv 사용)
const cipher = crypto.createCipheriv('des-ecb', DES_KEY, '')
cipher.setAutoPadding(true)
let encrypted = cipher.update(encryptString, 'utf8', 'base64')
encrypted += cipher.final('base64')
const encryptedKey = encrypted.replace(/\+/g, '|||')
-
+
const downloadUrl = `${process.env.DOLCE_DOWNLOAD_URL}?key=${encryptedKey}` || `http://60.100.99.217:1111/Download.aspx?key=${encryptedKey}`
-
+
console.log(`🧪 DOLCE 파일 다운로드 테스트:`)
console.log(` 파일명: ${fileName}`)
console.log(` FileId: ${fileId}`)
@@ -919,7 +949,7 @@ private transformToDoLCEDocument(
const buffer = Buffer.from(await response.arrayBuffer())
console.log(`✅ DOLCE 파일 다운로드 테스트 성공: ${fileName} (${buffer.length} bytes)`)
-
+
return {
success: true,
downloadUrl
diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts
index 05ace8d5..f2d9c26f 100644
--- a/lib/vendor-document-list/enhanced-document-service.ts
+++ b/lib/vendor-document-list/enhanced-document-service.ts
@@ -2,7 +2,7 @@
"use server"
import { revalidatePath, unstable_cache } from "next/cache"
-import { and, asc, desc, eq, ilike, or, count, avg, inArray, sql } from "drizzle-orm"
+import { and, asc, desc, eq, ilike, or, count, avg, inArray, sql, ne } from "drizzle-orm"
import db from "@/db/db"
import { StageDocumentsView, documentAttachments, documentStagesOnlyView, documents, enhancedDocumentsView, issueStages, revisions, simplifiedDocumentsView, type EnhancedDocumentsView } from "@/db/schema/vendorDocu"
import { filterColumns } from "@/lib/filter-columns"
@@ -1175,3 +1175,164 @@ export async function getDocumentDetails(documentId: number) {
+ export interface UpdateRevisionInput {
+ revisionId: number
+ revision: string // ✅ revision 필드 추가
+ comment?: string | null
+ usage: string
+ usageType?: string | null
+ }
+
+ export interface UpdateRevisionResult {
+ success: boolean
+ message?: string
+ error?: string
+ updatedRevision?: any
+ }
+
+ export async function updateRevisionAction(
+ input: UpdateRevisionInput
+ ): Promise<UpdateRevisionResult> {
+ try {
+ const { revisionId, revision, comment, usage, usageType } = input
+
+ // 1. 리비전 존재 여부 확인
+ const existingRevision = await db
+ .select()
+ .from(revisions)
+ .where(eq(revisions.id, revisionId))
+ .limit(1)
+
+ if (!existingRevision || existingRevision.length === 0) {
+ return {
+ success: false,
+ error: "Revision not found"
+ }
+ }
+
+ // 2. 동일한 revision 번호가 같은 문서에 이미 존재하는지 확인 (자기 자신 제외)
+ const duplicateRevision = await db
+ .select()
+ .from(revisions)
+ .innerJoin(issueStages, eq(revisions.issueStageId, issueStages.id))
+ .where(
+ and(
+ eq(revisions.revision, revision.trim()),
+ eq(issueStages.documentId, existingRevision[0].issueStageId), // 같은 문서 내에서
+ ne(revisions.id, revisionId) // 자기 자신 제외
+ )
+ )
+ .limit(1)
+
+ if (duplicateRevision && duplicateRevision.length > 0) {
+ return {
+ success: false,
+ error: `Revision "${revision.trim()}" already exists in this document`
+ }
+ }
+
+ // 3. 첨부파일이 처리된 상태인지 확인 (수정 가능 여부 체크)
+ const attachments = await db
+ .select()
+ .from(documentAttachments)
+ .where(eq(documentAttachments.revisionId, revisionId))
+
+ const hasProcessedFiles = attachments.some(att =>
+ att.dolceFilePath && att.dolceFilePath.trim() !== ''
+ )
+
+ if (hasProcessedFiles) {
+ return {
+ success: false,
+ error: "Cannot edit revision with processed files"
+ }
+ }
+
+ // 4. 리비전 업데이트
+ const [updatedRevision] = await db
+ .update(revisions)
+ .set({
+ revision: revision.trim(), // ✅ revision 필드 업데이트 추가
+ comment: comment?.trim() || null,
+ usage: usage.trim(),
+ usageType: usageType?.trim() || null,
+ updatedAt: new Date(),
+ })
+ .where(eq(revisions.id, revisionId))
+ .returning()
+
+ revalidatePath("/partners/document-list-ship") // ✅ 경로 오타 수정
+
+ return {
+ success: true,
+ message: `Revision ${revision.trim()} updated successfully`, // ✅ 새 revision 값 사용
+ updatedRevision
+ }
+
+ } catch (error) {
+ console.error("❌ Revision update server action error:", error)
+
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Failed to update revision"
+ }
+ }
+ }
+ // 삭제 서버 액션도 함께 만들어드릴게요
+ export interface DeleteRevisionInput {
+ revisionId: number
+ }
+
+ export interface DeleteRevisionResult {
+ success: boolean
+ message?: string
+ error?: string
+ deletedRevisionId?: number
+ deletedAttachmentsCount?: number
+ }
+
+ export async function deleteRevisionAction(
+ input: DeleteRevisionInput
+ ): Promise<DeleteRevisionResult> {
+ try {
+ const { revisionId } = input
+
+ // 1. 리비전과 첨부파일 정보 조회
+ const revision = await db
+ .select()
+ .from(revisions)
+ .where(eq(revisions.id, revisionId))
+ .limit(1)
+
+ if (!revision || revision.length === 0) {
+ return {
+ success: false,
+ error: "Revision not found"
+ }
+ }
+
+
+ // 5. 리비전 삭제
+ await db
+ .delete(revisions)
+ .where(eq(revisions.id, revisionId))
+
+ // 6. 캐시 재검증
+ revalidatePath("/parnters/document-list-ship")
+
+ return {
+ success: true,
+ message: `Revision ${revision[0].revision} deleted successfully`,
+ deletedRevisionId: revisionId,
+ deletedAttachmentsCount: 0 // revisionAttachments.length
+ }
+
+ } catch (error) {
+ console.error("❌ Revision delete server action error:", error)
+
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Failed to delete revision"
+ }
+ }
+ } \ No newline at end of file
diff --git a/lib/vendor-document-list/plant/document-stages-table.tsx b/lib/vendor-document-list/plant/document-stages-table.tsx
index f843862d..ccf35f4b 100644
--- a/lib/vendor-document-list/plant/document-stages-table.tsx
+++ b/lib/vendor-document-list/plant/document-stages-table.tsx
@@ -22,6 +22,8 @@ import {
Plus,
FileSpreadsheet
} from "lucide-react"
+import { useTranslation } from "@/i18n/client"
+import { useParams } from "next/navigation"
import { getDocumentStagesColumns } from "./document-stages-columns"
import { ExpandableDataTable } from "@/components/data-table/expandable-data-table"
import { toast } from "sonner"
@@ -45,6 +47,11 @@ export function DocumentStagesTable({
projectType,
}: DocumentStagesTableProps) {
const [{ data, pageCount, total }] = React.use(promises)
+
+ // URL에서 언어 파라미터 가져오기
+ const params = useParams()
+ const lng = (params?.lng as string) || 'ko'
+ const { t } = useTranslation(lng, 'document')
// 상태 관리
@@ -160,13 +167,13 @@ export function DocumentStagesTable({
.filter(Boolean)
if (stageIds.length > 0) {
- toast.success(`${stageIds.length}개 스테이지가 완료 처리되었습니다.`)
+ toast.success(t('documentList.messages.stageCompletionSuccess', { count: stageIds.length }))
}
} else if (action === 'bulk_assign') {
- toast.info("일괄 담당자 지정 기능은 준비 중입니다.")
+ toast.info(t('documentList.messages.bulkAssignPending'))
}
} catch (error) {
- toast.error("일괄 작업 중 오류가 발생했습니다.")
+ toast.error(t('documentList.messages.bulkActionError'))
}
}
@@ -260,47 +267,47 @@ export function DocumentStagesTable({
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('all')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">전체 문서</CardTitle>
+ <CardTitle className="text-sm font-medium">{t('documentList.dashboard.totalDocuments')}</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground">
- 총 {total}개 문서
+ {t('documentList.dashboard.totalDocumentCount', { total })}
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('overdue')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">지연 문서</CardTitle>
+ <CardTitle className="text-sm font-medium">{t('documentList.dashboard.overdueDocuments')}</CardTitle>
<AlertTriangle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{stats.overdue}</div>
- <p className="text-xs text-muted-foreground">즉시 확인 필요</p>
+ <p className="text-xs text-muted-foreground">{t('documentList.dashboard.checkImmediately')}</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('due_soon')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">마감 임박</CardTitle>
+ <CardTitle className="text-sm font-medium">{t('documentList.dashboard.dueSoonDocuments')}</CardTitle>
<Clock className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{stats.dueSoon}</div>
- <p className="text-xs text-muted-foreground">3일 이내 마감</p>
+ <p className="text-xs text-muted-foreground">{t('documentList.dashboard.dueInDays')}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">평균 진행률</CardTitle>
+ <CardTitle className="text-sm font-medium">{t('documentList.dashboard.averageProgress')}</CardTitle>
<Target className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.avgProgress}%</div>
- <p className="text-xs text-muted-foreground">전체 프로젝트 진행도</p>
+ <p className="text-xs text-muted-foreground">{t('documentList.dashboard.overallProgress')}</p>
</CardContent>
</Card>
</div>
@@ -312,7 +319,7 @@ export function DocumentStagesTable({
className="cursor-pointer hover:bg-primary hover:text-primary-foreground whitespace-nowrap"
onClick={() => setQuickFilter('all')}
>
- 전체 ({stats.total})
+ {t('documentList.quickFilters.all')} ({stats.total})
</Badge>
<Badge
variant={quickFilter === 'overdue' ? 'destructive' : 'outline'}
@@ -320,7 +327,7 @@ export function DocumentStagesTable({
onClick={() => setQuickFilter('overdue')}
>
<AlertTriangle className="w-3 h-3 mr-1" />
- 지연 ({stats.overdue})
+ {t('documentList.quickFilters.overdue')} ({stats.overdue})
</Badge>
<Badge
variant={quickFilter === 'due_soon' ? 'default' : 'outline'}
@@ -328,7 +335,7 @@ export function DocumentStagesTable({
onClick={() => setQuickFilter('due_soon')}
>
<Clock className="w-3 h-3 mr-1" />
- 마감임박 ({stats.dueSoon})
+ {t('documentList.quickFilters.dueSoon')} ({stats.dueSoon})
</Badge>
<Badge
variant={quickFilter === 'in_progress' ? 'default' : 'outline'}
@@ -336,7 +343,7 @@ export function DocumentStagesTable({
onClick={() => setQuickFilter('in_progress')}
>
<Users className="w-3 h-3 mr-1" />
- 진행중 ({stats.inProgress})
+ {t('documentList.quickFilters.inProgress')} ({stats.inProgress})
</Badge>
<Badge
variant={quickFilter === 'high_priority' ? 'destructive' : 'outline'}
@@ -344,7 +351,7 @@ export function DocumentStagesTable({
onClick={() => setQuickFilter('high_priority')}
>
<Target className="w-3 h-3 mr-1" />
- 높은우선순위 ({stats.highPriority})
+ {t('documentList.quickFilters.highPriority')} ({stats.highPriority})
</Badge>
</div>
diff --git a/lib/vendor-document-list/ship/send-to-shi-button.tsx b/lib/vendor-document-list/ship/send-to-shi-button.tsx
index 447b461b..87cc6ff5 100644
--- a/lib/vendor-document-list/ship/send-to-shi-button.tsx
+++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx
@@ -28,6 +28,7 @@ import { useClientSyncStatus, useTriggerSync, syncUtils } from "@/hooks/use-sync
import type { EnhancedDocument } from "@/types/enhanced-documents"
import { useParams } from "next/navigation"
import { useTranslation } from "@/i18n/client"
+import { useSession } from "next-auth/react"
interface SendToSHIButtonProps {
documents?: EnhancedDocument[]
@@ -43,6 +44,7 @@ export function SendToSHIButton({
const [isDialogOpen, setIsDialogOpen] = React.useState(false)
const [syncProgress, setSyncProgress] = React.useState(0)
const [currentSyncingContract, setCurrentSyncingContract] = React.useState<number | null>(null)
+ const { data: session } = useSession();
const params = useParams()
const lng = (params?.lng as string) || "ko"
@@ -60,6 +62,8 @@ export function SendToSHIButton({
return uniqueIds.sort()
}, [documents])
+ const vendorId = session?.user.companyId
+
// ✅ 클라이언트 전용 Hook 사용 (서버 사이드 렌더링 호환)
const { contractStatuses, totalStats, refetchAll } = useClientSyncStatus(
documentsContractIds,
@@ -68,20 +72,6 @@ export function SendToSHIButton({
const { triggerSync, isLoading: isSyncing, error: syncError } = useTriggerSync()
- // 개발 환경에서 디버깅 정보
- React.useEffect(() => {
- if (process.env.NODE_ENV === 'development') {
- console.log('SendToSHIButton Debug Info:', {
- documentsContractIds,
- totalStats,
- contractStatuses: contractStatuses.map(({ projectId, syncStatus, error }) => ({
- projectId,
- pendingChanges: syncStatus?.pendingChanges,
- hasError: !!error
- }))
- })
- }
- }, [documentsContractIds, totalStats, contractStatuses])
// 동기화 실행 함수
const handleSync = async () => {
diff --git a/lib/vendor-document-list/sync-service.ts b/lib/vendor-document-list/sync-service.ts
index 0544ce06..cdc22e11 100644
--- a/lib/vendor-document-list/sync-service.ts
+++ b/lib/vendor-document-list/sync-service.ts
@@ -8,6 +8,8 @@ import {
} from "@/db/schema/vendorDocu"
import { documents, revisions, documentAttachments } from "@/db/schema/vendorDocu"
import { eq, and, lt, desc, sql, inArray } from "drizzle-orm"
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
export interface SyncableEntity {
entityType: 'document' | 'revision' | 'attachment'
@@ -42,7 +44,7 @@ class SyncService {
* 변경사항을 change_logs에 기록
*/
async logChange(
- projectId: number,
+ vendorId: number,
entityType: 'document' | 'revision' | 'attachment',
entityId: number,
action: 'CREATE' | 'UPDATE' | 'DELETE',
@@ -56,7 +58,7 @@ class SyncService {
const changedFields = this.detectChangedFields(oldValues, newValues)
await db.insert(changeLogs).values({
- projectId,
+ vendorId,
entityType,
entityId,
action,
@@ -99,7 +101,7 @@ class SyncService {
* 동기화할 변경사항 조회 (증분)
*/
async getPendingChanges(
- projectId: number,
+ vendorId: number,
targetSystem: string = 'DOLCE',
limit?: number
): Promise<ChangeLog[]> {
@@ -107,7 +109,7 @@ class SyncService {
.select()
.from(changeLogs)
.where(and(
- eq(changeLogs.projectId, projectId),
+ eq(changeLogs.vendorId, vendorId),
eq(changeLogs.isSynced, false),
lt(changeLogs.syncAttempts, 3),
sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})`
@@ -136,14 +138,14 @@ class SyncService {
* 동기화 배치 생성
*/
async createSyncBatch(
- projectId: number,
+ vendorId: number,
targetSystem: string,
changeLogIds: number[]
): Promise<number> {
const [batch] = await db
.insert(syncBatches)
.values({
- projectId,
+ vendorId,
targetSystem,
batchSize: changeLogIds.length,
changeLogIds,
@@ -168,8 +170,16 @@ class SyncService {
throw new Error(`Sync not enabled for ${targetSystem}`)
}
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.companyId) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+ const vendorId = Number(session.user.companyId)
+
+
// 2. 대기 중인 변경사항 조회 (전체)
- const pendingChanges = await this.getPendingChanges(projectId, targetSystem)
+ const pendingChanges = await this.getPendingChanges(vendorId, targetSystem)
if (pendingChanges.length === 0) {
return {
@@ -182,7 +192,7 @@ class SyncService {
// 3. 배치 생성
const batchId = await this.createSyncBatch(
- projectId,
+ vendorId,
targetSystem,
pendingChanges.map(c => c.id)
)
@@ -446,11 +456,20 @@ class SyncService {
*/
async getSyncStatus(projectId: number, targetSystem: string = 'DOLCE') {
try {
+
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.companyId) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+ const vendorId = Number(session.user.companyId)
+
+
// 대기 중인 변경사항 수 조회
const pendingCount = await db.$count(
changeLogs,
and(
- eq(changeLogs.projectId, projectId),
+ eq(changeLogs.vendorId, vendorId),
eq(changeLogs.isSynced, false),
lt(changeLogs.syncAttempts, 3),
sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})`
@@ -461,7 +480,7 @@ class SyncService {
const syncedCount = await db.$count(
changeLogs,
and(
- eq(changeLogs.projectId, projectId),
+ eq(changeLogs.vendorId, vendorId),
eq(changeLogs.isSynced, true),
sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})`
)
@@ -471,7 +490,7 @@ class SyncService {
const failedCount = await db.$count(
changeLogs,
and(
- eq(changeLogs.projectId, projectId),
+ eq(changeLogs.vendorId, vendorId),
eq(changeLogs.isSynced, false),
sql`${changeLogs.syncAttempts} >= 3`,
sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})`
@@ -483,7 +502,7 @@ class SyncService {
.select()
.from(syncBatches)
.where(and(
- eq(syncBatches.projectId, projectId),
+ eq(syncBatches.vendorId, vendorId),
eq(syncBatches.targetSystem, targetSystem),
eq(syncBatches.status, 'SUCCESS')
))
@@ -491,7 +510,7 @@ class SyncService {
.limit(1)
return {
- projectId,
+ vendorId,
targetSystem,
totalChanges: pendingCount + syncedCount + failedCount,
pendingChanges: pendingCount,
@@ -511,11 +530,19 @@ class SyncService {
*/
async getRecentSyncBatches(projectId: number, targetSystem: string = 'DOLCE', limit: number = 10) {
try {
+
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.companyId) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+ const vendorId = Number(session.user.companyId)
+
const batches = await db
.select()
.from(syncBatches)
.where(and(
- eq(syncBatches.projectId, projectId),
+ eq(syncBatches.vendorId, vendorId),
eq(syncBatches.targetSystem, targetSystem)
))
.orderBy(desc(syncBatches.createdAt))
@@ -524,7 +551,7 @@ class SyncService {
// Date 객체를 문자열로 변환
return batches.map(batch => ({
id: Number(batch.id),
- projectId: batch.projectId,
+ vendorId: batch.vendorId,
targetSystem: batch.targetSystem,
batchSize: batch.batchSize,
status: batch.status,
@@ -561,7 +588,7 @@ export async function logDocumentChange(
}
export async function logRevisionChange(
- projectId: number,
+ vendorId: number,
revisionId: number,
action: 'CREATE' | 'UPDATE' | 'DELETE',
newValues?: any,
@@ -570,11 +597,11 @@ export async function logRevisionChange(
userName?: string,
targetSystems: string[] = ["DOLCE", "SWP"]
) {
- return syncService.logChange(projectId, 'revision', revisionId, action, newValues, oldValues, userId, userName, targetSystems)
+ return syncService.logChange(vendorId, 'revision', revisionId, action, newValues, oldValues, userId, userName, targetSystems)
}
export async function logAttachmentChange(
- projectId: number,
+ vendorId: number,
attachmentId: number,
action: 'CREATE' | 'UPDATE' | 'DELETE',
newValues?: any,
@@ -583,5 +610,5 @@ export async function logAttachmentChange(
userName?: string,
targetSystems: string[] = ["DOLCE", "SWP"]
) {
- return syncService.logChange(projectId, 'attachment', attachmentId, action, newValues, oldValues, userId, userName, targetSystems)
+ return syncService.logChange(vendorId, 'attachment', attachmentId, action, newValues, oldValues, userId, userName, targetSystems)
} \ No newline at end of file
diff --git a/lib/vendor-document-list/table/enhanced-documents-table.tsx b/lib/vendor-document-list/table/enhanced-documents-table.tsx
index cb49f796..7e20892e 100644
--- a/lib/vendor-document-list/table/enhanced-documents-table.tsx
+++ b/lib/vendor-document-list/table/enhanced-documents-table.tsx
@@ -26,6 +26,8 @@ import {
Settings,
Filter
} from "lucide-react"
+import { useTranslation } from "@/i18n/client"
+import { useParams } from "next/navigation"
import { getUpdatedEnhancedColumns } from "./enhanced-doc-table-columns"
import { ExpandableDataTable } from "@/components/data-table/expandable-data-table"
import { toast } from "sonner"
@@ -48,7 +50,7 @@ interface FinalIntegratedDocumentsTableProps {
}
// ✅ Drawing Kind 옵션 정의
-const DRAWING_KIND_OPTIONS = [
+const DRAWING_KIND_KEYS = [
{ value: "all", label: "전체 문서" },
{ value: "B3", label: "B3: Vendor" },
{ value: "B4", label: "B4: GTT" },
@@ -63,6 +65,20 @@ export function EnhancedDocumentsTable({
initialDrawingKind = "all"
}: FinalIntegratedDocumentsTableProps) {
const [{ data, pageCount, total }] = React.use(promises)
+
+ // URL에서 언어 파라미터 가져오기
+ const params = useParams()
+ const lng = (params?.lng as string) || 'ko'
+ const { t } = useTranslation(lng, 'engineering')
+ console.log(t, 't')
+
+ // ✅ Drawing Kind 옵션 동적 생성
+ const DRAWING_KIND_OPTIONS = React.useMemo(() =>
+ DRAWING_KIND_KEYS.map(item => ({
+ value: item.value,
+ label: t(`documentList.drawingKindOptions.${item.value}`)
+ }))
+ , [t])
// ✅ Drawing Kind 필터 상태 추가
const [drawingKindFilter, setDrawingKindFilter] = React.useState<string>(initialDrawingKind)
@@ -267,13 +283,13 @@ export function EnhancedDocumentsTable({
.filter(Boolean)
if (stageIds.length > 0) {
- toast.success(`${stageIds.length}개 항목이 승인되었습니다.`)
+ toast.success(t('documentList.messages.approvalSuccess', { count: stageIds.length }))
}
} else if (action === 'bulk_upload') {
- toast.info("일괄 업로드 기능은 준비 중입니다.")
+ toast.info(t('documentList.messages.bulkUploadPending'))
}
} catch (error) {
- toast.error("일괄 작업 중 오류가 발생했습니다.")
+ toast.error(t('documentList.messages.bulkActionError'))
}
}
@@ -318,17 +334,17 @@ export function EnhancedDocumentsTable({
const advancedFilterFields: DataTableAdvancedFilterField<EnhancedDocument>[] = [
{
id: "docNumber",
- label: "문서번호",
+ label: t('documentList.filters.documentNumber'),
type: "text",
},
{
id: "title",
- label: "문서제목",
+ label: t('documentList.filters.documentTitle'),
type: "text",
},
{
id: "drawingKind",
- label: "문서종류",
+ label: t('documentList.filters.documentType'),
type: "select",
options: [
{ label: "B3", value: "B3" },
@@ -338,43 +354,43 @@ export function EnhancedDocumentsTable({
},
{
id: "currentStageStatus",
- label: "스테이지 상태",
+ label: t('documentList.filters.stageStatus'),
type: "select",
options: [
- { label: "계획됨", value: "PLANNED" },
- { label: "진행중", value: "IN_PROGRESS" },
- { label: "제출됨", value: "SUBMITTED" },
- { label: "승인됨", value: "APPROVED" },
- { label: "완료됨", value: "COMPLETED" },
+ { label: t('documentList.statusOptions.planned'), value: "PLANNED" },
+ { label: t('documentList.statusOptions.inProgress'), value: "IN_PROGRESS" },
+ { label: t('documentList.statusOptions.submitted'), value: "SUBMITTED" },
+ { label: t('documentList.statusOptions.approved'), value: "APPROVED" },
+ { label: t('documentList.statusOptions.completed'), value: "COMPLETED" },
],
},
{
id: "currentStagePriority",
- label: "우선순위",
+ label: t('documentList.filters.priority'),
type: "select",
options: [
- { label: "높음", value: "HIGH" },
- { label: "보통", value: "MEDIUM" },
- { label: "낮음", value: "LOW" },
+ { label: t('documentList.priorityOptions.high'), value: "HIGH" },
+ { label: t('documentList.priorityOptions.medium'), value: "MEDIUM" },
+ { label: t('documentList.priorityOptions.low'), value: "LOW" },
],
},
{
id: "isOverdue",
- label: "지연 여부",
+ label: t('documentList.filters.overdueStatus'),
type: "select",
options: [
- { label: "지연됨", value: "true" },
- { label: "정상", value: "false" },
+ { label: t('documentList.overdueOptions.overdue'), value: "true" },
+ { label: t('documentList.overdueOptions.normal'), value: "false" },
],
},
{
id: "currentStageAssigneeName",
- label: "담당자",
+ label: t('documentList.filters.assignee'),
type: "text",
},
{
id: "createdAt",
- label: "생성일",
+ label: t('documentList.filters.createdDate'),
type: "date",
},
]
@@ -405,48 +421,48 @@ export function EnhancedDocumentsTable({
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('all')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
- {drawingKindFilter === "all" ? "전체 문서" : `${DRAWING_KIND_OPTIONS.find(o => o.value === drawingKindFilter)?.label} 문서`}
+ {drawingKindFilter === "all" ? t('documentList.dashboard.totalDocuments') : `${DRAWING_KIND_OPTIONS.find(o => o.value === drawingKindFilter)?.label}`}
</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground">
- 총 {total}개 중 {stats.total}개 표시
+ {t('documentList.dashboard.totalCount', { total, shown: stats.total })}
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('overdue')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">지연 문서</CardTitle>
+ <CardTitle className="text-sm font-medium">{t('documentList.dashboard.overdueDocuments')}</CardTitle>
<AlertTriangle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{stats.overdue}</div>
- <p className="text-xs text-muted-foreground">즉시 확인 필요</p>
+ <p className="text-xs text-muted-foreground">{t('documentList.dashboard.checkImmediately')}</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('due_soon')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">마감 임박</CardTitle>
+ <CardTitle className="text-sm font-medium">{t('documentList.dashboard.dueSoonDocuments')}</CardTitle>
<Clock className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{stats.dueSoon}</div>
- <p className="text-xs text-muted-foreground">3일 이내 마감</p>
+ <p className="text-xs text-muted-foreground">{t('documentList.dashboard.dueInDays')}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">평균 진행률</CardTitle>
+ <CardTitle className="text-sm font-medium">{t('documentList.dashboard.averageProgress')}</CardTitle>
<Target className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.avgProgress}%</div>
- <p className="text-xs text-muted-foreground">전체 프로젝트 진행도</p>
+ <p className="text-xs text-muted-foreground">{t('documentList.dashboard.overallProgress')}</p>
</CardContent>
</Card>
</div>
@@ -460,7 +476,7 @@ export function EnhancedDocumentsTable({
className="cursor-pointer hover:bg-primary hover:text-primary-foreground whitespace-nowrap"
onClick={() => setQuickFilter('all')}
>
- 전체 ({stats.total})
+ {t('documentList.quickFilters.all')} ({stats.total})
</Badge>
<Badge
variant={quickFilter === 'overdue' ? 'destructive' : 'outline'}
@@ -468,7 +484,7 @@ export function EnhancedDocumentsTable({
onClick={() => setQuickFilter('overdue')}
>
<AlertTriangle className="w-3 h-3 mr-1" />
- 지연 ({stats.overdue})
+ {t('documentList.quickFilters.overdue')} ({stats.overdue})
</Badge>
<Badge
variant={quickFilter === 'due_soon' ? 'default' : 'outline'}
@@ -476,7 +492,7 @@ export function EnhancedDocumentsTable({
onClick={() => setQuickFilter('due_soon')}
>
<Clock className="w-3 h-3 mr-1" />
- 마감임박 ({stats.dueSoon})
+ {t('documentList.quickFilters.dueSoon')} ({stats.dueSoon})
</Badge>
<Badge
variant={quickFilter === 'in_progress' ? 'default' : 'outline'}
@@ -484,7 +500,7 @@ export function EnhancedDocumentsTable({
onClick={() => setQuickFilter('in_progress')}
>
<Users className="w-3 h-3 mr-1" />
- 진행중 ({stats.inProgress})
+ {t('documentList.quickFilters.inProgress')} ({stats.inProgress})
</Badge>
<Badge
variant={quickFilter === 'high_priority' ? 'destructive' : 'outline'}
@@ -492,7 +508,7 @@ export function EnhancedDocumentsTable({
onClick={() => setQuickFilter('high_priority')}
>
<Target className="w-3 h-3 mr-1" />
- 높은우선순위 ({stats.highPriority})
+ {t('documentList.quickFilters.highPriority')} ({stats.highPriority})
</Badge>
</div>
@@ -500,7 +516,7 @@ export function EnhancedDocumentsTable({
<div className="flex items-center gap-2 flex-shrink-0">
<Select value={drawingKindFilter} onValueChange={setDrawingKindFilter}>
<SelectTrigger className="w-[140px]">
- <SelectValue placeholder="문서 종류" />
+ <SelectValue placeholder={t('documentList.filters.selectDocumentType')} />
</SelectTrigger>
<SelectContent>
{DRAWING_KIND_OPTIONS.map(option => (
@@ -520,7 +536,7 @@ export function EnhancedDocumentsTable({
{drawingKindFilter === "B4" && (
<div className="flex items-center gap-1 text-blue-600 bg-blue-50 px-2 py-1 rounded text-xs">
<Settings className="h-3 w-3" />
- <span className="hidden sm:inline">상세정보 확장가능</span>
+ <span className="hidden sm:inline">{t('documentList.ui.expandedInfoAvailable')}</span>
</div>
)}
</div>