From accf0ebc4be5ba2179d38954db02dc9305c53657 Mon Sep 17 00:00:00 2001 From: biryu2000 Date: Sun, 25 May 2025 00:38:12 +0900 Subject: [PATCH] chlchzjalt --- .gitignore | 24 ++++ Dockerfile | 10 ++ app/api/auth.py | 52 +++++++ app/api/character.py | 62 +++++++++ app/api/character_homework.py | 43 ++++++ app/api/dashboard.py | 26 ++++ app/api/homework.py | 39 ++++++ app/api/user.py | 27 ++++ app/core/config.py | 19 +++ app/core/database.py | 19 +++ app/core/deps.py | 40 ++++++ app/core/security.py | 30 ++++ app/crud/character.py | 43 ++++++ app/crud/homework.py | 18 +++ app/crud/user.py | 20 +++ app/main.py | 38 ++++++ app/models/character.py | 40 ++++++ app/models/homework.py | 20 +++ app/models/user.py | 17 +++ app/schemas/character.py | 25 ++++ app/schemas/character_homework.py | 6 + app/schemas/dashboard.py | 13 ++ app/schemas/homework.py | 31 +++++ app/schemas/user.py | 18 +++ app/services/character_homework_service.py | 152 +++++++++++++++++++++ app/services/dashboard_service.py | 55 ++++++++ create_db.py | 12 ++ docker-compose.yml | 14 ++ requirements.txt | 10 ++ run_reset_homeworks.bat | 5 + scripts/reset_homeworks.py | 40 ++++++ 31 files changed, 968 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app/api/auth.py create mode 100644 app/api/character.py create mode 100644 app/api/character_homework.py create mode 100644 app/api/dashboard.py create mode 100644 app/api/homework.py create mode 100644 app/api/user.py create mode 100644 app/core/config.py create mode 100644 app/core/database.py create mode 100644 app/core/deps.py create mode 100644 app/core/security.py create mode 100644 app/crud/character.py create mode 100644 app/crud/homework.py create mode 100644 app/crud/user.py create mode 100644 app/main.py create mode 100644 app/models/character.py create mode 100644 app/models/homework.py create mode 100644 app/models/user.py create mode 100644 app/schemas/character.py create mode 100644 app/schemas/character_homework.py create mode 100644 app/schemas/dashboard.py create mode 100644 app/schemas/homework.py create mode 100644 app/schemas/user.py create mode 100644 app/services/character_homework_service.py create mode 100644 app/services/dashboard_service.py create mode 100644 create_db.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100644 run_reset_homeworks.bat create mode 100644 scripts/reset_homeworks.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6197976 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# 가상환경 +venv/ + +# Python 캐시 +__pycache__/ +*.py[cod] + +# 환경변수 파일 +.env + +# SQLite 등 테스트 DB 파일 +*.sqlite3 + +# 로그 +*.log +logs/ + +# IDE 설정 +.idea/ +.vscode/ + +# OS별 무시 +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..52f2acb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/api/auth.py b/app/api/auth.py new file mode 100644 index 0000000..779a5c4 --- /dev/null +++ b/app/api/auth.py @@ -0,0 +1,52 @@ +# app/api/auth.py + +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from app.core.database import SessionLocal +from app.models.user import User +from app.core.security import verify_password, create_access_token +from pydantic import BaseModel, EmailStr +from datetime import timedelta + +router = APIRouter() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# 로그인 요청 스키마 +class LoginRequest(BaseModel): + email: EmailStr + password: str + + +# 로그인 응답 스키마 +class TokenResponse(BaseModel): + access_token: str + token_type: str = "bearer" + + +@router.post("/login", response_model=TokenResponse) +def login(request: LoginRequest, db: Session = Depends(get_db)): + user = db.query(User).filter(User.email == request.email).first() + if not user or not verify_password(request.password, user.password_hash): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") + + token = create_access_token( + data={"sub": str(user.id)}, + expires_delta=timedelta(minutes=60 * 24) # 1일 + ) + return {"access_token": token, "token_type": "bearer"} + +@router.get("/check-email") +def check_email_availability( + email: str = Query(..., description="중복 확인할 이메일 주소"), + db: Session = Depends(get_db) +): + user = db.query(User).filter(User.email == email).first() + return {"available": user is None} \ No newline at end of file diff --git a/app/api/character.py b/app/api/character.py new file mode 100644 index 0000000..25b7d99 --- /dev/null +++ b/app/api/character.py @@ -0,0 +1,62 @@ +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.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 +from app.models.user import User +from app.models.character import Character +from app.core.deps import get_db, get_current_user + +router = APIRouter() + + +@router.post("", response_model=CharacterResponse) +def register_character( + character_data: CharacterCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return create_character(current_user.id, character_data, db) + + +@router.get("", response_model=List[CharacterResponse]) +def list_my_characters( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return get_characters_by_user(current_user.id, db) + + +@router.get("/{character_id}/homeworks/selectable", response_model=List[HomeworkSelectableResponse]) +def get_selectable_homeworks( + character_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + character = db.query(Character).filter_by(id=character_id, user_id=current_user.id).first() + if not character: + raise HTTPException(status_code=404, detail="캐릭터를 찾을 수 없습니다.") + + return get_homeworks_with_assignment_status(db, current_user.id, character_id) + +@router.post("/{character_id}/homeworks/{homework_type_id}") +def assign_homework( + character_id: int, + homework_type_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return assign_homework_to_character(db, current_user.id, character_id, homework_type_id) + + +@router.delete("/{character_id}/homeworks/{homework_type_id}") +def unassign_homework( + character_id: int, + homework_type_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return unassign_homework_from_character(db, current_user.id, character_id, homework_type_id) diff --git a/app/api/character_homework.py b/app/api/character_homework.py new file mode 100644 index 0000000..4cdc9bf --- /dev/null +++ b/app/api/character_homework.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from app.core.deps import get_db, get_current_user +from app.models.user import User +from app.models.character import Character +from app.schemas.character_homework import HomeworkCompletionUpdateRequest +from app.schemas.homework import HomeworkSelectableResponse +from app.services.character_homework_service import ( + get_homeworks_with_assignment_status, + update_homework_completion +) + +router = APIRouter(tags=["Character Homeworks"]) + + +@router.get("/{character_id}/homeworks/selectable", response_model=list[HomeworkSelectableResponse]) +def get_selectable_homeworks( + character_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + character = db.query(Character).filter_by(id=character_id, user_id=current_user.id).first() + if not character: + raise HTTPException(status_code=404, detail="캐릭터를 찾을 수 없습니다.") + + return get_homeworks_with_assignment_status(db, current_user.id, character_id) + + +@router.patch("/{character_id}/homeworks/{homework_type_id}") +def update_homework_completion_api( + character_id: int, + homework_type_id: int, + body: HomeworkCompletionUpdateRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return update_homework_completion( + db=db, + user_id=current_user.id, + character_id=character_id, + homework_type_id=homework_type_id, + new_complete_cnt=body.complete_cnt + ) diff --git a/app/api/dashboard.py b/app/api/dashboard.py new file mode 100644 index 0000000..24c3bd3 --- /dev/null +++ b/app/api/dashboard.py @@ -0,0 +1,26 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from app.core.deps import get_db, get_current_user +from app.models.user import User +from app.services.dashboard_service import ( + get_dashboard_characters, + get_dashboard_homeworks_for_character +) + +router = APIRouter(tags=["Dashboard"]) + +@router.get("/characters") +def dashboard_characters( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return get_dashboard_characters(db, current_user.id) + + +@router.get("/characters/{character_id}/homeworks") +def dashboard_homeworks( + character_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return get_dashboard_homeworks_for_character(db, current_user.id, character_id) diff --git a/app/api/homework.py b/app/api/homework.py new file mode 100644 index 0000000..0bd0b60 --- /dev/null +++ b/app/api/homework.py @@ -0,0 +1,39 @@ +from datetime import datetime, time + +from fastapi import APIRouter, Depends +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.crud.homework import create_homework_type, get_homework_types_by_user + +router = APIRouter() + +@router.post("", response_model=HomeworkTypeResponse) +def register_homework_type( + homework_data: HomeworkTypeCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + homework_type = HomeworkType( + user_id=current_user.id, + title=homework_data.title, + description=homework_data.description, + reset_type=homework_data.reset_type, + reset_time=homework_data.reset_time or time(6, 0), + clear_count=homework_data.clear_count or 0, + created_at=datetime.utcnow(), + ) + db.add(homework_type) + db.commit() + db.refresh(homework_type) + return homework_type + +@router.get("", response_model=List[HomeworkTypeResponse]) +def list_homework_types( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + return get_homework_types_by_user(current_user.id, db) diff --git a/app/api/user.py b/app/api/user.py new file mode 100644 index 0000000..cbb96c3 --- /dev/null +++ b/app/api/user.py @@ -0,0 +1,27 @@ +# app/api/user.py + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from app.schemas.user import UserCreate, UserResponse +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 + +router = APIRouter() + +# DB 세션 주입 함수 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@router.post("/", response_model=UserResponse) +def register_user(user_create: UserCreate, db: Session = Depends(get_db)): + return create_user(db, user_create) + +@router.get("/me", response_model=UserResponse) +def get_my_profile(current_user: User = Depends(get_current_user)): + return current_user diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..f480a5a --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,19 @@ +# app/core/config.py + +from pydantic_settings import BaseSettings +from sqlalchemy.orm import declarative_base + +class Settings(BaseSettings): + database_url: str + secret_key: str + algorithm: str + access_token_expire_minutes: int + + class Config: + env_file = ".env" + +settings = Settings() + +# 베이스 클래스 +Base = declarative_base() + diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..05a8411 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,19 @@ +# app/core/database.py + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from app.core.config import settings + +engine = create_engine(settings.database_url, echo=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# 세션 클래스 생성 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) \ No newline at end of file diff --git a/app/core/deps.py b/app/core/deps.py new file mode 100644 index 0000000..ed2bdbb --- /dev/null +++ b/app/core/deps.py @@ -0,0 +1,40 @@ +# app/core/deps.py + +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from jose import JWTError, jwt +from sqlalchemy.orm import Session +from app.core.database import SessionLocal +from app.models.user import User +from app.core.security import SECRET_KEY, ALGORITHM + +oauth2_scheme = HTTPBearer() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def get_current_user( + token: HTTPAuthorizationCredentials = Depends(oauth2_scheme), + db: Session = Depends(get_db) +) -> User: + credentials_exception = HTTPException( + status_code=401, + detail="Could not validate credentials", + ) + + try: + payload = jwt.decode(token.credentials, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = db.query(User).filter(User.id == int(user_id)).first() + if user is None: + raise credentials_exception + return user diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..1b737c2 --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,30 @@ +# app/core/security.py + +from datetime import datetime, timedelta +from typing import Union +from jose import JWTError, jwt +from passlib.context import CryptContext + +# 시크릿 키는 임의의 문자열로 설정 (환경변수로 빼도 좋음) +SECRET_KEY = "sukjenogi_super_secret_key" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 1일 + +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: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + diff --git a/app/crud/character.py b/app/crud/character.py new file mode 100644 index 0000000..c69c7ff --- /dev/null +++ b/app/crud/character.py @@ -0,0 +1,43 @@ +#app/crud/character.py + +from sqlalchemy.orm import Session +from app.models.character import Character +from app.schemas.character import CharacterCreate +from datetime import datetime + +# 외부 크롤링 모듈 불러오기 (예시) +def fetch_character_stats(name: str, server: str): + # 실제 크롤링 로직으로 교체 필요 + # 예시: {'job': '전사', 'power': 123456} + return { + "job": "(크롤링된 직업)", + "power": 123456 + } + +def create_character(user_id: int, character_data: CharacterCreate, db: Session) -> Character: + character = Character( + user_id=user_id, + name=character_data.name, + server=character_data.server, + job=character_data.job, + combat_power=character_data.combat_power, # ← 수동 입력 허용 + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ) + db.add(character) + db.commit() + db.refresh(character) + + # 자동 동기화는 combat_power 없을 때만 시도 (선택사항) + if character.combat_power is None: + stats = fetch_character_stats(character.name, character.server) + # character.job = stats.get("job") + # character.combat_power = stats.get("power") + # character.auto_synced_at = datetime.utcnow() + # db.commit() + # db.refresh(character) + + return character + +def get_characters_by_user(user_id: int, db: Session): + return db.query(Character).filter(Character.user_id == user_id).all() diff --git a/app/crud/homework.py b/app/crud/homework.py new file mode 100644 index 0000000..422c33d --- /dev/null +++ b/app/crud/homework.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import Session +from app.models.homework import HomeworkType +from app.schemas.homework import HomeworkTypeCreate + +def create_homework_type(user_id: int, data: HomeworkTypeCreate, db: Session): + new_homework = HomeworkType( + user_id=user_id, + name=data.name, + reset_time=data.reset_time, + clear_count=data.clear_count, + ) + db.add(new_homework) + db.commit() + db.refresh(new_homework) + return new_homework + +def get_homework_types_by_user(user_id: int, db: Session): + return db.query(HomeworkType).filter(HomeworkType.user_id == user_id).all() diff --git a/app/crud/user.py b/app/crud/user.py new file mode 100644 index 0000000..ed2264f --- /dev/null +++ b/app/crud/user.py @@ -0,0 +1,20 @@ +# app/crud/user.py + +from sqlalchemy.orm import Session +from app.models.user import User +from app.schemas.user import UserCreate +from passlib.context import CryptContext + +# 비밀번호 해싱용 도구 설정 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + +def create_user(db: Session, user_create: UserCreate) -> User: + hashed_pw = get_password_hash(user_create.password) + db_user = User(email=user_create.email, password_hash=hashed_pw) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..2a6fdc4 --- /dev/null +++ b/app/main.py @@ -0,0 +1,38 @@ +#pp/main.py +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.security import OAuth2PasswordBearer + +from app.api import user, auth, character, homework, character_homework, dashboard + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + +app = FastAPI( + title="숙제노기 API", + description="마비노기 모바일 숙제 관리용 백엔드 API", + version="0.1.0", + docs_url="/docs", + redoc_url=None, + openapi_url="/openapi.json", + root_path="/api" +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 또는 ["http://localhost:5173"]만 명시 가능 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(user.router, prefix="/users", tags=["Users"]) +app.include_router(auth.router, prefix="/auth", tags=["Auth"]) +app.include_router(character.router, prefix="/characters", tags=["Characters"]) +app.include_router(homework.router, prefix="/homeworks", tags=["Homeworks"]) +app.include_router(character_homework.router, prefix="/characterHomework", tags=["Character Homeworks"]) +app.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"]) + +@app.get("/") +def read_root(): + return {"message": "숙제노기 서버가 잘 작동 중입니다."} + diff --git a/app/models/character.py b/app/models/character.py new file mode 100644 index 0000000..47342bf --- /dev/null +++ b/app/models/character.py @@ -0,0 +1,40 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, func +from sqlalchemy.orm import relationship +from datetime import datetime +from app.core.config import Base + + +class Character(Base): + __tablename__ = "characters" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id")) + name = Column(String(100), nullable=False) + server = Column(String(50)) + job = Column(String(50)) + combat_power = Column(Integer) # ← 추가된 필드 + auto_synced_at = Column(DateTime) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="characters") + homeworks = relationship("CharacterHomework", back_populates="character", cascade="all, delete") + + +class CharacterHomework(Base): + __tablename__ = "character_homeworks" + + id = Column(Integer, primary_key=True, index=True) + character_id = Column(Integer, ForeignKey("characters.id", ondelete="CASCADE"), nullable=False) + homework_type_id = Column(Integer, ForeignKey("homework_types.id", ondelete="CASCADE"), nullable=False) # ✅ 중요!! + + is_done = Column(Boolean, default=False) + last_completed_at = Column(DateTime, nullable=True) + + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + complete_cnt = Column(Integer, nullable=False, default=0) + + character = relationship("Character", back_populates="homeworks") + homework_type = relationship("HomeworkType", back_populates="assigned_characters") \ No newline at end of file diff --git a/app/models/homework.py b/app/models/homework.py new file mode 100644 index 0000000..2714e2e --- /dev/null +++ b/app/models/homework.py @@ -0,0 +1,20 @@ +from sqlalchemy import Column, Integer, String, Time, ForeignKey, DateTime +from sqlalchemy.orm import relationship +from datetime import time, datetime + +from app.core.config import Base + +class HomeworkType(Base): + __tablename__ = "homework_types" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + title = Column(String(100), nullable=False) + description = Column(String) + reset_type = Column(String(20), nullable=False) + reset_time = Column(Time, nullable=False, default=datetime.strptime("06:00", "%H:%M").time()) + clear_count = Column(Integer, nullable=False, default=0) + created_at = Column(DateTime, default=datetime.utcnow) + + user = relationship("User", back_populates="homework_types") + assigned_characters = relationship("CharacterHomework", back_populates="homework_type", cascade="all, delete") diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..002592e --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,17 @@ +# app/models/user.py +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.orm import relationship +from datetime import datetime +from app.core.config import Base + +class User(Base): + __tablename__ = "users" + + 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) + + 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 new file mode 100644 index 0000000..96eceb2 --- /dev/null +++ b/app/schemas/character.py @@ -0,0 +1,25 @@ +# app/schemas/character.py + +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +# 캐릭터 생성 요청용 +class CharacterCreate(BaseModel): + name: str + server: Optional[str] = None + job: Optional[str] = None + combat_power: Optional[int] = None # ← 추가 + +# 캐릭터 응답용 +class CharacterResponse(BaseModel): + id: int + name: str + server: Optional[str] + job: Optional[str] + combat_power: Optional[int] # ← 추가 + auto_synced_at: Optional[datetime] + created_at: datetime + + class Config: + orm_mode = True diff --git a/app/schemas/character_homework.py b/app/schemas/character_homework.py new file mode 100644 index 0000000..933a167 --- /dev/null +++ b/app/schemas/character_homework.py @@ -0,0 +1,6 @@ +# app/schemas/character_homework.py + +from pydantic import BaseModel + +class HomeworkCompletionUpdateRequest(BaseModel): + complete_cnt: int diff --git a/app/schemas/dashboard.py b/app/schemas/dashboard.py new file mode 100644 index 0000000..ab76ccd --- /dev/null +++ b/app/schemas/dashboard.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + +class DashboardCharacter(BaseModel): + character_id: int + character_name: str + server: str + +class DashboardHomework(BaseModel): + homework_id: int + title: str + reset_type: str + clear_count: int + complete_cnt: int \ No newline at end of file diff --git a/app/schemas/homework.py b/app/schemas/homework.py new file mode 100644 index 0000000..9af5e8b --- /dev/null +++ b/app/schemas/homework.py @@ -0,0 +1,31 @@ +# app/schemas/homework.py + +from pydantic import BaseModel +from datetime import time, datetime +from typing import Optional + +class HomeworkTypeCreate(BaseModel): + title: str + description: Optional[str] = None + reset_type: str + reset_time: Optional[time] = None + clear_count: Optional[int] = 0 + +class HomeworkTypeResponse(BaseModel): + id: int + title: str + description: Optional[str] + reset_type: str + reset_time: time + clear_count: int + created_at: datetime + + class Config: + orm_mode = True + +class HomeworkSelectableResponse(BaseModel): + homework_id: int + title: str + assigned: str # 'Y' or 'N' + reset_type: str + clear_count: int \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..8097afd --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,18 @@ +# app/schemas/user.py + +from pydantic import BaseModel, EmailStr +from datetime import datetime + +# 사용자 생성 요청용 (회원가입) +class UserCreate(BaseModel): + email: EmailStr + password: str + +# 사용자 응답용 +class UserResponse(BaseModel): + id: int + email: EmailStr + created_at: datetime + + class Config: + orm_mode = True diff --git a/app/services/character_homework_service.py b/app/services/character_homework_service.py new file mode 100644 index 0000000..06925af --- /dev/null +++ b/app/services/character_homework_service.py @@ -0,0 +1,152 @@ +from sqlalchemy import select, case +from fastapi import HTTPException +from sqlalchemy.orm import Session +from datetime import datetime +from app.models.character import CharacterHomework +from app.models.character import Character +from app.models.homework import HomeworkType + +def get_homeworks_with_assignment_status(db: Session, user_id: int, character_id: int): + stmt = ( + select( + HomeworkType.id.label("homework_id"), + HomeworkType.title, + case( + (CharacterHomework.id != None, 'Y'), + else_='N' + ).label("assigned"), + HomeworkType.reset_type, + HomeworkType.clear_count + ) + .outerjoin( + CharacterHomework, + (HomeworkType.id == CharacterHomework.homework_type_id) & + (CharacterHomework.character_id == character_id) + ) + .where(HomeworkType.user_id == user_id) + .order_by(HomeworkType.id) + ) + + return db.execute(stmt).mappings().all() + +def assign_homework_to_character(db: Session, user_id: int, character_id: int, homework_type_id: int): + # 캐릭터/숙제 소유자 확인 + character = db.query(Character).filter_by(id=character_id, user_id=user_id).first() + if not character: + raise HTTPException(status_code=404, detail="캐릭터가 없습니다.") + + homework = db.query(HomeworkType).filter_by(id=homework_type_id, user_id=user_id).first() + if not homework: + raise HTTPException(status_code=404, detail="숙제가 없습니다.") + + # 중복 확인 + exists = db.query(CharacterHomework).filter_by( + character_id=character_id, + homework_type_id=homework_type_id + ).first() + if exists: + raise HTTPException(status_code=400, detail="이미 지정된 숙제입니다.") + + # INSERT + ch = CharacterHomework( + character_id=character_id, + homework_type_id=homework_type_id + ) + db.add(ch) + db.commit() + db.refresh(ch) + return {"message": "숙제가 지정되었습니다."} + + +def unassign_homework_from_character(db: Session, user_id: int, character_id: int, homework_type_id: int): + # 소유 확인 + 존재 여부 + ch = ( + db.query(CharacterHomework) + .join(Character) + .filter( + Character.id == character_id, + Character.user_id == user_id, + CharacterHomework.homework_type_id == homework_type_id + ) + .first() + ) + if not ch: + raise HTTPException(status_code=404, detail="해당 숙제가 지정되어 있지 않습니다.") + + db.delete(ch) + db.commit() + return {"message": "숙제가 해제되었습니다."} + +def update_homework_completion( + db: Session, + user_id: int, + character_id: int, + homework_type_id: int, + new_complete_cnt: int +): + character = db.query(Character).filter_by(id=character_id, user_id=user_id).first() + if not character: + raise HTTPException(status_code=404, detail="캐릭터가 없습니다.") + + homework = db.query(HomeworkType).filter_by(id=homework_type_id, user_id=user_id).first() + if not homework: + raise HTTPException(status_code=404, detail="숙제를 찾을 수 없습니다.") + + ch = db.query(CharacterHomework).filter_by( + character_id=character_id, + homework_type_id=homework_type_id + ).first() + + if not ch: + raise HTTPException(status_code=404, detail="지정된 숙제가 없습니다.") + + if new_complete_cnt > homework.clear_count: + raise HTTPException(status_code=400, detail="완료 횟수가 숙제 클리어 기준을 초과했습니다.") + + # 공통 업데이트 + ch.complete_cnt = new_complete_cnt + ch.last_completed_at = datetime.utcnow() + ch.updated_at = datetime.utcnow() + + # 완료 처리 + if new_complete_cnt == homework.clear_count: + ch.is_done = True + else: + ch.is_done = False + + db.commit() + return {"message": "숙제 완료 상태가 업데이트되었습니다."} + +def update_homework_completion( + db: Session, + user_id: int, + character_id: int, + homework_type_id: int, + new_complete_cnt: int +): + character = db.query(Character).filter_by(id=character_id, user_id=user_id).first() + if not character: + raise HTTPException(status_code=404, detail="캐릭터가 없습니다.") + + homework = db.query(HomeworkType).filter_by(id=homework_type_id, user_id=user_id).first() + if not homework: + raise HTTPException(status_code=404, detail="숙제를 찾을 수 없습니다.") + + ch = db.query(CharacterHomework).filter_by( + character_id=character_id, + homework_type_id=homework_type_id + ).first() + + if not ch: + raise HTTPException(status_code=404, detail="지정된 숙제가 없습니다.") + + if new_complete_cnt > homework.clear_count: + raise HTTPException(status_code=400, detail="완료 횟수가 숙제 클리어 기준을 초과했습니다.") + + ch.complete_cnt = new_complete_cnt + ch.last_completed_at = datetime.utcnow() + ch.updated_at = datetime.utcnow() + ch.is_done = (new_complete_cnt == homework.clear_count) + + db.commit() + return {"message": "숙제 완료 상태가 업데이트되었습니다."} \ No newline at end of file diff --git a/app/services/dashboard_service.py b/app/services/dashboard_service.py new file mode 100644 index 0000000..31b87bd --- /dev/null +++ b/app/services/dashboard_service.py @@ -0,0 +1,55 @@ +from sqlalchemy.orm import Session +from fastapi import HTTPException +from app.models.character import Character, CharacterHomework +from app.models.homework import HomeworkType +from app.schemas.dashboard import DashboardCharacter, DashboardHomework + + +def get_dashboard_characters(db: Session, user_id: int): + subq = db.query(CharacterHomework.character_id).distinct().subquery() + + rows = ( + db.query(Character.id.label("character_id"), Character.name.label("character_name"), Character.server) + .join(subq, Character.id == subq.c.character_id) + .filter(Character.user_id == user_id) + .order_by(Character.id) + .all() + ) + + return [ + DashboardCharacter( + character_id=row[0], + character_name=row[1], + server=row[2] + ) for row in rows + ] + + +def get_dashboard_homeworks_for_character(db: Session, user_id: int, character_id: int): + character = db.query(Character).filter_by(id=character_id, user_id=user_id).first() + if not character: + raise HTTPException(status_code=404, detail="캐릭터를 찾을 수 없습니다.") + + rows = ( + db.query( + HomeworkType.id.label("homework_id"), + HomeworkType.title, + HomeworkType.reset_type, + HomeworkType.clear_count, + CharacterHomework.complete_cnt + ) + .join(CharacterHomework, CharacterHomework.homework_type_id == HomeworkType.id) + .filter(CharacterHomework.character_id == character_id) + .order_by(HomeworkType.id) + .all() + ) + + return [ + DashboardHomework( + homework_id=row[0], + title=row[1], + reset_type=row[2], + clear_count=row[3], + complete_cnt=row[4] + ) for row in rows + ] diff --git a/create_db.py b/create_db.py new file mode 100644 index 0000000..d690f6a --- /dev/null +++ b/create_db.py @@ -0,0 +1,12 @@ +# create_db.py + +from app.core.config import Base +from app.core.database import engine +from app.models.user import User +from app.models.character import Character +from app.models.homework import HomeworkType, CharacterHomework + +print("📦 DB 테이블 생성 중...") +Base.metadata.create_all(bind=engine) +print("✅ 완료: sukjenogi.db에 테이블 생성됨.") + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d742337 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.8" + +services: + web: + build: . + container_name: sukjenogi-backend + ports: + - "8000:8000" + volumes: + - .:/app + environment: + - DATABASE_URL=postgresql://sukje_user:sukje_pass@host.docker.internal:5432/sukjenogi_db +volumes: + postgres_data: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e8e8c93 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn[standard] +SQLAlchemy +psycopg2-binary +alembic +python-jose[cryptography] +passlib[bcrypt] +python-dotenv +pydantic[email] +pydantic-settings \ No newline at end of file diff --git a/run_reset_homeworks.bat b/run_reset_homeworks.bat new file mode 100644 index 0000000..580b294 --- /dev/null +++ b/run_reset_homeworks.bat @@ -0,0 +1,5 @@ +@echo off +cd /d C:\sukjenogi +set PYTHONPATH=C:\sukjenogi +call venv\Scripts\activate.bat +python scripts\reset_homeworks.py >> logs\reset.log 2>&1 diff --git a/scripts/reset_homeworks.py b/scripts/reset_homeworks.py new file mode 100644 index 0000000..68fc8d5 --- /dev/null +++ b/scripts/reset_homeworks.py @@ -0,0 +1,40 @@ +import datetime +from dotenv import load_dotenv # 👈 추가 +load_dotenv() # 👈 반드시 최상단에서 실행 + +from sqlalchemy.orm import Session +from app.core.database import SessionLocal +from app.models.user import User +from app.models.character import CharacterHomework +from app.models.homework import HomeworkType + +def reset_homeworks(): + db: Session = SessionLocal() + now = datetime.datetime.now() + weekday = now.weekday() # 0: 월요일 + day = now.day # 1: 매월 1일 + + reset_targets = ["daily"] + if weekday == 0: + reset_targets.append("weekly") + if day == 1: + reset_targets.append("monthly") + + # 초기화 대상 조회 + character_homeworks = ( + db.query(CharacterHomework) + .join(HomeworkType, CharacterHomework.homework_type_id == HomeworkType.id) + .filter(HomeworkType.reset_type.in_(reset_targets)) + .all() + ) + + for ch in character_homeworks: + ch.is_done = False + ch.complete_cnt = 0 + + db.commit() + db.close() + print(f"[{now}] 초기화 완료: {len(character_homeworks)}건 처리됨") + +if __name__ == "__main__": + reset_homeworks()