chlchzjalt

This commit is contained in:
biryu2000 2025-05-25 00:38:12 +09:00
commit accf0ebc4b
31 changed files with 968 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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)

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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

View 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": "숙제 완료 상태가 업데이트되었습니다."}

View 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
View 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
View 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
View 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
View 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

View 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()