feat: 목록 관리 및 보고서 페이지 개선
- 목록 관리 페이지에 고급 필터링 시스템 추가 - 프로젝트별, 검토상태별, 날짜별 필터링 - 검토 완료/필요 항목 시각적 구분 및 정렬 - 해결 시간 입력 + 확인 버튼으로 검토 완료 처리 - 부적합 조회 페이지에 동일한 필터링 기능 적용 - 검토 상태에 따른 카드 스타일링 (음영 처리) - JavaScript 템플릿 리터럴 오류 수정 - 보고서 페이지 프로젝트별 분석 기능 추가 - 프로젝트 선택 드롭다운 추가 - 총 작업 공수를 프로젝트별 일일공수 데이터로 계산 - 부적합 처리 시간, 카테고리 분석, 상세 목록 모두 프로젝트별 필터링 - localStorage 키 이름 통일 (daily-work-data)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum
|
||||
from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Float, Boolean, Text, ForeignKey, Enum
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone, timedelta
|
||||
@@ -42,6 +42,7 @@ class User(Base):
|
||||
# Relationships
|
||||
issues = relationship("Issue", back_populates="reporter")
|
||||
daily_works = relationship("DailyWork", back_populates="created_by")
|
||||
projects = relationship("Project", back_populates="created_by")
|
||||
|
||||
class Issue(Base):
|
||||
__tablename__ = "issues"
|
||||
@@ -53,12 +54,28 @@ class Issue(Base):
|
||||
description = Column(Text, nullable=False)
|
||||
status = Column(Enum(IssueStatus), default=IssueStatus.new)
|
||||
reporter_id = Column(Integer, ForeignKey("users.id"))
|
||||
project_id = Column(BigInteger, ForeignKey("projects.id"))
|
||||
report_date = Column(DateTime, default=get_kst_now)
|
||||
work_hours = Column(Float, default=0)
|
||||
detail_notes = Column(Text)
|
||||
|
||||
# Relationships
|
||||
reporter = relationship("User", back_populates="issues")
|
||||
project = relationship("Project", back_populates="issues")
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
job_no = Column(String, unique=True, nullable=False, index=True)
|
||||
project_name = Column(String, nullable=False)
|
||||
created_by_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_at = Column(DateTime, default=get_kst_now)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
created_by = relationship("User", back_populates="projects")
|
||||
issues = relationship("Issue", back_populates="project")
|
||||
|
||||
class DailyWork(Base):
|
||||
__tablename__ = "daily_works"
|
||||
|
||||
@@ -62,6 +62,7 @@ class LoginRequest(BaseModel):
|
||||
class IssueBase(BaseModel):
|
||||
category: IssueCategory
|
||||
description: str
|
||||
project_id: Optional[int] = None
|
||||
|
||||
class IssueCreate(IssueBase):
|
||||
photo: Optional[str] = None # Base64 encoded image
|
||||
@@ -70,6 +71,7 @@ class IssueCreate(IssueBase):
|
||||
class IssueUpdate(BaseModel):
|
||||
category: Optional[IssueCategory] = None
|
||||
description: Optional[str] = None
|
||||
project_id: Optional[int] = None
|
||||
work_hours: Optional[float] = None
|
||||
detail_notes: Optional[str] = None
|
||||
status: Optional[IssueStatus] = None
|
||||
@@ -83,6 +85,8 @@ class Issue(IssueBase):
|
||||
status: IssueStatus
|
||||
reporter_id: int
|
||||
reporter: User
|
||||
project_id: Optional[int] = None
|
||||
# project: Optional['Project'] = None # 순환 참조 방지를 위해 제거
|
||||
report_date: datetime
|
||||
work_hours: float
|
||||
detail_notes: Optional[str] = None
|
||||
@@ -90,6 +94,29 @@ class Issue(IssueBase):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Project schemas
|
||||
class ProjectBase(BaseModel):
|
||||
job_no: str = Field(..., min_length=1, max_length=50)
|
||||
project_name: str = Field(..., min_length=1, max_length=200)
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
pass
|
||||
|
||||
class ProjectUpdate(BaseModel):
|
||||
project_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class Project(ProjectBase):
|
||||
id: int
|
||||
created_by_id: int
|
||||
created_by: User
|
||||
created_at: datetime
|
||||
is_active: bool
|
||||
# issues: Optional[List['Issue']] = None # 순환 참조 방지를 위해 제거
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Daily Work schemas
|
||||
class DailyWorkBase(BaseModel):
|
||||
date: datetime
|
||||
|
||||
@@ -5,7 +5,7 @@ import uvicorn
|
||||
|
||||
from database.database import engine, get_db
|
||||
from database.models import Base
|
||||
from routers import auth, issues, daily_work, reports
|
||||
from routers import auth, issues, daily_work, reports, projects
|
||||
from services.auth_service import create_admin_user
|
||||
|
||||
# 데이터베이스 테이블 생성
|
||||
@@ -34,6 +34,7 @@ app.include_router(auth.router)
|
||||
app.include_router(issues.router)
|
||||
app.include_router(daily_work.router)
|
||||
app.include_router(reports.router)
|
||||
app.include_router(projects.router)
|
||||
|
||||
# 시작 시 관리자 계정 생성
|
||||
@app.on_event("startup")
|
||||
|
||||
14
backend/migrations/006_add_projects_table.sql
Normal file
14
backend/migrations/006_add_projects_table.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- 프로젝트 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_no VARCHAR(50) UNIQUE NOT NULL,
|
||||
project_name VARCHAR(200) NOT NULL,
|
||||
created_by_id INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_job_no ON projects(job_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_created_by_id ON projects(created_by_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_is_active ON projects(is_active);
|
||||
15
backend/migrations/007_add_project_id_to_issues.sql
Normal file
15
backend/migrations/007_add_project_id_to_issues.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- 부적합 사항 테이블에 프로젝트 ID 컬럼 추가
|
||||
ALTER TABLE issues ADD COLUMN project_id INTEGER;
|
||||
|
||||
-- 외래키 제약조건 추가
|
||||
ALTER TABLE issues ADD CONSTRAINT fk_issues_project_id
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_issues_project_id ON issues(project_id);
|
||||
|
||||
-- 기존 부적합 사항들을 첫 번째 프로젝트로 할당 (있는 경우)
|
||||
UPDATE issues
|
||||
SET project_id = (SELECT id FROM projects ORDER BY created_at LIMIT 1)
|
||||
WHERE project_id IS NULL
|
||||
AND EXISTS (SELECT 1 FROM projects LIMIT 1);
|
||||
13
backend/migrations/008_fix_project_id_bigint.sql
Normal file
13
backend/migrations/008_fix_project_id_bigint.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- project_id 컬럼을 BIGINT로 변경
|
||||
ALTER TABLE issues ALTER COLUMN project_id TYPE BIGINT;
|
||||
|
||||
-- projects 테이블의 id도 BIGINT로 변경 (일관성을 위해)
|
||||
ALTER TABLE projects ALTER COLUMN id TYPE BIGINT;
|
||||
|
||||
-- 외래키 제약조건 재생성 (타입 변경으로 인해 필요)
|
||||
ALTER TABLE issues DROP CONSTRAINT IF EXISTS fk_issues_project_id;
|
||||
ALTER TABLE issues ADD CONSTRAINT fk_issues_project_id
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id);
|
||||
|
||||
-- 다른 테이블들도 확인하여 project_id 참조하는 곳이 있으면 수정
|
||||
-- (현재는 issues 테이블만 project_id를 가지고 있음)
|
||||
BIN
backend/routers/__pycache__/projects.cpython-311.pyc
Normal file
BIN
backend/routers/__pycache__/projects.cpython-311.pyc
Normal file
Binary file not shown.
126
backend/routers/projects.py
Normal file
126
backend/routers/projects.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from database.database import get_db
|
||||
from database.models import Project, User, UserRole
|
||||
from database.schemas import ProjectCreate, ProjectUpdate, Project as ProjectSchema
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/projects",
|
||||
tags=["projects"]
|
||||
)
|
||||
|
||||
def check_admin_permission(current_user: User = Depends(get_current_user)):
|
||||
"""관리자 권한 확인"""
|
||||
if current_user.role != UserRole.admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="관리자 권한이 필요합니다."
|
||||
)
|
||||
return current_user
|
||||
|
||||
@router.post("/", response_model=ProjectSchema)
|
||||
async def create_project(
|
||||
project: ProjectCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(check_admin_permission)
|
||||
):
|
||||
"""프로젝트 생성 (관리자만)"""
|
||||
# Job No. 중복 확인
|
||||
existing_project = db.query(Project).filter(Project.job_no == project.job_no).first()
|
||||
if existing_project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="이미 존재하는 Job No.입니다."
|
||||
)
|
||||
|
||||
# 프로젝트 생성
|
||||
db_project = Project(
|
||||
job_no=project.job_no,
|
||||
project_name=project.project_name,
|
||||
created_by_id=current_user.id
|
||||
)
|
||||
|
||||
db.add(db_project)
|
||||
db.commit()
|
||||
db.refresh(db_project)
|
||||
|
||||
return db_project
|
||||
|
||||
@router.get("/", response_model=List[ProjectSchema])
|
||||
async def get_projects(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
active_only: bool = True,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""프로젝트 목록 조회"""
|
||||
query = db.query(Project)
|
||||
|
||||
if active_only:
|
||||
query = query.filter(Project.is_active == True)
|
||||
|
||||
projects = query.offset(skip).limit(limit).all()
|
||||
return projects
|
||||
|
||||
@router.get("/{project_id}", response_model=ProjectSchema)
|
||||
async def get_project(
|
||||
project_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""특정 프로젝트 조회"""
|
||||
project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="프로젝트를 찾을 수 없습니다."
|
||||
)
|
||||
return project
|
||||
|
||||
@router.put("/{project_id}", response_model=ProjectSchema)
|
||||
async def update_project(
|
||||
project_id: int,
|
||||
project_update: ProjectUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(check_admin_permission)
|
||||
):
|
||||
"""프로젝트 수정 (관리자만)"""
|
||||
project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="프로젝트를 찾을 수 없습니다."
|
||||
)
|
||||
|
||||
# 업데이트할 필드만 수정
|
||||
update_data = project_update.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(project, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
|
||||
return project
|
||||
|
||||
@router.delete("/{project_id}")
|
||||
async def delete_project(
|
||||
project_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(check_admin_permission)
|
||||
):
|
||||
"""프로젝트 삭제 (비활성화) (관리자만)"""
|
||||
project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="프로젝트를 찾을 수 없습니다."
|
||||
)
|
||||
|
||||
# 실제 삭제 대신 비활성화
|
||||
project.is_active = False
|
||||
db.commit()
|
||||
|
||||
return {"message": "프로젝트가 삭제되었습니다."}
|
||||
Reference in New Issue
Block a user