diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b66a6cb --- /dev/null +++ b/.env.example @@ -0,0 +1,115 @@ +# TK-FB-Project 환경 변수 설정 예시 +# 이 파일을 복사하여 .env 파일을 생성하고 실제 값으로 변경하세요 +# +# 사용법: +# 1. cp .env.example .env +# 2. .env 파일을 편집하여 실제 비밀번호로 변경 +# 3. .env 파일은 절대 Git에 커밋하지 마세요! + +# ============================================================================= +# 데이터베이스 설정 +# ============================================================================= + +# MariaDB/MySQL Root 비밀번호 (최소 12자 이상, 영문/숫자/특수문자 조합) +MYSQL_ROOT_PASSWORD=change_this_root_password_min_12_chars + +# 데이터베이스 이름 +MYSQL_DATABASE=hyungi + +# 데이터베이스 사용자명 +MYSQL_USER=hyungi_user + +# 데이터베이스 사용자 비밀번호 (최소 12자 이상, 영문/숫자/특수문자 조합) +MYSQL_PASSWORD=change_this_user_password_min_12_chars + +# ============================================================================= +# API 서버 설정 +# ============================================================================= + +# Node.js 환경 (development | production) +NODE_ENV=production + +# API 서버 포트 (컨테이너 내부) +PORT=3005 + +# 데이터베이스 연결 정보 +DB_HOST=db +DB_PORT=3306 +DB_USER=hyungi_user +DB_PASSWORD=change_this_user_password_min_12_chars +DB_NAME=hyungi + +# ============================================================================= +# JWT 인증 설정 +# ============================================================================= + +# JWT Secret Key (최소 32자 이상의 랜덤 문자열) +# 생성 방법: openssl rand -base64 32 +# 또는: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +JWT_SECRET=change_this_to_random_string_min_32_chars + +# JWT 액세스 토큰 만료 시간 +JWT_EXPIRES_IN=7d + +# JWT 리프레시 토큰 만료 시간 +JWT_REFRESH_SECRET=change_this_to_another_random_string_min_32_chars +JWT_REFRESH_EXPIRES_IN=30d + +# ============================================================================= +# FastAPI 설정 +# ============================================================================= + +# API 서버 URL (컨테이너 간 통신) +API_BASE_URL=http://api:3005 + +# ============================================================================= +# phpMyAdmin 설정 +# ============================================================================= + +# phpMyAdmin에서 사용할 데이터베이스 호스트 +PMA_HOST=db + +# phpMyAdmin 사용자 (일반적으로 root) +PMA_USER=root + +# phpMyAdmin 비밀번호 (MYSQL_ROOT_PASSWORD와 동일하게) +PMA_PASSWORD=change_this_root_password_min_12_chars + +# 파일 업로드 제한 +UPLOAD_LIMIT=50M + +# ============================================================================= +# 외부 서비스 (선택사항) +# ============================================================================= + +# OpenAI API (AI 기능 사용 시) +# OPENAI_API_KEY=sk-your-openai-api-key-here + +# 이메일 발송 (SMTP 설정) +# EMAIL_HOST=smtp.gmail.com +# EMAIL_PORT=587 +# EMAIL_USER=your-email@gmail.com +# EMAIL_PASSWORD=your-app-password +# EMAIL_FROM=noreply@tkfb.com + +# ============================================================================= +# 보안 참고사항 +# ============================================================================= +# +# 강력한 비밀번호 생성 방법: +# +# 1. 터미널에서 랜덤 비밀번호 생성: +# openssl rand -base64 24 +# +# 2. Node.js로 랜덤 문자열 생성: +# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +# +# 3. 온라인 도구 사용: +# https://www.lastpass.com/features/password-generator +# +# 주의사항: +# - .env 파일은 절대 Git에 커밋하지 마세요 +# - 프로덕션 환경에서는 더 강력한 비밀번호를 사용하세요 +# - 정기적으로 비밀번호를 변경하세요 +# - JWT_SECRET은 유출되면 모든 토큰이 무효화됩니다 +# diff --git a/.gitignore b/.gitignore index 861f7c2..f8cfaf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Dependencies **/node_modules/ +**/venv/ +**/__pycache__/ # Databases *.db @@ -13,6 +15,7 @@ # Environment variables .env .env.* +!.env.example # OS generated files .DS_Store @@ -20,4 +23,44 @@ Thumbs.db # IDEs .idea/ -.vscode/ \ No newline at end of file +.vscode/ + +# Backup files +*.backup +*.bak +*.old +*복사본* +*이전* +*백업* +*.swp +*.swo +*~ + +# Build artifacts +dist/ +build/ +*.tar.gz +*.zip + +# Deployment files +synology_deployment/ +deployment_*.tar.gz +TK-FB-Project_*.tar.gz + +# Temporary files +*.tmp +*.temp +*.new + +# Security files +secrets/ +*.pem +*.key +*.crt +*.p12 +*.pfx + +# Coverage & Test +coverage/ +.nyc_output/ +*.lcov \ No newline at end of file diff --git a/README.md b/README.md index 9b61e59..65f7f18 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,25 @@ # TK-FB-Project - 통합 실행 가이드 +## ⚙️ 사전 준비 + +### 환경 변수 설정 (필수) +처음 실행하기 전에 환경 변수 파일을 생성해야 합니다: + +```bash +# 1. .env.example을 복사하여 .env 파일 생성 +cp .env.example .env + +# 2. .env 파일을 편집하여 실제 비밀번호로 변경 +nano .env # 또는 vi, code 등 사용 + +# 3. 강력한 비밀번호로 변경 (예시) +# MYSQL_ROOT_PASSWORD=your_secure_password_here +# MYSQL_PASSWORD=your_secure_password_here +# JWT_SECRET=your_random_jwt_secret_min_32_chars +``` + +**중요**: `.env` 파일은 절대 Git에 커밋하지 마세요! + ## 🚀 한 번에 모든 서비스 실행 ### 🎯 간편 실행 (권장) @@ -50,10 +70,12 @@ docker-compose logs -f ## 💾 데이터베이스 정보 - **호스트**: localhost:20306 -- **데이터베이스**: hyungi -- **사용자**: hyungi -- **비밀번호**: hyungi_password_2025 -- **Root 비밀번호**: hyungi_root_password_2025 +- **데이터베이스**: hyungi (`.env` 파일의 `MYSQL_DATABASE`) +- **사용자**: hyungi_user (`.env` 파일의 `MYSQL_USER`) +- **비밀번호**: `.env` 파일에서 설정한 `MYSQL_PASSWORD` +- **Root 비밀번호**: `.env` 파일에서 설정한 `MYSQL_ROOT_PASSWORD` + +**참고**: 실제 비밀번호는 `.env` 파일을 확인하세요. ## ✨ 주요 개선사항 diff --git a/TK-FB-Project_Synology_Volume2_20251105_120332.tar.gz b/TK-FB-Project_Synology_Volume2_20251105_120332.tar.gz deleted file mode 100644 index 852998a..0000000 Binary files a/TK-FB-Project_Synology_Volume2_20251105_120332.tar.gz and /dev/null differ diff --git a/api.hyungi.net/controllers/dailyWorkReportController 이전.js b/api.hyungi.net/controllers/dailyWorkReportController 이전.js deleted file mode 100644 index 9722f6d..0000000 --- a/api.hyungi.net/controllers/dailyWorkReportController 이전.js +++ /dev/null @@ -1,750 +0,0 @@ -// controllers/dailyWorkReportController.js - 누적입력 방식 + 모든 기존 기능 포함 -const dailyWorkReportModel = require('../models/dailyWorkReportModel'); - -/** - * 📝 작업보고서 생성 (누적 방식 - 덮어쓰기 없음!) - */ -const createDailyWorkReport = (req, res) => { - const { report_date, worker_id, work_entries } = req.body; - const created_by = req.user?.user_id || req.user?.id; - const created_by_name = req.user?.name || req.user?.username || '알 수 없는 사용자'; - - // 1. 기본 유효성 검사 - if (!report_date || !worker_id || !work_entries) { - return res.status(400).json({ - error: '필수 필드가 누락되었습니다.', - required: ['report_date', 'worker_id', 'work_entries'], - received: { - report_date: !!report_date, - worker_id: !!worker_id, - work_entries: !!work_entries - } - }); - } - - if (!Array.isArray(work_entries) || work_entries.length === 0) { - return res.status(400).json({ - error: '최소 하나의 작업 항목이 필요합니다.', - received_entries: work_entries?.length || 0 - }); - } - - if (!created_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - // 2. 작업 항목 유효성 검사 - for (let i = 0; i < work_entries.length; i++) { - const entry = work_entries[i]; - const requiredFields = ['project_id', 'work_type_id', 'work_status_id', 'work_hours']; - - for (const field of requiredFields) { - if (entry[field] === undefined || entry[field] === null || entry[field] === '') { - return res.status(400).json({ - error: `작업 항목 ${i + 1}의 ${field}가 누락되었습니다.`, - entry_index: i, - missing_field: field - }); - } - } - - // 에러 상태인 경우 에러 타입 필수 - if (entry.work_status_id === 2 && (!entry.error_type_id)) { - return res.status(400).json({ - error: `작업 항목 ${i + 1}이 에러 상태인 경우 error_type_id가 필요합니다.`, - entry_index: i - }); - } - - // 시간 유효성 검사 - const hours = parseFloat(entry.work_hours); - if (isNaN(hours) || hours < 0 || hours > 24) { - return res.status(400).json({ - error: `작업 항목 ${i + 1}의 작업시간이 유효하지 않습니다. (0-24시간)`, - entry_index: i, - received_hours: entry.work_hours - }); - } - } - - // 3. 총 시간 계산 - const total_hours = work_entries.reduce((sum, entry) => sum + (parseFloat(entry.work_hours) || 0), 0); - - // 4. 요청 데이터 구성 - const reportData = { - report_date, - worker_id: parseInt(worker_id), - work_entries, - created_by, - created_by_name, - total_hours, - is_update: false - }; - - console.log('📝 작업보고서 누적 추가 요청:', { - date: report_date, - worker: worker_id, - creator: created_by_name, - creator_id: created_by, - entries: work_entries.length, - total_hours - }); - - // 5. 누적 추가 실행 (덮어쓰기 없음!) - dailyWorkReportModel.createDailyReport(reportData, (err, result) => { - if (err) { - console.error('작업보고서 생성 오류:', err); - return res.status(500).json({ - error: '작업보고서 생성 중 오류가 발생했습니다.', - details: err.message, - timestamp: new Date().toISOString() - }); - } - - console.log('✅ 작업보고서 누적 추가 성공:', result); - res.status(201).json({ - message: '작업보고서가 성공적으로 누적 추가되었습니다.', - report_date, - worker_id, - created_by: created_by_name, - timestamp: new Date().toISOString(), - ...result - }); - }); -}; - -/** - * 📊 누적 현황 조회 (새로운 기능) - */ -const getAccumulatedReports = (req, res) => { - const { date, worker_id } = req.query; - - if (!date || !worker_id) { - return res.status(400).json({ - error: 'date와 worker_id가 필요합니다.', - example: 'date=2024-06-16&worker_id=1' - }); - } - - console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`); - - dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => { - if (err) { - console.error('누적 현황 조회 오류:', err); - return res.status(500).json({ - error: '누적 현황 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - console.log(`📊 누적 현황 조회 결과: ${data.length}개`); - res.json({ - date, - worker_id, - total_entries: data.length, - accumulated_data: data, - timestamp: new Date().toISOString() - }); - }); -}; - -/** - * 📊 기여자별 요약 조회 (새로운 기능) - */ -const getContributorsSummary = (req, res) => { - const { date, worker_id } = req.query; - - if (!date || !worker_id) { - return res.status(400).json({ - error: 'date와 worker_id가 필요합니다.', - example: 'date=2024-06-16&worker_id=1' - }); - } - - console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`); - - dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => { - if (err) { - console.error('기여자별 요약 조회 오류:', err); - return res.status(500).json({ - error: '기여자별 요약 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0); - - console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`); - res.json({ - date, - worker_id, - contributors: data, - total_contributors: data.length, - grand_total_hours: totalHours, - timestamp: new Date().toISOString() - }); - }); -}; - -/** - * 📊 개인 누적 현황 조회 (새로운 기능) - */ -const getMyAccumulatedData = (req, res) => { - const { date, worker_id } = req.query; - const created_by = req.user?.user_id || req.user?.id; - - if (!date || !worker_id) { - return res.status(400).json({ - error: 'date와 worker_id가 필요합니다.', - example: 'date=2024-06-16&worker_id=1' - }); - } - - if (!created_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`); - - dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => { - if (err) { - console.error('개인 누적 현황 조회 오류:', err); - return res.status(500).json({ - error: '개인 누적 현황 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`); - res.json({ - date, - worker_id, - created_by, - my_data: data, - timestamp: new Date().toISOString() - }); - }); -}; - -/** - * 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능) - */ -const removeMyEntry = (req, res) => { - const { id } = req.params; - const deleted_by = req.user?.user_id || req.user?.id; - - if (!deleted_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`); - - dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => { - if (err) { - console.error('개별 항목 삭제 오류:', err); - return res.status(500).json({ - error: '항목 삭제 중 오류가 발생했습니다.', - details: err.message - }); - } - - console.log(`✅ 개별 항목 삭제 완료: id=${id}`); - res.json({ - message: '항목이 성공적으로 삭제되었습니다.', - id: id, - deleted_by, - timestamp: new Date().toISOString(), - ...result - }); - }); -}; - -/** - * 📊 작업보고서 조회 (쿼리 파라미터 기반 - 작성자별 필터링 강화) - */ -const getDailyWorkReports = (req, res) => { - const { date, worker_id, created_by: requested_created_by } = req.query; - const current_user_id = req.user?.user_id || req.user?.id; - - if (!current_user_id) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - // 일반 사용자는 자신이 작성한 것만 볼 수 있음 - const created_by = requested_created_by || current_user_id; - - console.log('📊 작업보고서 조회 요청:', { - date, - worker_id, - requested_created_by, - current_user_id, - final_created_by: created_by - }); - - if (date && created_by) { - // 날짜 + 작성자별 조회 - dailyWorkReportModel.getByDateAndCreator(date, created_by, (err, data) => { - if (err) { - console.error('작업보고서 조회 오류:', err); - return res.status(500).json({ - error: '작업보고서 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - console.log(`📊 날짜+작성자별 조회 결과: ${data.length}개`); - res.json(data); - }); - } else if (date && worker_id) { - // 기존 방식: 날짜 + 작업자별 (하지만 작성자 필터링 추가) - dailyWorkReportModel.getByDateAndWorker(date, worker_id, (err, data) => { - if (err) { - console.error('작업보고서 조회 오류:', err); - return res.status(500).json({ - error: '작업보고서 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - // 본인이 작성한 것만 필터링 - const filteredData = data.filter(report => report.created_by === current_user_id); - console.log(`📊 날짜+작업자별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`); - res.json(filteredData); - }); - } else if (date) { - // 날짜별 조회 (작성자 필터링) - dailyWorkReportModel.getByDate(date, (err, data) => { - if (err) { - console.error('작업보고서 조회 오류:', err); - return res.status(500).json({ - error: '작업보고서 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - // 본인이 작성한 것만 필터링 - const filteredData = data.filter(report => report.created_by === current_user_id); - console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`); - res.json(filteredData); - }); - } else { - res.status(400).json({ - error: '날짜(date) 파라미터가 필요합니다.', - example: 'date=2024-06-16', - optional: ['worker_id', 'created_by'] - }); - } -}; - -/** - * 📊 날짜별 작업보고서 조회 (경로 파라미터) - */ -const getDailyWorkReportsByDate = (req, res) => { - const { date } = req.params; - const created_by = req.user?.user_id || req.user?.id; - - if (!created_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - console.log(`📊 날짜별 조회 (경로): date=${date}, created_by=${created_by}`); - - dailyWorkReportModel.getByDate(date, (err, data) => { - if (err) { - console.error('날짜별 작업보고서 조회 오류:', err); - return res.status(500).json({ - error: '작업보고서 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - // 본인이 작성한 것만 필터링 - const filteredData = data.filter(report => report.created_by === created_by); - console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`); - res.json(filteredData); - }); -}; - -/** - * 🔍 작업보고서 검색 (페이지네이션 포함) - */ -const searchWorkReports = (req, res) => { - const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query; - const created_by = req.user?.user_id || req.user?.id; - - if (!start_date || !end_date) { - return res.status(400).json({ - error: 'start_date와 end_date가 필요합니다.', - example: 'start_date=2024-01-01&end_date=2024-01-31', - optional: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit'] - }); - } - - if (!created_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - const searchParams = { - start_date, - end_date, - worker_id: worker_id ? parseInt(worker_id) : null, - project_id: project_id ? parseInt(project_id) : null, - work_status_id: work_status_id ? parseInt(work_status_id) : null, - created_by, // 작성자 필터링 추가 - page: parseInt(page), - limit: parseInt(limit) - }; - - console.log('🔍 작업보고서 검색 요청:', searchParams); - - dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => { - if (err) { - console.error('작업보고서 검색 오류:', err); - return res.status(500).json({ - error: '작업보고서 검색 중 오류가 발생했습니다.', - details: err.message - }); - } - - console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`); - res.json(data); - }); -}; - -/** - * 📈 통계 조회 (작성자별 필터링) - */ -const getWorkReportStats = (req, res) => { - const { start_date, end_date } = req.query; - const created_by = req.user?.user_id || req.user?.id; - - if (!start_date || !end_date) { - return res.status(400).json({ - error: 'start_date와 end_date가 필요합니다.', - example: 'start_date=2024-01-01&end_date=2024-01-31' - }); - } - - if (!created_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - console.log(`📈 통계 조회: ${start_date} ~ ${end_date}, 요청자: ${created_by}`); - - dailyWorkReportModel.getStatistics(start_date, end_date, (err, data) => { - if (err) { - console.error('통계 조회 오류:', err); - return res.status(500).json({ - error: '통계 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - res.json({ - ...data, - metadata: { - note: '현재는 전체 통계입니다. 개인별 통계는 추후 구현 예정', - requested_by: created_by, - period: `${start_date} ~ ${end_date}`, - timestamp: new Date().toISOString() - } - }); - }); -}; - -/** - * 📊 일일 근무 요약 조회 - */ -const getDailySummary = (req, res) => { - const { date, worker_id } = req.query; - - if (date) { - console.log(`📊 일일 요약 조회: date=${date}`); - dailyWorkReportModel.getSummaryByDate(date, (err, data) => { - if (err) { - console.error('일일 요약 조회 오류:', err); - return res.status(500).json({ - error: '일일 요약 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - res.json(data); - }); - } else if (worker_id) { - console.log(`📊 작업자별 요약 조회: worker_id=${worker_id}`); - dailyWorkReportModel.getSummaryByWorker(worker_id, (err, data) => { - if (err) { - console.error('작업자별 요약 조회 오류:', err); - return res.status(500).json({ - error: '작업자별 요약 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - res.json(data); - }); - } else { - res.status(400).json({ - error: 'date 또는 worker_id 파라미터가 필요합니다.', - examples: [ - 'date=2024-06-16', - 'worker_id=1' - ] - }); - } -}; - -/** - * 📅 월간 요약 조회 - */ -const getMonthlySummary = (req, res) => { - const { year, month } = req.query; - - if (!year || !month) { - return res.status(400).json({ - error: 'year와 month가 필요합니다.', - example: 'year=2024&month=01', - note: 'month는 01, 02, ..., 12 형식으로 입력하세요.' - }); - } - - console.log(`📅 월간 요약 조회: ${year}-${month}`); - - dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => { - if (err) { - console.error('월간 요약 조회 오류:', err); - return res.status(500).json({ - error: '월간 요약 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - res.json({ - year: parseInt(year), - month: parseInt(month), - summary: data, - total_entries: data.length, - timestamp: new Date().toISOString() - }); - }); -}; - -/** - * ✏️ 작업보고서 수정 - */ -const updateWorkReport = (req, res) => { - const { id } = req.params; - const updateData = req.body; - const updated_by = req.user?.user_id || req.user?.id; - - if (!updated_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - updateData.updated_by = updated_by; - - console.log(`✏️ 작업보고서 수정 요청: id=${id}, 수정자=${updated_by}`); - - dailyWorkReportModel.updateById(id, updateData, (err, affectedRows) => { - if (err) { - console.error('작업보고서 수정 오류:', err); - return res.status(500).json({ - error: '작업보고서 수정 중 오류가 발생했습니다.', - details: err.message - }); - } - - if (affectedRows === 0) { - return res.status(404).json({ - error: '수정할 작업보고서를 찾을 수 없습니다.', - id: id - }); - } - - console.log(`✅ 작업보고서 수정 완료: id=${id}`); - res.json({ - message: '작업보고서가 성공적으로 수정되었습니다.', - id: id, - affected_rows: affectedRows, - updated_by, - timestamp: new Date().toISOString() - }); - }); -}; - -/** - * 🗑️ 특정 작업보고서 삭제 - */ -const removeDailyWorkReport = (req, res) => { - const { id } = req.params; - const deleted_by = req.user?.user_id || req.user?.id; - - if (!deleted_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - console.log(`🗑️ 작업보고서 삭제 요청: id=${id}, 삭제자=${deleted_by}`); - - dailyWorkReportModel.removeById(id, deleted_by, (err, affectedRows) => { - if (err) { - console.error('작업보고서 삭제 오류:', err); - return res.status(500).json({ - error: '작업보고서 삭제 중 오류가 발생했습니다.', - details: err.message - }); - } - - if (affectedRows === 0) { - return res.status(404).json({ - error: '삭제할 작업보고서를 찾을 수 없습니다.', - id: id - }); - } - - console.log(`✅ 작업보고서 삭제 완료: id=${id}`); - res.json({ - message: '작업보고서가 성공적으로 삭제되었습니다.', - id: id, - affected_rows: affectedRows, - deleted_by, - timestamp: new Date().toISOString() - }); - }); -}; - -/** - * 🗑️ 작업자의 특정 날짜 전체 삭제 - */ -const removeDailyWorkReportByDateAndWorker = (req, res) => { - const { date, worker_id } = req.params; - const deleted_by = req.user?.user_id || req.user?.id; - - if (!deleted_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } - - console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`); - - dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => { - if (err) { - console.error('작업보고서 전체 삭제 오류:', err); - return res.status(500).json({ - error: '작업보고서 삭제 중 오류가 발생했습니다.', - details: err.message - }); - } - - if (affectedRows === 0) { - return res.status(404).json({ - error: '삭제할 작업보고서를 찾을 수 없습니다.', - date: date, - worker_id: worker_id - }); - } - - console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}개`); - res.json({ - message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`, - date, - worker_id, - affected_rows: affectedRows, - deleted_by, - timestamp: new Date().toISOString() - }); - }); -}; - -/** - * 📋 마스터 데이터 조회 함수들 - */ -const getWorkTypes = (req, res) => { - console.log('📋 작업 유형 조회 요청'); - dailyWorkReportModel.getAllWorkTypes((err, data) => { - if (err) { - console.error('작업 유형 조회 오류:', err); - return res.status(500).json({ - error: '작업 유형 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - console.log(`📋 작업 유형 조회 결과: ${data.length}개`); - res.json(data); - }); -}; - -const getWorkStatusTypes = (req, res) => { - console.log('📋 업무 상태 유형 조회 요청'); - dailyWorkReportModel.getAllWorkStatusTypes((err, data) => { - if (err) { - console.error('업무 상태 유형 조회 오류:', err); - return res.status(500).json({ - error: '업무 상태 유형 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - console.log(`📋 업무 상태 유형 조회 결과: ${data.length}개`); - res.json(data); - }); -}; - -const getErrorTypes = (req, res) => { - console.log('📋 에러 유형 조회 요청'); - dailyWorkReportModel.getAllErrorTypes((err, data) => { - if (err) { - console.error('에러 유형 조회 오류:', err); - return res.status(500).json({ - error: '에러 유형 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - console.log(`📋 에러 유형 조회 결과: ${data.length}개`); - res.json(data); - }); -}; - -// 모든 컨트롤러 함수 내보내기 (기존 기능 + 누적 기능) -module.exports = { - // 📝 핵심 CRUD 함수들 - createDailyWorkReport, // 누적 추가 (덮어쓰기 없음) - getDailyWorkReports, // 조회 (작성자별 필터링) - getDailyWorkReportsByDate, // 날짜별 조회 - searchWorkReports, // 검색 (페이지네이션) - updateWorkReport, // 수정 - removeDailyWorkReport, // 개별 삭제 - removeDailyWorkReportByDateAndWorker, // 전체 삭제 - - // 🔄 누적 관련 새로운 함수들 - getAccumulatedReports, // 누적 현황 조회 - getContributorsSummary, // 기여자별 요약 - getMyAccumulatedData, // 개인 누적 현황 - removeMyEntry, // 개별 항목 삭제 (본인 것만) - - // 📊 요약 및 통계 함수들 - getDailySummary, // 일일 요약 - getMonthlySummary, // 월간 요약 - getWorkReportStats, // 통계 - - // 📋 마스터 데이터 함수들 - getWorkTypes, // 작업 유형 목록 - getWorkStatusTypes, // 업무 상태 유형 목록 - getErrorTypes // 에러 유형 목록 -}; \ No newline at end of file diff --git a/api.hyungi.net/docker-compose.yml.backup b/api.hyungi.net/docker-compose.yml.backup deleted file mode 100644 index f81c2f4..0000000 --- a/api.hyungi.net/docker-compose.yml.backup +++ /dev/null @@ -1,99 +0,0 @@ -version: "3.8" - -services: - db: - image: mariadb:10.9 - container_name: db_hyungi_net - restart: unless-stopped - env_file: - - ./.env - environment: - - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - - MYSQL_DATABASE=${DB_NAME} - - MYSQL_USER=${DB_USER} - - MYSQL_PASSWORD=${DB_PASSWORD} - volumes: - - db_data:/var/lib/mysql - - ./migrations:/docker-entrypoint-initdb.d # SQL 마이그레이션 자동 실행 - ports: - - "3306:3306" # 개발 시 외부 접속용 (운영 시 제거) - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - timeout: 20s - retries: 10 - - api: - build: - context: . - dockerfile: Dockerfile - container_name: api_hyungi_net - depends_on: - db: - condition: service_healthy # DB가 준비된 후 시작 - restart: unless-stopped - ports: - - "${PORT:-3005}:3005" - env_file: - - ./.env - environment: - - NODE_ENV=production - volumes: - - ./public/img:/usr/src/app/public/img:ro - - ./uploads:/usr/src/app/uploads - - ./logs:/usr/src/app/logs # 로그 파일 저장 - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" - - phpmyadmin: - image: phpmyadmin/phpmyadmin:latest - container_name: pma_hyungi_net - depends_on: - - db - restart: unless-stopped - ports: - - "18080:80" - env_file: - - ./.env - environment: - - PMA_HOST=${DB_HOST:-db} - - PMA_USER=${DB_ROOT_USER:-root} - - PMA_PASSWORD=${DB_ROOT_PASSWORD} - - UPLOAD_LIMIT=50M - - # Redis 캐시 서버 (선택사항 - 세션 관리 및 속도 제한용) - # redis: - # image: redis:7-alpine - # container_name: redis_hyungi_net - # restart: unless-stopped - # ports: - # - "6379:6379" - # volumes: - # - redis_data:/data - # command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-yourredispassword} - - # Nginx 리버스 프록시 (선택사항 - HTTPS 및 로드밸런싱용) - # nginx: - # image: nginx:alpine - # container_name: nginx_hyungi_net - # restart: unless-stopped - # ports: - # - "80:80" - # - "443:443" - # volumes: - # - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - # - ./nginx/ssl:/etc/nginx/ssl:ro - # depends_on: - # - api - -volumes: - db_data: - external: true - name: 7a5a13668b77b18bc1efaf1811d09560aa3be0e722d782e8460cb74f37328d81 # 기존 볼륨명으로 연결 - # redis_data: # Redis 사용 시 주석 해제 - -networks: - default: - name: hyungi_network \ No newline at end of file diff --git a/api.hyungi.net/index.js.backup b/api.hyungi.net/index.js.backup deleted file mode 100644 index 3d1e1ec..0000000 --- a/api.hyungi.net/index.js.backup +++ /dev/null @@ -1,79 +0,0 @@ -require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const path = require('path'); -const app = express(); - -// ✅ 요청 바디 용량 제한 확장 -app.use(express.urlencoded({ extended: true, limit: '50mb' })); -app.use(express.json({ limit: '50mb' })); - -// ✅ CORS 설정: 허용 origin 명시 -app.use(cors({ - origin: function (origin, callback) { - const allowedOrigins = [ - 'https://ahn.hyungi.net', - 'https://tech.hyungi.net', - 'https://pdf.hyungi.net' - ]; - if (!origin || allowedOrigins.includes(origin)) { - callback(null, true); - } else { - callback(new Error('CORS 차단됨: ' + origin)); - } - } -})); - -// ✅ 라우터 등록 -const authRoutes = require('./routes/authRoutes'); -const projectRoutes = require('./routes/projectRoutes'); -const workerRoutes = require('./routes/workerRoutes'); -const taskRoutes = require('./routes/taskRoutes'); -const processRoutes = require('./routes/processRoutes'); -const workReportRoutes = require('./routes/workReportRoutes'); -const cuttingPlanRoutes = require('./routes/cuttingPlanRoutes'); -const factoryInfoRoutes = require('./routes/factoryInfoRoutes'); -const equipmentListRoutes = require('./routes/equipmentListRoutes'); -const toolsRoute = require('./routes/toolsRoute'); -const uploadRoutes = require('./routes/uploadRoutes'); -const uploadBgRoutes = require('./routes/uploadBgRoutes'); -const dailyIssueReportRoutes = require('./routes/dailyIssueReportRoutes'); -const issueTypeRoutes = require('./routes/issueTypeRoutes'); -const healthRoutes = require('./routes/healthRoutes'); -const pipeSpecRoutes = require('./routes/pipeSpecRoutes'); - -// ahn.hyungi.net 배포용 -app.use(express.static(path.join(__dirname, 'public'))); - -// ✅ 업로드된 파일 정적 라우팅 추가 (웹에서 이미지 접근 가능하게) -app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); - -// ✅ 각 라우트 등록 -app.use('/api/auth', authRoutes); -app.use('/api/projects', projectRoutes); -app.use('/api/workers', workerRoutes); -app.use('/api/tasks', taskRoutes); -app.use('/api/processes', processRoutes); -app.use('/api/workreports', workReportRoutes); -app.use('/api/cuttingplans', cuttingPlanRoutes); -app.use('/api/factoryinfo', factoryInfoRoutes); -app.use('/api/equipment', equipmentListRoutes); -app.use('/api/tools', toolsRoute); -app.use('/api/uploads', uploadRoutes); -app.use('/api', uploadBgRoutes); // ✅ upload-bg 경로용 -app.use('/api/issue-reports', dailyIssueReportRoutes); -app.use('/api/issue-types', issueTypeRoutes); -app.use('/api', healthRoutes); -app.use('/api/pipespecs', pipeSpecRoutes); - -// ✅ 서버 실행 -const PORT = process.env.PORT || 3005; -app.listen(PORT, () => { - console.log(`🚀 서버가 ${PORT}번 포트에서 실행 중...`); -}).on('error', (err) => { - console.error('❌ 서버 실행 중 오류 발생:', err); -}); - -app.use((req, res) => { - res.status(404).json({ error: '존재하지 않는 경로입니다.' }); -}); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index dd0676a..446d196 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,10 +7,10 @@ services: container_name: tkfb_db restart: unless-stopped environment: - - MYSQL_ROOT_PASSWORD=tkfb2024! - - MYSQL_DATABASE=hyungi - - MYSQL_USER=hyungi_user - - MYSQL_PASSWORD=hyungi2024! + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${MYSQL_DATABASE:-hyungi} + - MYSQL_USER=${MYSQL_USER:-hyungi_user} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} volumes: - db_data:/var/lib/mysql - ./api.hyungi.net/migrations:/docker-entrypoint-initdb.d @@ -36,17 +36,18 @@ services: ports: - "20005:3005" environment: - - NODE_ENV=production - - PORT=3005 - - DB_HOST=db - - DB_PORT=3306 - - DB_USER=hyungi_user - - DB_PASSWORD=hyungi2024! - - DB_NAME=hyungi - - DB_ROOT_PASSWORD=tkfb2024! - - JWT_SECRET=tkfb_jwt_secret_2024_hyungi_secure_key - - JWT_EXPIRES_IN=7d - - JWT_REFRESH_EXPIRES_IN=30d + - NODE_ENV=${NODE_ENV:-production} + - PORT=${PORT:-3005} + - DB_HOST=${DB_HOST:-db} + - DB_PORT=${DB_PORT:-3306} + - DB_USER=${MYSQL_USER:-hyungi_user} + - DB_PASSWORD=${MYSQL_PASSWORD} + - DB_NAME=${MYSQL_DATABASE:-hyungi} + - DB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d} + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} + - JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-30d} volumes: - ./api.hyungi.net/public/img:/usr/src/app/public/img:ro - ./api.hyungi.net/uploads:/usr/src/app/uploads @@ -89,7 +90,7 @@ services: ports: - "20008:8000" environment: - - API_BASE_URL=http://api:3005 + - API_BASE_URL=${API_BASE_URL:-http://api:3005} depends_on: - api networks: @@ -105,10 +106,10 @@ services: ports: - "20080:80" environment: - - PMA_HOST=db - - PMA_USER=root - - PMA_PASSWORD=tkfb2024! - - UPLOAD_LIMIT=50M + - PMA_HOST=${PMA_HOST:-db} + - PMA_USER=${PMA_USER:-root} + - PMA_PASSWORD=${MYSQL_ROOT_PASSWORD} + - UPLOAD_LIMIT=${UPLOAD_LIMIT:-50M} networks: - tkfb_network diff --git a/docker-compose.yml.new b/docker-compose.yml.new deleted file mode 100644 index 87a974d..0000000 --- a/docker-compose.yml.new +++ /dev/null @@ -1,120 +0,0 @@ -version: "3.8" - -services: - # MariaDB 데이터베이스 - db: - image: mariadb:10.9 - container_name: tkfb_db - restart: unless-stopped - environment: - - MYSQL_ROOT_PASSWORD=tkfb2024! - - MYSQL_DATABASE=hyungi - - MYSQL_USER=hyungi_user - - MYSQL_PASSWORD=hyungi2024! - volumes: - - db_data:/var/lib/mysql - - ./api.hyungi.net/migrations:/docker-entrypoint-initdb.d - ports: - - "20306:3306" - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - timeout: 20s - retries: 10 - networks: - - tkfb_network - - # Node.js API 서버 - api: - build: - context: ./api.hyungi.net - dockerfile: Dockerfile - container_name: tkfb_api - depends_on: - db: - condition: service_healthy - restart: unless-stopped - ports: - - "20005:3005" - environment: - - NODE_ENV=production - - PORT=3005 - - DB_HOST=db - - DB_PORT=3306 - - DB_USER=hyungi_user - - DB_PASSWORD=hyungi2024! - - DB_NAME=hyungi - - DB_ROOT_PASSWORD=tkfb2024! - - JWT_SECRET=tkfb_jwt_secret_2024_hyungi_secure_key - volumes: - - ./api.hyungi.net/public/img:/usr/src/app/public/img:ro - - ./api.hyungi.net/uploads:/usr/src/app/uploads - - ./api.hyungi.net/logs:/usr/src/app/logs - - ./api.hyungi.net/routes:/usr/src/app/routes - - ./api.hyungi.net/controllers:/usr/src/app/controllers - - ./api.hyungi.net/models:/usr/src/app/models - - ./api.hyungi.net/index.js:/usr/src/app/index.js - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" - networks: - - tkfb_network - - # Web UI (Nginx) - web: - build: - context: ./web-ui - dockerfile: Dockerfile - container_name: tkfb_web - restart: unless-stopped - ports: - - "20000:80" - volumes: - - ./web-ui:/usr/share/nginx/html:ro - depends_on: - - api - networks: - - tkfb_network - - # FastAPI Bridge - fastapi: - build: - context: ./fastapi-bridge - dockerfile: Dockerfile - container_name: tkfb_fastapi - restart: unless-stopped - ports: - - "20008:8000" - environment: - - API_BASE_URL=http://api:3005 - depends_on: - - api - networks: - - tkfb_network - - # phpMyAdmin - phpmyadmin: - image: phpmyadmin/phpmyadmin:latest - container_name: tkfb_phpmyadmin - depends_on: - - db - restart: unless-stopped - ports: - - "20080:80" - environment: - - PMA_HOST=db - - PMA_USER=root - - PMA_PASSWORD=tkfb2024! - - UPLOAD_LIMIT=50M - networks: - - tkfb_network - -volumes: - db_data: - driver: local - -networks: - tkfb_network: - driver: bridge - name: tkfb_network diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..121ead2 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,89 @@ +# TK-FB-Project 개발 문서 + +## 📚 문서 구조 + +이 디렉토리는 TK-FB-Project의 개발 및 유지보수를 위한 종합 문서 저장소입니다. + +### 디렉토리 구조 + +``` +docs/ +├── README.md # 이 파일 - 문서 인덱스 +├── architecture/ # 아키텍처 관련 문서 +│ ├── OVERVIEW.md # 시스템 아키텍처 개요 +│ ├── DATABASE.md # 데이터베이스 스키마 및 설계 +│ └── API_DESIGN.md # API 설계 원칙 +├── refactoring/ # 리팩토링 관련 문서 +│ ├── PLAN.md # 리팩토링 계획 및 로드맵 +│ ├── LOG.md # 리팩토링 작업 로그 +│ └── ANALYSIS.md # 코드 분석 리포트 +├── guides/ # 개발 가이드 +│ ├── SETUP.md # 개발 환경 설정 +│ ├── CODING_STYLE.md # 코딩 스타일 가이드 +│ ├── GIT_WORKFLOW.md # Git 워크플로우 +│ ├── SECURITY.md # 보안 가이드라인 +│ └── TROUBLESHOOTING.md # 문제 해결 가이드 +└── api/ # API 문서 + ├── ENDPOINTS.md # API 엔드포인트 목록 + ├── AUTHENTICATION.md # 인증 및 권한 + └── EXAMPLES.md # API 사용 예제 +``` + +## 📖 주요 문서 + +### 시작하기 +- [개발 환경 설정](guides/SETUP.md) - 프로젝트 설정 및 실행 방법 +- [시스템 아키텍처](architecture/OVERVIEW.md) - 전체 시스템 구조 이해 +- [코딩 스타일 가이드](guides/CODING_STYLE.md) - 코드 작성 규칙 + +### 개발 +- [API 문서](api/ENDPOINTS.md) - REST API 엔드포인트 레퍼런스 +- [데이터베이스 스키마](architecture/DATABASE.md) - DB 구조 및 관계 +- [문제 해결](guides/TROUBLESHOOTING.md) - 자주 발생하는 문제 해결법 + +### 리팩토링 +- [리팩토링 계획](refactoring/PLAN.md) - 개선 로드맵 +- [리팩토링 로그](refactoring/LOG.md) - 변경 이력 +- [코드 분석](refactoring/ANALYSIS.md) - 현재 코드베이스 분석 + +## 🔄 문서 업데이트 규칙 + +1. **리팩토링 시**: 변경 사항을 `refactoring/LOG.md`에 기록 +2. **API 변경 시**: `api/ENDPOINTS.md` 업데이트 +3. **아키텍처 변경 시**: 관련 다이어그램 및 문서 갱신 +4. **새로운 기능 추가 시**: 해당 가이드 문서 작성 + +## 📝 문서 작성 가이드 + +### 문서 작성 원칙 +- **명확성**: 기술 용어는 쉽게 설명 +- **최신성**: 코드 변경 시 즉시 업데이트 +- **완결성**: 독립적으로 이해 가능하도록 작성 +- **예제 포함**: 코드 예제와 스크린샷 활용 + +### 마크다운 스타일 +```markdown +# H1: 문서 제목 (한 문서에 한 개만) +## H2: 주요 섹션 +### H3: 하위 섹션 + +- 리스트 사용 +- 코드 블록 활용 +- 테이블로 정보 정리 +``` + +## 🔗 외부 리소스 + +- [프로젝트 README](../README.md) +- [데이터베이스 스키마](../DATABASE_SCHEMA.md) +- [MySQL 호환성 노트](../MYSQL_COMPATIBILITY_NOTES.md) + +## 📅 문서 이력 + +| 날짜 | 버전 | 변경 내용 | 작성자 | +|------|------|----------|--------| +| 2025-12-11 | 1.0 | 문서 구조 초기 생성 | Claude Code | + +## 📧 문의 + +문서 관련 질문이나 개선 제안은 프로젝트 관리자에게 문의하세요. diff --git a/docs/architecture/OVERVIEW.md b/docs/architecture/OVERVIEW.md new file mode 100644 index 0000000..ebadef6 --- /dev/null +++ b/docs/architecture/OVERVIEW.md @@ -0,0 +1,624 @@ +# 시스템 아키텍처 개요 + +> TK-FB-Project 시스템 아키텍처 및 설계 문서 + +## 📊 시스템 개요 + +TK-FB-Project는 Technical Korea의 **일일 작업 관리 시스템**으로, 작업자들의 일일 업무를 기록하고 관리하는 웹 기반 애플리케이션입니다. + +### 주요 기능 +1. **작업 보고서 관리**: 일일 작업 내용 기록 및 조회 +2. **작업자 관리**: 작업자 정보 및 권한 관리 +3. **프로젝트 관리**: 프로젝트별 작업 추적 +4. **근태 관리**: 출퇴근 및 휴가 관리 +5. **대시보드**: 작업 통계 및 분석 +6. **이슈 관리**: 작업 중 발생한 이슈 추적 + +--- + +## 🏗️ 아키텍처 다이어그램 + +### 전체 시스템 구조 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 클라이언트 │ +│ (Web Browser) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Dashboard │ │ Calendar │ │ Report │ │ +│ │ (HTML/ │ │ (HTML/ │ │ (HTML/ │ │ +│ │ CSS/JS) │ │ CSS/JS) │ │ CSS/JS) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + │ HTTP/HTTPS + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Nginx (Port 8080) │ +│ (Static File Server) │ +└─────────────────────────────────────────────────────────────┘ + │ + │ Reverse Proxy + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ API Server (Port 20005) │ +│ Node.js + Express │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Controllers │ │ Services │ │ Models │ │ +│ │ (Routes) │ │ (Business │ │ (Data │ │ +│ │ │ │ Logic) │ │ Access) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Middlewares │ │ Utilities │ │ +│ │ (Auth, CORS) │ │ (Logger) │ │ +│ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + │ MySQL Protocol + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ MySQL 8.0 (Port 3306) │ +│ (Database) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ users │ │ workers │ │ projects │ │ +│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ +│ │daily_work_ │ │ attendance │ │ issues │ │ +│ │ reports │ │ _records │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 배포 구조 (Docker Compose) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Host (Synology NAS) │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Container: web-ui (Nginx) │ │ +│ │ Port: 8080:80 │ │ +│ │ Volume: ./web-ui:/usr/share/nginx/html │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Container: api (Node.js) │ │ +│ │ Port: 20005:20005 │ │ +│ │ Volume: ./api.hyungi.net:/app │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Container: db (MySQL 8.0) │ │ +│ │ Port: 3306:3306 │ │ +│ │ Volume: mysql_data:/var/lib/mysql │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Container: fastapi-bridge (Python) │ │ +│ │ Port: 8000:8000 │ │ +│ │ (Optional - AI 기능용) │ │ +│ └────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 🔧 기술 스택 + +### 프론트엔드 +| 기술 | 버전 | 용도 | +|------|------|------| +| HTML5 | - | 마크업 | +| CSS3 | - | 스타일링 | +| JavaScript (ES6+) | - | 클라이언트 로직 | +| FullCalendar | 6.x | 캘린더 UI | +| Chart.js | 3.x | 차트 시각화 | +| jQuery | 3.x | DOM 조작 (레거시) | + +### 백엔드 +| 기술 | 버전 | 용도 | +|------|------|------| +| Node.js | 20.x | 런타임 | +| Express | 4.18+ | 웹 프레임워크 | +| MySQL2 | 3.6+ | 데이터베이스 드라이버 | +| JWT | 9.0+ | 인증 | +| bcryptjs | 2.4+ | 비밀번호 해싱 | +| dotenv | 16.3+ | 환경변수 관리 | + +### 데이터베이스 +| 기술 | 버전 | 용도 | +|------|------|------| +| MySQL | 8.0+ | 관계형 데이터베이스 | + +### 인프라 +| 기술 | 버전 | 용도 | +|------|------|------| +| Docker | 24+ | 컨테이너화 | +| Docker Compose | 2.x | 오케스트레이션 | +| Nginx | 1.25+ | 웹 서버 / 리버스 프록시 | + +### 추가 도구 +| 기술 | 용도 | +|------|------| +| Python FastAPI | AI 기능 브릿지 (선택) | +| OpenAI API | 작업 내용 분석 (선택) | + +--- + +## 📂 디렉토리 구조 + +### 현재 구조 + +``` +TK-FB-Project/ +├── api.hyungi.net/ # 백엔드 API 서버 +│ ├── controllers/ # 컨트롤러 (라우트 핸들러) +│ │ ├── dailyWorkReportController.js +│ │ ├── workerController.js +│ │ ├── projectController.js +│ │ └── ... +│ ├── models/ # 데이터 모델 +│ │ ├── dailyWorkReportModel.js +│ │ ├── workerModel.js +│ │ └── ... +│ ├── routes/ # 라우트 정의 +│ │ ├── dailyWorkReportRoutes.js +│ │ ├── workerRoutes.js +│ │ └── ... +│ ├── services/ # 비즈니스 로직 (일부) +│ │ ├── authService.js +│ │ ├── emailService.js +│ │ └── ... +│ ├── middleware/ # 미들웨어 +│ │ ├── errorMiddleware.js +│ │ └── uploadMiddleware.js +│ ├── utils/ # 유틸리티 +│ │ └── logger.js +│ ├── uploads/ # 업로드 파일 저장소 +│ ├── index.js # 애플리케이션 진입점 +│ ├── package.json +│ └── .env # 환경 변수 +│ +├── web-ui/ # 프론트엔드 +│ ├── pages/ # HTML 페이지 (51개) +│ │ ├── index.html +│ │ ├── daily-work-report.html +│ │ ├── work-report-calendar.html +│ │ └── ... +│ ├── js/ # JavaScript (49개) +│ │ ├── api-config.js +│ │ ├── daily-work-report.js +│ │ ├── work-report-calendar.js +│ │ └── ... +│ ├── css/ # CSS (22개) +│ │ ├── common.css +│ │ ├── daily-work-report.css +│ │ └── ... +│ └── assets/ # 정적 리소스 +│ └── images/ +│ +├── fastapi-bridge/ # Python FastAPI (선택) +│ ├── main.py +│ ├── requirements.txt +│ └── ... +│ +├── docs/ # 📚 프로젝트 문서 +│ ├── README.md +│ ├── architecture/ +│ │ └── OVERVIEW.md # 이 파일 +│ ├── refactoring/ +│ │ ├── ANALYSIS.md +│ │ ├── PLAN.md +│ │ └── LOG.md +│ ├── guides/ +│ │ ├── SETUP.md +│ │ └── CODING_STYLE.md +│ └── api/ +│ +├── docker-compose.yml # Docker Compose 설정 +├── .gitignore +├── README.md +└── package.json +``` + +### 목표 구조 (리팩토링 후) + +``` +TK-FB-Project/ +├── api/ # 백엔드 (이름 변경) +│ ├── src/ +│ │ ├── config/ # 설정 파일 +│ │ │ ├── database.js +│ │ │ ├── cors.js +│ │ │ └── routes.js +│ │ ├── controllers/ # 라우트 핸들러만 +│ │ │ └── ... +│ │ ├── services/ # 비즈니스 로직 (완성) +│ │ │ └── ... +│ │ ├── repositories/ # 데이터 접근 계층 (신규) +│ │ │ └── ... +│ │ ├── models/ # 데이터 모델 / 스키마 +│ │ │ └── ... +│ │ ├── middlewares/ # 미들웨어 +│ │ │ ├── auth.js +│ │ │ ├── permission.js +│ │ │ └── errorHandler.js +│ │ ├── utils/ # 유틸리티 +│ │ │ ├── errors.js +│ │ │ ├── logger.js +│ │ │ └── validator.js +│ │ └── index.js # 진입점 (간결화) +│ ├── tests/ # 테스트 +│ │ ├── unit/ +│ │ └── integration/ +│ └── package.json +│ +├── web/ # 프론트엔드 (이름 변경) +│ ├── public/ # 정적 파일 +│ │ └── assets/ +│ ├── src/ +│ │ ├── modules/ # 모듈화된 코드 +│ │ │ ├── common/ +│ │ │ │ ├── api-client.js +│ │ │ │ ├── utils.js +│ │ │ │ └── validator.js +│ │ │ ├── calendar/ +│ │ │ │ ├── CalendarView.js +│ │ │ │ ├── CalendarAPI.js +│ │ │ │ └── CalendarUtils.js +│ │ │ └── dashboard/ +│ │ ├── pages/ # 페이지별 스크립트 +│ │ └── styles/ # CSS +│ │ ├── base/ +│ │ ├── components/ +│ │ ├── layouts/ +│ │ └── pages/ +│ └── index.html +│ +├── database/ # 데이터베이스 관련 +│ ├── migrations/ # 마이그레이션 +│ ├── seeds/ # 시드 데이터 +│ └── schema/ # 스키마 정의 +│ +├── deployment/ # 배포 스크립트 +│ ├── docker/ +│ │ ├── api.Dockerfile +│ │ ├── web.Dockerfile +│ │ └── nginx.conf +│ ├── docker-compose.yml +│ ├── docker-compose.dev.yml +│ └── deploy.sh +│ +├── docs/ # 문서 +└── scripts/ # 유틸리티 스크립트 +``` + +--- + +## 🔄 데이터 흐름 + +### 1. 사용자 인증 플로우 + +``` +┌──────┐ ┌──────┐ ┌──────────┐ ┌──────┐ +│Client│ │ API │ │ Service │ │ DB │ +└──┬───┘ └──┬───┘ └────┬─────┘ └──┬───┘ + │ │ │ │ + │ POST /login │ │ │ + ├────────────>│ │ │ + │ │ validate │ │ + │ ├───────────────>│ │ + │ │ │ findByUsername│ + │ │ ├──────────────>│ + │ │ │<──────────────┤ + │ │ │ user data │ + │ │ verify pwd │ │ + │ │<───────────────┤ │ + │ │ generate JWT │ │ + │ ├───────────────>│ │ + │ JWT Token│ │ │ + │<────────────┤ │ │ + │ │ │ │ + └─────────────┴────────────────┴───────────────┘ +``` + +### 2. 작업 보고서 생성 플로우 + +``` +┌──────┐ ┌─────┐ ┌──────────┐ ┌─────────┐ ┌────┐ +│Client│ │ API │ │ Service │ │ Repo │ │ DB │ +└──┬───┘ └──┬──┘ └────┬─────┘ └────┬────┘ └─┬──┘ + │ │ │ │ │ + │POST /reports │ │ │ + ├──────────>│ │ │ │ + │ │ auth check │ │ │ + │ │ │ │ │ + │ │ validate │ │ │ + │ ├───────────>│ │ │ + │ │ │ checkDup │ │ + │ │ ├─────────────>│ │ + │ │ │ │ SELECT │ + │ │ │ ├─────────>│ + │ │ │ │<─────────┤ + │ │ │<─────────────┤ │ + │ │ │ create │ │ + │ │ ├─────────────>│ │ + │ │ │ │ INSERT │ + │ │ │ ├─────────>│ + │ │ │ │<─────────┤ + │ │ │<─────────────┤ │ + │ │ │ notify │ │ + │ │ │ (email/push) │ │ + │ │<───────────┤ │ │ + │<──────────┤ │ │ │ + │ 201 │ │ │ │ + └───────────┴────────────┴──────────────┴──────────┘ +``` + +### 3. 대시보드 데이터 조회 플로우 + +``` +Client → API → Service → Repository → DB + ↓ + Cache (Redis - 향후 도입 예정) +``` + +--- + +## 🔐 보안 아키텍처 + +### 인증 (Authentication) + +**JWT 기반 인증**: +```javascript +// 로그인 성공 시 +const token = jwt.sign( + { + userId: user.id, + username: user.username, + access_level: user.access_level + }, + process.env.JWT_SECRET, + { expiresIn: '24h' } +); + +// 요청 시 헤더에 포함 +Authorization: Bearer +``` + +**토큰 검증 플로우**: +``` +Request → Auth Middleware → Verify JWT → Attach user to req → Next + ↓ (invalid) + 401 Unauthorized +``` + +### 권한 (Authorization) + +**역할 기반 접근 제어 (RBAC)**: + +| 역할 | 권한 | +|------|------| +| `system` | 모든 권한 (시스템 관리자) | +| `admin` | 전체 데이터 조회 및 수정 | +| `group_leader` | 그룹 내 데이터 관리 | +| `worker` | 본인 데이터만 조회/수정 | + +**권한 체크 미들웨어** (목표): +```javascript +// 특정 역할 필요 +router.get('/admin', requireRole('admin', 'system'), getAdminData); + +// 본인 또는 관리자 +router.put('/reports/:id', requireOwnerOrAdmin, updateReport); +``` + +### 데이터 보안 + +1. **비밀번호**: bcrypt로 해싱 (salt rounds: 10) +2. **환경변수**: .env 파일 (Git 제외) +3. **SQL Injection**: Prepared Statements 사용 +4. **XSS**: 입력값 sanitization +5. **CORS**: 허용된 origin만 접근 + +--- + +## 📊 데이터베이스 설계 + +### ER 다이어그램 + +``` +┌─────────────┐ ┌─────────────┐ +│ users │ │ workers │ +├─────────────┤ ├─────────────┤ +│ id (PK) │ ┌───>│ id (PK) │ +│ username │ │ │ name │ +│ password │ │ │ email │ +│ access_level│ │ │ position │ +│ worker_id(FK├────┘ │ is_active │ +└─────────────┘ └──────┬──────┘ + │ + │ 1:N + │ + ┌──────▼──────────┐ + │daily_work_ │ + │ reports │ + ├─────────────────┤ + │ id (PK) │ + │ worker_id (FK) │ + │ project_id (FK) │ + │ report_date │ + │ work_content │ + │ work_hours │ + │ created_at │ + └──────┬──────────┘ + │ N:1 + │ + ┌──────▼──────┐ + │ projects │ + ├─────────────┤ + │ id (PK) │ + │ name │ + │ description │ + │ start_date │ + │ end_date │ + └─────────────┘ +``` + +### 주요 테이블 + +| 테이블명 | 설명 | 주요 컬럼 | +|----------|------|-----------| +| `users` | 사용자 계정 | id, username, password, access_level | +| `workers` | 작업자 정보 | id, name, email, position, group_id | +| `daily_work_reports` | 일일 작업 보고서 | id, worker_id, project_id, report_date, work_content | +| `projects` | 프로젝트 | id, name, description, start_date, end_date | +| `attendance_records` | 근태 기록 | id, worker_id, date, clock_in, clock_out | +| `issues` | 이슈 | id, project_id, reporter_id, title, status | +| `work_types` | 작업 유형 | id, name, category | +| `work_status_types` | 작업 상태 | id, name, color | + +자세한 스키마는 [DATABASE_SCHEMA.md](../../DATABASE_SCHEMA.md) 참조 + +--- + +## 🚀 배포 프로세스 + +### 현재 배포 방식 + +```bash +# 1. Docker Compose로 배포 +docker-compose up -d + +# 2. 수동 업데이트 +docker-compose down +git pull origin master +docker-compose up -d --build +``` + +### 목표 CI/CD 파이프라인 + +``` +┌────────────┐ ┌────────────┐ ┌────────────┐ +│ Push │ │ Build │ │ Test │ +│ to GitHub ├─────>│ & Lint ├─────>│ (Jest) │ +└────────────┘ └────────────┘ └──────┬─────┘ + │ + ▼ + ┌────────────┐ ┌────────────┐ + │ Deploy │<─────│ Security │ + │ Production │ │ Scan │ + └────────────┘ └────────────┘ +``` + +**GitHub Actions 예시**: +```yaml +name: CI/CD + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '20' + - run: npm ci + - run: npm test + - run: npm run lint + + deploy: + needs: test + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - name: Deploy to Synology + run: ./deployment/deploy.sh +``` + +--- + +## 📈 성능 고려사항 + +### 현재 성능 이슈 + +1. **N+1 쿼리 문제**: JOIN 사용 필요 +2. **SELECT * 사용**: 명시적 컬럼 지정 필요 +3. **인덱스 부족**: 주요 컬럼에 인덱스 추가 필요 +4. **캐싱 없음**: Redis 도입 검토 + +### 최적화 계획 + +```javascript +// 1. 쿼리 최적화 +// Before: N+1 쿼리 +const reports = await getReports(); +for (const report of reports) { + report.worker = await getWorker(report.worker_id); +} + +// After: JOIN 사용 +const reports = await db.query(` + SELECT r.*, w.name as worker_name + FROM daily_work_reports r + JOIN workers w ON r.worker_id = w.id +`); + +// 2. 인덱스 추가 +CREATE INDEX idx_report_date ON daily_work_reports(report_date); +CREATE INDEX idx_worker_id ON daily_work_reports(worker_id); + +// 3. 캐싱 (Redis - 향후) +const cachedData = await redis.get('dashboard:stats'); +if (cachedData) return JSON.parse(cachedData); + +const data = await fetchDashboardStats(); +await redis.setex('dashboard:stats', 300, JSON.stringify(data)); +``` + +--- + +## 🔮 향후 계획 + +### Phase 1: 안정화 (현재) +- 보안 강화 +- 코드 리팩토링 +- 테스트 작성 + +### Phase 2: 모듈화 (3개월) +- 서비스 레이어 완성 +- 프론트엔드 모듈화 +- API 문서화 + +### Phase 3: 현대화 (6개월) +- TypeScript 전환 +- React/Vue 도입 +- ORM 적용 (TypeORM/Prisma) + +### Phase 4: 확장 (12개월) +- 마이크로서비스 아키텍처 +- GraphQL API +- 실시간 기능 (WebSocket) +- 모바일 앱 (React Native) + +--- + +## 📚 관련 문서 + +- [데이터베이스 스키마](../../DATABASE_SCHEMA.md) +- [API 문서](../api/ENDPOINTS.md) +- [개발 환경 설정](../guides/SETUP.md) +- [리팩토링 계획](../refactoring/PLAN.md) + +--- + +*마지막 업데이트: 2025-12-11* diff --git a/docs/guides/CODING_STYLE.md b/docs/guides/CODING_STYLE.md new file mode 100644 index 0000000..553bf75 --- /dev/null +++ b/docs/guides/CODING_STYLE.md @@ -0,0 +1,686 @@ +# 코딩 스타일 가이드 + +> TK-FB-Project 코드 작성 규칙 및 베스트 프랙티스 + +## 📚 목차 + +1. [일반 원칙](#일반-원칙) +2. [JavaScript/Node.js](#javascriptnodejs) +3. [HTML/CSS](#htmlcss) +4. [SQL](#sql) +5. [Git 커밋 메시지](#git-커밋-메시지) +6. [파일 및 디렉토리 구조](#파일-및-디렉토리-구조) + +--- + +## 일반 원칙 + +### 1. 가독성 우선 +```javascript +// ❌ 나쁜 예 +const x = a.filter(i => i.active).map(i => ({...i, name: i.n})); + +// ✅ 좋은 예 +const activeWorkers = workers + .filter(worker => worker.is_active) + .map(worker => ({ + ...worker, + name: worker.name + })); +``` + +### 2. 명확한 네이밍 +```javascript +// ❌ 나쁜 예 +const d = new Date(); +const arr = []; +const temp = getUserData(); + +// ✅ 좋은 예 +const currentDate = new Date(); +const activeWorkers = []; +const userData = getUserData(); +``` + +### 3. 단일 책임 원칙 +```javascript +// ❌ 나쁜 예 - 하나의 함수가 너무 많은 일을 함 +async function processReport(data) { + // 검증 + if (!data.worker_id) throw new Error('Invalid'); + // DB 저장 + await db.query('INSERT INTO ...'); + // 이메일 발송 + await sendEmail(); + // 알림 전송 + await sendNotification(); +} + +// ✅ 좋은 예 - 책임 분리 +async function processReport(data) { + validateReport(data); + const report = await saveReport(data); + await notifyReportCreation(report); + return report; +} + +function validateReport(data) { + if (!data.worker_id) { + throw new ValidationError('작업자를 선택해주세요'); + } +} + +async function saveReport(data) { + return await reportRepository.create(data); +} + +async function notifyReportCreation(report) { + await emailService.send(report); + await notificationService.send(report); +} +``` + +### 4. DRY (Don't Repeat Yourself) +```javascript +// ❌ 나쁜 예 - 중복 코드 +if (!req.user || !['admin', 'system'].includes(req.user.access_level)) { + return res.status(403).json({ error: 'Forbidden' }); +} + +// ✅ 좋은 예 - 재사용 가능한 미들웨어 +const requireRole = (...roles) => { + return (req, res, next) => { + if (!req.user || !roles.includes(req.user.access_level)) { + throw new ForbiddenError(); + } + next(); + }; +}; + +// 사용 +router.get('/admin', requireRole('admin', 'system'), getAdminData); +``` + +--- + +## JavaScript/Node.js + +### 변수 선언 + +```javascript +// ✅ const 우선, 재할당 필요시 let, var 사용 금지 +const API_URL = 'http://api.example.com'; +let currentPage = 1; + +// ❌ var 사용 금지 +var x = 10; // NO! +``` + +### 함수 작성 + +```javascript +// ✅ 화살표 함수 사용 (콜백, 간단한 함수) +const double = (n) => n * 2; +const sum = (a, b) => a + b; + +// ✅ 일반 함수 (메서드, 복잡한 로직) +function calculateTotalHours(reports) { + let total = 0; + for (const report of reports) { + total += report.work_hours; + } + return total; +} + +// ✅ async/await 사용 (Promise보다 선호) +async function fetchUserData(userId) { + try { + const user = await userModel.findById(userId); + const reports = await reportModel.findByUser(userId); + return { user, reports }; + } catch (error) { + logger.error('Failed to fetch user data', { userId, error }); + throw error; + } +} +``` + +### 에러 처리 + +```javascript +// ❌ 나쁜 예 +try { + await saveData(); +} catch (error) { + console.log(error); // 단순 로그만 +} + +// ✅ 좋은 예 +try { + await saveData(data); +} catch (error) { + logger.error('데이터 저장 실패', { + data, + error: error.message, + stack: error.stack + }); + throw new AppError('데이터 저장에 실패했습니다', 500); +} +``` + +### 객체 및 배열 + +```javascript +// ✅ 구조 분해 할당 +const { name, email, phone } = user; +const [first, second, ...rest] = items; + +// ✅ 스프레드 연산자 +const newUser = { ...user, is_active: true }; +const allItems = [...items1, ...items2]; + +// ✅ 단축 속성 +const name = 'John'; +const age = 30; +const user = { name, age }; // { name: name, age: age } 대신 +``` + +### 비동기 처리 + +```javascript +// ❌ 콜백 지옥 +getData(function(a) { + getMoreData(a, function(b) { + getMoreData(b, function(c) { + console.log(c); + }); + }); +}); + +// ✅ async/await +async function processData() { + const a = await getData(); + const b = await getMoreData(a); + const c = await getMoreData(b); + return c; +} + +// ✅ 병렬 처리가 가능한 경우 +const [users, projects, reports] = await Promise.all([ + fetchUsers(), + fetchProjects(), + fetchReports() +]); +``` + +### 조건문 + +```javascript +// ✅ Early Return 패턴 +function processUser(user) { + if (!user) { + throw new ValidationError('User is required'); + } + + if (!user.is_active) { + throw new ValidationError('User is not active'); + } + + // 메인 로직 + return processActiveUser(user); +} + +// ❌ 중첩된 조건문 +function processUser(user) { + if (user) { + if (user.is_active) { + // 메인 로직 + return processActiveUser(user); + } else { + throw new Error('Not active'); + } + } else { + throw new Error('No user'); + } +} +``` + +### 주석 + +```javascript +// ✅ JSDoc 사용 +/** + * 작업 보고서를 생성합니다 + * @param {Object} reportData - 보고서 데이터 + * @param {number} reportData.worker_id - 작업자 ID + * @param {string} reportData.work_content - 작업 내용 + * @param {number} userId - 생성하는 사용자 ID + * @returns {Promise} 생성된 보고서 + * @throws {ValidationError} 검증 실패 시 + */ +async function createReport(reportData, userId) { + // ... +} + +// ✅ 복잡한 로직 설명 +// NOTE: MySQL 8.0에서는 GROUP BY 동작이 다르므로 명시적으로 컬럼 지정 +const query = ` + SELECT worker_id, COUNT(*) as count + FROM reports + GROUP BY worker_id + ORDER BY count DESC +`; + +// ⚠️ TODO, FIXME 등 명확히 표시 +// TODO: 캐싱 로직 추가 필요 +// FIXME: 날짜 범위 검증 개선 필요 +// HACK: 임시 해결책, 나중에 리팩토링 필요 +``` + +### 모듈 구조 + +```javascript +// ✅ 명확한 import/export +// 파일 상단에 모든 import +const express = require('express'); +const { ValidationError } = require('../utils/errors'); +const reportService = require('../services/reportService'); + +// 함수 정의 +function createReport(req, res, next) { + // ... +} + +function getReports(req, res, next) { + // ... +} + +// 파일 하단에 export +module.exports = { + createReport, + getReports +}; +``` + +--- + +## HTML/CSS + +### HTML + +```html + +
+ +
+ +
+
+
+

제목

+

내용

+
+
+
+ +
+

© 2025 TK-FB

+
+ + +
+
+
+ 내용 +
+
+
+ + + + +``` + +### CSS + +```css +/* ✅ 클래스 네이밍: BEM 방식 */ +.block {} +.block__element {} +.block--modifier {} + +/* 예시 */ +.card {} +.card__header {} +.card__body {} +.card__footer {} +.card--large {} +.card--primary {} + +/* ✅ 속성 순서 */ +.element { + /* 1. Positioning */ + position: absolute; + top: 0; + left: 0; + z-index: 10; + + /* 2. Box Model */ + display: block; + width: 100px; + height: 100px; + margin: 10px; + padding: 10px; + border: 1px solid #000; + + /* 3. Typography */ + font-family: Arial; + font-size: 14px; + line-height: 1.5; + color: #333; + + /* 4. Visual */ + background-color: #fff; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + + /* 5. Other */ + cursor: pointer; + transition: all 0.3s ease; +} + +/* ✅ CSS 변수 사용 */ +:root { + --color-primary: #007bff; + --color-success: #28a745; + --spacing-md: 1rem; +} + +.button { + background-color: var(--color-primary); + padding: var(--spacing-md); +} + +/* ✅ 중첩 최소화 (3단계 이하) */ +.nav {} +.nav__item {} +.nav__link {} + +/* ❌ 과도한 중첩 */ +.nav ul li a span {} /* NO! */ +``` + +--- + +## SQL + +### 쿼리 작성 + +```sql +-- ✅ 대문자 키워드, 명시적 컬럼 지정 +SELECT + id, + name, + email, + created_at +FROM users +WHERE is_active = 1 + AND role = 'admin' +ORDER BY created_at DESC +LIMIT 10; + +-- ❌ SELECT * 사용 금지 +SELECT * FROM users; -- NO! + +-- ✅ 조인 명시적 작성 +SELECT + r.id, + r.work_content, + w.name AS worker_name, + p.name AS project_name +FROM daily_work_reports r +INNER 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 ?; + +-- ✅ 파라미터 바인딩 사용 +const query = 'SELECT * FROM users WHERE id = ? AND email = ?'; +const [rows] = await db.query(query, [userId, email]); + +-- ❌ 문자열 연결 금지 (SQL Injection 위험) +const query = `SELECT * FROM users WHERE email = '${email}'`; -- NO! +``` + +### 테이블/컬럼 네이밍 + +```sql +-- ✅ 스네이크 케이스 +CREATE TABLE daily_work_reports ( + id INT PRIMARY KEY AUTO_INCREMENT, + worker_id INT NOT NULL, + report_date DATE NOT NULL, + work_content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- ✅ 복수형 테이블명 +users, workers, projects, reports + +-- ✅ 외래키 명확히 +worker_id, project_id (테이블명_id 형식) +``` + +--- + +## Git 커밋 메시지 + +### 형식 + +``` +(): + + + +