summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/plant/excel-import-export.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/plant/excel-import-export.ts')
-rw-r--r--lib/vendor-document-list/plant/excel-import-export.ts788
1 files changed, 0 insertions, 788 deletions
diff --git a/lib/vendor-document-list/plant/excel-import-export.ts b/lib/vendor-document-list/plant/excel-import-export.ts
deleted file mode 100644
index c1409205..00000000
--- a/lib/vendor-document-list/plant/excel-import-export.ts
+++ /dev/null
@@ -1,788 +0,0 @@
-// excel-import-export.ts
-"use client"
-
-import ExcelJS from 'exceljs'
-import {
- excelDocumentRowSchema,
- excelStageRowSchema,
- type ExcelDocumentRow,
- type ExcelStageRow,
- type ExcelImportResult,
- type CreateDocumentInput
-} from './document-stage-validations'
-import { StageDocumentsView } from '@/db/schema'
-
-// =============================================================================
-// 1. 엑셀 템플릿 생성 및 다운로드
-// =============================================================================
-
-// 문서 템플릿 생성
-export async function createDocumentTemplate(projectType: "ship" | "plant") {
- const workbook = new ExcelJS.Workbook()
- const worksheet = workbook.addWorksheet("문서목록", {
- properties: { defaultColWidth: 15 }
- })
-
- const baseHeaders = [
- "문서번호*",
- "문서명*",
- "문서종류*",
- "PIC",
- "발행일",
- "설명"
- ]
-
- const plantHeaders = [
- "벤더문서번호",
- "벤더명",
- "벤더코드"
- ]
-
- const b4Headers = [
- "C구분",
- "D구분",
- "Degree구분",
- "부서구분",
- "S구분",
- "J구분"
- ]
-
- const headers = [
- ...baseHeaders,
- ...(projectType === "plant" ? plantHeaders : []),
- ...b4Headers
- ]
-
- // 헤더 행 추가 및 스타일링
- const headerRow = worksheet.addRow(headers)
- headerRow.eachCell((cell, colNumber) => {
- cell.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FF4472C4' }
- }
- cell.font = {
- color: { argb: 'FFFFFFFF' },
- bold: true,
- size: 11
- }
- cell.alignment = {
- horizontal: 'center',
- vertical: 'middle'
- }
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- }
-
- // 필수 필드 표시
- if (cell.value && String(cell.value).includes('*')) {
- cell.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFE74C3C' }
- }
- }
- })
-
- // 샘플 데이터 추가
- const sampleData = projectType === "ship" ? [
- "SH-2024-001",
- "기본 설계 도면",
- "B3",
- "김철수",
- new Date("2024-01-15"),
- "선박 기본 설계 관련 문서",
- "", "", "", "", "", "" // B4 필드들
- ] : [
- "PL-2024-001",
- "공정 설계 도면",
- "B4",
- "이영희",
- new Date("2024-01-15"),
- "플랜트 공정 설계 관련 문서",
- "V-001", // 벤더문서번호
- "삼성엔지니어링", // 벤더명
- "SENG", // 벤더코드
- "C1", "D1", "DEG1", "DEPT1", "S1", "J1" // B4 필드들
- ]
-
- const sampleRow = worksheet.addRow(sampleData)
- sampleRow.eachCell((cell, colNumber) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- }
-
- // 날짜 형식 설정
- if (cell.value instanceof Date) {
- cell.numFmt = 'yyyy-mm-dd'
- }
- })
-
- // 컬럼 너비 자동 조정
- worksheet.columns.forEach((column, index) => {
- if (index < 6) {
- column.width = headers[index].length + 5
- } else {
- column.width = 12
- }
- })
-
- // 문서종류 드롭다운 설정
- const docTypeCol = headers.indexOf("문서종류*") + 1
- worksheet.dataValidations.add(`${String.fromCharCode(64 + docTypeCol)}2:${String.fromCharCode(64 + docTypeCol)}1000`, {
- type: 'list',
- allowBlank: false,
- formulae: ['"B3,B4,B5"']
- })
-
- // Plant 프로젝트의 경우 우선순위 드롭다운 추가
- if (projectType === "plant") {
- // 여기에 추가적인 드롭다운들을 설정할 수 있습니다
- }
-
- return workbook
-}
-
-// 스테이지 템플릿 생성
-export async function createStageTemplate() {
- const workbook = new ExcelJS.Workbook()
- const worksheet = workbook.addWorksheet("스테이지목록", {
- properties: { defaultColWidth: 15 }
- })
-
- const headers = [
- "문서번호*",
- "스테이지명*",
- "계획일",
- "우선순위",
- "담당자",
- "설명",
- "스테이지순서"
- ]
-
- // 헤더 행 추가 및 스타일링
- const headerRow = worksheet.addRow(headers)
- headerRow.eachCell((cell, colNumber) => {
- cell.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FF27AE60' }
- }
- cell.font = {
- color: { argb: 'FFFFFFFF' },
- bold: true,
- size: 11
- }
- cell.alignment = {
- horizontal: 'center',
- vertical: 'middle'
- }
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- }
-
- // 필수 필드 표시
- if (cell.value && String(cell.value).includes('*')) {
- cell.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFE74C3C' }
- }
- }
- })
-
- // 샘플 데이터 추가
- const sampleData = [
- [
- "SH-2024-001",
- "초기 설계 검토",
- new Date("2024-02-15"),
- "HIGH",
- "김철수",
- "초기 설계안 검토 및 승인",
- 0
- ],
- [
- "SH-2024-001",
- "상세 설계",
- new Date("2024-03-15"),
- "MEDIUM",
- "이영희",
- "상세 설계 작업 수행",
- 1
- ]
- ]
-
- sampleData.forEach(rowData => {
- const row = worksheet.addRow(rowData)
- row.eachCell((cell, colNumber) => {
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- }
-
- // 날짜 형식 설정
- if (cell.value instanceof Date) {
- cell.numFmt = 'yyyy-mm-dd'
- }
- })
- })
-
- // 컬럼 너비 설정
- worksheet.columns = [
- { width: 15 }, // 문서번호
- { width: 20 }, // 스테이지명
- { width: 12 }, // 계획일
- { width: 10 }, // 우선순위
- { width: 15 }, // 담당자
- { width: 30 }, // 설명
- { width: 12 }, // 스테이지순서
- ]
-
- // 우선순위 드롭다운 설정
- worksheet.dataValidations.add('D2:D1000', {
- type: 'list',
- allowBlank: true,
- formulae: ['"HIGH,MEDIUM,LOW"']
- })
-
- return workbook
-}
-
-// 템플릿 다운로드 함수
-export async function downloadTemplate(type: "documents" | "stages", projectType: "ship" | "plant") {
- const workbook = await (type === "documents"
- ? createDocumentTemplate(projectType)
- : createStageTemplate())
-
- const filename = type === "documents"
- ? `문서_임포트_템플릿_${projectType}.xlsx`
- : `스테이지_임포트_템플릿.xlsx`
-
- // 브라우저에서 다운로드
- const buffer = await workbook.xlsx.writeBuffer()
- const blob = new Blob([buffer], {
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
- })
-
- const url = window.URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.href = url
- link.download = filename
- link.click()
-
- // 메모리 정리
- window.URL.revokeObjectURL(url)
-}
-
-// =============================================================================
-// 2. 엑셀 파일 읽기 및 파싱
-// =============================================================================
-
-// 엑셀 파일을 읽어서 JSON으로 변환
-export async function readExcelFile(file: File): Promise<any[]> {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
-
- reader.onload = async (e) => {
- try {
- const buffer = e.target?.result as ArrayBuffer
- const workbook = new ExcelJS.Workbook()
- await workbook.xlsx.load(buffer)
-
- const worksheet = workbook.getWorksheet(1) // 첫 번째 워크시트
- if (!worksheet) {
- throw new Error('워크시트를 찾을 수 없습니다')
- }
-
- const jsonData: any[] = []
-
- worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
- const rowData: any[] = []
- row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
- let value = cell.value
-
- // 날짜 처리
- if (cell.type === ExcelJS.ValueType.Date) {
- value = cell.value as Date
- }
- // 수식 결과값 처리
- else if (cell.type === ExcelJS.ValueType.Formula && cell.result) {
- value = cell.result
- }
- // 하이퍼링크 처리
- else if (cell.type === ExcelJS.ValueType.Hyperlink) {
- value = cell.value?.text || cell.value
- }
-
- rowData[colNumber - 1] = value || ""
- })
-
- jsonData.push(rowData)
- })
-
- resolve(jsonData)
- } catch (error) {
- reject(new Error('엑셀 파일을 읽는 중 오류가 발생했습니다: ' + error))
- }
- }
-
- reader.onerror = () => {
- reject(new Error('파일을 읽을 수 없습니다'))
- }
-
- reader.readAsArrayBuffer(file)
- })
-}
-
-// 문서 데이터 유효성 검사 및 변환
-export function validateDocumentRows(
- rawData: any[],
- contractId: number,
- projectType: "ship" | "plant"
-): { validData: CreateDocumentInput[], errors: any[] } {
- if (rawData.length < 2) {
- throw new Error('데이터가 없습니다. 최소 헤더와 1개 행이 필요합니다.')
- }
-
- const headers = rawData[0] as string[]
- const rows = rawData.slice(1)
-
- const validData: CreateDocumentInput[] = []
- const errors: any[] = []
-
- // 필수 헤더 검사
- const requiredHeaders = ["문서번호", "문서명", "문서종류"]
- const missingHeaders = requiredHeaders.filter(h =>
- !headers.some(header => header.includes(h.replace("*", "")))
- )
-
- if (missingHeaders.length > 0) {
- throw new Error(`필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`)
- }
-
- // 헤더 인덱스 매핑
- const headerMap: Record<string, number> = {}
- headers.forEach((header, index) => {
- const cleanHeader = header.replace("*", "").trim()
- headerMap[cleanHeader] = index
- })
-
- // 각 행 처리
- rows.forEach((row: any[], rowIndex) => {
- try {
- // 빈 행 스킵
- if (row.every(cell => !cell || String(cell).trim() === "")) {
- return
- }
-
- const rowData: any = {
- contractId,
- docNumber: String(row[headerMap["문서번호"]] || "").trim(),
- title: String(row[headerMap["문서명"]] || "").trim(),
- drawingKind: String(row[headerMap["문서종류"]] || "").trim(),
- pic: String(row[headerMap["PIC"]] || "").trim() || undefined,
- issuedDate: row[headerMap["발행일"]] ?
- formatExcelDate(row[headerMap["발행일"]]) : undefined,
- }
-
- // Plant 프로젝트 전용 필드
- if (projectType === "plant") {
- rowData.vendorDocNumber = String(row[headerMap["벤더문서번호"]] || "").trim() || undefined
- }
-
- // B4 전용 필드들
- const b4Fields = ["C구분", "D구분", "Degree구분", "부서구분", "S구분", "J구분"]
- const b4FieldMap = {
- "C구분": "cGbn",
- "D구분": "dGbn",
- "Degree구분": "degreeGbn",
- "부서구분": "deptGbn",
- "S구분": "sGbn",
- "J구분": "jGbn"
- }
-
- b4Fields.forEach(field => {
- if (headerMap[field] !== undefined) {
- const value = String(row[headerMap[field]] || "").trim()
- if (value) {
- rowData[b4FieldMap[field as keyof typeof b4FieldMap]] = value
- }
- }
- })
-
- // 유효성 검사
- const validatedData = excelDocumentRowSchema.parse({
- "문서번호": rowData.docNumber,
- "문서명": rowData.title,
- "문서종류": rowData.drawingKind,
- "벤더문서번호": rowData.vendorDocNumber,
- "PIC": rowData.pic,
- "발행일": rowData.issuedDate,
- "C구분": rowData.cGbn,
- "D구분": rowData.dGbn,
- "Degree구분": rowData.degreeGbn,
- "부서구분": rowData.deptGbn,
- "S구분": rowData.sGbn,
- "J구분": rowData.jGbn,
- })
-
- // CreateDocumentInput 형태로 변환
- const documentInput: CreateDocumentInput = {
- contractId,
- docNumber: validatedData["문서번호"],
- title: validatedData["문서명"],
- drawingKind: validatedData["문서종류"],
- vendorDocNumber: validatedData["벤더문서번호"],
- pic: validatedData["PIC"],
- issuedDate: validatedData["발행일"],
- cGbn: validatedData["C구분"],
- dGbn: validatedData["D구분"],
- degreeGbn: validatedData["Degree구분"],
- deptGbn: validatedData["부서구분"],
- sGbn: validatedData["S구분"],
- jGbn: validatedData["J구분"],
- }
-
- validData.push(documentInput)
-
- } catch (error) {
- errors.push({
- row: rowIndex + 2, // 엑셀 행 번호 (헤더 포함)
- message: error instanceof Error ? error.message : "알 수 없는 오류",
- data: row
- })
- }
- })
-
- return { validData, errors }
-}
-
-// 엑셀 날짜 형식 변환
-function formatExcelDate(value: any): string | undefined {
- if (!value) return undefined
-
- // ExcelJS에서 Date 객체로 처리된 경우
- if (value instanceof Date) {
- return value.toISOString().split('T')[0]
- }
-
- // 이미 문자열 날짜 형식인 경우
- if (typeof value === 'string') {
- const dateMatch = value.match(/^\d{4}-\d{2}-\d{2}$/)
- if (dateMatch) return value
-
- // 다른 형식 시도
- const date = new Date(value)
- if (!isNaN(date.getTime())) {
- return date.toISOString().split('T')[0]
- }
- }
-
- // 엑셀 시리얼 날짜인 경우
- if (typeof value === 'number') {
- // ExcelJS는 이미 Date 객체로 변환해주므로 이 경우는 드물지만
- // 1900년 1월 1일부터의 일수로 계산
- const excelEpoch = new Date(1900, 0, 1)
- const date = new Date(excelEpoch.getTime() + (value - 2) * 24 * 60 * 60 * 1000)
- if (!isNaN(date.getTime())) {
- return date.toISOString().split('T')[0]
- }
- }
-
- return undefined
-}
-
-// =============================================================================
-// 3. 데이터 익스포트
-// =============================================================================
-
-// 문서 데이터를 엑셀로 익스포트
-export function exportDocumentsToExcel(
- documents: StageDocumentsView[],
- projectType: "ship" | "plant"
-) {
- const headers = [
- "문서번호",
- "문서명",
- "문서종류",
- "PIC",
- "발행일",
- "현재스테이지",
- "스테이지상태",
- "계획일",
- "담당자",
- "우선순위",
- "진행률(%)",
- "완료스테이지",
- "전체스테이지",
- "지연여부",
- "남은일수",
- "생성일",
- "수정일"
- ]
-
- // Plant 프로젝트 전용 헤더 추가
- if (projectType === "plant") {
- headers.splice(3, 0, "벤더문서번호", "벤더명", "벤더코드")
- }
-
- const data = documents.map(doc => {
- const baseData = [
- doc.docNumber,
- doc.title,
- doc.drawingKind || "",
- doc.pic || "",
- doc.issuedDate || "",
- doc.currentStageName || "",
- getStatusText(doc.currentStageStatus || ""),
- doc.currentStagePlanDate || "",
- doc.currentStageAssigneeName || "",
- getPriorityText(doc.currentStagePriority || ""),
- doc.progressPercentage || 0,
- doc.completedStages || 0,
- doc.totalStages || 0,
- doc.isOverdue ? "예" : "아니오",
- doc.daysUntilDue || "",
- doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : "",
- doc.updatedAt ? new Date(doc.updatedAt).toLocaleDateString() : ""
- ]
-
- // Plant 프로젝트 데이터 추가
- if (projectType === "plant") {
- baseData.splice(3, 0,
- doc.vendorDocNumber || "",
- doc.vendorName || "",
- doc.vendorCode || ""
- )
- }
-
- return baseData
- })
-
- const worksheet = XLSX.utils.aoa_to_sheet([headers, ...data])
-
- // 컬럼 너비 설정
- const colWidths = [
- { wch: 15 }, // 문서번호
- { wch: 30 }, // 문서명
- { wch: 10 }, // 문서종류
- ...(projectType === "plant" ? [
- { wch: 15 }, // 벤더문서번호
- { wch: 20 }, // 벤더명
- { wch: 10 }, // 벤더코드
- ] : []),
- { wch: 10 }, // PIC
- { wch: 12 }, // 발행일
- { wch: 15 }, // 현재스테이지
- { wch: 10 }, // 스테이지상태
- { wch: 12 }, // 계획일
- { wch: 10 }, // 담당자
- { wch: 8 }, // 우선순위
- { wch: 8 }, // 진행률
- { wch: 8 }, // 완료스테이지
- { wch: 8 }, // 전체스테이지
- { wch: 8 }, // 지연여부
- { wch: 8 }, // 남은일수
- { wch: 12 }, // 생성일
- { wch: 12 }, // 수정일
- ]
-
- worksheet['!cols'] = colWidths
-
- const workbook = XLSX.utils.book_new()
- XLSX.utils.book_append_sheet(workbook, worksheet, "문서목록")
-
- const filename = `문서목록_${new Date().toISOString().split('T')[0]}.xlsx`
- XLSX.writeFile(workbook, filename)
-}
-
-// 스테이지 상세 데이터를 엑셀로 익스포트
-export function exportStageDetailsToExcel(documents: StageDocumentsView[]) {
- const headers = [
- "문서번호",
- "문서명",
- "스테이지명",
- "스테이지상태",
- "스테이지순서",
- "계획일",
- "담당자",
- "우선순위",
- "설명",
- "노트",
- "알림일수"
- ]
-
- const data: any[] = []
-
- documents.forEach(doc => {
- if (doc.allStages && doc.allStages.length > 0) {
- doc.allStages.forEach(stage => {
- data.push([
- doc.docNumber,
- doc.title,
- stage.stageName,
- getStatusText(stage.stageStatus),
- stage.stageOrder,
- stage.planDate || "",
- stage.assigneeName || "",
- getPriorityText(stage.priority),
- stage.description || "",
- stage.notes || "",
- stage.reminderDays || ""
- ])
- })
- } else {
- // 스테이지가 없는 문서도 포함
- data.push([
- doc.docNumber,
- doc.title,
- "", "", "", "", "", "", "", "", ""
- ])
- }
- })
-
- const worksheet = XLSX.utils.aoa_to_sheet([headers, ...data])
-
- // 컬럼 너비 설정
- worksheet['!cols'] = [
- { wch: 15 }, // 문서번호
- { wch: 30 }, // 문서명
- { wch: 20 }, // 스테이지명
- { wch: 12 }, // 스테이지상태
- { wch: 8 }, // 스테이지순서
- { wch: 12 }, // 계획일
- { wch: 10 }, // 담당자
- { wch: 8 }, // 우선순위
- { wch: 25 }, // 설명
- { wch: 25 }, // 노트
- { wch: 8 }, // 알림일수
- ]
-
- const workbook = XLSX.utils.book_new()
- XLSX.utils.book_append_sheet(workbook, worksheet, "스테이지상세")
-
- const filename = `스테이지상세_${new Date().toISOString().split('T')[0]}.xlsx`
- XLSX.writeFile(workbook, filename)
-}
-
-// =============================================================================
-// 4. 유틸리티 함수들
-// =============================================================================
-
-function getStatusText(status: string): string {
- switch (status) {
- case 'PLANNED': return '계획됨'
- case 'IN_PROGRESS': return '진행중'
- case 'SUBMITTED': return '제출됨'
- case 'UNDER_REVIEW': return '검토중'
- case 'APPROVED': return '승인됨'
- case 'REJECTED': return '반려됨'
- case 'COMPLETED': return '완료됨'
- default: return status
- }
-}
-
-function getPriorityText(priority: string): string {
- switch (priority) {
- case 'HIGH': return '높음'
- case 'MEDIUM': return '보통'
- case 'LOW': return '낮음'
- default: return priority
- }
-}
-
-// 파일 크기 검증
-export function validateFileSize(file: File, maxSizeMB: number = 10): boolean {
- const maxSizeBytes = maxSizeMB * 1024 * 1024
- return file.size <= maxSizeBytes
-}
-
-// 파일 확장자 검증
-export function validateFileExtension(file: File): boolean {
- const allowedExtensions = ['.xlsx', '.xls']
- const fileName = file.name.toLowerCase()
- return allowedExtensions.some(ext => fileName.endsWith(ext))
-}
-
-// ExcelJS 워크북의 유효성 검사
-export async function validateExcelWorkbook(file: File): Promise<{
- isValid: boolean
- error?: string
- worksheetCount?: number
- firstWorksheetName?: string
-}> {
- try {
- const buffer = await file.arrayBuffer()
- const workbook = new ExcelJS.Workbook()
- await workbook.xlsx.load(buffer)
-
- const worksheets = workbook.worksheets
- if (worksheets.length === 0) {
- return {
- isValid: false,
- error: '워크시트가 없는 파일입니다'
- }
- }
-
- const firstWorksheet = worksheets[0]
- if (firstWorksheet.rowCount < 2) {
- return {
- isValid: false,
- error: '데이터가 없습니다. 최소 헤더와 1개 행이 필요합니다'
- }
- }
-
- return {
- isValid: true,
- worksheetCount: worksheets.length,
- firstWorksheetName: firstWorksheet.name
- }
- } catch (error) {
- return {
- isValid: false,
- error: `파일을 읽을 수 없습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`
- }
- }
-}
-
-// 셀 값을 안전하게 문자열로 변환
-export function getCellValueAsString(cell: ExcelJS.Cell): string {
- if (!cell.value) return ""
-
- if (cell.value instanceof Date) {
- return cell.value.toISOString().split('T')[0]
- }
-
- if (typeof cell.value === 'object' && 'text' in cell.value) {
- return cell.value.text || ""
- }
-
- if (typeof cell.value === 'object' && 'result' in cell.value) {
- return String(cell.value.result || "")
- }
-
- return String(cell.value)
-}
-
-// 엑셀 컬럼 인덱스를 문자로 변환 (A, B, C, ... Z, AA, AB, ...)
-export function getExcelColumnName(index: number): string {
- let result = ""
- while (index > 0) {
- index--
- result = String.fromCharCode(65 + (index % 26)) + result
- index = Math.floor(index / 26)
- }
- return result
-} \ No newline at end of file