chlchzjalt
This commit is contained in:
commit
accf0ebc4b
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# 가상환경
|
||||
venv/
|
||||
|
||||
# Python 캐시
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# 환경변수 파일
|
||||
.env
|
||||
|
||||
# SQLite 등 테스트 DB 파일
|
||||
*.sqlite3
|
||||
|
||||
# 로그
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# IDE 설정
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# OS별 무시
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@ -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"]
|
||||
52
app/api/auth.py
Normal file
52
app/api/auth.py
Normal file
@ -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}
|
||||
62
app/api/character.py
Normal file
62
app/api/character.py
Normal file
@ -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)
|
||||
43
app/api/character_homework.py
Normal file
43
app/api/character_homework.py
Normal file
@ -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
|
||||
)
|
||||
26
app/api/dashboard.py
Normal file
26
app/api/dashboard.py
Normal file
@ -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)
|
||||
39
app/api/homework.py
Normal file
39
app/api/homework.py
Normal file
@ -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)
|
||||
27
app/api/user.py
Normal file
27
app/api/user.py
Normal file
@ -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
|
||||
19
app/core/config.py
Normal file
19
app/core/config.py
Normal file
@ -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()
|
||||
|
||||
19
app/core/database.py
Normal file
19
app/core/database.py
Normal file
@ -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)
|
||||
40
app/core/deps.py
Normal file
40
app/core/deps.py
Normal file
@ -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
|
||||
30
app/core/security.py
Normal file
30
app/core/security.py
Normal file
@ -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
|
||||
|
||||
43
app/crud/character.py
Normal file
43
app/crud/character.py
Normal file
@ -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()
|
||||
18
app/crud/homework.py
Normal file
18
app/crud/homework.py
Normal file
@ -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()
|
||||
20
app/crud/user.py
Normal file
20
app/crud/user.py
Normal file
@ -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
|
||||
38
app/main.py
Normal file
38
app/main.py
Normal file
@ -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": "숙제노기 서버가 잘 작동 중입니다."}
|
||||
|
||||
40
app/models/character.py
Normal file
40
app/models/character.py
Normal file
@ -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")
|
||||
20
app/models/homework.py
Normal file
20
app/models/homework.py
Normal file
@ -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")
|
||||
17
app/models/user.py
Normal file
17
app/models/user.py
Normal file
@ -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")
|
||||
25
app/schemas/character.py
Normal file
25
app/schemas/character.py
Normal file
@ -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
|
||||
6
app/schemas/character_homework.py
Normal file
6
app/schemas/character_homework.py
Normal file
@ -0,0 +1,6 @@
|
||||
# app/schemas/character_homework.py
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class HomeworkCompletionUpdateRequest(BaseModel):
|
||||
complete_cnt: int
|
||||
13
app/schemas/dashboard.py
Normal file
13
app/schemas/dashboard.py
Normal file
@ -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
|
||||
31
app/schemas/homework.py
Normal file
31
app/schemas/homework.py
Normal file
@ -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
|
||||
18
app/schemas/user.py
Normal file
18
app/schemas/user.py
Normal file
@ -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
|
||||
152
app/services/character_homework_service.py
Normal file
152
app/services/character_homework_service.py
Normal file
@ -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": "숙제 완료 상태가 업데이트되었습니다."}
|
||||
55
app/services/dashboard_service.py
Normal file
55
app/services/dashboard_service.py
Normal file
@ -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
|
||||
]
|
||||
12
create_db.py
Normal file
12
create_db.py
Normal file
@ -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에 테이블 생성됨.")
|
||||
|
||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -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:
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
SQLAlchemy
|
||||
psycopg2-binary
|
||||
alembic
|
||||
python-jose[cryptography]
|
||||
passlib[bcrypt]
|
||||
python-dotenv
|
||||
pydantic[email]
|
||||
pydantic-settings
|
||||
5
run_reset_homeworks.bat
Normal file
5
run_reset_homeworks.bat
Normal file
@ -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
|
||||
40
scripts/reset_homeworks.py
Normal file
40
scripts/reset_homeworks.py
Normal file
@ -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()
|
||||
Loading…
x
Reference in New Issue
Block a user