From b78f7c8d3b3c3291ac4cb24642d02a1be2fd51fe Mon Sep 17 00:00:00 2001 From: SR07 Date: Mon, 26 May 2025 16:59:35 +0900 Subject: [PATCH] v0.2 --- app/api/character.py | 35 +++++++++++++++++++++++++++++++- app/api/homework.py | 43 ++++++++++++++++++++++++++++++++++++++-- app/api/user.py | 36 +++++++++++++++++++++++++++++++-- app/core/database.py | 11 ++++++++-- app/main.py | 15 ++++++++++++-- app/models/user.py | 5 +++-- app/schemas/character.py | 7 ++++++- app/schemas/homework.py | 10 ++++++++-- app/schemas/user.py | 6 +++++- 9 files changed, 153 insertions(+), 15 deletions(-) diff --git a/app/api/character.py b/app/api/character.py index 25b7d99..b8305e4 100644 --- a/app/api/character.py +++ b/app/api/character.py @@ -2,7 +2,7 @@ from typing import List from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session -from app.schemas.character import CharacterCreate, CharacterResponse +from app.schemas.character import CharacterCreate, CharacterResponse, CharacterUpdateRequest from app.schemas.homework import HomeworkSelectableResponse from app.crud.character import create_character, get_characters_by_user from app.services.character_homework_service import get_homeworks_with_assignment_status, assign_homework_to_character, unassign_homework_from_character @@ -60,3 +60,36 @@ def unassign_homework( current_user: User = Depends(get_current_user) ): return unassign_homework_from_character(db, current_user.id, character_id, homework_type_id) + +@router.put("/{character_id}") +def update_character( + character_id: int, + req: CharacterUpdateRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + character = db.query(Character).filter(Character.id == character_id).first() + + if not character or character.user_id != current_user.id: + raise HTTPException(status_code=403, detail="권한이 없습니다.") + + character.name = req.name + character.server = req.server + character.power = req.power + db.commit() + return {"message": "캐릭터가 수정되었습니다."} + +@router.delete("/{character_id}") +def delete_character( + character_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + character = db.query(Character).filter(Character.id == character_id).first() + + if not character or character.user_id != current_user.id: + raise HTTPException(status_code=403, detail="권한이 없습니다.") + + db.delete(character) + db.commit() + return {"message": "캐릭터가 삭제되었습니다."} \ No newline at end of file diff --git a/app/api/homework.py b/app/api/homework.py index 0bd0b60..22914ac 100644 --- a/app/api/homework.py +++ b/app/api/homework.py @@ -1,12 +1,12 @@ from datetime import datetime, time -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List from app.core.deps import get_db, get_current_user from app.models.homework import HomeworkType from app.models.user import User -from app.schemas.homework import HomeworkTypeCreate, HomeworkTypeResponse +from app.schemas.homework import HomeworkTypeCreate, HomeworkTypeResponse, HomeworkTypeUpdateRequest from app.crud.homework import create_homework_type, get_homework_types_by_user router = APIRouter() @@ -37,3 +37,42 @@ def list_homework_types( current_user: User = Depends(get_current_user) ): return get_homework_types_by_user(current_user.id, db) + +@router.put("/{homework_type_id}") +def update_homework_type( + homework_type_id: int, + req: HomeworkTypeUpdateRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + current_user = db.merge(current_user) + + homework_type = db.query(HomeworkType).filter(HomeworkType.id == homework_type_id).first() + + if not homework_type or homework_type.user_id != current_user.id: + raise HTTPException(status_code=403, detail="권한이 없습니다.") + + homework_type.name = req.name + homework_type.description = req.description + homework_type.repeat_type = req.repeat_type + homework_type.repeat_count = req.repeat_count + + db.commit() + return {"message": "숙제가 수정되었습니다."} + +@router.delete("/{homework_type_id}") +def delete_homework_type( + homework_type_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + current_user = db.merge(current_user) + + homework_type = db.query(HomeworkType).filter(HomeworkType.id == homework_type_id).first() + + if not homework_type or homework_type.user_id != current_user.id: + raise HTTPException(status_code=403, detail="권한이 없습니다.") + + db.delete(homework_type) + db.commit() + return {"message": "숙제가 삭제되었습니다."} \ No newline at end of file diff --git a/app/api/user.py b/app/api/user.py index cbb96c3..68c5084 100644 --- a/app/api/user.py +++ b/app/api/user.py @@ -1,12 +1,16 @@ # app/api/user.py -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.logger import logger +import traceback +import sys from sqlalchemy.orm import Session -from app.schemas.user import UserCreate, UserResponse +from app.schemas.user import UserCreate, UserResponse, PasswordUpdateRequest from app.crud.user import create_user from app.models.user import User from app.core.database import SessionLocal from app.core.deps import get_current_user +from app.core.security import verify_password, get_password_hash router = APIRouter() @@ -25,3 +29,31 @@ def register_user(user_create: UserCreate, db: Session = Depends(get_db)): @router.get("/me", response_model=UserResponse) def get_my_profile(current_user: User = Depends(get_current_user)): return current_user + +@router.put("/me/password", status_code=204) +def update_password( + pw_req: PasswordUpdateRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + try: + if not verify_password(pw_req.current_password, current_user.password_hash): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="현재 비밀번호가 일치하지 않습니다." + ) + + print("기존:", current_user.password_hash) + print("신규:", get_password_hash(pw_req.new_password)) + + current_user = db.merge(current_user) # 세션에 붙임 + current_user.password_hash = get_password_hash(pw_req.new_password) + db.flush() + db.expunge(current_user) + print("✅ flush 완료됨. 커밋 시도 중...") + db.commit() + print("✅ 커밋 완료됨.") + except Exception as e: + logger.error(f"❌ 비밀번호 변경 중 예외 발생: {e}") + traceback.print_exc(file=sys.stdout) # ← 여기가 핵심 + raise \ No newline at end of file diff --git a/app/core/database.py b/app/core/database.py index 05a8411..52c84af 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -1,13 +1,20 @@ # app/core/database.py -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker, declarative_base from app.core.config import settings +from sqlalchemy.engine import Engine +import traceback -engine = create_engine(settings.database_url, echo=True) +engine = create_engine(settings.database_url, echo=True, future=True) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() +@event.listens_for(Engine, "handle_error") +def receive_handle_error(exception_context): + print("🔥 SQLAlchemy DB 에러 감지!") + traceback.print_exc() + def get_db(): db = SessionLocal() try: diff --git a/app/main.py b/app/main.py index 15630de..1448ac4 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,8 @@ #pp/main.py -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.security import OAuth2PasswordBearer +import traceback from app.api import user, auth, character, homework, character_homework, dashboard @@ -17,13 +18,23 @@ app = FastAPI( root_path="/api" ) +@app.middleware("http") +async def log_exceptions_middleware(request: Request, call_next): + try: + response = await call_next(request) + return response + except Exception as exc: + print("❌ 전역 예외 발생:") + traceback.print_exc() + raise + origins = [ "https://sukjenogi.biryu2000.kr", # 프론트 도메인 ] app.add_middleware( CORSMiddleware, - allow_origins=origins, + allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/app/models/user.py b/app/models/user.py index 002592e..8e1984f 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,6 +1,7 @@ # app/models/user.py from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy.orm import relationship +from sqlalchemy.sql import func from datetime import datetime from app.core.config import Base @@ -10,8 +11,8 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, nullable=False, index=True) password_hash = Column(String, nullable=False) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, server_default=func.now(), nullable=False) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False) characters = relationship("Character", back_populates="user") homework_types = relationship("HomeworkType", back_populates="user", cascade="all, delete") diff --git a/app/schemas/character.py b/app/schemas/character.py index 96eceb2..f2eee4d 100644 --- a/app/schemas/character.py +++ b/app/schemas/character.py @@ -1,6 +1,6 @@ # app/schemas/character.py -from pydantic import BaseModel +from pydantic import BaseModel, constr, conint from typing import Optional from datetime import datetime @@ -23,3 +23,8 @@ class CharacterResponse(BaseModel): class Config: orm_mode = True + +class CharacterUpdateRequest(BaseModel): + name: constr(min_length=1) + server: constr(min_length=1) + power: conint(ge=0) # 0 이상 정수 \ No newline at end of file diff --git a/app/schemas/homework.py b/app/schemas/homework.py index 9af5e8b..376627f 100644 --- a/app/schemas/homework.py +++ b/app/schemas/homework.py @@ -1,6 +1,6 @@ # app/schemas/homework.py -from pydantic import BaseModel +from pydantic import BaseModel, constr, conint from datetime import time, datetime from typing import Optional @@ -28,4 +28,10 @@ class HomeworkSelectableResponse(BaseModel): title: str assigned: str # 'Y' or 'N' reset_type: str - clear_count: int \ No newline at end of file + clear_count: int + +class HomeworkTypeUpdateRequest(BaseModel): + name: constr(min_length=1) + description: str | None = None + repeat_type: constr(min_length=1) + repeat_count: conint(ge=1) \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py index 8097afd..1b5605b 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -1,6 +1,6 @@ # app/schemas/user.py -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, constr from datetime import datetime # 사용자 생성 요청용 (회원가입) @@ -14,5 +14,9 @@ class UserResponse(BaseModel): email: EmailStr created_at: datetime +class PasswordUpdateRequest(BaseModel): + current_password: constr(min_length=4) + new_password: constr(min_length=6) + class Config: orm_mode = True