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:
@@ -3,10 +3,18 @@ from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
import os
|
||||
from typing import Generator
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://mproject:mproject2024@localhost:5432/mproject")
|
||||
# DB 설정 — 개별 환경변수에서 읽어서 URL 구성 (비밀번호 특수문자 처리)
|
||||
DB_HOST = os.getenv("DB_HOST", "mariadb")
|
||||
DB_PORT = os.getenv("DB_PORT", "3306")
|
||||
DB_USER = os.getenv("DB_USER", "hyungi_user")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD", "password")
|
||||
DB_NAME = os.getenv("DB_NAME", "hyungi")
|
||||
|
||||
engine = create_engine(DATABASE_URL)
|
||||
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{quote_plus(DB_PASSWORD)}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
|
||||
|
||||
engine = create_engine(DATABASE_URL, pool_recycle=3600)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Float, Boolean, Text, ForeignKey, Enum, Index
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Float, Boolean, Text, ForeignKey, Enum, Index, JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone, timedelta
|
||||
@@ -15,8 +14,11 @@ def get_kst_now():
|
||||
Base = declarative_base()
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
admin = "admin" # 관리자
|
||||
user = "user" # 일반 사용자
|
||||
system = "system" # 시스템 관리자
|
||||
admin = "admin" # 관리자
|
||||
support_team = "support_team" # 지원팀
|
||||
leader = "leader" # 리더
|
||||
user = "user" # 일반 사용자
|
||||
|
||||
class IssueStatus(str, enum.Enum):
|
||||
new = "new"
|
||||
@@ -51,77 +53,76 @@ class DepartmentType(str, enum.Enum):
|
||||
sales = "sales" # 영업
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
full_name = Column(String)
|
||||
__tablename__ = "sso_users"
|
||||
|
||||
# Column mapping: Python attr → DB column
|
||||
id = Column("user_id", Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
hashed_password = Column("password_hash", String(255), nullable=False)
|
||||
full_name = Column("name", String(100))
|
||||
role = Column(Enum(UserRole), default=UserRole.user)
|
||||
department = Column(Enum(DepartmentType)) # 부서 정보 추가
|
||||
department = Column(String(50))
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=get_kst_now)
|
||||
|
||||
|
||||
# Relationships
|
||||
issues = relationship("Issue", back_populates="reporter", foreign_keys="Issue.reporter_id")
|
||||
reviewed_issues = relationship("Issue", foreign_keys="Issue.reviewed_by_id")
|
||||
daily_works = relationship("DailyWork", back_populates="created_by")
|
||||
projects = relationship("Project", back_populates="created_by")
|
||||
page_permissions = relationship("UserPagePermission", back_populates="user", foreign_keys="UserPagePermission.user_id")
|
||||
|
||||
class UserPagePermission(Base):
|
||||
__tablename__ = "user_page_permissions"
|
||||
|
||||
__tablename__ = "qc_user_page_permissions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("sso_users.user_id", ondelete="CASCADE"), nullable=False)
|
||||
page_name = Column(String(50), nullable=False)
|
||||
can_access = Column(Boolean, default=False)
|
||||
granted_by_id = Column(Integer, ForeignKey("users.id"))
|
||||
granted_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
|
||||
granted_at = Column(DateTime, default=get_kst_now)
|
||||
notes = Column(Text)
|
||||
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="page_permissions", foreign_keys=[user_id])
|
||||
granted_by = relationship("User", foreign_keys=[granted_by_id], post_update=True)
|
||||
|
||||
# Unique constraint
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_user_page_permissions_user_id', 'user_id'),
|
||||
Index('idx_user_page_permissions_page_name', 'page_name'),
|
||||
Index('idx_qc_user_page_perm_user_id', 'user_id'),
|
||||
Index('idx_qc_user_page_perm_page_name', 'page_name'),
|
||||
)
|
||||
|
||||
class Issue(Base):
|
||||
__tablename__ = "issues"
|
||||
|
||||
__tablename__ = "qc_issues"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
photo_path = Column(String)
|
||||
photo_path2 = Column(String)
|
||||
photo_path3 = Column(String)
|
||||
photo_path4 = Column(String)
|
||||
photo_path5 = Column(String)
|
||||
photo_path = Column(String(500))
|
||||
photo_path2 = Column(String(500))
|
||||
photo_path3 = Column(String(500))
|
||||
photo_path4 = Column(String(500))
|
||||
photo_path5 = Column(String(500))
|
||||
category = Column(Enum(IssueCategory), nullable=False)
|
||||
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"))
|
||||
reporter_id = Column(Integer, ForeignKey("sso_users.user_id"))
|
||||
project_id = Column(Integer) # FK 제거 — projects는 tkuser에서 관리
|
||||
report_date = Column(DateTime, default=get_kst_now)
|
||||
work_hours = Column(Float, default=0)
|
||||
detail_notes = Column(Text)
|
||||
|
||||
|
||||
# 수신함 워크플로우 관련 컬럼들
|
||||
review_status = Column(Enum(ReviewStatus), default=ReviewStatus.pending_review)
|
||||
disposal_reason = Column(Enum(DisposalReasonType))
|
||||
custom_disposal_reason = Column(Text)
|
||||
disposed_at = Column(DateTime)
|
||||
reviewed_by_id = Column(Integer, ForeignKey("users.id"))
|
||||
reviewed_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
|
||||
reviewed_at = Column(DateTime)
|
||||
original_data = Column(JSONB) # 원본 데이터 보존
|
||||
modification_log = Column(JSONB, default=lambda: []) # 수정 이력
|
||||
|
||||
original_data = Column(JSON)
|
||||
modification_log = Column(JSON, default=lambda: [])
|
||||
|
||||
# 중복 신고 추적 시스템
|
||||
duplicate_of_issue_id = Column(Integer, ForeignKey("issues.id")) # 중복 대상 이슈 ID
|
||||
duplicate_reporters = Column(JSONB, default=lambda: []) # 중복 신고자 목록
|
||||
|
||||
duplicate_of_issue_id = Column(Integer, ForeignKey("qc_issues.id"))
|
||||
duplicate_reporters = Column(JSON, default=lambda: [])
|
||||
|
||||
# 관리함에서 사용할 추가 필드들
|
||||
solution = Column(Text) # 해결방안 (관리함에서 입력)
|
||||
responsible_department = Column(Enum(DepartmentType)) # 담당부서
|
||||
@@ -133,16 +134,16 @@ class Issue(Base):
|
||||
project_sequence_no = Column(Integer) # 프로젝트별 순번 (No)
|
||||
final_description = Column(Text) # 최종 내용 (수정본 또는 원본)
|
||||
final_category = Column(Enum(IssueCategory)) # 최종 카테고리 (수정본 또는 원본)
|
||||
|
||||
|
||||
# 추가 정보 필드들 (관리함에서 기록용)
|
||||
responsible_person_detail = Column(String(200)) # 해당자 상세 정보
|
||||
cause_detail = Column(Text) # 원인 상세 정보
|
||||
additional_info_updated_at = Column(DateTime) # 추가 정보 입력 시간
|
||||
additional_info_updated_by_id = Column(Integer, ForeignKey("users.id")) # 추가 정보 입력자
|
||||
|
||||
additional_info_updated_by_id = Column(Integer, ForeignKey("sso_users.user_id")) # 추가 정보 입력자
|
||||
|
||||
# 완료 신청 관련 필드들
|
||||
completion_requested_at = Column(DateTime) # 완료 신청 시간
|
||||
completion_requested_by_id = Column(Integer, ForeignKey("users.id")) # 완료 신청자
|
||||
completion_requested_by_id = Column(Integer, ForeignKey("sso_users.user_id")) # 완료 신청자
|
||||
completion_photo_path = Column(String(500)) # 완료 사진 1
|
||||
completion_photo_path2 = Column(String(500)) # 완료 사진 2
|
||||
completion_photo_path3 = Column(String(500)) # 완료 사진 3
|
||||
@@ -152,7 +153,7 @@ class Issue(Base):
|
||||
|
||||
# 완료 반려 관련 필드들
|
||||
completion_rejected_at = Column(DateTime) # 완료 반려 시간
|
||||
completion_rejected_by_id = Column(Integer, ForeignKey("users.id")) # 완료 반려자
|
||||
completion_rejected_by_id = Column(Integer, ForeignKey("sso_users.user_id")) # 완료 반려자
|
||||
completion_rejection_reason = Column(Text) # 완료 반려 사유
|
||||
|
||||
# 일일보고서 추출 이력
|
||||
@@ -162,26 +163,29 @@ class Issue(Base):
|
||||
# Relationships
|
||||
reporter = relationship("User", back_populates="issues", foreign_keys=[reporter_id])
|
||||
reviewer = relationship("User", foreign_keys=[reviewed_by_id], overlaps="reviewed_issues")
|
||||
project = relationship("Project", back_populates="issues")
|
||||
project = relationship("Project", back_populates="issues",
|
||||
primaryjoin="Issue.project_id == Project.id",
|
||||
foreign_keys=[project_id])
|
||||
duplicate_of = relationship("Issue", remote_side=[id], foreign_keys=[duplicate_of_issue_id])
|
||||
|
||||
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)
|
||||
|
||||
# Column mapping: Python attr id → DB column project_id
|
||||
id = Column("project_id", Integer, primary_key=True, index=True)
|
||||
job_no = Column(String(50), unique=True, nullable=False, index=True)
|
||||
project_name = Column(String(255), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
created_at = Column(DateTime, default=get_kst_now)
|
||||
|
||||
# Relationships
|
||||
created_by = relationship("User", back_populates="projects")
|
||||
issues = relationship("Issue", back_populates="project")
|
||||
issues = relationship("Issue", back_populates="project",
|
||||
primaryjoin="Project.id == Issue.project_id",
|
||||
foreign_keys="[Issue.project_id]")
|
||||
|
||||
class DailyWork(Base):
|
||||
__tablename__ = "daily_works"
|
||||
|
||||
__tablename__ = "qc_daily_works"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
date = Column(DateTime, nullable=False, index=True)
|
||||
worker_count = Column(Integer, nullable=False)
|
||||
@@ -190,20 +194,20 @@ class DailyWork(Base):
|
||||
overtime_hours = Column(Float, default=0)
|
||||
overtime_total = Column(Float, default=0)
|
||||
total_hours = Column(Float, nullable=False)
|
||||
created_by_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
|
||||
created_at = Column(DateTime, default=get_kst_now)
|
||||
|
||||
|
||||
# Relationships
|
||||
created_by = relationship("User", back_populates="daily_works")
|
||||
|
||||
class ProjectDailyWork(Base):
|
||||
__tablename__ = "project_daily_works"
|
||||
__tablename__ = "qc_project_daily_works"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
date = Column(DateTime, nullable=False, index=True)
|
||||
project_id = Column(BigInteger, ForeignKey("projects.id"), nullable=False)
|
||||
project_id = Column(Integer, ForeignKey("projects.project_id"), nullable=False)
|
||||
hours = Column(Float, nullable=False)
|
||||
created_by_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
|
||||
created_at = Column(DateTime, default=get_kst_now)
|
||||
|
||||
# Relationships
|
||||
@@ -211,13 +215,13 @@ class ProjectDailyWork(Base):
|
||||
created_by = relationship("User")
|
||||
|
||||
class DeletionLog(Base):
|
||||
__tablename__ = "deletion_logs"
|
||||
__tablename__ = "qc_deletion_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_type = Column(String(50), nullable=False) # 'issue', 'project', 'daily_work' 등
|
||||
entity_id = Column(Integer, nullable=False) # 삭제된 엔티티의 ID
|
||||
entity_data = Column(JSONB, nullable=False) # 삭제된 데이터 전체 (JSON)
|
||||
deleted_by_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
entity_data = Column(JSON, nullable=False) # 삭제된 데이터 전체 (JSON)
|
||||
deleted_by_id = Column(Integer, ForeignKey("sso_users.user_id"), nullable=False)
|
||||
deleted_at = Column(DateTime, default=get_kst_now, nullable=False)
|
||||
reason = Column(Text) # 삭제 사유 (선택사항)
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ from typing import Optional, List, Dict, Any
|
||||
from enum import Enum
|
||||
|
||||
class UserRole(str, Enum):
|
||||
system = "system"
|
||||
admin = "admin"
|
||||
support_team = "support_team"
|
||||
leader = "leader"
|
||||
user = "user"
|
||||
|
||||
class IssueStatus(str, Enum):
|
||||
@@ -285,12 +288,11 @@ class ProjectUpdate(BaseModel):
|
||||
|
||||
class Project(ProjectBase):
|
||||
id: int
|
||||
created_by_id: int
|
||||
created_by: User
|
||||
created_by_id: Optional[int] = None
|
||||
created_by: Optional[User] = None
|
||||
created_at: datetime
|
||||
is_active: bool
|
||||
# issues: Optional[List['Issue']] = None # 순환 참조 방지를 위해 제거
|
||||
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ from database.models import Base
|
||||
from routers import auth, issues, daily_work, reports, projects, page_permissions, inbox, management
|
||||
from services.auth_service import create_admin_user
|
||||
|
||||
# 데이터베이스 테이블 생성
|
||||
# 메타데이터 캐시 클리어
|
||||
Base.metadata.clear()
|
||||
Base.metadata.create_all(bind=engine)
|
||||
# 데이터베이스 테이블 생성 (sso_users, projects는 이미 존재하므로 제외)
|
||||
tables_to_create = [
|
||||
table for name, table in Base.metadata.tables.items()
|
||||
if name not in ("sso_users", "projects")
|
||||
]
|
||||
Base.metadata.create_all(bind=engine, tables=tables_to_create)
|
||||
|
||||
# FastAPI 앱 생성
|
||||
app = FastAPI(
|
||||
|
||||
@@ -5,7 +5,7 @@ python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
bcrypt==4.1.2
|
||||
sqlalchemy==2.0.23
|
||||
psycopg2-binary==2.9.9
|
||||
pymysql==1.1.0
|
||||
alembic==1.12.1
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
@@ -13,3 +13,4 @@ pillow==10.1.0
|
||||
pillow-heif==0.13.0
|
||||
reportlab==4.0.7
|
||||
openpyxl==3.1.2
|
||||
httpx==0.27.0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -75,21 +75,11 @@ def authenticate_user(db: Session, username: str, password: str):
|
||||
return user
|
||||
|
||||
def create_admin_user(db: Session):
|
||||
"""초기 관리자 계정 생성"""
|
||||
"""관리자 계정 확인 (SSO에서 관리, 여기서는 조회만)"""
|
||||
admin_username = os.getenv("ADMIN_USERNAME", "hyungi")
|
||||
admin_password = os.getenv("ADMIN_PASSWORD", "djg3-jj34-X3Q3")
|
||||
|
||||
existing_admin = db.query(User).filter(User.username == admin_username).first()
|
||||
if not existing_admin:
|
||||
admin_user = User(
|
||||
username=admin_username,
|
||||
hashed_password=get_password_hash(admin_password),
|
||||
full_name="관리자",
|
||||
role=UserRole.admin,
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin_user)
|
||||
db.commit()
|
||||
print(f"관리자 계정 생성됨: {admin_username}")
|
||||
if existing_admin:
|
||||
print(f"관리자 계정 확인됨: {admin_username} (role: {existing_admin.role.value})")
|
||||
else:
|
||||
print(f"관리자 계정이 이미 존재함: {admin_username}")
|
||||
print(f"경고: 관리자 계정이 sso_users에 없습니다: {admin_username}")
|
||||
|
||||
0
system3-nonconformance/api/utils/__init__.py
Normal file
0
system3-nonconformance/api/utils/__init__.py
Normal file
106
system3-nonconformance/api/utils/tkuser_client.py
Normal file
106
system3-nonconformance/api/utils/tkuser_client.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import httpx
|
||||
import os
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
TKUSER_API_URL = os.getenv("TKUSER_API_URL", "http://tkuser-api:3000")
|
||||
|
||||
|
||||
def get_token_from_request(request: Request) -> str:
|
||||
"""Request 헤더에서 Bearer 토큰 추출"""
|
||||
auth = request.headers.get("Authorization", "")
|
||||
if auth.startswith("Bearer "):
|
||||
return auth[7:]
|
||||
return request.cookies.get("sso_token", "")
|
||||
|
||||
|
||||
def _headers(token: str) -> dict:
|
||||
if not token:
|
||||
raise HTTPException(status_code=401, detail="인증 토큰이 필요합니다.")
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _map_project(data: dict) -> dict:
|
||||
"""tkuser API 응답을 S3 프론트엔드 형식으로 매핑 (project_id → id)"""
|
||||
return {
|
||||
"id": data.get("project_id"),
|
||||
"job_no": data.get("job_no"),
|
||||
"project_name": data.get("project_name"),
|
||||
"is_active": data.get("is_active", True),
|
||||
"created_at": data.get("created_at"),
|
||||
}
|
||||
|
||||
|
||||
async def get_projects(token: str, active_only: bool = True) -> list:
|
||||
"""프로젝트 목록 조회"""
|
||||
endpoint = "/api/projects/active" if active_only else "/api/projects"
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(f"{TKUSER_API_URL}{endpoint}", headers=_headers(token))
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=resp.status_code, detail="프로젝트 목록 조회 실패")
|
||||
body = resp.json()
|
||||
if not body.get("success"):
|
||||
raise HTTPException(status_code=500, detail=body.get("error", "알 수 없는 오류"))
|
||||
return [_map_project(p) for p in body.get("data", [])]
|
||||
|
||||
|
||||
async def get_project(token: str, project_id: int) -> dict | None:
|
||||
"""특정 프로젝트 조회. 없으면 None 반환"""
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
f"{TKUSER_API_URL}/api/projects/{project_id}", headers=_headers(token)
|
||||
)
|
||||
if resp.status_code == 404:
|
||||
return None
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=resp.status_code, detail="프로젝트 조회 실패")
|
||||
body = resp.json()
|
||||
if not body.get("success"):
|
||||
raise HTTPException(status_code=500, detail=body.get("error", "알 수 없는 오류"))
|
||||
return _map_project(body.get("data"))
|
||||
|
||||
|
||||
async def create_project(token: str, data: dict) -> dict:
|
||||
"""프로젝트 생성"""
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.post(
|
||||
f"{TKUSER_API_URL}/api/projects", headers=_headers(token), json=data
|
||||
)
|
||||
if resp.status_code == 409:
|
||||
raise HTTPException(status_code=400, detail="이미 존재하는 Job No.입니다.")
|
||||
if resp.status_code not in (200, 201):
|
||||
raise HTTPException(status_code=resp.status_code, detail="프로젝트 생성 실패")
|
||||
body = resp.json()
|
||||
if not body.get("success"):
|
||||
raise HTTPException(status_code=500, detail=body.get("error", "알 수 없는 오류"))
|
||||
return _map_project(body.get("data"))
|
||||
|
||||
|
||||
async def update_project(token: str, project_id: int, data: dict) -> dict:
|
||||
"""프로젝트 수정"""
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.put(
|
||||
f"{TKUSER_API_URL}/api/projects/{project_id}",
|
||||
headers=_headers(token),
|
||||
json=data,
|
||||
)
|
||||
if resp.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=resp.status_code, detail="프로젝트 수정 실패")
|
||||
body = resp.json()
|
||||
if not body.get("success"):
|
||||
raise HTTPException(status_code=500, detail=body.get("error", "알 수 없는 오류"))
|
||||
return _map_project(body.get("data"))
|
||||
|
||||
|
||||
async def delete_project(token: str, project_id: int) -> dict:
|
||||
"""프로젝트 삭제 (비활성화)"""
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.delete(
|
||||
f"{TKUSER_API_URL}/api/projects/{project_id}", headers=_headers(token)
|
||||
)
|
||||
if resp.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=resp.status_code, detail="프로젝트 삭제 실패")
|
||||
return resp.json()
|
||||
@@ -339,7 +339,7 @@
|
||||
async function loadProjects() {
|
||||
try {
|
||||
// API에서 최신 프로젝트 데이터 가져오기
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/projects/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TokenManager.getToken()}`,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>폐기함 - 작업보고서</title>
|
||||
<title>폐기함 - 부적합 관리</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@@ -91,39 +91,39 @@
|
||||
|
||||
<!-- 아카이브 통계 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-green-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #16a34a;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-check-circle text-green-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-green-600">완료</p>
|
||||
<p class="text-2xl font-bold text-green-700" id="completedCount">0</p>
|
||||
<p class="text-sm text-slate-500">완료</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="completedCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #475569;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-archive text-gray-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-archive text-slate-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">보관</p>
|
||||
<p class="text-2xl font-bold text-gray-700" id="archivedCount">0</p>
|
||||
<p class="text-sm text-slate-500">보관</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="archivedCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-red-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #dc2626;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-times-circle text-red-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-times-circle text-red-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-red-600">취소</p>
|
||||
<p class="text-2xl font-bold text-red-700" id="cancelledCount">0</p>
|
||||
<p class="text-sm text-slate-500">취소</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="cancelledCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-purple-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #7c3aed;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-alt text-purple-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-calendar-alt text-purple-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-purple-600">이번 달</p>
|
||||
<p class="text-2xl font-bold text-purple-700" id="thisMonthCount">0</p>
|
||||
<p class="text-sm text-slate-500">이번 달</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="thisMonthCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,7 +287,7 @@
|
||||
// 프로젝트 로드
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/projects/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TokenManager.getToken()}`,
|
||||
|
||||
@@ -18,29 +18,27 @@
|
||||
|
||||
/* 대시보드 카드 스타일 */
|
||||
.dashboard-card {
|
||||
transition: all 0.3s ease;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
transition: all 0.2s ease;
|
||||
background: #ffffff;
|
||||
border-left: 4px solid #64748b;
|
||||
}
|
||||
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 이슈 카드 스타일 (세련된 모던 스타일) */
|
||||
/* 이슈 카드 스타일 */
|
||||
.issue-card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all 0.2s ease;
|
||||
border-left: 4px solid transparent;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
.issue-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
border-left-color: #3b82f6;
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(59, 130, 246, 0.1),
|
||||
0 0 20px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-2px);
|
||||
border-left-color: #475569;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.issue-card label {
|
||||
@@ -92,7 +90,7 @@
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
|
||||
background: #475569;
|
||||
transition: width 0.8s ease;
|
||||
}
|
||||
|
||||
@@ -155,55 +153,43 @@
|
||||
|
||||
<!-- 전체 통계 대시보드 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="dashboard-card text-white p-6 rounded-xl">
|
||||
<div class="dashboard-card p-6 rounded-xl shadow-sm" style="border-left-color: #475569;">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-blue-100 text-sm flex items-center space-x-1">
|
||||
<span>전체 진행 중</span>
|
||||
<div class="w-1.5 h-1.5 bg-blue-200 rounded-full animate-pulse"></div>
|
||||
</p>
|
||||
<p class="text-3xl font-bold" id="totalInProgress">0</p>
|
||||
<p class="text-sm text-slate-500">전체 진행 중</p>
|
||||
<p class="text-3xl font-bold text-slate-800" id="totalInProgress">0</p>
|
||||
</div>
|
||||
<i class="fas fa-tasks text-4xl text-blue-200"></i>
|
||||
<i class="fas fa-tasks text-3xl text-slate-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-green-400 to-green-600 text-white p-6 rounded-xl dashboard-card">
|
||||
|
||||
<div class="dashboard-card p-6 rounded-xl shadow-sm" style="border-left-color: #16a34a;">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-green-100 text-sm flex items-center space-x-1">
|
||||
<span>오늘 신규</span>
|
||||
<div class="w-1.5 h-1.5 bg-green-200 rounded-full animate-pulse"></div>
|
||||
</p>
|
||||
<p class="text-3xl font-bold" id="todayNew">0</p>
|
||||
<p class="text-sm text-slate-500">오늘 신규</p>
|
||||
<p class="text-3xl font-bold text-slate-800" id="todayNew">0</p>
|
||||
</div>
|
||||
<i class="fas fa-plus-circle text-4xl text-green-200"></i>
|
||||
<i class="fas fa-plus-circle text-3xl text-green-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-purple-400 to-purple-600 text-white p-6 rounded-xl dashboard-card">
|
||||
|
||||
<div class="dashboard-card p-6 rounded-xl shadow-sm" style="border-left-color: #7c3aed;">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-purple-100 text-sm flex items-center space-x-1">
|
||||
<span>완료 대기</span>
|
||||
<div class="w-1.5 h-1.5 bg-purple-200 rounded-full animate-pulse"></div>
|
||||
</p>
|
||||
<p class="text-3xl font-bold" id="pendingCompletion">0</p>
|
||||
<p class="text-sm text-slate-500">완료 대기</p>
|
||||
<p class="text-3xl font-bold text-slate-800" id="pendingCompletion">0</p>
|
||||
</div>
|
||||
<i class="fas fa-hourglass-half text-4xl text-purple-200"></i>
|
||||
<i class="fas fa-hourglass-half text-3xl text-purple-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-red-400 to-red-600 text-white p-6 rounded-xl dashboard-card">
|
||||
|
||||
<div class="dashboard-card p-6 rounded-xl shadow-sm" style="border-left-color: #dc2626;">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-red-100 text-sm flex items-center space-x-1">
|
||||
<span>지연 중</span>
|
||||
<div class="w-1.5 h-1.5 bg-red-200 rounded-full animate-pulse"></div>
|
||||
</p>
|
||||
<p class="text-3xl font-bold" id="overdue">0</p>
|
||||
<p class="text-sm text-slate-500">지연 중</p>
|
||||
<p class="text-3xl font-bold text-slate-800" id="overdue">0</p>
|
||||
</div>
|
||||
<i class="fas fa-clock text-4xl text-red-200"></i>
|
||||
<i class="fas fa-clock text-3xl text-red-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,7 +309,7 @@
|
||||
// 데이터 로드 함수들
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/projects/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TokenManager.getToken()}`,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>수신함 - 작업보고서</title>
|
||||
<title>수신함 - 부적합 관리</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@@ -200,30 +200,30 @@
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="bg-yellow-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #d97706;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-plus-circle text-yellow-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-plus-circle text-amber-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-yellow-600">금일 신규</p>
|
||||
<p class="text-2xl font-bold text-yellow-700" id="todayNewCount">0</p>
|
||||
<p class="text-sm text-slate-500">금일 신규</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="todayNewCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-green-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #16a34a;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-check-circle text-green-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-green-600">금일 처리</p>
|
||||
<p class="text-2xl font-bold text-green-700" id="todayProcessedCount">0</p>
|
||||
<p class="text-sm text-slate-500">금일 처리</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="todayProcessedCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-red-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #dc2626;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-exclamation-triangle text-red-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-exclamation-triangle text-red-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-red-600">미해결</p>
|
||||
<p class="text-2xl font-bold text-red-700" id="unresolvedCount">0</p>
|
||||
<p class="text-sm text-slate-500">미해결</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="unresolvedCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -668,7 +668,7 @@
|
||||
async function loadProjects() {
|
||||
console.log('🔄 프로젝트 로드 시작');
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/projects/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TokenManager.getToken()}`,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>관리함 - 작업보고서</title>
|
||||
<title>관리함 - 부적합 관리</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@@ -273,39 +273,39 @@
|
||||
|
||||
<!-- 프로젝트별 통계 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #475569;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-chart-bar text-gray-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-chart-bar text-slate-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">총 부적합</p>
|
||||
<p class="text-2xl font-bold text-gray-700" id="totalCount">0</p>
|
||||
<p class="text-sm text-slate-500">총 부적합</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="totalCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #2563eb;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-cog text-blue-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-cog text-blue-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-blue-600">진행 중</p>
|
||||
<p class="text-2xl font-bold text-blue-700" id="inProgressCount">0</p>
|
||||
<p class="text-sm text-slate-500">진행 중</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="inProgressCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-purple-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #7c3aed;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-hourglass-half text-purple-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-hourglass-half text-purple-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-purple-600">완료 대기</p>
|
||||
<p class="text-2xl font-bold text-purple-700" id="pendingCompletionCount">0</p>
|
||||
<p class="text-sm text-slate-500">완료 대기</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="pendingCompletionCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-green-50 p-4 rounded-lg">
|
||||
<div class="bg-white p-4 rounded-lg border-l-4" style="border-left-color: #16a34a;">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 text-xl mr-3"></i>
|
||||
<i class="fas fa-check-circle text-green-400 text-xl mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm text-green-600">완료됨</p>
|
||||
<p class="text-2xl font-bold text-green-700" id="completedCount">0</p>
|
||||
<p class="text-sm text-slate-500">완료됨</p>
|
||||
<p class="text-2xl font-bold text-slate-800" id="completedCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -472,7 +472,7 @@
|
||||
// 프로젝트 로드
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/projects/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TokenManager.getToken()}`,
|
||||
|
||||
@@ -5,7 +5,7 @@ server {
|
||||
client_max_body_size 10M;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
index issues-dashboard.html;
|
||||
|
||||
# HTML 캐시 비활성화
|
||||
location ~* \.html$ {
|
||||
@@ -46,6 +46,6 @@ server {
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
try_files $uri $uri/ /issues-dashboard.html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
// 프로젝트 목록 로드
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
|
||||
const response = await fetch(`${apiUrl}/projects/`, {
|
||||
headers: {
|
||||
@@ -302,7 +302,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/reports/daily-preview?project_id=${selectedProjectId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TokenManager.getToken()}`
|
||||
@@ -427,7 +427,7 @@
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>생성 중...';
|
||||
button.disabled = true;
|
||||
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/reports/daily-export`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -41,6 +41,11 @@ const API_BASE_URL = (() => {
|
||||
return protocol + '//' + hostname + ':16080/api';
|
||||
}
|
||||
|
||||
// 통합 Docker 환경에서 직접 접근 (포트 30280)
|
||||
if (port === '30280') {
|
||||
return protocol + '//' + hostname + ':30200/api';
|
||||
}
|
||||
|
||||
// 기타 환경
|
||||
return '/api';
|
||||
})();
|
||||
@@ -77,6 +82,10 @@ const TokenManager = {
|
||||
}
|
||||
};
|
||||
|
||||
// 전역 노출 (permissions.js 등 다른 스크립트에서 접근)
|
||||
window.TokenManager = TokenManager;
|
||||
window.API_BASE_URL = API_BASE_URL;
|
||||
|
||||
// API 요청 헬퍼
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
const token = TokenManager.getToken();
|
||||
|
||||
@@ -10,81 +10,66 @@ class CommonHeader {
|
||||
this.menuItems = this.initMenuItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 관리 URL (tkuser 서브도메인 또는 로컬 포트)
|
||||
*/
|
||||
_getUserManageUrl() {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
return window.location.protocol + '//tkuser.technicalkorea.net';
|
||||
}
|
||||
return window.location.protocol + '//' + hostname + ':30380';
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 아이템 정의
|
||||
*/
|
||||
initMenuItems() {
|
||||
return [
|
||||
{
|
||||
id: 'daily_work',
|
||||
title: '일일 공수',
|
||||
icon: 'fas fa-calendar-check',
|
||||
url: '/daily-work.html',
|
||||
pageName: 'daily_work',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50 hover:bg-blue-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_create',
|
||||
title: '부적합 등록',
|
||||
icon: 'fas fa-plus-circle',
|
||||
url: '/index.html',
|
||||
pageName: 'issues_create',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50 hover:bg-green-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_view',
|
||||
title: '신고내용조회',
|
||||
icon: 'fas fa-search',
|
||||
url: '/issue-view.html',
|
||||
pageName: 'issues_view',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50 hover:bg-purple-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_manage',
|
||||
title: '목록 관리',
|
||||
icon: 'fas fa-tasks',
|
||||
url: '/index.html#list',
|
||||
pageName: 'issues_manage',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50 hover:bg-orange-100',
|
||||
subMenus: [
|
||||
{
|
||||
id: 'issues_inbox',
|
||||
title: '수신함',
|
||||
icon: 'fas fa-inbox',
|
||||
url: '/issues-inbox.html',
|
||||
pageName: 'issues_inbox',
|
||||
color: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
id: 'issues_management',
|
||||
title: '관리함',
|
||||
icon: 'fas fa-cog',
|
||||
url: '/issues-management.html',
|
||||
pageName: 'issues_management',
|
||||
color: 'text-green-600'
|
||||
},
|
||||
{
|
||||
id: 'issues_archive',
|
||||
title: '폐기함',
|
||||
icon: 'fas fa-archive',
|
||||
url: '/issues-archive.html',
|
||||
pageName: 'issues_archive',
|
||||
color: 'text-gray-600'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'issues_dashboard',
|
||||
title: '현황판',
|
||||
icon: 'fas fa-chart-line',
|
||||
url: '/issues-dashboard.html',
|
||||
pageName: 'issues_dashboard',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50 hover:bg-purple-100'
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_inbox',
|
||||
title: '수신함',
|
||||
icon: 'fas fa-inbox',
|
||||
url: '/issues-inbox.html',
|
||||
pageName: 'issues_inbox',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_management',
|
||||
title: '관리함',
|
||||
icon: 'fas fa-cog',
|
||||
url: '/issues-management.html',
|
||||
pageName: 'issues_management',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_archive',
|
||||
title: '폐기함',
|
||||
icon: 'fas fa-archive',
|
||||
url: '/issues-archive.html',
|
||||
pageName: 'issues_archive',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'daily_work',
|
||||
title: '일일 공수',
|
||||
icon: 'fas fa-calendar-check',
|
||||
url: '/daily-work.html',
|
||||
pageName: 'daily_work',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
@@ -92,8 +77,8 @@ class CommonHeader {
|
||||
icon: 'fas fa-chart-bar',
|
||||
url: '/reports.html',
|
||||
pageName: 'reports',
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-50 hover:bg-red-100',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100',
|
||||
subMenus: [
|
||||
{
|
||||
id: 'reports_daily',
|
||||
@@ -101,7 +86,7 @@ class CommonHeader {
|
||||
icon: 'fas fa-file-excel',
|
||||
url: '/reports-daily.html',
|
||||
pageName: 'reports_daily',
|
||||
color: 'text-green-600'
|
||||
color: 'text-slate-600'
|
||||
},
|
||||
{
|
||||
id: 'reports_weekly',
|
||||
@@ -109,7 +94,7 @@ class CommonHeader {
|
||||
icon: 'fas fa-calendar-week',
|
||||
url: '/reports-weekly.html',
|
||||
pageName: 'reports_weekly',
|
||||
color: 'text-blue-600'
|
||||
color: 'text-slate-600'
|
||||
},
|
||||
{
|
||||
id: 'reports_monthly',
|
||||
@@ -117,7 +102,7 @@ class CommonHeader {
|
||||
icon: 'fas fa-calendar-alt',
|
||||
url: '/reports-monthly.html',
|
||||
pageName: 'reports_monthly',
|
||||
color: 'text-purple-600'
|
||||
color: 'text-slate-600'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,17 +112,18 @@ class CommonHeader {
|
||||
icon: 'fas fa-folder-open',
|
||||
url: '/project-management.html',
|
||||
pageName: 'projects_manage',
|
||||
color: 'text-indigo-600',
|
||||
bgColor: 'bg-indigo-50 hover:bg-indigo-100'
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'users_manage',
|
||||
title: '사용자 관리',
|
||||
icon: 'fas fa-users-cog',
|
||||
url: '/admin.html',
|
||||
url: this._getUserManageUrl(),
|
||||
pageName: 'users_manage',
|
||||
color: 'text-gray-600',
|
||||
bgColor: 'bg-gray-50 hover:bg-gray-100'
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100',
|
||||
external: true
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -225,8 +211,8 @@ class CommonHeader {
|
||||
<!-- 로고 및 제목 -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<i class="fas fa-clipboard-check text-2xl text-blue-600 mr-3"></i>
|
||||
<h1 class="text-xl font-bold text-gray-900">작업보고서</h1>
|
||||
<i class="fas fa-shield-halved text-2xl text-slate-700 mr-3"></i>
|
||||
<h1 class="text-xl font-bold text-gray-900">부적합 관리</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -243,7 +229,7 @@ class CommonHeader {
|
||||
<div class="text-sm font-medium text-gray-900">${userDisplayName}</div>
|
||||
<div class="text-xs text-gray-500">${userRole}</div>
|
||||
</div>
|
||||
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
|
||||
<div class="w-8 h-8 bg-slate-600 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-sm font-semibold">
|
||||
${userDisplayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -299,7 +285,7 @@ class CommonHeader {
|
||||
|
||||
// 권한 시스템이 로드되지 않았으면 기본 메뉴만
|
||||
if (!window.canAccessPage) {
|
||||
return ['issues_create', 'issues_view'].includes(menu.id);
|
||||
return ['issues_dashboard', 'issues_inbox'].includes(menu.id);
|
||||
}
|
||||
|
||||
// 메인 메뉴 권한 체크
|
||||
@@ -324,8 +310,8 @@ class CommonHeader {
|
||||
*/
|
||||
generateMenuItemHTML(menu) {
|
||||
const isActive = this.currentPage === menu.id;
|
||||
const activeClass = isActive ? 'bg-blue-100 text-blue-700' : `${menu.bgColor} ${menu.color}`;
|
||||
|
||||
const activeClass = isActive ? 'bg-slate-700 text-white' : `${menu.bgColor} ${menu.color}`;
|
||||
|
||||
// 하위 메뉴가 있는 경우 드롭다운 메뉴 생성
|
||||
if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) {
|
||||
return `
|
||||
@@ -342,7 +328,7 @@ class CommonHeader {
|
||||
<div class="py-1">
|
||||
${menu.accessibleSubMenus.map(subMenu => `
|
||||
<a href="${subMenu.url}"
|
||||
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 ${this.currentPage === subMenu.id ? 'bg-blue-50 text-blue-700' : ''}"
|
||||
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-slate-100 ${this.currentPage === subMenu.id ? 'bg-slate-100 text-slate-800 font-medium' : ''}"
|
||||
data-page="${subMenu.id}"
|
||||
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
|
||||
<i class="${subMenu.icon} mr-3 ${subMenu.color}"></i>
|
||||
@@ -355,9 +341,21 @@ class CommonHeader {
|
||||
`;
|
||||
}
|
||||
|
||||
// 외부 링크 (tkuser 등)
|
||||
if (menu.external) {
|
||||
return `
|
||||
<a href="${menu.url}"
|
||||
class="nav-item flex items-center px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${activeClass}"
|
||||
data-page="${menu.id}">
|
||||
<i class="${menu.icon} mr-2"></i>
|
||||
${menu.title}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
// 일반 메뉴 아이템
|
||||
return `
|
||||
<a href="${menu.url}"
|
||||
<a href="${menu.url}"
|
||||
class="nav-item flex items-center px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${activeClass}"
|
||||
data-page="${menu.id}"
|
||||
onclick="CommonHeader.navigateToPage(event, '${menu.url}', '${menu.id}')">
|
||||
@@ -372,7 +370,7 @@ class CommonHeader {
|
||||
*/
|
||||
generateMobileMenuItemHTML(menu) {
|
||||
const isActive = this.currentPage === menu.id;
|
||||
const activeClass = isActive ? 'bg-blue-50 text-blue-700 border-blue-500' : 'text-gray-700 hover:bg-gray-50';
|
||||
const activeClass = isActive ? 'bg-slate-100 text-slate-800 border-slate-600' : 'text-gray-700 hover:bg-gray-50';
|
||||
|
||||
// 하위 메뉴가 있는 경우
|
||||
if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) {
|
||||
@@ -392,7 +390,7 @@ class CommonHeader {
|
||||
<div class="hidden ml-6 mt-1 space-y-1">
|
||||
${menu.accessibleSubMenus.map(subMenu => `
|
||||
<a href="${subMenu.url}"
|
||||
class="flex items-center px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:bg-gray-100 ${this.currentPage === subMenu.id ? 'bg-blue-50 text-blue-700' : ''}"
|
||||
class="flex items-center px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:bg-slate-100 ${this.currentPage === subMenu.id ? 'bg-slate-100 text-slate-800' : ''}"
|
||||
data-page="${subMenu.id}"
|
||||
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
|
||||
<i class="${subMenu.icon} mr-3 ${subMenu.color}"></i>
|
||||
@@ -684,10 +682,11 @@ class CommonHeader {
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
const itemPageId = item.getAttribute('data-page');
|
||||
if (itemPageId === pageId) {
|
||||
item.classList.add('bg-blue-100', 'text-blue-700');
|
||||
item.classList.remove('bg-blue-50', 'hover:bg-blue-100');
|
||||
item.classList.add('bg-slate-700', 'text-white');
|
||||
item.classList.remove('text-slate-600', 'hover:bg-slate-100');
|
||||
} else {
|
||||
item.classList.remove('bg-blue-100', 'text-blue-700');
|
||||
item.classList.remove('bg-slate-700', 'text-white');
|
||||
item.classList.add('text-slate-600');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class PageManager {
|
||||
async checkAuthentication() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/issues-dashboard.html';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class PageManager {
|
||||
console.error('인증 실패:', error);
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/issues-dashboard.html';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class PageManager {
|
||||
|
||||
// 권한 시스템이 로드되지 않았으면 기본 페이지만 허용
|
||||
if (!window.canAccessPage) {
|
||||
return ['issues_create', 'issues_view'].includes(pageId);
|
||||
return ['issues_dashboard', 'issues_inbox'].includes(pageId);
|
||||
}
|
||||
|
||||
return window.canAccessPage(pageId);
|
||||
@@ -130,11 +130,7 @@ class PageManager {
|
||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||
|
||||
// 기본적으로 접근 가능한 페이지로 이동
|
||||
if (window.canAccessPage && window.canAccessPage('issues_view')) {
|
||||
window.location.href = '/issue-view.html';
|
||||
} else {
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
window.location.href = '/issues-dashboard.html';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,7 +246,7 @@ class PageManager {
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
다시 시도
|
||||
</button>
|
||||
<button onclick="window.location.href='/index.html'"
|
||||
<button onclick="window.location.href='/issues-dashboard.html'"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700">
|
||||
홈으로
|
||||
</button>
|
||||
|
||||
@@ -15,13 +15,11 @@ class PagePermissionManager {
|
||||
*/
|
||||
initDefaultPages() {
|
||||
return {
|
||||
'issues_create': { title: '부적합 등록', defaultAccess: true },
|
||||
'issues_view': { title: '부적합 조회', defaultAccess: true },
|
||||
'issues_dashboard': { title: '현황판', defaultAccess: true },
|
||||
'issues_manage': { title: '부적합 관리', defaultAccess: true },
|
||||
'issues_inbox': { title: '수신함', defaultAccess: true },
|
||||
'issues_management': { title: '관리함', defaultAccess: false },
|
||||
'issues_archive': { title: '폐기함', defaultAccess: false },
|
||||
'issues_dashboard': { title: '현황판', defaultAccess: true },
|
||||
'projects_manage': { title: '프로젝트 관리', defaultAccess: false },
|
||||
'daily_work': { title: '일일 공수', defaultAccess: false },
|
||||
'reports': { title: '보고서', defaultAccess: false },
|
||||
@@ -41,15 +39,32 @@ class PagePermissionManager {
|
||||
/**
|
||||
* 사용자별 페이지 권한 로드
|
||||
*/
|
||||
/**
|
||||
* SSO 토큰 직접 읽기 (api.js 로딩 전에도 동작)
|
||||
*/
|
||||
_getToken() {
|
||||
// 1) window.TokenManager (api.js 로딩 완료 시)
|
||||
if (window.TokenManager) return window.TokenManager.getToken();
|
||||
// 2) SSO 쿠키 직접 읽기
|
||||
const match = document.cookie.match(/(?:^|; )sso_token=([^;]*)/);
|
||||
if (match) return decodeURIComponent(match[1]);
|
||||
// 3) localStorage 폴백
|
||||
return localStorage.getItem('sso_token') || localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
async loadPagePermissions() {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
const userId = this.currentUser.id || this.currentUser.user_id;
|
||||
if (!userId) return;
|
||||
|
||||
try {
|
||||
// API에서 사용자별 페이지 권한 가져오기
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const response = await fetch(`${apiUrl}/users/${this.currentUser.id}/page-permissions`, {
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const token = this._getToken();
|
||||
const response = await fetch(`${apiUrl}/users/${userId}/page-permissions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -199,12 +214,12 @@ class PagePermissionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/page-permissions/grant`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
'Authorization': `Bearer ${this._getToken()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
@@ -232,10 +247,10 @@ class PagePermissionManager {
|
||||
*/
|
||||
async getUserPagePermissions(userId) {
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/users/${userId}/page-permissions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
'Authorization': `Bearer ${this._getToken()}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
formData.append('username', 'hyungi');
|
||||
formData.append('password', '123456');
|
||||
|
||||
const response = await fetch('http://localhost:16080/api/auth/login', {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:16080/api/auth/users', {
|
||||
const response = await fetch('/api/auth/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user