## 🚨 보안 강화 - 하드코딩된 비밀번호를 환경변수로 전환 - .env.example 생성 및 보안 가이드 추가 - docker-compose.yml 환경변수 적용 - README.md에서 실제 비밀번호 제거 ## 🗑️ 중복 제거 - synology_deployment/ 디렉토리 제거 (268MB) - synology_deployment*.tar.gz 아카이브 제거 (234MB) - 총 502MB의 중복 파일 삭제 ## 🧹 백업 파일 정리 - *.backup 파일 제거 (10개) - *복사본* 파일 제거 - *이전* 파일 제거 - json(백업)/ 디렉토리 제거 ## 📋 .gitignore 업데이트 - 백업 파일 패턴 추가 - 보안 파일 제외 (.env, *.pem, *.key) - 임시 파일 제외 (*.tmp, *.new) - 빌드 아티팩트 제외 (*.tar.gz) ## 📚 문서화 - docs/ 디렉토리 구조 생성 - 리팩토링 분석 및 계획 문서 작성 - 코딩 스타일 가이드 작성 - 개발 환경 설정 가이드 작성 - 시스템 아키텍처 문서 작성 ## 변경된 파일 - .env.example (신규) - .gitignore (업데이트) - docker-compose.yml (환경변수 적용) - README.md (보안 정보 제거) - docs/* (신규 문서 7개) ## 보안 개선 효과 ✅ 비밀번호 노출 위험 제거 ✅ Git 히스토리에서 민감 정보 분리 ✅ 환경별 설정 분리 가능 ✅ 배포 보안 강화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1151 lines
25 KiB
Markdown
1151 lines
25 KiB
Markdown
# 코드베이스 분석 리포트
|
|
|
|
> **분석 일자**: 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 = `<div>${userInput}</div>`;
|
|
|
|
// ✅ 안전
|
|
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
|
|
<!-- Lazy loading -->
|
|
<img src="image.jpg" loading="lazy" alt="...">
|
|
|
|
<!-- Responsive images -->
|
|
<img
|
|
srcset="image-320w.jpg 320w,
|
|
image-640w.jpg 640w,
|
|
image-1280w.jpg 1280w"
|
|
sizes="(max-width: 320px) 280px,
|
|
(max-width: 640px) 600px,
|
|
1200px"
|
|
src="image-640w.jpg"
|
|
alt="..."
|
|
>
|
|
```
|
|
|
|
### 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) 참고
|