From b024a178d09876b1319cb246823858b20d148840 Mon Sep 17 00:00:00 2001 From: hyungi Date: Fri, 24 Oct 2025 10:13:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AA=A9=EB=A1=9D=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EB=B0=8F=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 목록 관리 페이지에 고급 필터링 시스템 추가 - 프로젝트별, 검토상태별, 날짜별 필터링 - 검토 완료/필요 항목 시각적 구분 및 정렬 - 해결 시간 입력 + 확인 버튼으로 검토 완료 처리 - 부적합 조회 페이지에 동일한 필터링 기능 적용 - 검토 상태에 따른 카드 스타일링 (음영 처리) - JavaScript 템플릿 리터럴 오류 수정 - 보고서 페이지 프로젝트별 분석 기능 추가 - 프로젝트 선택 드롭다운 추가 - 총 작업 공수를 프로젝트별 일일공수 데이터로 계산 - 부적합 처리 시간, 카테고리 분석, 상세 목록 모두 프로젝트별 필터링 - localStorage 키 이름 통일 (daily-work-data) --- API_DOCUMENTATION.md | 402 ++++++++++++++++ DATABASE_SCHEMA.md | 173 +++++++ PROJECT_OVERVIEW.md | 328 +++++++++++++ README.md | 72 ++- backend/__pycache__/main.cpython-311.pyc | Bin 3129 -> 3228 bytes .../__pycache__/models.cpython-311.pyc | Bin 4875 -> 5904 bytes .../__pycache__/schemas.cpython-311.pyc | Bin 9079 -> 10859 bytes backend/database/models.py | 19 +- backend/database/schemas.py | 27 ++ backend/main.py | 3 +- backend/migrations/006_add_projects_table.sql | 14 + .../007_add_project_id_to_issues.sql | 15 + .../migrations/008_fix_project_id_bigint.sql | 13 + .../__pycache__/projects.cpython-311.pyc | Bin 0 -> 6392 bytes backend/routers/projects.py | 126 +++++ frontend/create-project-api.html | 193 ++++++++ frontend/daily-work.html | 414 ++++++++++------- frontend/debug-data.html | 158 +++++++ frontend/fix-api-data.html | 171 +++++++ frontend/fix-project-id.html | 134 ++++++ frontend/index.html | 423 +++++++++++++++-- frontend/issue-view.html | 439 ++++++++++++++---- frontend/migrate-data.html | 307 ++++++++++++ frontend/project-management.html | 350 ++++++++++++++ frontend/static/js/api.js | 24 + 25 files changed, 3498 insertions(+), 307 deletions(-) create mode 100644 API_DOCUMENTATION.md create mode 100644 DATABASE_SCHEMA.md create mode 100644 PROJECT_OVERVIEW.md create mode 100644 backend/migrations/006_add_projects_table.sql create mode 100644 backend/migrations/007_add_project_id_to_issues.sql create mode 100644 backend/migrations/008_fix_project_id_bigint.sql create mode 100644 backend/routers/__pycache__/projects.cpython-311.pyc create mode 100644 backend/routers/projects.py create mode 100644 frontend/create-project-api.html create mode 100644 frontend/debug-data.html create mode 100644 frontend/fix-api-data.html create mode 100644 frontend/fix-project-id.html create mode 100644 frontend/migrate-data.html create mode 100644 frontend/project-management.html diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..b28c412 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,402 @@ +# M-Project API 문서 + +## 개요 +작업보고서 시스템의 FastAPI 백엔드 API 엔드포인트 문서 + +**Base URL:** `http://localhost:16000` + +--- + +## 인증 (Authentication) + +### JWT 토큰 기반 인증 +- **토큰 타입:** Bearer Token +- **만료 시간:** 7일 (10080분) +- **헤더:** `Authorization: Bearer ` + +--- + +## API 엔드포인트 + +### 🔐 인증 관련 API (`/api/auth`) + +#### 1. 로그인 +```http +POST /api/auth/login +Content-Type: application/x-www-form-urlencoded + +username=hyungi&password=djg3-jj34-X3Q3 +``` + +**응답:** +```json +{ + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "user": { + "id": 1, + "username": "hyungi", + "full_name": "관리자", + "role": "admin", + "is_active": true, + "created_at": "2024-01-01T00:00:00" + } +} +``` + +#### 2. 현재 사용자 정보 +```http +GET /api/auth/me +Authorization: Bearer +``` + +#### 3. 사용자 생성 (관리자만) +```http +POST /api/auth/users +Authorization: Bearer +Content-Type: application/json + +{ + "username": "user1", + "password": "password123", + "full_name": "사용자1", + "role": "user" +} +``` + +#### 4. 사용자 목록 조회 (관리자만) +```http +GET /api/auth/users?skip=0&limit=100 +Authorization: Bearer +``` + +#### 5. 사용자 수정 (관리자만) +```http +PUT /api/auth/users/{user_id} +Authorization: Bearer +Content-Type: application/json + +{ + "full_name": "수정된 이름", + "role": "admin" +} +``` + +#### 6. 사용자 삭제 (관리자만) +```http +DELETE /api/auth/users/{username} +Authorization: Bearer +``` + +#### 7. 비밀번호 변경 +```http +POST /api/auth/change-password +Authorization: Bearer +Content-Type: application/json + +{ + "current_password": "old_password", + "new_password": "new_password" +} +``` + +--- + +### 📁 프로젝트 관리 API (`/api/projects`) + +#### 1. 프로젝트 생성 (관리자만) +```http +POST /api/projects +Authorization: Bearer +Content-Type: application/json + +{ + "job_no": "JOB-2024-001", + "project_name": "신규 프로젝트" +} +``` + +**응답:** +```json +{ + "id": 1, + "job_no": "JOB-2024-001", + "project_name": "신규 프로젝트", + "created_by_id": 1, + "created_by": { + "id": 1, + "username": "hyungi", + "full_name": "관리자", + "role": "admin" + }, + "created_at": "2024-01-01T00:00:00", + "is_active": true +} +``` + +#### 2. 프로젝트 목록 조회 +```http +GET /api/projects?skip=0&limit=100&active_only=true +Authorization: Bearer +``` + +#### 3. 특정 프로젝트 조회 +```http +GET /api/projects/{project_id} +Authorization: Bearer +``` + +#### 4. 프로젝트 수정 (관리자만) +```http +PUT /api/projects/{project_id} +Authorization: Bearer +Content-Type: application/json + +{ + "project_name": "수정된 프로젝트명", + "is_active": true +} +``` + +#### 5. 프로젝트 삭제 (관리자만) +```http +DELETE /api/projects/{project_id} +Authorization: Bearer +``` + +--- + +### 🚨 부적합 사항 API (`/api/issues`) + +#### 1. 부적합 사항 생성 +```http +POST /api/issues +Authorization: Bearer +Content-Type: application/json + +{ + "category": "material_missing", + "description": "자재 누락 발견", + "photo": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...", + "photo2": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..." +} +``` + +**카테고리 값:** +- `material_missing`: 자재누락 +- `design_error`: 설계미스 +- `incoming_defect`: 입고자재 불량 +- `inspection_miss`: 검사미스 + +#### 2. 부적합 사항 목록 조회 +```http +GET /api/issues?skip=0&limit=100 +Authorization: Bearer +``` + +#### 3. 특정 부적합 사항 조회 +```http +GET /api/issues/{issue_id} +Authorization: Bearer +``` + +#### 4. 부적합 사항 수정 +```http +PUT /api/issues/{issue_id} +Authorization: Bearer +Content-Type: application/json + +{ + "category": "design_error", + "description": "수정된 설명", + "work_hours": 2.5, + "status": "complete", + "detail_notes": "해결 완료" +} +``` + +**상태 값:** +- `new`: 신규 +- `progress`: 진행중 +- `complete`: 완료 + +#### 5. 부적합 사항 삭제 +```http +DELETE /api/issues/{issue_id} +Authorization: Bearer +``` + +--- + +### 📊 일일 공수 API (`/api/daily-work`) + +#### 1. 일일 공수 생성 +```http +POST /api/daily-work +Authorization: Bearer +Content-Type: application/json + +{ + "date": "2024-01-01T00:00:00", + "worker_count": 10, + "overtime_workers": 3, + "overtime_hours": 4.0 +} +``` + +#### 2. 일일 공수 목록 조회 +```http +GET /api/daily-work?skip=0&limit=100 +Authorization: Bearer +``` + +#### 3. 특정 일일 공수 조회 +```http +GET /api/daily-work/{work_id} +Authorization: Bearer +``` + +#### 4. 일일 공수 수정 +```http +PUT /api/daily-work/{work_id} +Authorization: Bearer +Content-Type: application/json + +{ + "worker_count": 12, + "overtime_workers": 5, + "overtime_hours": 6.0 +} +``` + +#### 5. 일일 공수 삭제 +```http +DELETE /api/daily-work/{work_id} +Authorization: Bearer +``` + +--- + +### 📈 보고서 API (`/api/reports`) + +#### 1. 보고서 생성 +```http +POST /api/reports/generate +Authorization: Bearer +Content-Type: application/json + +{ + "start_date": "2024-01-01T00:00:00", + "end_date": "2024-01-31T23:59:59" +} +``` + +**응답:** +```json +{ + "start_date": "2024-01-01T00:00:00", + "end_date": "2024-01-31T23:59:59", + "total_hours": 156.5, + "total_issues": 25, + "category_stats": { + "material_missing": 10, + "dimension_defect": 8, + "incoming_defect": 7 + }, + "completed_issues": 20, + "average_resolution_time": 2.3 +} +``` + +--- + +## 헬스체크 + +### 서버 상태 확인 +```http +GET /api/health +``` + +**응답:** +```json +{ + "status": "healthy" +} +``` + +--- + +## 에러 응답 형식 + +### 인증 오류 (401) +```json +{ + "detail": "Could not validate credentials" +} +``` + +### 권한 오류 (403) +```json +{ + "detail": "관리자 권한이 필요합니다." +} +``` + +### 리소스 없음 (404) +```json +{ + "detail": "프로젝트를 찾을 수 없습니다." +} +``` + +### 유효성 검사 오류 (422) +```json +{ + "detail": [ + { + "loc": ["body", "job_no"], + "msg": "field required", + "type": "value_error.missing" + } + ] +} +``` + +--- + +## 파일 업로드 + +### 이미지 업로드 +- **형식:** Base64 인코딩된 이미지 데이터 +- **지원 형식:** JPEG, PNG, GIF, WebP +- **최대 크기:** 10MB +- **저장 위치:** `/app/uploads/` + +### Base64 형식 예시 +``` +data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k= +``` + +--- + +## 개발 환경 설정 + +### 환경 변수 +```env +DATABASE_URL=postgresql://mproject:mproject2024@db:5432/mproject +SECRET_KEY=your-secret-key-here-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=10080 +ADMIN_USERNAME=hyungi +ADMIN_PASSWORD=djg3-jj34-X3Q3 +TZ=Asia/Seoul +``` + +### Docker 컨테이너 실행 +```bash +docker-compose up -d +``` + +### API 문서 확인 +- **Swagger UI:** http://localhost:16000/docs +- **ReDoc:** http://localhost:16000/redoc diff --git a/DATABASE_SCHEMA.md b/DATABASE_SCHEMA.md new file mode 100644 index 0000000..5d78e52 --- /dev/null +++ b/DATABASE_SCHEMA.md @@ -0,0 +1,173 @@ +# M-Project 데이터베이스 스키마 문서 + +## 개요 +작업보고서 시스템의 PostgreSQL 데이터베이스 스키마 정의 + +--- + +## 테이블 구조 + +### 1. users (사용자) +사용자 계정 정보를 저장하는 테이블 + +| 컬럼명 | 타입 | 제약조건 | 설명 | +|--------|------|----------|------| +| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 사용자 고유 ID | +| username | VARCHAR | UNIQUE, NOT NULL, INDEX | 로그인 아이디 | +| hashed_password | VARCHAR | NOT NULL | 암호화된 비밀번호 | +| full_name | VARCHAR | NULL | 사용자 실명 | +| role | ENUM | DEFAULT 'user' | 사용자 권한 (admin, user) | +| is_active | BOOLEAN | DEFAULT TRUE | 계정 활성화 상태 | +| created_at | TIMESTAMP | DEFAULT NOW() | 계정 생성일시 | + +**인덱스:** +- `idx_users_username` ON username +- `idx_users_role` ON role + +**ENUM 값:** +- role: 'admin', 'user' + +--- + +### 2. projects (프로젝트) +프로젝트 정보를 저장하는 테이블 + +| 컬럼명 | 타입 | 제약조건 | 설명 | +|--------|------|----------|------| +| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 프로젝트 고유 ID | +| job_no | VARCHAR(50) | UNIQUE, NOT NULL, INDEX | Job 번호 | +| project_name | VARCHAR(200) | NOT NULL | 프로젝트 이름 | +| created_by_id | INTEGER | FOREIGN KEY → users(id) | 생성자 ID | +| created_at | TIMESTAMP | DEFAULT NOW() | 생성일시 | +| is_active | BOOLEAN | DEFAULT TRUE | 활성 상태 | + +**인덱스:** +- `idx_projects_job_no` ON job_no +- `idx_projects_created_by_id` ON created_by_id +- `idx_projects_is_active` ON is_active + +**외래키:** +- created_by_id → users(id) + +--- + +### 3. issues (부적합 사항) +부적합 사항 정보를 저장하는 테이블 + +| 컬럼명 | 타입 | 제약조건 | 설명 | +|--------|------|----------|------| +| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 부적합 사항 고유 ID | +| photo_path | VARCHAR | NULL | 첫 번째 사진 경로 | +| photo_path2 | VARCHAR | NULL | 두 번째 사진 경로 | +| category | ENUM | NOT NULL | 부적합 카테고리 | +| description | TEXT | NOT NULL | 부적합 사항 설명 | +| status | ENUM | DEFAULT 'new' | 처리 상태 | +| reporter_id | INTEGER | FOREIGN KEY → users(id) | 보고자 ID | +| report_date | TIMESTAMP | DEFAULT NOW() | 보고일시 | +| work_hours | FLOAT | DEFAULT 0 | 작업 시간 | +| detail_notes | TEXT | NULL | 상세 메모 | + +**인덱스:** +- `idx_issues_reporter_id` ON reporter_id +- `idx_issues_status` ON status +- `idx_issues_category` ON category +- `idx_issues_report_date` ON report_date + +**ENUM 값:** +- category: 'material_missing', 'design_error', 'incoming_defect', 'inspection_miss' +- status: 'new', 'progress', 'complete' + +**외래키:** +- reporter_id → users(id) + +--- + +### 4. daily_works (일일 공수) +일일 작업 공수 정보를 저장하는 테이블 + +| 컬럼명 | 타입 | 제약조건 | 설명 | +|--------|------|----------|------| +| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 일일 공수 고유 ID | +| date | TIMESTAMP | NOT NULL, INDEX | 작업 날짜 | +| worker_count | INTEGER | NOT NULL | 작업자 수 | +| regular_hours | FLOAT | NOT NULL | 정규 시간 | +| overtime_workers | INTEGER | DEFAULT 0 | 야근 작업자 수 | +| overtime_hours | FLOAT | DEFAULT 0 | 야근 시간 | +| overtime_total | FLOAT | DEFAULT 0 | 야근 총 시간 | +| total_hours | FLOAT | NOT NULL | 총 작업 시간 | +| created_by_id | INTEGER | FOREIGN KEY → users(id) | 생성자 ID | +| created_at | TIMESTAMP | DEFAULT NOW() | 생성일시 | + +**인덱스:** +- `idx_daily_works_date` ON date +- `idx_daily_works_created_by_id` ON created_by_id + +**외래키:** +- created_by_id → users(id) + +--- + +## 관계도 (Relationships) + +``` +users (1) ←→ (N) projects + ↓ +users (1) ←→ (N) issues + ↓ +users (1) ←→ (N) daily_works +``` + +--- + +## 마이그레이션 파일 목록 + +1. `001_init.sql` - 초기 테이블 생성 +2. `002_add_second_photo.sql` - issues 테이블에 두 번째 사진 필드 추가 +3. `003_update_categories.sql` - 카테고리 업데이트 +4. `004_fix_category_values.sql` - 카테고리 값 수정 +5. `005_recreate_enum_type.sql` - ENUM 타입 재생성 +6. `006_add_projects_table.sql` - projects 테이블 추가 + +--- + +## 기본 데이터 + +### 관리자 계정 +- **Username:** hyungi +- **Password:** djg3-jj34-X3Q3 +- **Role:** admin +- **Full Name:** 관리자 + +--- + +## 데이터베이스 연결 정보 + +- **Host:** localhost (Docker 내부: db) +- **Port:** 16432 (외부), 5432 (내부) +- **Database:** mproject +- **Username:** mproject +- **Password:** mproject2024 + +--- + +## 주의사항 + +1. **비밀번호 암호화:** bcrypt 해시 사용 +2. **시간대:** 모든 TIMESTAMP는 Asia/Seoul (KST) 기준 +3. **파일 업로드:** 사진 파일은 `/app/uploads` 디렉토리에 저장 +4. **소프트 삭제:** projects 테이블은 is_active 필드로 소프트 삭제 구현 +5. **인덱스 최적화:** 자주 조회되는 컬럼에 인덱스 설정 + +--- + +## 백업 및 복구 + +### 백업 +```bash +docker-compose exec db pg_dump -U mproject mproject > backup.sql +``` + +### 복구 +```bash +docker-compose exec -T db psql -U mproject mproject < backup.sql +``` diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..9b27424 --- /dev/null +++ b/PROJECT_OVERVIEW.md @@ -0,0 +1,328 @@ +# M-Project 프로젝트 개요 + +## 📋 프로젝트 소개 +작업보고서 시스템 - 부적합 사항 관리 및 공수 계산을 위한 웹 애플리케이션 + +--- + +## 🏗️ 시스템 아키텍처 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend │ │ Database │ +│ (HTML/JS) │◄──►│ (FastAPI) │◄──►│ (PostgreSQL) │ +│ Port: 16080 │ │ Port: 16000 │ │ Port: 16432 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + ▲ ▲ ▲ + │ │ │ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Nginx │ │ Docker │ │ File Storage │ +│ (Reverse Proxy)│ │ (Container) │ │ (/uploads) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +--- + +## 📁 프로젝트 구조 + +``` +M-Project/ +├── backend/ # FastAPI 백엔드 +│ ├── database/ # 데이터베이스 관련 +│ │ ├── database.py # DB 연결 설정 +│ │ ├── models.py # SQLAlchemy 모델 +│ │ └── schemas.py # Pydantic 스키마 +│ ├── migrations/ # DB 마이그레이션 파일 +│ │ ├── 001_init.sql +│ │ ├── 002_add_second_photo.sql +│ │ ├── 003_update_categories.sql +│ │ ├── 004_fix_category_values.sql +│ │ ├── 005_recreate_enum_type.sql +│ │ └── 006_add_projects_table.sql +│ ├── routers/ # API 라우터 +│ │ ├── auth.py # 인증 관련 API +│ │ ├── projects.py # 프로젝트 관리 API +│ │ ├── issues.py # 부적합 사항 API +│ │ ├── daily_work.py # 일일 공수 API +│ │ └── reports.py # 보고서 API +│ ├── services/ # 비즈니스 로직 +│ │ ├── auth_service.py # 인증 서비스 +│ │ └── file_service.py # 파일 처리 서비스 +│ ├── uploads/ # 업로드된 파일 저장소 +│ ├── main.py # FastAPI 애플리케이션 진입점 +│ ├── requirements.txt # Python 의존성 +│ └── Dockerfile # 백엔드 Docker 설정 +├── frontend/ # 프론트엔드 +│ ├── static/ # 정적 파일 +│ │ ├── css/ # 스타일시트 +│ │ └── js/ # JavaScript 파일 +│ │ ├── api.js # API 통신 +│ │ ├── date-utils.js # 날짜 유틸리티 +│ │ └── image-utils.js # 이미지 처리 +│ ├── admin.html # 관리자 페이지 +│ ├── index.html # 메인 페이지 +│ ├── daily-work.html # 일일 공수 페이지 +│ ├── chart.html # 차트 페이지 +│ └── issue-view.html # 부적합 사항 상세 페이지 +├── nginx/ # Nginx 설정 +│ ├── nginx.conf # Nginx 설정 파일 +│ └── Dockerfile # Nginx Docker 설정 +├── docker-compose.yml # Docker Compose 설정 +├── DATABASE_SCHEMA.md # 데이터베이스 스키마 문서 +├── API_DOCUMENTATION.md # API 문서 +├── PROJECT_OVERVIEW.md # 프로젝트 개요 (이 파일) +└── README.md # 프로젝트 설명 +``` + +--- + +## 🚀 시작하기 + +### 1. 시스템 요구사항 +- Docker & Docker Compose +- Git + +### 2. 프로젝트 클론 및 실행 +```bash +# 프로젝트 클론 +git clone +cd M-Project + +# Docker 컨테이너 실행 +docker-compose up -d + +# 로그 확인 +docker-compose logs -f +``` + +### 3. 접속 정보 +- **프론트엔드:** http://localhost:16080 +- **백엔드 API:** http://localhost:16000 +- **API 문서:** http://localhost:16000/docs +- **데이터베이스:** localhost:16432 + +### 4. 기본 계정 +- **관리자:** hyungi / djg3-jj34-X3Q3 +- **일반 사용자:** inspector1 / pass123, inspector2 / pass456 + +--- + +## 👥 사용자 권한 + +### 관리자 (admin) +- ✅ 프로젝트 생성/수정/삭제 +- ✅ 사용자 계정 관리 +- ✅ 모든 부적합 사항 조회/수정 +- ✅ 보고서 생성 +- ✅ 시스템 설정 + +### 일반 사용자 (user) +- ✅ 부적합 사항 등록 +- ✅ 자신이 등록한 부적합 사항 수정 +- ✅ 일일 공수 입력 +- ✅ 보고서 조회 +- ❌ 프로젝트 관리 +- ❌ 사용자 관리 + +--- + +## 🔧 주요 기능 + +### 1. 프로젝트 관리 (신규 추가) +- **Job No.** 기반 프로젝트 생성 +- 관리자만 접근 가능한 프로젝트 생성 배너 +- 프로젝트 목록 조회 및 관리 + +### 2. 부적합 사항 관리 +- 사진 업로드 (최대 2장) +- 카테고리별 분류 + - 자재누락 (material_missing) + - 설계미스 (design_error) + - 입고자재 불량 (incoming_defect) + - 검사미스 (inspection_miss) +- 상태 관리 (신규/진행중/완료) +- 작업 시간 기록 + +### 3. 일일 공수 관리 +- 작업자 수 입력 +- 정규/야근 시간 계산 +- 날짜별 공수 추적 + +### 4. 보고서 생성 +- 기간별 통계 +- 카테고리별 분석 +- 작업 시간 집계 +- 인쇄 기능 + +--- + +## 🛠️ 개발 가이드 + +### 백엔드 개발 +```bash +# 백엔드 컨테이너 접속 +docker-compose exec backend bash + +# 의존성 설치 +pip install -r requirements.txt + +# 개발 서버 실행 +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +### 프론트엔드 개발 +- 정적 파일은 `frontend/` 디렉토리에 위치 +- Nginx를 통해 서빙됨 +- 실시간 수정 가능 (볼륨 마운트) + +### 데이터베이스 관리 +```bash +# DB 접속 +docker-compose exec db psql -U mproject -d mproject + +# 마이그레이션 실행 +docker-compose exec db psql -U mproject -d mproject -f /docker-entrypoint-initdb.d/새파일.sql + +# 백업 +docker-compose exec db pg_dump -U mproject mproject > backup.sql + +# 복구 +docker-compose exec -T db psql -U mproject mproject < backup.sql +``` + +--- + +## 🔍 트러블슈팅 + +### 1. 백엔드가 시작되지 않는 경우 +```bash +# 로그 확인 +docker-compose logs backend + +# 컨테이너 재시작 +docker-compose restart backend +``` + +### 2. 데이터베이스 연결 오류 +```bash +# DB 상태 확인 +docker-compose ps db + +# DB 로그 확인 +docker-compose logs db + +# DB 재시작 +docker-compose restart db +``` + +### 3. 마이그레이션 오류 +```bash +# 수동 마이그레이션 실행 +docker-compose exec db psql -U mproject -d mproject -f /docker-entrypoint-initdb.d/파일명.sql +``` + +### 4. 파일 업로드 오류 +```bash +# uploads 디렉토리 권한 확인 +docker-compose exec backend ls -la /app/uploads + +# 권한 수정 +docker-compose exec backend chmod 755 /app/uploads +``` + +--- + +## 📊 모니터링 + +### 헬스체크 +```bash +# 백엔드 상태 확인 +curl http://localhost:16000/api/health + +# 프론트엔드 상태 확인 +curl http://localhost:16080 +``` + +### 로그 모니터링 +```bash +# 전체 로그 +docker-compose logs -f + +# 특정 서비스 로그 +docker-compose logs -f backend +docker-compose logs -f db +docker-compose logs -f nginx +``` + +--- + +## 🔐 보안 고려사항 + +### 1. 인증 +- JWT 토큰 기반 인증 +- 토큰 만료 시간: 7일 +- bcrypt 해시를 사용한 비밀번호 암호화 + +### 2. 권한 관리 +- 역할 기반 접근 제어 (RBAC) +- API 레벨에서 권한 검증 +- 프론트엔드 UI 레벨에서 권한별 표시/숨김 + +### 3. 데이터 보호 +- SQL 인젝션 방지 (SQLAlchemy ORM 사용) +- XSS 방지 (입력값 검증) +- 파일 업로드 검증 + +--- + +## 📈 성능 최적화 + +### 1. 데이터베이스 +- 자주 조회되는 컬럼에 인덱스 설정 +- 페이지네이션 구현 +- 연결 풀링 사용 + +### 2. API +- 응답 캐싱 +- 압축 전송 +- 비동기 처리 + +### 3. 프론트엔드 +- 이미지 최적화 +- 지연 로딩 +- 로컬 스토리지 활용 + +--- + +## 🚀 배포 가이드 + +### 프로덕션 환경 설정 +1. 환경 변수 수정 (`docker-compose.yml`) + - `SECRET_KEY` 변경 + - `ADMIN_PASSWORD` 변경 + - 데이터베이스 비밀번호 변경 + +2. SSL 인증서 설정 +3. 방화벽 설정 +4. 백업 스케줄 설정 + +### 업데이트 절차 +1. 코드 업데이트 +2. 데이터베이스 백업 +3. 마이그레이션 실행 +4. 컨테이너 재시작 +5. 헬스체크 확인 + +--- + +## 📞 지원 및 문의 + +### 개발자 정보 +- **개발자:** M-Project Team +- **이메일:** support@m-project.com +- **문서 업데이트:** 2024-10-24 + +### 버전 정보 +- **현재 버전:** 1.0.0 +- **최종 업데이트:** 2024-10-24 +- **호환성:** Docker 20.10+, Python 3.11+ diff --git a/README.md b/README.md index 49741c0..688a78b 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,12 @@ ## 🚀 빠른 시작 -1. 웹 서버 실행: +### Docker를 사용한 실행 (권장) + +1. Docker 컨테이너 실행: ```bash cd M-Project -python3 -m http.server 16080 +docker-compose up -d ``` 2. 브라우저에서 접속: @@ -16,26 +18,43 @@ http://localhost:16080 ``` 3. 로그인: +- **관리자**: `hyungi` / `djg3-jj34-X3Q3` - 검사자1: `inspector1` / `pass123` - 검사자2: `inspector2` / `pass456` -- 관리자: `admin` / `admin123` + +### 간단한 웹 서버 실행 (개발용) + +```bash +cd M-Project +python3 -m http.server 16080 +``` ## 📱 주요 기능 -### 1. 부적합 등록 (모바일 최적화) -- 사진 촬영 또는 업로드 -- 위치, 카테고리, 설명 입력 -- 긴급도 선택 (낮음/보통/높음) +### 1. 프로젝트 관리 (신규 추가 ✨) +- **Job No.** 기반 프로젝트 생성 +- 관리자 전용 프로젝트 생성 배너 +- 프로젝트 목록 조회 및 관리 -### 2. 목록 관리 +### 2. 부적합 등록 (모바일 최적화) +- 사진 촬영 또는 업로드 (최대 2장) +- 카테고리별 분류 (자재누락, 설계미스, 입고자재 불량, 검사미스) +- 상세 설명 입력 + +### 3. 목록 관리 - 등록된 부적합 사항 조회 - 작업 시간 입력 - 상태 변경 (신규→진행중→완료) - 추가 메모 작성 -### 3. 보고서 +### 4. 일일 공수 관리 +- 작업자 수 및 정규/야근 시간 입력 +- 날짜별 공수 추적 +- 자동 계산 기능 + +### 5. 보고서 - 작업 기간 및 총 공수 자동 계산 -- 긴급도별 통계 +- 카테고리별 통계 - 부적합 사항 상세 내역 - 인쇄 가능한 형식 @@ -50,10 +69,13 @@ M-Project/ ## 🛠️ 기술 스택 +- **Backend**: FastAPI (Python), SQLAlchemy, PostgreSQL - **Frontend**: HTML5, Tailwind CSS, JavaScript (Vanilla) -- **Storage**: LocalStorage (브라우저 로컬 저장) +- **Database**: PostgreSQL 15 +- **Container**: Docker & Docker Compose +- **Web Server**: Nginx +- **Authentication**: JWT (JSON Web Token) - **Icons**: Font Awesome -- **Charts**: Chart.js ## 📋 데이터 구조 @@ -79,16 +101,23 @@ M-Project/ ## ⚡ 특징 - **모바일 우선**: 현장에서 스마트폰으로 쉽게 입력 -- **오프라인 작동**: 인터넷 연결 없이도 사용 가능 -- **간단한 설치**: 별도의 서버나 데이터베이스 불필요 -- **즉시 사용**: 웹 서버만 실행하면 바로 사용 +- **실시간 데이터**: PostgreSQL 데이터베이스로 실시간 동기화 +- **권한 관리**: 관리자/일반 사용자 역할 기반 접근 제어 +- **Docker 기반**: 간편한 배포 및 확장성 +- **RESTful API**: 표준 API 설계로 확장 가능 -## 🔒 보안 주의사항 +## 🔒 보안 기능 -현재 버전은 프로토타입으로: -- 사용자 인증이 간단함 (하드코딩된 계정) -- 데이터가 브라우저에만 저장됨 -- 실제 운영 환경에서는 백엔드 서버 구축 필요 +- **JWT 인증**: 토큰 기반 보안 인증 시스템 +- **비밀번호 암호화**: bcrypt 해시를 사용한 안전한 비밀번호 저장 +- **권한 기반 접근**: 역할별 API 접근 제어 +- **SQL 인젝션 방지**: SQLAlchemy ORM 사용 + +## 📚 문서 + +- **[DATABASE_SCHEMA.md](DATABASE_SCHEMA.md)**: 데이터베이스 스키마 상세 문서 +- **[API_DOCUMENTATION.md](API_DOCUMENTATION.md)**: API 엔드포인트 문서 +- **[PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md)**: 프로젝트 전체 구조 및 가이드 ## 📝 라이선스 @@ -96,4 +125,5 @@ M-Project/ --- -작성일: 2025-09-17 +**최종 업데이트**: 2024-10-24 +**버전**: 1.0.0 (프로젝트 관리 기능 추가) diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index c495f2e495d633f9b1bff956cb3aede71d7094df..2533e85e9984edbfb44c80c1060bf0a5148f49a7 100644 GIT binary patch delta 773 zcma)3J#5oZ5Pr`;iPI#cHA&+%G#Ce(LZJvz5h@{CfyBh$h$4X%*^eY{+@R;Ch>d{- zu_U^V3=u;RbYuX;#=^o75E5XaiVarQPTXs#P&-i0@15?u@9y1?UmK5e$*0NW2tiqU zzgd6NP3CB`TdJA9UePIWiK{jD`{_!W5|;2XB2Gwey!*Tq{|mF-Zx>~Q8PK6wn5UL( z^d{gI&C)z<&|DtTFmI=3DevF>=pOvo9=tx}v6}-hE0xtrX7mc+p>=S}>PuzEMC;Jb zzJ`cdeM-nW-l4czCbMRwSoSdxT;XC%U+-jQt*x3VbTr7FCV zi(07N;Vq9jFTTpFl&0XiI;V>2)9_H`XU0Ujj_RlsMtv3p)f(es%g+d(5}YNJi7Hfh ztF^*QXr6>qkx6R7waAqVTS2=b6LnT?uGDR(&YUGagBf#p15xWIe)q}uM7q-$#Lab?tw4?(jPnu5kq2@VfHKSssfv>gm_>~h(^yg@TUeF>9lRRs zY!DHpl&YK}mCXcV7l|@4q_StJfY>0ADn7Y_xtx(@@&{(+&9W>djEsDfC$XBaDgfow zC!c053K0OZiUfg#CetnU+|=UY#Prl6P_V3IC{hKYy;R1^S&=>{w<3id%xgs5qDv$>i6pG|0|74eAH3D)hCd+U*Fe*-t<1tD>8TL+7HLi1z^P;o(sYX>K0YroH#I)~7IRT)9-1G2 saoFVMrN6*B^HasFgZu1Y1-3k(F%b%7 diff --git a/backend/database/__pycache__/models.cpython-311.pyc b/backend/database/__pycache__/models.cpython-311.pyc index 093e30dc60dc7133e7b876625ef171d0a0bb75e4..930b851ed7ab387a899a95716dc09931044578ec 100644 GIT binary patch delta 2463 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 L5I>X_RCMw$WQ|fZ diff --git a/backend/database/__pycache__/schemas.cpython-311.pyc b/backend/database/__pycache__/schemas.cpython-311.pyc index 597b32ceeaca7c7db6268a078e8996bd6dbc26e4..ea07441e366a16413feac0ea3c71f874c8fd46fb 100644 GIT binary patch delta 2513 zcmZuyZERCj7{2HBw!LlF?`{3sM`<@l8K5vk3S>+dVNl%W1c?=y-R>P5`{=kG5kjDq zAT0Qi!8?=r1EYT?L_$CvUBZvqXafIS{Ii86MiQg%d%9Y-?(Uvv?>WzV z&UxSGocH#x-G3giePOkl1$-ude$s#0aoOe-t8`zlJtMMem=@QkMPZMiY&tI}o7L)R zogjRPE&s%WoNGdEi^^7TYdH5ja$8Hd5a(V%Zd(Z#R-&&7k?nJ|MMTV*ZaoqYSy(eL zb2zBz;s({I%D$xl1VNP)X(#p+9Zu3ik1cG=nMbwcF|~KBdq9bp;G(V8(uN+IfnWzD zB)9!#G6|ZorJCUfTZ4Z#SeMy1Ay2s{ty!rK-x-c*z=G|QiWp7dON7a)BiK)infH-N z&x{ zFg0_|vNx4Fp&lGn5@Tw+wwpHHL(m5QI-arc@b+Ur;)3U#8yk7XJBaT<$m#orl8G^` zlLlTTC@08I;)=9WpMcg(-Gz^~5ZtQ}%5AG?ny;)AoUu6{*~TU3Lpdn$BE zktuYw49|xrp`r$gnWrPhZRl-W=4~N{Uy3rk6xL}GuNk)d6SpfON??N;cf%HnK4;O? z(WEw}YIwvsxzN!l=>peHI`QPSPWZ0U2WgCU+8s%}iB3nbY1;{$1W|;DL$i`?Bd`;2 z_4qj-q;B!-U#Bh?a5Gt4YCTz91f8(K;<*R7=5N?*$)4_!!u;dMg#VenKP!(Vk5Y^wOf{|?RI6?u{@uFEO1y7)IZMJB;1=&T%KDGX+psPz-)J53xxnhcUFe?d-D z=5WMw#c_hh_zryWQ&+tHTHfW`6u$#`mYnmuo*|Rq9PU~HgWf~z3}5s&?>A-I2(d|m zEd_1hci#s6c^sMmyZ>3%1-twqGY^a4A`JSkvNX^618XC^66iEvp!o!o@V%`9?gql_ z0(ba3aK-RGkyTuPOJovE!Of~Jb_spnvs|RXae{Mjxq86DzfU}!OEk&;AyEWZv8DRp zMxX)OYub!7)E=pWt2HOn{MU{DI`B^#RTVE3e)sqpa^1PWTnmlo!{g>b@l6~wjel!h zXg!g44kwipLu#}3F?Q(-l=@Z@gn1ofY*whx7T;T$+A49Kj@8c#r7&SypwY!5WriQu ztV_#m&Ad|BR8GfoTAr4alUBvVuZTHe`y3m{MWrpX)t%^#0bOw zNI?YZ?;#>0pA1R@v$yJ@mmY%99->&`Lr+0HCp3h#D4e9!Ou+r8g&&fSl} zU+wB+MUgpr%_QDUoL83AGJcI;ubblufmQyPXfzFQdUT`HHtK|Zr5x_a4$-jDngBo5 z2(OC}(1P$$&>*g7u{YOF(irgnOvW6(y3Xv1zN9YO4?7LF8L8rPhseL^~as6}aJUg)R3K>4vsa zA9(?3*$3UC9j=si^_WgmFxaC_8ryKN5PV!QMbrLIJi(CB;kgdZaNgvM;#HpGp3;%O z;;O;Z;0iQ&Y8x?A&S{xbDQnuaMkbA%bTBz6j~HXr!qr&4@Ff_9mmb|0r&Ao(afA!e zN}+4;+vC>HMM$JO+3aSS}gHAB-Z{^T(mB(jR1V7KJ6NF4(B@$r30L7vWxIk|fw21HtFu z@`qqMI7CvgR!RB+-T98$sa{!1xf$_ diff --git a/backend/database/models.py b/backend/database/models.py index 77943b2..c1a17a7 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum +from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Float, Boolean, Text, ForeignKey, Enum from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime, timezone, timedelta @@ -42,6 +42,7 @@ class User(Base): # Relationships issues = relationship("Issue", back_populates="reporter") daily_works = relationship("DailyWork", back_populates="created_by") + projects = relationship("Project", back_populates="created_by") class Issue(Base): __tablename__ = "issues" @@ -53,12 +54,28 @@ class Issue(Base): description = Column(Text, nullable=False) status = Column(Enum(IssueStatus), default=IssueStatus.new) reporter_id = Column(Integer, ForeignKey("users.id")) + project_id = Column(BigInteger, ForeignKey("projects.id")) report_date = Column(DateTime, default=get_kst_now) work_hours = Column(Float, default=0) detail_notes = Column(Text) # Relationships reporter = relationship("User", back_populates="issues") + project = relationship("Project", back_populates="issues") + +class Project(Base): + __tablename__ = "projects" + + id = Column(BigInteger, primary_key=True, index=True) + job_no = Column(String, unique=True, nullable=False, index=True) + project_name = Column(String, nullable=False) + created_by_id = Column(Integer, ForeignKey("users.id")) + created_at = Column(DateTime, default=get_kst_now) + is_active = Column(Boolean, default=True) + + # Relationships + created_by = relationship("User", back_populates="projects") + issues = relationship("Issue", back_populates="project") class DailyWork(Base): __tablename__ = "daily_works" diff --git a/backend/database/schemas.py b/backend/database/schemas.py index d5d3014..a26dad7 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -62,6 +62,7 @@ class LoginRequest(BaseModel): class IssueBase(BaseModel): category: IssueCategory description: str + project_id: Optional[int] = None class IssueCreate(IssueBase): photo: Optional[str] = None # Base64 encoded image @@ -70,6 +71,7 @@ class IssueCreate(IssueBase): class IssueUpdate(BaseModel): category: Optional[IssueCategory] = None description: Optional[str] = None + project_id: Optional[int] = None work_hours: Optional[float] = None detail_notes: Optional[str] = None status: Optional[IssueStatus] = None @@ -83,6 +85,8 @@ class Issue(IssueBase): status: IssueStatus reporter_id: int reporter: User + project_id: Optional[int] = None + # project: Optional['Project'] = None # 순환 참조 방지를 위해 제거 report_date: datetime work_hours: float detail_notes: Optional[str] = None @@ -90,6 +94,29 @@ class Issue(IssueBase): class Config: from_attributes = True +# Project schemas +class ProjectBase(BaseModel): + job_no: str = Field(..., min_length=1, max_length=50) + project_name: str = Field(..., min_length=1, max_length=200) + +class ProjectCreate(ProjectBase): + pass + +class ProjectUpdate(BaseModel): + project_name: Optional[str] = Field(None, min_length=1, max_length=200) + is_active: Optional[bool] = None + +class Project(ProjectBase): + id: int + created_by_id: int + created_by: User + created_at: datetime + is_active: bool + # issues: Optional[List['Issue']] = None # 순환 참조 방지를 위해 제거 + + class Config: + from_attributes = True + # Daily Work schemas class DailyWorkBase(BaseModel): date: datetime diff --git a/backend/main.py b/backend/main.py index ba57295..d28307f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,7 +5,7 @@ import uvicorn from database.database import engine, get_db from database.models import Base -from routers import auth, issues, daily_work, reports +from routers import auth, issues, daily_work, reports, projects from services.auth_service import create_admin_user # 데이터베이스 테이블 생성 @@ -34,6 +34,7 @@ app.include_router(auth.router) app.include_router(issues.router) app.include_router(daily_work.router) app.include_router(reports.router) +app.include_router(projects.router) # 시작 시 관리자 계정 생성 @app.on_event("startup") diff --git a/backend/migrations/006_add_projects_table.sql b/backend/migrations/006_add_projects_table.sql new file mode 100644 index 0000000..1746f87 --- /dev/null +++ b/backend/migrations/006_add_projects_table.sql @@ -0,0 +1,14 @@ +-- 프로젝트 테이블 생성 +CREATE TABLE IF NOT EXISTS projects ( + id SERIAL PRIMARY KEY, + job_no VARCHAR(50) UNIQUE NOT NULL, + project_name VARCHAR(200) NOT NULL, + created_by_id INTEGER REFERENCES users(id), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE +); + +-- 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_projects_job_no ON projects(job_no); +CREATE INDEX IF NOT EXISTS idx_projects_created_by_id ON projects(created_by_id); +CREATE INDEX IF NOT EXISTS idx_projects_is_active ON projects(is_active); diff --git a/backend/migrations/007_add_project_id_to_issues.sql b/backend/migrations/007_add_project_id_to_issues.sql new file mode 100644 index 0000000..725354b --- /dev/null +++ b/backend/migrations/007_add_project_id_to_issues.sql @@ -0,0 +1,15 @@ +-- 부적합 사항 테이블에 프로젝트 ID 컬럼 추가 +ALTER TABLE issues ADD COLUMN project_id INTEGER; + +-- 외래키 제약조건 추가 +ALTER TABLE issues ADD CONSTRAINT fk_issues_project_id + FOREIGN KEY (project_id) REFERENCES projects(id); + +-- 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_issues_project_id ON issues(project_id); + +-- 기존 부적합 사항들을 첫 번째 프로젝트로 할당 (있는 경우) +UPDATE issues +SET project_id = (SELECT id FROM projects ORDER BY created_at LIMIT 1) +WHERE project_id IS NULL +AND EXISTS (SELECT 1 FROM projects LIMIT 1); diff --git a/backend/migrations/008_fix_project_id_bigint.sql b/backend/migrations/008_fix_project_id_bigint.sql new file mode 100644 index 0000000..5401534 --- /dev/null +++ b/backend/migrations/008_fix_project_id_bigint.sql @@ -0,0 +1,13 @@ +-- project_id 컬럼을 BIGINT로 변경 +ALTER TABLE issues ALTER COLUMN project_id TYPE BIGINT; + +-- projects 테이블의 id도 BIGINT로 변경 (일관성을 위해) +ALTER TABLE projects ALTER COLUMN id TYPE BIGINT; + +-- 외래키 제약조건 재생성 (타입 변경으로 인해 필요) +ALTER TABLE issues DROP CONSTRAINT IF EXISTS fk_issues_project_id; +ALTER TABLE issues ADD CONSTRAINT fk_issues_project_id + FOREIGN KEY (project_id) REFERENCES projects(id); + +-- 다른 테이블들도 확인하여 project_id 참조하는 곳이 있으면 수정 +-- (현재는 issues 테이블만 project_id를 가지고 있음) diff --git a/backend/routers/__pycache__/projects.cpython-311.pyc b/backend/routers/__pycache__/projects.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1005bddd3985fd004e25b70811858f784b012454 GIT binary patch literal 6392 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 Z5p8-Jy*R%LOOX+4^I*B;))vj%{tfDauDSpK literal 0 HcmV?d00001 diff --git a/backend/routers/projects.py b/backend/routers/projects.py new file mode 100644 index 0000000..7896290 --- /dev/null +++ b/backend/routers/projects.py @@ -0,0 +1,126 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from database.database import get_db +from database.models import Project, User, UserRole +from database.schemas import ProjectCreate, ProjectUpdate, Project as ProjectSchema +from routers.auth import get_current_user + +router = APIRouter( + prefix="/api/projects", + tags=["projects"] +) + +def check_admin_permission(current_user: User = Depends(get_current_user)): + """관리자 권한 확인""" + if current_user.role != UserRole.admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="관리자 권한이 필요합니다." + ) + return current_user + +@router.post("/", response_model=ProjectSchema) +async def create_project( + project: ProjectCreate, + db: Session = Depends(get_db), + current_user: User = Depends(check_admin_permission) +): + """프로젝트 생성 (관리자만)""" + # Job No. 중복 확인 + existing_project = db.query(Project).filter(Project.job_no == project.job_no).first() + if existing_project: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="이미 존재하는 Job No.입니다." + ) + + # 프로젝트 생성 + db_project = Project( + job_no=project.job_no, + project_name=project.project_name, + created_by_id=current_user.id + ) + + db.add(db_project) + db.commit() + db.refresh(db_project) + + return db_project + +@router.get("/", response_model=List[ProjectSchema]) +async def get_projects( + skip: int = 0, + limit: int = 100, + active_only: bool = True, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """프로젝트 목록 조회""" + query = db.query(Project) + + if active_only: + query = query.filter(Project.is_active == True) + + projects = query.offset(skip).limit(limit).all() + return projects + +@router.get("/{project_id}", response_model=ProjectSchema) +async def get_project( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """특정 프로젝트 조회""" + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="프로젝트를 찾을 수 없습니다." + ) + return project + +@router.put("/{project_id}", response_model=ProjectSchema) +async def update_project( + project_id: int, + project_update: ProjectUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(check_admin_permission) +): + """프로젝트 수정 (관리자만)""" + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="프로젝트를 찾을 수 없습니다." + ) + + # 업데이트할 필드만 수정 + update_data = project_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(project, field, value) + + db.commit() + db.refresh(project) + + return project + +@router.delete("/{project_id}") +async def delete_project( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(check_admin_permission) +): + """프로젝트 삭제 (비활성화) (관리자만)""" + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="프로젝트를 찾을 수 없습니다." + ) + + # 실제 삭제 대신 비활성화 + project.is_active = False + db.commit() + + return {"message": "프로젝트가 삭제되었습니다."} diff --git a/frontend/create-project-api.html b/frontend/create-project-api.html new file mode 100644 index 0000000..9525173 --- /dev/null +++ b/frontend/create-project-api.html @@ -0,0 +1,193 @@ + + + + + + API 프로젝트 생성 - M Project + + + + + +
+
+

+ API 프로젝트 생성 +

+ +
+
+

🎯 작업 내용

+

+ 백엔드 API를 통해 TKR-25009R M Project를 생성합니다.
+ 데이터베이스에 실제 프로젝트 레코드가 생성됩니다. +

+
+ +
+ +
+ + + + + API 데이터 수정 도구로 이동 + + + + 디버그 도구로 이동 + +
+
+
+ + + + diff --git a/frontend/daily-work.html b/frontend/daily-work.html index 2feb851..8e9aa7f 100644 --- a/frontend/daily-work.html +++ b/frontend/daily-work.html @@ -98,11 +98,14 @@

- 작업보고서 시스템 + 작업보고서

- +
+ + +
@@ -126,6 +129,9 @@ + @@ -152,65 +158,34 @@ id="workDate" class="input-field w-full px-4 py-3 rounded-lg text-lg" required + onchange="loadExistingData()" > - +
- - -

기본 근무시간: 8시간/인

-
- - -
- - +
`; - container.appendChild(div); - }); + + return div; } // 수정 상태 표시 @@ -1013,17 +1310,25 @@ } try { - // 작업 시간만 업데이트 + // 작업 시간 업데이트 및 검토 완료 상태로 변경 await IssuesAPI.update(issueId, { - work_hours: workHours + work_hours: workHours, + status: 'complete' // 검토 완료 상태로 변경 }); - // 성공 시 데이터 다시 로드 - await loadIssues(); + // 로컬 데이터도 업데이트 + const issue = issues.find(i => i.id === issueId); + if (issue) { + issue.work_hours = workHours; + issue.status = 'complete'; + issue.reviewed_at = new Date().toISOString(); // 검토 완료 시간 기록 + } + + // 성공 시 목록 다시 표시 displayIssueList(); // 성공 메시지 - showToastMessage(`${workHours}시간 저장 완료!`, 'success'); + showToastMessage(`${workHours}시간 저장 및 검토 완료!`, 'success'); } catch (error) { alert(error.message || '저장에 실패했습니다.'); @@ -1151,39 +1456,81 @@ async function generateReport() { const container = document.getElementById('reportContent'); + // 선택된 프로젝트 가져오기 + const selectedProjectId = document.getElementById('reportProjectFilter').value; + + // 프로젝트별 필터링된 부적합 사항 + let filteredIssues = issues; + if (selectedProjectId) { + filteredIssues = issues.filter(issue => { + const issueProjectId = issue.project_id || issue.projectId; + return issueProjectId && (issueProjectId == selectedProjectId || issueProjectId.toString() === selectedProjectId.toString()); + }); + } + // 날짜 범위 계산 - const dates = issues.map(i => new Date(i.report_date)); + const dates = filteredIssues.map(i => new Date(i.report_date)); const startDate = dates.length > 0 ? new Date(Math.min(...dates)) : new Date(); const endDate = new Date(); - // 일일 공수 데이터 가져오기 + // 프로젝트별 일일 공수 데이터 계산 let dailyWorkTotal = 0; - try { - const dailyWorks = await DailyWorkAPI.getAll({ - start_date: startDate.toISOString().split('T')[0], - end_date: endDate.toISOString().split('T')[0] + const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]'); + + console.log('일일공수 데이터:', dailyWorkData); + console.log('선택된 프로젝트 ID:', selectedProjectId); + + if (selectedProjectId) { + // 선택된 프로젝트의 일일 공수만 합계 + dailyWorkData.forEach(dayWork => { + console.log('일일공수 항목:', dayWork); + if (dayWork.projects) { + dayWork.projects.forEach(project => { + console.log('프로젝트:', project, '매칭 확인:', project.projectId == selectedProjectId); + if (project.projectId == selectedProjectId || project.projectId.toString() === selectedProjectId.toString()) { + dailyWorkTotal += project.hours || 0; + console.log('시간 추가:', project.hours, '누적:', dailyWorkTotal); + } + }); + } + }); + } else { + // 전체 프로젝트의 일일 공수 합계 + dailyWorkData.forEach(dayWork => { + console.log('전체 일일공수 항목:', dayWork); + dailyWorkTotal += dayWork.totalHours || 0; }); - dailyWorkTotal = dailyWorks.reduce((sum, work) => sum + work.total_hours, 0); - } catch (error) { - console.error('일일 공수 데이터 로드 실패:', error); } - // 부적합 사항 해결 시간 계산 - const issueHours = issues.reduce((sum, issue) => sum + issue.work_hours, 0); + console.log('최종 일일공수 합계:', dailyWorkTotal); + + // 부적합 사항 해결 시간 계산 (필터링된 이슈만) + const issueHours = filteredIssues.reduce((sum, issue) => sum + (issue.work_hours || 0), 0); const categoryCount = {}; - issues.forEach(issue => { + filteredIssues.forEach(issue => { categoryCount[issue.category] = (categoryCount[issue.category] || 0) + 1; }); // 부적합 시간 비율 계산 const issuePercentage = dailyWorkTotal > 0 ? ((issueHours / dailyWorkTotal) * 100).toFixed(1) : 0; + // 선택된 프로젝트 정보 + let projectInfo = '전체 프로젝트'; + if (selectedProjectId) { + const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]'); + const selectedProject = projects.find(p => p.id == selectedProjectId); + if (selectedProject) { + projectInfo = `${selectedProject.jobNo} - ${selectedProject.projectName}`; + } + } + container.innerHTML = `
-

작업 보고서

+

작업 보고서

+

${projectInfo}

@@ -1254,7 +1601,7 @@

부적합 사항 상세

- ${issues.map(issue => { + ${filteredIssues.map(issue => { const categoryNames = { material_missing: '자재누락', design_error: '설계미스', diff --git a/frontend/issue-view.html b/frontend/issue-view.html index 73722a9..bd090b9 100644 --- a/frontend/issue-view.html +++ b/frontend/issue-view.html @@ -45,41 +45,72 @@ -webkit-line-clamp: 2; -webkit-box-orient: vertical; } + + .nav-link { + color: #6b7280; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.2s; + text-decoration: none; + } + + .nav-link:hover { + background-color: #f3f4f6; + color: #3b82f6; + } + + .nav-link.active { + background-color: #3b82f6; + color: white; + } - -
+
+
+ + + + diff --git a/frontend/project-management.html b/frontend/project-management.html new file mode 100644 index 0000000..8ecf77e --- /dev/null +++ b/frontend/project-management.html @@ -0,0 +1,350 @@ + + + + + + 프로젝트 관리 - 작업보고서 시스템 + + + + + + +
+
+
+

+ 프로젝트 관리 +

+ +
+
+
+ + +
+ +
+

+ 새 프로젝트 생성 +

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+
+

프로젝트 목록

+ +
+ +
+ +
+
+
+ + + + diff --git a/frontend/static/js/api.js b/frontend/static/js/api.js index e4da282..844eb8b 100644 --- a/frontend/static/js/api.js +++ b/frontend/static/js/api.js @@ -239,3 +239,27 @@ function checkAdminAuth() { } return user; } + +// 프로젝트 API +const ProjectsAPI = { + getAll: (activeOnly = false) => { + const params = activeOnly ? '?active_only=true' : ''; + return apiRequest(`/projects${params}`); + }, + + get: (id) => apiRequest(`/projects/${id}`), + + create: (projectData) => apiRequest('/projects', { + method: 'POST', + body: JSON.stringify(projectData) + }), + + update: (id, projectData) => apiRequest(`/projects/${id}`, { + method: 'PUT', + body: JSON.stringify(projectData) + }), + + delete: (id) => apiRequest(`/projects/${id}`, { + method: 'DELETE' + }) +};