Compare commits

...

33 Commits

Author SHA1 Message Date
93d92a0686
Merge pull request #15 from nightbug-xx/q3u13c-codex/add-from_user_email-to-response
Add complete count to friend homework listing
2025-06-11 11:08:00 +09:00
6a3bce8765
Merge branch 'master' into q3u13c-codex/add-from_user_email-to-response 2025-06-11 11:07:41 +09:00
a4b90e8219 Include completion count in friend homework listing 2025-06-11 11:06:01 +09:00
b8a2d02c12
Merge pull request #14 from nightbug-xx/egkxkg-codex/add-from_user_email-to-response
Add friend character homework listing
2025-06-11 10:54:08 +09:00
55c5a1da01 Add endpoint for friend character public homeworks 2025-06-11 10:53:46 +09:00
40ae7dfc4a
Merge pull request #13 from nightbug-xx/bn52dv-codex/add-from_user_email-to-response
Add friend emails to listing
2025-06-11 10:02:00 +09:00
371687a861 Allow toggling public visibility 2025-06-11 10:01:28 +09:00
314e95d00f
Merge pull request #12 from nightbug-xx/x0l35n-codex/add-from_user_email-to-response
Add friend emails to listing
2025-06-10 10:19:42 +09:00
116716f4dc feat: include friend emails in list 2025-06-10 10:19:25 +09:00
392c71f630
Merge pull request #11 from nightbug-xx/codex/add-from_user_email-to-response
Add user emails to friend request listings
2025-06-10 10:12:10 +09:00
06ff916a31 Include user emails in friend request listings 2025-06-10 10:11:43 +09:00
f7ac481314
Merge pull request #10 from nightbug-xx/codex/get_db-함수-삭제-및-임포트-수정
Update auth dependency usage
2025-06-09 18:10:53 +09:00
b7a3f1e3e3
Merge pull request #9 from nightbug-xx/codex/add-newline-to-requirements.txt
Add newline at end of requirements.txt
2025-06-09 18:10:42 +09:00
cb53873952
Merge pull request #8 from nightbug-xx/codex/중복-코드-제거
Fix duplicate order assignment
2025-06-09 18:10:29 +09:00
d3d1b71779
Merge pull request #7 from nightbug-xx/codex/remove-base-from-config.py-or-update-models
Fix Base usage in models
2025-06-09 18:10:19 +09:00
89f7683233
Merge pull request #6 from nightbug-xx/codex/필드-이름을-모델에-맞게-수정
Update homework schema field names
2025-06-09 18:10:05 +09:00
61a4fc9868 refactor: use shared get_db dependency 2025-06-09 18:09:11 +09:00
c946bba67f Add newline at end of requirements.txt 2025-06-09 18:08:57 +09:00
ed041e9f75 Remove duplicated assignment in character order update 2025-06-09 18:08:40 +09:00
cf16128fce refactor: centralize Base definition 2025-06-09 18:08:19 +09:00
810dcda318 Fix homework fields 2025-06-09 18:07:56 +09:00
e0deaaecd5
Merge pull request #4 from nightbug-xx/codex/구문-분리하기-homeworktype,-characterhomework
Split CharacterHomework import
2025-06-09 18:07:19 +09:00
36188d2f8d
Merge pull request #5 from nightbug-xx/codex/변경-character.power---character.combat_power
Fix character update power property
2025-06-09 18:07:08 +09:00
165bf63e2b Update character update endpoint to use combat_power 2025-06-09 18:05:40 +09:00
88401d981d Split CharacterHomework import 2025-06-09 18:05:06 +09:00
SR07
86845c81c5 친구 작업중 2025-06-09 17:53:38 +09:00
SR07
a240562564 친구 작업중 2025-06-09 16:24:42 +09:00
61c484ffd0
Merge pull request #3 from nightbug-xx/codex/add-readme.md-to-repository-root
Add basic README
2025-06-09 16:12:09 +09:00
dac33dd39a Add README with basic usage 2025-06-09 16:11:54 +09:00
75711ab7cb
Merge pull request #2 from nightbug-xx/codex/중복된-sessionlocal-선언-제거
Fix duplicate SessionLocal in database module
2025-06-09 16:09:24 +09:00
670613c174 Remove duplicate SessionLocal declaration 2025-06-09 16:09:08 +09:00
7e1d28bb55
Merge pull request #1 from nightbug-xx/codex/삭제-두-번째-update_homework_completion-정의
Remove duplicate function definition
2025-06-09 16:06:06 +09:00
8a77558439 Remove duplicate update_homework_completion function 2025-06-09 16:05:45 +09:00
25 changed files with 626 additions and 95 deletions

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# SukjeNogi Backend
숙제노기 프로젝트의 백엔드 서버입니다. FastAPI로 작성되었으며 캐릭터별 과제 관리 기능 등을 REST API로 제공합니다.
## 실행 방법
### Docker 사용
```bash
docker build -t sukjenogi-backend .
docker run -d --env-file .env -p 8000:8000 sukjenogi-backend
```
### docker-compose 사용
`docker-compose.yml` 파일에 필요한 환경변수를 정의한 뒤 다음 명령으로 실행할 수 있습니다.
```bash
docker compose up -d --build
```
서버가 시작되면 `http://localhost:8000` 에서 서비스에 접근할 수 있습니다.

View File

@ -2,7 +2,7 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.core.database import SessionLocal
from app.core.deps import get_db
from app.models.user import User
from app.core.security import verify_password, create_access_token
from pydantic import BaseModel, EmailStr
@ -11,14 +11,6 @@ from datetime import timedelta
router = APIRouter()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# 로그인 요청 스키마
class LoginRequest(BaseModel):
email: EmailStr
@ -49,4 +41,4 @@ def check_email_availability(
db: Session = Depends(get_db)
):
user = db.query(User).filter(User.email == email).first()
return {"available": user is None}
return {"available": user is None}

View File

@ -75,7 +75,8 @@ def update_character(
character.name = req.name
character.server = req.server
character.power = req.power
character.combat_power = req.power
character.is_public = req.is_public
db.commit()
return {"message": "캐릭터가 수정되었습니다."}
@ -118,7 +119,6 @@ def update_character_order(
for update in updates:
character = db.query(Character).filter_by(id=update.id, user_id=user.id).first()
if character:
character.order = update.order
character.order = update.order
db.add(character)
db.commit()

104
app/api/friend.py Normal file
View File

@ -0,0 +1,104 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.deps import get_db, get_current_user
from app.schemas.friend import (
FriendRequestCreate,
FriendRequestResponse,
FriendResponse,
FriendListItem,
)
from app.schemas.character import CharacterResponse
from app.schemas.dashboard import DashboardHomework
from app.services import friend_service
from app.models.user import User
router = APIRouter()
@router.post("/request", response_model=FriendRequestResponse)
def send_request(
request_data: FriendRequestCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return friend_service.send_friend_request(db, current_user.id, request_data.to_user_email)
@router.get("/requests/received", response_model=list[FriendRequestResponse])
def get_received_requests(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return friend_service.get_received_requests(db, current_user.id)
@router.get("/requests/sent", response_model=list[FriendRequestResponse])
def get_sent_requests(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return friend_service.get_sent_requests(db, current_user.id)
@router.post("/requests/{request_id}/cancel")
def cancel_sent_request(
request_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
friend_service.cancel_sent_request(db, request_id, current_user.id)
return {"detail": "요청을 취소했습니다."}
@router.post("/requests/{request_id}/respond")
def respond_to_request(
request_id: int,
accept: bool,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
friend_service.respond_to_request(db, request_id, current_user.id, accept)
return {"detail": "요청을 처리했습니다."}
@router.get("/list", response_model=list[FriendListItem])
def get_friend_list(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return friend_service.get_friend_list(db, current_user.id)
@router.get("/{friend_id}/characters", response_model=list[CharacterResponse])
def get_friend_characters(
friend_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return friend_service.get_public_characters_of_friend(db, current_user.id, friend_id)
@router.get(
"/{friend_id}/characters/{character_id}/homeworks",
response_model=list[DashboardHomework],
)
def get_friend_character_homeworks(
friend_id: int,
character_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return friend_service.get_public_homeworks_of_friend_character(
db,
current_user.id,
friend_id,
character_id,
)
@router.delete("/{friend_id}")
def delete_friend(
friend_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
friend_service.delete_friend(db, current_user.id, friend_id)
return {"detail": "친구가 삭제되었습니다."}

View File

@ -24,6 +24,7 @@ def register_homework_type(
reset_type=homework_data.reset_type,
reset_time=homework_data.reset_time or time(6, 0),
clear_count=homework_data.clear_count or 0,
is_public=homework_data.is_public,
created_at=datetime.utcnow(),
)
db.add(homework_type)
@ -52,10 +53,11 @@ def update_homework_type(
if not homework_type or homework_type.user_id != current_user.id:
raise HTTPException(status_code=403, detail="권한이 없습니다.")
homework_type.name = req.name
homework_type.title = req.title
homework_type.description = req.description
homework_type.repeat_type = req.repeat_type
homework_type.repeat_count = req.repeat_count
homework_type.reset_type = req.reset_type
homework_type.clear_count = req.clear_count
homework_type.is_public = req.is_public
db.commit()
return {"message": "숙제가 수정되었습니다."}

View File

@ -1,16 +1,17 @@
# app/api/user.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi.logger import logger
import traceback
import sys
from sqlalchemy.orm import Session
from app.schemas.user import UserCreate, UserResponse, PasswordUpdateRequest
from app.schemas.user import UserCreate, UserResponse, PasswordUpdateRequest, UserPublicInfoResponse, UserByCharacterResponse
from app.crud.user import create_user
from app.models.user import User
from app.core.database import SessionLocal
from app.core.deps import get_current_user
from app.core.security import verify_password, get_password_hash
from app.services import user_service
router = APIRouter()
@ -56,4 +57,20 @@ def update_password(
except Exception as e:
logger.error(f"❌ 비밀번호 변경 중 예외 발생: {e}")
traceback.print_exc(file=sys.stdout) # ← 여기가 핵심
raise
raise
@router.get("/public-info", response_model=UserPublicInfoResponse)
def get_public_info(
email: str = Query(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return user_service.get_user_public_info(db, current_user.id, email)
@router.get("/by-character", response_model=UserByCharacterResponse)
def get_by_character(
server: str,
name: str,
db: Session = Depends(get_db)
):
return user_service.get_user_by_character(db, server, name)

View File

@ -1,7 +1,6 @@
# app/core/config.py
from pydantic_settings import BaseSettings
from sqlalchemy.orm import declarative_base
class Settings(BaseSettings):
database_url: str
@ -14,6 +13,4 @@ class Settings(BaseSettings):
settings = Settings()
# 베이스 클래스
Base = declarative_base()

View File

@ -10,6 +10,8 @@ engine = create_engine(settings.database_url, echo=True, future=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
import app.models # ✅ 그대로 유지
@event.listens_for(Engine, "handle_error")
def receive_handle_error(exception_context):
print("🔥 SQLAlchemy DB 에러 감지!")
@ -22,5 +24,3 @@ def get_db():
finally:
db.close()
# 세션 클래스 생성
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@ -21,6 +21,7 @@ def create_character(user_id: int, character_data: CharacterCreate, db: Session)
server=character_data.server,
job=character_data.job,
combat_power=character_data.combat_power, # ← 수동 입력 허용
is_public=character_data.is_public,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)

View File

@ -5,9 +5,12 @@ 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,
title=data.title,
description=data.description,
reset_type=data.reset_type,
reset_time=data.reset_time,
clear_count=data.clear_count,
is_public=data.is_public,
)
db.add(new_homework)
db.commit()

View File

@ -1,4 +1,5 @@
#pp/main.py
from app.models import User, Character, HomeworkType, Friend, FriendRequest # 👈 명시적 import!
from fastapi import FastAPI, Request, Depends
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
@ -7,37 +8,37 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordBearer
import traceback
from app.api import user, auth, character, homework, character_homework, dashboard
from app.api import user, auth, character, homework, character_homework, dashboard, friend
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 = FastAPI(
docs_url=None,
title="숙제노기 API",
description="마비노기 모바일 숙제 관리용 백엔드 API",
version="0.1.0",
docs_url="/docs",
redoc_url=None,
openapi_url=None
openapi_url="/openapi.json",
root_path="/api"
)
@app.get("/docs", include_in_schema=False)
def custom_docs(user=Depends(get_current_user)):
return get_swagger_ui_html(openapi_url="/openapi.json", title="Sukjenogi API Docs")
@app.get("/openapi.json", include_in_schema=False)
def custom_openapi(user=Depends(get_current_user)):
return get_openapi(
title="Sukjenogi API",
version="0.2",
routes=app.routes
)
# app = FastAPI(
# docs_url=None,
# redoc_url=None,
# openapi_url=None
# )
#
# @app.get("/docs", include_in_schema=False)
# def custom_docs(user=Depends(get_current_user)):
# return get_swagger_ui_html(openapi_url="/openapi.json", title="Sukjenogi API Docs")
#
# @app.get("/openapi.json", include_in_schema=False)
# def custom_openapi(user=Depends(get_current_user)):
# return get_openapi(
# title="Sukjenogi API",
# version="0.2",
# routes=app.routes
# )
@app.middleware("http")
async def log_exceptions_middleware(request: Request, call_next):
@ -57,7 +58,8 @@ origins = [
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
# allow_origins=origins,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -69,6 +71,7 @@ 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.include_router(friend.router, prefix="/friends", tags=["Friends"])
@app.get("/")
def read_root():

5
app/models/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# app/models/__init__.py
from app.models.user import User
from app.models.character import Character
from app.models.homework import HomeworkType
from app.models.friend import Friend, FriendRequest

View File

@ -1,7 +1,7 @@
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
from app.core.database import Base
class Character(Base):
@ -22,6 +22,8 @@ class Character(Base):
order = Column(Integer, default=0)
is_public = Column(Boolean, default=False, nullable=False)
class CharacterHomework(Base):
__tablename__ = "character_homeworks"

41
app/models/friend.py Normal file
View File

@ -0,0 +1,41 @@
from sqlalchemy import Column, Integer, Enum, ForeignKey, DateTime, CheckConstraint, UniqueConstraint
from sqlalchemy.orm import relationship
from datetime import datetime
from app.core.database import Base
import enum
class FriendRequestStatus(enum.Enum):
pending = "pending"
accepted = "accepted"
rejected = "rejected"
cancelled = "cancelled"
class FriendRequest(Base):
__tablename__ = "friend_requests"
id = Column(Integer, primary_key=True, index=True)
from_user_id = Column(Integer, ForeignKey("users.id"))
to_user_id = Column(Integer, ForeignKey("users.id"))
status = Column(Enum(FriendRequestStatus), default=FriendRequestStatus.pending, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 🔽 문자열로 참조
from_user = relationship("User", foreign_keys=[from_user_id], back_populates="sent_requests")
to_user = relationship("User", foreign_keys=[to_user_id], back_populates="received_requests")
class Friend(Base):
__tablename__ = "friends"
id = Column(Integer, primary_key=True, index=True)
user_id_1 = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id_2 = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
__table_args__ = (
UniqueConstraint("user_id_1", "user_id_2", name="unique_friend_pair"),
CheckConstraint("user_id_1 < user_id_2", name="check_user_order"),
)
user1 = relationship("User", foreign_keys=[user_id_1], back_populates="friendships1")
user2 = relationship("User", foreign_keys=[user_id_2], back_populates="friendships2")

View File

@ -1,8 +1,8 @@
from sqlalchemy import Column, Integer, String, Time, ForeignKey, DateTime
from sqlalchemy import Column, Integer, String, Time, ForeignKey, DateTime, Boolean
from sqlalchemy.orm import relationship
from datetime import time, datetime
from app.core.config import Base
from app.core.database import Base
class HomeworkType(Base):
__tablename__ = "homework_types"
@ -20,3 +20,5 @@ class HomeworkType(Base):
assigned_characters = relationship("CharacterHomework", back_populates="homework_type", cascade="all, delete")
order = Column(Integer, default=0)
is_public = Column(Boolean, default=False, nullable=False)

View File

@ -1,9 +1,8 @@
# app/models/user.py
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from datetime import datetime
from app.core.config import Base
from app.core.database import Base
class User(Base):
__tablename__ = "users"
@ -14,5 +13,12 @@ class User(Base):
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
characters = relationship("Character", back_populates="user")
from app.models.character import Character
characters = relationship(Character, back_populates="user")
homework_types = relationship("HomeworkType", back_populates="user", cascade="all, delete")
# 🔽 문자열만 사용하고 foreign_keys 생략 (권장)
sent_requests = relationship("FriendRequest", back_populates="from_user", foreign_keys="FriendRequest.from_user_id")
received_requests = relationship("FriendRequest", back_populates="to_user", foreign_keys="FriendRequest.to_user_id")
friendships1 = relationship("Friend", back_populates="user1", foreign_keys="Friend.user_id_1")
friendships2 = relationship("Friend", back_populates="user2", foreign_keys="Friend.user_id_2")

View File

@ -10,6 +10,7 @@ class CharacterCreate(BaseModel):
server: Optional[str] = None
job: Optional[str] = None
combat_power: Optional[int] = None # ← 추가
is_public: bool = False
# 캐릭터 응답용
class CharacterResponse(BaseModel):
@ -18,6 +19,7 @@ class CharacterResponse(BaseModel):
server: Optional[str]
job: Optional[str]
combat_power: Optional[int] # ← 추가
is_public: bool
auto_synced_at: Optional[datetime]
created_at: datetime
@ -28,6 +30,7 @@ class CharacterUpdateRequest(BaseModel):
name: constr(min_length=1)
server: constr(min_length=1)
power: conint(ge=0) # 0 이상 정수
is_public: bool
class CharacterDetailResponse(BaseModel):
id: int
@ -35,6 +38,7 @@ class CharacterDetailResponse(BaseModel):
server: str
combat_power: int
user_id: int
is_public: bool
created_at: datetime
updated_at: datetime

46
app/schemas/friend.py Normal file
View File

@ -0,0 +1,46 @@
from pydantic import BaseModel
from datetime import datetime
from enum import Enum
class FriendRequestStatus(str, Enum):
pending = "pending"
accepted = "accepted"
rejected = "rejected"
cancelled = "cancelled"
class FriendRequestCreate(BaseModel):
to_user_email: str
class FriendRequestResponse(BaseModel):
id: int
from_user_id: int
to_user_id: int
from_user_email: str | None = None
to_user_email: str | None = None
status: FriendRequestStatus
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class FriendResponse(BaseModel):
id: int
user_id_1: int
user_id_2: int
created_at: datetime
class Config:
orm_mode = True
class FriendListItem(BaseModel):
id: int
email: str
class Config:
orm_mode = True

View File

@ -10,6 +10,7 @@ class HomeworkTypeCreate(BaseModel):
reset_type: str
reset_time: Optional[time] = None
clear_count: Optional[int] = 0
is_public: bool = False
class HomeworkTypeResponse(BaseModel):
id: int
@ -18,6 +19,7 @@ class HomeworkTypeResponse(BaseModel):
reset_type: str
reset_time: time
clear_count: int
is_public: bool
created_at: datetime
class Config:
@ -31,10 +33,11 @@ class HomeworkSelectableResponse(BaseModel):
clear_count: int
class HomeworkTypeUpdateRequest(BaseModel):
name: constr(min_length=1)
title: constr(min_length=1)
description: str | None = None
repeat_type: constr(min_length=1)
repeat_count: conint(ge=1)
reset_type: constr(min_length=1)
clear_count: conint(ge=1)
is_public: bool
class HomeworkTypeDetailResponse(BaseModel):
id: int
@ -44,6 +47,7 @@ class HomeworkTypeDetailResponse(BaseModel):
reset_type: str
reset_time: time
clear_count: int
is_public: bool
created_at: datetime
model_config = {

View File

@ -20,3 +20,19 @@ class PasswordUpdateRequest(BaseModel):
class Config:
orm_mode = True
class UserPublicInfoResponse(BaseModel):
id: int
email: str
is_friend: bool
request_sent: bool
request_received: bool
class UserByCharacterResponse(BaseModel):
user_id: int
email: str
character_id: int
character_name: str
server: str
is_public: bool

View File

@ -117,36 +117,3 @@ def update_homework_completion(
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,232 @@
from sqlalchemy.orm import Session, aliased
from sqlalchemy import select
from app.models.friend import FriendRequest, Friend, FriendRequestStatus
from app.models.user import User
from app.models.character import Character, CharacterHomework
from app.models.homework import HomeworkType
from fastapi import HTTPException, status
from datetime import datetime
def send_friend_request(db: Session, from_user_id: int, to_user_email: str):
to_user = db.query(User).filter(User.email == to_user_email).first()
if not to_user:
raise HTTPException(status_code=404, detail="해당 이메일의 유저를 찾을 수 없습니다.")
if to_user.id == from_user_id:
raise HTTPException(status_code=400, detail="자기 자신에게 친구 요청을 보낼 수 없습니다.")
# 이미 친구인지 확인
user_ids = sorted([from_user_id, to_user.id])
existing_friend = db.query(Friend).filter(
Friend.user_id_1 == user_ids[0],
Friend.user_id_2 == user_ids[1]
).first()
if existing_friend:
raise HTTPException(status_code=400, detail="이미 친구 상태입니다.")
# 이미 pending 요청이 있는지 확인
existing_request = db.query(FriendRequest).filter(
FriendRequest.from_user_id == from_user_id,
FriendRequest.to_user_id == to_user.id,
FriendRequest.status == FriendRequestStatus.pending
).first()
if existing_request:
raise HTTPException(status_code=400, detail="이미 친구 요청을 보낸 상태입니다.")
friend_request = FriendRequest(
from_user_id=from_user_id,
to_user_id=to_user.id,
)
db.add(friend_request)
db.commit()
db.refresh(friend_request)
return friend_request
def get_received_requests(db: Session, user_id: int):
sender = aliased(User)
receiver = aliased(User)
stmt = (
select(
FriendRequest.id,
FriendRequest.from_user_id,
FriendRequest.to_user_id,
sender.email.label("from_user_email"),
receiver.email.label("to_user_email"),
FriendRequest.status,
FriendRequest.created_at,
FriendRequest.updated_at,
)
.join(sender, FriendRequest.from_user_id == sender.id)
.join(receiver, FriendRequest.to_user_id == receiver.id)
.where(
FriendRequest.to_user_id == user_id,
FriendRequest.status == FriendRequestStatus.pending,
)
)
return db.execute(stmt).mappings().all()
def get_sent_requests(db: Session, user_id: int):
sender = aliased(User)
receiver = aliased(User)
stmt = (
select(
FriendRequest.id,
FriendRequest.from_user_id,
FriendRequest.to_user_id,
sender.email.label("from_user_email"),
receiver.email.label("to_user_email"),
FriendRequest.status,
FriendRequest.created_at,
FriendRequest.updated_at,
)
.join(sender, FriendRequest.from_user_id == sender.id)
.join(receiver, FriendRequest.to_user_id == receiver.id)
.where(
FriendRequest.from_user_id == user_id,
FriendRequest.status == FriendRequestStatus.pending,
)
)
return db.execute(stmt).mappings().all()
def cancel_sent_request(db: Session, request_id: int, user_id: int):
request = db.query(FriendRequest).filter(
FriendRequest.id == request_id,
FriendRequest.from_user_id == user_id
).first()
if not request:
raise HTTPException(status_code=404, detail="요청을 찾을 수 없습니다.")
request.status = FriendRequestStatus.cancelled
request.updated_at = datetime.utcnow()
db.commit()
def respond_to_request(db: Session, request_id: int, user_id: int, accept: bool):
request = db.query(FriendRequest).filter(
FriendRequest.id == request_id,
FriendRequest.to_user_id == user_id,
FriendRequest.status == FriendRequestStatus.pending
).first()
if not request:
raise HTTPException(status_code=404, detail="요청을 찾을 수 없습니다.")
request.status = FriendRequestStatus.accepted if accept else FriendRequestStatus.rejected
request.updated_at = datetime.utcnow()
# 친구 수락 시 friends 테이블에도 저장
if accept:
user_ids = sorted([request.from_user_id, request.to_user_id])
friend = Friend(
user_id_1=user_ids[0],
user_id_2=user_ids[1]
)
db.add(friend)
db.commit()
def get_friend_list(db: Session, user_id: int):
friends = db.query(Friend).filter(
(Friend.user_id_1 == user_id) | (Friend.user_id_2 == user_id)
).all()
result = []
for f in friends:
friend_id = f.user_id_2 if f.user_id_1 == user_id else f.user_id_1
friend = db.query(User).filter(User.id == friend_id).first()
friend_email = friend.email if friend else None
result.append({"id": friend_id, "email": friend_email})
return result
def get_public_characters_of_friend(db: Session, current_user_id: int, friend_id: int):
# 친구 관계 확인
user_ids = sorted([current_user_id, friend_id])
is_friend = db.query(Friend).filter(
Friend.user_id_1 == user_ids[0],
Friend.user_id_2 == user_ids[1]
).first()
if not is_friend:
raise HTTPException(status_code=403, detail="친구가 아닙니다.")
# 공개된 캐릭터 목록
characters = db.query(Character).filter(
Character.user_id == friend_id,
Character.is_public == True
).all()
return characters
def get_public_homeworks_of_friend_character(
db: Session,
current_user_id: int,
friend_id: int,
character_id: int
):
# 1. 친구인지 확인
user_ids = sorted([current_user_id, friend_id])
is_friend = db.query(Friend).filter(
Friend.user_id_1 == user_ids[0],
Friend.user_id_2 == user_ids[1]
).first()
if not is_friend:
raise HTTPException(status_code=403, detail="친구가 아닙니다.")
# 2. 해당 캐릭터가 공개인지 확인
character = db.query(Character).filter(
Character.id == character_id,
Character.user_id == friend_id,
Character.is_public == True
).first()
if not character:
raise HTTPException(status_code=404, detail="공개된 캐릭터를 찾을 수 없습니다.")
# 3. 공개된 숙제만 조회하며 완료 횟수도 포함
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,
HomeworkType.is_public == True,
)
.order_by(HomeworkType.order.asc())
.all()
)
return [
{
"homework_id": row[0],
"title": row[1],
"reset_type": row[2],
"clear_count": row[3],
"complete_cnt": row[4],
}
for row in rows
]
def delete_friend(db: Session, user_id: int, friend_id: int):
user_ids = sorted([user_id, friend_id])
friend = db.query(Friend).filter(
Friend.user_id_1 == user_ids[0],
Friend.user_id_2 == user_ids[1]
).first()
if not friend:
raise HTTPException(status_code=404, detail="친구 관계가 존재하지 않습니다.")
db.delete(friend)
db.commit()

View File

@ -0,0 +1,68 @@
from fastapi import HTTPException
from sqlalchemy.orm import Session
from app.models.user import User
from app.models.friend import Friend, FriendRequest, FriendRequestStatus
from app.models.character import Character
def get_user_public_info(
db: Session,
current_user_id: int,
target_email: str
):
target_user = db.query(User).filter(User.email == target_email).first()
if not target_user:
raise HTTPException(status_code=404, detail="해당 유저를 찾을 수 없습니다.")
if target_user.id == current_user_id:
raise HTTPException(status_code=400, detail="자기 자신은 검색할 수 없습니다.")
user_ids = sorted([current_user_id, target_user.id])
is_friend = db.query(Friend).filter(
Friend.user_id_1 == user_ids[0],
Friend.user_id_2 == user_ids[1]
).first() is not None
request_sent = db.query(FriendRequest).filter(
FriendRequest.from_user_id == current_user_id,
FriendRequest.to_user_id == target_user.id,
FriendRequest.status == FriendRequestStatus.pending
).first() is not None
request_received = db.query(FriendRequest).filter(
FriendRequest.from_user_id == target_user.id,
FriendRequest.to_user_id == current_user_id,
FriendRequest.status == FriendRequestStatus.pending
).first() is not None
return {
"id": target_user.id,
"email": target_user.email,
"is_friend": is_friend,
"request_sent": request_sent,
"request_received": request_received,
}
def get_user_by_character(
db: Session,
server: str,
name: str
):
character = db.query(Character).filter(
Character.server == server,
Character.name == name
).first()
if not character:
raise HTTPException(status_code=404, detail="캐릭터를 찾을 수 없습니다.")
user = db.query(User).filter(User.id == character.user_id).first()
return {
"user_id": user.id,
"email": user.email,
"character_id": character.id,
"character_name": character.name,
"server": character.server,
"is_public": character.is_public
}

View File

@ -1,10 +1,10 @@
# create_db.py
from app.core.config import Base
from app.core.database import engine
from app.core.database import Base, engine
from app.models.user import User
from app.models.character import Character
from app.models.homework import HomeworkType, CharacterHomework
from app.models.homework import HomeworkType
from app.models.character import CharacterHomework
print("📦 DB 테이블 생성 중...")
Base.metadata.create_all(bind=engine)

View File

@ -7,4 +7,4 @@ python-jose[cryptography]
passlib[bcrypt]
python-dotenv
pydantic[email]
pydantic-settings
pydantic-settings