docs: 프로젝트 문서화 및 개발 가이드 추가
- 데이터베이스 스키마 및 변경 로그 문서화 - 신규 페이지 개발 가이드 작성 - 모듈 아키텍처 설계 문서 추가 - 성능 최적화 전략 문서화 - 리팩토링 계획 및 진행 상황 정리 Documentation: - DATABASE_SCHEMA.md: 전체 DB 스키마 구조 - DB_CHANGE_LOG.md: 마이그레이션 변경 이력 - DEVELOPMENT_GUIDE.md: 신규 기능 개발 표준 - MODULE_ARCHITECTURE.md: 프론트엔드 모듈 구조 - PERFORMANCE_OPTIMIZATION.md: 성능 최적화 가이드 - REFACTORING_PLAN.md: 리팩토링 진행 상황 Test Files: - app.html, app.js: SPA 테스트 파일 - test_api.html: API 테스트 페이지
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
## 개요
|
||||
작업보고서 시스템의 PostgreSQL 데이터베이스 스키마 정의
|
||||
|
||||
**최종 업데이트:** 2025-10-25
|
||||
**데이터베이스 버전:** PostgreSQL 15
|
||||
**스키마 버전:** 1.4 (마이그레이션 009까지 적용)
|
||||
|
||||
---
|
||||
|
||||
## 테이블 구조
|
||||
@@ -13,19 +17,22 @@
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| 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) |
|
||||
| username | VARCHAR(50) | UNIQUE, NOT NULL | 로그인 아이디 |
|
||||
| hashed_password | VARCHAR(255) | NOT NULL | 암호화된 비밀번호 (bcrypt) |
|
||||
| full_name | VARCHAR(100) | NULL | 사용자 실명 |
|
||||
| role | userrole | DEFAULT 'user' | 사용자 권한 |
|
||||
| is_active | BOOLEAN | DEFAULT TRUE | 계정 활성화 상태 |
|
||||
| created_at | TIMESTAMP | DEFAULT NOW() | 계정 생성일시 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 계정 생성일시 |
|
||||
|
||||
**인덱스:**
|
||||
- `idx_users_username` ON username
|
||||
- `idx_users_role` ON role
|
||||
- `users_pkey` PRIMARY KEY (id)
|
||||
- `users_username_key` UNIQUE (username)
|
||||
|
||||
**ENUM 값:**
|
||||
- role: 'admin', 'user'
|
||||
**참조되는 테이블:**
|
||||
- daily_works.created_by_id
|
||||
- issues.reporter_id
|
||||
- project_daily_works.created_by_id
|
||||
- projects.created_by_id
|
||||
|
||||
---
|
||||
|
||||
@@ -34,20 +41,26 @@
|
||||
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 프로젝트 고유 ID |
|
||||
| job_no | VARCHAR(50) | UNIQUE, NOT NULL, INDEX | Job 번호 |
|
||||
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 프로젝트 고유 ID |
|
||||
| job_no | VARCHAR(50) | UNIQUE, NOT NULL | Job 번호 |
|
||||
| project_name | VARCHAR(200) | NOT NULL | 프로젝트 이름 |
|
||||
| created_by_id | INTEGER | FOREIGN KEY → users(id) | 생성자 ID |
|
||||
| created_at | TIMESTAMP | DEFAULT NOW() | 생성일시 |
|
||||
| created_at | TIMESTAMP WITH TIME ZONE | 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
|
||||
- `projects_pkey` PRIMARY KEY (id)
|
||||
- `projects_job_no_key` UNIQUE (job_no)
|
||||
- `idx_projects_job_no` (job_no)
|
||||
- `idx_projects_created_by_id` (created_by_id)
|
||||
- `idx_projects_is_active` (is_active)
|
||||
|
||||
**외래키:**
|
||||
- created_by_id → users(id)
|
||||
- `projects_created_by_id_fkey` created_by_id → users(id)
|
||||
|
||||
**참조되는 테이블:**
|
||||
- issues.project_id
|
||||
- project_daily_works.project_id
|
||||
|
||||
---
|
||||
|
||||
@@ -57,76 +70,163 @@
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 부적합 사항 고유 ID |
|
||||
| photo_path | VARCHAR | NULL | 첫 번째 사진 경로 |
|
||||
| photo_path2 | VARCHAR | NULL | 두 번째 사진 경로 |
|
||||
| category | ENUM | NOT NULL | 부적합 카테고리 |
|
||||
| photo_path | VARCHAR(500) | NULL | 첫 번째 사진 경로 |
|
||||
| photo_path2 | VARCHAR(500) | NULL | 두 번째 사진 경로 |
|
||||
| category | issuecategory | NOT NULL | 부적합 카테고리 |
|
||||
| description | TEXT | NOT NULL | 부적합 사항 설명 |
|
||||
| status | ENUM | DEFAULT 'new' | 처리 상태 |
|
||||
| status | issuestatus | DEFAULT 'new' | 처리 상태 |
|
||||
| reporter_id | INTEGER | FOREIGN KEY → users(id) | 보고자 ID |
|
||||
| report_date | TIMESTAMP | DEFAULT NOW() | 보고일시 |
|
||||
| work_hours | FLOAT | DEFAULT 0 | 작업 시간 |
|
||||
| report_date | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 보고일시 |
|
||||
| work_hours | DOUBLE PRECISION | DEFAULT 0 | 작업 시간 |
|
||||
| detail_notes | TEXT | NULL | 상세 메모 |
|
||||
| project_id | BIGINT | FOREIGN KEY → projects(id) | 프로젝트 ID |
|
||||
|
||||
**인덱스:**
|
||||
- `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'
|
||||
- `issues_pkey` PRIMARY KEY (id)
|
||||
- `idx_issues_category` (category)
|
||||
- `idx_issues_photo_path2` (photo_path2)
|
||||
- `idx_issues_project_id` (project_id)
|
||||
- `idx_issues_reporter_id` (reporter_id)
|
||||
- `idx_issues_status` (status)
|
||||
|
||||
**외래키:**
|
||||
- reporter_id → users(id)
|
||||
- `fk_issues_project_id` project_id → projects(id)
|
||||
- `issues_reporter_id_fkey` reporter_id → users(id)
|
||||
|
||||
---
|
||||
|
||||
### 4. daily_works (일일 공수)
|
||||
일일 작업 공수 정보를 저장하는 테이블
|
||||
전체 일일 작업 공수 정보를 저장하는 테이블 (레거시)
|
||||
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 일일 공수 고유 ID |
|
||||
| date | TIMESTAMP | NOT NULL, INDEX | 작업 날짜 |
|
||||
| date | DATE | NOT NULL, UNIQUE | 작업 날짜 |
|
||||
| worker_count | INTEGER | NOT NULL | 작업자 수 |
|
||||
| regular_hours | FLOAT | NOT NULL | 정규 시간 |
|
||||
| regular_hours | DOUBLE PRECISION | NOT NULL | 정규 시간 |
|
||||
| overtime_workers | INTEGER | DEFAULT 0 | 야근 작업자 수 |
|
||||
| overtime_hours | FLOAT | DEFAULT 0 | 야근 시간 |
|
||||
| overtime_total | FLOAT | DEFAULT 0 | 야근 총 시간 |
|
||||
| total_hours | FLOAT | NOT NULL | 총 작업 시간 |
|
||||
| overtime_hours | DOUBLE PRECISION | DEFAULT 0 | 야근 시간 |
|
||||
| overtime_total | DOUBLE PRECISION | DEFAULT 0 | 야근 총 시간 |
|
||||
| total_hours | DOUBLE PRECISION | NOT NULL | 총 작업 시간 |
|
||||
| created_by_id | INTEGER | FOREIGN KEY → users(id) | 생성자 ID |
|
||||
| created_at | TIMESTAMP | DEFAULT NOW() | 생성일시 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 생성일시 |
|
||||
|
||||
**인덱스:**
|
||||
- `idx_daily_works_date` ON date
|
||||
- `idx_daily_works_created_by_id` ON created_by_id
|
||||
- `daily_works_pkey` PRIMARY KEY (id)
|
||||
- `daily_works_date_key` UNIQUE (date)
|
||||
- `idx_daily_works_date` (date)
|
||||
- `idx_daily_works_created_by_id` (created_by_id)
|
||||
|
||||
**외래키:**
|
||||
- created_by_id → users(id)
|
||||
- `daily_works_created_by_id_fkey` created_by_id → users(id)
|
||||
|
||||
---
|
||||
|
||||
## 관계도 (Relationships)
|
||||
### 5. project_daily_works (프로젝트별 일일공수)
|
||||
프로젝트별 일일 작업 공수 정보를 저장하는 테이블
|
||||
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| id | INTEGER | PRIMARY KEY, AUTO_INCREMENT | 프로젝트 일일공수 고유 ID |
|
||||
| date | DATE | NOT NULL | 작업 날짜 |
|
||||
| project_id | BIGINT | NOT NULL, FOREIGN KEY → projects(id) | 프로젝트 ID |
|
||||
| hours | DOUBLE PRECISION | NOT NULL | 작업 시간 |
|
||||
| created_by_id | INTEGER | NOT NULL, FOREIGN KEY → users(id) | 생성자 ID |
|
||||
| created_at | TIMESTAMP WITH TIME ZONE | DEFAULT CURRENT_TIMESTAMP | 생성일시 |
|
||||
|
||||
**인덱스:**
|
||||
- `project_daily_works_pkey` PRIMARY KEY (id)
|
||||
- `idx_project_daily_works_date` (date)
|
||||
- `idx_project_daily_works_project_id` (project_id)
|
||||
- `idx_project_daily_works_date_project` (date, project_id)
|
||||
|
||||
**외래키:**
|
||||
- `project_daily_works_created_by_id_fkey` created_by_id → users(id)
|
||||
- `project_daily_works_project_id_fkey` project_id → projects(id) ON DELETE CASCADE
|
||||
|
||||
---
|
||||
|
||||
## ENUM 타입 정의
|
||||
|
||||
### userrole
|
||||
사용자 권한 타입
|
||||
- `admin`: 관리자 (모든 권한)
|
||||
- `user`: 일반 사용자 (제한된 권한)
|
||||
|
||||
### issuestatus
|
||||
부적합 사항 처리 상태
|
||||
- `new`: 신규 (미처리)
|
||||
- `progress`: 진행중
|
||||
- `complete`: 완료
|
||||
|
||||
### issuecategory
|
||||
부적합 사항 카테고리
|
||||
- `material_missing`: 자재누락
|
||||
- `design_error`: 설계미스
|
||||
- `incoming_defect`: 입고자재 불량
|
||||
- `inspection_miss`: 검사미스
|
||||
- `etc`: 기타
|
||||
|
||||
---
|
||||
|
||||
## 관계도 (Entity Relationship)
|
||||
|
||||
```
|
||||
users (1) ←→ (N) projects
|
||||
↓
|
||||
users (1) ←→ (N) issues
|
||||
↓
|
||||
↓ ↓
|
||||
users (1) ←→ (N) issues ←→ (1) projects
|
||||
↓ ↓
|
||||
users (1) ←→ (N) daily_works
|
||||
↓
|
||||
users (1) ←→ (N) project_daily_works ←→ (1) projects
|
||||
```
|
||||
|
||||
**주요 관계:**
|
||||
1. **users → projects**: 사용자가 프로젝트를 생성
|
||||
2. **users → issues**: 사용자가 부적합 사항을 보고
|
||||
3. **projects → issues**: 프로젝트에 부적합 사항이 속함
|
||||
4. **users → daily_works**: 사용자가 전체 일일공수를 입력 (레거시)
|
||||
5. **users → project_daily_works**: 사용자가 프로젝트별 일일공수를 입력
|
||||
6. **projects → project_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 테이블 추가
|
||||
| 파일명 | 설명 | 적용일 |
|
||||
|--------|------|--------|
|
||||
| `001_init.sql` | 초기 테이블 생성 (users, issues, daily_works) | 초기 |
|
||||
| `002_add_second_photo.sql` | issues 테이블에 두 번째 사진 필드 추가 | 초기 |
|
||||
| `003_update_categories.sql` | 카테고리 업데이트 (dimension_defect → design_error) | 초기 |
|
||||
| `004_fix_category_values.sql` | 카테고리 값 정규화 (대소문자 통일) | 초기 |
|
||||
| `005_recreate_enum_type.sql` | issuecategory ENUM 타입 재생성 | 초기 |
|
||||
| `006_add_projects_table.sql` | projects 테이블 추가 | 2025-10-25 |
|
||||
| `007_add_project_id_to_issues.sql` | issues 테이블에 project_id 컬럼 추가 | 2025-10-25 |
|
||||
| `008_fix_project_id_bigint.sql` | project_id를 BIGINT로 변경 | 2025-10-25 |
|
||||
| `009_add_project_daily_works_table.sql` | project_daily_works 테이블 추가 | 2025-10-25 |
|
||||
| `010_add_etc_category.sql` | issuecategory에 'etc' 값 추가 | 2025-10-25 |
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 연결 정보
|
||||
|
||||
### Docker 환경
|
||||
- **Host:** db (컨테이너 내부), localhost (외부)
|
||||
- **Port:** 5432 (내부), 16432 (외부)
|
||||
- **Database:** mproject
|
||||
- **Username:** mproject
|
||||
- **Password:** mproject2024
|
||||
- **Timezone:** Asia/Seoul
|
||||
|
||||
### 환경변수
|
||||
```bash
|
||||
DATABASE_URL=postgresql://mproject:mproject2024@db:5432/mproject
|
||||
POSTGRES_USER=mproject
|
||||
POSTGRES_PASSWORD=mproject2024
|
||||
POSTGRES_DB=mproject
|
||||
TZ=Asia/Seoul
|
||||
PGTZ=Asia/Seoul
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -134,40 +234,96 @@ users (1) ←→ (N) daily_works
|
||||
|
||||
### 관리자 계정
|
||||
- **Username:** hyungi
|
||||
- **Password:** djg3-jj34-X3Q3
|
||||
- **Password:** 123456
|
||||
- **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
|
||||
# 전체 백업
|
||||
docker exec m-project-db pg_dump -U mproject mproject > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 스키마만 백업
|
||||
docker exec m-project-db pg_dump -U mproject -s mproject > schema_backup.sql
|
||||
|
||||
# 데이터만 백업
|
||||
docker exec m-project-db pg_dump -U mproject -a mproject > data_backup.sql
|
||||
```
|
||||
|
||||
### 복구
|
||||
```bash
|
||||
docker-compose exec -T db psql -U mproject mproject < backup.sql
|
||||
# 전체 복구
|
||||
docker exec -i m-project-db psql -U mproject mproject < backup.sql
|
||||
|
||||
# 스키마 복구
|
||||
docker exec -i m-project-db psql -U mproject mproject < schema_backup.sql
|
||||
```
|
||||
|
||||
### 테이블 상태 확인
|
||||
```bash
|
||||
# 모든 테이블 목록
|
||||
docker exec m-project-db psql -U mproject -d mproject -c "\dt"
|
||||
|
||||
# 특정 테이블 구조
|
||||
docker exec m-project-db psql -U mproject -d mproject -c "\d table_name"
|
||||
|
||||
# ENUM 타입 확인
|
||||
docker exec m-project-db psql -U mproject -d mproject -c "\dT+"
|
||||
|
||||
# 인덱스 확인
|
||||
docker exec m-project-db psql -U mproject -d mproject -c "\di"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 성능 최적화
|
||||
|
||||
### 인덱스 전략
|
||||
1. **Primary Keys**: 모든 테이블에 자동 생성
|
||||
2. **Foreign Keys**: 모든 외래키에 인덱스 생성
|
||||
3. **검색 필드**: 자주 검색되는 컬럼 (date, status, category 등)
|
||||
4. **복합 인덱스**: 날짜+프로젝트 조합 검색 최적화
|
||||
|
||||
### 쿼리 최적화 권장사항
|
||||
1. **날짜 범위 검색**: date 컬럼 인덱스 활용
|
||||
2. **프로젝트별 필터링**: project_id 인덱스 활용
|
||||
3. **상태별 필터링**: status, is_active 인덱스 활용
|
||||
4. **JOIN 최적화**: 외래키 인덱스 활용
|
||||
|
||||
---
|
||||
|
||||
## 보안 고려사항
|
||||
|
||||
1. **비밀번호 암호화**: bcrypt 해시 사용 (최소 12 rounds)
|
||||
2. **SQL 인젝션 방지**: 파라미터화된 쿼리 사용
|
||||
3. **접근 제어**: 역할 기반 권한 관리
|
||||
4. **데이터 검증**: 애플리케이션 레벨에서 입력값 검증
|
||||
5. **백업 암호화**: 민감한 데이터 백업 시 암호화 권장
|
||||
|
||||
---
|
||||
|
||||
## 향후 확장 계획
|
||||
|
||||
### 예정된 변경사항
|
||||
- [ ] 부적합 사항에 우선순위 필드 추가
|
||||
- [ ] 프로젝트 상태 관리 (진행중, 완료, 보류 등)
|
||||
- [ ] 파일 첨부 기능 확장 (문서, 도면 등)
|
||||
- [ ] 알림 시스템을 위한 notifications 테이블
|
||||
- [ ] 감사 로그를 위한 audit_logs 테이블
|
||||
|
||||
### 성능 개선 계획
|
||||
- [ ] 파티셔닝: 대용량 데이터 처리를 위한 날짜별 파티셔닝
|
||||
- [ ] 아카이빙: 오래된 데이터 아카이브 전략
|
||||
- [ ] 캐싱: Redis를 활용한 자주 조회되는 데이터 캐싱
|
||||
|
||||
---
|
||||
|
||||
**문서 작성자:** AI Assistant
|
||||
**검토자:** -
|
||||
**승인자:** -
|
||||
|
||||
> 이 문서는 데이터베이스 스키마 변경 시마다 업데이트되어야 합니다.
|
||||
549
DB_CHANGE_LOG.md
Normal file
549
DB_CHANGE_LOG.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# 데이터베이스 변경 로그
|
||||
|
||||
이 문서는 M-Project 데이터베이스의 모든 변경사항을 추적하고 기록합니다.
|
||||
|
||||
---
|
||||
|
||||
## 변경 로그 템플릿
|
||||
|
||||
새로운 변경사항이 있을 때마다 아래 템플릿을 사용하여 기록해주세요.
|
||||
|
||||
```markdown
|
||||
## [변경 ID] - [변경 제목] (YYYY-MM-DD)
|
||||
|
||||
### 변경 유형
|
||||
- [ ] 새 테이블 추가
|
||||
- [ ] 기존 테이블 수정
|
||||
- [ ] 컬럼 추가/삭제/수정
|
||||
- [ ] 인덱스 추가/삭제
|
||||
- [ ] 제약조건 추가/삭제
|
||||
- [ ] ENUM 타입 수정
|
||||
- [ ] 데이터 마이그레이션
|
||||
- [ ] 성능 최적화
|
||||
- [ ] 기타: ___________
|
||||
|
||||
### 변경 내용
|
||||
**요약:** [변경사항을 한 줄로 요약]
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유:
|
||||
- 영향받는 테이블:
|
||||
- 새로 추가된 컬럼/테이블:
|
||||
- 삭제된 컬럼/테이블:
|
||||
|
||||
### 마이그레이션 정보
|
||||
- **파일명:** `XXX_description.sql`
|
||||
- **실행 순서:** [이전 마이그레이션 이후 순서]
|
||||
- **롤백 가능 여부:** [Yes/No]
|
||||
- **데이터 손실 위험:** [High/Medium/Low/None]
|
||||
|
||||
### SQL 스크립트
|
||||
```sql
|
||||
-- 마이그레이션 SQL 코드
|
||||
```
|
||||
|
||||
### 테스트 체크리스트
|
||||
- [ ] 로컬 환경에서 테스트 완료
|
||||
- [ ] 기존 데이터 호환성 확인
|
||||
- [ ] 애플리케이션 코드 업데이트 완료
|
||||
- [ ] API 테스트 완료
|
||||
- [ ] 성능 영향 확인
|
||||
|
||||
### 영향도 분석
|
||||
**애플리케이션 영향:**
|
||||
- 백엔드 API: [영향 있음/없음]
|
||||
- 프론트엔드: [영향 있음/없음]
|
||||
- 기존 데이터: [영향 있음/없음]
|
||||
|
||||
**호환성:**
|
||||
- 이전 버전과의 호환성: [유지/중단]
|
||||
- 필요한 코드 변경사항: [있음/없음]
|
||||
|
||||
### 담당자
|
||||
- **개발자:** [이름]
|
||||
- **검토자:** [이름]
|
||||
- **승인자:** [이름]
|
||||
- **적용일:** YYYY-MM-DD
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경 히스토리
|
||||
|
||||
### [012] - 권한 시스템 단순화 및 페이지별 접근 권한 구현 (2025-10-25)
|
||||
|
||||
#### 변경 유형
|
||||
- [x] 테이블 구조 변경
|
||||
- [x] ENUM 타입 수정
|
||||
- [x] 기존 테이블 삭제
|
||||
- [x] 새 테이블 추가
|
||||
|
||||
#### 변경 내용
|
||||
**요약:** 복잡한 4단계 권한을 admin/user 구조로 단순화하고 페이지별 접근 권한 시스템 도입
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유: 사용자 요청에 따른 권한 시스템 단순화
|
||||
- 삭제된 테이블: user_permissions (복잡한 권한 테이블)
|
||||
- 새로 추가된 테이블: user_page_permissions (페이지별 권한)
|
||||
- 역할 단순화: super_admin, manager → admin으로 통합
|
||||
- 새로운 권한 방식: 페이지별 접근 허용/차단
|
||||
|
||||
#### 마이그레이션 정보
|
||||
- **파일명:** `012_simplify_permissions.sql`
|
||||
- **실행 순서:** 011 이후
|
||||
- **롤백 가능 여부:** Partial (기존 복잡한 권한 데이터 손실)
|
||||
- **데이터 손실 위험:** Medium (기존 권한 설정 초기화)
|
||||
|
||||
#### SQL 스크립트
|
||||
```sql
|
||||
-- 기존 복잡한 권한 테이블 삭제
|
||||
DROP TABLE IF EXISTS user_permissions CASCADE;
|
||||
|
||||
-- 페이지별 접근 권한 테이블 생성
|
||||
CREATE TABLE user_page_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
page_name VARCHAR(50) NOT NULL,
|
||||
can_access BOOLEAN DEFAULT FALSE,
|
||||
granted_by_id INTEGER REFERENCES users(id),
|
||||
granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
notes TEXT,
|
||||
UNIQUE(user_id, page_name)
|
||||
);
|
||||
|
||||
-- 페이지 접근 권한 체크 함수
|
||||
CREATE OR REPLACE FUNCTION check_page_access(p_user_id INTEGER, p_page_name VARCHAR) RETURNS BOOLEAN;
|
||||
|
||||
-- 기존 복잡한 역할을 admin으로 통합
|
||||
UPDATE users SET role = 'admin' WHERE role IN ('super_admin', 'manager');
|
||||
```
|
||||
|
||||
#### 새로운 페이지 권한 시스템
|
||||
**기본 페이지 목록:**
|
||||
- `issues_create`: 부적합 등록 (기본: 허용)
|
||||
- `issues_view`: 부적합 조회 (기본: 허용)
|
||||
- `issues_manage`: 부적합 관리 (기본: 차단)
|
||||
- `projects_manage`: 프로젝트 관리 (기본: 차단)
|
||||
- `daily_work`: 일일 공수 (기본: 차단)
|
||||
- `reports`: 보고서 (기본: 차단)
|
||||
|
||||
**권한 규칙:**
|
||||
- **admin**: 모든 페이지 접근 가능
|
||||
- **user**: 개별 페이지 권한에 따라 접근
|
||||
- 권한 미설정 시: 기본값 적용
|
||||
|
||||
#### 관리자 인터페이스 개선
|
||||
**새로운 기능:**
|
||||
1. **페이지 권한 관리 섹션** (`admin.html`)
|
||||
- 사용자별 페이지 접근 권한 설정
|
||||
- 체크박스 방식의 직관적 인터페이스
|
||||
- 실시간 권한 저장 기능
|
||||
|
||||
2. **단순화된 권한 JavaScript** (`permissions.js`)
|
||||
- `PagePermissionManager` 클래스
|
||||
- `canAccessPage()` 함수
|
||||
- 페이지별 UI 제어 기능
|
||||
|
||||
#### 테스트 체크리스트
|
||||
- [x] 로컬 환경에서 테스트 완료
|
||||
- [x] 데이터베이스 마이그레이션 성공
|
||||
- [x] 기존 admin 사용자 권한 유지
|
||||
- [x] 새로운 권한 관리 UI 구현
|
||||
- [ ] 페이지별 접근 제어 테스트
|
||||
- [ ] API 엔드포인트 구현
|
||||
|
||||
#### 영향도 분석
|
||||
**애플리케이션 영향:**
|
||||
- 백엔드 API: 페이지 권한 API 엔드포인트 추가 필요
|
||||
- 프론트엔드: 권한 체크 로직 단순화
|
||||
- 기존 데이터: admin 사용자는 모든 권한 유지
|
||||
|
||||
**호환성:**
|
||||
- 이전 버전과의 호환성: 부분적 유지
|
||||
- 필요한 코드 변경사항: 권한 체크 함수 변경
|
||||
|
||||
#### 사용 방법
|
||||
1. **관리자 권한 설정**
|
||||
```
|
||||
http://localhost:16080/admin.html
|
||||
→ 페이지 접근 권한 관리 섹션
|
||||
→ 사용자 선택 → 페이지별 체크박스 설정
|
||||
```
|
||||
|
||||
2. **권한 체크 (JavaScript)**
|
||||
```javascript
|
||||
// 기존
|
||||
if (hasPermission('issues.edit')) { ... }
|
||||
|
||||
// 신규
|
||||
if (canAccessPage('issues_manage')) { ... }
|
||||
```
|
||||
|
||||
#### 담당자
|
||||
- **개발자:** AI Assistant
|
||||
- **검토자:** -
|
||||
- **승인자:** -
|
||||
- **적용일:** 2025-10-25
|
||||
|
||||
---
|
||||
|
||||
### [011] - 권한 시스템 개선 및 애플리케이션 리팩토링 (2025-10-25)
|
||||
|
||||
#### 변경 유형
|
||||
- [x] 새 테이블 추가
|
||||
- [x] ENUM 타입 수정
|
||||
- [x] 제약조건 추가/삭제
|
||||
- [x] 기타: 대규모 리팩토링
|
||||
|
||||
#### 변경 내용
|
||||
**요약:** 세분화된 권한 시스템 도입 및 통합 SPA 애플리케이션으로 리팩토링
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유: 단순한 admin/user 구조의 한계, 코드 중복 및 유지보수성 문제 해결
|
||||
- 영향받는 테이블: users (role 확장), user_permissions (신규)
|
||||
- 새로 추가된 테이블: user_permissions
|
||||
- 새로 추가된 역할: super_admin, manager
|
||||
- 새로 추가된 파일: app.html, permissions.js, app.js
|
||||
|
||||
#### 마이그레이션 정보
|
||||
- **파일명:** `011_add_permission_system.sql`
|
||||
- **실행 순서:** 010 이후
|
||||
- **롤백 가능 여부:** Partial (새 역할은 롤백 불가)
|
||||
- **데이터 손실 위험:** Low
|
||||
|
||||
#### SQL 스크립트
|
||||
```sql
|
||||
ALTER TYPE userrole ADD VALUE 'super_admin';
|
||||
ALTER TYPE userrole ADD VALUE 'manager';
|
||||
|
||||
CREATE TABLE user_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
permission VARCHAR(50) NOT NULL,
|
||||
granted BOOLEAN DEFAULT TRUE,
|
||||
granted_by_id INTEGER REFERENCES users(id),
|
||||
granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
revoked_at TIMESTAMP WITH TIME ZONE,
|
||||
notes TEXT,
|
||||
UNIQUE(user_id, permission)
|
||||
);
|
||||
|
||||
-- 권한 체크 함수들 생성
|
||||
CREATE OR REPLACE FUNCTION check_user_permission(p_user_id INTEGER, p_permission VARCHAR) RETURNS BOOLEAN;
|
||||
-- ... 기타 함수들
|
||||
```
|
||||
|
||||
#### 새로운 기능
|
||||
1. **세분화된 권한 시스템**
|
||||
- 4단계 사용자 역할 (super_admin, admin, manager, user)
|
||||
- 개별 권한 부여/취소 기능
|
||||
- 권한 기반 UI 동적 제어
|
||||
|
||||
2. **통합 SPA 애플리케이션**
|
||||
- 단일 페이지 애플리케이션 (app.html)
|
||||
- 모듈 기반 아키텍처
|
||||
- 동적 라우팅 시스템
|
||||
|
||||
3. **모듈화된 코드 구조**
|
||||
- 핵심 시스템 모듈 (core/)
|
||||
- 기능별 모듈 분리 (modules/)
|
||||
- 재사용 가능한 컴포넌트
|
||||
|
||||
#### 테스트 체크리스트
|
||||
- [x] 로컬 환경에서 테스트 완료
|
||||
- [x] 기존 데이터 호환성 확인
|
||||
- [x] 데이터베이스 마이그레이션 성공
|
||||
- [ ] 새 권한 시스템 테스트
|
||||
- [ ] 통합 앱 기능 테스트
|
||||
|
||||
#### 영향도 분석
|
||||
**애플리케이션 영향:**
|
||||
- 백엔드 API: 권한 체크 로직 업데이트 필요
|
||||
- 프론트엔드: 완전히 새로운 구조로 변경
|
||||
- 기존 데이터: 호환성 유지 (기존 admin → super_admin 자동 변경)
|
||||
|
||||
**호환성:**
|
||||
- 이전 버전과의 호환성: 부분적 유지 (API 호환)
|
||||
- 필요한 코드 변경사항: 대규모 변경
|
||||
|
||||
#### 새로운 파일 목록
|
||||
**프론트엔드:**
|
||||
- `app.html` - 통합 메인 애플리케이션
|
||||
- `static/js/core/permissions.js` - 권한 관리 시스템
|
||||
- `static/js/app.js` - 메인 애플리케이션 로직
|
||||
|
||||
**문서:**
|
||||
- `REFACTORING_PLAN.md` - 리팩토링 계획서
|
||||
- `MODULE_ARCHITECTURE.md` - 모듈 아키텍처 문서
|
||||
|
||||
#### 권한 매트릭스
|
||||
| 기능 | super_admin | admin | manager | user |
|
||||
|------|-------------|--------|---------|------|
|
||||
| 부적합 등록 | ✅ | ✅ | ✅ | ✅ |
|
||||
| 부적합 조회 | ✅ | ✅ | ✅ | ✅ |
|
||||
| 부적합 수정 | ✅ | ✅ | ✅ | ❌ |
|
||||
| 부적합 삭제 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 프로젝트 생성 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 프로젝트 수정 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 프로젝트 삭제 | ✅ | ❌ | ❌ | ❌ |
|
||||
| 사용자 관리 | ✅ | ❌ | ❌ | ❌ |
|
||||
| 권한 관리 | ✅ | ❌ | ❌ | ❌ |
|
||||
|
||||
#### 담당자
|
||||
- **개발자:** AI Assistant
|
||||
- **검토자:** -
|
||||
- **승인자:** -
|
||||
- **적용일:** 2025-10-25
|
||||
|
||||
---
|
||||
|
||||
### [010] - 부적합 카테고리에 'etc' 값 추가 (2025-10-25)
|
||||
|
||||
#### 변경 유형
|
||||
- [x] ENUM 타입 수정
|
||||
|
||||
#### 변경 내용
|
||||
**요약:** issuecategory ENUM 타입에 'etc' (기타) 값 추가하여 백엔드 코드와 DB 불일치 해결
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유: 백엔드 models.py에는 'etc' 값이 있지만 DB enum에는 없어서 INSERT 시 에러 발생
|
||||
- 영향받는 테이블: issues (category 컬럼)
|
||||
- 새로 추가된 값: issuecategory.etc
|
||||
- 삭제된 컬럼/테이블: 없음
|
||||
|
||||
#### 마이그레이션 정보
|
||||
- **파일명:** `010_add_etc_category.sql`
|
||||
- **실행 순서:** 009 이후
|
||||
- **롤백 가능 여부:** No (PostgreSQL enum 값 삭제 불가)
|
||||
- **데이터 손실 위험:** None
|
||||
|
||||
#### SQL 스크립트
|
||||
```sql
|
||||
ALTER TYPE issuecategory ADD VALUE 'etc';
|
||||
```
|
||||
|
||||
#### 테스트 체크리스트
|
||||
- [x] 로컬 환경에서 테스트 완료
|
||||
- [x] 기존 데이터 호환성 확인
|
||||
- [x] 애플리케이션 코드 업데이트 완료 (이미 존재함)
|
||||
- [x] API 테스트 완료
|
||||
- [x] 성능 영향 확인
|
||||
|
||||
#### 영향도 분석
|
||||
**애플리케이션 영향:**
|
||||
- 백엔드 API: 영향 없음 (이미 코드에 존재)
|
||||
- 프론트엔드: 영향 없음 (이미 UI에 존재)
|
||||
- 기존 데이터: 영향 없음
|
||||
|
||||
**호환성:**
|
||||
- 이전 버전과의 호환성: 유지
|
||||
- 필요한 코드 변경사항: 없음
|
||||
|
||||
#### 담당자
|
||||
- **개발자:** AI Assistant
|
||||
- **검토자:** -
|
||||
- **승인자:** -
|
||||
- **적용일:** 2025-10-25
|
||||
|
||||
---
|
||||
|
||||
### [009] - 프로젝트별 일일공수 테이블 추가 (2025-10-25)
|
||||
|
||||
#### 변경 유형
|
||||
- [x] 새 테이블 추가
|
||||
- [x] 데이터 마이그레이션
|
||||
|
||||
#### 변경 내용
|
||||
**요약:** 프로젝트별로 일일 공수를 관리하기 위한 project_daily_works 테이블 추가
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유: 기존 daily_works는 전체 공수만 관리하여 프로젝트별 공수 추적 불가
|
||||
- 영향받는 테이블: 없음 (신규 테이블)
|
||||
- 새로 추가된 테이블: project_daily_works
|
||||
- 삭제된 컬럼/테이블: 없음
|
||||
|
||||
#### 마이그레이션 정보
|
||||
- **파일명:** `009_add_project_daily_works_table.sql`
|
||||
- **실행 순서:** 008 이후
|
||||
- **롤백 가능 여부:** Yes
|
||||
- **데이터 손실 위험:** None
|
||||
|
||||
#### SQL 스크립트
|
||||
```sql
|
||||
CREATE TABLE project_daily_works (
|
||||
id SERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
hours FLOAT NOT NULL,
|
||||
created_by_id INTEGER NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_project_daily_works_date ON project_daily_works(date);
|
||||
CREATE INDEX idx_project_daily_works_project_id ON project_daily_works(project_id);
|
||||
CREATE INDEX idx_project_daily_works_date_project ON project_daily_works(date, project_id);
|
||||
```
|
||||
|
||||
#### 테스트 체크리스트
|
||||
- [x] 로컬 환경에서 테스트 완료
|
||||
- [x] 기존 데이터 호환성 확인
|
||||
- [ ] 애플리케이션 코드 업데이트 완료
|
||||
- [ ] API 테스트 완료
|
||||
- [x] 성능 영향 확인
|
||||
|
||||
#### 영향도 분석
|
||||
**애플리케이션 영향:**
|
||||
- 백엔드 API: 영향 있음 (새 API 엔드포인트 필요)
|
||||
- 프론트엔드: 영향 있음 (프로젝트별 공수 입력 UI 필요)
|
||||
- 기존 데이터: 영향 없음
|
||||
|
||||
**호환성:**
|
||||
- 이전 버전과의 호환성: 유지
|
||||
- 필요한 코드 변경사항: 있음
|
||||
|
||||
#### 담당자
|
||||
- **개발자:** AI Assistant
|
||||
- **검토자:** -
|
||||
- **승인자:** -
|
||||
- **적용일:** 2025-10-25
|
||||
|
||||
---
|
||||
|
||||
### [008] - 프로젝트 ID를 BIGINT로 변경 (2025-10-25)
|
||||
|
||||
#### 변경 유형
|
||||
- [x] 기존 테이블 수정
|
||||
- [x] 컬럼 추가/삭제/수정
|
||||
|
||||
#### 변경 내용
|
||||
**요약:** 데이터 타입 일관성을 위해 project_id를 BIGINT로 변경
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유: 향후 대용량 데이터 처리 및 타입 일관성 확보
|
||||
- 영향받는 테이블: projects, issues
|
||||
- 수정된 컬럼: projects.id, issues.project_id
|
||||
- 삭제된 컬럼/테이블: 없음
|
||||
|
||||
#### 마이그레이션 정보
|
||||
- **파일명:** `008_fix_project_id_bigint.sql`
|
||||
- **실행 순서:** 007 이후
|
||||
- **롤백 가능 여부:** Yes (데이터 범위 내에서)
|
||||
- **데이터 손실 위험:** None
|
||||
|
||||
#### 담당자
|
||||
- **개발자:** AI Assistant
|
||||
- **검토자:** -
|
||||
- **승인자:** -
|
||||
- **적용일:** 2025-10-25
|
||||
|
||||
---
|
||||
|
||||
### [007] - 부적합 사항에 프로젝트 ID 추가 (2025-10-25)
|
||||
|
||||
#### 변경 유형
|
||||
- [x] 기존 테이블 수정
|
||||
- [x] 컬럼 추가/삭제/수정
|
||||
- [x] 제약조건 추가/삭제
|
||||
|
||||
#### 변경 내용
|
||||
**요약:** issues 테이블에 project_id 컬럼 추가하여 프로젝트별 부적합 사항 관리
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유: 부적합 사항을 프로젝트별로 분류하여 관리 필요
|
||||
- 영향받는 테이블: issues
|
||||
- 새로 추가된 컬럼: issues.project_id
|
||||
- 삭제된 컬럼/테이블: 없음
|
||||
|
||||
#### 마이그레이션 정보
|
||||
- **파일명:** `007_add_project_id_to_issues.sql`
|
||||
- **실행 순서:** 006 이후
|
||||
- **롤백 가능 여부:** Yes
|
||||
- **데이터 손실 위험:** None
|
||||
|
||||
#### 담당자
|
||||
- **개발자:** AI Assistant
|
||||
- **검토자:** -
|
||||
- **승인자:** -
|
||||
- **적용일:** 2025-10-25
|
||||
|
||||
---
|
||||
|
||||
### [006] - 프로젝트 테이블 추가 (2025-10-25)
|
||||
|
||||
#### 변경 유형
|
||||
- [x] 새 테이블 추가
|
||||
- [x] 인덱스 추가/삭제
|
||||
|
||||
#### 변경 내용
|
||||
**요약:** 프로젝트 관리를 위한 projects 테이블 추가
|
||||
|
||||
**상세 설명:**
|
||||
- 변경 이유: 여러 프로젝트를 관리하고 부적합 사항을 프로젝트별로 분류 필요
|
||||
- 영향받는 테이블: 없음 (신규 테이블)
|
||||
- 새로 추가된 테이블: projects
|
||||
- 삭제된 컬럼/테이블: 없음
|
||||
|
||||
#### 마이그레이션 정보
|
||||
- **파일명:** `006_add_projects_table.sql`
|
||||
- **실행 순서:** 005 이후
|
||||
- **롤백 가능 여부:** Yes
|
||||
- **데이터 손실 위험:** None
|
||||
|
||||
#### 담당자
|
||||
- **개발자:** AI Assistant
|
||||
- **검토자:** -
|
||||
- **승인자:** -
|
||||
- **적용일:** 2025-10-25
|
||||
|
||||
---
|
||||
|
||||
## 다음 변경 예정 사항
|
||||
|
||||
### 우선순위 높음
|
||||
- [ ] 부적합 사항 우선순위 필드 추가
|
||||
- [ ] 프로젝트 상태 관리 (진행중, 완료, 보류)
|
||||
- [ ] 알림 시스템 테이블 설계
|
||||
|
||||
### 우선순위 중간
|
||||
- [ ] 파일 첨부 기능 확장
|
||||
- [ ] 감사 로그 테이블 추가
|
||||
- [ ] 사용자 권한 세분화
|
||||
|
||||
### 우선순위 낮음
|
||||
- [ ] 데이터 아카이빙 전략
|
||||
- [ ] 성능 최적화를 위한 파티셔닝
|
||||
- [ ] 전문 검색 기능
|
||||
|
||||
---
|
||||
|
||||
## 변경 승인 프로세스
|
||||
|
||||
1. **계획 단계**
|
||||
- 요구사항 분석
|
||||
- 영향도 평가
|
||||
- 마이그레이션 스크립트 작성
|
||||
|
||||
2. **검토 단계**
|
||||
- 코드 리뷰
|
||||
- 테스트 계획 수립
|
||||
- 롤백 계획 수립
|
||||
|
||||
3. **테스트 단계**
|
||||
- 로컬 환경 테스트
|
||||
- 스테이징 환경 테스트
|
||||
- 성능 테스트
|
||||
|
||||
4. **배포 단계**
|
||||
- 백업 수행
|
||||
- 마이그레이션 실행
|
||||
- 검증 테스트
|
||||
- 모니터링
|
||||
|
||||
---
|
||||
|
||||
**문서 관리자:** 개발팀
|
||||
**최종 업데이트:** 2025-10-25
|
||||
|
||||
> 모든 데이터베이스 변경사항은 이 문서에 기록되어야 하며, 변경 전 반드시 검토 과정을 거쳐야 합니다.
|
||||
235
DB_SETUP_SUMMARY.md
Normal file
235
DB_SETUP_SUMMARY.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# 데이터베이스 설치 및 검토 완료 보고서
|
||||
|
||||
**작업 일시:** 2025-10-25
|
||||
**작업자:** AI Assistant
|
||||
**검토 대상:** M-Project 데이터베이스 스키마 및 테이블 구조
|
||||
|
||||
---
|
||||
|
||||
## 작업 요약
|
||||
|
||||
✅ **데이터베이스 스키마 검토 완료**
|
||||
✅ **누락된 마이그레이션 적용 완료**
|
||||
✅ **문서화 작업 완료**
|
||||
✅ **향후 변경사항 추적 체계 구축**
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 현황
|
||||
|
||||
### 설치된 테이블 (5개)
|
||||
| 테이블명 | 상태 | 데이터 수 | 설명 |
|
||||
|---------|------|-----------|------|
|
||||
| `users` | ✅ 정상 | 1개 | 사용자 계정 (관리자 1명) |
|
||||
| `projects` | ✅ 정상 | 0개 | 프로젝트 관리 |
|
||||
| `issues` | ✅ 정상 | 0개 | 부적합 사항 |
|
||||
| `daily_works` | ✅ 정상 | 0개 | 전체 일일공수 (레거시) |
|
||||
| `project_daily_works` | ✅ 정상 | 0개 | 프로젝트별 일일공수 |
|
||||
|
||||
### ENUM 타입 (3개)
|
||||
| 타입명 | 값 | 설명 |
|
||||
|--------|-----|------|
|
||||
| `userrole` | admin, user | 사용자 권한 |
|
||||
| `issuestatus` | new, progress, complete | 부적합 사항 상태 |
|
||||
| `issuecategory` | material_missing, design_error, incoming_defect, inspection_miss | 부적합 카테고리 |
|
||||
|
||||
---
|
||||
|
||||
## 적용된 마이그레이션
|
||||
|
||||
### 기존 마이그레이션 (1-5)
|
||||
- ✅ `001_init.sql` - 초기 테이블 생성
|
||||
- ✅ `002_add_second_photo.sql` - 두 번째 사진 필드 추가
|
||||
- ✅ `003_update_categories.sql` - 카테고리 업데이트
|
||||
- ✅ `004_fix_category_values.sql` - 카테고리 값 수정
|
||||
- ✅ `005_recreate_enum_type.sql` - ENUM 타입 재생성
|
||||
|
||||
### 신규 적용 마이그레이션 (6-9)
|
||||
- ✅ `006_add_projects_table.sql` - 프로젝트 테이블 추가
|
||||
- ✅ `007_add_project_id_to_issues.sql` - 부적합 사항에 프로젝트 ID 추가
|
||||
- ✅ `008_fix_project_id_bigint.sql` - 프로젝트 ID를 BIGINT로 변경
|
||||
- ✅ `009_add_project_daily_works_table.sql` - 프로젝트별 일일공수 테이블 추가
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 연결 정보
|
||||
|
||||
### 접속 정보
|
||||
- **Host:** localhost (외부), db (컨테이너 내부)
|
||||
- **Port:** 16432 (외부), 5432 (내부)
|
||||
- **Database:** mproject
|
||||
- **Username:** mproject
|
||||
- **Password:** mproject2024
|
||||
|
||||
### 관리자 계정
|
||||
- **Username:** hyungi
|
||||
- **Password:** 123456
|
||||
- **Role:** admin
|
||||
- **Status:** 활성화됨
|
||||
|
||||
---
|
||||
|
||||
## 인덱스 최적화 현황
|
||||
|
||||
### Primary Keys
|
||||
- ✅ 모든 테이블에 PRIMARY KEY 설정됨
|
||||
- ✅ AUTO_INCREMENT 정상 작동
|
||||
|
||||
### Foreign Keys & 참조 무결성
|
||||
- ✅ `users` ← `projects.created_by_id`
|
||||
- ✅ `users` ← `issues.reporter_id`
|
||||
- ✅ `users` ← `daily_works.created_by_id`
|
||||
- ✅ `users` ← `project_daily_works.created_by_id`
|
||||
- ✅ `projects` ← `issues.project_id`
|
||||
- ✅ `projects` ← `project_daily_works.project_id` (CASCADE DELETE)
|
||||
|
||||
### 성능 인덱스
|
||||
- ✅ 검색용 인덱스: date, status, category, project_id
|
||||
- ✅ 복합 인덱스: (date, project_id)
|
||||
- ✅ 유니크 인덱스: username, job_no, date (daily_works)
|
||||
|
||||
---
|
||||
|
||||
## 생성된 문서
|
||||
|
||||
### 1. DATABASE_SCHEMA.md (업데이트됨)
|
||||
- **내용:** 전체 데이터베이스 스키마 문서
|
||||
- **포함사항:**
|
||||
- 테이블 구조 상세 정보
|
||||
- ENUM 타입 정의
|
||||
- 관계도 (ERD)
|
||||
- 마이그레이션 히스토리
|
||||
- 성능 최적화 가이드
|
||||
- 보안 고려사항
|
||||
|
||||
### 2. DB_CHANGE_LOG.md (신규 생성)
|
||||
- **내용:** 데이터베이스 변경사항 추적 로그
|
||||
- **포함사항:**
|
||||
- 변경 로그 템플릿
|
||||
- 기존 변경사항 히스토리 (006-009)
|
||||
- 향후 변경 예정 사항
|
||||
- 변경 승인 프로세스
|
||||
|
||||
### 3. DB_SETUP_SUMMARY.md (신규 생성)
|
||||
- **내용:** 이번 작업의 요약 보고서
|
||||
- **목적:** 작업 내역 기록 및 현황 파악
|
||||
|
||||
---
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### 스키마 무결성
|
||||
- ✅ 모든 테이블 정상 생성됨
|
||||
- ✅ 외래키 제약조건 정상 작동
|
||||
- ✅ ENUM 타입 정상 정의됨
|
||||
- ✅ 인덱스 정상 생성됨
|
||||
|
||||
### 데이터 무결성
|
||||
- ✅ 관리자 계정 정상 존재
|
||||
- ✅ 기본 데이터 구조 정상
|
||||
- ✅ 타입 제약조건 정상 작동
|
||||
|
||||
### 성능 검증
|
||||
- ✅ 쿼리 실행 속도 정상
|
||||
- ✅ 인덱스 활용 정상
|
||||
- ✅ 메모리 사용량 적정
|
||||
|
||||
---
|
||||
|
||||
## 향후 작업 계획
|
||||
|
||||
### 즉시 필요한 작업
|
||||
1. **기본 프로젝트 생성**
|
||||
- 현재 진행 중인 프로젝트들을 데이터베이스에 등록
|
||||
- 기존 부적합 사항들을 해당 프로젝트에 연결
|
||||
|
||||
2. **API 테스트**
|
||||
- 새로 추가된 프로젝트 관련 API 엔드포인트 테스트
|
||||
- 프론트엔드와의 연동 테스트
|
||||
|
||||
### 중장기 계획
|
||||
1. **기능 확장**
|
||||
- 부적합 사항 우선순위 관리
|
||||
- 프로젝트 상태 관리 (진행중, 완료, 보류)
|
||||
- 알림 시스템 구축
|
||||
|
||||
2. **성능 최적화**
|
||||
- 대용량 데이터 처리를 위한 파티셔닝
|
||||
- 캐싱 전략 수립
|
||||
- 아카이빙 정책 수립
|
||||
|
||||
---
|
||||
|
||||
## 백업 및 복구 준비
|
||||
|
||||
### 백업 스크립트
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 일일 백업 스크립트
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
docker exec m-project-db pg_dump -U mproject mproject > "backup/mproject_${DATE}.sql"
|
||||
echo "백업 완료: backup/mproject_${DATE}.sql"
|
||||
```
|
||||
|
||||
### 복구 스크립트
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 복구 스크립트
|
||||
if [ -z "$1" ]; then
|
||||
echo "사용법: $0 <백업파일명>"
|
||||
exit 1
|
||||
fi
|
||||
docker exec -i m-project-db psql -U mproject mproject < "$1"
|
||||
echo "복구 완료: $1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문제 해결 가이드
|
||||
|
||||
### 자주 발생하는 문제
|
||||
|
||||
1. **마이그레이션 실패**
|
||||
```bash
|
||||
# 마이그레이션 상태 확인
|
||||
docker exec m-project-db psql -U mproject -d mproject -c "\dt"
|
||||
|
||||
# 수동 마이그레이션 실행
|
||||
docker exec m-project-db psql -U mproject -d mproject -f /docker-entrypoint-initdb.d/XXX_migration.sql
|
||||
```
|
||||
|
||||
2. **연결 문제**
|
||||
```bash
|
||||
# 데이터베이스 상태 확인
|
||||
docker-compose ps
|
||||
|
||||
# 로그 확인
|
||||
docker-compose logs db
|
||||
```
|
||||
|
||||
3. **권한 문제**
|
||||
```bash
|
||||
# 사용자 권한 확인
|
||||
docker exec m-project-db psql -U mproject -d mproject -c "\du"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 결론
|
||||
|
||||
✅ **데이터베이스 스키마 검토 및 설치 작업이 성공적으로 완료되었습니다.**
|
||||
|
||||
- 모든 테이블이 정상적으로 생성되고 구성됨
|
||||
- 누락된 마이그레이션이 모두 적용됨
|
||||
- 완전한 문서화 체계 구축됨
|
||||
- 향후 변경사항 추적 체계 마련됨
|
||||
|
||||
**이제 안정적으로 개발을 진행할 수 있는 환경이 준비되었습니다.**
|
||||
|
||||
---
|
||||
|
||||
**작성자:** AI Assistant
|
||||
**검토 완료일:** 2025-10-25
|
||||
**다음 검토 예정일:** 필요시 또는 주요 변경사항 발생시
|
||||
|
||||
> 이 문서는 데이터베이스 관련 작업의 기준점이 되며, 향후 모든 변경사항은 DB_CHANGE_LOG.md에 기록되어야 합니다.
|
||||
511
DEVELOPMENT_GUIDE.md
Normal file
511
DEVELOPMENT_GUIDE.md
Normal file
@@ -0,0 +1,511 @@
|
||||
# M-Project 개발 가이드
|
||||
|
||||
## 개요
|
||||
M-Project의 신규 페이지 및 기능 개발을 위한 표준 가이드입니다.
|
||||
|
||||
---
|
||||
|
||||
## 📋 기본 규칙
|
||||
|
||||
### 1. 파일 구조 규칙
|
||||
```
|
||||
frontend/
|
||||
├── [page-name].html # 메인 HTML 파일
|
||||
├── static/
|
||||
│ ├── js/
|
||||
│ │ ├── modules/
|
||||
│ │ │ └── [page-name]/ # 페이지별 모듈
|
||||
│ │ │ ├── [page-name].js
|
||||
│ │ │ ├── components/ # 페이지 전용 컴포넌트
|
||||
│ │ │ └── utils/ # 페이지 전용 유틸리티
|
||||
│ │ ├── components/ # 공통 컴포넌트
|
||||
│ │ ├── core/ # 핵심 시스템
|
||||
│ │ └── utils/ # 공통 유틸리티
|
||||
│ └── css/
|
||||
│ └── [page-name].css # 페이지별 스타일 (필요시)
|
||||
```
|
||||
|
||||
### 2. 네이밍 규칙
|
||||
- **파일명**: kebab-case (`user-management.html`)
|
||||
- **클래스명**: PascalCase (`UserManagementModule`)
|
||||
- **함수명**: camelCase (`loadUserData`)
|
||||
- **변수명**: camelCase (`currentUser`)
|
||||
- **상수명**: UPPER_SNAKE_CASE (`API_BASE_URL`)
|
||||
- **CSS 클래스**: kebab-case (`user-list-item`)
|
||||
|
||||
### 3. 권한 체크 규칙
|
||||
- 모든 페이지는 권한 체크를 구현해야 함
|
||||
- 페이지별 권한명은 `[category]_[action]` 형식 사용
|
||||
- 예: `users_manage`, `reports_view`, `projects_create`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 신규 페이지 개발 단계
|
||||
|
||||
### 1단계: 권한 정의
|
||||
```javascript
|
||||
// backend/routers/page_permissions.py의 DEFAULT_PAGES에 추가
|
||||
'new_feature': {'title': '새 기능', 'default_access': false}
|
||||
```
|
||||
|
||||
### 2단계: HTML 템플릿 생성
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>새 기능 - 작업보고서</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- 페이지별 스타일 (필요시) -->
|
||||
<link rel="stylesheet" href="/static/css/new-feature.css">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<!-- 공통 헤더는 자동으로 삽입됩니다 -->
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main id="main-content" class="container mx-auto px-4 py-8">
|
||||
<!-- 페이지 콘텐츠 -->
|
||||
</main>
|
||||
|
||||
<!-- 필수 스크립트 -->
|
||||
<script src="/static/js/core/permissions.js?v=20251025"></script>
|
||||
<script src="/static/js/components/common-header.js?v=20251025"></script>
|
||||
<script src="/static/js/core/page-manager.js?v=20251025"></script>
|
||||
|
||||
<!-- 페이지별 모듈 -->
|
||||
<script src="/static/js/modules/new-feature/new-feature.js?v=20251025"></script>
|
||||
|
||||
<!-- 초기화 스크립트 -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await pageManager.initializePage('new_feature');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 3단계: 페이지 모듈 생성
|
||||
```javascript
|
||||
// static/js/modules/new-feature/new-feature.js
|
||||
class NewFeatureModule extends BasePageModule {
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.data = [];
|
||||
this.currentView = 'list';
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 초기화
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
this.showLoading('main-content', '새 기능을 로드하는 중...');
|
||||
|
||||
// 데이터 로드
|
||||
await this.loadData();
|
||||
|
||||
// UI 렌더링
|
||||
this.render();
|
||||
|
||||
// 이벤트 바인딩
|
||||
this.bindEvents();
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('NewFeatureModule 초기화 실패:', error);
|
||||
this.showError('main-content', '새 기능을 로드할 수 없습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 로드
|
||||
*/
|
||||
async loadData() {
|
||||
try {
|
||||
// API 호출 예시
|
||||
this.data = await NewFeatureAPI.getAll();
|
||||
} catch (error) {
|
||||
console.error('데이터 로드 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 렌더링
|
||||
*/
|
||||
render() {
|
||||
const container = document.getElementById('main-content');
|
||||
container.innerHTML = this.generateHTML();
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 생성
|
||||
*/
|
||||
generateHTML() {
|
||||
return `
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">새 기능</h1>
|
||||
<p class="text-gray-600 mt-1">새 기능에 대한 설명</p>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="mb-6">
|
||||
<button id="add-btn" class="btn-primary px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
<i class="fas fa-plus mr-2"></i>새 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 데이터 목록 -->
|
||||
<div id="data-list" class="bg-white rounded-lg shadow">
|
||||
${this.generateDataList()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 목록 HTML 생성
|
||||
*/
|
||||
generateDataList() {
|
||||
if (this.data.length === 0) {
|
||||
return `
|
||||
<div class="p-8 text-center text-gray-500">
|
||||
<i class="fas fa-inbox text-4xl mb-4"></i>
|
||||
<p>데이터가 없습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
제목
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
생성일
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
액션
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
${this.data.map(item => this.generateDataRow(item)).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 행 HTML 생성
|
||||
*/
|
||||
generateDataRow(item) {
|
||||
return `
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
${item.title}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
${new Date(item.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button onclick="newFeatureModule.editItem(${item.id})"
|
||||
class="text-blue-600 hover:text-blue-900 mr-3">
|
||||
수정
|
||||
</button>
|
||||
<button onclick="newFeatureModule.deleteItem(${item.id})"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
삭제
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 바인딩
|
||||
*/
|
||||
bindEvents() {
|
||||
const addBtn = document.getElementById('add-btn');
|
||||
if (addBtn) {
|
||||
this.addEventListener(addBtn, 'click', () => this.showAddModal());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목 추가 모달 표시
|
||||
*/
|
||||
showAddModal() {
|
||||
// 모달 구현
|
||||
console.log('새 항목 추가 모달 표시');
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목 수정
|
||||
*/
|
||||
editItem(id) {
|
||||
console.log('항목 수정:', id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목 삭제
|
||||
*/
|
||||
async deleteItem(id) {
|
||||
if (confirm('정말 삭제하시겠습니까?')) {
|
||||
try {
|
||||
await NewFeatureAPI.delete(id);
|
||||
await this.loadData();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
alert('삭제에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API 정의
|
||||
const NewFeatureAPI = {
|
||||
getAll: () => apiRequest('/new-feature/'),
|
||||
get: (id) => apiRequest(`/new-feature/${id}`),
|
||||
create: (data) => apiRequest('/new-feature/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
}),
|
||||
update: (id, data) => apiRequest(`/new-feature/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
}),
|
||||
delete: (id) => apiRequest(`/new-feature/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
};
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.newFeatureModule = null;
|
||||
|
||||
// 페이지 매니저에 모듈 등록
|
||||
if (window.pageManager) {
|
||||
window.pageManager.createPageModule = function(pageId, options) {
|
||||
switch (pageId) {
|
||||
case 'new_feature':
|
||||
window.newFeatureModule = new NewFeatureModule(options);
|
||||
return window.newFeatureModule;
|
||||
// ... 기존 케이스들
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4단계: 공통 헤더에 메뉴 추가
|
||||
```javascript
|
||||
// static/js/components/common-header.js의 initMenuItems()에 추가
|
||||
{
|
||||
id: 'new_feature',
|
||||
title: '새 기능',
|
||||
icon: 'fas fa-star',
|
||||
url: '/new-feature.html',
|
||||
pageName: 'new_feature',
|
||||
color: 'text-yellow-600',
|
||||
bgColor: 'bg-yellow-50 hover:bg-yellow-100'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI/UX 가이드라인
|
||||
|
||||
### 1. 색상 팔레트
|
||||
- **Primary**: Blue (blue-600, blue-700)
|
||||
- **Success**: Green (green-600, green-700)
|
||||
- **Warning**: Yellow (yellow-600, yellow-700)
|
||||
- **Danger**: Red (red-600, red-700)
|
||||
- **Info**: Purple (purple-600, purple-700)
|
||||
- **Gray**: Gray (gray-500, gray-600, gray-700)
|
||||
|
||||
### 2. 컴포넌트 스타일
|
||||
```css
|
||||
/* 버튼 */
|
||||
.btn-primary { @apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors; }
|
||||
.btn-secondary { @apply px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors; }
|
||||
.btn-success { @apply px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors; }
|
||||
.btn-danger { @apply px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors; }
|
||||
|
||||
/* 입력 필드 */
|
||||
.input-field { @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500; }
|
||||
|
||||
/* 카드 */
|
||||
.card { @apply bg-white rounded-lg shadow-sm border border-gray-200; }
|
||||
.card-header { @apply px-6 py-4 border-b border-gray-200; }
|
||||
.card-body { @apply px-6 py-4; }
|
||||
```
|
||||
|
||||
### 3. 반응형 디자인
|
||||
- **Mobile First**: 모바일부터 디자인 시작
|
||||
- **Breakpoints**: sm(640px), md(768px), lg(1024px), xl(1280px)
|
||||
- **Grid System**: Tailwind CSS Grid 사용
|
||||
|
||||
---
|
||||
|
||||
## 🔧 API 개발 가이드
|
||||
|
||||
### 1. 백엔드 라우터 생성
|
||||
```python
|
||||
# backend/routers/new_feature.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from database.database import get_db
|
||||
from database.models import User
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/new-feature", tags=["new-feature"])
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_items(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""모든 항목 조회"""
|
||||
# 구현 내용
|
||||
pass
|
||||
|
||||
@router.post("/")
|
||||
async def create_item(
|
||||
item_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""새 항목 생성"""
|
||||
# 구현 내용
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 메인 앱에 라우터 등록
|
||||
```python
|
||||
# backend/main.py
|
||||
from routers import new_feature
|
||||
|
||||
app.include_router(new_feature.router)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 코드 품질 가이드
|
||||
|
||||
### 1. 에러 처리
|
||||
```javascript
|
||||
// 좋은 예
|
||||
try {
|
||||
const data = await API.getData();
|
||||
this.processData(data);
|
||||
} catch (error) {
|
||||
console.error('데이터 로드 실패:', error);
|
||||
this.showError('데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
|
||||
// 나쁜 예
|
||||
const data = await API.getData(); // 에러 처리 없음
|
||||
```
|
||||
|
||||
### 2. 로딩 상태 관리
|
||||
```javascript
|
||||
// 좋은 예
|
||||
async loadData() {
|
||||
this.showLoading('data-container');
|
||||
try {
|
||||
const data = await API.getData();
|
||||
this.renderData(data);
|
||||
} catch (error) {
|
||||
this.showError('data-container', '데이터 로드 실패');
|
||||
}
|
||||
}
|
||||
|
||||
// 나쁜 예
|
||||
async loadData() {
|
||||
const data = await API.getData(); // 로딩 상태 없음
|
||||
this.renderData(data);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 메모리 관리
|
||||
```javascript
|
||||
// 좋은 예 - BasePageModule 사용
|
||||
class MyModule extends BasePageModule {
|
||||
bindEvents() {
|
||||
const button = document.getElementById('my-button');
|
||||
this.addEventListener(button, 'click', this.handleClick.bind(this));
|
||||
}
|
||||
|
||||
// cleanup()은 BasePageModule에서 자동 처리
|
||||
}
|
||||
|
||||
// 나쁜 예 - 수동 이벤트 관리
|
||||
class MyModule {
|
||||
bindEvents() {
|
||||
document.getElementById('my-button').addEventListener('click', this.handleClick);
|
||||
// 이벤트 리스너 제거 코드 없음
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 테스트 가이드
|
||||
|
||||
### 1. 기능 테스트 체크리스트
|
||||
- [ ] 페이지 로드 정상 작동
|
||||
- [ ] 권한 체크 정상 작동
|
||||
- [ ] CRUD 기능 정상 작동
|
||||
- [ ] 에러 처리 정상 작동
|
||||
- [ ] 반응형 디자인 정상 작동
|
||||
- [ ] 브라우저 호환성 확인
|
||||
|
||||
### 2. 성능 테스트
|
||||
- [ ] 페이지 로드 시간 < 3초
|
||||
- [ ] API 응답 시간 < 1초
|
||||
- [ ] 메모리 누수 없음
|
||||
- [ ] 모바일 성능 최적화
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고 자료
|
||||
|
||||
### 1. 기존 모듈 참고
|
||||
- `static/js/components/common-header.js` - 공통 컴포넌트 예시
|
||||
- `static/js/core/page-manager.js` - 페이지 관리 예시
|
||||
- `static/js/core/permissions.js` - 권한 시스템 예시
|
||||
|
||||
### 2. 외부 라이브러리
|
||||
- **Tailwind CSS**: https://tailwindcss.com/docs
|
||||
- **Font Awesome**: https://fontawesome.com/icons
|
||||
- **FastAPI**: https://fastapi.tiangolo.com/
|
||||
|
||||
### 3. 코딩 컨벤션
|
||||
- **JavaScript**: Airbnb Style Guide
|
||||
- **Python**: PEP 8
|
||||
- **HTML/CSS**: Google Style Guide
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-10-25
|
||||
**버전**: 1.0
|
||||
**작성자**: AI Assistant
|
||||
|
||||
> 이 가이드는 프로젝트 발전에 따라 지속적으로 업데이트됩니다.
|
||||
478
MODULE_ARCHITECTURE.md
Normal file
478
MODULE_ARCHITECTURE.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# M-Project 모듈 아키텍처 문서
|
||||
|
||||
## 개요
|
||||
리팩토링된 M-Project의 모듈 구조와 연결점을 설명하는 문서입니다.
|
||||
|
||||
---
|
||||
|
||||
## 전체 아키텍처
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── index.html # 로그인 페이지
|
||||
├── app.html # 통합 메인 애플리케이션
|
||||
└── static/
|
||||
└── js/
|
||||
├── core/ # 핵심 시스템 모듈
|
||||
│ ├── api.js # API 통신 (기존)
|
||||
│ ├── permissions.js # 권한 관리 시스템 (신규)
|
||||
│ ├── router.js # 라우팅 시스템 (예정)
|
||||
│ └── events.js # 이벤트 시스템 (예정)
|
||||
├── components/ # 재사용 가능한 UI 컴포넌트 (예정)
|
||||
├── modules/ # 기능별 모듈 (예정)
|
||||
├── utils/ # 유틸리티 함수들
|
||||
│ ├── date-utils.js # 날짜 관련 유틸리티 (기존)
|
||||
│ └── image-utils.js # 이미지 관련 유틸리티 (기존)
|
||||
└── app.js # 메인 애플리케이션 (신규)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 핵심 모듈 상세
|
||||
|
||||
### 1. 권한 관리 시스템 (`core/permissions.js`)
|
||||
|
||||
#### 클래스: `PermissionManager`
|
||||
|
||||
**주요 기능:**
|
||||
- 사용자 권한 체크
|
||||
- UI 요소 권한 제어
|
||||
- 동적 메뉴 생성
|
||||
- 개별 권한 관리
|
||||
|
||||
**주요 메서드:**
|
||||
```javascript
|
||||
// 권한 체크
|
||||
hasPermission(permission: string): boolean
|
||||
hasAnyPermission(permissions: Array<string>): boolean
|
||||
hasAllPermissions(permissions: Array<string>): boolean
|
||||
|
||||
// UI 제어
|
||||
controlElement(selector: string, permission: string, action: string): void
|
||||
|
||||
// 메뉴 구성
|
||||
getMenuConfig(): Array<MenuItem>
|
||||
|
||||
// 권한 관리
|
||||
grantPermission(userId: number, permission: string, notes?: string): Promise
|
||||
revokePermission(userId: number, permission: string, notes?: string): Promise
|
||||
```
|
||||
|
||||
**연결점:**
|
||||
- `app.js`에서 사용자 설정 시 초기화
|
||||
- 모든 모듈에서 권한 체크 시 사용
|
||||
- 전역 함수로 노출: `hasPermission()`, `controlElement()` 등
|
||||
|
||||
### 2. 메인 애플리케이션 (`app.js`)
|
||||
|
||||
#### 클래스: `App`
|
||||
|
||||
**주요 기능:**
|
||||
- 애플리케이션 초기화
|
||||
- 라우팅 관리
|
||||
- 모듈 로딩
|
||||
- UI 상태 관리
|
||||
|
||||
**주요 메서드:**
|
||||
```javascript
|
||||
// 초기화
|
||||
init(): Promise<void>
|
||||
checkAuth(): Promise<void>
|
||||
loadAPIScript(): Promise<void>
|
||||
|
||||
// 라우팅
|
||||
handleRouteChange(): Promise<void>
|
||||
navigateTo(path: string): void
|
||||
loadModule(module: string, action?: string): Promise<void>
|
||||
|
||||
// UI 관리
|
||||
updateUserDisplay(): void
|
||||
createNavigationMenu(): void
|
||||
toggleSidebar(): void
|
||||
|
||||
// 모달 관리
|
||||
showPasswordChangeModal(): void
|
||||
hidePasswordChangeModal(): void
|
||||
```
|
||||
|
||||
**연결점:**
|
||||
- `permissions.js`와 연동하여 권한 기반 UI 구성
|
||||
- `api.js`를 동적으로 로드하여 API 통신 활성화
|
||||
- 각 모듈을 동적으로 로드하고 실행
|
||||
|
||||
---
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
### 1. 애플리케이션 시작 흐름
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[app.html 로드] --> B[App 클래스 초기화]
|
||||
B --> C[인증 확인]
|
||||
C --> D[API 스크립트 로드]
|
||||
D --> E[권한 시스템 초기화]
|
||||
E --> F[UI 초기화]
|
||||
F --> G[라우터 초기화]
|
||||
G --> H[대시보드 표시]
|
||||
```
|
||||
|
||||
### 2. 권한 체크 흐름
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[권한 체크 요청] --> B{사용자 로그인?}
|
||||
B -->|No| C[권한 없음 반환]
|
||||
B -->|Yes| D{super_admin?}
|
||||
D -->|Yes| E[모든 권한 허용]
|
||||
D -->|No| F[개별 권한 확인]
|
||||
F --> G{개별 권한 존재?}
|
||||
G -->|Yes| H[개별 권한 값 반환]
|
||||
G -->|No| I[기본 권한 매트릭스 확인]
|
||||
I --> J[권한 결과 반환]
|
||||
```
|
||||
|
||||
### 3. 모듈 로딩 흐름
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[라우트 변경] --> B[모듈명 추출]
|
||||
B --> C{모듈 로드됨?}
|
||||
C -->|Yes| D[모듈 실행]
|
||||
C -->|No| E[모듈 스크립트 로드]
|
||||
E --> F[모듈 인스턴스 생성]
|
||||
F --> G[모듈 캐시 저장]
|
||||
G --> D[모듈 실행]
|
||||
D --> H[콘텐츠 렌더링]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 이벤트 시스템 (예정)
|
||||
|
||||
### EventBus 클래스
|
||||
|
||||
**목적:** 모듈 간 느슨한 결합을 위한 이벤트 기반 통신
|
||||
|
||||
**주요 이벤트:**
|
||||
```javascript
|
||||
// 사용자 관련
|
||||
'user.login' // 사용자 로그인
|
||||
'user.logout' // 사용자 로그아웃
|
||||
'user.permission.changed' // 권한 변경
|
||||
|
||||
// 데이터 관련
|
||||
'issue.created' // 부적합 사항 생성
|
||||
'issue.updated' // 부적합 사항 업데이트
|
||||
'issue.deleted' // 부적합 사항 삭제
|
||||
'project.created' // 프로젝트 생성
|
||||
'project.updated' // 프로젝트 업데이트
|
||||
|
||||
// UI 관련
|
||||
'modal.open' // 모달 열기
|
||||
'modal.close' // 모달 닫기
|
||||
'notification.show' // 알림 표시
|
||||
```
|
||||
|
||||
**사용 예시:**
|
||||
```javascript
|
||||
// 이벤트 발생
|
||||
eventBus.emit('issue.created', { id: 123, title: '새 부적합' });
|
||||
|
||||
// 이벤트 수신
|
||||
eventBus.on('issue.created', (data) => {
|
||||
console.log('새 부적합 사항:', data);
|
||||
refreshIssueList();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 상태 관리 시스템 (예정)
|
||||
|
||||
### StateManager 클래스
|
||||
|
||||
**목적:** 애플리케이션 전역 상태 관리
|
||||
|
||||
**상태 구조:**
|
||||
```javascript
|
||||
{
|
||||
user: {
|
||||
id: number,
|
||||
username: string,
|
||||
role: string,
|
||||
permissions: Array<string>
|
||||
},
|
||||
projects: Array<Project>,
|
||||
issues: Array<Issue>,
|
||||
currentProject: Project | null,
|
||||
ui: {
|
||||
sidebarCollapsed: boolean,
|
||||
currentPage: string,
|
||||
loading: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**사용 예시:**
|
||||
```javascript
|
||||
// 상태 설정
|
||||
stateManager.setState('user', userData);
|
||||
|
||||
// 상태 구독
|
||||
stateManager.subscribe('user', (user) => {
|
||||
updateUserDisplay(user);
|
||||
});
|
||||
|
||||
// 상태 조회
|
||||
const currentUser = stateManager.getState('user');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 모듈 개발 가이드
|
||||
|
||||
### 1. 새 모듈 생성 규칙
|
||||
|
||||
**파일 구조:**
|
||||
```
|
||||
modules/
|
||||
└── [module-name]/
|
||||
├── [module-name].js # 메인 모듈 파일
|
||||
├── [module-name].css # 모듈 전용 스타일 (선택)
|
||||
└── components/ # 모듈 내 컴포넌트 (선택)
|
||||
```
|
||||
|
||||
**모듈 클래스 템플릿:**
|
||||
```javascript
|
||||
class ModuleNameModule {
|
||||
constructor() {
|
||||
this.name = 'module-name';
|
||||
this.permissions = ['module.view']; // 필요한 권한
|
||||
}
|
||||
|
||||
// 필수 메서드: 콘텐츠 렌더링
|
||||
async render(action = 'list') {
|
||||
// 권한 체크
|
||||
if (!hasPermission(this.permissions[0])) {
|
||||
return '<div class="text-center p-8">접근 권한이 없습니다.</div>';
|
||||
}
|
||||
|
||||
// 액션별 처리
|
||||
switch (action) {
|
||||
case 'list':
|
||||
return await this.renderList();
|
||||
case 'create':
|
||||
return await this.renderCreate();
|
||||
case 'edit':
|
||||
return await this.renderEdit();
|
||||
default:
|
||||
return await this.renderList();
|
||||
}
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
// 목록 화면 HTML 반환
|
||||
return `<div>목록 화면</div>`;
|
||||
}
|
||||
|
||||
async renderCreate() {
|
||||
// 생성 화면 HTML 반환
|
||||
return `<div>생성 화면</div>`;
|
||||
}
|
||||
|
||||
async renderEdit() {
|
||||
// 편집 화면 HTML 반환
|
||||
return `<div>편집 화면</div>`;
|
||||
}
|
||||
|
||||
// 이벤트 핸들러 등록
|
||||
bindEvents() {
|
||||
// 이벤트 리스너 등록
|
||||
}
|
||||
|
||||
// 정리 작업
|
||||
destroy() {
|
||||
// 이벤트 리스너 제거 등
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 등록 (필수)
|
||||
window.ModuleNameModule = ModuleNameModule;
|
||||
```
|
||||
|
||||
### 2. 모듈 간 통신
|
||||
|
||||
**권장 방법:**
|
||||
1. **EventBus 사용** (모듈 간 느슨한 결합)
|
||||
2. **StateManager 사용** (공유 상태 관리)
|
||||
3. **API 직접 호출** (데이터 조회/수정)
|
||||
|
||||
**비권장 방법:**
|
||||
- 직접적인 모듈 참조
|
||||
- 전역 변수 사용
|
||||
- DOM 직접 조작으로 통신
|
||||
|
||||
### 3. 권한 체크 패턴
|
||||
|
||||
**UI 요소 제어:**
|
||||
```javascript
|
||||
// 버튼 표시/숨김
|
||||
controlElement('.create-btn', 'issues.create', 'show');
|
||||
|
||||
// 입력 필드 활성/비활성
|
||||
controlElement('.edit-form input', 'issues.edit', 'disable');
|
||||
```
|
||||
|
||||
**조건부 렌더링:**
|
||||
```javascript
|
||||
if (hasPermission('issues.delete')) {
|
||||
html += '<button class="delete-btn">삭제</button>';
|
||||
}
|
||||
```
|
||||
|
||||
**다중 권한 체크:**
|
||||
```javascript
|
||||
if (hasAnyPermission(['issues.edit', 'issues.delete'])) {
|
||||
html += '<div class="action-buttons">...</div>';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 연동 패턴
|
||||
|
||||
### 1. 기존 API 사용
|
||||
|
||||
```javascript
|
||||
// 기존 API 객체 사용
|
||||
const issues = await IssuesAPI.getAll();
|
||||
const projects = await ProjectsAPI.getAll();
|
||||
```
|
||||
|
||||
### 2. 에러 처리
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const data = await API.call();
|
||||
// 성공 처리
|
||||
} catch (error) {
|
||||
console.error('API 호출 실패:', error);
|
||||
app.showError('데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 로딩 상태 관리
|
||||
|
||||
```javascript
|
||||
app.showLoading();
|
||||
try {
|
||||
const data = await API.call();
|
||||
// 데이터 처리
|
||||
} finally {
|
||||
app.hideLoading();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 성능 최적화 가이드
|
||||
|
||||
### 1. 모듈 지연 로딩
|
||||
|
||||
- 필요한 시점에만 모듈 로드
|
||||
- 사용하지 않는 모듈은 로드하지 않음
|
||||
- 모듈 캐싱으로 중복 로드 방지
|
||||
|
||||
### 2. 이벤트 리스너 관리
|
||||
|
||||
- 모듈 파괴 시 이벤트 리스너 제거
|
||||
- 전역 이벤트 리스너 최소화
|
||||
- 이벤트 위임 패턴 사용
|
||||
|
||||
### 3. DOM 조작 최적화
|
||||
|
||||
- 가상 DOM 또는 DocumentFragment 사용
|
||||
- 일괄 DOM 업데이트
|
||||
- 불필요한 리렌더링 방지
|
||||
|
||||
---
|
||||
|
||||
## 테스트 전략
|
||||
|
||||
### 1. 단위 테스트
|
||||
|
||||
- 각 모듈의 핵심 로직 테스트
|
||||
- 권한 체크 로직 테스트
|
||||
- API 호출 모킹
|
||||
|
||||
### 2. 통합 테스트
|
||||
|
||||
- 모듈 간 상호작용 테스트
|
||||
- 라우팅 테스트
|
||||
- 권한 기반 UI 테스트
|
||||
|
||||
### 3. E2E 테스트
|
||||
|
||||
- 사용자 시나리오 테스트
|
||||
- 권한별 접근 테스트
|
||||
- 크로스 브라우저 테스트
|
||||
|
||||
---
|
||||
|
||||
## 배포 및 버전 관리
|
||||
|
||||
### 1. 버전 관리 전략
|
||||
|
||||
- 모듈별 독립적 버전 관리
|
||||
- 호환성 매트릭스 유지
|
||||
- 점진적 업그레이드 지원
|
||||
|
||||
### 2. 캐시 관리
|
||||
|
||||
- 모듈별 캐시 버스팅
|
||||
- 버전 기반 캐시 무효화
|
||||
- CDN 캐시 전략
|
||||
|
||||
### 3. 모니터링
|
||||
|
||||
- 모듈 로딩 성능 모니터링
|
||||
- 에러 추적 및 로깅
|
||||
- 사용자 행동 분석
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 가이드
|
||||
|
||||
### 기존 코드에서 새 구조로 이전
|
||||
|
||||
1. **권한 체크 코드 변경**
|
||||
```javascript
|
||||
// 기존
|
||||
if (currentUser.role === 'admin') { ... }
|
||||
|
||||
// 신규
|
||||
if (hasPermission('users.edit')) { ... }
|
||||
```
|
||||
|
||||
2. **페이지 이동 코드 변경**
|
||||
```javascript
|
||||
// 기존
|
||||
window.location.href = 'admin.html';
|
||||
|
||||
// 신규
|
||||
app.navigateTo('#users/list');
|
||||
```
|
||||
|
||||
3. **모듈화 적용**
|
||||
- 기능별로 코드 분리
|
||||
- 공통 로직 유틸리티로 이동
|
||||
- 이벤트 기반 통신으로 변경
|
||||
|
||||
---
|
||||
|
||||
**작성일:** 2025-10-25
|
||||
**작성자:** AI Assistant
|
||||
**버전:** 1.0
|
||||
|
||||
> 이 문서는 시스템 발전에 따라 지속적으로 업데이트됩니다.
|
||||
255
PERFORMANCE_OPTIMIZATION.md
Normal file
255
PERFORMANCE_OPTIMIZATION.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# M-Project 성능 최적화 가이드
|
||||
|
||||
## 개요
|
||||
M-Project의 성능 최적화 기능들과 사용 방법을 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 구현된 최적화 기능
|
||||
|
||||
### 1. 페이지 프리로딩 시스템
|
||||
**파일**: `frontend/static/js/core/page-preloader.js`
|
||||
|
||||
#### 기능
|
||||
- **우선순위 기반 프리로딩**: 사용자 권한에 따른 접근 가능한 페이지를 우선순위별로 프리로드
|
||||
- **호버 프리로딩**: 링크에 마우스를 올렸을 때 해당 페이지를 미리 로드
|
||||
- **리소스 프리로딩**: HTML과 함께 CSS, JS 파일도 함께 프리로드
|
||||
- **네트워크 감지**: 느린 연결에서는 프리로딩을 중단하여 사용자 경험 보호
|
||||
|
||||
#### 사용법
|
||||
```javascript
|
||||
// 자동 초기화 (공통 헤더에서 자동 실행)
|
||||
window.pagePreloader.init();
|
||||
|
||||
// 수동 프리로드
|
||||
await window.pagePreloader.preloadPage({
|
||||
id: 'issues_view',
|
||||
url: '/issue-view.html',
|
||||
priority: 1
|
||||
});
|
||||
|
||||
// 캐시 정리
|
||||
window.pagePreloader.clearCache();
|
||||
```
|
||||
|
||||
#### 우선순위 설정
|
||||
1. **Priority 1**: 부적합 등록, 부적합 조회 (기본 접근 페이지)
|
||||
2. **Priority 2**: 부적합 관리, 일일 공수
|
||||
3. **Priority 3**: 프로젝트 관리, 보고서
|
||||
4. **Priority 4**: 사용자 관리 (관리자 전용)
|
||||
|
||||
### 2. 서비스 워커 캐싱
|
||||
**파일**: `frontend/sw.js`
|
||||
|
||||
#### 캐시 전략
|
||||
- **Network First**: API 호출 (`/api/`, `/auth/`)
|
||||
- **Cache First**: 정적 리소스 (CSS, JS, 이미지, 폰트)
|
||||
- **Stale While Revalidate**: HTML 페이지
|
||||
|
||||
#### 캐시 종류
|
||||
- **Static Cache**: 변경되지 않는 리소스
|
||||
- **Dynamic Cache**: API 응답 및 동적 콘텐츠
|
||||
- **CDN Cache**: 외부 라이브러리 (Tailwind, Font Awesome)
|
||||
|
||||
#### 오프라인 지원
|
||||
- HTML 페이지: 기본 페이지로 폴백
|
||||
- 이미지: SVG 플레이스홀더 제공
|
||||
- API: 캐시된 응답 반환
|
||||
|
||||
### 3. 키보드 단축키 시스템
|
||||
**파일**: `frontend/static/js/core/keyboard-shortcuts.js`
|
||||
|
||||
#### 주요 단축키
|
||||
| 단축키 | 기능 | 설명 |
|
||||
|--------|------|------|
|
||||
| `?` | 도움말 표시 | 모든 단축키 목록 보기 |
|
||||
| `Esc` | 모달/메뉴 닫기 | 열린 모달이나 드롭다운 닫기 |
|
||||
| `g h` | 홈 이동 | 부적합 등록 페이지로 이동 |
|
||||
| `g v` | 조회 이동 | 부적합 조회 페이지로 이동 |
|
||||
| `g d` | 일일 공수 | 일일 공수 페이지로 이동 |
|
||||
| `g p` | 프로젝트 관리 | 프로젝트 관리 페이지로 이동 |
|
||||
| `g a` | 관리자 | 관리자 페이지로 이동 |
|
||||
| `n` | 새 항목 | 새 항목 생성 버튼 클릭 |
|
||||
| `s` | 저장 | 저장 버튼 클릭 |
|
||||
| `r` | 새로고침 | 페이지 새로고침 |
|
||||
| `f` | 검색 포커스 | 검색 필드에 포커스 |
|
||||
|
||||
#### 권한 기반 단축키
|
||||
- **일반 사용자**: 기본 네비게이션 및 액션 단축키
|
||||
- **관리자**: 모든 단축키 + 관리자 전용 단축키
|
||||
- **입력 필드**: 제한된 단축키만 작동 (Esc, Ctrl+S 등)
|
||||
|
||||
### 4. 공통 헤더 시스템
|
||||
**파일**: `frontend/static/js/components/common-header.js`
|
||||
|
||||
#### 성능 최적화
|
||||
- **권한 기반 메뉴**: 접근 불가능한 메뉴는 렌더링하지 않음
|
||||
- **지연 로딩**: 권한 시스템 로드 후 헤더 렌더링
|
||||
- **이벤트 위임**: 효율적인 이벤트 처리
|
||||
- **부드러운 전환**: 150ms 딜레이로 자연스러운 페이지 이동
|
||||
|
||||
---
|
||||
|
||||
## 📊 성능 측정 및 모니터링
|
||||
|
||||
### 1. 브라우저 개발자 도구 활용
|
||||
```javascript
|
||||
// 페이지 로드 시간 측정
|
||||
console.time('페이지 로드');
|
||||
window.addEventListener('load', () => {
|
||||
console.timeEnd('페이지 로드');
|
||||
});
|
||||
|
||||
// 프리로드 상태 확인
|
||||
console.log('프리로드된 페이지:', window.pagePreloader.preloadedPages);
|
||||
console.log('리소스 캐시:', window.pagePreloader.resourceCache);
|
||||
```
|
||||
|
||||
### 2. 서비스 워커 상태 확인
|
||||
```javascript
|
||||
// 캐시 상태 조회
|
||||
navigator.serviceWorker.controller?.postMessage({
|
||||
type: 'GET_CACHE_STATUS'
|
||||
});
|
||||
|
||||
// 캐시 정리
|
||||
navigator.serviceWorker.controller?.postMessage({
|
||||
type: 'CLEAR_CACHE'
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 네트워크 성능 모니터링
|
||||
```javascript
|
||||
// 연결 상태 확인
|
||||
if ('connection' in navigator) {
|
||||
const connection = navigator.connection;
|
||||
console.log('연결 타입:', connection.effectiveType);
|
||||
console.log('데이터 절약 모드:', connection.saveData);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 성능 최적화 팁
|
||||
|
||||
### 1. 프리로딩 최적화
|
||||
```javascript
|
||||
// 중요한 페이지만 프리로드
|
||||
const priorityPages = ['issues_create', 'issues_view'];
|
||||
|
||||
// 네트워크 상태에 따른 조건부 프리로딩
|
||||
if (!window.pagePreloader.isSlowConnection()) {
|
||||
window.pagePreloader.startPreloading(priorityPages);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 캐시 전략 조정
|
||||
```javascript
|
||||
// 자주 변경되는 API는 네트워크 우선
|
||||
const networkFirstPatterns = [
|
||||
/\/api\/issues/,
|
||||
/\/api\/users/
|
||||
];
|
||||
|
||||
// 정적 리소스는 캐시 우선
|
||||
const cacheFirstPatterns = [
|
||||
/\.css$/,
|
||||
/\.js$/,
|
||||
/\.png$/
|
||||
];
|
||||
```
|
||||
|
||||
### 3. 메모리 관리
|
||||
```javascript
|
||||
// 페이지 이동 시 정리
|
||||
window.addEventListener('beforeunload', () => {
|
||||
// 이벤트 리스너 정리
|
||||
if (window.pageManager) {
|
||||
window.pageManager.cleanup();
|
||||
}
|
||||
|
||||
// 캐시 정리 (필요시)
|
||||
if (performance.memory?.usedJSHeapSize > 50 * 1024 * 1024) {
|
||||
window.pagePreloader.clearCache();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 이미지 최적화
|
||||
```javascript
|
||||
// 지연 로딩
|
||||
const images = document.querySelectorAll('img[data-src]');
|
||||
const imageObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.dataset.src;
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
images.forEach(img => imageObserver.observe(img));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 디버깅 및 문제 해결
|
||||
|
||||
### 1. 프리로더 디버깅
|
||||
```javascript
|
||||
// 프리로드 상태 확인
|
||||
console.log('프리로드 진행 중:', window.pagePreloader.isPreloading);
|
||||
console.log('프리로드된 페이지 수:', window.pagePreloader.preloadedPages.size);
|
||||
|
||||
// 프리로드 실패 원인 확인
|
||||
window.pagePreloader.addEventListener('error', (error) => {
|
||||
console.error('프리로드 실패:', error);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 서비스 워커 디버깅
|
||||
```javascript
|
||||
// 서비스 워커 상태 확인
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
console.log('서비스 워커 상태:', registration.active?.state);
|
||||
});
|
||||
|
||||
// 캐시 내용 확인
|
||||
caches.keys().then(cacheNames => {
|
||||
console.log('사용 가능한 캐시:', cacheNames);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 성능 문제 해결
|
||||
- **느린 페이지 로드**: 프리로딩 우선순위 조정
|
||||
- **메모리 누수**: 이벤트 리스너 정리 확인
|
||||
- **캐시 문제**: 서비스 워커 재등록
|
||||
- **키보드 단축키 충돌**: 입력 필드 예외 처리 확인
|
||||
|
||||
---
|
||||
|
||||
## 📈 성능 지표 목표
|
||||
|
||||
### 1. 로딩 성능
|
||||
- **첫 페이지 로드**: < 3초
|
||||
- **후속 페이지 로드**: < 1초 (프리로드된 경우)
|
||||
- **API 응답**: < 1초
|
||||
|
||||
### 2. 사용자 경험
|
||||
- **페이지 전환**: < 150ms
|
||||
- **키보드 단축키 반응**: < 100ms
|
||||
- **검색 결과**: < 500ms
|
||||
|
||||
### 3. 리소스 사용량
|
||||
- **메모리 사용량**: < 100MB
|
||||
- **캐시 크기**: < 50MB
|
||||
- **네트워크 요청**: 최소화
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-10-25
|
||||
**버전**: 1.0
|
||||
**작성자**: AI Assistant
|
||||
|
||||
> 성능 최적화는 지속적인 모니터링과 개선이 필요합니다.
|
||||
380
REFACTORING_PLAN.md
Normal file
380
REFACTORING_PLAN.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# M-Project 리팩토링 계획서
|
||||
|
||||
## 개요
|
||||
사용자 권한 시스템을 세분화하고, 코드를 모듈화하여 유지보수성을 향상시키는 리팩토링 작업
|
||||
|
||||
---
|
||||
|
||||
## 1. 권한 시스템 개선
|
||||
|
||||
### 현재 문제점
|
||||
- 단순한 2단계 권한 (admin/user)
|
||||
- 하드코딩된 권한 체크
|
||||
- 페이지별로 다른 권한 체크 로직
|
||||
|
||||
### 개선 방향
|
||||
|
||||
#### 1.1 새로운 권한 구조
|
||||
```javascript
|
||||
// 기존
|
||||
enum UserRole {
|
||||
admin = "admin",
|
||||
user = "user"
|
||||
}
|
||||
|
||||
// 개선
|
||||
enum UserRole {
|
||||
super_admin = "super_admin", // 최고 관리자
|
||||
admin = "admin", // 관리자
|
||||
manager = "manager", // 매니저
|
||||
user = "user" // 일반 사용자
|
||||
}
|
||||
|
||||
// 권한별 기능 매트릭스
|
||||
const PERMISSIONS = {
|
||||
// 부적합 사항 관리
|
||||
'issues.create': ['super_admin', 'admin', 'manager', 'user'],
|
||||
'issues.view': ['super_admin', 'admin', 'manager', 'user'],
|
||||
'issues.edit': ['super_admin', 'admin', 'manager'],
|
||||
'issues.delete': ['super_admin', 'admin'],
|
||||
'issues.review': ['super_admin', 'admin', 'manager'],
|
||||
|
||||
// 프로젝트 관리
|
||||
'projects.create': ['super_admin', 'admin'],
|
||||
'projects.view': ['super_admin', 'admin', 'manager', 'user'],
|
||||
'projects.edit': ['super_admin', 'admin'],
|
||||
'projects.delete': ['super_admin'],
|
||||
|
||||
// 일일 공수 관리
|
||||
'daily_work.create': ['super_admin', 'admin', 'manager'],
|
||||
'daily_work.view': ['super_admin', 'admin', 'manager', 'user'],
|
||||
'daily_work.edit': ['super_admin', 'admin', 'manager'],
|
||||
'daily_work.delete': ['super_admin', 'admin'],
|
||||
|
||||
// 보고서
|
||||
'reports.view': ['super_admin', 'admin', 'manager'],
|
||||
'reports.export': ['super_admin', 'admin'],
|
||||
|
||||
// 사용자 관리
|
||||
'users.create': ['super_admin'],
|
||||
'users.view': ['super_admin', 'admin'],
|
||||
'users.edit': ['super_admin'],
|
||||
'users.delete': ['super_admin'],
|
||||
'users.change_role': ['super_admin']
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 데이터베이스 스키마 변경
|
||||
```sql
|
||||
-- 새로운 권한 ENUM 타입
|
||||
ALTER TYPE userrole ADD VALUE 'super_admin';
|
||||
ALTER TYPE userrole ADD VALUE 'manager';
|
||||
|
||||
-- 사용자별 개별 권한 테이블 (선택적)
|
||||
CREATE TABLE user_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
permission VARCHAR(50) NOT NULL,
|
||||
granted BOOLEAN DEFAULT TRUE,
|
||||
granted_by_id INTEGER REFERENCES users(id),
|
||||
granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(user_id, permission)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 페이지 통합 계획
|
||||
|
||||
### 현재 문제점
|
||||
- 사용자/관리자별 별도 페이지
|
||||
- 중복된 코드와 로직
|
||||
- 일관성 없는 UI/UX
|
||||
|
||||
### 통합 방향
|
||||
|
||||
#### 2.1 단일 페이지 구조
|
||||
```
|
||||
기존:
|
||||
- index.html (사용자용)
|
||||
- admin.html (관리자용)
|
||||
- project-management.html (관리자용)
|
||||
- issue-view.html (공통)
|
||||
- daily-work.html (관리자용)
|
||||
|
||||
개선:
|
||||
- app.html (통합 메인 애플리케이션)
|
||||
├── 부적합 등록 (모든 사용자)
|
||||
├── 부적합 조회 (모든 사용자)
|
||||
├── 프로젝트 관리 (권한별 제한)
|
||||
├── 일일 공수 (권한별 제한)
|
||||
├── 보고서 (권한별 제한)
|
||||
└── 사용자 관리 (최고 관리자만)
|
||||
```
|
||||
|
||||
#### 2.2 동적 메뉴 시스템
|
||||
```javascript
|
||||
// 권한 기반 메뉴 구성
|
||||
const MENU_CONFIG = {
|
||||
'issues': {
|
||||
title: '부적합 사항',
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
permission: 'issues.view',
|
||||
children: [
|
||||
{ title: '등록', path: '#issues/create', permission: 'issues.create' },
|
||||
{ title: '조회', path: '#issues/list', permission: 'issues.view' },
|
||||
{ title: '관리', path: '#issues/manage', permission: 'issues.edit' }
|
||||
]
|
||||
},
|
||||
'projects': {
|
||||
title: '프로젝트',
|
||||
icon: 'fas fa-folder-open',
|
||||
permission: 'projects.view',
|
||||
children: [
|
||||
{ title: '목록', path: '#projects/list', permission: 'projects.view' },
|
||||
{ title: '관리', path: '#projects/manage', permission: 'projects.edit' }
|
||||
]
|
||||
}
|
||||
// ... 기타 메뉴
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 코드 모듈화 계획
|
||||
|
||||
### 현재 문제점
|
||||
- 긴 HTML 파일 (1000+ 라인)
|
||||
- 중복된 JavaScript 코드
|
||||
- 하드코딩된 로직
|
||||
|
||||
### 모듈화 구조
|
||||
|
||||
#### 3.1 디렉토리 구조
|
||||
```
|
||||
frontend/
|
||||
├── index.html (로그인 페이지)
|
||||
├── app.html (메인 애플리케이션)
|
||||
├── static/
|
||||
│ ├── js/
|
||||
│ │ ├── core/
|
||||
│ │ │ ├── api.js (API 통신)
|
||||
│ │ │ ├── auth.js (인증/권한)
|
||||
│ │ │ ├── router.js (라우팅)
|
||||
│ │ │ └── permissions.js (권한 체크)
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── header.js (공통 헤더)
|
||||
│ │ │ ├── sidebar.js (사이드바)
|
||||
│ │ │ ├── modal.js (모달 컴포넌트)
|
||||
│ │ │ └── form-validator.js (폼 검증)
|
||||
│ │ ├── modules/
|
||||
│ │ │ ├── issues/
|
||||
│ │ │ │ ├── issue-list.js
|
||||
│ │ │ │ ├── issue-form.js
|
||||
│ │ │ │ └── issue-detail.js
|
||||
│ │ │ ├── projects/
|
||||
│ │ │ │ ├── project-list.js
|
||||
│ │ │ │ └── project-form.js
|
||||
│ │ │ ├── reports/
|
||||
│ │ │ │ └── report-generator.js
|
||||
│ │ │ └── users/
|
||||
│ │ │ ├── user-list.js
|
||||
│ │ │ └── user-form.js
|
||||
│ │ ├── utils/
|
||||
│ │ │ ├── date-utils.js
|
||||
│ │ │ ├── image-utils.js
|
||||
│ │ │ └── format-utils.js
|
||||
│ │ └── app.js (메인 애플리케이션)
|
||||
│ └── css/
|
||||
│ ├── components/
|
||||
│ └── modules/
|
||||
```
|
||||
|
||||
#### 3.2 모듈 간 연결점 (Interface)
|
||||
|
||||
##### 3.2.1 이벤트 시스템
|
||||
```javascript
|
||||
// 중앙 이벤트 버스
|
||||
class EventBus {
|
||||
constructor() {
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
this.events[event].push(callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.events[event]) {
|
||||
this.events[event].forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (this.events[event]) {
|
||||
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 이벤트 버스
|
||||
window.eventBus = new EventBus();
|
||||
|
||||
// 사용 예시
|
||||
// 부적합 등록 완료 시
|
||||
eventBus.emit('issue.created', { id: 123, title: '새 부적합' });
|
||||
|
||||
// 부적합 목록에서 수신
|
||||
eventBus.on('issue.created', (data) => {
|
||||
refreshIssueList();
|
||||
});
|
||||
```
|
||||
|
||||
##### 3.2.2 상태 관리
|
||||
```javascript
|
||||
// 중앙 상태 관리
|
||||
class StateManager {
|
||||
constructor() {
|
||||
this.state = {
|
||||
user: null,
|
||||
projects: [],
|
||||
issues: [],
|
||||
currentProject: null
|
||||
};
|
||||
this.subscribers = {};
|
||||
}
|
||||
|
||||
setState(key, value) {
|
||||
this.state[key] = value;
|
||||
this.notify(key, value);
|
||||
}
|
||||
|
||||
getState(key) {
|
||||
return this.state[key];
|
||||
}
|
||||
|
||||
subscribe(key, callback) {
|
||||
if (!this.subscribers[key]) {
|
||||
this.subscribers[key] = [];
|
||||
}
|
||||
this.subscribers[key].push(callback);
|
||||
}
|
||||
|
||||
notify(key, value) {
|
||||
if (this.subscribers[key]) {
|
||||
this.subscribers[key].forEach(callback => callback(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 상태 관리자
|
||||
window.stateManager = new StateManager();
|
||||
```
|
||||
|
||||
##### 3.2.3 모듈 로더
|
||||
```javascript
|
||||
// 동적 모듈 로딩
|
||||
class ModuleLoader {
|
||||
constructor() {
|
||||
this.loadedModules = new Set();
|
||||
}
|
||||
|
||||
async loadModule(modulePath) {
|
||||
if (this.loadedModules.has(modulePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = modulePath;
|
||||
script.onload = () => {
|
||||
this.loadedModules.add(modulePath);
|
||||
resolve();
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async loadModules(modules) {
|
||||
const promises = modules.map(module => this.loadModule(module));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 모듈 로더
|
||||
window.moduleLoader = new ModuleLoader();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 구현 단계
|
||||
|
||||
### Phase 1: 권한 시스템 개선
|
||||
1. 데이터베이스 스키마 업데이트
|
||||
2. 백엔드 권한 체크 로직 개선
|
||||
3. 프론트엔드 권한 관리 모듈 개발
|
||||
|
||||
### Phase 2: 코어 모듈 개발
|
||||
1. 이벤트 시스템 구현
|
||||
2. 상태 관리 시스템 구현
|
||||
3. 라우터 시스템 구현
|
||||
4. 권한 체크 모듈 구현
|
||||
|
||||
### Phase 3: 컴포넌트 모듈화
|
||||
1. 공통 컴포넌트 분리
|
||||
2. 기능별 모듈 분리
|
||||
3. 유틸리티 함수 분리
|
||||
|
||||
### Phase 4: 페이지 통합
|
||||
1. 통합 메인 페이지 개발
|
||||
2. 동적 메뉴 시스템 구현
|
||||
3. 기존 페이지 마이그레이션
|
||||
|
||||
### Phase 5: 테스트 및 최적화
|
||||
1. 기능 테스트
|
||||
2. 성능 최적화
|
||||
3. 문서화 완료
|
||||
|
||||
---
|
||||
|
||||
## 5. 예상 효과
|
||||
|
||||
### 개발 효율성
|
||||
- 코드 재사용성 증가
|
||||
- 유지보수 비용 감소
|
||||
- 새 기능 개발 속도 향상
|
||||
|
||||
### 사용자 경험
|
||||
- 일관된 UI/UX
|
||||
- 권한별 맞춤 인터페이스
|
||||
- 빠른 페이지 전환
|
||||
|
||||
### 시스템 안정성
|
||||
- 명확한 권한 체계
|
||||
- 모듈 간 낮은 결합도
|
||||
- 높은 테스트 가능성
|
||||
|
||||
---
|
||||
|
||||
## 6. 위험 요소 및 대응 방안
|
||||
|
||||
### 위험 요소
|
||||
1. 기존 기능 호환성 문제
|
||||
2. 개발 기간 연장
|
||||
3. 사용자 적응 시간
|
||||
|
||||
### 대응 방안
|
||||
1. 단계별 점진적 마이그레이션
|
||||
2. 기존 기능 유지하면서 새 기능 추가
|
||||
3. 충분한 테스트 및 문서화
|
||||
|
||||
---
|
||||
|
||||
**작성일:** 2025-10-25
|
||||
**작성자:** AI Assistant
|
||||
**검토자:** -
|
||||
**승인자:** -
|
||||
344
frontend/app.html
Normal file
344
frontend/app.html
Normal file
@@ -0,0 +1,344 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>작업보고서 시스템</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 50%, #f0f9ff 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
transition: margin-left 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.main-content.expanded {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: white;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: 1px solid #d1d5db;
|
||||
background: white;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: #3b82f6;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 모바일 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-overlay.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 로딩 오버레이 -->
|
||||
<div id="loadingOverlay" class="loading-overlay">
|
||||
<div class="bg-white rounded-xl p-8 text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-spinner fa-spin text-5xl text-blue-500"></i>
|
||||
</div>
|
||||
<p class="text-gray-700 font-medium text-lg">처리 중입니다...</p>
|
||||
<p class="text-gray-500 text-sm mt-2">잠시만 기다려주세요</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 오버레이 -->
|
||||
<div id="mobileOverlay" class="mobile-overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- 사이드바 -->
|
||||
<aside id="sidebar" class="sidebar fixed left-0 top-0 h-full w-64 bg-white shadow-lg z-50">
|
||||
<!-- 헤더 -->
|
||||
<div class="p-6 border-b">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-clipboard-check text-2xl text-blue-500 mr-3"></i>
|
||||
<h1 class="text-xl font-bold text-gray-800">작업보고서</h1>
|
||||
</div>
|
||||
<button onclick="toggleSidebar()" class="md:hidden text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 정보 -->
|
||||
<div class="p-4 border-b bg-gray-50">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold">
|
||||
<span id="userInitial">U</span>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-800" id="userDisplayName">사용자</p>
|
||||
<p class="text-sm text-gray-500" id="userRole">user</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 네비게이션 메뉴 -->
|
||||
<nav class="p-4">
|
||||
<ul id="navigationMenu" class="space-y-2">
|
||||
<!-- 메뉴 항목들이 동적으로 생성됩니다 -->
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 하단 메뉴 -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 border-t bg-gray-50">
|
||||
<button onclick="showPasswordChangeModal()" class="w-full text-left p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<i class="fas fa-key mr-3 text-gray-500"></i>
|
||||
<span class="text-gray-700">비밀번호 변경</span>
|
||||
</button>
|
||||
<button onclick="logout()" class="w-full text-left p-2 rounded-lg hover:bg-red-50 text-red-600 transition-colors mt-2">
|
||||
<i class="fas fa-sign-out-alt mr-3"></i>
|
||||
<span>로그아웃</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main id="mainContent" class="main-content ml-64 min-h-screen">
|
||||
<!-- 상단 바 -->
|
||||
<header class="bg-white shadow-sm border-b p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<button onclick="toggleSidebar()" class="md:hidden mr-4 text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h2 id="pageTitle" class="text-xl font-semibold text-gray-800">대시보드</h2>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-lg hover:bg-gray-100">
|
||||
<i class="fas fa-bell"></i>
|
||||
</button>
|
||||
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-lg hover:bg-gray-100">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 페이지 콘텐츠 -->
|
||||
<div id="pageContent" class="p-6">
|
||||
<!-- 기본 대시보드 -->
|
||||
<div id="dashboard" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- 통계 카드들 -->
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">총 부적합 사항</p>
|
||||
<p class="text-2xl font-bold text-gray-800" id="totalIssues">0</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-red-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">진행 중인 프로젝트</p>
|
||||
<p class="text-2xl font-bold text-gray-800" id="activeProjects">0</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-folder-open text-blue-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">이번 달 공수</p>
|
||||
<p class="text-2xl font-bold text-gray-800" id="monthlyHours">0</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-clock text-green-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">완료율</p>
|
||||
<p class="text-2xl font-bold text-gray-800" id="completionRate">0%</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-chart-pie text-purple-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 활동 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">최근 부적합 사항</h3>
|
||||
<div id="recentIssues" class="space-y-3">
|
||||
<!-- 최근 부적합 사항 목록 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">프로젝트 현황</h3>
|
||||
<div id="projectStatus" class="space-y-3">
|
||||
<!-- 프로젝트 현황 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 동적 콘텐츠 영역 -->
|
||||
<div id="dynamicContent" class="hidden">
|
||||
<!-- 각 모듈의 콘텐츠가 여기에 로드됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 비밀번호 변경 모달 -->
|
||||
<div id="passwordModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-xl p-6 w-96 max-w-md mx-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">비밀번호 변경</h3>
|
||||
<button onclick="hidePasswordChangeModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="passwordChangeForm" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label>
|
||||
<input type="password" id="currentPassword" class="input-field w-full px-3 py-2 rounded-lg" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
|
||||
<input type="password" id="newPassword" class="input-field w-full px-3 py-2 rounded-lg" required minlength="6">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
|
||||
<input type="password" id="confirmPassword" class="input-field w-full px-3 py-2 rounded-lg" required>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<button type="button" onclick="hidePasswordChangeModal()"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">
|
||||
취소
|
||||
</button>
|
||||
<button type="submit" class="flex-1 px-4 py-2 btn-primary rounded-lg">
|
||||
변경
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/static/js/utils/date-utils.js"></script>
|
||||
<script src="/static/js/utils/image-utils.js"></script>
|
||||
<script src="/static/js/core/permissions.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
490
frontend/static/js/app.js
Normal file
490
frontend/static/js/app.js
Normal file
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* 메인 애플리케이션 JavaScript
|
||||
* 통합된 SPA 애플리케이션의 핵심 로직
|
||||
*/
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.currentUser = null;
|
||||
this.currentPage = 'dashboard';
|
||||
this.modules = new Map();
|
||||
this.sidebarCollapsed = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 애플리케이션 초기화
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// 인증 확인
|
||||
await this.checkAuth();
|
||||
|
||||
// API 스크립트 로드
|
||||
await this.loadAPIScript();
|
||||
|
||||
// 권한 시스템 초기화
|
||||
window.pagePermissionManager.setUser(this.currentUser);
|
||||
|
||||
// UI 초기화
|
||||
this.initializeUI();
|
||||
|
||||
// 라우터 초기화
|
||||
this.initializeRouter();
|
||||
|
||||
// 대시보드 데이터 로드
|
||||
await this.loadDashboardData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('앱 초기화 실패:', error);
|
||||
this.redirectToLogin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 확인
|
||||
*/
|
||||
async checkAuth() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
throw new Error('토큰 없음');
|
||||
}
|
||||
|
||||
// 임시로 localStorage에서 사용자 정보 가져오기
|
||||
const storedUser = localStorage.getItem('currentUser');
|
||||
if (storedUser) {
|
||||
this.currentUser = JSON.parse(storedUser);
|
||||
} else {
|
||||
throw new Error('사용자 정보 없음');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 스크립트 동적 로드
|
||||
*/
|
||||
async loadAPIScript() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = `/static/js/api.js?v=${Date.now()}`;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 초기화
|
||||
*/
|
||||
initializeUI() {
|
||||
// 사용자 정보 표시
|
||||
this.updateUserDisplay();
|
||||
|
||||
// 네비게이션 메뉴 생성
|
||||
this.createNavigationMenu();
|
||||
|
||||
// 이벤트 리스너 등록
|
||||
this.registerEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 정보 표시 업데이트
|
||||
*/
|
||||
updateUserDisplay() {
|
||||
const userInitial = document.getElementById('userInitial');
|
||||
const userDisplayName = document.getElementById('userDisplayName');
|
||||
const userRole = document.getElementById('userRole');
|
||||
|
||||
const displayName = this.currentUser.full_name || this.currentUser.username;
|
||||
const initial = displayName.charAt(0).toUpperCase();
|
||||
|
||||
userInitial.textContent = initial;
|
||||
userDisplayName.textContent = displayName;
|
||||
userRole.textContent = this.getRoleDisplayName(this.currentUser.role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 표시명 가져오기
|
||||
*/
|
||||
getRoleDisplayName(role) {
|
||||
const roleNames = {
|
||||
'admin': '관리자',
|
||||
'user': '사용자'
|
||||
};
|
||||
return roleNames[role] || role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 메뉴 생성
|
||||
*/
|
||||
createNavigationMenu() {
|
||||
const menuConfig = window.pagePermissionManager.getMenuConfig();
|
||||
const navigationMenu = document.getElementById('navigationMenu');
|
||||
|
||||
navigationMenu.innerHTML = '';
|
||||
|
||||
menuConfig.forEach(item => {
|
||||
const menuItem = this.createMenuItem(item);
|
||||
navigationMenu.appendChild(menuItem);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 아이템 생성
|
||||
*/
|
||||
createMenuItem(item) {
|
||||
const li = document.createElement('li');
|
||||
|
||||
// 단순한 단일 메뉴 아이템만 지원
|
||||
li.innerHTML = `
|
||||
<div class="nav-item p-3 rounded-lg cursor-pointer" onclick="app.navigateTo('${item.path}')">
|
||||
<div class="flex items-center">
|
||||
<i class="${item.icon} mr-3 text-gray-500"></i>
|
||||
<span class="text-gray-700">${item.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 라우터 초기화
|
||||
*/
|
||||
initializeRouter() {
|
||||
// 해시 변경 감지
|
||||
window.addEventListener('hashchange', () => {
|
||||
this.handleRouteChange();
|
||||
});
|
||||
|
||||
// 초기 라우트 처리
|
||||
this.handleRouteChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 라우트 변경 처리
|
||||
*/
|
||||
async handleRouteChange() {
|
||||
const hash = window.location.hash.substring(1) || 'dashboard';
|
||||
const [module, action] = hash.split('/');
|
||||
|
||||
try {
|
||||
await this.loadModule(module, action);
|
||||
this.updateActiveNavigation(hash);
|
||||
this.updatePageTitle(module, action);
|
||||
} catch (error) {
|
||||
console.error('라우트 처리 실패:', error);
|
||||
this.showError('페이지를 로드할 수 없습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 로드
|
||||
*/
|
||||
async loadModule(module, action = 'list') {
|
||||
if (module === 'dashboard') {
|
||||
this.showDashboard();
|
||||
return;
|
||||
}
|
||||
|
||||
// 모듈이 이미 로드되어 있는지 확인
|
||||
if (!this.modules.has(module)) {
|
||||
await this.loadModuleScript(module);
|
||||
}
|
||||
|
||||
// 모듈 실행
|
||||
const moduleInstance = this.modules.get(module);
|
||||
if (moduleInstance && typeof moduleInstance.render === 'function') {
|
||||
const content = await moduleInstance.render(action);
|
||||
this.showDynamicContent(content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 스크립트 로드
|
||||
*/
|
||||
async loadModuleScript(module) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = `/static/js/modules/${module}/${module}.js?v=${Date.now()}`;
|
||||
script.onload = () => {
|
||||
// 모듈이 전역 객체에 등록되었는지 확인
|
||||
const moduleClass = window[module.charAt(0).toUpperCase() + module.slice(1) + 'Module'];
|
||||
if (moduleClass) {
|
||||
this.modules.set(module, new moduleClass());
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 대시보드 표시
|
||||
*/
|
||||
showDashboard() {
|
||||
document.getElementById('dashboard').classList.remove('hidden');
|
||||
document.getElementById('dynamicContent').classList.add('hidden');
|
||||
this.currentPage = 'dashboard';
|
||||
}
|
||||
|
||||
/**
|
||||
* 동적 콘텐츠 표시
|
||||
*/
|
||||
showDynamicContent(content) {
|
||||
document.getElementById('dashboard').classList.add('hidden');
|
||||
const dynamicContent = document.getElementById('dynamicContent');
|
||||
dynamicContent.innerHTML = content;
|
||||
dynamicContent.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 활성화 상태 업데이트
|
||||
*/
|
||||
updateActiveNavigation(hash) {
|
||||
// 모든 네비게이션 아이템에서 active 클래스 제거
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// 현재 페이지에 해당하는 네비게이션 아이템에 active 클래스 추가
|
||||
// 구현 필요
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 제목 업데이트
|
||||
*/
|
||||
updatePageTitle(module, action) {
|
||||
const titles = {
|
||||
'dashboard': '대시보드',
|
||||
'issues': '부적합 사항',
|
||||
'projects': '프로젝트',
|
||||
'daily_work': '일일 공수',
|
||||
'reports': '보고서',
|
||||
'users': '사용자 관리'
|
||||
};
|
||||
|
||||
const title = titles[module] || module;
|
||||
document.getElementById('pageTitle').textContent = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대시보드 데이터 로드
|
||||
*/
|
||||
async loadDashboardData() {
|
||||
try {
|
||||
// 통계 데이터 로드 (임시 데이터)
|
||||
document.getElementById('totalIssues').textContent = '0';
|
||||
document.getElementById('activeProjects').textContent = '0';
|
||||
document.getElementById('monthlyHours').textContent = '0';
|
||||
document.getElementById('completionRate').textContent = '0%';
|
||||
|
||||
// 실제 API 호출로 대체 예정
|
||||
// const stats = await API.getDashboardStats();
|
||||
// this.updateDashboardStats(stats);
|
||||
} catch (error) {
|
||||
console.error('대시보드 데이터 로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 등록
|
||||
*/
|
||||
registerEventListeners() {
|
||||
// 비밀번호 변경 폼
|
||||
document.getElementById('passwordChangeForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.handlePasswordChange();
|
||||
});
|
||||
|
||||
// 모바일 반응형
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
this.hideMobileOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 이동
|
||||
*/
|
||||
navigateTo(path) {
|
||||
window.location.hash = path.startsWith('#') ? path.substring(1) : path;
|
||||
|
||||
// 모바일에서 사이드바 닫기
|
||||
if (window.innerWidth < 768) {
|
||||
this.toggleSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이드바 토글
|
||||
*/
|
||||
toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const mainContent = document.getElementById('mainContent');
|
||||
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||
|
||||
if (window.innerWidth < 768) {
|
||||
// 모바일
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
sidebar.classList.remove('collapsed');
|
||||
mobileOverlay.classList.add('active');
|
||||
} else {
|
||||
sidebar.classList.add('collapsed');
|
||||
mobileOverlay.classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
// 데스크톱
|
||||
if (this.sidebarCollapsed) {
|
||||
sidebar.classList.remove('collapsed');
|
||||
mainContent.classList.remove('expanded');
|
||||
this.sidebarCollapsed = false;
|
||||
} else {
|
||||
sidebar.classList.add('collapsed');
|
||||
mainContent.classList.add('expanded');
|
||||
this.sidebarCollapsed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모바일 오버레이 숨기기
|
||||
*/
|
||||
hideMobileOverlay() {
|
||||
document.getElementById('sidebar').classList.add('collapsed');
|
||||
document.getElementById('mobileOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 모달 표시
|
||||
*/
|
||||
showPasswordChangeModal() {
|
||||
document.getElementById('passwordModal').classList.remove('hidden');
|
||||
document.getElementById('passwordModal').classList.add('flex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 모달 숨기기
|
||||
*/
|
||||
hidePasswordChangeModal() {
|
||||
document.getElementById('passwordModal').classList.add('hidden');
|
||||
document.getElementById('passwordModal').classList.remove('flex');
|
||||
document.getElementById('passwordChangeForm').reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 처리
|
||||
*/
|
||||
async handlePasswordChange() {
|
||||
const currentPassword = document.getElementById('currentPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
this.showError('새 비밀번호가 일치하지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// API 호출 (구현 필요)
|
||||
// await AuthAPI.changePassword(currentPassword, newPassword);
|
||||
|
||||
this.showSuccess('비밀번호가 성공적으로 변경되었습니다.');
|
||||
this.hidePasswordChangeModal();
|
||||
} catch (error) {
|
||||
this.showError('비밀번호 변경에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
*/
|
||||
logout() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
this.redirectToLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 페이지로 리다이렉트
|
||||
*/
|
||||
redirectToLogin() {
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 표시
|
||||
*/
|
||||
showLoading() {
|
||||
document.getElementById('loadingOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 숨기기
|
||||
*/
|
||||
hideLoading() {
|
||||
document.getElementById('loadingOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 메시지 표시
|
||||
*/
|
||||
showSuccess(message) {
|
||||
this.showToast(message, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 메시지 표시
|
||||
*/
|
||||
showError(message) {
|
||||
this.showToast(message, 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* 토스트 메시지 표시
|
||||
*/
|
||||
showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
|
||||
type === 'success' ? 'bg-green-500' :
|
||||
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
|
||||
}`;
|
||||
toast.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'} mr-2"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수들 (HTML에서 호출)
|
||||
function toggleSidebar() {
|
||||
window.app.toggleSidebar();
|
||||
}
|
||||
|
||||
function showPasswordChangeModal() {
|
||||
window.app.showPasswordChangeModal();
|
||||
}
|
||||
|
||||
function hidePasswordChangeModal() {
|
||||
window.app.hidePasswordChangeModal();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
window.app.logout();
|
||||
}
|
||||
|
||||
// 앱 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.app = new App();
|
||||
});
|
||||
58
frontend/test_api.html
Normal file
58
frontend/test_api.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>API 테스트</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>API 테스트 페이지</h1>
|
||||
<button onclick="testLogin()">로그인 테스트</button>
|
||||
<button onclick="testUsers()">사용자 목록 테스트</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
let token = null;
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', 'hyungi');
|
||||
formData.append('password', '123456');
|
||||
|
||||
const response = await fetch('http://localhost:16080/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString()
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
token = data.access_token;
|
||||
document.getElementById('result').innerHTML = `<pre>로그인 성공: ${JSON.stringify(data, null, 2)}</pre>`;
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML = `<pre>로그인 실패: ${error.message}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testUsers() {
|
||||
if (!token) {
|
||||
alert('먼저 로그인하세요');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:16080/api/auth/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
document.getElementById('result').innerHTML = `<pre>사용자 목록: ${JSON.stringify(data, null, 2)}</pre>`;
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML = `<pre>사용자 목록 실패: ${error.message}</pre>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
58
test_api.html
Normal file
58
test_api.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>API 테스트</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>API 테스트 페이지</h1>
|
||||
<button onclick="testLogin()">로그인 테스트</button>
|
||||
<button onclick="testUsers()">사용자 목록 테스트</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
let token = null;
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', 'hyungi');
|
||||
formData.append('password', '123456');
|
||||
|
||||
const response = await fetch('http://localhost:16080/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString()
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
token = data.access_token;
|
||||
document.getElementById('result').innerHTML = `<pre>로그인 성공: ${JSON.stringify(data, null, 2)}</pre>`;
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML = `<pre>로그인 실패: ${error.message}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testUsers() {
|
||||
if (!token) {
|
||||
alert('먼저 로그인하세요');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:16080/api/auth/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
document.getElementById('result').innerHTML = `<pre>사용자 목록: ${JSON.stringify(data, null, 2)}</pre>`;
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML = `<pre>사용자 목록 실패: ${error.message}</pre>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user