refactor(tkqc): UI 스타일 통일 + 일일공수 제거 + 메뉴 정리

- UI: tkuser 스타일로 통일 (dark slate 헤더, flat 배경, gradient/glass 제거)
- tkqc-common.css 공통 스타일시트 신규 생성
- 의견 제시 API 별도 엔드포인트 추가 (모든 사용자 접근 가능)
- 일일 공수 기능 완전 제거 (라우터, 모델, 스키마, DB 테이블 DROP)
- 프로젝트 관리/사용자 관리 메뉴 숨김 (통합관리 페이지로 이관)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-13 10:43:37 +09:00
parent ce270b9e4a
commit c52734154f
26 changed files with 294 additions and 782 deletions

View File

@@ -68,7 +68,6 @@ class User(Base):
# Relationships # Relationships
issues = relationship("Issue", back_populates="reporter", foreign_keys="Issue.reporter_id") issues = relationship("Issue", back_populates="reporter", foreign_keys="Issue.reporter_id")
reviewed_issues = relationship("Issue", foreign_keys="Issue.reviewed_by_id") reviewed_issues = relationship("Issue", foreign_keys="Issue.reviewed_by_id")
daily_works = relationship("DailyWork", back_populates="created_by")
page_permissions = relationship("UserPagePermission", back_populates="user", foreign_keys="UserPagePermission.user_id") page_permissions = relationship("UserPagePermission", back_populates="user", foreign_keys="UserPagePermission.user_id")
class UserPagePermission(Base): class UserPagePermission(Base):
@@ -183,37 +182,6 @@ class Project(Base):
primaryjoin="Project.id == Issue.project_id", primaryjoin="Project.id == Issue.project_id",
foreign_keys="[Issue.project_id]") foreign_keys="[Issue.project_id]")
class DailyWork(Base):
__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)
regular_hours = Column(Float, nullable=False)
overtime_workers = Column(Integer, default=0)
overtime_hours = Column(Float, default=0)
overtime_total = Column(Float, default=0)
total_hours = Column(Float, nullable=False)
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__ = "qc_project_daily_works"
id = Column(Integer, primary_key=True, index=True)
date = Column(DateTime, nullable=False, index=True)
project_id = Column(Integer, ForeignKey("projects.project_id"), nullable=False)
hours = Column(Float, nullable=False)
created_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
created_at = Column(DateTime, default=get_kst_now)
# Relationships
project = relationship("Project")
created_by = relationship("User")
class DeletionLog(Base): class DeletionLog(Base):
__tablename__ = "qc_deletion_logs" __tablename__ = "qc_deletion_logs"

View File

@@ -250,6 +250,10 @@ class ManagementUpdateRequest(BaseModel):
completion_photo5: Optional[str] = None # Base64 - 완료 사진 5 completion_photo5: Optional[str] = None # Base64 - 완료 사진 5
review_status: Optional[ReviewStatus] = None review_status: Optional[ReviewStatus] = None
class OpinionRequest(BaseModel):
"""의견 제시 요청 (로그인한 모든 사용자 가능)"""
opinion: str # 의견 내용
class InboxIssue(BaseModel): class InboxIssue(BaseModel):
"""수신함용 부적합 정보 (간소화된 버전)""" """수신함용 부적합 정보 (간소화된 버전)"""
id: int id: int
@@ -296,33 +300,6 @@ class Project(ProjectBase):
class Config: class Config:
from_attributes = True from_attributes = True
# Daily Work schemas
class DailyWorkBase(BaseModel):
date: datetime
worker_count: int = Field(gt=0)
overtime_workers: Optional[int] = 0
overtime_hours: Optional[float] = 0
class DailyWorkCreate(DailyWorkBase):
pass
class DailyWorkUpdate(BaseModel):
worker_count: Optional[int] = Field(None, gt=0)
overtime_workers: Optional[int] = None
overtime_hours: Optional[float] = None
class DailyWork(DailyWorkBase):
id: int
regular_hours: float
overtime_total: float
total_hours: float
created_by_id: int
created_by: User
created_at: datetime
class Config:
from_attributes = True
# Report schemas # Report schemas
class ReportRequest(BaseModel): class ReportRequest(BaseModel):
start_date: datetime start_date: datetime
@@ -342,24 +319,6 @@ class ReportSummary(BaseModel):
completed_issues: int completed_issues: int
average_resolution_time: float average_resolution_time: float
# Project Daily Work schemas
class ProjectDailyWorkBase(BaseModel):
date: datetime
project_id: int
hours: float
class ProjectDailyWorkCreate(ProjectDailyWorkBase):
pass
class ProjectDailyWork(ProjectDailyWorkBase):
id: int
created_by_id: int
created_at: datetime
project: Project
class Config:
from_attributes = True
# 일일보고서 관련 스키마 # 일일보고서 관련 스키마
class DailyReportRequest(BaseModel): class DailyReportRequest(BaseModel):
project_id: int project_id: int

View File

@@ -5,7 +5,7 @@ import uvicorn
from database.database import engine, get_db from database.database import engine, get_db
from database.models import Base from database.models import Base
from routers import auth, issues, daily_work, reports, projects, page_permissions, inbox, management from routers import auth, issues, reports, projects, page_permissions, inbox, management
from services.auth_service import create_admin_user from services.auth_service import create_admin_user
# 데이터베이스 테이블 생성 (sso_users, projects는 이미 존재하므로 제외) # 데이터베이스 테이블 생성 (sso_users, projects는 이미 존재하므로 제외)
@@ -36,7 +36,6 @@ app.add_middleware(
app.include_router(auth.router) app.include_router(auth.router)
app.include_router(issues.router) app.include_router(issues.router)
app.include_router(inbox.router) # 수신함 라우터 추가 app.include_router(inbox.router) # 수신함 라우터 추가
app.include_router(daily_work.router)
app.include_router(reports.router) app.include_router(reports.router)
app.include_router(projects.router) app.include_router(projects.router)
app.include_router(page_permissions.router) app.include_router(page_permissions.router)

View File

@@ -246,6 +246,42 @@ async def delete_issue(
db.commit() db.commit()
return {"detail": "Issue deleted successfully", "logged": True} return {"detail": "Issue deleted successfully", "logged": True}
@router.post("/{issue_id}/opinion")
async def add_opinion(
issue_id: int,
opinion_request: schemas.OpinionRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
의견 제시 — 로그인한 모든 사용자가 사용 가능 (권한 체크 없음)
solution 필드에 의견을 추가합니다.
"""
issue = db.query(Issue).filter(Issue.id == issue_id).first()
if not issue:
raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.")
# 새 의견 형식: [작성자] (날짜시간)\n내용
now = datetime.now()
date_str = now.strftime("%Y. %m. %d. %H:%M")
author = current_user.full_name or current_user.username
new_opinion = f"[{author}] ({date_str})\n{opinion_request.opinion}"
# 기존 solution에 추가 (최신이 위로)
separator = "" * 50
if issue.solution:
issue.solution = f"{new_opinion}\n{separator}\n{issue.solution}"
else:
issue.solution = new_opinion
db.commit()
db.refresh(issue)
return {
"message": "의견이 추가되었습니다.",
"issue_id": issue.id
}
@router.get("/stats/summary") @router.get("/stats/summary")
async def get_issue_stats( async def get_issue_stats(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),

View File

@@ -50,10 +50,7 @@ DEFAULT_PAGES = {
'issues_management': {'title': '관리함', 'default_access': False}, 'issues_management': {'title': '관리함', 'default_access': False},
'issues_archive': {'title': '폐기함', 'default_access': False}, 'issues_archive': {'title': '폐기함', 'default_access': False},
'issues_dashboard': {'title': '현황판', 'default_access': True}, 'issues_dashboard': {'title': '현황판', 'default_access': True},
'projects_manage': {'title': '프로젝트 관리', 'default_access': False}, 'reports': {'title': '보고서', 'default_access': False}
'daily_work': {'title': '일일 공수', 'default_access': False},
'reports': {'title': '보고서', 'default_access': False},
'users_manage': {'title': '사용자 관리', 'default_access': False}
} }
@router.post("/page-permissions/grant") @router.post("/page-permissions/grant")

View File

@@ -13,7 +13,7 @@ from openpyxl.drawing.image import Image as XLImage
import os import os
from database.database import get_db from database.database import get_db
from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole, ReviewStatus from database.models import Issue, IssueStatus, IssueCategory, User, UserRole, ReviewStatus
from database import schemas from database import schemas
from routers.auth import get_current_user from routers.auth import get_current_user
from utils.tkuser_client import get_token_from_request from utils.tkuser_client import get_token_from_request
@@ -30,14 +30,9 @@ async def generate_report_summary(
"""보고서 요약 생성""" """보고서 요약 생성"""
start_date = report_request.start_date start_date = report_request.start_date
end_date = report_request.end_date end_date = report_request.end_date
# 일일 공수 합계 total_hours = 0
daily_works = db.query(DailyWork).filter(
DailyWork.date >= start_date.date(),
DailyWork.date <= end_date.date()
).all()
total_hours = sum(w.total_hours for w in daily_works)
# 이슈 통계 # 이슈 통계
issues_query = db.query(Issue).filter( issues_query = db.query(Issue).filter(
Issue.report_date >= start_date, Issue.report_date >= start_date,
@@ -116,29 +111,6 @@ async def get_report_issues(
"detail_notes": issue.detail_notes "detail_notes": issue.detail_notes
} for issue in issues] } for issue in issues]
@router.get("/daily-works")
async def get_report_daily_works(
start_date: datetime,
end_date: datetime,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""보고서용 일일 공수 목록"""
works = db.query(DailyWork).filter(
DailyWork.date >= start_date.date(),
DailyWork.date <= end_date.date()
).order_by(DailyWork.date).all()
return [{
"date": work.date,
"worker_count": work.worker_count,
"regular_hours": work.regular_hours,
"overtime_workers": work.overtime_workers,
"overtime_hours": work.overtime_hours,
"overtime_total": work.overtime_total,
"total_hours": work.total_hours
} for work in works]
@router.get("/daily-preview") @router.get("/daily-preview")
async def preview_daily_report( async def preview_daily_report(
project_id: int, project_id: int,

View File

@@ -714,8 +714,6 @@
'issues_inbox': { title: '수신함', defaultAccess: true }, 'issues_inbox': { title: '수신함', defaultAccess: true },
'issues_management': { title: '관리함', defaultAccess: false }, 'issues_management': { title: '관리함', defaultAccess: false },
'issues_archive': { title: '폐기함', defaultAccess: false }, 'issues_archive': { title: '폐기함', defaultAccess: false },
'projects_manage': { title: '프로젝트 관리', defaultAccess: false },
'daily_work': { title: '일일 공수', defaultAccess: false },
'reports': { title: '보고서', defaultAccess: false } 'reports': { title: '보고서', defaultAccess: false }
}; };
@@ -769,10 +767,7 @@
'issues_archive': { title: '🗃️ 폐기함', icon: 'fas fa-archive', color: 'text-gray-600' } 'issues_archive': { title: '🗃️ 폐기함', icon: 'fas fa-archive', color: 'text-gray-600' }
}, },
'시스템 관리': { '시스템 관리': {
'projects_manage': { title: '프로젝트 관리', icon: 'fas fa-folder-open', color: 'text-indigo-600' }, 'reports': { title: '보고서', icon: 'fas fa-chart-bar', color: 'text-red-600' }
'daily_work': { title: '일일 공수', icon: 'fas fa-calendar-check', color: 'text-blue-600' },
'reports': { title: '보고서', icon: 'fas fa-chart-bar', color: 'text-red-600' },
'users_manage': { title: '사용자 관리', icon: 'fas fa-users-cog', color: 'text-purple-600' }
} }
}; };
@@ -839,7 +834,7 @@
const allPages = [ const allPages = [
'issues_create', 'issues_view', 'issues_manage', 'issues_create', 'issues_view', 'issues_manage',
'issues_inbox', 'issues_management', 'issues_archive', 'issues_inbox', 'issues_management', 'issues_archive',
'projects_manage', 'daily_work', 'reports', 'users_manage' 'reports'
]; ];
const permissions = {}; const permissions = {};

View File

@@ -6,6 +6,10 @@
<title>일일 공수 입력</title> <title>일일 공수 입력</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<style> <style>
:root { :root {
--primary: #3b82f6; --primary: #3b82f6;
@@ -16,62 +20,55 @@
--gray-200: #e5e7eb; --gray-200: #e5e7eb;
--gray-300: #d1d5db; --gray-300: #d1d5db;
} }
body {
background-color: var(--gray-50);
}
.btn-primary { .btn-primary {
background-color: var(--primary); background-color: var(--primary);
color: white; color: white;
transition: all 0.2s; transition: all 0.2s;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: var(--primary-dark); background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
} }
.btn-success { .btn-success {
background-color: var(--success); background-color: var(--success);
color: white; color: white;
transition: all 0.2s; transition: all 0.2s;
} }
.btn-success:hover { .btn-success:hover {
background-color: #059669; background-color: #059669;
transform: translateY(-1px);
} }
.input-field { .input-field {
border: 1px solid var(--gray-300); border: 1px solid var(--gray-300);
background: white; background: white;
transition: all 0.2s; transition: all 0.2s;
} }
.input-field:focus { .input-field:focus {
border-color: var(--primary); border-color: var(--primary);
outline: none; outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
} }
.work-card { .work-card {
background: white; background: white;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s; transition: all 0.2s;
} }
.work-card:hover { .work-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.summary-card { .summary-card {
background: linear-gradient(135deg, #3b82f6, #2563eb); background: #2563eb;
color: white; color: white;
} }
.nav-link { .nav-link {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
@@ -79,61 +76,24 @@
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
} }
.nav-link:hover { .nav-link:hover {
background-color: #f3f4f6; background-color: #f3f4f6;
color: #1f2937; color: #1f2937;
} }
.nav-link.active { .nav-link.active {
background-color: #3b82f6; background-color: #3b82f6;
color: white; color: white;
} }
/* 부드러운 페이드인 애니메이션 */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 헤더 전용 빠른 페이드인 */
.header-fade-in {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.header-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 본문 컨텐츠 지연 페이드인 */
.content-fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
transition-delay: 0.2s;
}
.content-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
</style> </style>
</head> </head>
<body> <body>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen">
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 메인 컨텐츠 --> <!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-6 max-w-2xl content-fade-in" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-6 max-w-2xl content-fade-in" style="padding-top: 72px;">
<!-- 입력 카드 --> <!-- 입력 카드 -->
<div class="work-card p-6 mb-6"> <div class="work-card p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-6"> <h2 class="text-lg font-semibold text-gray-800 mb-6">

View File

@@ -11,42 +11,19 @@
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<!-- Custom Styles --> <!-- Custom Styles -->
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 50%, #f0f9ff 100%);
min-height: 100vh;
}
.glass-effect {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.input-field {
background: white;
border: 1px solid #e5e7eb;
transition: all 0.2s;
}
.input-field:focus {
outline: none;
border-color: #60a5fa;
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
}
.line-clamp-2 { .line-clamp-2 {
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.nav-link { .nav-link {
color: #6b7280; color: #6b7280;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
@@ -54,49 +31,23 @@
transition: all 0.2s; transition: all 0.2s;
text-decoration: none; text-decoration: none;
} }
.nav-link:hover { .nav-link:hover {
background-color: #f3f4f6; background-color: #f3f4f6;
color: #3b82f6; color: #334155;
} }
.nav-link.active { .nav-link.active {
background-color: #3b82f6; background-color: #334155;
color: white; color: white;
} }
/* 부드러운 페이드인 애니메이션 */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 헤더 전용 빠른 페이드인 */
.header-fade-in {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.header-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 본문 컨텐츠 지연 페이드인 */
.content-fade-in { .content-fade-in {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(10px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out; transition: opacity 0.4s ease-out, transform 0.4s ease-out;
transition-delay: 0.2s;
} }
.content-fade-in.visible { .content-fade-in.visible {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -107,7 +58,7 @@
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-6"> <div class="bg-white rounded-xl shadow-sm p-4 mb-6">
<div class="mb-4"> <div class="mb-4">

View File

@@ -13,60 +13,33 @@
<!-- 모바일 캘린더 스타일 --> <!-- 모바일 캘린더 스타일 -->
<link rel="stylesheet" href="/static/css/mobile-calendar.css"> <link rel="stylesheet" href="/static/css/mobile-calendar.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<!-- Custom Styles --> <!-- Custom Styles -->
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.issue-card {
transition: all 0.2s ease;
opacity: 0.8;
}
.issue-card:hover {
opacity: 1;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.archived-card { .archived-card {
border-left: 4px solid #6b7280; border-left: 4px solid #6b7280;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); background: #f8fafc;
} }
.completed-card { .completed-card {
border-left: 4px solid #10b981; border-left: 4px solid #10b981;
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 100%); background: #f0fdf4;
} }
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-completed { background: #d1fae5; color: #065f46; }
.badge-archived { background: #f3f4f6; color: #374151; }
.badge-cancelled { background: #fee2e2; color: #991b1b; }
.chart-container { .chart-container {
position: relative; position: relative;
height: 300px; height: 300px;
} }
</style> </style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -6,96 +6,55 @@
<title>부적합 현황판</title> <title>부적합 현황판</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<style> <style>
.fade-in { opacity: 0; animation: fadeIn 0.5s ease-in forwards; } .content-fade-in { opacity: 0; animation: contentFadeIn 0.4s ease-out 0.2s forwards; }
@keyframes fadeIn { to { opacity: 1; } } @keyframes contentFadeIn { to { opacity: 1; transform: translateY(0); } from { transform: translateY(10px); } }
.header-fade-in { opacity: 0; animation: headerFadeIn 0.6s ease-out forwards; }
@keyframes headerFadeIn { to { opacity: 1; transform: translateY(0); } from { transform: translateY(-10px); } }
.content-fade-in { opacity: 0; animation: contentFadeIn 0.7s ease-out 0.2s forwards; }
@keyframes contentFadeIn { to { opacity: 1; transform: translateY(0); } from { transform: translateY(20px); } }
/* 대시보드 카드 스타일 */ /* 대시보드 카드 스타일 */
.dashboard-card { .dashboard-card {
transition: all 0.3s ease; transition: all 0.2s ease;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
} }
.dashboard-card:hover { .dashboard-card:hover {
transform: translateY(-5px); box-shadow: 0 4px 12px rgba(0,0,0,0.08);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
} }
/* 이슈 카드 스타일 (세련된 모던 스타일) */
.issue-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-left: 4px solid transparent;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
}
.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);
}
.issue-card label { .issue-card label {
font-weight: 600; font-weight: 600;
color: #374151; color: #374151;
} }
.issue-card .bg-gray-50 { .issue-card .bg-gray-50 {
background-color: #f9fafb; background-color: #f9fafb;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
transition: all 0.2s ease;
}
.issue-card .bg-gray-50:hover {
background-color: #f3f4f6;
}
.issue-card .fas.fa-image:hover {
transform: scale(1.2);
color: #3b82f6;
}
/* 진행 중 애니메이션 */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
/* 날짜 그룹 스타일 */ /* 날짜 그룹 스타일 */
.date-group { .date-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.date-header { .date-header {
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.date-header:hover { .date-header:hover {
background-color: #f3f4f6 !important; background-color: #f3f4f6 !important;
} }
.collapse-content { .collapse-content {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.progress-bar { .progress-bar {
background: linear-gradient(90deg, #10b981 0%, #059669 100%); background: #10b981;
transition: width 0.8s ease; transition: width 0.8s ease;
} }
/* 반응형 그리드 */ /* 반응형 그리드 */
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
@@ -104,7 +63,7 @@
} }
</style> </style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body>
<!-- 로딩 스크린 --> <!-- 로딩 스크린 -->
<div id="loadingScreen" class="fixed inset-0 bg-white z-50 flex items-center justify-center"> <div id="loadingScreen" class="fixed inset-0 bg-white z-50 flex items-center justify-center">
<div class="text-center"> <div class="text-center">
@@ -139,7 +98,7 @@
<div id="commonHeader"></div> <div id="commonHeader"></div>
<!-- 메인 콘텐츠 --> <!-- 메인 콘텐츠 -->
<main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -155,7 +114,7 @@
<!-- 전체 통계 대시보드 --> <!-- 전체 통계 대시보드 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> <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="bg-blue-500 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-blue-100 text-sm flex items-center space-x-1"> <p class="text-blue-100 text-sm flex items-center space-x-1">
@@ -167,43 +126,43 @@
<i class="fas fa-tasks text-4xl text-blue-200"></i> <i class="fas fa-tasks text-4xl text-blue-200"></i>
</div> </div>
</div> </div>
<div class="bg-gradient-to-br from-green-400 to-green-600 text-white p-6 rounded-xl dashboard-card"> <div class="bg-emerald-500 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-green-100 text-sm flex items-center space-x-1"> <p class="text-emerald-100 text-sm flex items-center space-x-1">
<span>오늘 신규</span> <span>오늘 신규</span>
<div class="w-1.5 h-1.5 bg-green-200 rounded-full animate-pulse"></div> <div class="w-1.5 h-1.5 bg-emerald-200 rounded-full animate-pulse"></div>
</p> </p>
<p class="text-3xl font-bold" id="todayNew">0</p> <p class="text-3xl font-bold" id="todayNew">0</p>
</div> </div>
<i class="fas fa-plus-circle text-4xl text-green-200"></i> <i class="fas fa-plus-circle text-4xl text-emerald-200"></i>
</div> </div>
</div> </div>
<div class="bg-gradient-to-br from-purple-400 to-purple-600 text-white p-6 rounded-xl dashboard-card"> <div class="bg-amber-500 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-purple-100 text-sm flex items-center space-x-1"> <p class="text-amber-100 text-sm flex items-center space-x-1">
<span>완료 대기</span> <span>완료 대기</span>
<div class="w-1.5 h-1.5 bg-purple-200 rounded-full animate-pulse"></div> <div class="w-1.5 h-1.5 bg-amber-200 rounded-full animate-pulse"></div>
</p> </p>
<p class="text-3xl font-bold" id="pendingCompletion">0</p> <p class="text-3xl font-bold" id="pendingCompletion">0</p>
</div> </div>
<i class="fas fa-hourglass-half text-4xl text-purple-200"></i> <i class="fas fa-hourglass-half text-4xl text-amber-200"></i>
</div> </div>
</div> </div>
<div class="bg-gradient-to-br from-red-400 to-red-600 text-white p-6 rounded-xl dashboard-card"> <div class="bg-slate-500 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-red-100 text-sm flex items-center space-x-1"> <p class="text-slate-200 text-sm flex items-center space-x-1">
<span>지연 중</span> <span>지연 중</span>
<div class="w-1.5 h-1.5 bg-red-200 rounded-full animate-pulse"></div> <div class="w-1.5 h-1.5 bg-slate-300 rounded-full animate-pulse"></div>
</p> </p>
<p class="text-3xl font-bold" id="overdue">0</p> <p class="text-3xl font-bold" id="overdue">0</p>
</div> </div>
<i class="fas fa-clock text-4xl text-red-200"></i> <i class="fas fa-clock text-4xl text-slate-300"></i>
</div> </div>
</div> </div>
</div> </div>
@@ -2131,44 +2090,15 @@
} }
try { try {
// 현재 이슈 정보 가져오기 // 별도 의견 제시 API 사용 (권한 체크 없음, 로그인만 필요)
const issueResponse = await fetch(`/api/issues/${selectedOpinionIssueId}`, { const response = await fetch(`/api/issues/${selectedOpinionIssueId}/opinion`, {
headers: { method: 'POST',
'Authorization': `Bearer ${TokenManager.getToken()}`
}
});
if (!issueResponse.ok) {
throw new Error('이슈 정보를 가져올 수 없습니다.');
}
const issue = await issueResponse.json();
// 새 의견 형식: [작성자] (날짜시간)\n내용
const now = new Date();
const dateStr = now.toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
const newOpinion = `[${currentUser.full_name || currentUser.username}] (${dateStr})\n${opinionText}`;
// 기존 solution에 추가 (최신이 위로)
const updatedSolution = issue.solution
? `${newOpinion}\n${'─'.repeat(50)}\n${issue.solution}`
: newOpinion;
// 백엔드 업데이트
const response = await fetch(`/api/issues/${selectedOpinionIssueId}/management`, {
method: 'PUT',
headers: { headers: {
'Authorization': `Bearer ${TokenManager.getToken()}`, 'Authorization': `Bearer ${TokenManager.getToken()}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
solution: updatedSolution opinion: opinionText
}) })
}); });

View File

@@ -13,15 +13,12 @@
<!-- 모바일 캘린더 스타일 --> <!-- 모바일 캘린더 스타일 -->
<link rel="stylesheet" href="/static/css/mobile-calendar.css"> <link rel="stylesheet" href="/static/css/mobile-calendar.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<!-- Custom Styles --> <!-- Custom Styles -->
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.loading-overlay { .loading-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -37,81 +34,32 @@
visibility: hidden; visibility: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.loading-overlay.active { .loading-overlay.active {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
.issue-card {
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.issue-card.unread { .issue-card.unread {
border-left-color: #3b82f6; border-left-color: #3b82f6;
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%); background: #f8fafc;
} }
.issue-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.priority-high { border-left-color: #ef4444; } .priority-high { border-left-color: #ef4444; }
.priority-medium { border-left-color: #f59e0b; } .priority-medium { border-left-color: #f59e0b; }
.priority-low { border-left-color: #10b981; } .priority-low { border-left-color: #10b981; }
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-new { background: #dbeafe; color: #1e40af; }
.badge-processing { background: #fef3c7; color: #92400e; }
.badge-completed { background: #d1fae5; color: #065f46; }
/* 부드러운 페이드인 애니메이션 */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 헤더 전용 빠른 페이드인 */
.header-fade-in {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.header-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 본문 컨텐츠 지연 페이드인 */
.content-fade-in { .content-fade-in {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(10px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out; transition: opacity 0.4s ease-out, transform 0.4s ease-out;
transition-delay: 0.2s;
} }
.content-fade-in.visible { .content-fade-in.visible {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
/* 사진 미리보기 스타일 */ /* 사진 미리보기 스타일 */
.photo-preview { .photo-preview {
max-width: 150px; max-width: 150px;
@@ -121,18 +69,18 @@
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
.photo-preview:hover { .photo-preview:hover {
transform: scale(1.05); transform: scale(1.05);
} }
.photo-gallery { .photo-gallery {
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 8px; margin-top: 8px;
} }
.photo-modal { .photo-modal {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -145,14 +93,14 @@
align-items: center; align-items: center;
z-index: 1000; z-index: 1000;
} }
.photo-modal img { .photo-modal img {
max-width: 90%; max-width: 90%;
max-height: 90%; max-height: 90%;
object-fit: contain; object-fit: contain;
border-radius: 8px; border-radius: 8px;
} }
.photo-modal .close-btn { .photo-modal .close-btn {
position: absolute; position: absolute;
top: 20px; top: 20px;
@@ -167,7 +115,7 @@
} }
</style> </style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body>
<!-- 로딩 오버레이 --> <!-- 로딩 오버레이 -->
<div id="loadingOverlay" class="loading-overlay"> <div id="loadingOverlay" class="loading-overlay">
<div class="text-center"> <div class="text-center">
@@ -179,7 +127,7 @@
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -13,69 +13,43 @@
<!-- 모바일 캘린더 스타일 --> <!-- 모바일 캘린더 스타일 -->
<link rel="stylesheet" href="/static/css/mobile-calendar.css"> <link rel="stylesheet" href="/static/css/mobile-calendar.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<!-- Custom Styles --> <!-- Custom Styles -->
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.issue-card {
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.issue-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.priority-high { border-left-color: #ef4444; } .priority-high { border-left-color: #ef4444; }
.priority-medium { border-left-color: #f59e0b; } .priority-medium { border-left-color: #f59e0b; }
.priority-low { border-left-color: #10b981; } .priority-low { border-left-color: #10b981; }
.status-new { border-left-color: #3b82f6; } .status-new { border-left-color: #3b82f6; }
.status-processing { border-left-color: #f59e0b; } .status-processing { border-left-color: #f59e0b; }
.status-pending { border-left-color: #8b5cf6; } .status-pending { border-left-color: #8b5cf6; }
.status-completed { border-left-color: #10b981; } .status-completed { border-left-color: #10b981; }
.action-btn { .action-btn {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.action-btn:hover {
transform: scale(1.05);
}
.modal { .modal {
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
/* 날짜 그룹 스타일 */ /* 날짜 그룹 스타일 */
.date-group { .date-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.date-header { .date-header {
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.date-header:hover { .date-header:hover {
background-color: #f9fafb; background-color: #f9fafb;
} }
/* 좌우 스크롤 가능한 이슈 테이블 */ /* 좌우 스크롤 가능한 이슈 테이블 */
.issue-table-container { .issue-table-container {
overflow-x: auto; overflow-x: auto;
@@ -83,13 +57,13 @@
border-radius: 0.5rem; border-radius: 0.5rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.issue-table { .issue-table {
min-width: 2000px; /* 더 넓은 최소 너비 설정 */ min-width: 2000px;
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
} }
.issue-table th, .issue-table th,
.issue-table td { .issue-table td {
padding: 0.75rem; padding: 0.75rem;
@@ -97,7 +71,7 @@
border-bottom: 1px solid #f3f4f6; border-bottom: 1px solid #f3f4f6;
vertical-align: top; vertical-align: top;
} }
.issue-table th { .issue-table th {
background-color: #f9fafb; background-color: #f9fafb;
font-weight: 600; font-weight: 600;
@@ -105,11 +79,11 @@
font-size: 0.875rem; font-size: 0.875rem;
white-space: nowrap; white-space: nowrap;
} }
.issue-table tbody tr:hover { .issue-table tbody tr:hover {
background-color: #f9fafb; background-color: #f9fafb;
} }
/* 컬럼별 너비 조정 */ /* 컬럼별 너비 조정 */
.col-no { min-width: 60px; } .col-no { min-width: 60px; }
.col-project { min-width: 120px; } .col-project { min-width: 120px; }
@@ -125,7 +99,7 @@
.col-photos { min-width: 150px; } .col-photos { min-width: 150px; }
.col-completion { min-width: 80px; } .col-completion { min-width: 80px; }
.col-actions { min-width: 120px; } .col-actions { min-width: 120px; }
.issue-photo { .issue-photo {
width: 60px; width: 60px;
height: 40px; height: 40px;
@@ -134,13 +108,13 @@
cursor: pointer; cursor: pointer;
margin: 2px; margin: 2px;
} }
.photo-container { .photo-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
} }
/* 편집 가능한 필드 스타일 */ /* 편집 가능한 필드 스타일 */
.editable-field { .editable-field {
min-width: 100%; min-width: 100%;
@@ -149,19 +123,19 @@
border-radius: 4px; border-radius: 4px;
font-size: 0.875rem; font-size: 0.875rem;
} }
.editable-field:focus { .editable-field:focus {
outline: none; outline: none;
border-color: #3b82f6; border-color: #64748b;
box-shadow: 0 0 0 1px #3b82f6; box-shadow: 0 0 0 1px #64748b;
} }
.text-wrap { .text-wrap {
white-space: normal; white-space: normal;
word-wrap: break-word; word-wrap: break-word;
line-height: 1.4; line-height: 1.4;
} }
.btn-sm { .btn-sm {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
@@ -170,58 +144,39 @@
white-space: nowrap; white-space: nowrap;
min-width: fit-content; min-width: fit-content;
} }
.collapse-content { .collapse-content {
max-height: 1000px; max-height: 1000px;
overflow: hidden; overflow: hidden;
transition: max-height 0.3s ease-out; transition: max-height 0.3s ease-out;
} }
.collapse-content.collapsed { .collapse-content.collapsed {
max-height: 0; max-height: 0;
} }
/* 진행 중 카드 스타일 */
.issue-card {
transition: all 0.2s ease;
}
.issue-card:hover {
transform: translateY(-2px);
}
.issue-card label { .issue-card label {
font-weight: 500; font-weight: 500;
} }
.issue-card input:focus,
.issue-card select:focus,
.issue-card textarea:focus {
transform: scale(1.01);
transition: transform 0.1s ease;
}
.issue-card .bg-gray-50 { .issue-card .bg-gray-50 {
border-left: 4px solid #e5e7eb; border-left: 4px solid #e5e7eb;
} }
/* 카드 내 아이콘 스타일 */ /* 카드 내 아이콘 스타일 */
.issue-card i { .issue-card i {
width: 16px; width: 16px;
text-align: center; text-align: center;
} }
.badge-new { background: #dbeafe; color: #1e40af; }
.badge-processing { background: #fef3c7; color: #92400e; }
.badge-pending { background: #ede9fe; color: #7c3aed; } .badge-pending { background: #ede9fe; color: #7c3aed; }
.badge-completed { background: #d1fae5; color: #065f46; }
</style> </style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -6,6 +6,10 @@
<title>프로젝트 관리 - 작업보고서 시스템</title> <title>프로젝트 관리 - 작업보고서 시스템</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<style> <style>
:root { :root {
--primary: #3b82f6; --primary: #3b82f6;
@@ -18,78 +22,35 @@
--gray-200: #e5e7eb; --gray-200: #e5e7eb;
--gray-300: #d1d5db; --gray-300: #d1d5db;
} }
body {
background-color: var(--gray-50);
}
.btn-primary { .btn-primary {
background-color: var(--primary); background-color: var(--primary);
color: white; color: white;
transition: all 0.2s; transition: all 0.2s;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: var(--primary-dark); background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
} }
.input-field { .input-field {
border: 1px solid var(--gray-300); border: 1px solid var(--gray-300);
background: white; background: white;
transition: all 0.2s; transition: all 0.2s;
} }
.input-field:focus { .input-field:focus {
border-color: var(--primary); border-color: var(--primary);
outline: none; outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
} }
/* 부드러운 페이드인 애니메이션 */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 헤더 전용 빠른 페이드인 */
.header-fade-in {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.header-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 본문 컨텐츠 지연 페이드인 */
.content-fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
transition-delay: 0.2s;
}
.content-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
</style> </style>
</head> </head>
<body> <body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 메인 컨텐츠 --> <!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-8 max-w-4xl content-fade-in" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8 max-w-4xl content-fade-in" style="padding-top: 72px;">
<!-- 프로젝트 생성 섹션 --> <!-- 프로젝트 생성 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-8"> <div class="bg-white rounded-xl shadow-sm p-6 mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4"> <h2 class="text-lg font-semibold text-gray-800 mb-4">

View File

@@ -11,22 +11,17 @@
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<!-- Custom Styles --> <!-- Custom Styles -->
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.report-card { .report-card {
transition: all 0.2s ease; transition: all 0.2s ease;
border-left: 4px solid transparent; border-left: 4px solid transparent;
} }
.report-card:hover { .report-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-left-color: #10b981; border-left-color: #10b981;
} }
@@ -34,10 +29,6 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.stats-card:hover {
transform: translateY(-1px);
}
.issue-row { .issue-row {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -47,11 +38,11 @@
} }
</style> </style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -10,21 +10,15 @@
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles --> <!-- 공통 스타일 -->
<style> <link rel="stylesheet" href="/static/css/tkqc-common.css">
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
</style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -10,21 +10,15 @@
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles --> <!-- 공통 스타일 -->
<style> <link rel="stylesheet" href="/static/css/tkqc-common.css">
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
</style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 --> <!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -10,49 +10,44 @@
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<!-- Custom Styles --> <!-- Custom Styles -->
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.report-card { .report-card {
transition: all 0.3s ease; transition: all 0.2s ease;
border-left: 4px solid transparent; border-left: 4px solid transparent;
} }
.report-card:hover { .report-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
border-left-color: #3b82f6; border-left-color: #3b82f6;
} }
.report-card.daily-report { .report-card.daily-report {
border-left-color: #10b981; border-left-color: #10b981;
} }
.report-card.daily-report:hover { .report-card.daily-report:hover {
border-left-color: #059669; border-left-color: #059669;
} }
.gradient-bg { .gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: #6366f1;
} }
.stats-card { .stats-card {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); background: #ec4899;
} }
</style> </style>
</head> </head>
<body class="bg-gray-50"> <body>
<!-- 공통 헤더 --> <!-- 공통 헤더 -->
<div id="commonHeader"></div> <div id="commonHeader"></div>
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;"> <main class="container mx-auto px-4 py-8" style="padding-top: 72px;">
<!-- 페이지 헤더 --> <!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -0,0 +1,39 @@
/* tkqc-common.css — tkuser 스타일 통일 */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f1f5f9;
min-height: 100vh;
}
.input-field {
background: white;
border: 1px solid #e2e8f0;
transition: all 0.2s;
}
.input-field:focus {
outline: none;
border-color: #64748b;
box-shadow: 0 0 0 3px rgba(100,116,139,0.1);
}
.issue-card {
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.issue-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.badge { display: inline-flex; align-items: center; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 500; }
.badge-new { background: #dbeafe; color: #1e40af; }
.badge-processing { background: #fef3c7; color: #92400e; }
.badge-completed { background: #d1fae5; color: #065f46; }
.badge-archived { background: #f3f4f6; color: #374151; }
.badge-cancelled { background: #fee2e2; color: #991b1b; }
.fade-in { opacity: 0; transform: translateY(10px); transition: opacity 0.4s, transform 0.4s; }
.fade-in.visible { opacity: 1; transform: translateY(0); }
.toast-message { transition: all 0.3s ease; }

View File

@@ -273,35 +273,6 @@ const IssuesAPI = {
getStats: () => apiRequest('/issues/stats/summary') getStats: () => apiRequest('/issues/stats/summary')
}; };
// Daily Work API
const DailyWorkAPI = {
create: (workData) => apiRequest('/daily-work/', {
method: 'POST',
body: JSON.stringify(workData)
}),
getAll: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
return apiRequest(`/daily-work/${queryString ? '?' + queryString : ''}`);
},
get: (id) => apiRequest(`/daily-work/${id}`),
update: (id, workData) => apiRequest(`/daily-work/${id}`, {
method: 'PUT',
body: JSON.stringify(workData)
}),
delete: (id) => apiRequest(`/daily-work/${id}`, {
method: 'DELETE'
}),
getStats: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
return apiRequest(`/daily-work/stats/summary${queryString ? '?' + queryString : ''}`);
}
};
// Reports API // Reports API
const ReportsAPI = { const ReportsAPI = {
getSummary: (startDate, endDate) => apiRequest('/reports/summary', { getSummary: (startDate, endDate) => apiRequest('/reports/summary', {
@@ -320,13 +291,6 @@ const ReportsAPI = {
return apiRequest(`/reports/issues?${params}`); return apiRequest(`/reports/issues?${params}`);
}, },
getDailyWorks: (startDate, endDate) => {
const params = new URLSearchParams({
start_date: startDate,
end_date: endDate
}).toString();
return apiRequest(`/reports/daily-works?${params}`);
}
}; };
// 권한 체크 // 권한 체크

View File

@@ -270,10 +270,7 @@ class App {
const titles = { const titles = {
'dashboard': '대시보드', 'dashboard': '대시보드',
'issues': '부적합 사항', 'issues': '부적합 사항',
'projects': '프로젝트', 'reports': '보고서'
'daily_work': '일일 공수',
'reports': '보고서',
'users': '사용자 관리'
}; };
const title = titles[module] || module; const title = titles[module] || module;

View File

@@ -32,8 +32,8 @@ class CommonHeader {
icon: 'fas fa-chart-line', icon: 'fas fa-chart-line',
url: '/issues-dashboard.html', url: '/issues-dashboard.html',
pageName: 'issues_dashboard', pageName: 'issues_dashboard',
color: 'text-slate-600', color: 'text-slate-300',
bgColor: 'text-slate-600 hover:bg-slate-100' bgColor: 'text-slate-300 hover:bg-slate-700'
}, },
{ {
id: 'issues_inbox', id: 'issues_inbox',
@@ -41,8 +41,8 @@ class CommonHeader {
icon: 'fas fa-inbox', icon: 'fas fa-inbox',
url: '/issues-inbox.html', url: '/issues-inbox.html',
pageName: 'issues_inbox', pageName: 'issues_inbox',
color: 'text-slate-600', color: 'text-slate-300',
bgColor: 'text-slate-600 hover:bg-slate-100' bgColor: 'text-slate-300 hover:bg-slate-700'
}, },
{ {
id: 'issues_management', id: 'issues_management',
@@ -50,8 +50,8 @@ class CommonHeader {
icon: 'fas fa-cog', icon: 'fas fa-cog',
url: '/issues-management.html', url: '/issues-management.html',
pageName: 'issues_management', pageName: 'issues_management',
color: 'text-slate-600', color: 'text-slate-300',
bgColor: 'text-slate-600 hover:bg-slate-100' bgColor: 'text-slate-300 hover:bg-slate-700'
}, },
{ {
id: 'issues_archive', id: 'issues_archive',
@@ -59,17 +59,8 @@ class CommonHeader {
icon: 'fas fa-archive', icon: 'fas fa-archive',
url: '/issues-archive.html', url: '/issues-archive.html',
pageName: 'issues_archive', pageName: 'issues_archive',
color: 'text-slate-600', color: 'text-slate-300',
bgColor: 'text-slate-600 hover:bg-slate-100' bgColor: 'text-slate-300 hover:bg-slate-700'
},
{
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', id: 'reports',
@@ -77,8 +68,8 @@ class CommonHeader {
icon: 'fas fa-chart-bar', icon: 'fas fa-chart-bar',
url: '/reports.html', url: '/reports.html',
pageName: 'reports', pageName: 'reports',
color: 'text-slate-600', color: 'text-slate-300',
bgColor: 'text-slate-600 hover:bg-slate-100', bgColor: 'text-slate-300 hover:bg-slate-700',
subMenus: [ subMenus: [
{ {
id: 'reports_daily', id: 'reports_daily',
@@ -86,7 +77,7 @@ class CommonHeader {
icon: 'fas fa-file-excel', icon: 'fas fa-file-excel',
url: '/reports-daily.html', url: '/reports-daily.html',
pageName: 'reports_daily', pageName: 'reports_daily',
color: 'text-slate-600' color: 'text-slate-300'
}, },
{ {
id: 'reports_weekly', id: 'reports_weekly',
@@ -94,7 +85,7 @@ class CommonHeader {
icon: 'fas fa-calendar-week', icon: 'fas fa-calendar-week',
url: '/reports-weekly.html', url: '/reports-weekly.html',
pageName: 'reports_weekly', pageName: 'reports_weekly',
color: 'text-slate-600' color: 'text-slate-300'
}, },
{ {
id: 'reports_monthly', id: 'reports_monthly',
@@ -102,29 +93,10 @@ class CommonHeader {
icon: 'fas fa-calendar-alt', icon: 'fas fa-calendar-alt',
url: '/reports-monthly.html', url: '/reports-monthly.html',
pageName: 'reports_monthly', pageName: 'reports_monthly',
color: 'text-slate-600' color: 'text-slate-300'
} }
] ]
}, },
{
id: 'projects_manage',
title: '프로젝트 관리',
icon: 'fas fa-folder-open',
url: '/project-management.html',
pageName: 'projects_manage',
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100'
},
{
id: 'users_manage',
title: '사용자 관리',
icon: 'fas fa-users-cog',
url: this._getUserManageUrl(),
pageName: 'users_manage',
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100',
external: true
}
]; ];
} }
@@ -205,14 +177,14 @@ class CommonHeader {
const userRole = this.getUserRoleDisplay(); const userRole = this.getUserRoleDisplay();
return ` return `
<header class="bg-white shadow-sm border-b sticky top-0 z-50"> <header class="bg-slate-800 text-white sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-14">
<!-- 로고 및 제목 --> <!-- 로고 및 제목 -->
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0 flex items-center"> <div class="flex-shrink-0 flex items-center">
<i class="fas fa-shield-halved text-2xl text-slate-700 mr-3"></i> <i class="fas fa-shield-halved text-2xl text-slate-300 mr-3"></i>
<h1 class="text-xl font-bold text-gray-900">부적합 관리</h1> <h1 class="text-xl font-bold text-white">부적합 관리</h1>
</div> </div>
</div> </div>
@@ -226,8 +198,8 @@ class CommonHeader {
<!-- 사용자 정보 --> <!-- 사용자 정보 -->
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="text-right"> <div class="text-right">
<div class="text-sm font-medium text-gray-900">${userDisplayName}</div> <div class="text-sm font-medium text-white">${userDisplayName}</div>
<div class="text-xs text-gray-500">${userRole}</div> <div class="text-xs text-slate-400">${userRole}</div>
</div> </div>
<div class="w-8 h-8 bg-slate-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"> <span class="text-white text-sm font-semibold">
@@ -238,7 +210,7 @@ class CommonHeader {
<!-- 드롭다운 메뉴 --> <!-- 드롭다운 메뉴 -->
<div class="relative"> <div class="relative">
<button id="user-menu-button" class="p-2 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-md"> <button id="user-menu-button" class="p-2 text-slate-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 focus:ring-offset-slate-800 rounded-md">
<i class="fas fa-chevron-down"></i> <i class="fas fa-chevron-down"></i>
</button> </button>
<div id="user-menu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5"> <div id="user-menu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5">
@@ -252,14 +224,14 @@ class CommonHeader {
</div> </div>
<!-- 모바일 메뉴 버튼 --> <!-- 모바일 메뉴 버튼 -->
<button id="mobile-menu-button" class="md:hidden p-2 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-md"> <button id="mobile-menu-button" class="md:hidden p-2 text-slate-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 focus:ring-offset-slate-800 rounded-md">
<i class="fas fa-bars"></i> <i class="fas fa-bars"></i>
</button> </button>
</div> </div>
</div> </div>
<!-- 모바일 메뉴 --> <!-- 모바일 메뉴 -->
<div id="mobile-menu" class="md:hidden hidden border-t border-gray-200 py-3"> <div id="mobile-menu" class="md:hidden hidden border-t border-slate-700 py-3">
<div class="space-y-1"> <div class="space-y-1">
${accessibleMenus.map(menu => this.generateMobileMenuItemHTML(menu)).join('')} ${accessibleMenus.map(menu => this.generateMobileMenuItemHTML(menu)).join('')}
</div> </div>
@@ -370,7 +342,7 @@ class CommonHeader {
*/ */
generateMobileMenuItemHTML(menu) { generateMobileMenuItemHTML(menu) {
const isActive = this.currentPage === menu.id; const isActive = this.currentPage === menu.id;
const activeClass = isActive ? 'bg-slate-100 text-slate-800 border-slate-600' : 'text-gray-700 hover:bg-gray-50'; const activeClass = isActive ? 'bg-slate-700 text-white border-white' : 'text-slate-300 hover:bg-slate-700';
// 하위 메뉴가 있는 경우 // 하위 메뉴가 있는 경우
if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) { if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) {
@@ -390,7 +362,7 @@ class CommonHeader {
<div class="hidden ml-6 mt-1 space-y-1"> <div class="hidden ml-6 mt-1 space-y-1">
${menu.accessibleSubMenus.map(subMenu => ` ${menu.accessibleSubMenus.map(subMenu => `
<a href="${subMenu.url}" <a href="${subMenu.url}"
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' : ''}" class="flex items-center px-3 py-2 rounded-md text-sm font-medium text-slate-400 hover:bg-slate-700 hover:text-white ${this.currentPage === subMenu.id ? 'bg-slate-700 text-white' : ''}"
data-page="${subMenu.id}" data-page="${subMenu.id}"
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')"> onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
<i class="${subMenu.icon} mr-3 ${subMenu.color}"></i> <i class="${subMenu.icon} mr-3 ${subMenu.color}"></i>
@@ -491,7 +463,7 @@ class CommonHeader {
loader.className = 'fixed inset-0 bg-white bg-opacity-75 flex items-center justify-center z-50'; loader.className = 'fixed inset-0 bg-white bg-opacity-75 flex items-center justify-center z-50';
loader.innerHTML = ` loader.innerHTML = `
<div class="text-center"> <div class="text-center">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-slate-600"></div>
<p class="mt-2 text-sm text-gray-600">페이지를 로드하는 중...</p> <p class="mt-2 text-sm text-gray-600">페이지를 로드하는 중...</p>
</div> </div>
`; `;
@@ -515,7 +487,7 @@ class CommonHeader {
<div class="bg-white rounded-xl p-6 w-96 max-w-md mx-4 shadow-2xl"> <div class="bg-white rounded-xl p-6 w-96 max-w-md mx-4 shadow-2xl">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800"> <h3 class="text-lg font-semibold text-gray-800">
<i class="fas fa-key mr-2 text-blue-500"></i>비밀번호 변경 <i class="fas fa-key mr-2 text-slate-500"></i>비밀번호 변경
</h3> </h3>
<button onclick="CommonHeader.hidePasswordModal()" class="text-gray-400 hover:text-gray-600 transition-colors"> <button onclick="CommonHeader.hidePasswordModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-lg"></i> <i class="fas fa-times text-lg"></i>
@@ -526,21 +498,21 @@ class CommonHeader {
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label> <label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label>
<input type="password" id="currentPasswordInput" <input type="password" id="currentPasswordInput"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500"
required placeholder="현재 비밀번호를 입력하세요"> required placeholder="현재 비밀번호를 입력하세요">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label> <label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
<input type="password" id="newPasswordInput" <input type="password" id="newPasswordInput"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500"
required minlength="6" placeholder="새 비밀번호 (최소 6자)"> required minlength="6" placeholder="새 비밀번호 (최소 6자)">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label> <label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
<input type="password" id="confirmPasswordInput" <input type="password" id="confirmPasswordInput"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500"
required placeholder="새 비밀번호를 다시 입력하세요"> required placeholder="새 비밀번호를 다시 입력하세요">
</div> </div>
@@ -550,7 +522,7 @@ class CommonHeader {
취소 취소
</button> </button>
<button type="submit" <button type="submit"
class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"> class="flex-1 px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 transition-colors">
<i class="fas fa-save mr-1"></i>변경 <i class="fas fa-save mr-1"></i>변경
</button> </button>
</div> </div>
@@ -683,10 +655,10 @@ class CommonHeader {
const itemPageId = item.getAttribute('data-page'); const itemPageId = item.getAttribute('data-page');
if (itemPageId === pageId) { if (itemPageId === pageId) {
item.classList.add('bg-slate-700', 'text-white'); item.classList.add('bg-slate-700', 'text-white');
item.classList.remove('text-slate-600', 'hover:bg-slate-100'); item.classList.remove('text-slate-300', 'hover:bg-slate-700');
} else { } else {
item.classList.remove('bg-slate-700', 'text-white'); item.classList.remove('bg-slate-700', 'text-white');
item.classList.add('text-slate-600'); item.classList.add('text-slate-300');
} }
}); });
} }

View File

@@ -28,10 +28,7 @@ class KeyboardShortcutManager {
// 네비게이션 단축키 // 네비게이션 단축키
this.register('g h', () => this.navigateToPage('/index.html', 'issues_create'), '홈 (부적합 등록)'); this.register('g h', () => this.navigateToPage('/index.html', 'issues_create'), '홈 (부적합 등록)');
this.register('g v', () => this.navigateToPage('/issue-view.html', 'issues_view'), '부적합 조회'); this.register('g v', () => this.navigateToPage('/issue-view.html', 'issues_view'), '부적합 조회');
this.register('g d', () => this.navigateToPage('/daily-work.html', 'daily_work'), '일일 공수');
this.register('g p', () => this.navigateToPage('/project-management.html', 'projects_manage'), '프로젝트 관리');
this.register('g r', () => this.navigateToPage('/reports.html', 'reports'), '보고서'); this.register('g r', () => this.navigateToPage('/reports.html', 'reports'), '보고서');
this.register('g a', () => this.navigateToPage('/admin.html', 'users_manage'), '관리자');
// 액션 단축키 // 액션 단축키
this.register('n', () => this.triggerNewAction(), '새 항목 생성'); this.register('n', () => this.triggerNewAction(), '새 항목 생성');
@@ -40,8 +37,6 @@ class KeyboardShortcutManager {
this.register('f', () => this.focusSearchField(), '검색 포커스'); this.register('f', () => this.focusSearchField(), '검색 포커스');
// 관리자 전용 단축키 // 관리자 전용 단축키
this.register('ctrl+shift+u', () => this.navigateToPage('/admin.html', 'users_manage'), '사용자 관리 (관리자)');
console.log('⌨️ 키보드 단축키 등록 완료'); console.log('⌨️ 키보드 단축키 등록 완료');
} }

View File

@@ -169,14 +169,8 @@ class PageManager {
return new IssuesViewModule(options); return new IssuesViewModule(options);
case 'issues_manage': case 'issues_manage':
return new IssuesManageModule(options); return new IssuesManageModule(options);
case 'projects_manage':
return new ProjectsManageModule(options);
case 'daily_work':
return new DailyWorkModule(options);
case 'reports': case 'reports':
return new ReportsModule(options); return new ReportsModule(options);
case 'users_manage':
return new UsersManageModule(options);
default: default:
console.warn(`알 수 없는 페이지 ID: ${pageId}`); console.warn(`알 수 없는 페이지 ID: ${pageId}`);
return null; return null;

View File

@@ -57,10 +57,7 @@ class PagePreloader {
{ id: 'issues_create', url: '/index.html', priority: 1 }, { id: 'issues_create', url: '/index.html', priority: 1 },
{ id: 'issues_view', url: '/issue-view.html', priority: 1 }, { id: 'issues_view', url: '/issue-view.html', priority: 1 },
{ id: 'issues_manage', url: '/index.html#list', priority: 2 }, { id: 'issues_manage', url: '/index.html#list', priority: 2 },
{ id: 'projects_manage', url: '/project-management.html', priority: 3 }, { id: 'reports', url: '/reports.html', priority: 3 }
{ id: 'daily_work', url: '/daily-work.html', priority: 2 },
{ id: 'reports', url: '/reports.html', priority: 3 },
{ id: 'users_manage', url: '/admin.html', priority: 4 }
]; ];
// 권한 체크 // 권한 체크

View File

@@ -20,10 +20,7 @@ class PagePermissionManager {
'issues_inbox': { title: '수신함', defaultAccess: true }, 'issues_inbox': { title: '수신함', defaultAccess: true },
'issues_management': { title: '관리함', defaultAccess: false }, 'issues_management': { title: '관리함', defaultAccess: false },
'issues_archive': { title: '폐기함', defaultAccess: false }, 'issues_archive': { title: '폐기함', defaultAccess: false },
'projects_manage': { title: '프로젝트 관리', defaultAccess: false }, 'reports': { title: '보고서', defaultAccess: false }
'daily_work': { title: '일일 공수', defaultAccess: false },
'reports': { title: '보고서', defaultAccess: false },
'users_manage': { title: '사용자 관리', defaultAccess: false }
}; };
} }
@@ -167,20 +164,6 @@ class PagePermissionManager {
path: '#issues/manage', path: '#issues/manage',
pageName: 'issues_manage' pageName: 'issues_manage'
}, },
{
id: 'projects_manage',
title: '프로젝트 관리',
icon: 'fas fa-folder-open',
path: '#projects/manage',
pageName: 'projects_manage'
},
{
id: 'daily_work',
title: '일일 공수',
icon: 'fas fa-calendar-check',
path: '#daily-work',
pageName: 'daily_work'
},
{ {
id: 'reports', id: 'reports',
title: '보고서', title: '보고서',
@@ -188,13 +171,6 @@ class PagePermissionManager {
path: '#reports', path: '#reports',
pageName: 'reports' pageName: 'reports'
}, },
{
id: 'users_manage',
title: '사용자 관리',
icon: 'fas fa-users-cog',
path: '#users/manage',
pageName: 'users_manage'
}
]; ];
// 페이지 권한에 따라 메뉴 필터링 // 페이지 권한에 따라 메뉴 필터링