feat: tkuser 통합 관리 서비스 + 전체 시스템 SSO 쿠키 인증 통합
- tkuser 서비스 신규 추가 (API + Web) - 사용자/권한/프로젝트/부서/작업자/작업장/설비/작업/휴가 통합 관리 - 작업장 탭: 공장→작업장 드릴다운 네비게이션 + 구역지도 클릭 연동 - 작업 탭: 공정(work_types)→작업(tasks) 계층 관리 - 휴가 탭: 유형 관리 + 연차 배정(근로기준법 자동계산) - 전 시스템 SSO 쿠키 인증으로 통합 (.technicalkorea.net 공유) - System 2: 작업 이슈 리포트 기능 강화 - System 3: tkuser API 연동, 페이지 권한 체계 적용 - docker-compose에 tkuser-api, tkuser-web 서비스 추가 - ARCHITECTURE.md, DEPLOYMENT.md 문서 작성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,11 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De
|
||||
sso_role = payload.get("role", "user")
|
||||
role_map = {
|
||||
"Admin": UserRole.admin,
|
||||
"System Admin": UserRole.admin,
|
||||
"System Admin": UserRole.system,
|
||||
"system": UserRole.system,
|
||||
"admin": UserRole.admin,
|
||||
"support_team": UserRole.support_team,
|
||||
"leader": UserRole.leader,
|
||||
}
|
||||
mapped_role = role_map.get(sso_role, UserRole.user)
|
||||
|
||||
@@ -50,7 +54,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De
|
||||
return sso_user
|
||||
|
||||
async def get_current_admin(current_user: User = Depends(get_current_user)):
|
||||
if current_user.role != UserRole.admin:
|
||||
if current_user.role not in [UserRole.admin, UserRole.system]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from database.database import get_db
|
||||
from database.models import Issue, User, Project, ReviewStatus, DisposalReasonType
|
||||
from database.models import Issue, User, ReviewStatus, DisposalReasonType
|
||||
from utils.tkuser_client import get_token_from_request
|
||||
import utils.tkuser_client as tkuser_client
|
||||
from database.schemas import (
|
||||
InboxIssue, IssueDisposalRequest, IssueReviewRequest,
|
||||
IssueStatusUpdateRequest, ModificationLogEntry, ManagementUpdateRequest
|
||||
@@ -123,6 +125,7 @@ async def dispose_issue(
|
||||
async def review_issue(
|
||||
issue_id: int,
|
||||
review_request: IssueReviewRequest,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user), # 수신함 권한이 있는 사용자
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -157,9 +160,10 @@ async def review_issue(
|
||||
|
||||
# 프로젝트 변경
|
||||
if review_request.project_id is not None and review_request.project_id != issue.project_id:
|
||||
# 프로젝트 존재 확인
|
||||
# 프로젝트 존재 확인 (tkuser API)
|
||||
if review_request.project_id != 0: # 0은 프로젝트 없음을 의미
|
||||
project = db.query(Project).filter(Project.id == review_request.project_id).first()
|
||||
token = get_token_from_request(request)
|
||||
project = await tkuser_client.get_project(token, review_request.project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=400, detail="존재하지 않는 프로젝트입니다.")
|
||||
|
||||
@@ -264,12 +268,11 @@ async def update_issue_status(
|
||||
# 진행 중 또는 완료 상태로 변경 시 프로젝트별 순번 자동 할당
|
||||
if status_request.review_status in [ReviewStatus.in_progress, ReviewStatus.completed]:
|
||||
if not issue.project_sequence_no:
|
||||
from sqlalchemy import text
|
||||
result = db.execute(
|
||||
text("SELECT generate_project_sequence_no(:project_id)"),
|
||||
{"project_id": issue.project_id}
|
||||
)
|
||||
issue.project_sequence_no = result.scalar()
|
||||
from sqlalchemy import func
|
||||
max_seq = db.query(func.coalesce(func.max(Issue.project_sequence_no), 0)).filter(
|
||||
Issue.project_id == issue.project_id
|
||||
).scalar()
|
||||
issue.project_sequence_no = max_seq + 1
|
||||
|
||||
# 완료 사진 업로드 처리
|
||||
if status_request.completion_photo and status_request.review_status == ReviewStatus.completed:
|
||||
|
||||
@@ -259,9 +259,9 @@ async def get_issue_stats(
|
||||
query = query.filter(Issue.reporter_id == current_user.id)
|
||||
|
||||
total = query.count()
|
||||
new = query.filter(Issue.status == IssueStatus.NEW).count()
|
||||
progress = query.filter(Issue.status == IssueStatus.PROGRESS).count()
|
||||
complete = query.filter(Issue.status == IssueStatus.COMPLETE).count()
|
||||
new = query.filter(Issue.status == IssueStatus.new).count()
|
||||
progress = query.filter(Issue.status == IssueStatus.progress).count()
|
||||
complete = query.filter(Issue.status == IssueStatus.complete).count()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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 fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from database.models import User, UserRole
|
||||
from database.schemas import ProjectCreate, ProjectUpdate
|
||||
from routers.auth import get_current_user
|
||||
from utils.tkuser_client import get_token_from_request
|
||||
import utils.tkuser_client as tkuser_client
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/projects",
|
||||
@@ -25,57 +24,36 @@ async def projects_options():
|
||||
"""OPTIONS preflight 요청 처리"""
|
||||
return {"message": "OK"}
|
||||
|
||||
@router.post("/", response_model=ProjectSchema)
|
||||
@router.post("/")
|
||||
async def create_project(
|
||||
project: ProjectCreate,
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
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
|
||||
"""프로젝트 생성 (관리자만) - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
return await tkuser_client.create_project(token, project.dict())
|
||||
|
||||
@router.get("/", response_model=List[ProjectSchema])
|
||||
@router.get("/")
|
||||
async def get_projects(
|
||||
request: Request,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
active_only: bool = True,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""프로젝트 목록 조회"""
|
||||
query = db.query(Project)
|
||||
|
||||
if active_only:
|
||||
query = query.filter(Project.is_active == True)
|
||||
|
||||
projects = query.offset(skip).limit(limit).all()
|
||||
return projects
|
||||
"""프로젝트 목록 조회 - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
projects = await tkuser_client.get_projects(token, active_only=active_only)
|
||||
return projects[skip:skip + limit]
|
||||
|
||||
@router.get("/{project_id}", response_model=ProjectSchema)
|
||||
@router.get("/{project_id}")
|
||||
async def get_project(
|
||||
project_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
):
|
||||
"""특정 프로젝트 조회"""
|
||||
project = db.query(Project).filter(Project.id == project_id).first()
|
||||
"""특정 프로젝트 조회 - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
project = await tkuser_client.get_project(token, project_id)
|
||||
if not project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -83,47 +61,26 @@ async def get_project(
|
||||
)
|
||||
return project
|
||||
|
||||
@router.put("/{project_id}", response_model=ProjectSchema)
|
||||
@router.put("/{project_id}")
|
||||
async def update_project(
|
||||
project_id: int,
|
||||
project_update: ProjectUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
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
|
||||
"""프로젝트 수정 (관리자만) - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
return await tkuser_client.update_project(
|
||||
token, project_id, project_update.dict(exclude_unset=True)
|
||||
)
|
||||
|
||||
@router.delete("/{project_id}")
|
||||
async def delete_project(
|
||||
project_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
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()
|
||||
|
||||
"""프로젝트 삭제 (비활성화) (관리자만) - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
await tkuser_client.delete_project(token, project_id)
|
||||
return {"message": "프로젝트가 삭제되었습니다."}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_, or_
|
||||
@@ -13,9 +13,11 @@ from openpyxl.drawing.image import Image as XLImage
|
||||
import os
|
||||
|
||||
from database.database import get_db
|
||||
from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole, Project, ReviewStatus
|
||||
from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole, ReviewStatus
|
||||
from database import schemas
|
||||
from routers.auth import get_current_user
|
||||
from utils.tkuser_client import get_token_from_request
|
||||
import utils.tkuser_client as tkuser_client
|
||||
|
||||
router = APIRouter(prefix="/api/reports", tags=["reports"])
|
||||
|
||||
@@ -140,6 +142,7 @@ async def get_report_daily_works(
|
||||
@router.get("/daily-preview")
|
||||
async def preview_daily_report(
|
||||
project_id: int,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -149,8 +152,9 @@ async def preview_daily_report(
|
||||
if current_user.role != UserRole.admin:
|
||||
raise HTTPException(status_code=403, detail="품질팀만 접근 가능합니다")
|
||||
|
||||
# 프로젝트 확인
|
||||
project = db.query(Project).filter(Project.id == project_id).first()
|
||||
# 프로젝트 확인 (tkuser API)
|
||||
token = get_token_from_request(request)
|
||||
project = await tkuser_client.get_project(token, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
|
||||
|
||||
@@ -178,7 +182,7 @@ async def preview_daily_report(
|
||||
issues_data = [schemas.Issue.from_orm(issue) for issue in issues]
|
||||
|
||||
return {
|
||||
"project": schemas.Project.from_orm(project),
|
||||
"project": project,
|
||||
"stats": stats,
|
||||
"issues": issues_data,
|
||||
"total_issues": len(issues)
|
||||
@@ -186,31 +190,33 @@ async def preview_daily_report(
|
||||
|
||||
@router.post("/daily-export")
|
||||
async def export_daily_report(
|
||||
request: schemas.DailyReportRequest,
|
||||
report_req: schemas.DailyReportRequest,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""품질팀용 일일보고서 엑셀 내보내기"""
|
||||
|
||||
|
||||
# 권한 확인 (품질팀만 접근 가능)
|
||||
if current_user.role != UserRole.admin:
|
||||
raise HTTPException(status_code=403, detail="품질팀만 접근 가능합니다")
|
||||
|
||||
# 프로젝트 확인
|
||||
project = db.query(Project).filter(Project.id == request.project_id).first()
|
||||
|
||||
# 프로젝트 확인 (tkuser API)
|
||||
token = get_token_from_request(request)
|
||||
project = await tkuser_client.get_project(token, report_req.project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
|
||||
|
||||
# 관리함 데이터 조회
|
||||
# 1. 진행 중인 항목 (모두 포함)
|
||||
in_progress_only = db.query(Issue).filter(
|
||||
Issue.project_id == request.project_id,
|
||||
Issue.project_id == report_req.project_id,
|
||||
Issue.review_status == ReviewStatus.in_progress
|
||||
).all()
|
||||
|
||||
# 2. 완료된 항목 (모두 조회)
|
||||
all_completed = db.query(Issue).filter(
|
||||
Issue.project_id == request.project_id,
|
||||
Issue.project_id == report_req.project_id,
|
||||
Issue.review_status == ReviewStatus.completed
|
||||
).all()
|
||||
|
||||
@@ -307,7 +313,7 @@ async def export_daily_report(
|
||||
for ws, sheet_issues, sheet_title in sheets_data:
|
||||
# 제목 및 기본 정보
|
||||
ws.merge_cells('A1:L1')
|
||||
ws['A1'] = f"{project.project_name} - {sheet_title}"
|
||||
ws['A1'] = f"{project['project_name']} - {sheet_title}"
|
||||
ws['A1'].font = Font(bold=True, size=16)
|
||||
ws['A1'].alignment = center_alignment
|
||||
|
||||
@@ -725,7 +731,7 @@ async def export_daily_report(
|
||||
|
||||
# 파일명 생성
|
||||
today = date.today().strftime('%Y%m%d')
|
||||
filename = f"{project.project_name}_일일보고서_{today}.xlsx"
|
||||
filename = f"{project['project_name']}_일일보고서_{today}.xlsx"
|
||||
|
||||
# 한글 파일명을 위한 URL 인코딩
|
||||
from urllib.parse import quote
|
||||
|
||||
Reference in New Issue
Block a user