# 코드베이스 분석 리포트 > **분석 일자**: 2025-12-11 > **분석 도구**: Claude Code > **프로젝트**: TK-FB-Project v2.2.0 ## 📊 프로젝트 개요 ### 시스템 구조 Technical Korea 작업 관리 시스템 - 3-tier 아키텍처 ``` TK-FB-Project/ ├── web-ui/ # 프론트엔드 (Nginx + Vanilla JS) │ ├── js/ # 49개 파일, 554개 함수 │ ├── css/ # 22개 파일 │ └── pages/ # 51개 HTML 페이지 ├── api.hyungi.net/ # 백엔드 (Node.js + Express) │ ├── controllers/ # 17개 컨트롤러 │ ├── models/ # 14개 모델 │ ├── routes/ # 22개 라우트 │ ├── services/ # 6개 서비스 (불완전) │ └── index.js # 889줄 메인 파일 ├── fastapi-bridge/ # Python FastAPI └── synology_deployment/ # ⚠️ 중복 디렉토리 (268MB) ``` ### 주요 기능 1. 일일 작업 보고서 관리 2. 작업자 및 프로젝트 관리 3. 근태 관리 시스템 4. 작업 분석 및 대시보드 5. 이슈 관리 6. 사용자 인증 및 권한 --- ## 🔴 우선순위 1: 심각 (즉시 해결 필요) ### 1.1 코드 중복 - synology_deployment **문제점**: - 전체 프로젝트가 `/synology_deployment`에 중복 복사 (268MB) - 두 버전이 서로 다른 내용 포함 (CORS 설정 차이) - 이중 관리 문제 **영향도**: 높음 (유지보수성, 저장소 크기) **해결 방안**: ```bash # 제거 대상 synology_deployment/ synology_deployment_v3.tar.gz synology_deployment.tar.gz # 대안: Docker 빌드 스크립트 deployment/ ├── Dockerfile ├── docker-compose.yml └── deploy.sh ``` ### 1.2 하드코딩된 보안 정보 **발견된 위치**: 1. **docker-compose.yml** (15-17줄): ```yaml MYSQL_ROOT_PASSWORD=tkfb2024! DB_PASSWORD=hyungi2024! JWT_SECRET=tkfb_jwt_secret_2024_hyungi_secure_key ``` 2. **README.md** (공개 문서): ```markdown - 비밀번호: hyungi_password_2025 - Root 비밀번호: hyungi_root_password_2025 ``` 3. **web-ui/js/api-config.js** (17줄): ```javascript const baseUrl = `${protocol}//${hostname}:20005/api`; // 포트 하드코딩 ``` **보안 위험**: 높음 (비밀번호 노출) **해결 방안**: ```bash # .env 파일 사용 DB_PASSWORD=${DB_PASSWORD} JWT_SECRET=${JWT_SECRET} # Docker secrets secrets: db_root_password: file: ./secrets/db_root_password.txt ``` ### 1.3 백업/임시 파일 정리 **발견된 파일들**: ``` web-ui/js/daily-report-viewer 복사본.js api.hyungi.net/index.js.backup api.hyungi.net/controllers/dailyWorkReportController 이전.js hyungi.sql.backup docker-compose.yml.backup docker-compose.yml.new json(백업)/ 개발 log/ ``` **문제점**: 13개 백업 파일이 Git 저장소에 포함됨 **해결 방안**: ```gitignore # .gitignore 추가 *.backup *복사본* *이전* *.old *.sql.backup *.new *백업* ``` --- ## 🟠 우선순위 2: 높음 (단기 개선) ### 2.1 거대한 파일들 | 파일 | 줄 수 | 문제점 | |------|------|--------| | work-report-calendar.js | 1,720 | 달력 + API + UI 로직 혼재 | | modern-dashboard.js | 1,299 | 대시보드 모든 기능 포함 | | daily-work-report.js | 1,137 | 폼 + 검증 + API 통합 | | work-report-review.js | 1,060 | 리뷰 전체 로직 | | index.js (백엔드) | 889 | 라우트 + 컨트롤러 + 설정 | | dailyWorkReportController.js | 851 | 모든 CRUD 로직 | **개선 방안**: #### 백엔드 (index.js → 여러 파일로 분리) ``` api.hyungi.net/ ├── index.js # 100줄 이하로 축소 ├── config/ │ ├── middleware.js # 미들웨어 설정 │ ├── cors.js # CORS 설정 │ ├── database.js # DB 연결 │ └── routes.js # 라우트 등록 ├── middlewares/ │ ├── auth.js # 인증 미들웨어 │ ├── permission.js # 권한 체크 │ └── errorHandler.js # 에러 핸들링 └── controllers/ └── userController.js # 인라인 코드 이동 ``` #### 프론트엔드 (모듈화) ``` web-ui/js/ ├── modules/ │ ├── calendar/ │ │ ├── CalendarView.js # UI 렌더링 │ │ ├── CalendarAPI.js # API 호출 │ │ ├── CalendarUtils.js # 유틸리티 │ │ └── CalendarState.js # 상태 관리 │ ├── dashboard/ │ │ ├── DashboardView.js │ │ ├── DashboardCharts.js │ │ └── DashboardData.js │ └── common/ │ ├── api-client.js │ ├── utils.js │ └── validator.js ``` ### 2.2 일관성 없는 에러 처리 **통계**: - `console.log/error/warn`: 684개 (프론트엔드) - `try-catch` 블록: 534개 (백엔드) - 에러 처리 패턴: 5가지 이상 **현재 패턴**: ```javascript // 패턴 1: 단순 로그 catch (error) { console.error('오류:', error); } // 패턴 2: 커스텀 응답 catch (err) { return res.status(500).json({ error: '오류가 발생했습니다.', details: err.message }); } // 패턴 3: errorMiddleware throw new ApiError('작업보고서 생성 실패', 400); // 패턴 4: 에러 무시 catch (error) { } // 패턴 5: 부분적 처리 catch (error) { console.error(error); alert('오류 발생'); } ``` **표준화 방안**: ```javascript // utils/errors.js class AppError extends Error { constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') { super(message); this.statusCode = statusCode; this.code = code; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } class ValidationError extends AppError { constructor(message, details = {}) { super(message, 400, 'VALIDATION_ERROR'); this.details = details; } } class AuthenticationError extends AppError { constructor(message = '인증이 필요합니다') { super(message, 401, 'AUTHENTICATION_ERROR'); } } class ForbiddenError extends AppError { constructor(message = '권한이 없습니다') { super(message, 403, 'FORBIDDEN'); } } // 사용 예시 if (!user) { throw new AuthenticationError(); } if (!['admin', 'system'].includes(user.access_level)) { throw new ForbiddenError('관리자 권한이 필요합니다'); } ``` ```javascript // middlewares/errorHandler.js const errorHandler = (err, req, res, next) => { // 로깅 logger.error({ message: err.message, stack: err.stack, code: err.code, path: req.path, method: req.method, user: req.user?.id }); // 운영 에러만 클라이언트에 전송 if (err.isOperational) { return res.status(err.statusCode).json({ success: false, error: { message: err.message, code: err.code, details: err.details } }); } // 프로그래밍 에러는 일반 메시지만 return res.status(500).json({ success: false, error: { message: '서버 오류가 발생했습니다', code: 'INTERNAL_ERROR' } }); }; ``` ```javascript // 프론트엔드 로거 class Logger { static error(message, context = {}) { const logData = { timestamp: new Date().toISOString(), message, ...context, userAgent: navigator.userAgent, url: window.location.href }; if (window.ENV === 'development') { console.error('[ERROR]', logData); } else { // 프로덕션: 외부 로깅 서비스로 전송 this.sendToLoggingService(logData); } } static warn(message, context = {}) { if (window.ENV === 'development') { console.warn('[WARN]', message, context); } } static info(message, context = {}) { if (window.ENV === 'development') { console.log('[INFO]', message, context); } } static sendToLoggingService(data) { // Sentry, LogRocket 등 통합 // fetch('/api/logs', { method: 'POST', body: JSON.stringify(data) }); } } // 사용 try { await saveReport(data); } catch (error) { Logger.error('작업 보고서 저장 실패', { reportId: data.id, error: error.message }); showErrorToast('저장에 실패했습니다. 다시 시도해주세요.'); } ``` ### 2.3 SELECT * 쿼리 사용 **발견된 위치**: 20개 이상 파일 **예시**: ```sql -- models/workTypeModel.js SELECT * FROM work_types ORDER BY name ASC -- models/toolModel.js SELECT * FROM Tools -- controllers/dailyWorkReportController.js SELECT * FROM daily_work_reports WHERE id = ? ``` **문제점**: - 불필요한 데이터 전송 - 성능 저하 - 스키마 변경 시 예측 불가능 **개선 방안**: ```sql -- 명시적 컬럼 지정 SELECT id, name, description, category, is_active, created_at, updated_at FROM work_types WHERE is_active = 1 ORDER BY name ASC; -- 조인 시에도 명시적 지정 SELECT r.id, r.report_date, r.work_content, w.name AS worker_name, p.name AS project_name FROM daily_work_reports r JOIN workers w ON r.worker_id = w.id JOIN projects p ON r.project_id = p.id WHERE r.report_date = ?; ``` --- ## 🟡 우선순위 3: 중간 (중기 개선) ### 3.1 불완전한 서비스 레이어 **현재 구조**: ``` Controllers (17개) → Services (6개만) → Models (14개) ↘ Models (직접 호출) ``` **문제점**: - 대부분의 비즈니스 로직이 컨트롤러에 직접 구현 - 코드 재사용 어려움 - 테스트 작성 어려움 **서비스 레이어가 있는 모듈**: 1. authService.js 2. emailService.js 3. openaiService.js 4. reportSubmissionService.js 5. tokenService.js 6. workAnalysisService.js **서비스 레이어가 없는 모듈**: - dailyWorkReport (851줄 컨트롤러) - worker 관리 - project 관리 - attendance 관리 - issue 관리 - 등 11개 도메인 **개선 방안**: ```javascript // services/dailyWorkReportService.js class DailyWorkReportService { constructor(reportModel, workerModel, projectModel) { this.reportModel = reportModel; this.workerModel = workerModel; this.projectModel = projectModel; } async createReport(reportData, userId) { // 1. 검증 await this.validateReport(reportData); // 2. 권한 확인 await this.checkPermission(userId, reportData.workerId); // 3. 중복 체크 const exists = await this.reportModel.findByDateAndWorker( reportData.date, reportData.workerId ); if (exists) { throw new ValidationError('해당 날짜에 이미 보고서가 존재합니다'); } // 4. 생성 const report = await this.reportModel.create(reportData); // 5. 후처리 (알림, 로그 등) await this.notifyReportCreation(report); return report; } async validateReport(data) { if (!data.workContent || data.workContent.length < 10) { throw new ValidationError('작업 내용은 10자 이상이어야 합니다'); } if (!data.workerId) { throw new ValidationError('작업자를 선택해주세요'); } // 작업자 존재 확인 const worker = await this.workerModel.findById(data.workerId); if (!worker) { throw new ValidationError('존재하지 않는 작업자입니다'); } if (!worker.is_active) { throw new ValidationError('비활성화된 작업자입니다'); } } async checkPermission(userId, workerId) { const user = await this.userModel.findById(userId); // 관리자는 모든 보고서 작성 가능 if (['admin', 'system'].includes(user.access_level)) { return true; } // 일반 사용자는 자신의 보고서만 if (user.worker_id !== workerId) { throw new ForbiddenError('다른 작업자의 보고서를 작성할 수 없습니다'); } } async notifyReportCreation(report) { // 그룹 리더에게 알림 // 이메일 발송 // 로그 기록 } } // controllers/dailyWorkReportController.js const createReport = async (req, res, next) => { try { const report = await dailyWorkReportService.createReport( req.body, req.user.id ); res.status(201).json({ success: true, data: report }); } catch (error) { next(error); } }; ``` ### 3.2 인증/권한 로직 중복 **발견된 패턴**: ```javascript // 패턴 1: 컨트롤러 내부에서 체크 (가장 많음) if (!req.user || !['admin', 'system'].includes(req.user.access_level)) { return res.status(403).json({ success: false, error: '관리자 권한이 필요합니다.' }); } // 패턴 2: 미들웨어 (index.js 276-314줄) const checkAdmin = (req, res, next) => { if (req.user && ['admin', 'system'].includes(req.user.access_level)) { next(); } else { res.status(403).json({ error: 'Forbidden' }); } }; // 패턴 3: 직접 user 확인 if (!user) { return res.status(401).json({ error: 'Unauthorized' }); } ``` **개선 방안**: ```javascript // middlewares/permission.js const requireAuth = (req, res, next) => { if (!req.user) { throw new AuthenticationError(); } next(); }; const requireRole = (...allowedRoles) => { return (req, res, next) => { if (!req.user) { throw new AuthenticationError(); } if (!allowedRoles.includes(req.user.access_level)) { throw new ForbiddenError( `이 작업은 ${allowedRoles.join(', ')} 권한이 필요합니다` ); } next(); }; }; const requireOwnerOrAdmin = (getOwnerId) => { return async (req, res, next) => { if (!req.user) { throw new AuthenticationError(); } // 관리자는 모두 접근 가능 if (['admin', 'system'].includes(req.user.access_level)) { return next(); } // 소유자 확인 const ownerId = await getOwnerId(req); if (req.user.id !== ownerId) { throw new ForbiddenError('본인의 데이터만 수정할 수 있습니다'); } next(); }; }; // 사용 예시 router.get('/users', requireAuth, requireRole('admin', 'system'), getUsers ); router.put('/reports/:id', requireAuth, requireOwnerOrAdmin(async (req) => { const report = await ReportModel.findById(req.params.id); return report.user_id; }), updateReport ); router.post('/projects', requireAuth, requireRole('admin', 'project_manager'), createProject ); ``` ### 3.3 CORS 설정 중복 **위치**: index.js 136-181줄 (주석 처리됨) **문제점**: ```javascript // 136-181줄: 주석 처리된 CORS 설정 /* app.use(cors({ origin: function (origin, callback) { // ... 복잡한 로직 } })); */ // 183-228줄: 실제 사용 중인 CORS 설정 app.use(cors({ origin: function (origin, callback) { // ... 동일한 로직 } })); ``` **개선 방안**: ```javascript // config/cors.js const allowedOrigins = [ 'http://localhost:3000', 'http://localhost:8080', 'http://192.168.0.6:8080', 'http://hyungi.net', 'https://hyungi.net' ]; const corsOptions = { origin: (origin, callback) => { // 개발 환경에서는 모든 origin 허용 if (process.env.NODE_ENV === 'development' && !origin) { return callback(null, true); } // 허용된 origin 확인 if (allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('CORS policy violation')); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] }; module.exports = corsOptions; // index.js const corsOptions = require('./config/cors'); app.use(cors(corsOptions)); ``` --- ## 🟢 우선순위 4: 낮음 (장기 개선) ### 4.1 프론트엔드 모듈화 부족 **현재 상태**: - Vanilla JavaScript - 전역 변수 남용 - 파일 간 의존성 불명확 - 코드 재사용 어려움 **개선 방안**: ```javascript // 현재 (전역 변수) var currentDate = new Date(); var selectedReport = null; function loadReport() { // ... } // 개선 (ES6 모듈) // modules/calendar/CalendarState.js export class CalendarState { constructor() { this.currentDate = new Date(); this.selectedReport = null; this.events = []; } setCurrentDate(date) { this.currentDate = date; this.notifyListeners(); } selectReport(report) { this.selectedReport = report; this.notifyListeners(); } } // modules/calendar/CalendarView.js import { CalendarState } from './CalendarState.js'; import { CalendarAPI } from './CalendarAPI.js'; export class CalendarView { constructor(containerId) { this.container = document.getElementById(containerId); this.state = new CalendarState(); this.api = new CalendarAPI(); } async render() { const events = await this.api.fetchEvents(this.state.currentDate); this.renderCalendar(events); } renderCalendar(events) { // ... } } // main.js import { CalendarView } from './modules/calendar/CalendarView.js'; document.addEventListener('DOMContentLoaded', () => { const calendar = new CalendarView('calendar-container'); calendar.render(); }); ``` ### 4.2 CSS 구조 개선 **현재**: 22개의 개별 CSS 파일 **개선안**: ``` css/ ├── base/ │ ├── reset.css # CSS 리셋 │ ├── variables.css # CSS 변수 │ ├── typography.css # 폰트 스타일 │ └── utilities.css # 유틸리티 클래스 ├── components/ │ ├── button.css # 버튼 스타일 │ ├── form.css # 폼 요소 │ ├── modal.css # 모달 │ ├── table.css # 테이블 │ ├── card.css # 카드 │ └── badge.css # 뱃지 ├── layouts/ │ ├── header.css # 헤더 │ ├── sidebar.css # 사이드바 │ ├── footer.css # 푸터 │ └── grid.css # 그리드 시스템 └── pages/ ├── dashboard.css # 대시보드 ├── calendar.css # 캘린더 └── report.css # 보고서 # main.css (통합) @import 'base/reset.css'; @import 'base/variables.css'; @import 'base/typography.css'; /* ... */ ``` ```css /* base/variables.css - CSS 변수 사용 */ :root { /* Colors */ --color-primary: #007bff; --color-success: #28a745; --color-danger: #dc3545; --color-warning: #ffc107; /* Spacing */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --spacing-xl: 2rem; /* Typography */ --font-family-base: 'Noto Sans KR', sans-serif; --font-size-base: 14px; --line-height-base: 1.5; } /* components/button.css */ .btn { padding: var(--spacing-sm) var(--spacing-md); font-family: var(--font-family-base); border-radius: 4px; transition: all 0.3s ease; } .btn-primary { background-color: var(--color-primary); color: white; } ``` --- ## 🔒 보안 취약점 ### 1. 비밀번호 평문 노출 (심각) **위치**: - docker-compose.yml - README.md - .env 파일 (Git에 포함 가능성) **해결**: ```yaml # docker-compose.yml services: db: secrets: - db_root_password - db_password environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password MYSQL_PASSWORD_FILE: /run/secrets/db_password secrets: db_root_password: file: ./secrets/db_root_password.txt db_password: file: ./secrets/db_password.txt ``` ```gitignore # .gitignore .env .env.local .env.*.local secrets/ *.pem *.key ``` ### 2. SQL Injection 위험 (중간) **취약한 코드**: ```javascript // ❌ 위험 const query = `SELECT * FROM users WHERE username = '${username}'`; db.query(query); // ✅ 안전 const query = 'SELECT * FROM users WHERE username = ?'; db.query(query, [username]); ``` **검토 필요**: - 모든 동적 쿼리 검토 - Prepared Statement 사용 확인 - ORM 도입 검토 ### 3. XSS (Cross-Site Scripting) (중간) **취약한 코드**: ```javascript // ❌ 위험 element.innerHTML = `
${userInput}
`; // ✅ 안전 element.textContent = userInput; // 또는 const div = document.createElement('div'); div.textContent = userInput; element.appendChild(div); ``` **검토 필요**: - innerHTML 사용처 전수 조사 - 사용자 입력 sanitization - Content Security Policy 적용 ### 4. 인증 토큰 보안 (중간) **개선 사항**: ```javascript // JWT 설정 강화 const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h', // 만료 시간 설정 issuer: 'tkfb-api', audience: 'tkfb-client' } ); // Refresh Token 도입 const refreshToken = jwt.sign( { userId: user.id, type: 'refresh' }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' } ); // HttpOnly 쿠키 사용 res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 }); ``` --- ## ⚡ 성능 최적화 제안 ### 1. 데이터베이스 쿼리 **N+1 쿼리 문제**: ```javascript // ❌ 비효율 (N+1 쿼리) const reports = await ReportModel.findAll(); for (const report of reports) { report.worker = await WorkerModel.findById(report.worker_id); report.project = await ProjectModel.findById(report.project_id); } // ✅ 효율적 (JOIN 사용) const reports = await db.query(` SELECT r.*, w.name AS worker_name, p.name AS project_name FROM daily_work_reports r LEFT JOIN workers w ON r.worker_id = w.id LEFT JOIN projects p ON r.project_id = p.id WHERE r.report_date BETWEEN ? AND ? `, [startDate, endDate]); ``` **인덱스 추가**: ```sql -- 자주 조회되는 컬럼에 인덱스 CREATE INDEX idx_report_date ON daily_work_reports(report_date); CREATE INDEX idx_worker_id ON daily_work_reports(worker_id); CREATE INDEX idx_project_id ON daily_work_reports(project_id); -- 복합 인덱스 CREATE INDEX idx_report_worker_date ON daily_work_reports(worker_id, report_date); -- 쿼리 성능 분석 EXPLAIN SELECT * FROM daily_work_reports WHERE worker_id = 1 AND report_date BETWEEN '2025-01-01' AND '2025-12-31'; ``` ### 2. 프론트엔드 최적화 **번들링**: ```javascript // webpack.config.js module.exports = { entry: './web-ui/js/main.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'web-ui/dist') }, optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 }, common: { minChunks: 2, priority: 5, reuseExistingChunk: true } } } } }; ``` **이미지 최적화**: ```html ... ... ``` ### 3. 캐싱 전략 **서버 사이드**: ```javascript // Redis 캐싱 const getWorkers = async () => { const cacheKey = 'workers:active'; // 캐시 확인 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // DB 조회 const workers = await WorkerModel.findActive(); // 캐시 저장 (1시간) await redis.setex(cacheKey, 3600, JSON.stringify(workers)); return workers; }; ``` **클라이언트 사이드**: ```javascript // Service Worker로 정적 리소스 캐싱 self.addEventListener('install', (event) => { event.waitUntil( caches.open('tkfb-v1').then((cache) => { return cache.addAll([ '/', '/css/main.css', '/js/bundle.js', '/images/logo.png' ]); }) ); }); ``` --- ## 📈 코드 메트릭스 ### 복잡도 분석 | 지표 | 현재 | 목표 | 상태 | |------|------|------|------| | 평균 파일 크기 | 487줄 | <300줄 | 🔴 | | 평균 함수 크기 | 42줄 | <20줄 | 🟡 | | 최대 중첩 깊이 | 7단계 | <4단계 | 🔴 | | 코드 중복률 | ~35% | <10% | 🔴 | | 테스트 커버리지 | 0% | >80% | 🔴 | | JSDoc 커버리지 | ~5% | >60% | 🔴 | ### 의존성 분석 **백엔드**: ```json { "dependencies": { "express": "^4.18.2", "mysql2": "^3.6.0", "jsonwebtoken": "^9.0.2", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.3.1" } } ``` **프론트엔드**: - jQuery (버전 확인 필요) - 외부 CDN 의존성 (FullCalendar, Chart.js 등) --- ## 🎯 리팩토링 목표 ### 단기 (1-2개월) - [ ] synology_deployment 제거 - [ ] 보안 정보 환경변수화 - [ ] index.js 분리 (<200줄) - [ ] 에러 처리 표준화 - [ ] SELECT * 제거 ### 중기 (3-6개월) - [ ] 서비스 레이어 완성 - [ ] 큰 파일 모듈화 (<500줄) - [ ] 테스트 코드 작성 (>50% 커버리지) - [ ] API 문서화 - [ ] 성능 최적화 (DB 인덱스, 캐싱) ### 장기 (6-12개월) - [ ] TypeScript 마이그레이션 - [ ] 프론트엔드 프레임워크 도입 - [ ] ORM 도입 (TypeORM/Prisma) - [ ] CI/CD 파이프라인 구축 - [ ] 모니터링 시스템 구축 --- ## 📚 참고 자료 ### 코딩 표준 - [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) - [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices) ### 아키텍처 패턴 - Clean Architecture - Domain-Driven Design (DDD) - SOLID Principles ### 보안 - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [Node.js Security Checklist](https://blog.risingstack.com/node-js-security-checklist/) --- **다음 단계**: [리팩토링 계획](PLAN.md) 참고