UNPKG

task-master-monitoring

Version:

Project task dashboard monitoring tool with visualization and management features

967 lines (820 loc) 32.7 kB
const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); const os = require('os'); const app = express(); const PORT = process.env.PORT || 3001; // 사용자 데이터 디렉토리 설정 const USER_DATA_DIR = path.join(os.homedir(), '.task-master-monitoring'); const USER_PROJECTS_DIR = path.join(USER_DATA_DIR, 'projects'); // 사용자 데이터 디렉토리 생성 function ensureUserDataDir() { if (!fs.existsSync(USER_DATA_DIR)) { fs.mkdirSync(USER_DATA_DIR, { recursive: true }); console.log(`Created user data directory: ${USER_DATA_DIR}`); } if (!fs.existsSync(USER_PROJECTS_DIR)) { fs.mkdirSync(USER_PROJECTS_DIR, { recursive: true }); console.log(`Created user projects directory: ${USER_PROJECTS_DIR}`); } } // 기존 데이터 마이그레이션 (한 번만 실행) function migrateExistingData() { const oldProjectsPath = path.join(__dirname, 'public', 'projects'); const migrationFlagPath = path.join(USER_DATA_DIR, '.migrated'); // 이미 마이그레이션했거나, 기존 데이터가 없으면 건너뛰기 if (fs.existsSync(migrationFlagPath) || !fs.existsSync(oldProjectsPath)) { return; } try { console.log('Migrating existing project data to user directory...'); // 기존 projects 폴더의 모든 내용을 사용자 디렉토리로 복사 const copyRecursive = (src, dest) => { if (fs.statSync(src).isDirectory()) { if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } fs.readdirSync(src).forEach(item => { copyRecursive(path.join(src, item), path.join(dest, item)); }); } else { fs.copyFileSync(src, dest); } }; fs.readdirSync(oldProjectsPath).forEach(item => { const srcPath = path.join(oldProjectsPath, item); const destPath = path.join(USER_PROJECTS_DIR, item); if (fs.statSync(srcPath).isDirectory()) { copyRecursive(srcPath, destPath); console.log(`Migrated project: ${item}`); } }); // 마이그레이션 완료 플래그 생성 fs.writeFileSync(migrationFlagPath, new Date().toISOString()); console.log('Migration completed successfully!'); } catch (error) { console.warn('Migration failed, but continuing...', error.message); } } // 초기화 ensureUserDataDir(); migrateExistingData(); // 미들웨어 app.use(cors()); app.use(express.json()); // 정적 파일 서빙 (React 빌드 파일) - 프로덕션 모드에서만 if (process.env.NODE_ENV === 'production' && fs.existsSync(path.join(__dirname, 'build'))) { app.use(express.static(path.join(__dirname, 'build'))); } // 메모 저장 API app.post('/api/save-memo', (req, res) => { try { const { project, memos } = req.body; if (!project || !memos) { return res.status(400).json({ error: 'Project and memos are required' }); } // 프로젝트 폴더 경로 const projectPath = path.join(USER_PROJECTS_DIR, project); const memoFilePath = path.join(projectPath, 'task-memo.json'); // 폴더가 없으면 생성 if (!fs.existsSync(projectPath)) { fs.mkdirSync(projectPath, { recursive: true }); } // 메모 파일 저장 fs.writeFileSync(memoFilePath, JSON.stringify(memos, null, 2), 'utf8'); console.log(`Memo saved to: ${memoFilePath}`); res.json({ success: true, message: `Memo saved to ~/.task-master-monitoring/projects/${project}/task-memo.json`, path: `~/.task-master-monitoring/projects/${project}/task-memo.json` }); } catch (error) { console.error('Error saving memo:', error); res.status(500).json({ error: 'Failed to save memo', details: error.message }); } }); // 메모 로드 API app.get('/api/load-memo/:project', (req, res) => { try { const { project } = req.params; const memoFilePath = path.join(USER_PROJECTS_DIR, project, 'task-memo.json'); if (fs.existsSync(memoFilePath)) { const memoData = fs.readFileSync(memoFilePath, 'utf8'); const memos = JSON.parse(memoData); console.log(`Memo loaded from: ${memoFilePath}`); res.json({ success: true, memos, path: `~/.task-master-monitoring/projects/${project}/task-memo.json` }); } else { res.json({ success: false, message: 'No memo file found', memos: {} }); } } catch (error) { console.error('Error loading memo:', error); res.status(500).json({ error: 'Failed to load memo', details: error.message }); } }); // 대시보드 메모 저장 API app.post('/api/save-dashboard-memo', (req, res) => { try { const { project, memo } = req.body; if (!project || memo === undefined) { return res.status(400).json({ error: 'Project and memo are required' }); } // 프로젝트 폴더 경로 const projectPath = path.join(USER_PROJECTS_DIR, project); const dashboardMemoFilePath = path.join(projectPath, 'dashboard-memo.json'); // 폴더가 없으면 생성 if (!fs.existsSync(projectPath)) { fs.mkdirSync(projectPath, { recursive: true }); } // 대시보드 메모 파일 저장 fs.writeFileSync(dashboardMemoFilePath, JSON.stringify({ memo }, null, 2), 'utf8'); console.log(`Dashboard memo saved to: ${dashboardMemoFilePath}`); res.json({ success: true, message: `Dashboard memo saved to ~/.task-master-monitoring/projects/${project}/dashboard-memo.json`, path: `~/.task-master-monitoring/projects/${project}/dashboard-memo.json` }); } catch (error) { console.error('Error saving dashboard memo:', error); res.status(500).json({ error: 'Failed to save dashboard memo', details: error.message }); } }); // 대시보드 메모 로드 API app.get('/api/load-dashboard-memo/:project', (req, res) => { try { const { project } = req.params; const dashboardMemoFilePath = path.join(USER_PROJECTS_DIR, project, 'dashboard-memo.json'); if (fs.existsSync(dashboardMemoFilePath)) { const memoData = fs.readFileSync(dashboardMemoFilePath, 'utf8'); const { memo } = JSON.parse(memoData); console.log(`Dashboard memo loaded from: ${dashboardMemoFilePath}`); res.json({ success: true, memo, path: `~/.task-master-monitoring/projects/${project}/dashboard-memo.json` }); } else { res.json({ success: false, message: 'No dashboard memo file found', memo: '' }); } } catch (error) { console.error('Error loading dashboard memo:', error); res.status(500).json({ error: 'Failed to load dashboard memo', details: error.message }); } }); // 프로젝트 디렉토리 생성 API app.post('/api/create-project-dir', (req, res) => { try { const { project } = req.body; if (!project) { return res.status(400).json({ error: 'Project name is required' }); } const projectPath = path.join(__dirname, 'public', 'projects', project); if (!fs.existsSync(projectPath)) { fs.mkdirSync(projectPath, { recursive: true }); console.log(`Project directory created: ${projectPath}`); } res.json({ success: true, message: `Project directory ready: ~/.task-master-monitoring/projects/${project}/`, path: `~/.task-master-monitoring/projects/${project}/` }); } catch (error) { console.error('Error creating project directory:', error); res.status(500).json({ error: 'Failed to create project directory', details: error.message }); } }); // 프로젝트 목록 스캔 API app.get('/api/scan-projects', (req, res) => { try { const projectsPath = USER_PROJECTS_DIR; // projects 폴더가 없으면 빈 배열 반환 if (!fs.existsSync(projectsPath)) { return res.json({ success: true, projects: [] }); } const projects = []; const folders = fs.readdirSync(projectsPath, { withFileTypes: true }); folders.forEach((folder, index) => { if (folder.isDirectory()) { const folderName = folder.name; const tasksJsonPath = path.join(projectsPath, folderName, 'tasks.json'); // tasks.json 파일이 있는 폴더만 유효한 프로젝트로 인식 if (fs.existsSync(tasksJsonPath)) { try { // tasks.json에서 프로젝트 정보 읽기 const tasksData = JSON.parse(fs.readFileSync(tasksJsonPath, 'utf8')); // 프로젝트 이름은 folder 이름을 기본으로 하되, master.projectName이나 projectName이 있으면 사용 let projectName = folderName; let description = `${folderName} 프로젝트`; if (tasksData.master?.projectName) { projectName = tasksData.master.projectName; } else if (tasksData.projectName) { projectName = tasksData.projectName; } if (tasksData.master?.description) { description = tasksData.master.description; } else if (tasksData.description) { description = tasksData.description; } // 완료율 계산 const tasks = tasksData.master?.tasks || tasksData.tasks || []; const completedTasks = tasks.filter(task => task.status === 'done' || task.status === 'completed' ).length; const completionRate = tasks.length > 0 ? Math.round((completedTasks / tasks.length) * 100) : 0; projects.push({ id: index + 1, name: projectName, folderName: folderName, path: `~/.task-master-monitoring/projects/${folderName}/tasks.json`, description: description, taskCount: tasks.length, completedTasks: completedTasks, completionRate: completionRate }); } catch (parseError) { console.warn(`Failed to parse tasks.json for project ${folderName}:`, parseError.message); // JSON 파싱에 실패해도 기본 정보로 프로젝트 추가 projects.push({ id: index + 1, name: folderName, folderName: folderName, path: `~/.task-master-monitoring/projects/${folderName}/tasks.json`, description: `${folderName} 프로젝트 (파싱 오류)`, taskCount: 0, completedTasks: 0, completionRate: 0 }); } } } }); console.log(`Found ${projects.length} projects in ${projectsPath}`); res.json({ success: true, projects: projects }); } catch (error) { console.error('Error scanning projects:', error); res.status(500).json({ error: 'Failed to scan projects', details: error.message }); } }); // 외부 경로에서 데이터 로드 API app.post('/api/load-external-path', (req, res) => { try { const { externalPath } = req.body; if (!externalPath) { return res.status(400).json({ error: 'External path is required' }); } // 절대 경로인지 확인 const absolutePath = path.isAbsolute(externalPath) ? externalPath : path.resolve(externalPath); // 파일 존재 여부 확인 if (!fs.existsSync(absolutePath)) { return res.status(404).json({ error: 'File not found', path: absolutePath }); } // 파일 읽기 const fileContent = fs.readFileSync(absolutePath, 'utf8'); // JSON 파싱 시도 let data; try { data = JSON.parse(fileContent); } catch (parseError) { return res.status(400).json({ error: 'Invalid JSON format', details: parseError.message, path: absolutePath }); } console.log(`External data loaded from: ${absolutePath}`); res.json({ success: true, data: data, path: absolutePath }); } catch (error) { console.error('Error loading external data:', error); res.status(500).json({ error: 'Failed to load external data', details: error.message }); } }); // 외부 경로 링크 파일 스캔 API (txt 파일에서 경로 읽기) app.get('/api/scan-external-links', (req, res) => { try { const projectsPath = USER_PROJECTS_DIR; if (!fs.existsSync(projectsPath)) { return res.json({ success: true, externalProjects: [] }); } const externalProjects = []; const folders = fs.readdirSync(projectsPath, { withFileTypes: true }); folders.forEach((folder, index) => { if (folder.isDirectory()) { const folderName = folder.name; const linkFilePath = path.join(projectsPath, folderName, 'path.txt'); // path.txt 파일이 있는 폴더는 외부 링크 프로젝트로 인식 if (fs.existsSync(linkFilePath)) { try { const externalPath = fs.readFileSync(linkFilePath, 'utf8').trim(); if (externalPath && fs.existsSync(externalPath)) { // 외부 경로의 데이터를 미리 읽어서 프로젝트 정보 추출 try { const tasksData = JSON.parse(fs.readFileSync(externalPath, 'utf8')); let projectName = folderName; let description = `${folderName} 프로젝트 (외부 링크)`; if (tasksData.master?.projectName) { projectName = tasksData.master.projectName; } else if (tasksData.projectName) { projectName = tasksData.projectName; } if (tasksData.master?.description) { description = tasksData.master.description; } else if (tasksData.description) { description = tasksData.description; } // 완료율 계산 const tasks = tasksData.master?.tasks || tasksData.tasks || []; const completedTasks = tasks.filter(task => task.status === 'done' || task.status === 'completed' ).length; const completionRate = tasks.length > 0 ? Math.round((completedTasks / tasks.length) * 100) : 0; externalProjects.push({ id: `ext_${index + 1}`, name: projectName, folderName: folderName, externalPath: externalPath, description: `${description} (링크: ${externalPath})`, taskCount: tasks.length, completedTasks: completedTasks, completionRate: completionRate, isExternal: true }); } catch (parseError) { // JSON 파싱 실패 시에도 기본 정보로 추가 externalProjects.push({ id: `ext_${index + 1}`, name: folderName, folderName: folderName, externalPath: externalPath, description: `${folderName} 프로젝트 (외부 링크, 파싱 오류)`, taskCount: 0, completedTasks: 0, completionRate: 0, isExternal: true }); } } } catch (readError) { console.warn(`Failed to read path.txt for ${folderName}:`, readError.message); } } } }); console.log(`Found ${externalProjects.length} external link projects`); res.json({ success: true, externalProjects: externalProjects }); } catch (error) { console.error('Error scanning external links:', error); res.status(500).json({ error: 'Failed to scan external links', details: error.message }); } }); // 프로젝트 생성 API (폴더 생성 + path.txt 파일 생성) app.post('/api/create-project', (req, res) => { try { const { name, folderName, externalPath } = req.body; if (!name || !folderName || !externalPath) { return res.status(400).json({ error: 'Name, folderName, and externalPath are required' }); } // 프로젝트 폴더 경로 const projectPath = path.join(USER_PROJECTS_DIR, folderName); const pathFilePath = path.join(projectPath, 'path.txt'); // 폴더가 이미 존재하는지 확인 if (fs.existsSync(projectPath)) { return res.status(409).json({ error: 'Project folder already exists', folderName: folderName }); } // 외부 경로가 유효한지 확인 if (!fs.existsSync(externalPath)) { return res.status(400).json({ error: 'External path does not exist', externalPath: externalPath }); } // tasks.json인지 확인 if (!externalPath.endsWith('.json') && !externalPath.endsWith('tasks.json')) { return res.status(400).json({ error: 'External path must point to a JSON file (preferably tasks.json)', externalPath: externalPath }); } // JSON 파일이 올바른 형식인지 확인 try { const testData = JSON.parse(fs.readFileSync(externalPath, 'utf8')); if (!testData.tasks && !testData.master?.tasks) { return res.status(400).json({ error: 'JSON file must contain "tasks" array or "master.tasks" array', externalPath: externalPath }); } } catch (parseError) { return res.status(400).json({ error: 'External path does not contain valid JSON', externalPath: externalPath, details: parseError.message }); } // 프로젝트 폴더 생성 fs.mkdirSync(projectPath, { recursive: true }); // path.txt 파일 생성 (외부 경로 저장) fs.writeFileSync(pathFilePath, externalPath, 'utf8'); console.log(`Project "${name}" created successfully:`); console.log(` Folder: ${projectPath}`); console.log(` External path: ${externalPath}`); console.log(` Path file: ${pathFilePath}`); res.json({ success: true, message: `Project "${name}" created successfully`, projectPath: `~/.task-master-monitoring/projects/${folderName}/`, pathFile: `~/.task-master-monitoring/projects/${folderName}/path.txt`, externalPath: externalPath }); } catch (error) { console.error('Error creating project:', error); res.status(500).json({ error: 'Failed to create project', details: error.message }); } }); // 프로젝트 삭제 API app.delete('/api/delete-project', (req, res) => { try { const { projectId, folderName } = req.body; if (!folderName) { return res.status(400).json({ error: 'FolderName is required' }); } // 프로젝트 폴더 경로 const projectPath = path.join(USER_PROJECTS_DIR, folderName); // 폴더가 존재하는지 확인 if (!fs.existsSync(projectPath)) { return res.status(404).json({ error: 'Project folder not found', folderName: folderName, path: projectPath }); } // 재귀적으로 폴더와 모든 내용 삭제 const deleteRecursive = (dirPath) => { if (fs.existsSync(dirPath)) { fs.readdirSync(dirPath).forEach((file) => { const currentPath = path.join(dirPath, file); if (fs.lstatSync(currentPath).isDirectory()) { deleteRecursive(currentPath); } else { fs.unlinkSync(currentPath); } }); fs.rmdirSync(dirPath); } }; deleteRecursive(projectPath); console.log(`Project "${folderName}" deleted successfully from: ${projectPath}`); res.json({ success: true, message: `Project "${folderName}" deleted successfully`, deletedPath: `~/.task-master-monitoring/projects/${folderName}/` }); } catch (error) { console.error('Error deleting project:', error); res.status(500).json({ error: 'Failed to delete project', details: error.message }); } }); // 뽀모도로 세션 저장 API app.post('/api/save-pomodoro', (req, res) => { try { const { project, session } = req.body; if (!project || !session) { return res.status(400).json({ error: 'Project and session are required' }); } // 프로젝트 폴더 경로 const projectPath = path.join(USER_PROJECTS_DIR, project); const pomodoroFilePath = path.join(projectPath, 'pomodoro-history.json'); // 폴더가 없으면 생성 if (!fs.existsSync(projectPath)) { fs.mkdirSync(projectPath, { recursive: true }); } // 기존 기록 읽기 또는 초기 데이터 생성 let pomodoroData = { sessions: [], dailyStats: {} }; if (fs.existsSync(pomodoroFilePath)) { try { const existingData = fs.readFileSync(pomodoroFilePath, 'utf8'); pomodoroData = JSON.parse(existingData); } catch (parseError) { console.warn('Failed to parse existing pomodoro data, creating new file'); } } // 새 세션 추가 pomodoroData.sessions.push(session); // 일일 통계 업데이트 const sessionDate = session.startTime.split('T')[0]; // YYYY-MM-DD 형식 if (!pomodoroData.dailyStats[sessionDate]) { pomodoroData.dailyStats[sessionDate] = { workSessions: 0, breakSessions: 0, totalFocusTime: 0, completionRate: 0 }; } const dailyStat = pomodoroData.dailyStats[sessionDate]; if (session.type === 'work') { dailyStat.workSessions++; dailyStat.totalFocusTime += session.duration; } else { dailyStat.breakSessions++; } // 완료율 계산 (완료된 세션 / 전체 세션) const todaySessions = pomodoroData.sessions.filter(s => s.startTime.split('T')[0] === sessionDate); const completedSessions = todaySessions.filter(s => s.completed); dailyStat.completionRate = todaySessions.length > 0 ? Math.round((completedSessions.length / todaySessions.length) * 100) : 100; // 파일 저장 fs.writeFileSync(pomodoroFilePath, JSON.stringify(pomodoroData, null, 2), 'utf8'); console.log(`Pomodoro session saved to: ${pomodoroFilePath}`); res.json({ success: true, message: `Pomodoro session saved to ~/.task-master-monitoring/projects/${project}/pomodoro-history.json`, path: `~/.task-master-monitoring/projects/${project}/pomodoro-history.json`, sessionId: session.id }); } catch (error) { console.error('Error saving pomodoro session:', error); res.status(500).json({ error: 'Failed to save pomodoro session', details: error.message }); } }); // 뽀모도로 기록 조회 API app.get('/api/load-pomodoro-history/:project', (req, res) => { try { const { project } = req.params; const { date, limit } = req.query; // 선택적 쿼리 파라미터 const pomodoroFilePath = path.join(USER_PROJECTS_DIR, project, 'pomodoro-history.json'); if (fs.existsSync(pomodoroFilePath)) { const pomodoroData = JSON.parse(fs.readFileSync(pomodoroFilePath, 'utf8')); let filteredSessions = pomodoroData.sessions; // 날짜 필터링 if (date) { filteredSessions = filteredSessions.filter(session => session.startTime.split('T')[0] === date ); } // 개수 제한 if (limit) { const limitNum = parseInt(limit); filteredSessions = filteredSessions.slice(-limitNum); // 최근 N개 } // 통계 계산 const today = new Date().toISOString().split('T')[0]; const todayStats = pomodoroData.dailyStats[today] || { workSessions: 0, breakSessions: 0, totalFocusTime: 0, completionRate: 0 }; console.log(`Pomodoro history loaded from: ${pomodoroFilePath}`); res.json({ success: true, sessions: filteredSessions, dailyStats: pomodoroData.dailyStats, todayStats: todayStats, totalSessions: pomodoroData.sessions.length, path: `~/.task-master-monitoring/projects/${project}/pomodoro-history.json` }); } else { res.json({ success: false, message: 'No pomodoro history file found', sessions: [], dailyStats: {}, todayStats: { workSessions: 0, breakSessions: 0, totalFocusTime: 0, completionRate: 0 }, totalSessions: 0 }); } } catch (error) { console.error('Error loading pomodoro history:', error); res.status(500).json({ error: 'Failed to load pomodoro history', details: error.message }); } }); // 뽀모도로 세션 삭제 API app.delete('/api/delete-pomodoro-session', (req, res) => { try { const { project, sessionId } = req.body; if (!project || !sessionId) { return res.status(400).json({ error: 'Project and sessionId are required' }); } const pomodoroFilePath = path.join(USER_PROJECTS_DIR, project, 'pomodoro-history.json'); if (!fs.existsSync(pomodoroFilePath)) { return res.status(404).json({ error: 'Pomodoro history file not found' }); } // 기존 데이터 읽기 const pomodoroData = JSON.parse(fs.readFileSync(pomodoroFilePath, 'utf8')); // 해당 세션 찾기 및 삭제 const sessionToDelete = pomodoroData.sessions.find(s => s.id === sessionId); if (!sessionToDelete) { return res.status(404).json({ error: 'Session not found' }); } // 세션 삭제 pomodoroData.sessions = pomodoroData.sessions.filter(s => s.id !== sessionId); // 일일 통계 재계산 const sessionDate = sessionToDelete.startTime.split('T')[0]; const dailySessions = pomodoroData.sessions.filter(s => s.startTime.split('T')[0] === sessionDate); if (dailySessions.length === 0) { // 해당 날짜의 세션이 모두 삭제된 경우 통계 삭제 delete pomodoroData.dailyStats[sessionDate]; } else { // 통계 재계산 const workSessions = dailySessions.filter(s => s.type === 'work').length; const breakSessions = dailySessions.filter(s => s.type === 'break').length; const totalFocusTime = dailySessions .filter(s => s.type === 'work') .reduce((total, s) => total + s.duration, 0); const completedSessions = dailySessions.filter(s => s.completed); const completionRate = dailySessions.length > 0 ? Math.round((completedSessions.length / dailySessions.length) * 100) : 0; pomodoroData.dailyStats[sessionDate] = { workSessions, breakSessions, totalFocusTime, completionRate }; } // 파일 저장 fs.writeFileSync(pomodoroFilePath, JSON.stringify(pomodoroData, null, 2), 'utf8'); console.log(`Pomodoro session ${sessionId} deleted from: ${pomodoroFilePath}`); res.json({ success: true, message: `Session deleted successfully`, deletedSessionId: sessionId }); } catch (error) { console.error('Error deleting pomodoro session:', error); res.status(500).json({ error: 'Failed to delete pomodoro session', details: error.message }); } }); // 뽀모도로 세션 다중 삭제 API app.delete('/api/delete-pomodoro-sessions', (req, res) => { try { const { project, sessionIds } = req.body; if (!project || !sessionIds || !Array.isArray(sessionIds) || sessionIds.length === 0) { return res.status(400).json({ error: 'Project and sessionIds array are required' }); } const pomodoroFilePath = path.join(USER_PROJECTS_DIR, project, 'pomodoro-history.json'); if (!fs.existsSync(pomodoroFilePath)) { return res.status(404).json({ error: 'Pomodoro history file not found' }); } // 기존 데이터 읽기 const pomodoroData = JSON.parse(fs.readFileSync(pomodoroFilePath, 'utf8')); // 삭제할 세션들 찾기 const sessionsToDelete = pomodoroData.sessions.filter(s => sessionIds.includes(s.id)); if (sessionsToDelete.length === 0) { return res.status(404).json({ error: 'No matching sessions found' }); } // 세션들 삭제 pomodoroData.sessions = pomodoroData.sessions.filter(s => !sessionIds.includes(s.id)); // 영향받은 날짜들 수집 const affectedDates = [...new Set(sessionsToDelete.map(s => s.startTime.split('T')[0]))]; // 각 날짜별로 통계 재계산 affectedDates.forEach(sessionDate => { const dailySessions = pomodoroData.sessions.filter(s => s.startTime.split('T')[0] === sessionDate); if (dailySessions.length === 0) { // 해당 날짜의 세션이 모두 삭제된 경우 통계 삭제 delete pomodoroData.dailyStats[sessionDate]; } else { // 통계 재계산 const workSessions = dailySessions.filter(s => s.type === 'work').length; const breakSessions = dailySessions.filter(s => s.type === 'break').length; const totalFocusTime = dailySessions .filter(s => s.type === 'work') .reduce((total, s) => total + s.duration, 0); const completedSessions = dailySessions.filter(s => s.completed); const completionRate = dailySessions.length > 0 ? Math.round((completedSessions.length / dailySessions.length) * 100) : 0; pomodoroData.dailyStats[sessionDate] = { workSessions, breakSessions, totalFocusTime, completionRate }; } }); // 파일 저장 fs.writeFileSync(pomodoroFilePath, JSON.stringify(pomodoroData, null, 2), 'utf8'); console.log(`${sessionsToDelete.length} Pomodoro sessions deleted from: ${pomodoroFilePath}`); res.json({ success: true, message: `${sessionsToDelete.length} sessions deleted successfully`, deletedSessionIds: sessionIds, affectedDates: affectedDates }); } catch (error) { console.error('Error deleting pomodoro sessions:', error); res.status(500).json({ error: 'Failed to delete pomodoro sessions', details: error.message }); } }); // React 앱 서빙 (모든 다른 라우트) - 프로덕션 모드에서만 app.get('*', (_, res) => { const buildIndexPath = path.join(__dirname, 'build', 'index.html'); if (process.env.NODE_ENV === 'production' && fs.existsSync(buildIndexPath)) { res.sendFile(buildIndexPath); } else { res.status(404).send('API server running in development mode. Use React dev server on port 3000.'); } }); app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log(`API endpoints:`); console.log(` POST /api/save-memo - Save memo to file`); console.log(` GET /api/load-memo/:project - Load memo from file`); console.log(` POST /api/save-dashboard-memo - Save dashboard memo to file`); console.log(` GET /api/load-dashboard-memo/:project - Load dashboard memo from file`); console.log(` POST /api/create-project-dir - Create project directory`); console.log(` POST /api/create-project - Create new project with external path link`); console.log(` DELETE /api/delete-project - Delete project and its folder`); console.log(` GET /api/scan-projects - Scan projects folder for available projects`); console.log(` POST /api/load-external-path - Load data from external path`); console.log(` GET /api/scan-external-links - Scan for external link projects (path.txt files)`); console.log(` POST /api/save-pomodoro - Save pomodoro session to file`); console.log(` GET /api/load-pomodoro-history/:project - Load pomodoro history from file`); console.log(` DELETE /api/delete-pomodoro-session - Delete a specific pomodoro session`); console.log(` DELETE /api/delete-pomodoro-sessions - Delete multiple pomodoro sessions`); });