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:
Hyungi Ahn
2026-02-12 13:45:52 +09:00
parent 6495b8af32
commit 733bb0cb35
96 changed files with 9721 additions and 825 deletions

View File

@@ -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) # 삭제 사유 (선택사항)