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:
@@ -68,7 +68,6 @@ class User(Base):
|
||||
# Relationships
|
||||
issues = relationship("Issue", back_populates="reporter", foreign_keys="Issue.reporter_id")
|
||||
reviewed_issues = relationship("Issue", foreign_keys="Issue.reviewed_by_id")
|
||||
daily_works = relationship("DailyWork", back_populates="created_by")
|
||||
page_permissions = relationship("UserPagePermission", back_populates="user", foreign_keys="UserPagePermission.user_id")
|
||||
|
||||
class UserPagePermission(Base):
|
||||
@@ -184,37 +183,6 @@ class Project(Base):
|
||||
primaryjoin="Project.id == Issue.project_id",
|
||||
foreign_keys="[Issue.project_id]")
|
||||
|
||||
class DailyWork(Base):
|
||||
__tablename__ = "qc_daily_works"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
date = Column(DateTime, nullable=False, index=True)
|
||||
worker_count = Column(Integer, nullable=False)
|
||||
regular_hours = Column(Float, nullable=False)
|
||||
overtime_workers = Column(Integer, default=0)
|
||||
overtime_hours = Column(Float, default=0)
|
||||
overtime_total = Column(Float, default=0)
|
||||
total_hours = Column(Float, nullable=False)
|
||||
created_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
|
||||
created_at = Column(DateTime, default=get_kst_now)
|
||||
|
||||
# Relationships
|
||||
created_by = relationship("User", back_populates="daily_works")
|
||||
|
||||
class ProjectDailyWork(Base):
|
||||
__tablename__ = "qc_project_daily_works"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
date = Column(DateTime, nullable=False, index=True)
|
||||
project_id = Column(Integer, ForeignKey("projects.project_id"), nullable=False)
|
||||
hours = Column(Float, nullable=False)
|
||||
created_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
|
||||
created_at = Column(DateTime, default=get_kst_now)
|
||||
|
||||
# Relationships
|
||||
project = relationship("Project")
|
||||
created_by = relationship("User")
|
||||
|
||||
class DeletionLog(Base):
|
||||
__tablename__ = "qc_deletion_logs"
|
||||
|
||||
|
||||
@@ -49,16 +49,6 @@ class UserBase(BaseModel):
|
||||
role: UserRole = UserRole.user
|
||||
department: Optional[DepartmentType] = None
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
full_name: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
role: Optional[UserRole] = None
|
||||
department: Optional[DepartmentType] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class PasswordChange(BaseModel):
|
||||
current_password: str
|
||||
new_password: str
|
||||
@@ -286,13 +276,6 @@ class ProjectBase(BaseModel):
|
||||
job_no: str = Field(..., min_length=1, max_length=50)
|
||||
project_name: str = Field(..., min_length=1, max_length=200)
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
pass
|
||||
|
||||
class ProjectUpdate(BaseModel):
|
||||
project_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class Project(ProjectBase):
|
||||
id: int
|
||||
created_by_id: Optional[int] = None
|
||||
@@ -303,50 +286,6 @@ class Project(ProjectBase):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# DailyWork schemas
|
||||
class DailyWorkBase(BaseModel):
|
||||
date: datetime
|
||||
worker_count: int = Field(gt=0)
|
||||
overtime_workers: Optional[int] = 0
|
||||
overtime_hours: Optional[float] = 0
|
||||
|
||||
class DailyWorkCreate(DailyWorkBase):
|
||||
pass
|
||||
|
||||
class DailyWorkUpdate(BaseModel):
|
||||
worker_count: Optional[int] = Field(None, gt=0)
|
||||
overtime_workers: Optional[int] = None
|
||||
overtime_hours: Optional[float] = None
|
||||
|
||||
class DailyWork(DailyWorkBase):
|
||||
id: int
|
||||
regular_hours: float
|
||||
overtime_total: float
|
||||
total_hours: float
|
||||
created_by_id: int
|
||||
created_by: User
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ProjectDailyWorkBase(BaseModel):
|
||||
date: datetime
|
||||
project_id: int
|
||||
hours: float
|
||||
|
||||
class ProjectDailyWorkCreate(ProjectDailyWorkBase):
|
||||
pass
|
||||
|
||||
class ProjectDailyWork(ProjectDailyWorkBase):
|
||||
id: int
|
||||
created_by_id: int
|
||||
created_at: datetime
|
||||
project: Project
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Report schemas
|
||||
class ReportRequest(BaseModel):
|
||||
start_date: datetime
|
||||
|
||||
@@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from database.database import get_db
|
||||
from database.models import User, UserRole
|
||||
@@ -95,70 +94,6 @@ async def get_all_users(
|
||||
users = db.query(User).filter(User.is_active == True).all()
|
||||
return users
|
||||
|
||||
@router.post("/users", response_model=schemas.User)
|
||||
async def create_user(
|
||||
user: schemas.UserCreate,
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# 중복 확인
|
||||
db_user = db.query(User).filter(User.username == user.username).first()
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Username already registered")
|
||||
|
||||
# 사용자 생성
|
||||
db_user = User(
|
||||
username=user.username,
|
||||
hashed_password=get_password_hash(user.password),
|
||||
full_name=user.full_name,
|
||||
role=user.role
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
@router.put("/users/{user_id}", response_model=schemas.User)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_update: schemas.UserUpdate,
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
db_user = db.query(User).filter(User.id == user_id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# 업데이트
|
||||
update_data = user_update.dict(exclude_unset=True)
|
||||
if "password" in update_data:
|
||||
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(db_user, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
@router.delete("/users/{username}")
|
||||
async def delete_user(
|
||||
username: str,
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
db_user = db.query(User).filter(User.username == username).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# hyungi 계정은 삭제 불가
|
||||
if db_user.username == "hyungi":
|
||||
raise HTTPException(status_code=400, detail="Cannot delete primary admin user")
|
||||
|
||||
db.delete(db_user)
|
||||
db.commit()
|
||||
return {"detail": "User deleted successfully"}
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
password_change: schemas.PasswordChange,
|
||||
@@ -177,24 +112,3 @@ async def change_password(
|
||||
db.commit()
|
||||
|
||||
return {"detail": "Password changed successfully"}
|
||||
|
||||
class PasswordReset(BaseModel):
|
||||
new_password: str
|
||||
|
||||
@router.post("/users/{user_id}/reset-password")
|
||||
async def reset_user_password(
|
||||
user_id: int,
|
||||
password_reset: PasswordReset,
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""사용자 비밀번호 초기화 (관리자 전용)"""
|
||||
db_user = db.query(User).filter(User.id == user_id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# 새 비밀번호로 업데이트
|
||||
db_user.hashed_password = get_password_hash(password_reset.new_password)
|
||||
db.commit()
|
||||
|
||||
return {"detail": f"Password reset successfully for user {db_user.username}"}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -50,11 +50,8 @@ DEFAULT_PAGES = {
|
||||
'issues_management': {'title': '관리함', 'default_access': False},
|
||||
'issues_archive': {'title': '폐기함', 'default_access': False},
|
||||
'issues_dashboard': {'title': '현황판', 'default_access': True},
|
||||
'projects_manage': {'title': '프로젝트 관리', 'default_access': False},
|
||||
'daily_work': {'title': '일일 공수', 'default_access': False},
|
||||
'reports': {'title': '보고서', 'default_access': False},
|
||||
'reports_daily': {'title': '일일보고서', 'default_access': False},
|
||||
'users_manage': {'title': '사용자 관리', 'default_access': False}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from database.models import User, UserRole
|
||||
from database.schemas import ProjectCreate, ProjectUpdate
|
||||
from routers.auth import get_current_user
|
||||
from utils.tkuser_client import get_token_from_request
|
||||
import utils.tkuser_client as tkuser_client
|
||||
@@ -10,30 +9,11 @@ router = APIRouter(
|
||||
tags=["projects"]
|
||||
)
|
||||
|
||||
def check_admin_permission(current_user: User = Depends(get_current_user)):
|
||||
"""관리자 권한 확인"""
|
||||
if current_user.role != UserRole.admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="관리자 권한이 필요합니다."
|
||||
)
|
||||
return current_user
|
||||
|
||||
@router.options("/")
|
||||
async def projects_options():
|
||||
"""OPTIONS preflight 요청 처리"""
|
||||
return {"message": "OK"}
|
||||
|
||||
@router.post("/")
|
||||
async def create_project(
|
||||
project: ProjectCreate,
|
||||
request: Request,
|
||||
current_user: User = Depends(check_admin_permission)
|
||||
):
|
||||
"""프로젝트 생성 (관리자만) - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
return await tkuser_client.create_project(token, project.dict())
|
||||
|
||||
@router.get("/")
|
||||
async def get_projects(
|
||||
request: Request,
|
||||
@@ -60,27 +40,3 @@ async def get_project(
|
||||
detail="프로젝트를 찾을 수 없습니다."
|
||||
)
|
||||
return project
|
||||
|
||||
@router.put("/{project_id}")
|
||||
async def update_project(
|
||||
project_id: int,
|
||||
project_update: ProjectUpdate,
|
||||
request: Request,
|
||||
current_user: User = Depends(check_admin_permission)
|
||||
):
|
||||
"""프로젝트 수정 (관리자만) - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
return await tkuser_client.update_project(
|
||||
token, project_id, project_update.dict(exclude_unset=True)
|
||||
)
|
||||
|
||||
@router.delete("/{project_id}")
|
||||
async def delete_project(
|
||||
project_id: int,
|
||||
request: Request,
|
||||
current_user: User = Depends(check_admin_permission)
|
||||
):
|
||||
"""프로젝트 삭제 (비활성화) (관리자만) - tkuser API로 프록시"""
|
||||
token = get_token_from_request(request)
|
||||
await tkuser_client.delete_project(token, project_id)
|
||||
return {"message": "프로젝트가 삭제되었습니다."}
|
||||
|
||||
@@ -14,7 +14,7 @@ from openpyxl.drawing.image import Image as XLImage
|
||||
import os
|
||||
|
||||
from database.database import get_db
|
||||
from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole, ReviewStatus
|
||||
from database.models import Issue, IssueStatus, IssueCategory, User, UserRole, ReviewStatus
|
||||
from database import schemas
|
||||
from routers.auth import get_current_user
|
||||
from routers.page_permissions import check_page_access
|
||||
@@ -32,14 +32,9 @@ async def generate_report_summary(
|
||||
"""보고서 요약 생성"""
|
||||
start_date = report_request.start_date
|
||||
end_date = report_request.end_date
|
||||
|
||||
# 일일 공수 합계
|
||||
daily_works = db.query(DailyWork).filter(
|
||||
DailyWork.date >= start_date.date(),
|
||||
DailyWork.date <= end_date.date()
|
||||
).all()
|
||||
total_hours = sum(w.total_hours for w in daily_works)
|
||||
|
||||
|
||||
total_hours = 0
|
||||
|
||||
# 이슈 통계
|
||||
issues_query = db.query(Issue).filter(
|
||||
Issue.report_date >= start_date,
|
||||
@@ -118,29 +113,6 @@ async def get_report_issues(
|
||||
"detail_notes": issue.detail_notes
|
||||
} for issue in issues]
|
||||
|
||||
@router.get("/daily-works")
|
||||
async def get_report_daily_works(
|
||||
start_date: datetime,
|
||||
end_date: datetime,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""보고서용 일일 공수 목록"""
|
||||
works = db.query(DailyWork).filter(
|
||||
DailyWork.date >= start_date.date(),
|
||||
DailyWork.date <= end_date.date()
|
||||
).order_by(DailyWork.date).all()
|
||||
|
||||
return [{
|
||||
"date": work.date,
|
||||
"worker_count": work.worker_count,
|
||||
"regular_hours": work.regular_hours,
|
||||
"overtime_workers": work.overtime_workers,
|
||||
"overtime_hours": work.overtime_hours,
|
||||
"overtime_total": work.overtime_total,
|
||||
"total_hours": work.total_hours
|
||||
} for work in works]
|
||||
|
||||
@router.get("/daily-preview")
|
||||
async def preview_daily_report(
|
||||
project_id: int,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -182,25 +182,10 @@ const AuthAPI = {
|
||||
|
||||
getCurrentUser: () => apiRequest('/auth/me'),
|
||||
|
||||
createUser: (userData) => apiRequest('/auth/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userData)
|
||||
}),
|
||||
|
||||
getUsers: () => {
|
||||
console.log('🔍 AuthAPI.getUsers 호출 - 엔드포인트: /auth/users');
|
||||
return apiRequest('/auth/users');
|
||||
},
|
||||
|
||||
updateUser: (userId, userData) => apiRequest(`/auth/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(userData)
|
||||
}),
|
||||
|
||||
deleteUser: (userId) => apiRequest(`/auth/users/${userId}`, {
|
||||
method: 'DELETE'
|
||||
}),
|
||||
|
||||
|
||||
changePassword: (currentPassword, newPassword) => apiRequest('/auth/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -209,13 +194,6 @@ const AuthAPI = {
|
||||
})
|
||||
}),
|
||||
|
||||
resetPassword: (userId, newPassword = '000000') => apiRequest(`/auth/users/${userId}/reset-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
new_password: newPassword
|
||||
})
|
||||
}),
|
||||
|
||||
// 부서 목록 가져오기
|
||||
getDepartments: () => [
|
||||
{ value: 'production', label: '생산' },
|
||||
@@ -273,35 +251,6 @@ const IssuesAPI = {
|
||||
getStats: () => apiRequest('/issues/stats/summary')
|
||||
};
|
||||
|
||||
// Daily Work API
|
||||
const DailyWorkAPI = {
|
||||
create: (workData) => apiRequest('/daily-work/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(workData)
|
||||
}),
|
||||
|
||||
getAll: (params = {}) => {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
return apiRequest(`/daily-work/${queryString ? '?' + queryString : ''}`);
|
||||
},
|
||||
|
||||
get: (id) => apiRequest(`/daily-work/${id}`),
|
||||
|
||||
update: (id, workData) => apiRequest(`/daily-work/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(workData)
|
||||
}),
|
||||
|
||||
delete: (id) => apiRequest(`/daily-work/${id}`, {
|
||||
method: 'DELETE'
|
||||
}),
|
||||
|
||||
getStats: (params = {}) => {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
return apiRequest(`/daily-work/stats/summary${queryString ? '?' + queryString : ''}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Reports API
|
||||
const ReportsAPI = {
|
||||
getSummary: (startDate, endDate) => apiRequest('/reports/summary', {
|
||||
@@ -318,14 +267,6 @@ const ReportsAPI = {
|
||||
end_date: endDate
|
||||
}).toString();
|
||||
return apiRequest(`/reports/issues?${params}`);
|
||||
},
|
||||
|
||||
getDailyWorks: (startDate, endDate) => {
|
||||
const params = new URLSearchParams({
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
}).toString();
|
||||
return apiRequest(`/reports/daily-works?${params}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -373,20 +314,6 @@ const ProjectsAPI = {
|
||||
const params = `?active_only=${activeOnly}`;
|
||||
return apiRequest(`/projects/${params}`);
|
||||
},
|
||||
|
||||
get: (id) => apiRequest(`/projects/${id}`),
|
||||
|
||||
create: (projectData) => apiRequest('/projects/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(projectData)
|
||||
}),
|
||||
|
||||
update: (id, projectData) => apiRequest(`/projects/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(projectData)
|
||||
}),
|
||||
|
||||
delete: (id) => apiRequest(`/projects/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
get: (id) => apiRequest(`/projects/${id}`)
|
||||
};
|
||||
|
||||
@@ -271,9 +271,7 @@ class App {
|
||||
'dashboard': '대시보드',
|
||||
'issues': '부적합 사항',
|
||||
'projects': '프로젝트',
|
||||
'daily_work': '일일 공수',
|
||||
'reports': '보고서',
|
||||
'users': '사용자 관리'
|
||||
'reports': '보고서'
|
||||
};
|
||||
|
||||
const title = titles[module] || module;
|
||||
|
||||
@@ -10,17 +10,6 @@ class CommonHeader {
|
||||
this.menuItems = this.initMenuItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 관리 URL (tkuser 서브도메인 또는 로컬 포트)
|
||||
*/
|
||||
_getUserManageUrl() {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
return window.location.protocol + '//tkuser.technicalkorea.net';
|
||||
}
|
||||
return window.location.protocol + '//' + hostname + ':30380';
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 아이템 정의
|
||||
*/
|
||||
@@ -62,15 +51,6 @@ class CommonHeader {
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'daily_work',
|
||||
title: '일일 공수',
|
||||
icon: 'fas fa-calendar-check',
|
||||
url: '/daily-work.html',
|
||||
pageName: 'daily_work',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
title: '보고서',
|
||||
@@ -106,25 +86,6 @@ class CommonHeader {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'projects_manage',
|
||||
title: '프로젝트 관리',
|
||||
icon: 'fas fa-folder-open',
|
||||
url: '/project-management.html',
|
||||
pageName: 'projects_manage',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'users_manage',
|
||||
title: '사용자 관리',
|
||||
icon: 'fas fa-users-cog',
|
||||
url: this._getUserManageUrl(),
|
||||
pageName: 'users_manage',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100',
|
||||
external: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -28,20 +28,14 @@ class KeyboardShortcutManager {
|
||||
// 네비게이션 단축키
|
||||
this.register('g h', () => this.navigateToPage('/index.html', 'issues_create'), '홈 (부적합 등록)');
|
||||
this.register('g v', () => this.navigateToPage('/issue-view.html', 'issues_view'), '부적합 조회');
|
||||
this.register('g d', () => this.navigateToPage('/daily-work.html', 'daily_work'), '일일 공수');
|
||||
this.register('g p', () => this.navigateToPage('/project-management.html', 'projects_manage'), '프로젝트 관리');
|
||||
this.register('g r', () => this.navigateToPage('/reports.html', 'reports'), '보고서');
|
||||
this.register('g a', () => this.navigateToPage('/admin.html', 'users_manage'), '관리자');
|
||||
|
||||
|
||||
// 액션 단축키
|
||||
this.register('n', () => this.triggerNewAction(), '새 항목 생성');
|
||||
this.register('s', () => this.triggerSaveAction(), '저장');
|
||||
this.register('r', () => this.triggerRefreshAction(), '새로고침');
|
||||
this.register('f', () => this.focusSearchField(), '검색 포커스');
|
||||
|
||||
// 관리자 전용 단축키
|
||||
this.register('ctrl+shift+u', () => this.navigateToPage('/admin.html', 'users_manage'), '사용자 관리 (관리자)');
|
||||
|
||||
console.log('⌨️ 키보드 단축키 등록 완료');
|
||||
}
|
||||
|
||||
|
||||
@@ -169,14 +169,8 @@ class PageManager {
|
||||
return new IssuesViewModule(options);
|
||||
case 'issues_manage':
|
||||
return new IssuesManageModule(options);
|
||||
case 'projects_manage':
|
||||
return new ProjectsManageModule(options);
|
||||
case 'daily_work':
|
||||
return new DailyWorkModule(options);
|
||||
case 'reports':
|
||||
return new ReportsModule(options);
|
||||
case 'users_manage':
|
||||
return new UsersManageModule(options);
|
||||
default:
|
||||
console.warn(`알 수 없는 페이지 ID: ${pageId}`);
|
||||
return null;
|
||||
|
||||
@@ -57,10 +57,7 @@ class PagePreloader {
|
||||
{ id: 'issues_create', url: '/index.html', priority: 1 },
|
||||
{ id: 'issues_view', url: '/issue-view.html', priority: 1 },
|
||||
{ id: 'issues_manage', url: '/index.html#list', priority: 2 },
|
||||
{ id: 'projects_manage', url: '/project-management.html', priority: 3 },
|
||||
{ id: 'daily_work', url: '/daily-work.html', priority: 2 },
|
||||
{ id: 'reports', url: '/reports.html', priority: 3 },
|
||||
{ id: 'users_manage', url: '/admin.html', priority: 4 }
|
||||
{ id: 'reports', url: '/reports.html', priority: 3 }
|
||||
];
|
||||
|
||||
// 권한 체크
|
||||
|
||||
@@ -20,10 +20,7 @@ class PagePermissionManager {
|
||||
'issues_inbox': { title: '수신함', defaultAccess: true },
|
||||
'issues_management': { title: '관리함', defaultAccess: false },
|
||||
'issues_archive': { title: '폐기함', defaultAccess: false },
|
||||
'projects_manage': { title: '프로젝트 관리', defaultAccess: false },
|
||||
'daily_work': { title: '일일 공수', defaultAccess: false },
|
||||
'reports': { title: '보고서', defaultAccess: false },
|
||||
'users_manage': { title: '사용자 관리', defaultAccess: false }
|
||||
'reports': { title: '보고서', defaultAccess: false }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,33 +164,12 @@ class PagePermissionManager {
|
||||
path: '#issues/manage',
|
||||
pageName: 'issues_manage'
|
||||
},
|
||||
{
|
||||
id: 'projects_manage',
|
||||
title: '프로젝트 관리',
|
||||
icon: 'fas fa-folder-open',
|
||||
path: '#projects/manage',
|
||||
pageName: 'projects_manage'
|
||||
},
|
||||
{
|
||||
id: 'daily_work',
|
||||
title: '일일 공수',
|
||||
icon: 'fas fa-calendar-check',
|
||||
path: '#daily-work',
|
||||
pageName: 'daily_work'
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
title: '보고서',
|
||||
icon: 'fas fa-chart-bar',
|
||||
path: '#reports',
|
||||
pageName: 'reports'
|
||||
},
|
||||
{
|
||||
id: 'users_manage',
|
||||
title: '사용자 관리',
|
||||
icon: 'fas fa-users-cog',
|
||||
path: '#users/manage',
|
||||
pageName: 'users_manage'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -46,9 +46,6 @@ const DEFAULT_PAGES = {
|
||||
'issues_inbox': { title: '수신함', system: 'system3', group: '메인', default_access: true },
|
||||
'issues_management': { title: '관리함', system: 'system3', group: '메인', default_access: false },
|
||||
'issues_archive': { title: '폐기함', system: 'system3', group: '메인', default_access: false },
|
||||
// 업무
|
||||
'daily_work': { title: '일일 공수', system: 'system3', group: '업무', default_access: false },
|
||||
'projects_manage': { title: '프로젝트 관리', system: 'system3', group: '업무', default_access: false },
|
||||
// 보고서
|
||||
'reports': { title: '보고서', system: 'system3', group: '보고서', default_access: false },
|
||||
'reports_daily': { title: '일일보고서', system: 'system3', group: '보고서', default_access: false },
|
||||
|
||||
Reference in New Issue
Block a user