feat: 작업보고서 시스템 완성

- 일일 공수 입력 기능
- 부적합 사항 등록 (이미지 선택사항)
- 날짜별 부적합 조회 (시간순 나열)
- 목록 관리 (인라인 편집, 작업시간 확인 버튼)
- 보고서 생성 (총 공수/부적합 시간 분리)
- JWT 인증 및 권한 관리
- Docker 기반 배포 환경 구성
This commit is contained in:
hyungi
2025-09-17 10:41:25 +09:00
commit 1339e5dded
36 changed files with 5233 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
import os
from database.models import User, UserRole
from database.schemas import TokenData
# 환경 변수
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here")
ALGORITHM = os.getenv("ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7 days
# 비밀번호 암호화
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token: str, credentials_exception):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
return token_data
except JWTError:
raise credentials_exception
def authenticate_user(db: Session, username: str, password: str):
user = db.query(User).filter(User.username == username).first()
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_admin_user(db: Session):
"""초기 관리자 계정 생성"""
admin_username = os.getenv("ADMIN_USERNAME", "hyungi")
admin_password = os.getenv("ADMIN_PASSWORD", "djg3-jj34-X3Q3")
# 이미 존재하는지 확인
existing_admin = db.query(User).filter(User.username == admin_username).first()
if not existing_admin:
admin_user = User(
username=admin_username,
hashed_password=get_password_hash(admin_password),
full_name="관리자",
role=UserRole.ADMIN,
is_active=True
)
db.add(admin_user)
db.commit()
print(f"관리자 계정 생성됨: {admin_username}")
else:
print(f"관리자 계정이 이미 존재함: {admin_username}")

View File

@@ -0,0 +1,61 @@
import os
import base64
from datetime import datetime
from typing import Optional
import uuid
from PIL import Image
import io
UPLOAD_DIR = "/app/uploads"
def ensure_upload_dir():
"""업로드 디렉토리 생성"""
if not os.path.exists(UPLOAD_DIR):
os.makedirs(UPLOAD_DIR)
def save_base64_image(base64_string: str) -> Optional[str]:
"""Base64 이미지를 파일로 저장하고 경로 반환"""
try:
ensure_upload_dir()
# Base64 헤더 제거
if "," in base64_string:
base64_string = base64_string.split(",")[1]
# 디코딩
image_data = base64.b64decode(base64_string)
# 이미지 검증 및 형식 확인
image = Image.open(io.BytesIO(image_data))
format = image.format.lower() if image.format else 'png'
# 파일명 생성
filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.{format}"
filepath = os.path.join(UPLOAD_DIR, filename)
# 이미지 저장 (최대 크기 제한)
max_size = (1920, 1920)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
if format == 'png':
image.save(filepath, 'PNG', optimize=True)
else:
image.save(filepath, 'JPEG', quality=85, optimize=True)
# 웹 경로 반환
return f"/uploads/{filename}"
except Exception as e:
print(f"이미지 저장 실패: {e}")
return None
def delete_file(filepath: str):
"""파일 삭제"""
try:
if filepath and filepath.startswith("/uploads/"):
filename = filepath.replace("/uploads/", "")
full_path = os.path.join(UPLOAD_DIR, filename)
if os.path.exists(full_path):
os.remove(full_path)
except Exception as e:
print(f"파일 삭제 실패: {e}")