refactor(system3): 프로젝트/사용자/일일공수 관리 기능 제거

tkuser에서 프로젝트/사용자를 통합 관리하므로 TKQC에서 불필요한 기능 제거:
- 프로젝트 관리: POST/PUT/DELETE API 및 페이지 삭제 (GET 유지)
- 사용자 관리: CRUD API 및 admin.html 삭제 (login/me/change-password 유지)
- 일일 공수: daily_work.py, daily-work.html 삭제, reports.py에서 DailyWork 참조 제거
- 디버그 페이지 4개 삭제 (check-projects, sync-projects, test_api, mobile-fix)
- 네비게이션/권한/키보드 단축키에서 제거된 메뉴 정리
- tkuser permissionModel.js에서 daily_work, projects_manage 키 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-05 10:35:06 +09:00
parent 6f1efdb03c
commit 2197cdb3d5
22 changed files with 11 additions and 3355 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):
@@ -184,37 +183,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

@@ -49,16 +49,6 @@ class UserBase(BaseModel):
role: UserRole = UserRole.user
department: Optional[DepartmentType] = None
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
full_name: Optional[str] = None
password: Optional[str] = None
role: Optional[UserRole] = None
department: Optional[DepartmentType] = None
is_active: Optional[bool] = None
class PasswordChange(BaseModel):
current_password: str
new_password: str
@@ -286,13 +276,6 @@ class ProjectBase(BaseModel):
job_no: str = Field(..., min_length=1, max_length=50)
project_name: str = Field(..., min_length=1, max_length=200)
class ProjectCreate(ProjectBase):
pass
class ProjectUpdate(BaseModel):
project_name: Optional[str] = Field(None, min_length=1, max_length=200)
is_active: Optional[bool] = None
class Project(ProjectBase):
id: int
created_by_id: Optional[int] = None
@@ -303,50 +286,6 @@ class Project(ProjectBase):
class Config:
from_attributes = True
# DailyWork 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
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
# Report schemas
class ReportRequest(BaseModel):
start_date: datetime

View File

@@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import List
from pydantic import BaseModel
from database.database import get_db
from database.models import User, UserRole
@@ -95,70 +94,6 @@ async def get_all_users(
users = db.query(User).filter(User.is_active == True).all()
return users
@router.post("/users", response_model=schemas.User)
async def create_user(
user: schemas.UserCreate,
current_admin: User = Depends(get_current_admin),
db: Session = Depends(get_db)
):
# 중복 확인
db_user = db.query(User).filter(User.username == user.username).first()
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
# 사용자 생성
db_user = User(
username=user.username,
hashed_password=get_password_hash(user.password),
full_name=user.full_name,
role=user.role
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.put("/users/{user_id}", response_model=schemas.User)
async def update_user(
user_id: int,
user_update: schemas.UserUpdate,
current_admin: User = Depends(get_current_admin),
db: Session = Depends(get_db)
):
db_user = db.query(User).filter(User.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
# 업데이트
update_data = user_update.dict(exclude_unset=True)
if "password" in update_data:
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
for field, value in update_data.items():
setattr(db_user, field, value)
db.commit()
db.refresh(db_user)
return db_user
@router.delete("/users/{username}")
async def delete_user(
username: str,
current_admin: User = Depends(get_current_admin),
db: Session = Depends(get_db)
):
db_user = db.query(User).filter(User.username == username).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
# hyungi 계정은 삭제 불가
if db_user.username == "hyungi":
raise HTTPException(status_code=400, detail="Cannot delete primary admin user")
db.delete(db_user)
db.commit()
return {"detail": "User deleted successfully"}
@router.post("/change-password")
async def change_password(
password_change: schemas.PasswordChange,
@@ -177,24 +112,3 @@ async def change_password(
db.commit()
return {"detail": "Password changed successfully"}
class PasswordReset(BaseModel):
new_password: str
@router.post("/users/{user_id}/reset-password")
async def reset_user_password(
user_id: int,
password_reset: PasswordReset,
current_admin: User = Depends(get_current_admin),
db: Session = Depends(get_db)
):
"""사용자 비밀번호 초기화 (관리자 전용)"""
db_user = db.query(User).filter(User.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
# 새 비밀번호로 업데이트
db_user.hashed_password = get_password_hash(password_reset.new_password)
db.commit()
return {"detail": f"Password reset successfully for user {db_user.username}"}

View File

@@ -1,163 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime, date, timezone, timedelta
from database.database import get_db
from database.models import DailyWork, User, UserRole, KST
from database import schemas
from routers.auth import get_current_user
router = APIRouter(prefix="/api/daily-work", tags=["daily-work"])
@router.post("/", response_model=schemas.DailyWork)
async def create_daily_work(
work: schemas.DailyWorkCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 중복 확인 (같은 날짜)
existing = db.query(DailyWork).filter(
DailyWork.date == work.date.date()
).first()
if existing:
raise HTTPException(status_code=400, detail="Daily work for this date already exists")
# 계산
regular_hours = work.worker_count * 8 # 정규 근무 8시간
overtime_total = work.overtime_workers * work.overtime_hours
total_hours = regular_hours + overtime_total
# 생성
db_work = DailyWork(
date=work.date,
worker_count=work.worker_count,
regular_hours=regular_hours,
overtime_workers=work.overtime_workers,
overtime_hours=work.overtime_hours,
overtime_total=overtime_total,
total_hours=total_hours,
created_by_id=current_user.id
)
db.add(db_work)
db.commit()
db.refresh(db_work)
return db_work
@router.get("/", response_model=List[schemas.DailyWork])
async def read_daily_works(
skip: int = 0,
limit: int = 100,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
query = db.query(DailyWork)
if start_date:
query = query.filter(DailyWork.date >= start_date)
if end_date:
query = query.filter(DailyWork.date <= end_date)
works = query.order_by(DailyWork.date.desc()).offset(skip).limit(limit).all()
return works
@router.get("/{work_id}", response_model=schemas.DailyWork)
async def read_daily_work(
work_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
work = db.query(DailyWork).filter(DailyWork.id == work_id).first()
if not work:
raise HTTPException(status_code=404, detail="Daily work not found")
return work
@router.put("/{work_id}", response_model=schemas.DailyWork)
async def update_daily_work(
work_id: int,
work_update: schemas.DailyWorkUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
work = db.query(DailyWork).filter(DailyWork.id == work_id).first()
if not work:
raise HTTPException(status_code=404, detail="Daily work not found")
# 업데이트
update_data = work_update.dict(exclude_unset=True)
# 재계산 필요한 경우
if any(key in update_data for key in ["worker_count", "overtime_workers", "overtime_hours"]):
worker_count = update_data.get("worker_count", work.worker_count)
overtime_workers = update_data.get("overtime_workers", work.overtime_workers)
overtime_hours = update_data.get("overtime_hours", work.overtime_hours)
regular_hours = worker_count * 8
overtime_total = overtime_workers * overtime_hours
total_hours = regular_hours + overtime_total
update_data["regular_hours"] = regular_hours
update_data["overtime_total"] = overtime_total
update_data["total_hours"] = total_hours
for field, value in update_data.items():
setattr(work, field, value)
db.commit()
db.refresh(work)
return work
@router.delete("/{work_id}")
async def delete_daily_work(
work_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
work = db.query(DailyWork).filter(DailyWork.id == work_id).first()
if not work:
raise HTTPException(status_code=404, detail="Daily work not found")
# 권한 확인 (관리자만 삭제 가능)
if current_user.role != UserRole.admin:
raise HTTPException(status_code=403, detail="Only admin can delete daily work")
db.delete(work)
db.commit()
return {"detail": "Daily work deleted successfully"}
@router.get("/stats/summary")
async def get_daily_work_stats(
start_date: Optional[date] = None,
end_date: Optional[date] = None,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""일일 공수 통계"""
query = db.query(DailyWork)
if start_date:
query = query.filter(DailyWork.date >= start_date)
if end_date:
query = query.filter(DailyWork.date <= end_date)
works = query.all()
if not works:
return {
"total_days": 0,
"total_hours": 0,
"total_overtime": 0,
"average_daily_hours": 0
}
total_hours = sum(w.total_hours for w in works)
total_overtime = sum(w.overtime_total for w in works)
return {
"total_days": len(works),
"total_hours": total_hours,
"total_overtime": total_overtime,
"average_daily_hours": total_hours / len(works)
}

View File

@@ -50,11 +50,8 @@ 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},
'reports_daily': {'title': '일일보고서', 'default_access': False},
'users_manage': {'title': '사용자 관리', 'default_access': False}
}

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status
from database.models import User, UserRole
from database.schemas import ProjectCreate, ProjectUpdate
from routers.auth import get_current_user
from utils.tkuser_client import get_token_from_request
import utils.tkuser_client as tkuser_client
@@ -10,30 +9,11 @@ router = APIRouter(
tags=["projects"]
)
def check_admin_permission(current_user: User = Depends(get_current_user)):
"""관리자 권한 확인"""
if current_user.role != UserRole.admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="관리자 권한이 필요합니다."
)
return current_user
@router.options("/")
async def projects_options():
"""OPTIONS preflight 요청 처리"""
return {"message": "OK"}
@router.post("/")
async def create_project(
project: ProjectCreate,
request: Request,
current_user: User = Depends(check_admin_permission)
):
"""프로젝트 생성 (관리자만) - tkuser API로 프록시"""
token = get_token_from_request(request)
return await tkuser_client.create_project(token, project.dict())
@router.get("/")
async def get_projects(
request: Request,
@@ -60,27 +40,3 @@ async def get_project(
detail="프로젝트를 찾을 수 없습니다."
)
return project
@router.put("/{project_id}")
async def update_project(
project_id: int,
project_update: ProjectUpdate,
request: Request,
current_user: User = Depends(check_admin_permission)
):
"""프로젝트 수정 (관리자만) - tkuser API로 프록시"""
token = get_token_from_request(request)
return await tkuser_client.update_project(
token, project_id, project_update.dict(exclude_unset=True)
)
@router.delete("/{project_id}")
async def delete_project(
project_id: int,
request: Request,
current_user: User = Depends(check_admin_permission)
):
"""프로젝트 삭제 (비활성화) (관리자만) - tkuser API로 프록시"""
token = get_token_from_request(request)
await tkuser_client.delete_project(token, project_id)
return {"message": "프로젝트가 삭제되었습니다."}

View File

@@ -14,7 +14,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 routers.page_permissions import check_page_access
@@ -32,14 +32,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,
@@ -118,29 +113,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

@@ -1,913 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>관리자 페이지 - 작업보고서</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 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', -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);
}
.nav-link {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
color: #4b5563;
transition: all 0.2s;
white-space: nowrap;
}
.nav-link:hover {
background-color: #f3f4f6;
color: #1f2937;
}
.nav-link.active {
background-color: #3b82f6;
color: white;
}
.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);
}
/* 부드러운 페이드인 애니메이션 */
.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 Content -->
<main class="container mx-auto px-4 py-8 max-w-6xl content-fade-in" style="padding-top: 80px;">
<div class="grid md:grid-cols-2 gap-6">
<!-- 사용자 추가 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-user-plus text-blue-500 mr-2"></i>사용자 추가
</h2>
<form id="addUserForm" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">사용자 ID</label>
<input
type="text"
id="newUsername"
class="input-field w-full px-3 py-2 rounded-lg"
placeholder="한글 가능 (예: 홍길동)"
required
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">이름</label>
<input
type="text"
id="newFullName"
class="input-field w-full px-3 py-2 rounded-lg"
placeholder="실명 입력"
required
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>
<input
type="password"
id="newPassword"
class="input-field w-full px-3 py-2 rounded-lg"
placeholder="초기 비밀번호"
required
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">부서</label>
<select id="newDepartment" class="input-field w-full px-3 py-2 rounded-lg">
<option value="">부서 선택 (선택사항)</option>
<option value="production">생산</option>
<option value="quality">품질</option>
<option value="purchasing">구매</option>
<option value="design">설계</option>
<option value="sales">영업</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">권한</label>
<select id="newRole" class="input-field w-full px-3 py-2 rounded-lg">
<option value="user">일반 사용자</option>
<option value="admin">관리자</option>
</select>
</div>
<button
type="submit"
class="w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<i class="fas fa-plus mr-2"></i>사용자 추가
</button>
</form>
</div>
<!-- 사용자 목록 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-users text-green-500 mr-2"></i>사용자 목록
</h2>
<div id="userList" class="space-y-3">
<!-- 사용자 목록이 여기에 표시됩니다 -->
<div class="text-gray-500 text-center py-8">
<i class="fas fa-spinner fa-spin text-3xl"></i>
<p>로딩 중...</p>
</div>
</div>
</div>
</div>
<!-- 페이지 권한 관리 섹션 (관리자용) -->
<div id="pagePermissionSection" class="mt-6">
<div class="bg-white rounded-xl shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-shield-alt text-purple-500 mr-2"></i>페이지 접근 권한 관리
</h2>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">사용자 선택</label>
<select id="permissionUserSelect" class="input-field w-full max-w-xs px-3 py-2 rounded-lg">
<option value="">사용자를 선택하세요</option>
</select>
</div>
<div id="pagePermissionGrid" class="hidden">
<h3 class="text-md font-medium text-gray-700 mb-3">페이지별 접근 권한</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 페이지 권한 체크박스들이 여기에 동적으로 생성됩니다 -->
</div>
<div class="mt-4 pt-4 border-t">
<button
id="savePermissionsBtn"
class="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
>
<i class="fas fa-save mr-2"></i>권한 저장
</button>
<span id="permissionSaveStatus" class="ml-3 text-sm"></span>
</div>
</div>
</div>
</div>
<!-- 비밀번호 변경 섹션 (사용자용) -->
<div id="passwordChangeSection" class="hidden mt-6">
<div class="bg-white rounded-xl shadow-sm p-6 max-w-md mx-auto">
<h2 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-key text-yellow-500 mr-2"></i>비밀번호 변경
</h2>
<form id="changePasswordForm" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label>
<input
type="password"
id="currentPassword"
class="input-field w-full px-3 py-2 rounded-lg"
required
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
<input
type="password"
id="newPasswordChange"
class="input-field w-full px-3 py-2 rounded-lg"
required
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
<input
type="password"
id="confirmPassword"
class="input-field w-full px-3 py-2 rounded-lg"
required
>
</div>
<button
type="submit"
class="w-full px-4 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition-colors"
>
<i class="fas fa-save mr-2"></i>비밀번호 변경
</button>
</form>
</div>
</div>
</main>
<!-- 사용자 편집 모달 -->
<div id="editUserModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">사용자 정보 수정</h3>
<button onclick="closeEditModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<form id="editUserForm" class="space-y-4">
<input type="hidden" id="editUserId">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">사용자 ID</label>
<input
type="text"
id="editUsername"
class="input-field w-full px-3 py-2 rounded-lg bg-gray-100"
readonly
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">이름</label>
<input
type="text"
id="editFullName"
class="input-field w-full px-3 py-2 rounded-lg"
placeholder="실명 입력"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">부서</label>
<select id="editDepartment" class="input-field w-full px-3 py-2 rounded-lg">
<option value="">부서 선택 (선택사항)</option>
<option value="production">생산</option>
<option value="quality">품질</option>
<option value="purchasing">구매</option>
<option value="design">설계</option>
<option value="sales">영업</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">권한</label>
<select id="editRole" class="input-field w-full px-3 py-2 rounded-lg">
<option value="user">일반 사용자</option>
<option value="admin">관리자</option>
</select>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
onclick="closeEditModal()"
class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors"
>
취소
</button>
<button
type="submit"
class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<i class="fas fa-save mr-2"></i>저장
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script src="/static/js/date-utils.js?v=20250917"></script>
<script src="/static/js/core/permissions.js?v=20251025"></script>
<script src="/static/js/components/common-header.js?v=20251025"></script>
<script src="/static/js/core/page-manager.js?v=20251025"></script>
<script>
let currentUser = null;
let users = [];
// 애니메이션 함수들
function animateHeaderAppearance() {
console.log('🎨 헤더 애니메이션 시작');
// 헤더 요소 찾기 (공통 헤더가 생성한 요소)
const headerElement = document.querySelector('header') || document.querySelector('[class*="header"]') || document.querySelector('nav');
if (headerElement) {
headerElement.classList.add('header-fade-in');
setTimeout(() => {
headerElement.classList.add('visible');
console.log('✨ 헤더 페이드인 완료');
// 헤더 애니메이션 완료 후 본문 애니메이션
setTimeout(() => {
animateContentAppearance();
}, 200);
}, 50);
} else {
// 헤더를 찾지 못했으면 바로 본문 애니메이션
console.log('⚠️ 헤더 요소를 찾지 못함 - 본문 애니메이션 시작');
animateContentAppearance();
}
}
// 본문 컨텐츠 애니메이션
function animateContentAppearance() {
console.log('🎨 본문 컨텐츠 애니메이션 시작');
// 모든 content-fade-in 요소들을 순차적으로 애니메이션
const contentElements = document.querySelectorAll('.content-fade-in');
contentElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add('visible');
console.log(`✨ 컨텐츠 ${index + 1} 페이드인 완료`);
}, index * 100); // 100ms씩 지연
});
}
// API 로드 후 초기화 함수
async function initializeAdmin() {
const token = TokenManager.getToken();
if (!token) {
window.location.href = '/index.html';
return;
}
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'users_manage');
// 헤더 초기화 후 부드러운 애니메이션 시작
setTimeout(() => {
animateHeaderAppearance();
}, 100);
// 페이지 접근 권한 체크
setTimeout(() => {
if (!canAccessPage('users_manage')) {
alert('사용자 관리 페이지에 접근할 권한이 없습니다.');
window.location.href = '/index.html';
return;
}
}, 500);
} catch (error) {
console.error('인증 실패:', error);
TokenManager.removeToken();
TokenManager.removeUser();
window.location.href = '/index.html';
return;
}
// 관리자가 아니면 비밀번호 변경만 표시
if (currentUser.role !== 'admin') {
document.querySelector('.grid').style.display = 'none';
document.getElementById('passwordChangeSection').classList.remove('hidden');
} else {
// 관리자면 사용자 목록 로드
await loadUsers();
}
}
// 사용자 추가
document.getElementById('addUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const userData = {
username: document.getElementById('newUsername').value.trim(),
full_name: document.getElementById('newFullName').value.trim(),
password: document.getElementById('newPassword').value,
department: document.getElementById('newDepartment').value || null,
role: document.getElementById('newRole').value
};
try {
await AuthAPI.createUser(userData);
// 성공
alert('사용자가 추가되었습니다.');
// 폼 초기화
document.getElementById('addUserForm').reset();
// 목록 새로고침
await loadUsers();
} catch (error) {
alert(error.message || '사용자 추가에 실패했습니다.');
}
});
// 비밀번호 변경
document.getElementById('changePasswordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPasswordChange').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
alert('새 비밀번호가 일치하지 않습니다.');
return;
}
try {
await AuthAPI.changePassword(currentPassword, newPassword);
alert('비밀번호가 변경되었습니다. 다시 로그인해주세요.');
AuthAPI.logout();
} catch (error) {
alert(error.message || '비밀번호 변경에 실패했습니다.');
}
});
// 사용자 목록 로드
async function loadUsers() {
try {
// 백엔드 API에서 사용자 목록 로드
users = await AuthAPI.getUsers();
displayUsers();
} catch (error) {
console.error('사용자 목록 로드 실패:', error);
// API 실패 시 빈 배열로 초기화
users = [];
displayUsers();
}
}
// 사용자 목록 표시
function displayUsers() {
const container = document.getElementById('userList');
if (users.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center">등록된 사용자가 없습니다.</p>';
return;
}
container.innerHTML = users.map(user => `
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex-1">
<div class="font-medium text-gray-800">
<i class="fas fa-user mr-2 text-gray-500"></i>
${user.full_name || user.username}
</div>
<div class="text-sm text-gray-600 flex items-center gap-3">
<span>ID: ${user.username}</span>
${user.department ? `
<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-700">
<i class="fas fa-building mr-1"></i>${AuthAPI.getDepartmentLabel(user.department)}
</span>
` : ''}
<span class="px-2 py-0.5 rounded text-xs ${
user.role === 'admin'
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700'
}">
${user.role === 'admin' ? '관리자' : '사용자'}
</span>
</div>
</div>
<div class="flex gap-2">
<button
onclick="editUser(${user.id})"
class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm"
>
<i class="fas fa-edit mr-1"></i>편집
</button>
<button
onclick="resetPassword('${user.username}')"
class="px-3 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 transition-colors text-sm"
>
<i class="fas fa-key mr-1"></i>비밀번호 초기화
</button>
${user.username !== 'hyungi' ? `
<button
onclick="deleteUser('${user.username}')"
class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm"
>
<i class="fas fa-trash mr-1"></i>삭제
</button>
` : ''}
</div>
</div>
`).join('');
}
// 비밀번호 초기화
async function resetPassword(username) {
if (!confirm(`${username} 사용자의 비밀번호를 "000000"으로 초기화하시겠습니까?`)) {
return;
}
try {
// 사용자 ID 찾기
const user = users.find(u => u.username === username);
if (!user) {
alert('사용자를 찾을 수 없습니다.');
return;
}
// 백엔드 API로 비밀번호 초기화
await AuthAPI.resetPassword(user.id, '000000');
alert(`${username} 사용자의 비밀번호가 "000000"으로 초기화되었습니다.`);
// 목록 새로고침
await loadUsers();
} catch (error) {
alert('비밀번호 초기화에 실패했습니다: ' + error.message);
}
}
// 사용자 삭제
async function deleteUser(username) {
if (!confirm(`정말 ${username} 사용자를 삭제하시겠습니까?`)) {
return;
}
try {
await AuthAPI.deleteUser(username);
alert('사용자가 삭제되었습니다.');
await loadUsers();
} catch (error) {
alert(error.message || '삭제에 실패했습니다.');
}
}
// 사용자 편집 모달 열기
function editUser(userId) {
const user = users.find(u => u.id === userId);
if (!user) {
alert('사용자를 찾을 수 없습니다.');
return;
}
// 모달 필드에 현재 값 설정
document.getElementById('editUserId').value = user.id;
document.getElementById('editUsername').value = user.username;
document.getElementById('editFullName').value = user.full_name || '';
document.getElementById('editDepartment').value = user.department || '';
document.getElementById('editRole').value = user.role;
// 모달 표시
document.getElementById('editUserModal').classList.remove('hidden');
}
// 사용자 편집 모달 닫기
function closeEditModal() {
document.getElementById('editUserModal').classList.add('hidden');
}
// 사용자 편집 폼 제출
document.getElementById('editUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const userId = document.getElementById('editUserId').value;
const userData = {
full_name: document.getElementById('editFullName').value.trim() || null,
department: document.getElementById('editDepartment').value || null,
role: document.getElementById('editRole').value
};
try {
await AuthAPI.updateUser(userId, userData);
alert('사용자 정보가 수정되었습니다.');
closeEditModal();
await loadUsers(); // 목록 새로고침
} catch (error) {
alert(error.message || '사용자 정보 수정에 실패했습니다.');
}
});
// 페이지 권한 관리 기능
let selectedUserId = null;
let currentPermissions = {};
// AuthAPI를 사용하여 사용자 목록 로드
async function loadUsers() {
try {
users = await AuthAPI.getUsers();
displayUsers();
updatePermissionUserSelect(); // 권한 관리 드롭다운 업데이트
} catch (error) {
console.error('사용자 로드 실패:', error);
document.getElementById('userList').innerHTML = `
<div class="text-red-500 text-center py-8">
<i class="fas fa-exclamation-triangle text-3xl"></i>
<p>사용자 목록을 불러올 수 없습니다.</p>
<p class="text-sm mt-2">오류: ${error.message}</p>
</div>
`;
}
}
// 권한 관리 사용자 선택 드롭다운 업데이트
function updatePermissionUserSelect() {
const select = document.getElementById('permissionUserSelect');
select.innerHTML = '<option value="">사용자를 선택하세요</option>';
// 일반 사용자만 표시 (admin 제외)
const regularUsers = users.filter(user => user.role === 'user');
regularUsers.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = `${user.full_name || user.username} (${user.username})`;
select.appendChild(option);
});
}
// 사용자 선택 시 페이지 권한 그리드 표시
document.getElementById('permissionUserSelect').addEventListener('change', async (e) => {
selectedUserId = e.target.value;
if (selectedUserId) {
await loadUserPagePermissions(selectedUserId);
showPagePermissionGrid();
} else {
hidePagePermissionGrid();
}
});
// 사용자의 페이지 권한 로드
async function loadUserPagePermissions(userId) {
try {
// 기본 페이지 목록 가져오기
const defaultPages = {
'issues_create': { title: '부적합 등록', defaultAccess: true },
'issues_view': { title: '부적합 조회', defaultAccess: true },
'issues_manage': { title: '부적합 관리', defaultAccess: true },
'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 }
};
// 기본값으로 초기화
currentPermissions = {};
Object.keys(defaultPages).forEach(pageName => {
currentPermissions[pageName] = defaultPages[pageName].defaultAccess;
});
// 실제 API 호출로 사용자별 설정된 권한 가져오기
try {
const response = await fetch(`/api/users/${userId}/page-permissions`, {
headers: {
'Authorization': `Bearer ${TokenManager.getToken()}`
}
});
if (response.ok) {
const permissions = await response.json();
permissions.forEach(perm => {
currentPermissions[perm.page_name] = perm.can_access;
});
console.log('사용자 권한 로드 완료:', currentPermissions);
} else {
console.warn('사용자 권한 로드 실패, 기본값 사용');
}
} catch (apiError) {
console.warn('API 호출 실패, 기본값 사용:', apiError);
}
} catch (error) {
console.error('페이지 권한 로드 실패:', error);
}
}
// 페이지 권한 그리드 표시
function showPagePermissionGrid() {
const grid = document.getElementById('pagePermissionGrid');
const gridContainer = grid.querySelector('.grid');
// 페이지 권한 체크박스 생성 (카테고리별로 그룹화)
const pageCategories = {
'부적합 관리': {
'issues_create': { title: '부적합 등록', icon: 'fas fa-plus-circle', color: 'text-green-600' },
'issues_view': { title: '부적합 조회', icon: 'fas fa-search', color: 'text-purple-600' },
'issues_manage': { title: '목록 관리 (통합)', icon: 'fas fa-tasks', color: 'text-orange-600' }
},
'목록 관리 세부': {
'issues_inbox': { title: '📥 수신함', icon: 'fas fa-inbox', color: 'text-blue-600' },
'issues_management': { title: '⚙️ 관리함', icon: 'fas fa-cog', color: 'text-green-600' },
'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' }
}
};
let html = '';
// 카테고리별로 그룹화하여 표시
Object.entries(pageCategories).forEach(([categoryName, pages]) => {
html += `
<div class="col-span-full">
<h4 class="text-sm font-semibold text-gray-800 mb-3 pb-2 border-b border-gray-200">
${categoryName}
</h4>
</div>
`;
Object.entries(pages).forEach(([pageName, pageInfo]) => {
const isChecked = currentPermissions[pageName] || false;
const isDefault = currentPermissions[pageName] === undefined ?
(pageInfo.title.includes('등록') || pageInfo.title.includes('조회') || pageInfo.title.includes('수신함')) : false;
html += `
<div class="flex items-center p-3 border rounded-lg hover:bg-gray-50 transition-colors ${isChecked ? 'border-blue-300 bg-blue-50' : 'border-gray-200'}">
<input
type="checkbox"
id="perm_${pageName}"
${isChecked ? 'checked' : ''}
class="mr-3 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
onchange="this.parentElement.classList.toggle('border-blue-300', this.checked); this.parentElement.classList.toggle('bg-blue-50', this.checked);"
>
<label for="perm_${pageName}" class="flex-1 cursor-pointer">
<div class="flex items-center">
<i class="${pageInfo.icon} ${pageInfo.color} mr-2"></i>
<span class="text-sm font-medium text-gray-700">${pageInfo.title}</span>
${isDefault ? '<span class="ml-2 text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">기본</span>' : ''}
</div>
</label>
</div>
`;
});
});
gridContainer.innerHTML = html;
grid.classList.remove('hidden');
}
// 페이지 권한 그리드 숨기기
function hidePagePermissionGrid() {
document.getElementById('pagePermissionGrid').classList.add('hidden');
}
// 권한 저장
document.getElementById('savePermissionsBtn').addEventListener('click', async () => {
if (!selectedUserId) return;
const saveBtn = document.getElementById('savePermissionsBtn');
const statusSpan = document.getElementById('permissionSaveStatus');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>저장 중...';
statusSpan.textContent = '';
try {
// 체크박스 상태 수집 (모든 페이지 포함)
const allPages = [
'issues_create', 'issues_view', 'issues_manage',
'issues_inbox', 'issues_management', 'issues_archive',
'projects_manage', 'daily_work', 'reports', 'users_manage'
];
const permissions = {};
allPages.forEach(pageName => {
const checkbox = document.getElementById(`perm_${pageName}`);
if (checkbox) {
permissions[pageName] = checkbox.checked;
}
});
// 실제 API 호출로 권한 저장
const permissionArray = Object.entries(permissions).map(([pageName, canAccess]) => ({
page_name: pageName,
can_access: canAccess
}));
const response = await fetch('/api/page-permissions/bulk-grant', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TokenManager.getToken()}`
},
body: JSON.stringify({
user_id: parseInt(selectedUserId),
permissions: permissionArray
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || '권한 저장에 실패했습니다.');
}
const result = await response.json();
console.log('권한 저장 완료:', result);
statusSpan.textContent = '✅ 권한이 저장되었습니다.';
statusSpan.className = 'ml-3 text-sm text-green-600';
setTimeout(() => {
statusSpan.textContent = '';
}, 3000);
} catch (error) {
console.error('권한 저장 실패:', error);
statusSpan.textContent = '❌ 권한 저장에 실패했습니다.';
statusSpan.className = 'ml-3 text-sm text-red-600';
} finally {
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fas fa-save mr-2"></i>권한 저장';
}
});
// API 스크립트 동적 로딩
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
const script = document.createElement('script');
script.src = `/static/js/api.js?v=20251025-2&cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
script.setAttribute('cache-control', 'no-cache');
script.setAttribute('pragma', 'no-cache');
script.onload = function() {
console.log('✅ API 스크립트 로드 완료 (admin.html)');
// API 로드 후 초기화 시작
initializeAdmin();
};
script.onerror = function() {
console.error('❌ API 스크립트 로드 실패');
};
document.head.appendChild(script);
</script>
</body>
</html>

View File

@@ -1,120 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>프로젝트 데이터 확인</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
pre {
background: #f4f4f4;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
}
.info {
background: #e3f2fd;
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
button {
background: #3b82f6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
margin: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>프로젝트 데이터 확인</h1>
<div class="info">
<p><strong>화면 크기:</strong> <span id="screenSize"></span></p>
<p><strong>User Agent:</strong> <span id="userAgent"></span></p>
<p><strong>현재 시간:</strong> <span id="currentTime"></span></p>
</div>
<h2>localStorage 데이터</h2>
<pre id="localStorageData"></pre>
<h2>프로젝트 목록</h2>
<div id="projectList"></div>
<h2>액션</h2>
<button onclick="createDefaultProjects()">기본 프로젝트 생성</button>
<button onclick="clearProjects()">프로젝트 초기화</button>
<button onclick="location.reload()">새로고침</button>
<button onclick="location.href='index.html'">메인으로</button>
<script>
// 화면 정보 표시
document.getElementById('screenSize').textContent = `${window.innerWidth} x ${window.innerHeight}`;
document.getElementById('userAgent').textContent = navigator.userAgent;
document.getElementById('currentTime').textContent = new Date().toLocaleString('ko-KR');
// localStorage 데이터 표시
function showLocalStorageData() {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
data[key] = JSON.parse(localStorage.getItem(key));
} catch (e) {
data[key] = localStorage.getItem(key);
}
}
document.getElementById('localStorageData').textContent = JSON.stringify(data, null, 2);
}
// 프로젝트 목록 표시
function showProjects() {
const saved = localStorage.getItem('work-report-projects');
const projectListDiv = document.getElementById('projectList');
if (saved) {
try {
const projects = JSON.parse(saved);
let html = `<p>총 ${projects.length}개의 프로젝트</p><ul>`;
projects.forEach(p => {
html += `<li>${p.jobNo} - ${p.projectName} (${p.isActive ? '활성' : '비활성'})</li>`;
});
html += '</ul>';
projectListDiv.innerHTML = html;
} catch (e) {
projectListDiv.innerHTML = '<p style="color: red;">프로젝트 데이터 파싱 에러: ' + e.message + '</p>';
}
} else {
projectListDiv.innerHTML = '<p style="color: orange;">프로젝트 데이터가 없습니다.</p>';
}
}
// 기본 프로젝트 생성
function createDefaultProjects() {
alert('프로젝트 관리 페이지에서 프로젝트를 생성하세요.');
location.href = 'project-management.html';
}
// 프로젝트 초기화
function clearProjects() {
if (confirm('정말로 모든 프로젝트를 삭제하시겠습니까?')) {
localStorage.removeItem('work-report-projects');
alert('프로젝트가 초기화되었습니다.');
location.reload();
}
}
// 초기 로드
showLocalStorageData();
showProjects();
</script>
</body>
</html>

View File

@@ -1,661 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<style>
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--success: #10b981;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--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);
color: white;
}
.nav-link {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
color: #4b5563;
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">
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-6 max-w-2xl content-fade-in" style="padding-top: 80px;">
<!-- 입력 카드 -->
<div class="work-card p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-6">
<i class="fas fa-edit text-blue-500 mr-2"></i>공수 입력
</h2>
<form id="dailyWorkForm" class="space-y-6">
<!-- 날짜 선택 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-calendar mr-1"></i>날짜
</label>
<input
type="date"
id="workDate"
class="input-field w-full px-4 py-3 rounded-lg text-lg"
required
onchange="loadExistingData()"
>
</div>
<!-- 프로젝트별 시간 입력 섹션 -->
<div>
<div class="flex items-center justify-between mb-4">
<label class="text-sm font-medium text-gray-700">
<i class="fas fa-folder-open mr-1"></i>프로젝트별 작업 시간
</label>
<button
type="button"
id="addProjectBtn"
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
onclick="addProjectEntry()"
>
<i class="fas fa-plus mr-2"></i>프로젝트 추가
</button>
</div>
<div id="projectEntries" class="space-y-3">
<!-- 프로젝트 입력 항목들이 여기에 추가됩니다 -->
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-blue-900">총 작업 시간:</span>
<span id="totalHours" class="text-lg font-bold text-blue-600">0시간</span>
</div>
</div>
</div>
<!-- 총 공수 표시 -->
<div class="bg-blue-50 rounded-lg p-4">
<div class="flex justify-between items-center">
<span class="text-gray-700 font-medium">예상 총 공수</span>
<span class="text-2xl font-bold text-blue-600" id="totalHours">0시간</span>
</div>
</div>
<!-- 저장 버튼 -->
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-medium text-lg">
<i class="fas fa-save mr-2"></i>저장하기
</button>
</form>
</div>
<!-- 최근 입력 내역 -->
<div class="work-card p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-history text-gray-500 mr-2"></i>최근 입력 내역
</h3>
<div id="recentEntries" class="space-y-3">
<!-- 최근 입력 내역이 여기에 표시됩니다 -->
</div>
</div>
</main>
</div>
<script>
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
const script = document.createElement('script');
script.src = `/static/js/api.js?cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
script.setAttribute('cache-control', 'no-cache');
script.setAttribute('pragma', 'no-cache');
script.onload = function() {
console.log('✅ API 스크립트 로드 완료');
initializeDailyWork();
};
document.head.appendChild(script);
</script>
<script src="/static/js/date-utils.js?v=20250917"></script>
<script src="/static/js/core/permissions.js?v=20251025"></script>
<script src="/static/js/components/common-header.js?v=20251025"></script>
<script src="/static/js/core/page-manager.js?v=20251025"></script>
<script>
let currentUser = null;
let projects = [];
let dailyWorkData = [];
let projectEntryCounter = 0;
// 애니메이션 함수들
function animateHeaderAppearance() {
console.log('🎨 헤더 애니메이션 시작');
// 헤더 요소 찾기 (공통 헤더가 생성한 요소)
const headerElement = document.querySelector('header') || document.querySelector('[class*="header"]') || document.querySelector('nav');
if (headerElement) {
headerElement.classList.add('header-fade-in');
setTimeout(() => {
headerElement.classList.add('visible');
console.log('✨ 헤더 페이드인 완료');
// 헤더 애니메이션 완료 후 본문 애니메이션
setTimeout(() => {
animateContentAppearance();
}, 200);
}, 50);
} else {
// 헤더를 찾지 못했으면 바로 본문 애니메이션
console.log('⚠️ 헤더 요소를 찾지 못함 - 본문 애니메이션 시작');
animateContentAppearance();
}
}
// 본문 컨텐츠 애니메이션
function animateContentAppearance() {
console.log('🎨 본문 컨텐츠 애니메이션 시작');
// 모든 content-fade-in 요소들을 순차적으로 애니메이션
const contentElements = document.querySelectorAll('.content-fade-in');
contentElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add('visible');
console.log(`✨ 컨텐츠 ${index + 1} 페이드인 완료`);
}, index * 100); // 100ms씩 지연
});
}
// API 로드 후 초기화 함수
async function initializeDailyWork() {
const token = TokenManager.getToken();
if (!token) {
window.location.href = '/index.html';
return;
}
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'daily_work');
// 헤더 초기화 후 부드러운 애니메이션 시작
setTimeout(() => {
animateHeaderAppearance();
}, 100);
// 페이지 접근 권한 체크 (일일 공수 페이지)
setTimeout(() => {
if (!canAccessPage('daily_work')) {
alert('일일 공수 페이지에 접근할 권한이 없습니다.');
window.location.href = '/index.html';
return;
}
}, 500);
} catch (error) {
console.error('인증 실패:', error);
TokenManager.removeToken();
TokenManager.removeUser();
window.location.href = '/index.html';
return;
}
// 사용자 정보는 공통 헤더에서 표시됨
// 네비게이션은 공통 헤더에서 처리됨
// 프로젝트 및 일일 공수 데이터 로드
await loadProjects();
loadDailyWorkData();
// 오늘 날짜로 초기화
document.getElementById('workDate').valueAsDate = new Date();
// 첫 번째 프로젝트 입력 항목 추가
addProjectEntry();
// 최근 내역 로드
await loadRecentEntries();
}
// DOM 로드 완료 시 대기 (API 스크립트가 로드되면 initializeDailyWork 호출됨)
window.addEventListener('DOMContentLoaded', () => {
console.log('📄 DOM 로드 완료 - API 스크립트 로딩 대기 중...');
});
// 네비게이션은 공통 헤더에서 처리됨
// 프로젝트 데이터 로드
async function loadProjects() {
try {
// API에서 최신 프로젝트 데이터 가져오기
const apiUrl = window.API_BASE_URL || '/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${TokenManager.getToken()}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
projects = await response.json();
console.log('프로젝트 로드 완료:', projects.length, '개');
console.log('활성 프로젝트:', projects.filter(p => p.is_active).length, '개');
// localStorage에도 캐시 저장
localStorage.setItem('work-report-projects', JSON.stringify(projects));
} else {
console.error('프로젝트 로드 실패:', response.status);
// 실패 시 localStorage에서 로드
const saved = localStorage.getItem('work-report-projects');
if (saved) {
projects = JSON.parse(saved);
console.log('캐시에서 프로젝트 로드:', projects.length, '개');
}
}
} catch (error) {
console.error('프로젝트 로드 오류:', error);
// 오류 시 localStorage에서 로드
const saved = localStorage.getItem('work-report-projects');
if (saved) {
projects = JSON.parse(saved);
console.log('캐시에서 프로젝트 로드:', projects.length, '개');
}
}
}
// 일일 공수 데이터 로드
function loadDailyWorkData() {
const saved = localStorage.getItem('daily-work-data');
if (saved) {
dailyWorkData = JSON.parse(saved);
}
}
// 일일 공수 데이터 저장
function saveDailyWorkData() {
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
}
// 활성 프로젝트만 필터링
function getActiveProjects() {
return projects.filter(p => p.is_active);
}
// 프로젝트 입력 항목 추가
function addProjectEntry() {
const activeProjects = getActiveProjects();
if (activeProjects.length === 0) {
alert('활성 프로젝트가 없습니다. 먼저 프로젝트를 생성해주세요.');
return;
}
projectEntryCounter++;
const entryId = `project-entry-${projectEntryCounter}`;
const entryHtml = `
<div id="${entryId}" class="flex gap-3 items-center p-4 bg-gray-50 rounded-lg">
<div class="flex-1">
<select class="input-field w-full px-3 py-2 rounded-lg" onchange="updateTotalHours()">
<option value="">프로젝트 선택</option>
${activeProjects.map(p => `<option value="${p.id}">${p.jobNo} - ${p.projectName}</option>`).join('')}
</select>
</div>
<div class="w-32">
<input
type="number"
placeholder="시간"
min="0"
step="0.5"
class="input-field w-full px-3 py-2 rounded-lg text-center"
onchange="updateTotalHours()"
>
</div>
<div class="w-16 text-center text-gray-600">시간</div>
<button
type="button"
onclick="removeProjectEntry('${entryId}')"
class="text-red-500 hover:text-red-700 p-2"
title="제거"
>
<i class="fas fa-times"></i>
</button>
</div>
`;
document.getElementById('projectEntries').insertAdjacentHTML('beforeend', entryHtml);
updateTotalHours();
}
// 프로젝트 입력 항목 제거
function removeProjectEntry(entryId) {
const entry = document.getElementById(entryId);
if (entry) {
entry.remove();
updateTotalHours();
}
}
// 총 시간 계산 및 업데이트
function updateTotalHours() {
const entries = document.querySelectorAll('#projectEntries > div');
let totalHours = 0;
entries.forEach(entry => {
const hoursInput = entry.querySelector('input[type="number"]');
const hours = parseFloat(hoursInput.value) || 0;
totalHours += hours;
});
document.getElementById('totalHours').textContent = `${totalHours}시간`;
}
// 기존 데이터 로드 (날짜 선택 시)
function loadExistingData() {
const selectedDate = document.getElementById('workDate').value;
if (!selectedDate) return;
const existingData = dailyWorkData.find(d => d.date === selectedDate);
if (existingData) {
// 기존 프로젝트 입력 항목들 제거
document.getElementById('projectEntries').innerHTML = '';
projectEntryCounter = 0;
// 기존 데이터로 프로젝트 입력 항목들 생성
existingData.projects.forEach(projectData => {
addProjectEntry();
const lastEntry = document.querySelector('#projectEntries > div:last-child');
const select = lastEntry.querySelector('select');
const input = lastEntry.querySelector('input[type="number"]');
select.value = projectData.projectId;
input.value = projectData.hours;
});
updateTotalHours();
} else {
// 새로운 날짜인 경우 초기화
document.getElementById('projectEntries').innerHTML = '';
projectEntryCounter = 0;
addProjectEntry();
}
}
// 폼 제출
document.getElementById('dailyWorkForm').addEventListener('submit', async (e) => {
e.preventDefault();
const selectedDate = document.getElementById('workDate').value;
const entries = document.querySelectorAll('#projectEntries > div');
const projectData = [];
let hasValidEntry = false;
entries.forEach(entry => {
const select = entry.querySelector('select');
const input = entry.querySelector('input[type="number"]');
const projectId = select.value;
const hours = parseFloat(input.value) || 0;
if (projectId && hours > 0) {
const project = projects.find(p => p.id == projectId);
projectData.push({
projectId: projectId,
projectName: project ? `${project.jobNo} - ${project.projectName}` : '알 수 없음',
hours: hours
});
hasValidEntry = true;
}
});
if (!hasValidEntry) {
alert('최소 하나의 프로젝트에 시간을 입력해주세요.');
return;
}
try {
// 기존 데이터 업데이트 또는 새로 추가
const existingIndex = dailyWorkData.findIndex(d => d.date === selectedDate);
const newData = {
date: selectedDate,
projects: projectData,
totalHours: projectData.reduce((sum, p) => sum + p.hours, 0),
createdAt: new Date().toISOString(),
createdBy: currentUser.username || currentUser
};
if (existingIndex >= 0) {
dailyWorkData[existingIndex] = newData;
} else {
dailyWorkData.push(newData);
}
saveDailyWorkData();
// 성공 메시지
showSuccessMessage();
// 최근 내역 갱신
await loadRecentEntries();
} catch (error) {
alert(error.message || '저장에 실패했습니다.');
}
});
// 최근 데이터 로드
async function loadRecentEntries() {
try {
// 최근 7일 데이터 표시
const recentData = dailyWorkData
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, 7);
displayRecentEntries(recentData);
} catch (error) {
console.error('데이터 로드 실패:', error);
document.getElementById('recentEntries').innerHTML =
'<p class="text-gray-500 text-center py-4">데이터를 불러올 수 없습니다.</p>';
}
}
// 성공 메시지
function showSuccessMessage() {
const button = document.querySelector('button[type="submit"]');
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check-circle mr-2"></i>저장 완료!';
button.classList.remove('btn-primary');
button.classList.add('btn-success');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-primary');
}, 2000);
}
// 최근 입력 내역 표시
function displayRecentEntries(entries) {
const container = document.getElementById('recentEntries');
if (!entries || entries.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-4">최근 입력 내역이 없습니다.</p>';
return;
}
container.innerHTML = entries.map(item => {
const date = new Date(item.date);
const dateStr = `${date.getMonth() + 1}/${date.getDate()} (${['일','월','화','수','목','금','토'][date.getDay()]})`;
return `
<div class="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex justify-between items-start mb-2">
<p class="font-medium text-gray-800">${dateStr}</p>
<div class="text-right">
<p class="text-lg font-bold text-blue-600">${item.totalHours}시간</p>
${currentUser && (currentUser.role === 'admin' || currentUser.username === 'hyungi') ? `
<button
onclick="deleteDailyWork('${item.date}')"
class="mt-1 px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-xs"
>
<i class="fas fa-trash mr-1"></i>삭제
</button>
` : ''}
</div>
</div>
<div class="space-y-1">
${item.projects.map(p => `
<div class="flex justify-between text-sm text-gray-600">
<span>${p.projectName}</span>
<span>${p.hours}시간</span>
</div>
`).join('')}
</div>
</div>
`;
}).join('');
}
// 일일 공수 삭제 (관리자만)
async function deleteDailyWork(date) {
if (!currentUser || (currentUser.role !== 'admin' && currentUser.username !== 'hyungi')) {
alert('관리자만 삭제할 수 있습니다.');
return;
}
if (!confirm('정말로 이 일일 공수 기록을 삭제하시겠습니까?')) {
return;
}
try {
const index = dailyWorkData.findIndex(d => d.date === date);
if (index >= 0) {
dailyWorkData.splice(index, 1);
saveDailyWorkData();
await loadRecentEntries();
alert('삭제되었습니다.');
}
} catch (error) {
alert('삭제에 실패했습니다.');
}
}
// 로그아웃
function logout() {
TokenManager.removeToken();
TokenManager.removeUser();
window.location.href = '/index.html';
}
</script>
</body>
</html>

View File

@@ -1,323 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>모바일 프로젝트 문제 해결</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 20px;
margin: 0;
background: #f5f5f5;
}
.container {
max-width: 100%;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
font-size: 24px;
margin-bottom: 20px;
}
.section {
margin-bottom: 30px;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
font-size: 14px;
}
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.info { background: #d1ecf1; color: #0c5460; }
.warning { background: #fff3cd; color: #856404; }
button {
background: #3b82f6;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
margin: 5px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
button:active {
background: #2563eb;
}
select {
width: 100%;
padding: 12px;
font-size: 16px;
border: 2px solid #3b82f6;
border-radius: 8px;
background: white;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 20px;
}
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
font-size: 12px;
}
.test-select {
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🔧 모바일 프로젝트 문제 해결</h1>
<div class="section">
<h2>📱 디바이스 정보</h2>
<div id="deviceInfo"></div>
</div>
<div class="section">
<h2>💾 localStorage 상태</h2>
<div id="storageStatus"></div>
<button onclick="checkStorage()">localStorage 확인</button>
<button onclick="fixProjects()">프로젝트 복구</button>
</div>
<div class="section">
<h2>🧪 드롭다운 테스트</h2>
<div class="test-select">
<label>테스트 드롭다운:</label>
<select id="testSelect">
<option value="">선택하세요</option>
</select>
</div>
<button onclick="testDropdown()">드롭다운 테스트</button>
</div>
<div class="section">
<h2>📊 실제 프로젝트 드롭다운</h2>
<div class="test-select">
<label>프로젝트 선택:</label>
<select id="projectSelect">
<option value="">프로젝트를 선택하세요</option>
</select>
</div>
<button onclick="loadProjects()">프로젝트 로드</button>
</div>
<div class="section">
<h2>🔍 디버그 로그</h2>
<pre id="debugLog"></pre>
<button onclick="clearLog()">로그 지우기</button>
</div>
<div style="margin-top: 30px;">
<button onclick="location.href='index.html'">메인으로</button>
<button onclick="location.reload()">새로고침</button>
</div>
</div>
<script>
let logContent = '';
function log(message) {
const time = new Date().toLocaleTimeString('ko-KR');
logContent += `[${time}] ${message}\n`;
document.getElementById('debugLog').textContent = logContent;
}
function clearLog() {
logContent = '';
document.getElementById('debugLog').textContent = '';
}
// 디바이스 정보
function showDeviceInfo() {
const info = `
<div class="status info">
<strong>화면 크기:</strong> ${window.innerWidth} x ${window.innerHeight}<br>
<strong>User Agent:</strong> ${navigator.userAgent}<br>
<strong>플랫폼:</strong> ${navigator.platform}<br>
<strong>모바일 여부:</strong> ${window.innerWidth <= 768 ? '예' : '아니오'}
</div>
`;
document.getElementById('deviceInfo').innerHTML = info;
log('디바이스 정보 표시 완료');
}
// localStorage 확인
function checkStorage() {
log('localStorage 확인 시작');
const statusDiv = document.getElementById('storageStatus');
try {
// 프로젝트 데이터 확인
const projectData = localStorage.getItem('work-report-projects');
if (projectData) {
const projects = JSON.parse(projectData);
statusDiv.innerHTML = `
<div class="status success">
✅ 프로젝트 데이터 있음: ${projects.length}
</div>
<pre>${JSON.stringify(projects, null, 2)}</pre>
`;
log(`프로젝트 ${projects.length}개 발견`);
} else {
statusDiv.innerHTML = `
<div class="status warning">
⚠️ 프로젝트 데이터 없음
</div>
`;
log('프로젝트 데이터 없음');
}
// 사용자 데이터 확인
const userData = localStorage.getItem('currentUser');
if (userData) {
const user = JSON.parse(userData);
statusDiv.innerHTML += `
<div class="status success">
✅ 사용자: ${user.username} (${user.role})
</div>
`;
log(`사용자: ${user.username}`);
}
} catch (error) {
statusDiv.innerHTML = `
<div class="status error">
❌ 에러: ${error.message}
</div>
`;
log(`에러: ${error.message}`);
}
}
// 프로젝트 복구
function fixProjects() {
log('프로젝트 복구 시작');
const projects = [
{
id: 1,
jobNo: 'TKR-25009R',
projectName: 'M Project',
isActive: true,
createdAt: new Date().toISOString(),
createdByName: '관리자'
},
{
id: 2,
jobNo: 'TKG-24011P',
projectName: 'TKG Project',
isActive: true,
createdAt: new Date().toISOString(),
createdByName: '관리자'
}
];
try {
localStorage.setItem('work-report-projects', JSON.stringify(projects));
log('프로젝트 데이터 저장 완료');
alert('프로젝트가 복구되었습니다!');
checkStorage();
loadProjects();
} catch (error) {
log(`복구 실패: ${error.message}`);
alert('복구 실패: ' + error.message);
}
}
// 드롭다운 테스트
function testDropdown() {
log('드롭다운 테스트 시작');
const select = document.getElementById('testSelect');
// 옵션 추가
select.innerHTML = '<option value="">선택하세요</option>';
for (let i = 1; i <= 5; i++) {
const option = document.createElement('option');
option.value = i;
option.textContent = `테스트 옵션 ${i}`;
select.appendChild(option);
}
log(`테스트 옵션 ${select.options.length - 1}개 추가됨`);
// 이벤트 리스너
select.onchange = function() {
log(`선택됨: ${this.value} - ${this.options[this.selectedIndex].text}`);
};
}
// 프로젝트 로드
function loadProjects() {
log('프로젝트 로드 시작');
const select = document.getElementById('projectSelect');
try {
const saved = localStorage.getItem('work-report-projects');
if (!saved) {
log('localStorage에 프로젝트 없음');
alert('프로젝트 데이터가 없습니다. "프로젝트 복구" 버튼을 눌러주세요.');
return;
}
const projects = JSON.parse(saved);
const activeProjects = projects.filter(p => p.isActive);
log(`활성 프로젝트 ${activeProjects.length}개 발견`);
// 드롭다운 초기화
select.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
// 프로젝트 옵션 추가
activeProjects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.jobNo} - ${project.projectName}`;
select.appendChild(option);
log(`옵션 추가: ${project.jobNo} - ${project.projectName}`);
});
log(`드롭다운에 ${select.options.length - 1}개 프로젝트 표시됨`);
// 이벤트 리스너
select.onchange = function() {
log(`프로젝트 선택됨: ${this.value}`);
};
} catch (error) {
log(`프로젝트 로드 에러: ${error.message}`);
alert('프로젝트 로드 실패: ' + error.message);
}
}
// 페이지 로드 시 실행
window.onload = function() {
log('페이지 로드 완료');
showDeviceInfo();
checkStorage();
testDropdown();
loadProjects();
};
</script>
</body>
</html>

View File

@@ -1,592 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<style>
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--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;">
<!-- 프로젝트 생성 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-plus text-green-500 mr-2"></i>새 프로젝트 생성
</h2>
<form id="projectForm" class="grid md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Job No.</label>
<input
type="text"
id="jobNo"
class="input-field w-full px-4 py-2 rounded-lg"
placeholder="예: JOB-2024-001"
required
maxlength="50"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">프로젝트 이름</label>
<input
type="text"
id="projectName"
class="input-field w-full px-4 py-2 rounded-lg"
placeholder="프로젝트 이름을 입력하세요"
required
maxlength="200"
>
</div>
<div class="md:col-span-2">
<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
<i class="fas fa-plus mr-2"></i>프로젝트 생성
</button>
</div>
</form>
</div>
<!-- 프로젝트 목록 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800">프로젝트 목록</h2>
<button onclick="loadProjects()" class="text-blue-600 hover:text-blue-800">
<i class="fas fa-refresh mr-1"></i>새로고침
</button>
</div>
<div id="projectsList" class="space-y-3">
<!-- 프로젝트 목록이 여기에 표시됩니다 -->
</div>
</div>
<!-- 비활성화된 프로젝트 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6 mt-6">
<div class="flex justify-between items-center mb-4 cursor-pointer" onclick="toggleInactiveProjects()">
<div class="flex items-center space-x-2">
<i id="inactiveToggleIcon" class="fas fa-chevron-down transition-transform duration-200"></i>
<h2 class="text-lg font-semibold text-gray-600">비활성화된 프로젝트</h2>
<span id="inactiveProjectCount" class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-sm font-medium">0</span>
</div>
<div class="text-sm text-gray-500">
<i class="fas fa-info-circle mr-1"></i>클릭하여 펼치기/접기
</div>
</div>
<div id="inactiveProjectsList" class="space-y-3">
<!-- 비활성화된 프로젝트 목록이 여기에 표시됩니다 -->
</div>
</div>
</main>
<!-- API 스크립트 먼저 로드 (최강 캐시 무력화) -->
<script>
// 브라우저 캐시 완전 무력화
const timestamp = new Date().getTime();
const random1 = Math.random() * 1000000;
const random2 = Math.floor(Math.random() * 1000000);
const cacheBuster = `${timestamp}-${random1}-${random2}`;
const script = document.createElement('script');
script.src = `/static/js/api.js?force-reload=${cacheBuster}&no-cache=${timestamp}&bust=${random2}`;
script.onload = function() {
console.log('✅ API 스크립트 로드 완료');
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
console.log('🌐 현재 hostname:', window.location.hostname);
console.log('🔗 현재 protocol:', window.location.protocol);
// API 로드 후 인증 체크 시작
setTimeout(checkAdminAccess, 100);
};
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
script.setAttribute('pragma', 'no-cache');
script.setAttribute('expires', '0');
document.head.appendChild(script);
console.log('🚀 캐시 버스터:', cacheBuster);
</script>
<!-- 메인 스크립트 -->
<script>
// 관리자 권한 확인 함수
async function checkAdminAccess() {
try {
const currentUser = await AuthAPI.getCurrentUser();
if (!currentUser || currentUser.role !== 'admin') {
alert('관리자만 접근할 수 있습니다.');
window.location.href = '/index.html';
return;
}
// 권한 확인 후 페이지 초기화
initializeProjectManagement();
} catch (error) {
console.error('권한 확인 실패:', error);
alert('로그인이 필요합니다.');
window.location.href = '/index.html';
}
}
// 프로젝트 관리 페이지 초기화 함수
async function initializeProjectManagement() {
try {
console.log('🚀 프로젝트 관리 페이지 초기화 시작');
// 헤더 애니메이션 시작
animateHeaderAppearance();
// 프로젝트 목록 로드
await loadProjects();
console.log('✅ 프로젝트 관리 페이지 초기화 완료');
} catch (error) {
console.error('❌ 프로젝트 관리 페이지 초기화 실패:', error);
alert('페이지 로드 중 오류가 발생했습니다.');
}
}
</script>
<script src="/static/js/core/permissions.js?v=20251025"></script>
<script src="/static/js/components/common-header.js?v=20251025"></script>
<script src="/static/js/core/page-manager.js?v=20251025"></script>
<script>
// 전역 변수
let currentUser = null;
// 애니메이션 함수들
function animateHeaderAppearance() {
console.log('🎨 헤더 애니메이션 시작');
// 헤더 요소 찾기 (공통 헤더가 생성한 요소)
const headerElement = document.querySelector('header') || document.querySelector('[class*="header"]') || document.querySelector('nav');
if (headerElement) {
headerElement.classList.add('header-fade-in');
setTimeout(() => {
headerElement.classList.add('visible');
console.log('✨ 헤더 페이드인 완료');
// 헤더 애니메이션 완료 후 본문 애니메이션
setTimeout(() => {
animateContentAppearance();
}, 200);
}, 50);
} else {
// 헤더를 찾지 못했으면 바로 본문 애니메이션
console.log('⚠️ 헤더 요소를 찾지 못함 - 본문 애니메이션 시작');
animateContentAppearance();
}
}
// 본문 컨텐츠 애니메이션
function animateContentAppearance() {
console.log('🎨 본문 컨텐츠 애니메이션 시작');
// 모든 content-fade-in 요소들을 순차적으로 애니메이션
const contentElements = document.querySelectorAll('.content-fade-in');
contentElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add('visible');
console.log(`✨ 컨텐츠 ${index + 1} 페이드인 완료`);
}, index * 100); // 100ms씩 지연
});
}
async function initAuth() {
console.log('인증 초기화 시작');
const token = TokenManager.getToken();
console.log('토큰 존재:', !!token);
if (!token) {
console.log('토큰 없음 - 로그인 페이지로 이동');
alert('로그인이 필요합니다.');
window.location.href = 'index.html';
return false;
}
try {
console.log('API로 사용자 정보 가져오는 중...');
const user = await AuthAPI.getCurrentUser();
console.log('사용자 정보:', user);
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
return true;
} catch (error) {
console.error('인증 실패:', error);
TokenManager.removeToken();
TokenManager.removeUser();
alert('로그인이 필요합니다.');
window.location.href = 'index.html';
return false;
}
}
async function checkAdminAccess() {
const authSuccess = await initAuth();
if (!authSuccess) return;
// 공통 헤더 초기화
await window.commonHeader.init(currentUser, 'projects_manage');
// 헤더 초기화 후 부드러운 애니메이션 시작
setTimeout(() => {
animateHeaderAppearance();
}, 100);
// 페이지 접근 권한 체크 (프로젝트 관리 페이지)
setTimeout(() => {
if (!canAccessPage('projects_manage')) {
alert('프로젝트 관리 페이지에 접근할 권한이 없습니다.');
window.location.href = 'index.html';
return;
}
}, 500);
// 사용자 정보는 공통 헤더에서 표시됨
// 프로젝트 로드
loadProjects();
}
let projects = [];
// 프로젝트 데이터 로드 (API 기반)
async function loadProjects() {
console.log('프로젝트 로드 시작 (API)');
try {
// API에서 모든 프로젝트 로드 (활성/비활성 모두)
const apiProjects = await ProjectsAPI.getAll(false);
// API 데이터를 그대로 사용 (필드명 통일)
projects = apiProjects;
console.log('API에서 프로젝트 로드:', projects.length, '개');
} catch (error) {
console.error('API 로드 실패:', error);
projects = [];
}
displayProjectList();
}
// 프로젝트 데이터 저장 (더 이상 사용하지 않음 - API 기반)
// function saveProjects() {
// localStorage.setItem('work-report-projects', JSON.stringify(projects));
// }
// 프로젝트 생성 폼 처리
document.getElementById('projectForm').addEventListener('submit', async (e) => {
e.preventDefault();
const jobNo = document.getElementById('jobNo').value.trim();
const projectName = document.getElementById('projectName').value.trim();
// 중복 Job No. 확인
if (projects.some(p => p.job_no === jobNo)) {
alert('이미 존재하는 Job No.입니다.');
return;
}
try {
// API를 통한 프로젝트 생성
const newProject = await ProjectsAPI.create({
job_no: jobNo,
project_name: projectName
});
// 성공 메시지
alert('프로젝트가 생성되었습니다.');
// 폼 초기화
document.getElementById('projectForm').reset();
// 목록 새로고침
await loadProjects();
displayProjectList();
} catch (error) {
console.error('프로젝트 생성 실패:', error);
alert('프로젝트 생성에 실패했습니다: ' + error.message);
}
});
// 프로젝트 목록 표시
function displayProjectList() {
const activeContainer = document.getElementById('projectsList');
const inactiveContainer = document.getElementById('inactiveProjectsList');
const inactiveCount = document.getElementById('inactiveProjectCount');
activeContainer.innerHTML = '';
inactiveContainer.innerHTML = '';
if (projects.length === 0) {
activeContainer.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 프로젝트가 없습니다.</p>';
inactiveCount.textContent = '0';
return;
}
// 활성 프로젝트와 비활성 프로젝트 분리
const activeProjects = projects.filter(p => p.is_active);
const inactiveProjects = projects.filter(p => !p.is_active);
console.log('전체 프로젝트:', projects.length, '개');
console.log('활성 프로젝트:', activeProjects.length, '개');
console.log('비활성 프로젝트:', inactiveProjects.length, '개');
// 비활성 프로젝트 개수 업데이트
inactiveCount.textContent = inactiveProjects.length;
// 활성 프로젝트 표시
if (activeProjects.length > 0) {
activeProjects.forEach(project => {
const div = document.createElement('div');
div.className = 'border border-green-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-green-50';
div.innerHTML = `
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="font-semibold text-gray-800">${project.job_no}</h3>
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">활성</span>
</div>
<p class="text-gray-600 mb-2">${project.project_name}</p>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
</div>
</div>
<div class="flex gap-2">
<button onclick="editProject(${project.id})" class="text-blue-600 hover:text-blue-800 p-2" title="수정">
<i class="fas fa-edit"></i>
</button>
<button onclick="toggleProjectStatus(${project.id})" class="text-orange-600 hover:text-orange-800 p-2" title="비활성화">
<i class="fas fa-pause"></i>
</button>
<button onclick="deleteProject(${project.id})" class="text-red-600 hover:text-red-800 p-2" title="삭제">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
activeContainer.appendChild(div);
});
} else {
activeContainer.innerHTML = '<p class="text-gray-500 text-center py-8">활성 프로젝트가 없습니다.</p>';
}
// 비활성 프로젝트 표시
if (inactiveProjects.length > 0) {
inactiveProjects.forEach(project => {
const div = document.createElement('div');
div.className = 'border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-gray-50';
div.innerHTML = `
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="font-semibold text-gray-600">${project.job_no}</h3>
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">비활성</span>
</div>
<p class="text-gray-500 mb-2">${project.project_name}</p>
<div class="flex items-center gap-4 text-sm text-gray-400">
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
</div>
</div>
<div class="flex gap-2">
<button onclick="toggleProjectStatus(${project.id})" class="text-green-600 hover:text-green-800 p-2" title="활성화">
<i class="fas fa-play"></i>
</button>
<button onclick="deleteProject(${project.id})" class="text-red-600 hover:text-red-800 p-2" title="완전 삭제">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
inactiveContainer.appendChild(div);
});
} else {
inactiveContainer.innerHTML = '<p class="text-gray-500 text-center py-8">비활성화된 프로젝트가 없습니다.</p>';
}
}
// 비활성 프로젝트 섹션 토글
function toggleInactiveProjects() {
const inactiveList = document.getElementById('inactiveProjectsList');
const toggleIcon = document.getElementById('inactiveToggleIcon');
if (inactiveList.style.display === 'none') {
// 펼치기
inactiveList.style.display = 'block';
toggleIcon.classList.remove('fa-chevron-right');
toggleIcon.classList.add('fa-chevron-down');
} else {
// 접기
inactiveList.style.display = 'none';
toggleIcon.classList.remove('fa-chevron-down');
toggleIcon.classList.add('fa-chevron-right');
}
}
// 프로젝트 편집
async function editProject(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const newName = prompt('프로젝트 이름을 수정하세요:', project.project_name);
if (newName && newName.trim() && newName.trim() !== project.project_name) {
try {
// API를 통한 프로젝트 업데이트
await ProjectsAPI.update(projectId, {
project_name: newName.trim()
});
// 목록 새로고침
await loadProjects();
displayProjectList();
alert('프로젝트가 수정되었습니다.');
} catch (error) {
console.error('프로젝트 수정 실패:', error);
alert('프로젝트 수정에 실패했습니다: ' + error.message);
}
}
}
// 프로젝트 활성/비활성 토글
async function toggleProjectStatus(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const action = project.is_active ? '비활성화' : '활성화';
if (confirm(`"${project.job_no}" 프로젝트를 ${action}하시겠습니까?`)) {
try {
// API를 통한 프로젝트 상태 업데이트
await ProjectsAPI.update(projectId, {
is_active: !project.is_active
});
// 목록 새로고침
await loadProjects();
displayProjectList();
alert(`프로젝트가 ${action}되었습니다.`);
} catch (error) {
console.error('프로젝트 상태 변경 실패:', error);
alert('프로젝트 상태 변경에 실패했습니다: ' + error.message);
}
}
}
// 프로젝트 삭제 (완전 삭제)
async function deleteProject(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const confirmMessage = project.is_active
? `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
: `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
if (confirm(confirmMessage)) {
try {
// API를 통한 프로젝트 삭제
await ProjectsAPI.delete(projectId);
// 목록 새로고침
await loadProjects();
displayProjectList();
alert('프로젝트가 완전히 삭제되었습니다.');
} catch (error) {
console.error('프로젝트 삭제 실패:', error);
alert('프로젝트 삭제에 실패했습니다: ' + error.message);
}
}
}
// DOMContentLoaded 이벤트 제거 - API 스크립트 로드 후 checkAdminAccess() 호출됨
</script>
</body>
</html>

View File

@@ -182,25 +182,10 @@ const AuthAPI = {
getCurrentUser: () => apiRequest('/auth/me'),
createUser: (userData) => apiRequest('/auth/users', {
method: 'POST',
body: JSON.stringify(userData)
}),
getUsers: () => {
console.log('🔍 AuthAPI.getUsers 호출 - 엔드포인트: /auth/users');
return apiRequest('/auth/users');
},
updateUser: (userId, userData) => apiRequest(`/auth/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(userData)
}),
deleteUser: (userId) => apiRequest(`/auth/users/${userId}`, {
method: 'DELETE'
}),
changePassword: (currentPassword, newPassword) => apiRequest('/auth/change-password', {
method: 'POST',
body: JSON.stringify({
@@ -209,13 +194,6 @@ const AuthAPI = {
})
}),
resetPassword: (userId, newPassword = '000000') => apiRequest(`/auth/users/${userId}/reset-password`, {
method: 'POST',
body: JSON.stringify({
new_password: newPassword
})
}),
// 부서 목록 가져오기
getDepartments: () => [
{ value: 'production', label: '생산' },
@@ -273,35 +251,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', {
@@ -318,14 +267,6 @@ const ReportsAPI = {
end_date: endDate
}).toString();
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}`);
}
};
@@ -373,20 +314,6 @@ const ProjectsAPI = {
const params = `?active_only=${activeOnly}`;
return apiRequest(`/projects/${params}`);
},
get: (id) => apiRequest(`/projects/${id}`),
create: (projectData) => apiRequest('/projects/', {
method: 'POST',
body: JSON.stringify(projectData)
}),
update: (id, projectData) => apiRequest(`/projects/${id}`, {
method: 'PUT',
body: JSON.stringify(projectData)
}),
delete: (id) => apiRequest(`/projects/${id}`, {
method: 'DELETE'
})
get: (id) => apiRequest(`/projects/${id}`)
};

View File

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

View File

@@ -10,17 +10,6 @@ class CommonHeader {
this.menuItems = this.initMenuItems();
}
/**
* 사용자 관리 URL (tkuser 서브도메인 또는 로컬 포트)
*/
_getUserManageUrl() {
const hostname = window.location.hostname;
if (hostname.includes('technicalkorea.net')) {
return window.location.protocol + '//tkuser.technicalkorea.net';
}
return window.location.protocol + '//' + hostname + ':30380';
}
/**
* 메뉴 아이템 정의
*/
@@ -62,15 +51,6 @@ class CommonHeader {
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100'
},
{
id: 'daily_work',
title: '일일 공수',
icon: 'fas fa-calendar-check',
url: '/daily-work.html',
pageName: 'daily_work',
color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100'
},
{
id: 'reports',
title: '보고서',
@@ -106,25 +86,6 @@ class CommonHeader {
}
]
},
{
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
}
];
}

View File

@@ -28,20 +28,14 @@ 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(), '새 항목 생성');
this.register('s', () => this.triggerSaveAction(), '저장');
this.register('r', () => this.triggerRefreshAction(), '새로고침');
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,33 +164,12 @@ 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: '보고서',
icon: 'fas fa-chart-bar',
path: '#reports',
pageName: 'reports'
},
{
id: 'users_manage',
title: '사용자 관리',
icon: 'fas fa-users-cog',
path: '#users/manage',
pageName: 'users_manage'
}
];

View File

@@ -1,104 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>프로젝트 DB → localStorage 동기화</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.status {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
}
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.info { background: #d1ecf1; color: #0c5460; }
pre { background: #f4f4f4; padding: 10px; overflow-x: auto; }
</style>
</head>
<body>
<h1>프로젝트 DB → localStorage 동기화</h1>
<div id="status"></div>
<pre id="result"></pre>
<button onclick="syncProjects()">동기화 시작</button>
<button onclick="location.href='index.html'">메인으로</button>
<script>
// DB의 프로젝트 데이터 (backend에서 확인한 데이터)
const dbProjects = [
{
id: 1,
jobNo: 'TKR-25009R',
projectName: 'M Project',
isActive: true,
createdAt: '2025-10-24T09:49:42.456272+09:00',
createdByName: '관리자'
},
{
id: 2,
jobNo: 'TKG-24011P',
projectName: 'TKG Project',
isActive: true,
createdAt: '2025-10-24T10:59:49.71909+09:00',
createdByName: '관리자'
}
];
function showStatus(message, type = 'info') {
const statusDiv = document.getElementById('status');
statusDiv.className = 'status ' + type;
statusDiv.textContent = message;
}
function syncProjects() {
try {
// 기존 localStorage 데이터 확인
const existing = localStorage.getItem('work-report-projects');
if (existing) {
showStatus('기존 localStorage 데이터가 있습니다. 덮어쓰시겠습니까?', 'info');
if (!confirm('기존 프로젝트 데이터를 DB 데이터로 덮어쓰시겠습니까?')) {
showStatus('동기화 취소됨', 'error');
return;
}
}
// localStorage에 저장
localStorage.setItem('work-report-projects', JSON.stringify(dbProjects));
// 결과 표시
document.getElementById('result').textContent = JSON.stringify(dbProjects, null, 2);
showStatus('✅ DB 프로젝트 2개를 localStorage로 동기화 완료!', 'success');
// 2초 후 메인으로 이동
setTimeout(() => {
alert('동기화가 완료되었습니다. 메인 페이지로 이동합니다.');
location.href = 'index.html';
}, 2000);
} catch (error) {
showStatus('❌ 동기화 실패: ' + error.message, 'error');
}
}
// 페이지 로드 시 현재 상태 표시
window.onload = () => {
const current = localStorage.getItem('work-report-projects');
if (current) {
showStatus('현재 localStorage에 프로젝트 데이터가 있습니다.', 'info');
document.getElementById('result').textContent = 'Current: ' + current;
} else {
showStatus('localStorage에 프로젝트 데이터가 없습니다.', 'info');
}
};
</script>
</body>
</html>

View File

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>API 테스트</title>
</head>
<body>
<h1>API 테스트 페이지</h1>
<button onclick="testLogin()">로그인 테스트</button>
<button onclick="testUsers()">사용자 목록 테스트</button>
<div id="result"></div>
<script>
let token = null;
async function testLogin() {
try {
const formData = new URLSearchParams();
formData.append('username', 'hyungi');
formData.append('password', '123456');
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString()
});
const data = await response.json();
token = data.access_token;
document.getElementById('result').innerHTML = `<pre>로그인 성공: ${JSON.stringify(data, null, 2)}</pre>`;
} catch (error) {
document.getElementById('result').innerHTML = `<pre>로그인 실패: ${error.message}</pre>`;
}
}
async function testUsers() {
if (!token) {
alert('먼저 로그인하세요');
return;
}
try {
const response = await fetch('/api/auth/users', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
document.getElementById('result').innerHTML = `<pre>사용자 목록: ${JSON.stringify(data, null, 2)}</pre>`;
} catch (error) {
document.getElementById('result').innerHTML = `<pre>사용자 목록 실패: ${error.message}</pre>`;
}
}
</script>
</body>
</html>

View File

@@ -46,9 +46,6 @@ const DEFAULT_PAGES = {
'issues_inbox': { title: '수신함', system: 'system3', group: '메인', default_access: true },
'issues_management': { title: '관리함', system: 'system3', group: '메인', default_access: false },
'issues_archive': { title: '폐기함', system: 'system3', group: '메인', default_access: false },
// 업무
'daily_work': { title: '일일 공수', system: 'system3', group: '업무', default_access: false },
'projects_manage': { title: '프로젝트 관리', system: 'system3', group: '업무', default_access: false },
// 보고서
'reports': { title: '보고서', system: 'system3', group: '보고서', default_access: false },
'reports_daily': { title: '일일보고서', system: 'system3', group: '보고서', default_access: false },