mcp-excel-controller-pro
Version:
Excel Controller MCP Server
1,658 lines (1,485 loc) • 89.4 kB
JavaScript
#!/usr/bin/env node
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const {
StdioServerTransport,
} = require("@modelcontextprotocol/sdk/server/stdio.js");
const { z } = require("zod");
const ExcelJS = require("exceljs");
const fs = require("fs");
const path = require("path");
const { exec, spawn } = require("child_process");
// 새로운 MCP 서버 생성
const server = new McpServer({
name: "ExcelControllerPro",
version: "1.0.0",
});
// 명령 실행 Promise 래핑
const execPromise = (cmd) => {
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
resolve(stdout);
});
});
};
// Excel 앱 실행 함수
async function openExcelFile(filePath) {
try {
// 파일 경로 정규화
const normalizedPath = path.resolve(filePath);
// 엑셀 앱 시작
const process = spawn("start", ["excel", `"${normalizedPath}"`], {
shell: true,
detached: true,
stdio: "ignore",
});
// 프로세스를 추적하지 않음
process.unref();
return true;
} catch (error) {
return false;
}
}
// 파일이 열려있는지 확인하는 함수
function isFileOpen(filePath) {
try {
const fd = fs.openSync(filePath, "r+");
fs.closeSync(fd);
return false;
} catch (e) {
return true;
}
}
// 임시 PowerShell 스크립트 실행 함수
async function runPowerShellScript(scriptContent) {
const timestamp = Date.now();
const scriptPath = path.join(process.cwd(), `excel_script_${timestamp}.ps1`);
try {
// 스크립트를 파일로 저장
fs.writeFileSync(scriptPath, scriptContent);
// PowerShell 스크립트 실행
const result = await execPromise(
`powershell -ExecutionPolicy Bypass -NoLogo -NonInteractive -File "${scriptPath}" 2> NUL`
);
return result;
} catch (error) {
throw error;
} finally {
// 임시 파일 삭제
try {
fs.unlinkSync(scriptPath);
} catch (err) {
// 파일 삭제 오류는 무시
}
}
}
// Excel 셀 참조(예: "A1", "B5")를 행과 열 번호로 변환하는 함수
function cellReferenceToRowCol(cellReference) {
// 알파벳과 숫자 부분 분리 (예: "A11" -> "A"와 "11")
const match = cellReference.match(/([A-Za-z]+)([0-9]+)/);
if (!match) {
throw new Error(`유효하지 않은 셀 참조: ${cellReference}`);
}
const colStr = match[1].toUpperCase();
const row = parseInt(match[2], 10);
// 알파벳을 열 번호로 변환 (A=1, B=2, ..., Z=26, AA=27, ...)
let col = 0;
for (let i = 0; i < colStr.length; i++) {
col = col * 26 + (colStr.charCodeAt(i) - 64);
}
return { row, col };
}
// Excel 프로세스 확인 및 대기 함수
async function waitForExcelAvailable(maxAttempts = 3) {
for (let i = 0; i < maxAttempts; i++) {
try {
const result = await execPromise(
'powershell -Command "Get-Process excel -ErrorAction SilentlyContinue | Out-String"'
);
if (result && result.trim()) {
// Excel이 실행 중이면 잠시 대기
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
}
} catch (e) {
// 오류 무시
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
// 열려있는 Excel 파일의 데이터 업데이트 함수 (재시도 로직 포함)
async function updateExcelWithRetry(filePath, sheetName, data, maxAttempts = 3) {
for (let i = 0; i < maxAttempts; i++) {
try {
// Excel 프로세스 확인 및 대기
await waitForExcelAvailable();
// 정규화된 경로로 변환
const normalizedPath = path.resolve(filePath);
// 데이터 형식 검증
if (!Array.isArray(data) || data.length === 0) {
return {
success: false,
message: "유효한 데이터가 아닙니다. 2차원 배열 형식이어야 합니다.",
};
}
// PowerShell 스크립트 작성 - 디버그 출력 완전 제거
let psScript = `
# 모든 오류 메시지 숨기기 (중요한 오류만 캡처)
$ErrorActionPreference = "SilentlyContinue"
try {
# 기존 Excel 인스턴스 가져오기
$excel = $null
try {
$excel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
} catch {
# 기존 인스턴스가 없으면 새로 생성
$excel = New-Object -ComObject Excel.Application
}
$excel.Visible = $true
$excel.DisplayAlerts = $false
# 이미 열려있는 워크북 찾기
$workbook = $null
foreach ($wb in $excel.Workbooks) {
if ($wb.FullName -eq "${normalizedPath.replace(
/\\/g,
"\\\\"
)}") {
$workbook = $wb
break
}
}
# 워크북이 없으면 열기
if ($workbook -eq $null) {
$workbook = $excel.Workbooks.Open("${normalizedPath.replace(
/\\/g,
"\\\\"
)}")
}
# 워크북 활성화 - 현재 활성 워크북으로 설정
$workbook.Activate()
# 시트 선택
$worksheet = $null
`;
if (sheetName) {
psScript += `
# 지정된 시트 찾기 시도
try {
$worksheet = $workbook.Worksheets("${sheetName}")
} catch {
# 지정된 시트가 없으면 첫 번째 시트 사용
$worksheet = $workbook.Worksheets.Item(1)
}
`;
} else {
psScript += `
# 시트 이름을 지정하지 않았으므로 활성 시트 사용
$worksheet = $workbook.ActiveSheet
`;
}
psScript += `
# 시트 활성화 - 현재 활성 시트로 설정
$worksheet.Activate()
# 데이터가 있는 사용된 범위를 모두 지우기
# 주의: 기존 데이터가 없을 수도 있으므로 예외 처리
try {
$usedRange = $worksheet.UsedRange
if ($usedRange -ne $null -and $usedRange.Cells.Count -gt 0)
} catch {
# 계속 진행
}
# 데이터를 배열로 설정
$rowCount = ${data.length}
$colCount = ${Math.max(...data.map((row) => row.length))}
# 데이터를 한 번에 설정할 범위 생성
$targetRange = $worksheet.Range($worksheet.Cells(1, 1), $worksheet.Cells($rowCount, $colCount))
# 2차원 배열 생성
$dataArray = New-Object 'object[,]' $rowCount, $colCount
`;
// 데이터 행과 열에 대한 설정
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
const row = data[rowIndex];
for (let colIndex = 0; colIndex < row.length; colIndex++) {
const cellValue = row[colIndex];
// 값 타입에 따른 처리
if (typeof cellValue === "string") {
// 문자열은 따옴표로 묶고 특수문자 처리
const escapedValue = cellValue
.replace(/'/g, "''")
.replace(/"/g, '""');
psScript += `
$dataArray[${rowIndex}, ${colIndex}] = '${escapedValue}'`;
} else if (cellValue === null || cellValue === undefined) {
// null/undefined는 빈 문자열로
psScript += `
$dataArray[${rowIndex}, ${colIndex}] = ''`;
} else if (typeof cellValue === "number") {
// 숫자는 그대로
psScript += `
$dataArray[${rowIndex}, ${colIndex}] = ${cellValue}`;
} else if (typeof cellValue === "boolean") {
// 불리언 값 처리
psScript += `
$dataArray[${rowIndex}, ${colIndex}] = $${cellValue}`;
} else {
// 기타 값은 문자열로 변환
psScript += `
$dataArray[${rowIndex}, ${colIndex}] = '${String(cellValue).replace(
/'/g,
"''"
)}'`;
}
}
}
psScript += `
# 데이터 배열을 범위에 한 번에 설정
$targetRange.Value2 = $dataArray
# 변경 사항이 보이도록 셀 A1로 이동
$worksheet.Cells(1, 1).Select()
# 저장 확인
$workbook.Save()
# 자동 맞춤 적용 (열 너비 자동 조절)
$usedRange = $worksheet.UsedRange
$usedRange.Columns.AutoFit() | Out-Null
# 성공 메시지 - 표준 출력으로만 한 줄 출력
Write-Output "SUCCESS: Worksheet updated with ${data.length} rows of data and saved"
} catch {
# 오류 내용 - 표준 출력으로만 한 줄 출력
Write-Output "ERROR: $($_.Exception.Message)"
} finally {
# COM 객체 참조 해제 (Excel 프로그램은 종료하지 않음)
if ($worksheet -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) | Out-Null
}
if ($workbook -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
}
if ($excel -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
}
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
`;
// PowerShell 스크립트 실행
const result = await runPowerShellScript(psScript);
// 결과 확인
if (result && result.includes("SUCCESS")) {
return { success: true, message: result.trim() };
} else {
if (i === maxAttempts - 1) {
// 마지막 시도에서 실패했을 때만 오류 반환
return {
success: false,
message: result
? result.trim()
: "스크립트 실행 중 오류가 발생했습니다.",
};
}
// 아니면 재시도를 위해 계속 진행
}
} catch (error) {
console.error(`시도 ${i + 1}/${maxAttempts} 실패:`, error.message);
if (i === maxAttempts - 1) {
// 마지막 시도에서 실패했을 때만 오류 반환
return {
success: false,
message: `범위 데이터 업데이트 오류: ${error.message}`,
};
}
}
// 다음 시도 전 잠시 대기 (점진적으로 대기 시간 증가)
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
}
// 모든 시도 실패 시 기본 오류 반환
return {
success: false,
message: `${maxAttempts}번 시도 후에도 Excel 파일 업데이트 실패`,
};
}
// 특정 셀들만 업데이트하는 함수
async function updateSpecificCells(filePath, sheetName, cellData, maxAttempts = 3) {
for (let i = 0; i < maxAttempts; i++) {
try {
// Excel 프로세스 확인 및 대기
await waitForExcelAvailable();
// 정규화된 경로로 변환
const normalizedPath = path.resolve(filePath);
// PowerShell 스크립트 작성
let psScript = `
# 모든 오류 메시지 숨기기 (중요한 오류만 캡처)
$ErrorActionPreference = "SilentlyContinue"
try {
# 기존 Excel 인스턴스 가져오기
$excel = $null
try {
$excel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
} catch {
# 기존 인스턴스가 없으면 새로 생성
$excel = New-Object -ComObject Excel.Application
}
$excel.Visible = $true
$excel.DisplayAlerts = $false
# 이미 열려있는 워크북 찾기
$workbook = $null
foreach ($wb in $excel.Workbooks) {
if ($wb.FullName -eq "${normalizedPath.replace(
/\\/g,
"\\\\"
)}") {
$workbook = $wb
break
}
}
# 워크북이 없으면 열기
if ($workbook -eq $null) {
$workbook = $excel.Workbooks.Open("${normalizedPath.replace(
/\\/g,
"\\\\"
)}")
}
# 워크북 활성화 - 현재 활성 워크북으로 설정
$workbook.Activate()
# 시트 선택
$worksheet = $null
`;
if (sheetName) {
psScript += `
# 지정된 시트 찾기 시도
try {
$worksheet = $workbook.Worksheets("${sheetName}")
} catch {
# 지정된 시트가 없으면 첫 번째 시트 사용
$worksheet = $workbook.Worksheets.Item(1)
}
`;
} else {
psScript += `
# 시트 이름을 지정하지 않았으므로 활성 시트 사용
$worksheet = $workbook.ActiveSheet
`;
}
psScript += `
# 시트 활성화 - 현재 활성 시트로 설정
$worksheet.Activate()
# 각 셀 업데이트
$updateCount = 0
$errorCount = 0
`;
// 각 셀 업데이트 코드 추가
for (const cell of cellData) {
try {
let cellReference = cell.cellReference;
let value = cell.value;
// 셀 참조를 행과 열 번호로 변환 시도
let row, col;
try {
const result = cellReferenceToRowCol(cellReference);
row = result.row;
col = result.col;
} catch (err) {
// 셀 참조 변환 오류 처리
psScript += `
Write-Output "ERROR_CELL: 유효하지 않은 셀 참조 - ${cellReference}"
$errorCount++
`;
continue;
}
// 값 타입에 따른 처리
let valueStr;
if (typeof value === "string") {
valueStr = `"${value.replace(/"/g, '""')}"`;
} else if (value === null || value === undefined) {
valueStr = '""';
} else if (typeof value === "boolean") {
valueStr = value ? "$true" : "$false";
} else {
valueStr = value;
}
// 셀 업데이트 코드 추가
psScript += `
try {
$worksheet.Cells(${row}, ${col}).Value2 = ${valueStr}
$updateCount++
} catch {
$errorCount++
Write-Output "ERROR_CELL: 셀 ${cellReference} 업데이트 중 오류 발생"
}
`;
} catch (error) {
// 코드 생성 중 오류는 무시하고 계속 진행
continue;
}
}
psScript += `
# 저장 확인
$workbook.Save()
# 성공 메시지 - 표준 출력으로만 한 줄 출력
Write-Output "SUCCESS: 총 $updateCount 개 셀이 업데이트됨 (오류: $errorCount 개)"
} catch {
# 오류 내용 - 표준 출력으로만 한 줄 출력
Write-Output "ERROR: $($_.Exception.Message)"
} finally {
# COM 객체 참조 해제 (Excel 프로그램은 종료하지 않음)
if ($worksheet -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) | Out-Null
}
if ($workbook -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
}
if ($excel -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
}
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
`;
// PowerShell 스크립트 실행
const result = await runPowerShellScript(psScript);
// 결과 확인
if (result && result.includes("SUCCESS")) {
return { success: true, message: result.trim() };
} else {
if (i === maxAttempts - 1) {
// 마지막 시도에서 실패했을 때만 오류 반환
return {
success: false,
message: result
? result.trim()
: "스크립트 실행 중 오류가 발생했습니다.",
};
}
// 아니면 재시도를 위해 계속 진행
}
} catch (error) {
console.error(`시도 ${i + 1}/${maxAttempts} 실패:`, error.message);
if (i === maxAttempts - 1) {
// 마지막 시도에서 실패했을 때만 오류 반환
return {
success: false,
message: `셀 데이터 업데이트 오류: ${error.message}`,
};
}
}
// 다음 시도 전 잠시 대기 (점진적으로 대기 시간 증가)
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
}
// 모든 시도 실패 시 기본 오류 반환
return {
success: false,
message: `${maxAttempts}번 시도 후에도 Excel 셀 업데이트 실패`,
};
}
// 통합된 Excel 업데이트 도구
server.tool(
"update_excel",
"Excel 파일의 데이터를 업데이트합니다. 전체 범위 또는 특정 셀을 지정하여 업데이트할 수 있습니다.",
{
filePath: z.string().describe("엑셀 파일의 경로"),
sheetName: z
.string()
.optional()
.describe("시트 이름 (지정하지 않으면 활성 시트 또는 첫 번째 시트)"),
// 세 가지 업데이트 모드 중 하나 선택
mode: z
.enum(["full", "append", "cells"])
.default("full")
.describe("업데이트 모드 (full: 덮어쓰기, append: 추가, cells: 특정 셀)"),
data: z
.array(z.array(z.any()))
.optional()
.describe("2차원 배열 형태의 데이터 (full 또는 append 모드에서 사용)"),
cellData: z
.array(
z.object({
cellReference: z.string(),
value: z.any(),
})
)
.optional()
.describe("업데이트할 셀 데이터 배열 (cells 모드에서 사용)"),
openFile: z
.boolean()
.optional()
.default(false)
.describe("작업 후 Excel로 파일을 열지 여부"),
createBackup: z
.boolean()
.optional()
.default(true)
.describe("작업 전 백업 생성 여부"),
smartMerge: z
.boolean()
.optional()
.default(true)
.describe("스마트 데이터 병합 모드 (데이터 구조 분석 및 적절한 병합, append 모드에서만 사용)"),
},
async ({
filePath,
sheetName,
mode = "full",
data,
cellData,
openFile = false,
createBackup = true,
smartMerge = true,
}) => {
try {
// 파일 존재 확인
if (!fs.existsSync(filePath)) {
return {
content: [
{ type: "text", text: `파일을 찾을 수 없습니다: ${filePath}` },
],
isError: true,
};
}
// 모드에 따른 데이터 유효성 검사
if ((mode === "full" || mode === "append") && (!Array.isArray(data) || data.length === 0)) {
// 데이터가 없지만 파일을 열기만 원하는 경우
if (openFile) {
const success = await openExcelFile(filePath);
if (success) {
return {
content: [
{
type: "text",
text: `엑셀 파일이 성공적으로 열렸습니다: ${filePath}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `엑셀 파일을 열지 못했습니다: ${filePath}`,
},
],
isError: true,
};
}
}
return {
content: [
{ type: "text", text: "유효한 데이터가 제공되지 않았습니다." },
],
isError: true,
};
}
if (mode === "cells" && (!Array.isArray(cellData) || cellData.length === 0)) {
// 데이터가 없지만 파일을 열기만 원하는 경우
if (openFile) {
const success = await openExcelFile(filePath);
if (success) {
return {
content: [
{
type: "text",
text: `엑셀 파일이 성공적으로 열렸습니다: ${filePath}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `엑셀 파일을 열지 못했습니다: ${filePath}`,
},
],
isError: true,
};
}
}
return {
content: [
{ type: "text", text: "유효한 셀 데이터가 제공되지 않았습니다." },
],
isError: true,
};
}
// 백업 디렉토리 및 파일 생성 (작업 전)
let backupPath = "";
if (createBackup) {
// 파일 이름과 확장자 분리
const fileDir = path.dirname(filePath);
const fileName = path.basename(filePath);
const fileNameWithoutExt = path.basename(
fileName,
path.extname(fileName)
);
const fileExt = path.extname(fileName);
// 백업 디렉토리 경로 생성
const backupDir = path.join(fileDir, `log_${fileNameWithoutExt}`);
// 백업 디렉토리가 없으면 생성
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
// 현지 시간대로 형식화
const now = new Date();
const options = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
};
// 시스템의 현지 시간대를 사용
const localTime = new Intl.DateTimeFormat(undefined, options).format(now);
// 형식 정리 (yyyy-MM-dd HH:mm:ss → yyyyMMdd_HHmmss)
const timestamp = localTime
.replace(/[\/\-\,\s]/g, "") // 슬래시, 하이픈, 콤마, 공백 제거
.replace(/:/g, "") // 콜론 제거
.replace(/(\d{8})(\d{6})/, "$1_$2"); // 날짜와 시간 사이에 언더스코어 추가
backupPath = path.join(backupDir, `${fileNameWithoutExt}_${timestamp}${fileExt}`);
// 파일 복사
fs.copyFileSync(filePath, backupPath);
}
// 파일이 열려있는지 확인
const fileIsOpen = isFileOpen(filePath);
// 모드에 따른 처리 - cells 모드 또는 full/append 모드
let processCellData = [];
if (mode === "cells") {
// cells 모드: 사용자 제공 셀 데이터 그대로 사용
processCellData = cellData;
} else {
// full 또는 append 모드: 2D 배열을 셀 데이터로 변환
// append 모드면 기존 데이터 가져와서 병합
let mergedData = [];
if (mode === "append") {
// 기존 데이터 가져오기
let existingData = [];
try {
// ExcelJS를 사용하여 파일 읽기 시도
const tempWorkbook = new ExcelJS.Workbook();
await tempWorkbook.xlsx.readFile(filePath);
// 시트 선택
let tempWorksheet;
if (sheetName) {
tempWorksheet = tempWorkbook.getWorksheet(sheetName);
} else if (tempWorkbook.worksheets.length > 0) {
tempWorksheet = tempWorkbook.worksheets[0];
}
// 시트에서 데이터 추출
if (tempWorksheet) {
existingData = [];
tempWorksheet.eachRow((row, rowNumber) => {
const rowData = [];
row.eachCell((cell, colNumber) => {
rowData[colNumber - 1] = cell.value;
});
existingData[rowNumber - 1] = rowData;
});
// 빈 행 제거 (맨 앞에 있을 수 있는 undefined 요소)
existingData = existingData.filter((row) => row !== undefined);
}
} catch (err) {
// 읽기 실패 시 빈 배열로 진행
existingData = [];
}
// 데이터 병합
if (existingData.length > 0) {
if (smartMerge) {
// 기존 데이터 분석
const existingHeadersCount = existingData[0]?.length || 0;
const newHeadersCount = data[0]?.length || 0;
// 데이터 병합 방식 결정
if (newHeadersCount > existingHeadersCount) {
// 새 데이터에 열이 더 많은 경우: 열 확장 처리
mergedData = existingData.map((row, rowIndex) => {
if (rowIndex < data.length) {
// 기존 데이터의 키 열을 기준으로 일치하는 행 찾기
// 여기서는 첫 번째 열을 키로 가정 (예: 영어단어)
if (row[0] === data[rowIndex][0]) {
// 첫 번째 열 값이 일치하면 나머지 열 확장
return [...row, ...data[rowIndex].slice(existingHeadersCount)];
}
}
return row;
});
// 기존 데이터에 없는 새 행 추가
const existingKeys = existingData.map(row => row[0]);
const newRows = data.filter(row => !existingKeys.includes(row[0]));
mergedData = [...mergedData, ...newRows];
}
else if (data.length > existingData.length) {
// 새 데이터에 행이 더 많은 경우: 행 추가 처리
const existingKeys = existingData.map(row => row[0]);
// 기존 데이터와 중복되지 않는 새 행만 추가
const newRows = data.filter(row => !existingKeys.includes(row[0]));
mergedData = [...existingData, ...newRows];
}
else {
// 동일한 구조이거나 단순 업데이트: 키 비교 후 병합
const mergeMap = new Map();
// 기존 데이터를 Map에 저장 (키: 첫 열 값)
existingData.forEach(row => {
mergeMap.set(row[0], row);
});
// 새 데이터로 업데이트 또는 추가
data.forEach(row => {
mergeMap.set(row[0], row);
});
// Map을 다시 배열로 변환
mergedData = Array.from(mergeMap.values());
}
} else {
// 스마트 병합 비활성화: 단순 추가
mergedData = [...existingData, ...data];
}
} else {
// 기존 데이터가 없으면 새 데이터 그대로 사용
mergedData = data;
}
} else {
// full 모드: 데이터 그대로 사용
mergedData = data;
// 현재 시트의 사용된 범위 정보 가져오기
let maxRow = 0;
let maxCol = 0;
// 파일이 열려있지 않은 경우 기존 시트 범위 정보 가져오기
if (!fileIsOpen) {
try {
const tempWorkbook = new ExcelJS.Workbook();
await tempWorkbook.xlsx.readFile(filePath);
const tempWorksheet = sheetName
? tempWorkbook.getWorksheet(sheetName)
: tempWorkbook.worksheets[0];
if (tempWorksheet && tempWorksheet.usedRange) {
maxRow = tempWorksheet.rowCount || 100; // 기본값 제공
maxCol = tempWorksheet.columnCount || 20; // 기본값 제공
} else {
// 기본 범위 제공
maxRow = 100;
maxCol = 20;
}
} catch (err) {
// 읽기 실패 시 기본 범위 사용
maxRow = 100;
maxCol = 20;
}
} else {
// 파일이 열려있는 경우 더 넓은 기본 범위 사용
maxRow = 100;
maxCol = 20;
}
// 명시적으로 기존 셀 데이터를 빈 값으로 설정하는 코드
const cellDataToEmpty = [];
// 기존 데이터의 모든 셀에 대해 빈 값 할당
for (let row = 1; row <= maxRow; row++) {
for (let col = 1; col <= maxCol; col++) {
let colLetter = '';
let tempCol = col;
while (tempCol > 0) {
const remainder = (tempCol - 1) % 26;
colLetter = String.fromCharCode(65 + remainder) + colLetter;
tempCol = Math.floor((tempCol - 1) / 26);
}
cellDataToEmpty.push({
cellReference: `${colLetter}${row}`,
value: ""
});
}
}
// 빈 값으로 셀 초기화 먼저 수행
if (data.length === 0 || (data.length === 1 && data[0].length === 0)) {
// 빈 데이터인 경우 시트 초기화만 수행
processCellData = cellDataToEmpty;
} else {
// 빈 셀로 초기화 후 새 데이터 설정
processCellData = [...cellDataToEmpty];
}
}
// 2D 배열 데이터를 cellData 형식으로 변환 (빈 데이터가 아닌 경우만)
if (!(mode === "full" && (data.length === 0 || (data.length === 1 && data[0].length === 0)))) {
for (let rowIndex = 0; rowIndex < mergedData.length; rowIndex++) {
const row = mergedData[rowIndex];
for (let colIndex = 0; colIndex < row.length; colIndex++) {
if (row[colIndex] === undefined || row[colIndex] === null) continue;
// 열 문자 생성 (0 -> A, 1 -> B, ...)
let colLetter = '';
let tempCol = colIndex + 1;
while (tempCol > 0) {
const remainder = (tempCol - 1) % 26;
colLetter = String.fromCharCode(65 + remainder) + colLetter;
tempCol = Math.floor((tempCol - 1) / 26);
}
// A1 형식의 셀 참조 생성
const cellRef = `${colLetter}${rowIndex + 1}`;
// full 모드에서 기존에 빈 셀 초기화를 했다면 해당 셀 참조는 덮어씌우기
if (mode === "full") {
// 이미 초기화된 셀 참조 찾기
const existingIndex = processCellData.findIndex(
cell => cell.cellReference === cellRef
);
if (existingIndex !== -1) {
// 기존 셀 참조 업데이트
processCellData[existingIndex].value = row[colIndex];
} else {
// 없으면 새로 추가
processCellData.push({
cellReference: cellRef,
value: row[colIndex]
});
}
} else {
// append 모드에서는 그냥 추가
processCellData.push({
cellReference: cellRef,
value: row[colIndex]
});
}
}
}
}
}
// 이제 모든 모드에서 셀 단위 업데이트 방식 사용
if (fileIsOpen) {
// 파일이 열려있는 경우 COM 인터페이스 사용
let result;
// mode가 'full'이거나 'append'일 때 updateExcelWithRetry 사용
if (mode === 'full' || mode === 'append') {
// 2D 배열 데이터를 직접 전달
result = await updateExcelWithRetry(filePath, sheetName, mergedData, 3);
} else {
// 'cells' 모드일 때만 updateSpecificCells 사용
result = await updateSpecificCells(filePath, sheetName, processCellData, 3);
}
if (result && result.success) {
// 요청 시 Excel로 파일 열기
if (openFile) {
// COM 인터페이스를 통해 열린 파일을 전면에 가져옴
const bringToFrontScript = `
$excel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
$excel.Visible = $true
$excel.Application.WindowState = -4143 # xlMaximized
$excel.Application.Activate()
`;
await runPowerShellScript(bringToFrontScript);
}
let backupMessage = "";
if (createBackup && backupPath) {
backupMessage = `\n백업 생성됨: ${backupPath}`;
}
// 성공 개수 추출
const successMatch = result.message.match(/총 (\d+) 개 셀이/);
const successCount = successMatch ? successMatch[1] : processCellData.length;
// 오류 개수 추출
const errorMatch = result.message.match(/오류: (\d+) 개/);
const errorCount = errorMatch ? parseInt(errorMatch[1]) : 0;
// 모드에 따라 다른 성공 메시지 구성
let successText = "";
if (mode === "cells") {
successText = `엑셀 파일의 특정 셀들이 성공적으로 업데이트되었습니다:
- 파일: ${filePath}
- 시트: ${sheetName || "활성 시트"}
- 업데이트된 셀: ${successCount}개${errorCount > 0 ? ` (오류: ${errorCount}개)` : ''}${backupMessage}`;
} else {
successText = `엑셀 파일에 데이터가 성공적으로 업데이트되었습니다:
- 파일: ${filePath}
- 시트: ${sheetName || "활성 시트"}
- 모드: ${mode === "append" ? (smartMerge ? "스마트 병합" : "추가") : "덮어쓰기"}
- 업데이트된 셀: ${successCount}개${errorCount > 0 ? ` (오류: ${errorCount}개)` : ''}${backupMessage}`;
}
return {
content: [
{
type: "text",
text: successText + "\n\n변경 사항이 Excel에 즉시 반영되었습니다.",
},
],
};
} else {
// 실패했지만 파일 열기만 시도
if (openFile) {
await openExcelFile(filePath);
return {
content: [
{
type: "text",
text: `데이터 업데이트는 실패했지만 Excel 파일을 열기 시도했습니다: ${filePath}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `열려있는 엑셀 파일 업데이트 실패: ${
result ? result.message : "알 수 없는 오류"
}`,
},
],
isError: true,
};
}
} else {
// 파일이 열려있지 않은 경우 ExcelJS 사용
const workbook = new ExcelJS.Workbook();
try {
// 파일이 이미 있으면 읽기
if (fs.existsSync(filePath)) {
await workbook.xlsx.readFile(filePath);
}
} catch (err) {
// 읽기 실패시 새 워크북으로 간주
}
// 시트 확인 및 생성
let worksheet;
if (sheetName) {
worksheet = workbook.getWorksheet(sheetName);
if (!worksheet) {
worksheet = workbook.addWorksheet(sheetName);
} else if (mode === "full") {
// full 모드에서는 기존 데이터 지우기
const rowCount = worksheet.rowCount;
for (let i = rowCount; i >= 1; i--) {
worksheet.spliceRows(i, 1);
}
}
} else {
if (workbook.worksheets.length === 0) {
worksheet = workbook.addWorksheet("Sheet1");
} else {
worksheet = workbook.worksheets[0];
if (mode === "full") {
// full 모드에서는 기존 데이터 지우기
const rowCount = worksheet.rowCount;
for (let i = rowCount; i >= 1; i--) {
worksheet.spliceRows(i, 1);
}
}
}
}
// 셀 단위로 업데이트
let successCount = 0;
let errorCells = [];
for (const cell of processCellData) {
try {
worksheet.getCell(cell.cellReference).value = cell.value;
successCount++;
} catch (err) {
errorCells.push(cell.cellReference);
}
}
// 파일 저장
await workbook.xlsx.writeFile(filePath);
// 요청 시 Excel로 파일 열기
if (openFile) {
await openExcelFile(filePath);
}
let backupMessage = "";
if (createBackup && backupPath) {
backupMessage = `\n백업 생성됨: ${backupPath}`;
}
// 모드에 따라 다른 성공 메시지 구성
let successText = "";
if (mode === "cells") {
successText = `엑셀 파일의 특정 셀들이 성공적으로 업데이트되었습니다:
- 파일: ${filePath}
- 시트: ${worksheet.name}
- 업데이트된 셀: ${successCount}개${errorCells.length > 0 ? ` (실패: ${errorCells.length}개)` : ''}${backupMessage}`;
} else {
successText = `엑셀 파일에 데이터가 성공적으로 업데이트되었습니다:
- 파일: ${filePath}
- 시트: ${worksheet.name}
- 모드: ${mode === "append" ? (smartMerge ? "스마트 병합" : "추가") : "덮어쓰기"}
- 업데이트된 셀: ${successCount}개${errorCells.length > 0 ? ` (실패: ${errorCells.length}개)` : ''}${backupMessage}`;
}
return {
content: [
{
type: "text",
text: successText,
},
],
};
}
} catch (error) {
// 오류가 발생했지만 파일 열기만 시도
if (openFile) {
await openExcelFile(filePath);
return {
content: [
{
type: "text",
text: `데이터 업데이트 중 오류(${error.message})가 발생했지만 Excel 파일을 열기 시도했습니다: ${filePath}`,
},
],
};
}
return {
content: [
{ type: "text", text: `엑셀 데이터 업데이트 오류: ${error.message}` },
],
isError: true,
};
}
}
)
// 엑셀 파일 읽기 도구
server.tool(
"read_excel",
"엑셀 파일을 읽어 내용을 반환합니다.",
{
filePath: z.string().describe("읽을 엑셀 파일의 경로"),
sheetName: z.string().optional().describe("읽을 시트 이름 (옵션)"),
openFile: z
.boolean()
.optional()
.default(false)
.describe("파일을 Excel로 열지 여부 (기본값: false)"),
},
async ({ filePath, sheetName, openFile }) => {
try {
// 파일 존재 확인
if (!fs.existsSync(filePath)) {
return {
content: [
{ type: "text", text: `파일을 찾을 수 없습니다: ${filePath}` },
],
isError: true,
};
}
// 파일이 열려있는지 확인
let isFileOpen = false;
try {
const fd = fs.openSync(filePath, "r+");
fs.closeSync(fd);
} catch (e) {
isFileOpen = true;
}
// 파일이 열려있다면 PowerShell을 통해 COM 인터페이스로 읽기
if (isFileOpen) {
try {
// PowerShell 스크립트 작성
const psScript = `
try {
# Excel 애플리케이션 객체 생성 또는 가져오기
try {
$excel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
} catch {
$excel = New-Object -ComObject Excel.Application
}
$excel.Visible = $true
# 파일 열기 시도
$normalizedPath = "${filePath.replace(/\\/g, "\\\\")}"
# 이미 열려있는 워크북 찾기
$workbook = $null
foreach ($wb in $excel.Workbooks) {
if ($wb.FullName -eq $normalizedPath) {
$workbook = $wb
break
}
}
# 워크북이 없으면 열기
if ($workbook -eq $null) {
$workbook = $excel.Workbooks.Open($normalizedPath)
}
# 시트 선택
$worksheet = $null
${
sheetName
? `
try {
$worksheet = $workbook.Worksheets("${sheetName}")
} catch {
$worksheet = $workbook.ActiveSheet
}`
: `
$worksheet = $workbook.ActiveSheet`
}
# 사용된 범위 얻기
$usedRange = $worksheet.UsedRange
$rowCount = $usedRange.Rows.Count
$colCount = $usedRange.Columns.Count
# 시트 이름, 행 수, 열 수 기록
$sheetInfo = "SHEET_INFO:" + $worksheet.Name + "," + $rowCount + "," + $colCount
Write-Output $sheetInfo
# 전체 워크시트의 이름 목록
$sheetList = "SHEET_LIST:"
foreach ($sheet in $workbook.Sheets) {
$sheetList += $sheet.Name + ","
}
Write-Output $sheetList
# 데이터가 없으면 여기서 종료
if ($rowCount -eq 0 -or $colCount -eq 0) {
Write-Output "EMPTY_SHEET"
return
}
# 셀 데이터 읽기
Write-Output "DATA_START"
for ($row = 1; $row -le $rowCount; $row++) {
$rowData = ""
for ($col = 1; $col -le $colCount; $col++) {
$cellValue = $usedRange.Cells.Item($row, $col).Value2
if ($cellValue -eq $null) {
$cellValue = ""
}
$rowData += $cellValue.ToString() + "|CELL_DELIM|"
}
Write-Output $rowData
}
Write-Output "DATA_END"
} catch {
Write-Output "ERROR: $($_.Exception.Message)"
} finally {
# COM 객체 참조 해제 (Excel 프로그램은 종료하지 않음)
if ($worksheet -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) | Out-Null
}
if ($workbook -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
}
if ($excel -ne $null) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
}
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
`;
// PowerShell 스크립트 실행
const result = await runPowerShellScript(psScript);
// 결과 파싱
if (result && !result.includes("ERROR:")) {
const lines = result.split("\n");
let data = [];
let activeSheet = "";
let allSheets = [];
let rowCount = 0;
let colCount = 0;
let isDataSection = false;
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith("SHEET_INFO:")) {
const info = trimmedLine.substring(11).split(",");
activeSheet = info[0];
rowCount = parseInt(info[1]) || 0;
colCount = parseInt(info[2]) || 0;
} else if (trimmedLine.startsWith("SHEET_LIST:")) {
allSheets = trimmedLine
.substring(11)
.split(",")
.filter((s) => s.trim());
} else if (trimmedLine === "DATA_START") {
isDataSection = true;
continue;
} else if (trimmedLine === "DATA_END") {
isDataSection = false;
} else if (isDataSection) {
const cells = trimmedLine.split("|CELL_DELIM|");
cells.pop(); // 마지막 구분자 이후 빈 요소 제거
data.push(cells);
} else if (trimmedLine === "EMPTY_SHEET") {
data = [];
}
}
// 요청 시 Excel로 파일 열기 - 지금은 이미 열려있음
if (openFile) {
// 이미 열려있으므로 창만 활성화
const activateScript = `
try {
$excel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
$excel.Visible = $true
$excel.Application.WindowState = -4143 # xlMaximized
$excel.Application.Activate()
} catch {}
`;
await runPowerShellScript(activateScript);
}
// 결과 반환
return {
content: [
{
type: "text",
text: `엑셀 파일 읽기 결과:
- 파일: ${filePath}
- 활성 시트: ${activeSheet}
- 전체 시트 목록: ${allSheets.join(", ")}
- 행 수: ${rowCount}
- 열 수: ${colCount}
- 데이터: ${JSON.stringify(data, null, 2)}`,
},
],
};
} else {
// COM 인터페이스 실패 시 기본 워크북 생성
return {
content: [
{
type: "text",
text: `COM 인터페이스로 엑셀 파일을 읽을 수 없습니다: ${
result ? result.trim() : "알 수 없는 오류"
}`,
},
],
isError: true,
};
}
} catch (error) {
return {
content: [
{
type: "text",
text: `COM 인터페이스 오류: ${error.message}`,
},
],
isError: true,
};
}
} else {
// 파일이 열려있지 않은 경우 ExcelJS 사용
const workbook = new ExcelJS.Workbook();
try {
// 엑셀 파일 읽기
await workbook.xlsx.readFile(filePath);
} catch (readErr) {
return {
content: [
{
type: "text",
text: `엑셀 파일을 읽을 수 없습니다: ${readErr.message}`,
},
],
isError: true,
};
}
// 시트 목록 가져오기
const sheets = workbook.worksheets.map((sheet) => sheet.name);
// 시트 선택 (지정된 시트가 없거나 존재하지 않으면 첫 번째 시트 사용)
const selectedSheet =
sheetName && sheets.includes(sheetName)
? workbook.getWorksheet(sheetName)
: workbook.worksheets[0];
// JSON 데이터로 변환
const jsonData = [];
if (selectedSheet) {
selectedSheet.eachRow({ includeEmpty: true }, (row, rowNumber) => {
const rowData = [];
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
if (colNumber > rowData.length) {
// 중간 빈 셀 채우기
while (rowData.length < colNumber - 1) {
rowData.push("");
}
}
rowData.push(cell.value !== null ? cell.value : "");
});
jsonData.push(rowData);
});
}
// 요청 시 Excel로 파일 열기
if (openFile) {
await openExcelFile(filePath);
}
return {
content: [
{
type: "text",
text: `엑셀 파일 읽기 결과:
- 파일: ${filePath}
- 활성 시트: ${selectedSheet ? selectedSheet.name : "없음"}
- 전체 시트 목록: ${sheets.join(", ")}
- 행 수: ${jsonData.length}
- 열 수: ${
jsonData.length > 0
? Math.max(...jsonData.map((row) => row.length))
: 0
}
- 데이터: ${JSON.stringify(jsonData, null, 2)}`,
},
],
};
}
} catch (error) {
return {
content: [
{ type: "text", text: `엑셀 파일 읽기 오류: ${error.message}` },
],
isError: true,
};
}
}
);
// 시트 추가 도구
server.tool(
"add_sheet",
"엑셀 파일에 새 시트를 추가합니다.",
{
filePath: z.string().describe("엑셀 파일의 경로"),
sheetName: z.string().describe("추가할 시트 이름"),
data: z
.array(z.array(z.any()))
.optional()
.describe("시트에 추가할 2차원 배열 형태의 데이터 (선택사항)"),
openFile: z
.boolean()
.optional()
.default(false)
.describe("저장 후 Excel로 열지 여부 (기본값: false)"),
},
async ({ filePath, sheetName, data = [], openFile }) => {
try {
// 파일 존재 확인
if (!fs.existsSync(filePath)) {
return {
content: [
{ type: "text", text: `파일을 찾을 수 없습니다: ${filePath}` },
],
isError: true,
};
}
// 1. 파일이 열려있는지 확인
const fileIsOpen = isFileOpen(filePath);
// 2. 파일이 열려있다면, PowerShell로 시트 추가
if (fileIsOpen) {
try {
// PowerShell 스크립트 작성
const psScript = `
try {
# Excel 애플리케이션 객체 생성 또는 가져오기
try {
$excel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
} catch {
$excel = New-Object -ComObject Excel.Application
}
$excel.Visible = $true
$excel.DisplayAlerts = $false
# 파일 열기 시도
$normalizedPath = "${filePath.replace(/\\/g, "\\\\")}"
# 이미 열려있는 워크북 찾기
$workbook = $null
foreach ($wb in $excel.Workbooks) {
if ($wb.FullName -eq $normalizedPath) {
$workbook = $wb
break
}
}
# 워크북이 없으면 열기
if ($workbook -eq $null) {
$workbook = $excel.Workbooks.Open($normalizedPath)
}
# 워크북 활성화
$workbook.Activate()
# 시트 이름 중복 검사
$sheetExists = $false
foreach ($sheet in $workbook.Sheets) {
if ($sheet.Name -eq "${sheetName}") {
$sheetExists = $true
break
}
}
if ($sheetExists) {
Write-Output "ERROR: 시트 이름 '${sheetName}'이(가) 이미 존재합니다."
return
}
# 새 시트 추가
$newSheet = $workbook.Worksheets.Add()
$newSheet.Name = "${sheetName}"
# 시트 활성화
$newSheet.Activate()
# 데이터 추가 (있는 경우)
${
data.length > 0
? `
# 데이터 작성
$rowCount = ${data.length}
$colCount = ${Math.max(
...data.map((row) => row.length)
)}
# 데이터를 한 번에 설정할 범위 생성
$targetRange = $newSheet.Range($newSheet.Cells(1, 1), $newSheet.Cells($rowCount, $colCount))
# 2차원 배열 생성
$dataArray = New-Object 'object[,]' $rowCount, $colCount
${data
.map((row, rowIndex) => {
return row
.map((cell, colIndex) => {
// 값 타입에 따른 처리
if (typeof cell === "string") {
// 문자열은 따옴표로 묶고 특수문자 처리
const escapedValue = cell
.replace(/'/g, "''")
.replace(/"/g, '""');
return `$dataArray[${rowIndex}, ${colIndex}] = '${escapedValue}'`;
} else if (
cell === null ||
cell === undefined
) {
// null/undefined는 빈 문자열로
return `$dataArray[${rowIndex}, ${colIndex}] = ''`;
} else if (typeof cell === "number") {
// 숫자는 그대로
return `$dataArray[${rowIndex}, ${colIndex}] = ${cell}`;
} else if (typeof cell === "boolean") {
// 불리언 값 처리
return `$dataArray[${rowIndex}, ${colIndex}] = $${cell}`;
} else {
// 기타 값은 문자열로 변환
return `$dataArray[${rowIndex}, ${colIndex}] = '${String(
cell
).replace(/'/g, "''")}'`;
}
})
.join("\n");
})
.join("\n")}
# 데이터 배열을 범위에 한 번에 설정
$targetRange.Value2 = $dataArray
# 자동 맞춤 적용 (열 너비 자동 조절)
$newSheet.UsedRange.Columns.AutoFit() | Out-Null
`
: ""
}
# 저장
$workbook.Save()
Write-Output "SUCCESS: 새 시트 '${sheetName}'이(가) 성공적으로 추가되었습니다."
} catch {
Write-Output "ERROR: $($_.Exception.Message)"
} finally {
if ($excel -ne $null) {
$excel.DisplayAlerts = $true
}
}
`;
// PowerShell 스크립트 실행
const result = await runPowerShellScript(psScript);
// 결과 확인
if (result && result.includes("SUCCESS")) {
// 요청 시 Excel로 파일 열기
if (openFile) {
await openExcelFile(filePath);
}
return {
content: [{ type: "text", text: result.trim() }],
};
} else {
return {
content: [
{
type: "text",
text: result
? result.trim()
: "시트 추가 중 오류가 발생했습니다.",
},
],
isError: true,
};
}
} catch (error) {
return {
content: [
{ type: "text", text: `시트 추가 오류: ${error.message}` },
],
isError: true,
};
}
}
// 3. 파일이 열려있지 않은 경우 ExcelJS 사용