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

View File

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

View File

@@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List
from pydantic import BaseModel
from database.database import get_db from database.database import get_db
from database.models import User, UserRole 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() users = db.query(User).filter(User.is_active == True).all()
return users 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") @router.post("/change-password")
async def change_password( async def change_password(
password_change: schemas.PasswordChange, password_change: schemas.PasswordChange,
@@ -177,24 +112,3 @@ async def change_password(
db.commit() db.commit()
return {"detail": "Password changed successfully"} 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_management': {'title': '관리함', 'default_access': False},
'issues_archive': {'title': '폐기함', 'default_access': False}, 'issues_archive': {'title': '폐기함', 'default_access': False},
'issues_dashboard': {'title': '현황판', 'default_access': True}, 'issues_dashboard': {'title': '현황판', 'default_access': True},
'projects_manage': {'title': '프로젝트 관리', 'default_access': False},
'daily_work': {'title': '일일 공수', 'default_access': False},
'reports': {'title': '보고서', 'default_access': False}, 'reports': {'title': '보고서', 'default_access': False},
'reports_daily': {'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 fastapi import APIRouter, Depends, HTTPException, Request, status
from database.models import User, UserRole from database.models import User, UserRole
from database.schemas import ProjectCreate, ProjectUpdate
from routers.auth import get_current_user from routers.auth import get_current_user
from utils.tkuser_client import get_token_from_request from utils.tkuser_client import get_token_from_request
import utils.tkuser_client as tkuser_client import utils.tkuser_client as tkuser_client
@@ -10,30 +9,11 @@ router = APIRouter(
tags=["projects"] 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("/") @router.options("/")
async def projects_options(): async def projects_options():
"""OPTIONS preflight 요청 처리""" """OPTIONS preflight 요청 처리"""
return {"message": "OK"} 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("/") @router.get("/")
async def get_projects( async def get_projects(
request: Request, request: Request,
@@ -60,27 +40,3 @@ async def get_project(
detail="프로젝트를 찾을 수 없습니다." detail="프로젝트를 찾을 수 없습니다."
) )
return project 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 import os
from database.database import get_db from database.database import get_db
from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole, ReviewStatus from database.models import Issue, IssueStatus, IssueCategory, User, UserRole, ReviewStatus
from database import schemas from database import schemas
from routers.auth import get_current_user from routers.auth import get_current_user
from routers.page_permissions import check_page_access from routers.page_permissions import check_page_access
@@ -32,14 +32,9 @@ async def generate_report_summary(
"""보고서 요약 생성""" """보고서 요약 생성"""
start_date = report_request.start_date start_date = report_request.start_date
end_date = report_request.end_date end_date = report_request.end_date
# 일일 공수 합계 total_hours = 0
daily_works = db.query(DailyWork).filter(
DailyWork.date >= start_date.date(),
DailyWork.date <= end_date.date()
).all()
total_hours = sum(w.total_hours for w in daily_works)
# 이슈 통계 # 이슈 통계
issues_query = db.query(Issue).filter( issues_query = db.query(Issue).filter(
Issue.report_date >= start_date, Issue.report_date >= start_date,
@@ -118,29 +113,6 @@ async def get_report_issues(
"detail_notes": issue.detail_notes "detail_notes": issue.detail_notes
} for issue in issues] } for issue in issues]
@router.get("/daily-works")
async def get_report_daily_works(
start_date: datetime,
end_date: datetime,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""보고서용 일일 공수 목록"""
works = db.query(DailyWork).filter(
DailyWork.date >= start_date.date(),
DailyWork.date <= end_date.date()
).order_by(DailyWork.date).all()
return [{
"date": work.date,
"worker_count": work.worker_count,
"regular_hours": work.regular_hours,
"overtime_workers": work.overtime_workers,
"overtime_hours": work.overtime_hours,
"overtime_total": work.overtime_total,
"total_hours": work.total_hours
} for work in works]
@router.get("/daily-preview") @router.get("/daily-preview")
async def preview_daily_report( async def preview_daily_report(
project_id: int, project_id: int,

View File

@@ -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'), getCurrentUser: () => apiRequest('/auth/me'),
createUser: (userData) => apiRequest('/auth/users', {
method: 'POST',
body: JSON.stringify(userData)
}),
getUsers: () => { getUsers: () => {
console.log('🔍 AuthAPI.getUsers 호출 - 엔드포인트: /auth/users');
return apiRequest('/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', { changePassword: (currentPassword, newPassword) => apiRequest('/auth/change-password', {
method: 'POST', method: 'POST',
body: JSON.stringify({ 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: () => [ getDepartments: () => [
{ value: 'production', label: '생산' }, { value: 'production', label: '생산' },
@@ -273,35 +251,6 @@ const IssuesAPI = {
getStats: () => apiRequest('/issues/stats/summary') getStats: () => apiRequest('/issues/stats/summary')
}; };
// Daily Work API
const DailyWorkAPI = {
create: (workData) => apiRequest('/daily-work/', {
method: 'POST',
body: JSON.stringify(workData)
}),
getAll: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
return apiRequest(`/daily-work/${queryString ? '?' + queryString : ''}`);
},
get: (id) => apiRequest(`/daily-work/${id}`),
update: (id, workData) => apiRequest(`/daily-work/${id}`, {
method: 'PUT',
body: JSON.stringify(workData)
}),
delete: (id) => apiRequest(`/daily-work/${id}`, {
method: 'DELETE'
}),
getStats: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
return apiRequest(`/daily-work/stats/summary${queryString ? '?' + queryString : ''}`);
}
};
// Reports API // Reports API
const ReportsAPI = { const ReportsAPI = {
getSummary: (startDate, endDate) => apiRequest('/reports/summary', { getSummary: (startDate, endDate) => apiRequest('/reports/summary', {
@@ -318,14 +267,6 @@ const ReportsAPI = {
end_date: endDate end_date: endDate
}).toString(); }).toString();
return apiRequest(`/reports/issues?${params}`); return apiRequest(`/reports/issues?${params}`);
},
getDailyWorks: (startDate, endDate) => {
const params = new URLSearchParams({
start_date: startDate,
end_date: endDate
}).toString();
return apiRequest(`/reports/daily-works?${params}`);
} }
}; };
@@ -373,20 +314,6 @@ const ProjectsAPI = {
const params = `?active_only=${activeOnly}`; const params = `?active_only=${activeOnly}`;
return apiRequest(`/projects/${params}`); return apiRequest(`/projects/${params}`);
}, },
get: (id) => apiRequest(`/projects/${id}`), 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'
})
}; };

View File

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

View File

@@ -10,17 +10,6 @@ class CommonHeader {
this.menuItems = this.initMenuItems(); 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', color: 'text-slate-600',
bgColor: 'text-slate-600 hover:bg-slate-100' 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', id: 'reports',
title: '보고서', 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 h', () => this.navigateToPage('/index.html', 'issues_create'), '홈 (부적합 등록)');
this.register('g v', () => this.navigateToPage('/issue-view.html', 'issues_view'), '부적합 조회'); this.register('g v', () => this.navigateToPage('/issue-view.html', 'issues_view'), '부적합 조회');
this.register('g d', () => this.navigateToPage('/daily-work.html', 'daily_work'), '일일 공수');
this.register('g p', () => this.navigateToPage('/project-management.html', 'projects_manage'), '프로젝트 관리');
this.register('g r', () => this.navigateToPage('/reports.html', 'reports'), '보고서'); this.register('g r', () => this.navigateToPage('/reports.html', 'reports'), '보고서');
this.register('g a', () => this.navigateToPage('/admin.html', 'users_manage'), '관리자');
// 액션 단축키 // 액션 단축키
this.register('n', () => this.triggerNewAction(), '새 항목 생성'); this.register('n', () => this.triggerNewAction(), '새 항목 생성');
this.register('s', () => this.triggerSaveAction(), '저장'); this.register('s', () => this.triggerSaveAction(), '저장');
this.register('r', () => this.triggerRefreshAction(), '새로고침'); this.register('r', () => this.triggerRefreshAction(), '새로고침');
this.register('f', () => this.focusSearchField(), '검색 포커스'); this.register('f', () => this.focusSearchField(), '검색 포커스');
// 관리자 전용 단축키
this.register('ctrl+shift+u', () => this.navigateToPage('/admin.html', 'users_manage'), '사용자 관리 (관리자)');
console.log('⌨️ 키보드 단축키 등록 완료'); console.log('⌨️ 키보드 단축키 등록 완료');
} }

View File

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

View File

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

View File

@@ -20,10 +20,7 @@ class PagePermissionManager {
'issues_inbox': { title: '수신함', defaultAccess: true }, 'issues_inbox': { title: '수신함', defaultAccess: true },
'issues_management': { title: '관리함', defaultAccess: false }, 'issues_management': { title: '관리함', defaultAccess: false },
'issues_archive': { title: '폐기함', defaultAccess: false }, 'issues_archive': { title: '폐기함', defaultAccess: false },
'projects_manage': { title: '프로젝트 관리', defaultAccess: false }, 'reports': { title: '보고서', defaultAccess: false }
'daily_work': { title: '일일 공수', defaultAccess: false },
'reports': { title: '보고서', defaultAccess: false },
'users_manage': { title: '사용자 관리', defaultAccess: false }
}; };
} }
@@ -167,33 +164,12 @@ class PagePermissionManager {
path: '#issues/manage', path: '#issues/manage',
pageName: 'issues_manage' pageName: 'issues_manage'
}, },
{
id: 'projects_manage',
title: '프로젝트 관리',
icon: 'fas fa-folder-open',
path: '#projects/manage',
pageName: 'projects_manage'
},
{
id: 'daily_work',
title: '일일 공수',
icon: 'fas fa-calendar-check',
path: '#daily-work',
pageName: 'daily_work'
},
{ {
id: 'reports', id: 'reports',
title: '보고서', title: '보고서',
icon: 'fas fa-chart-bar', icon: 'fas fa-chart-bar',
path: '#reports', path: '#reports',
pageName: '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_inbox': { title: '수신함', system: 'system3', group: '메인', default_access: true },
'issues_management': { title: '관리함', system: 'system3', group: '메인', default_access: false }, 'issues_management': { title: '관리함', system: 'system3', group: '메인', default_access: false },
'issues_archive': { 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': { title: '보고서', system: 'system3', group: '보고서', default_access: false },
'reports_daily': { title: '일일보고서', system: 'system3', group: '보고서', default_access: false }, 'reports_daily': { title: '일일보고서', system: 'system3', group: '보고서', default_access: false },