친구 작업중

This commit is contained in:
SR07 2025-06-09 16:24:42 +09:00
parent 61c484ffd0
commit a240562564
11 changed files with 472 additions and 30 deletions

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

@ -0,0 +1,80 @@
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
from app.schemas.character import CharacterResponse
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[int])
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.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

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

@ -6,38 +6,39 @@ from app.core.deps import get_current_user
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordBearer
import traceback
from app.models import user, friend, character, homework
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():

View File

@ -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"

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

@ -0,0 +1,43 @@
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
from app.models.user import User
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", ondelete="CASCADE"), nullable=False)
to_user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
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])
to_user = relationship(User, foreign_keys=[to_user_id])
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])
user2 = relationship(User, foreign_keys=[user_id_2])

View File

@ -1,4 +1,4 @@
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
@ -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,4 +1,5 @@
# app/models/user.py
from pydantic import BaseModel
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
@ -15,4 +16,4 @@ class User(Base):
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
characters = relationship("Character", back_populates="user")
homework_types = relationship("HomeworkType", back_populates="user", cascade="all, delete")
homework_types = relationship("HomeworkType", back_populates="user", cascade="all, delete")

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

@ -0,0 +1,36 @@
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
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

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

@ -0,0 +1,174 @@
from sqlalchemy.orm import Session
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):
return db.query(FriendRequest).filter(
FriendRequest.to_user_id == user_id,
FriendRequest.status == FriendRequestStatus.pending
).all()
def get_sent_requests(db: Session, user_id: int):
return db.query(FriendRequest).filter(
FriendRequest.from_user_id == user_id,
FriendRequest.status == FriendRequestStatus.pending
).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
result.append(friend_id)
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. 공개된 숙제만 조회
results = db.query(HomeworkType).join(CharacterHomework).filter(
CharacterHomework.character_id == character_id,
HomeworkType.is_public == True
).all()
return results
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
}