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
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")
page_permissions = relationship("UserPagePermission", back_populates="user", foreign_keys="UserPagePermission.user_id")
class UserPagePermission(Base):
@@ -183,37 +182,6 @@ class Project(Base):
primaryjoin="Project.id == 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):
__tablename__ = "qc_deletion_logs"

View File

@@ -250,6 +250,10 @@ class ManagementUpdateRequest(BaseModel):
completion_photo5: Optional[str] = None # Base64 - 완료 사진 5
review_status: Optional[ReviewStatus] = None
class OpinionRequest(BaseModel):
"""의견 제시 요청 (로그인한 모든 사용자 가능)"""
opinion: str # 의견 내용
class InboxIssue(BaseModel):
"""수신함용 부적합 정보 (간소화된 버전)"""
id: int
@@ -296,33 +300,6 @@ class Project(ProjectBase):
class Config:
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
class ReportRequest(BaseModel):
start_date: datetime
@@ -342,24 +319,6 @@ class ReportSummary(BaseModel):
completed_issues: int
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):
project_id: int

View File

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

View File

@@ -246,6 +246,42 @@ async def delete_issue(
db.commit()
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")
async def get_issue_stats(
current_user: User = Depends(get_current_user),

View File

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

View File

@@ -13,7 +13,7 @@ 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, ReviewStatus
from database.models import Issue, 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
@@ -30,14 +30,9 @@ async def generate_report_summary(
"""보고서 요약 생성"""
start_date = report_request.start_date
end_date = report_request.end_date
# 일일 공수 합계
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)
total_hours = 0
# 이슈 통계
issues_query = db.query(Issue).filter(
Issue.report_date >= start_date,
@@ -116,29 +111,6 @@ async def get_report_issues(
"detail_notes": issue.detail_notes
} 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")
async def preview_daily_report(
project_id: int,

View File

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

View File

@@ -6,6 +6,10 @@
<title>일일 공수 입력</title>
<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="/static/css/tkqc-common.css">
<style>
:root {
--primary: #3b82f6;
@@ -16,62 +20,55 @@
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
}
body {
background-color: var(--gray-50);
}
.btn-primary {
background-color: var(--primary);
color: white;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-success {
background-color: var(--success);
color: white;
transition: all 0.2s;
}
.btn-success:hover {
background-color: #059669;
transform: translateY(-1px);
}
.input-field {
border: 1px solid var(--gray-300);
background: white;
transition: all 0.2s;
}
.input-field:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.work-card {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.work-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-card {
background: linear-gradient(135deg, #3b82f6, #2563eb);
background: #2563eb;
color: white;
}
.nav-link {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
@@ -79,61 +76,24 @@
transition: all 0.2s;
white-space: nowrap;
}
.nav-link:hover {
background-color: #f3f4f6;
color: #1f2937;
}
.nav-link.active {
background-color: #3b82f6;
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>
</head>
<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">
<h2 class="text-lg font-semibold text-gray-800 mb-6">

View File

@@ -11,42 +11,19 @@
<!-- 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="/static/css/tkqc-common.css">
<!-- Custom Styles -->
<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 {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.nav-link {
color: #6b7280;
padding: 0.5rem 1rem;
@@ -54,49 +31,23 @@
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover {
background-color: #f3f4f6;
color: #3b82f6;
color: #334155;
}
.nav-link.active {
background-color: #3b82f6;
background-color: #334155;
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;
transform: translateY(10px);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.content-fade-in.visible {
opacity: 1;
transform: translateY(0);
@@ -107,7 +58,7 @@
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 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="mb-4">

View File

@@ -13,60 +13,33 @@
<!-- 모바일 캘린더 스타일 -->
<link rel="stylesheet" href="/static/css/mobile-calendar.css">
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
<!-- Custom Styles -->
<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 {
border-left: 4px solid #6b7280;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
background: #f8fafc;
}
.completed-card {
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 {
position: relative;
height: 300px;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 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="flex items-center justify-between mb-4">

View File

@@ -6,96 +6,55 @@
<title>부적합 현황판</title>
<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="/static/css/tkqc-common.css">
<style>
.fade-in { opacity: 0; animation: fadeIn 0.5s ease-in forwards; }
@keyframes fadeIn { to { opacity: 1; } }
.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); } }
.content-fade-in { opacity: 0; animation: contentFadeIn 0.4s ease-out 0.2s forwards; }
@keyframes contentFadeIn { to { opacity: 1; transform: translateY(0); } from { transform: translateY(10px); } }
/* 대시보드 카드 스타일 */
.dashboard-card {
transition: all 0.3s ease;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: all 0.2s ease;
}
.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);
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);
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 {
font-weight: 600;
color: #374151;
}
.issue-card .bg-gray-50 {
background-color: #f9fafb;
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 {
margin-bottom: 1.5rem;
}
.date-header {
cursor: pointer;
transition: all 0.2s ease;
}
.date-header:hover {
background-color: #f3f4f6 !important;
}
.collapse-content {
transition: all 0.3s ease;
}
.progress-bar {
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
background: #10b981;
transition: width 0.8s ease;
}
/* 반응형 그리드 */
.dashboard-grid {
display: grid;
@@ -104,7 +63,7 @@
}
</style>
</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 class="text-center">
@@ -139,7 +98,7 @@
<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="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="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>
<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>
</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>
<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>
<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 class="text-3xl font-bold" id="todayNew">0</p>
</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 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>
<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>
<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 class="text-3xl font-bold" id="pendingCompletion">0</p>
</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 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>
<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>
<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 class="text-3xl font-bold" id="overdue">0</p>
</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>
@@ -2131,44 +2090,15 @@
}
try {
// 현재 이슈 정보 가져오기
const issueResponse = await fetch(`/api/issues/${selectedOpinionIssueId}`, {
headers: {
'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',
// 별도 의견 제시 API 사용 (권한 체크 없음, 로그인만 필요)
const response = await fetch(`/api/issues/${selectedOpinionIssueId}/opinion`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${TokenManager.getToken()}`,
'Content-Type': 'application/json'
},
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/tkqc-common.css">
<!-- Custom Styles -->
<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 {
position: fixed;
top: 0;
@@ -37,81 +34,32 @@
visibility: hidden;
transition: all 0.3s ease;
}
.loading-overlay.active {
opacity: 1;
visibility: visible;
}
.issue-card {
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.issue-card.unread {
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-medium { border-left-color: #f59e0b; }
.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 {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
transition-delay: 0.2s;
transform: translateY(10px);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.content-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 사진 미리보기 스타일 */
.photo-preview {
max-width: 150px;
@@ -121,18 +69,18 @@
cursor: pointer;
transition: transform 0.2s ease;
}
.photo-preview:hover {
transform: scale(1.05);
}
.photo-gallery {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 8px;
}
.photo-modal {
position: fixed;
top: 0;
@@ -145,14 +93,14 @@
align-items: center;
z-index: 1000;
}
.photo-modal img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 8px;
}
.photo-modal .close-btn {
position: absolute;
top: 20px;
@@ -167,7 +115,7 @@
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<body>
<!-- 로딩 오버레이 -->
<div id="loadingOverlay" class="loading-overlay">
<div class="text-center">
@@ -179,7 +127,7 @@
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 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="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/tkqc-common.css">
<!-- Custom Styles -->
<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-medium { border-left-color: #f59e0b; }
.priority-low { border-left-color: #10b981; }
.status-new { border-left-color: #3b82f6; }
.status-processing { border-left-color: #f59e0b; }
.status-pending { border-left-color: #8b5cf6; }
.status-completed { border-left-color: #10b981; }
.action-btn {
transition: all 0.2s ease;
}
.action-btn:hover {
transform: scale(1.05);
}
.modal {
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 {
margin-bottom: 1.5rem;
}
.date-header {
cursor: pointer;
transition: all 0.2s ease;
}
.date-header:hover {
background-color: #f9fafb;
}
/* 좌우 스크롤 가능한 이슈 테이블 */
.issue-table-container {
overflow-x: auto;
@@ -83,13 +57,13 @@
border-radius: 0.5rem;
margin-top: 0.5rem;
}
.issue-table {
min-width: 2000px; /* 더 넓은 최소 너비 설정 */
min-width: 2000px;
width: 100%;
border-collapse: collapse;
}
.issue-table th,
.issue-table td {
padding: 0.75rem;
@@ -97,7 +71,7 @@
border-bottom: 1px solid #f3f4f6;
vertical-align: top;
}
.issue-table th {
background-color: #f9fafb;
font-weight: 600;
@@ -105,11 +79,11 @@
font-size: 0.875rem;
white-space: nowrap;
}
.issue-table tbody tr:hover {
background-color: #f9fafb;
}
/* 컬럼별 너비 조정 */
.col-no { min-width: 60px; }
.col-project { min-width: 120px; }
@@ -125,7 +99,7 @@
.col-photos { min-width: 150px; }
.col-completion { min-width: 80px; }
.col-actions { min-width: 120px; }
.issue-photo {
width: 60px;
height: 40px;
@@ -134,13 +108,13 @@
cursor: pointer;
margin: 2px;
}
.photo-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* 편집 가능한 필드 스타일 */
.editable-field {
min-width: 100%;
@@ -149,19 +123,19 @@
border-radius: 4px;
font-size: 0.875rem;
}
.editable-field:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 1px #3b82f6;
border-color: #64748b;
box-shadow: 0 0 0 1px #64748b;
}
.text-wrap {
white-space: normal;
word-wrap: break-word;
line-height: 1.4;
}
.btn-sm {
padding: 4px 8px;
font-size: 0.75rem;
@@ -170,58 +144,39 @@
white-space: nowrap;
min-width: fit-content;
}
.collapse-content {
max-height: 1000px;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.collapse-content.collapsed {
max-height: 0;
}
/* 진행 중 카드 스타일 */
.issue-card {
transition: all 0.2s ease;
}
.issue-card:hover {
transform: translateY(-2px);
}
.issue-card label {
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 {
border-left: 4px solid #e5e7eb;
}
/* 카드 내 아이콘 스타일 */
.issue-card i {
width: 16px;
text-align: center;
}
.badge-new { background: #dbeafe; color: #1e40af; }
.badge-processing { background: #fef3c7; color: #92400e; }
.badge-pending { background: #ede9fe; color: #7c3aed; }
.badge-completed { background: #d1fae5; color: #065f46; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 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="flex items-center justify-between mb-4">

View File

@@ -6,6 +6,10 @@
<title>프로젝트 관리 - 작업보고서 시스템</title>
<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="/static/css/tkqc-common.css">
<style>
:root {
--primary: #3b82f6;
@@ -18,78 +22,35 @@
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
}
body {
background-color: var(--gray-50);
}
.btn-primary {
background-color: var(--primary);
color: white;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.input-field {
border: 1px solid var(--gray-300);
background: white;
transition: all 0.2s;
}
.input-field:focus {
border-color: var(--primary);
outline: none;
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>
</head>
<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">
<h2 class="text-lg font-semibold text-gray-800 mb-4">

View File

@@ -11,22 +11,17 @@
<!-- 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="/static/css/tkqc-common.css">
<!-- Custom Styles -->
<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 {
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.report-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-left-color: #10b981;
}
@@ -34,10 +29,6 @@
transition: all 0.2s ease;
}
.stats-card:hover {
transform: translateY(-1px);
}
.issue-row {
transition: all 0.2s ease;
}
@@ -47,11 +38,11 @@
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 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="flex items-center justify-between mb-4">

View File

@@ -10,21 +10,15 @@
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
</style>
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
</head>
<body class="bg-gray-50 min-h-screen">
<body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 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="flex items-center justify-between mb-4">

View File

@@ -10,21 +10,15 @@
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
</style>
<!-- 공통 스타일 -->
<link rel="stylesheet" href="/static/css/tkqc-common.css">
</head>
<body class="bg-gray-50 min-h-screen">
<body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 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="flex items-center justify-between mb-4">

View File

@@ -10,49 +10,44 @@
<!-- 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="/static/css/tkqc-common.css">
<!-- Custom Styles -->
<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 {
transition: all 0.3s ease;
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.report-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
border-left-color: #3b82f6;
}
.report-card.daily-report {
border-left-color: #10b981;
}
.report-card.daily-report:hover {
border-left-color: #059669;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #6366f1;
}
.stats-card {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
background: #ec4899;
}
</style>
</head>
<body class="bg-gray-50">
<body>
<!-- 공통 헤더 -->
<div id="commonHeader"></div>
<!-- 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="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')
};
// 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
const ReportsAPI = {
getSummary: (startDate, endDate) => apiRequest('/reports/summary', {
@@ -320,13 +291,6 @@ const ReportsAPI = {
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 = {
'dashboard': '대시보드',
'issues': '부적합 사항',
'projects': '프로젝트',
'daily_work': '일일 공수',
'reports': '보고서',
'users': '사용자 관리'
'reports': '보고서'
};
const title = titles[module] || module;

View File

@@ -32,8 +32,8 @@ class CommonHeader {
icon: 'fas fa-chart-line',
url: '/issues-dashboard.html',
pageName: 'issues_dashboard',
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100'
color: 'text-slate-300',
bgColor: 'text-slate-300 hover:bg-slate-700'
},
{
id: 'issues_inbox',
@@ -41,8 +41,8 @@ class CommonHeader {
icon: 'fas fa-inbox',
url: '/issues-inbox.html',
pageName: 'issues_inbox',
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100'
color: 'text-slate-300',
bgColor: 'text-slate-300 hover:bg-slate-700'
},
{
id: 'issues_management',
@@ -50,8 +50,8 @@ class CommonHeader {
icon: 'fas fa-cog',
url: '/issues-management.html',
pageName: 'issues_management',
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100'
color: 'text-slate-300',
bgColor: 'text-slate-300 hover:bg-slate-700'
},
{
id: 'issues_archive',
@@ -59,17 +59,8 @@ class CommonHeader {
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'
color: 'text-slate-300',
bgColor: 'text-slate-300 hover:bg-slate-700'
},
{
id: 'reports',
@@ -77,8 +68,8 @@ class CommonHeader {
icon: 'fas fa-chart-bar',
url: '/reports.html',
pageName: 'reports',
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100',
color: 'text-slate-300',
bgColor: 'text-slate-300 hover:bg-slate-700',
subMenus: [
{
id: 'reports_daily',
@@ -86,7 +77,7 @@ class CommonHeader {
icon: 'fas fa-file-excel',
url: '/reports-daily.html',
pageName: 'reports_daily',
color: 'text-slate-600'
color: 'text-slate-300'
},
{
id: 'reports_weekly',
@@ -94,7 +85,7 @@ class CommonHeader {
icon: 'fas fa-calendar-week',
url: '/reports-weekly.html',
pageName: 'reports_weekly',
color: 'text-slate-600'
color: 'text-slate-300'
},
{
id: 'reports_monthly',
@@ -102,29 +93,10 @@ class CommonHeader {
icon: 'fas fa-calendar-alt',
url: '/reports-monthly.html',
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();
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="flex justify-between items-center h-16">
<div class="flex justify-between items-center h-14">
<!-- 로고 및 제목 -->
<div class="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>
<h1 class="text-xl font-bold text-gray-900">부적합 관리</h1>
<i class="fas fa-shield-halved text-2xl text-slate-300 mr-3"></i>
<h1 class="text-xl font-bold text-white">부적합 관리</h1>
</div>
</div>
@@ -226,8 +198,8 @@ class CommonHeader {
<!-- 사용자 정보 -->
<div class="flex items-center space-x-3">
<div class="text-right">
<div class="text-sm font-medium text-gray-900">${userDisplayName}</div>
<div class="text-xs text-gray-500">${userRole}</div>
<div class="text-sm font-medium text-white">${userDisplayName}</div>
<div class="text-xs text-slate-400">${userRole}</div>
</div>
<div class="w-8 h-8 bg-slate-600 rounded-full flex items-center justify-center">
<span class="text-white text-sm font-semibold">
@@ -238,7 +210,7 @@ class CommonHeader {
<!-- 드롭다운 메뉴 -->
<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>
</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">
@@ -252,14 +224,14 @@ class CommonHeader {
</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>
</button>
</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">
${accessibleMenus.map(menu => this.generateMobileMenuItemHTML(menu)).join('')}
</div>
@@ -370,7 +342,7 @@ class CommonHeader {
*/
generateMobileMenuItemHTML(menu) {
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) {
@@ -390,7 +362,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-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}"
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
<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.innerHTML = `
<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>
</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="flex justify-between items-center mb-4">
<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>
<button onclick="CommonHeader.hidePasswordModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-lg"></i>
@@ -526,21 +498,21 @@ class CommonHeader {
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label>
<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="현재 비밀번호를 입력하세요">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
<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자)">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
<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="새 비밀번호를 다시 입력하세요">
</div>
@@ -550,7 +522,7 @@ class CommonHeader {
취소
</button>
<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>변경
</button>
</div>
@@ -683,10 +655,10 @@ class CommonHeader {
const itemPageId = item.getAttribute('data-page');
if (itemPageId === pageId) {
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 {
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 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 a', () => this.navigateToPage('/admin.html', 'users_manage'), '관리자');
// 액션 단축키
this.register('n', () => this.triggerNewAction(), '새 항목 생성');
@@ -40,8 +37,6 @@ class KeyboardShortcutManager {
this.register('f', () => this.focusSearchField(), '검색 포커스');
// 관리자 전용 단축키
this.register('ctrl+shift+u', () => this.navigateToPage('/admin.html', 'users_manage'), '사용자 관리 (관리자)');
console.log('⌨️ 키보드 단축키 등록 완료');
}

View File

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

View File

@@ -57,10 +57,7 @@ class PagePreloader {
{ id: 'issues_create', url: '/index.html', priority: 1 },
{ id: 'issues_view', url: '/issue-view.html', priority: 1 },
{ id: 'issues_manage', url: '/index.html#list', priority: 2 },
{ id: 'projects_manage', url: '/project-management.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 }
{ id: 'reports', url: '/reports.html', priority: 3 }
];
// 권한 체크

View File

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