feat: 목록 관리 및 보고서 페이지 개선

- 목록 관리 페이지에 고급 필터링 시스템 추가
  - 프로젝트별, 검토상태별, 날짜별 필터링
  - 검토 완료/필요 항목 시각적 구분 및 정렬
  - 해결 시간 입력 + 확인 버튼으로 검토 완료 처리

- 부적합 조회 페이지에 동일한 필터링 기능 적용
  - 검토 상태에 따른 카드 스타일링 (음영 처리)
  - JavaScript 템플릿 리터럴 오류 수정

- 보고서 페이지 프로젝트별 분석 기능 추가
  - 프로젝트 선택 드롭다운 추가
  - 총 작업 공수를 프로젝트별 일일공수 데이터로 계산
  - 부적합 처리 시간, 카테고리 분석, 상세 목록 모두 프로젝트별 필터링
  - localStorage 키 이름 통일 (daily-work-data)
This commit is contained in:
hyungi
2025-10-24 10:13:32 +09:00
parent 44e2fb2e44
commit b024a178d0
25 changed files with 3498 additions and 307 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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")

View 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);

View 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);

View 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를 가지고 있음)

Binary file not shown.

126
backend/routers/projects.py Normal file
View 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": "프로젝트가 삭제되었습니다."}