This commit is contained in:
암냥 2025-09-13 16:18:28 +09:00
commit 40266cc6e5
191 changed files with 5022 additions and 0 deletions

12
Backend/__init__.py Normal file
View file

@ -0,0 +1,12 @@
from .router.router import router
import sys
from pathlib import Path
from Backend.utils.run_server import init_FastAPI
# 프로젝트 루트를 Python 경로에 추가
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
app = init_FastAPI()
app.include_router(router)

78
Backend/core/security.py Normal file
View file

@ -0,0 +1,78 @@
from jose import jwt, JWTError
from datetime import datetime, timedelta
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional
ACCESS_TOKEN_EXPIRE_MINUTES = 30
SECRET_KEY = "WIXYAhAfU6tLOloxqHgI4thAAo6kshkK"
ALGORITHM = "HS256"
security = HTTPBearer()
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.now() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> Optional[dict]:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
from ..schemas.user import User
from ..utils.db import fetch_one
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = verify_token(credentials.credentials)
if payload is None:
raise credentials_exception
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
# 데이터베이스에서 사용자 조회
user_row = await fetch_one("SELECT * FROM users WHERE username = ?", (username,))
if user_row is None:
raise credentials_exception
user = User(
id=user_row["id"],
username=user_row["username"],
email=user_row["email"],
password_hash=user_row["password_hash"],
salt=user_row["salt"],
created_at=user_row["created_at"],
profile_image_path=user_row["profile_image_path"],
is_active=user_row["is_active"],
)
return user
async def get_current_active_user(current_user=Depends(get_current_user)):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user

View file

@ -0,0 +1,49 @@
from fastapi import APIRouter, HTTPException, Depends
from ...schemas.avatar import (
AvatarUpdate,
AvatarResponse,
AvatarOptions,
)
from ...services.avatar_service import AvatarService
from ...core.security import get_current_user
from ...schemas.user import User
router = APIRouter(prefix="/avatar", tags=["avatar"])
avatar_service = AvatarService()
@router.get("", response_model=AvatarResponse)
async def get_my_avatar(
current_user: User = Depends(get_current_user),
) -> AvatarResponse:
try:
avatar = await avatar_service.get_or_create_avatar(current_user.id)
return avatar.to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("", response_model=AvatarResponse)
async def update_avatar(
avatar_data: AvatarUpdate, current_user: User = Depends(get_current_user)
) -> AvatarResponse:
try:
avatar = await avatar_service.update_avatar(current_user.id, avatar_data)
return avatar.to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/options", response_model=AvatarOptions)
async def get_avatar_options() -> AvatarOptions:
try:
return await avatar_service.get_avatar_options()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{user_id}", response_model=AvatarResponse)
async def get_avatar_by_userId(user_id: int) -> AvatarResponse:
avatar = await avatar_service.get_avatar_by_userId(user_id)
if not avatar:
raise HTTPException(status_code=404, detail="Avatar not found")
return avatar.to_response()

View file

@ -0,0 +1,102 @@
from fastapi import (
APIRouter,
HTTPException,
Depends,
UploadFile,
File,
)
from typing import List, Optional
from ...schemas.diary import DiaryCreate, DiaryUpdate, DiaryResponse, Diary
from ...services.diary_service import DiaryService
from ...core.security import get_current_user
from ...schemas.user import User
router = APIRouter(prefix="/diary", tags=["diary"])
diary_service = DiaryService()
@router.post("", response_model=DiaryResponse)
async def create_diary(
diary_data: DiaryCreate = Depends(DiaryCreate.as_form),
file: List[UploadFile] = File(default=None),
current_user: User = Depends(get_current_user),
) -> DiaryResponse:
try:
diary = await diary_service.create_diary(current_user.id, diary_data, file)
return diary.to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("", response_model=List[DiaryResponse])
async def get_user_diaries(
skip: int = 0,
limit: int = 20,
category: Optional[str] = None,
current_user: User = Depends(get_current_user),
) -> List[DiaryResponse]:
try:
diaries = await diary_service.get_user_diaries(
current_user.id, skip, limit, category
)
return [diary.to_response() for diary in diaries]
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/w/{target_user_id}", response_model=List[DiaryResponse])
async def get_target_user_diaries(
target_user_id: int,
skip: int = 0,
limit: int = 20,
category: Optional[str] = None,
) -> List[DiaryResponse]:
try:
diaries = await diary_service.get_user_diaries(
target_user_id, skip, limit, category
)
return [diary.to_response() for diary in diaries]
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{diary_id}", response_model=DiaryResponse)
async def get_diary(diary_id: int) -> DiaryResponse:
try:
diary = await diary_service.get_diary_by_id(diary_id)
if not diary:
raise HTTPException(status_code=404, detail="Diary not found")
return diary.to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{diary_id}", response_model=DiaryResponse)
async def update_diary(
diary_id: int,
diary_data: DiaryUpdate = Depends(DiaryUpdate.as_form),
file: List[UploadFile] = File(default=None),
current_user: User = Depends(get_current_user),
) -> DiaryResponse:
try:
diary = await diary_service.update_diary(
diary_id, current_user.id, diary_data, file
)
if not diary:
raise HTTPException(status_code=404, detail="Diary not found")
return diary.to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/{diary_id}")
async def delete_diary(
diary_id: int, current_user: User = Depends(get_current_user)
) -> dict:
try:
success = await diary_service.delete_diary(diary_id, current_user.id)
if not success:
raise HTTPException(status_code=404, detail="Diary not found")
return {"message": "Diary deleted successfully"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View file

@ -0,0 +1,81 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Optional
from ...schemas.friendship import (
FriendshipRequest,
FriendshipResponse,
)
from ...services.friendship_service import FriendshipService
from ...core.security import get_current_user
from ...schemas.user import User
router = APIRouter(prefix="/friendship", tags=["friendship"])
friendship_service = FriendshipService()
@router.post("/request", response_model=FriendshipResponse)
async def send_friendship_request(
request_data: FriendshipRequest, current_user: User = Depends(get_current_user)
) -> FriendshipResponse:
try:
friendship = await friendship_service.send_friendship_request(
current_user.id, request_data.friend_username
)
return friendship
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{friendship_id}/accept", response_model=FriendshipResponse)
async def accept_friendship_request(
friendship_id: int, current_user: User = Depends(get_current_user)
) -> FriendshipResponse:
try:
friendship = await friendship_service.accept_friendship_request(
friendship_id, current_user.id
)
if not friendship:
raise HTTPException(status_code=404, detail="Friendship request not found")
return friendship
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("", response_model=List[FriendshipResponse])
async def get_friendships(
status: Optional[str] = None, current_user: User = Depends(get_current_user)
) -> List[FriendshipResponse]:
try:
friendships = await friendship_service.get_user_friendships(
current_user.id, status
)
return friendships
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/{friendship_id}")
async def delete_friendship(
friendship_id: int, current_user: User = Depends(get_current_user)
) -> dict:
try:
success = await friendship_service.delete_friendship(
friendship_id, current_user.id
)
if not success:
raise HTTPException(status_code=404, detail="Friendship not found")
return {"message": "Friendship deleted successfully"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/pending", response_model=List[FriendshipResponse])
async def get_pending_requests(
current_user: User = Depends(get_current_user),
) -> List[FriendshipResponse]:
try:
pending_requests = await friendship_service.get_pending_requests(
current_user.id
)
return pending_requests
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View file

@ -0,0 +1,63 @@
from typing import List
from fastapi import APIRouter, HTTPException, Depends, status
from Backend.core.security import get_current_user
from Backend.schemas.guestbook import (
GuestBookCreate,
GuestbookUpdate,
GuestbookResponse,
)
from Backend.schemas.user import User
from Backend.services.guestbook_service import GuestbookService
router = APIRouter(prefix="/guestbook", tags=["guestbook"])
guestbook_service = GuestbookService()
@router.post("", status_code=status.HTTP_201_CREATED, response_model=GuestbookResponse)
async def create_guestbook(
guestbook: GuestBookCreate, current_user: User = Depends(get_current_user)
) -> GuestbookResponse:
try:
response = await guestbook_service.create_guestbook(guestbook, current_user)
return response
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{target_user_id}", response_model=List[GuestbookResponse])
async def get_guestbook(target_user_id: int):
try:
response = await guestbook_service.get_target_user_guestbooks(target_user_id)
response.reverse()
return response
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{id}", response_model=GuestbookResponse)
async def update_guestbook(
id: int,
guestbook: GuestbookUpdate,
) -> GuestbookResponse:
try:
response = await guestbook_service.update_guestbook_by_id(id, guestbook.content)
return response
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/{id}")
async def delete_guestbook(
id: int,
user: User = Depends(get_current_user),
) -> dict:
try:
is_success = await guestbook_service.delete_guestbook_by_id(id, user.id)
if is_success:
return {"detail": "success"}
else:
raise HTTPException(status_code=404, detail="guestbook not found")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View file

@ -0,0 +1,79 @@
# from fastapi import APIRouter, HTTPException, Depends
#
# from ...schemas.letter import LetterCreate, LetterResponse, EmailRequest
# from ...services.letter_service import LetterService
# from ...core.security import get_current_user
# from ...schemas.user import User
#
# router = APIRouter(prefix="/letter", tags=["letter"])
# letter_service = LetterService()
#
#
# @router.post("", response_model=LetterResponse)
# async def create_letter(
# letter_data: LetterCreate,
# current_user: User = Depends(get_current_user),
# ) -> LetterResponse:
# try:
# letter = await letter_service.create_letter(current_user.id, letter_data)
# return letter.to_response()
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.get("/{letter_id}", response_model=LetterResponse)
# async def get_letter(
# letter_id: int, current_user: User = Depends(get_current_user)
# ) -> LetterResponse:
# try:
# letter = await letter_service.get_letter_by_id(letter_id, current_user.id)
# if not letter:
# raise HTTPException(status_code=404, detail="Letter not found")
# return letter.to_response()
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.delete("/{letter_id}")
# async def delete_letter(letter_id: int, current_user: User = Depends(get_current_user)):
# try:
# is_success = await letter_service.delete_letter(letter_id, current_user.id)
# if not is_success:
# raise HTTPException(status_code=404, detail="Letter not found")
# return {"detail": "Letter deleted"}
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.put("/{letter_id}", response_model=LetterResponse)
# async def update_letter(
# letter_id: int,
# letter_data: LetterCreate,
# current_user: User = Depends(get_current_user),
# ):
# try:
# letter = await letter_service.update_letter(
# letter_id, current_user.id, letter_data.content
# )
# if not letter:
# raise HTTPException(status_code=404, detail="Letter not found")
# return letter.to_response()
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.post("/{letter_id}/send")
# async def send_letter(
# letter_id: int,
# letter_data: EmailRequest,
# current_user: User = Depends(get_current_user),
# ):
# try:
# letter = await letter_service.get_letter_by_id(letter_id, current_user.id)
# if not letter:
# raise HTTPException(status_code=404, detail="Letter not found")
#
# await letter_service.send_letter(letter, letter_data)
# return {"message": "Email sent successfully!"}
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))

View file

@ -0,0 +1,117 @@
# from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Query, Form
# from typing import List
# from ...schemas.photo import (
# PhotoUpload,
# PhotoResponse,
# CommentCreate,
# CommentResponse,
# FilterRequest,
# )
# from ...services.photo_service import PhotoService
# from ...core.security import get_current_user
# from ...schemas.user import User
# from pydantic import ValidationError
#
# router = APIRouter(prefix="/photo", tags=["photo"])
# photo_service = PhotoService()
#
#
# @router.post("/upload", response_model=PhotoResponse)
# async def upload_photo(
# photo_data: UploadFile = Form(...),
# file: UploadFile = File(...),
# current_user: User = Depends(get_current_user),
# ) -> PhotoResponse:
# import json
#
# try:
# photo_data_bytes = await photo_data.read()
# photo_info = json.loads(photo_data_bytes.decode("utf-8"))
# photo_data = PhotoUpload(**photo_info)
# except (json.JSONDecodeError, ValidationError) as e:
# raise HTTPException(status_code=400, detail="Invalid photo data format")
# try:
# photo = await photo_service.upload_photo(current_user.id, photo_data, file)
# return photo.to_response()
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.get("", response_model=List[PhotoResponse])
# async def get_user_photos(
# skip: int = 0,
# limit: int = 20,
# album_name: str = None,
# current_user: User = Depends(get_current_user),
# ) -> List[PhotoResponse]:
# try:
# photos = await photo_service.get_user_photos(
# current_user.id, skip, limit, album_name
# )
# return [photo.to_response() for photo in photos]
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.post("/{photo_id}/comment", response_model=CommentResponse)
# async def add_photo_comment(
# photo_id: int,
# comment_data: CommentCreate,
# current_user: User = Depends(get_current_user),
# ) -> CommentResponse:
# try:
# is_friend = await photo_service.check_friendship(current_user.id, photo_id)
# if not is_friend:
# raise HTTPException(
# status_code=403, detail="Only friends can comment on photos"
# )
#
# comment = await photo_service.add_comment(
# photo_id, current_user.id, comment_data
# )
# return comment.to_response(current_user.username)
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.get("/{photo_id}/comments", response_model=List[CommentResponse])
# async def get_photo_comments(
# photo_id: int, current_user: User = Depends(get_current_user)
# ) -> List[CommentResponse]:
# try:
# comments = await photo_service.get_photo_comments(photo_id)
# return comments
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.post("/edit-filter")
# async def apply_photo_filter(
# filter_request: FilterRequest, current_user: User = Depends(get_current_user)
# ) -> dict:
# if filter_request.cover and filter_request.title is None:
# raise HTTPException(status_code=400, detail="title must be Not Null")
# try:
# filtered_image_path = await photo_service.apply_filter(
# filter_request.photo_id,
# filter_request.filter_type,
# current_user.id,
# filter_request.cover,
# filter_request.title,
# )
# return {"filtered_image_path": filtered_image_path}
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))
#
#
# @router.delete("/{photo_id}")
# async def delete_photo(
# photo_id: int, current_user: User = Depends(get_current_user)
# ) -> dict:
# try:
# success = await photo_service.delete_photo(photo_id, current_user.id)
# if not success:
# raise HTTPException(status_code=404, detail="Photo not found")
# return {"message": "Photo deleted successfully"}
# except Exception as e:
# raise HTTPException(status_code=400, detail=str(e))

View file

@ -0,0 +1,110 @@
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from Backend.schemas.room import (
FurnitureItem,
RoomFurniturePlacement,
FurniturePlacementRequest,
RoomNameUpdateRequest,
UpdateRoomTypeRequest,
RoomResponse,
RoomTypeResponse,
RoomFurnitureResponse,
)
from Backend.schemas.user import User
from Backend.core.security import get_current_user
from Backend.services.room_service import RoomService
router = APIRouter(prefix="/room", tags=["room"])
room_service = RoomService()
@router.get("/catalog", response_model=List[FurnitureItem])
async def get_furniture_catalog() -> List[FurnitureItem]:
return await room_service.get_furniture_catalog()
@router.get("/layout", response_model=RoomFurnitureResponse)
async def get_my_room_layout(current_user: User = Depends(get_current_user)):
room_id = await room_service.get_or_create_room(current_user.id)
return await room_service.get_room_furnitures(room_id)
@router.get("/layout/{user_id}", response_model=RoomFurnitureResponse)
async def get_user_room_layout(user_id: int) -> RoomFurnitureResponse:
room_id = await room_service.get_or_create_room(user_id)
return await room_service.get_room_furnitures(room_id)
@router.post("/furniture")
async def place_furniture(
request: FurniturePlacementRequest, current_user: User = Depends(get_current_user)
):
room_id = await room_service.get_or_create_room(current_user.id)
try:
await room_service.place_furniture(room_id, request)
return {"message": "Furniture placed successfully"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/furniture")
async def remove_furniture(
x: int, y: int, furniture_name: str, current_user: User = Depends(get_current_user)
):
room_id = await room_service.get_or_create_room(current_user.id)
await room_service.remove_furniture(room_id, x, y, furniture_name)
return {"message": "Furniture removed successfully"}
@router.put("/")
async def update_room_name(
data: RoomNameUpdateRequest, current_user: User = Depends(get_current_user)
):
try:
room_id = await room_service.get_or_create_room(current_user.id)
await room_service.update_room_name(room_id, data.new_name)
return {"message": "Room name updated successfully"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/")
async def get_my_room(current_user: User = Depends(get_current_user)) -> RoomResponse:
try:
room_id = await room_service.get_or_create_room(current_user.id)
return (await room_service.get_room_by_id(room_id)).to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/types")
async def get_room_types() -> List[RoomTypeResponse]:
return await room_service.get_room_types()
@router.patch("/")
async def update_room_type(
data: UpdateRoomTypeRequest, current_user: User = Depends(get_current_user)
) -> RoomResponse:
try:
room_id = await room_service.get_or_create_room(current_user.id)
return (await room_service.update_room_type(room_id, data.type)).to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/my")
async def get_my_furniture(current_user: User = Depends(get_current_user)):
try:
list_ = await room_service.get_user_furniture(current_user.id)
return list_
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{user_id}", response_model=RoomResponse)
async def get_room_by_userId(user_id: int) -> RoomResponse:
try:
room = await room_service.get_room_by_userId(user_id)
return room.to_response()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View file

@ -0,0 +1,48 @@
from fastapi import APIRouter, Depends, HTTPException
from ...core.security import get_current_user
from ...schemas.user import User
from Backend.services.store_service import StoreService
from ...services.room_service import RoomService
router = APIRouter(prefix="/store", tags=["store"])
store_service = StoreService()
room_service = RoomService()
@router.get("")
async def get_dotory(
current_user: User = Depends(get_current_user),
):
try:
dotory = await store_service.get_dotory_by_id(current_user.id)
return {"dotory": dotory}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{product_id}")
async def buy_product(
product_id: int,
product_name: str,
current_user: User = Depends(get_current_user),
):
try:
response = await store_service.buy_product(product_id, current_user.id)
if response["isSuccess"]:
await room_service.add_furniture(current_user.id, product_name)
return {"dotory": response["dotory"]}
raise HTTPException(status_code=404, detail="Product not found")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("")
async def update_dotory(
dotory_num: int,
current_user: User = Depends(get_current_user),
):
try:
dotory = await store_service.update_user_dotory(current_user.id, dotory_num)
return {"dotory": dotory}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View file

@ -0,0 +1,116 @@
from typing import List
from fastapi import APIRouter, HTTPException, status, UploadFile, File, Depends
from Backend.schemas.user import UserCreate, UserLogin, UserResponse, User, UserUpdate
from Backend.services.user_service import UserService
from Backend.core.security import create_access_token, get_current_user
router = APIRouter(prefix="/user", tags=["user"])
user_service = UserService()
@router.post(
"/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED
)
async def register_user(
user_data: UserCreate = Depends(UserCreate.as_form),
profile_file: UploadFile = File(default=None),
) -> UserResponse:
existing_user = await user_service.get_user_by_username
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered"
)
existing_email = await user_service.get_user_by_email(user_data.email)
if existing_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
try:
user = await user_service.create_user(user_data, profile_file)
return user.to_response()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to created user: {str(e)}"
)
@router.post("/login")
async def login_user(login_data: UserLogin) -> dict:
user = await user_service.authenticate_user(
login_data.username, login_data.password
)
if not user:
raise HTTPException(
status_code=
detail=
)
if not user.is_active:
raise HTTPException(
status_code=, detail=""
)
access_token =
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/profile/{username}", response_model=UserResponse)
async def get_user_profile(username: str) -> UserResponse:
user = await user_service.get_user_by_username(username)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
return user.to_response()
@router.get("/me", response_model=UserResponse)
async def get_user_me(user: User = Depends(get_current_user)) -> UserResponse:
return user.to_response()
@router.delete("/{username}")
async def delete_user(username: str) -> dict:
is_success = await user_service.delete_user(username)
if not is_success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
return {"detail": "User deleted"}
@router.get("/find/{username}", response_model=List[UserResponse])
async def find_user(username: str) -> List[UserResponse]:
users = await user_service.find_user(username)
return users
@router.put("/", response_model=UserResponse)
async def update_user(
current_user: User = Depends(get_current_user),
user_data: UserUpdate = Depends(UserUpdate.as_form),
profile_file: UploadFile = File(default=None),
) -> UserResponse:
try:
updated_user = await user_service.update_user(
user=current_user, user_data=user_data, profile_file=profile_file
)
return updated_user.to_response()
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update user: {str(e)}",
)

21
Backend/router/router.py Normal file
View file

@ -0,0 +1,21 @@
import os
import traceback
from fastapi import APIRouter
from fastapi.logger import logger
router = APIRouter()
for router_file in os.listdir("Backend/router/endpoints"):
try:
if router_file.endswith(".py"):
module_name = "Backend.router.endpoints." + router_file[:-3]
module = __import__(module_name, fromlist=[""])
router_object = getattr(module, "router")
prefix = getattr(module, "prefix", router_file[:-3])
router.include_router(router_object, prefix="/api", tags=[router_file[:-3]])
print(f"Loaded router: /api/{prefix} - {router_file}")
except Exception as e:
logger.error(f"Error loading router {router_file}:\n{traceback.format_exc()}")

View file

106
Backend/schemas/avatar.py Normal file
View file

@ -0,0 +1,106 @@
from pydantic import BaseModel
import enum
class AvatarType(str, enum.Enum):
MALE = "남성"
FEMALE = "여성"
class TopClothesType(str, enum.Enum):
SCHOOL_CLOTHES = "교복"
SCHOOL_CLOTHES_2 = "교복 조끼"
ANA_CLOTHES = "AnA 동잠"
SUSPENDERS_CLOTHES_1 = "멜빵 바지"
SUSPENDERS_CLOTHES_2 = "멜빵 치마"
RAINBOW_CLOTHES = "무지개 맨투맨"
SANTA_CLOTHES = "산타"
class BottomClothesType(str, enum.Enum):
SCHOOL_CLOTHES = "교복 바지"
SCHOOL_CLOTHES_2 = "교복 치마"
SCHOOL_CLOTHES_3 = "교복 조끼 바지"
SCHOOL_CLOTHES_4 = "교복 조끼 치마"
SANTA_CLOTHES = "산타 바지"
JEANS = "청바지"
avatar_path_ = {"남성": "public/avatar/남자.png", "여성": "public/avatar/여자.png"}
top_clothe_path_ = {
"교복": "public/avatar/교복상의.png",
"교복 조끼": "public/avatar/교복조끼상의.png",
"AnA 동잠": "public/avatar/동잠상의.png",
"멜빵 바지": "public/avatar/멜빵바지상의.png",
"멜빵 치마": "public/avatar/멜빵치마상의.png",
"무지개 맨투맨": "public/avatar/무지개맨투맨상의.png",
"산타": "public/avatar/산타상의.png",
}
bottom_clothe_path_ = {
"교복 바지": "public/avatar/교복하의남.png",
"교복 치마": "public/avatar/교복하의여.png",
"교복 조끼 바지": "public/avatar/교복조끼하의남.png",
"교복 조끼 치마": "public/avatar/교복조끼하의여.png",
"산타 바지": "public/avatar/산타하의.png",
"청바지": "public/avatar/청바지하의.png",
}
class AvatarTypeResponse(BaseModel):
name: str
path: str
class AvatarUpdate(BaseModel):
avatar_type: AvatarType
top_clothe_type: TopClothesType
bottom_clothe_type: BottomClothesType
class AvatarResponse(BaseModel):
id: int
user_id: int
avatar_type: AvatarTypeResponse
top_clothe_type: AvatarTypeResponse
bottom_clothe_type: AvatarTypeResponse
class AvatarOptions(BaseModel):
avatar_types: list[str]
top_clothe_types: list[str]
bottom_clothe_types: list[str]
class Avatar:
def __init__(
self,
id: int,
user_id: int,
avatar_type: AvatarType,
top_clothe_type: TopClothesType,
bottom_clothe_type: BottomClothesType,
):
self.id = id
self.user_id = user_id
self.avatar_type = avatar_type
self.top_clothe_type = top_clothe_type
self.bottom_clothe_type = bottom_clothe_type
def to_response(self) -> AvatarResponse:
return AvatarResponse(
id=self.id,
user_id=self.user_id,
avatar_type=AvatarTypeResponse(
name=self.avatar_type,
path=avatar_path_[self.avatar_type],
),
top_clothe_type=AvatarTypeResponse(
name=self.top_clothe_type,
path=top_clothe_path_[self.top_clothe_type],
),
bottom_clothe_type=AvatarTypeResponse(
name=self.bottom_clothe_type,
path=bottom_clothe_path_[self.bottom_clothe_type],
),
)

109
Backend/schemas/diary.py Normal file
View file

@ -0,0 +1,109 @@
from pydantic import BaseModel, field_validator
from datetime import datetime
from typing import Optional, List
from fastapi import Form
class DiaryCreate(BaseModel):
title: str
content: str
category: str
@classmethod
def as_form(
cls,
title: str = Form(...),
content: str = Form(...),
category: str = Form(...),
) -> "DiaryCreate":
return cls(title=title, content=content, category=category)
@field_validator("title")
@classmethod
def validate_title(cls, v):
if len(v.strip()) < 1:
raise ValueError("Title cannot be empty")
if len(v) > 100:
raise ValueError("Title must be less than 100 characters")
return v.strip()
@field_validator("content")
@classmethod
def validate_content(cls, v):
if len(v.strip()) < 1:
raise ValueError("Content cannot be empty")
if len(v) > 5000:
raise ValueError("Content must be less than 5000 characters")
return v.strip()
class DiaryUpdate(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
category: Optional[str] = None
@classmethod
def as_form(
cls,
title: Optional[str] = Form(None),
content: Optional[str] = Form(None),
category: Optional[str] = Form(None),
) -> "DiaryUpdate":
return cls(title=title, content=content, category=category)
class DiaryResponse(BaseModel):
id: int
user_id: int
title: str
content: str
images: List[str]
category: str
created_at: datetime
is_submitted: bool
email_sent: bool
class Diary:
def __init__(
self,
id: int,
user_id: int,
title: str,
content: str,
images: str, # JSON string
category: str,
created_at: datetime,
is_submitted: bool = False,
email_sent: bool = False,
):
self.id = id
self.user_id = user_id
self.title = title
self.content = content
self.images = images
self.category = category
self.created_at = created_at
self.is_submitted = is_submitted
self.email_sent = email_sent
@property
def image_list(self) -> List[str]:
return (
[img.strip() for img in self.images.split(",") if img.strip()]
if self.images
else []
)
def to_response(self) -> DiaryResponse:
return DiaryResponse(
id=self.id,
user_id=self.user_id,
title=self.title,
content=self.content,
images=self.image_list,
category=self.category,
created_at=self.created_at,
is_submitted=self.is_submitted,
email_sent=self.email_sent,
)

View file

@ -0,0 +1,59 @@
from pydantic import BaseModel, field_validator
from datetime import datetime
import enum
class FriendshipStatus(str, enum.Enum):
PENDING = "pending"
ACCEPTED = "accepted"
REJECTED = "rejected"
class FriendshipRequest(BaseModel):
friend_username: str
@field_validator("friend_username")
@classmethod
def validate_friend_username(cls, v):
if len(v.strip()) < 1:
raise ValueError("Friend username cannot be empty")
return v.strip()
class FriendshipResponse(BaseModel):
id: int
user_id: int
friend_id: int
friend_username: str
status: FriendshipStatus
created_at: datetime
class FriendshipUpdate(BaseModel):
status: FriendshipStatus
class Friendship:
def __init__(
self,
id: int,
user_id: int,
friend_id: int,
status: str,
created_at: datetime,
):
self.id = id
self.user_id = user_id
self.friend_id = friend_id
self.status = status
self.created_at = created_at
def to_response(self, friend_username: str) -> FriendshipResponse:
return FriendshipResponse(
id=self.id,
user_id=self.user_id,
friend_id=self.friend_id,
friend_username=friend_username,
status=FriendshipStatus(self.status),
created_at=self.created_at,
)

View file

@ -0,0 +1,46 @@
from datetime import datetime
from pydantic import BaseModel, field_validator
class GuestBookCreate(BaseModel):
content: str
target_user_id: int
@field_validator("content")
@classmethod
def validate_content(cls, v):
if len(v.strip()) < 1:
raise ValueError("GuestBook content cannot be empty")
if len(v) > 2000:
raise ValueError("GuestBook content must be less than 2000 characters")
return v.strip()
class GuestbookUpdate(BaseModel):
content: str
class GuestbookResponse(BaseModel):
id: int
content: str
target_user_id: int
user_id: int
user_profile_path: str
username: str
created_at: datetime
class GuestBook:
def __init__(
self,
id: int,
target_user_id: int,
user_id,
content,
created_at: datetime,
):
self.id = id
self.target_user_id = target_user_id
self.user_id = user_id
self.content = content
self.created_at = created_at

47
Backend/schemas/letter.py Normal file
View file

@ -0,0 +1,47 @@
# from pydantic import BaseModel, field_validator, EmailStr
#
# # email validator 삭제 및 EmailStr 사용
#
#
# class LetterCreate(BaseModel):
# content: str
#
# @field_validator("content")
# @classmethod
# def validate_content(cls, v):
# if len(v.strip()) < 1:
# raise ValueError("Letter content cannot be empty")
# if len(v) > 2000:
# raise ValueError("Letter content must be less than 2000 characters")
# return v.strip()
#
#
# class LetterResponse(BaseModel):
# id: int
# sender_id: int
# content: str
#
#
# class Letter:
# def __init__(
# self,
# id: int,
# sender_id: int,
# content: str,
# ):
# self.id = id
# self.sender_id = sender_id
# self.content = content
#
# def to_response(self) -> LetterResponse:
# return LetterResponse(
# id=self.id,
# sender_id=self.sender_id,
# content=self.content,
# )
#
#
# class EmailRequest(BaseModel):
# sender_email: EmailStr
# sender_password: str
# sender_name: str

117
Backend/schemas/photo.py Normal file
View file

@ -0,0 +1,117 @@
# from pydantic import BaseModel, field_validator
# from datetime import datetime
#
#
# class PhotoUpload(BaseModel):
# album_name: str
# title: str
#
# @field_validator("album_name")
# @classmethod
# def validate_album_name(cls, v):
# if len(v.strip()) < 1:
# raise ValueError("Album name cannot be empty")
# if len(v) > 50:
# raise ValueError("Album name must be less than 50 characters")
# return v.strip()
#
# @field_validator("title")
# @classmethod
# def validate_title(cls, v):
# if len(v.strip()) < 1:
# raise ValueError("Title cannot be empty")
# if len(v) > 100:
# raise ValueError("Title must be less than 100 characters")
# return v.strip()
#
#
# class PhotoResponse(BaseModel):
# id: int
# user_id: int
# album_name: str
# image_path: str
# title: str
# created_at: datetime
#
#
# class CommentCreate(BaseModel):
# content: str
#
# @field_validator("content")
# @classmethod
# def validate_content(cls, v):
# if len(v.strip()) < 1:
# raise ValueError("Comment cannot be empty")
# if len(v) > 500:
# raise ValueError("Comment must be less than 500 characters")
# return v.strip()
#
#
# class CommentResponse(BaseModel):
# id: int
# photo_id: int
# user_id: int
# username: str
# content: str
# created_at: datetime
#
#
# class FilterRequest(BaseModel):
# photo_id: int
# filter_type: str
# cover: bool
# title: str = None
#
#
# class Photo:
# def __init__(
# self,
# id: int,
# user_id: int,
# album_name: str,
# image_path: str,
# title: str,
# created_at: datetime,
# ):
# self.id = id
# self.user_id = user_id
# self.album_name = album_name
# self.image_path = image_path
# self.title = title
# self.created_at = created_at
#
# def to_response(self) -> PhotoResponse:
# return PhotoResponse(
# id=self.id,
# user_id=self.user_id,
# album_name=self.album_name,
# image_path=self.image_path,
# title=self.title,
# created_at=self.created_at,
# )
#
#
# class PhotoComment:
# def __init__(
# self,
# id: int,
# photo_id: int,
# user_id: int,
# content: str,
# created_at: datetime,
# ):
# self.id = id
# self.photo_id = photo_id
# self.user_id = user_id
# self.content = content
# self.created_at = created_at
#
# def to_response(self, username: str) -> CommentResponse:
# return CommentResponse(
# id=self.id,
# photo_id=self.photo_id,
# user_id=self.user_id,
# username=username,
# content=self.content,
# created_at=self.created_at,
# )

296
Backend/schemas/room.py Normal file
View file

@ -0,0 +1,296 @@
import enum
from pydantic import BaseModel, field_validator
from typing import Optional, List
class RoomNameUpdateRequest(BaseModel):
new_name: str
class RoomTypes(enum.Enum):
ROOM_1 = "room_1"
ROOM_2 = "room_2"
class UpdateRoomTypeRequest(BaseModel):
type: RoomTypes
class RoomTypeResponse(BaseModel):
type: str
image_path: str
class RoomResponse(BaseModel):
id: int
user_id: int
room_name: str
room_type: RoomTypes
room_image_path: str
room_path = {
"room_1": "public/room/room_1.png",
"room_2": "public/room/room_2.png",
}
class FurnitureItem(BaseModel):
name: str
image_path: str
width: int
class Furniture(str, enum.Enum):
BLACK_LAPTOP1_0 = "검정 노트북1-01"
BLACK_LAPTOP1_180 = "검정 노트북1-1801"
BLACK_LAPTOP1_270 = "검정 노트북1-2701"
BLACK_LAPTOP1_90 = "검정 노트북1-901"
BLACK_LAPTOP2_0 = "검정 노트북2-01"
BLACK_LAPTOP2_180 = "검정 노트북2-1801"
BLACK_LAPTOP2_270 = "검정 노트북2-2701"
BLACK_LAPTOP2_90 = "검정 노트북2-901"
BLACK_LAPTOP3_0 = "검정 노트북3-01"
BLACK_LAPTOP3_180 = "검정 노트북3-1801"
BLACK_LAPTOP3_270 = "검정 노트북3-2701"
BLACK_LAPTOP3_90 = "검정 노트북3-901"
WOODEN_TABLE_90 = "나무 탁자-901"
WOODEN_TABLE_0 = "나무 탁자-01"
LAPTOP1_0 = "노트북1-01"
LAPTOP1_180 = "노트북1-1801"
LAPTOP1_270 = "노트북1-2701"
LAPTOP1_90 = "노트북1-901"
LAPTOP2_0 = "노트북2-01"
LAPTOP2_180 = "노트북2-1801"
LAPTOP2_270 = "노트북2-2701"
LAPTOP2_90 = "노트북2-901"
LAPTOP3_0 = "노트북3-01"
LAPTOP3_180 = "노트북3-1801"
LAPTOP3_270 = "노트북3-2701"
LAPTOP3_90 = "노트북3-901"
GREEN_TABLE = "녹색 탁자1"
MINI_FRIDGE_0 = "미니 냉장고-01"
MINI_FRIDGE_180 = "미니 냉장고-1801"
MINI_FRIDGE_90 = "미니 냉장고-901"
BOX_0 = "박스-01"
BOX_90 = "박스-901"
PINK_TABLE = "분홍색 탁자1"
SHELF_0 = "선반-01"
SHELF_180 = "선반-1801"
SHELF_270 = "선반-2701"
SHELF_90 = "선반-901"
TRASH_CAN_CLOSED = "쓰레기통 닫힘1"
TRASH_CAN_OPEN = "쓰레기통 열림1"
FISHBOWL_0 = "어항-01"
FISHBOWL_180 = "어항-1801"
FISHBOWL_270 = "어항-2701"
FISHBOWL_90 = "어항-901"
BEVERAGE_FRIDGE_0 = "음료 냉장고-01"
BEVERAGE_FRIDGE_180 = "음료 냉장고-1801"
BEVERAGE_FRIDGE_270 = "음료 냉장고-2701"
BEVERAGE_FRIDGE_90 = "음료 냉장고-901"
CHAIR_0 = "의자-01"
CHAIR_180 = "의자-1801"
CHAIR_270 = "의자-2701"
CHAIR_90 = "의자-901"
SMALL_SHELF_0 = "작은 선반-01"
SMALL_SHELF_180 = "작은 선반-1801"
SMALL_SHELF_270 = "작은 선반-2701"
SMALL_SHELF_90 = "작은 선반-901"
SMALL_PLANT = "작은 식물1"
BOOKSHELF_0 = "책장-01"
BOOKSHELF_180 = "책장-1801"
BOOKSHELF_270 = "책장-2701"
BOOKSHELF_90 = "책장-901"
LARGE_PLANT = "큰 식물1"
TV_0 = "티비-01"
TV_180 = "티비-1801"
TV_270 = "티비-2701"
TV_90 = "티비-901"
BLUE_TABLE = "파란색 탁자1"
GRAY_TABLE = "회색 탁자1"
WHITE_LAPTOP1_0 = "흰 노트북1-01"
WHITE_LAPTOP1_180 = "흰 노트북1-1801"
WHITE_LAPTOP1_270 = "흰 노트북1-2701"
WHITE_LAPTOP1_90 = "흰 노트북1-901"
WHITE_LAPTOP2_0 = "흰 노트북2-01"
WHITE_LAPTOP2_180 = "흰 노트북2-1801"
WHITE_LAPTOP2_270 = "흰 노트북2-2701"
WHITE_LAPTOP2_90 = "흰 노트북2-901"
WHITE_LAPTOP3_0 = "흰 노트북3-01"
WHITE_LAPTOP3_180 = "흰 노트북3-1801"
WHITE_LAPTOP3_270 = "흰 노트북3-2701"
WHITE_LAPTOP3_90 = "흰 노트북3-901"
WHITE_SHELF_0 = "흰색 선반-01"
WHITE_SHELF_180 = "흰색 선반-1801"
WHITE_SHELF_270 = "흰색 선반-2701"
WHITE_SHELF_90 = "흰색 선반-901"
WHITE_SMALL_SHELF_0 = "흰색 작은 선반-01"
WHITE_SMALL_SHELF_180 = "흰색 작은 선반-1801"
WHITE_SMALL_SHELF_270 = "흰색 작은 선반-2701"
WHITE_SMALL_SHELF_90 = "흰색 작은 선반-901"
WHITE_TABLE = "흰색 탁자1"
EMPTY = "1"
class RoomFurniturePlacement(BaseModel):
id: int
room_id: int
furniture_name: Furniture
x: int
y: int
class FurniturePlacementRequest(BaseModel):
furniture_name: Furniture
x: int
y: int
@field_validator("x", "y")
@classmethod
def validate_coordinates(cls, v):
if v < 0 or v >= 10:
raise ValueError("position must be between 0 and 10")
return v
class FurniturePlacementResponse(BaseModel):
furniture_name: Furniture
x: int
y: int
image_path: str
class RoomFurnitureResponse(BaseModel):
room: RoomResponse
furniture: List[FurniturePlacementResponse]
furniture_path = {
"검정 노트북1-01": "public/funiture/검정 노트북1-0.png",
"검정 노트북1-1801": "public/funiture/검정 노트북1-180.png",
"검정 노트북1-2701": "public/funiture/검정 노트북1-270.png",
"검정 노트북1-901": "public/funiture/검정 노트북1-90.png",
"검정 노트북2-01": "public/funiture/검정 노트북2-0.png",
"검정 노트북2-1801": "public/funiture/검정 노트북2-180.png",
"검정 노트북2-2701": "public/funiture/검정 노트북2-270.png",
"검정 노트북2-901": "public/funiture/검정 노트북2-90.png",
"검정 노트북3-01": "public/funiture/검정 노트북3-0.png",
"검정 노트북3-1801": "public/funiture/검정 노트북3-180.png",
"검정 노트북3-2701": "public/funiture/검정 노트북3-270.png",
"검정 노트북3-901": "public/funiture/검정 노트북3-90.png",
"나무 탁자-901": "public/funiture/나무 탁자-90.png",
"나무 탁자-01": "public/funiture/나무탁자-0.png",
"노트북1-01": "public/funiture/노트북1-0.png",
"노트북1-1801": "public/funiture/노트북1-180.png",
"노트북1-2701": "public/funiture/노트북1-270.png",
"노트북1-901": "public/funiture/노트북1-90.png",
"노트북2-01": "public/funiture/노트북2-0.png",
"노트북2-1801": "public/funiture/노트북2-180.png",
"노트북2-2701": "public/funiture/노트북2-270.png",
"노트북2-901": "public/funiture/노트북2-90.png",
"노트북3-01": "public/funiture/노트북3-0.png",
"노트북3-1801": "public/funiture/노트북3-180.png",
"노트북3-2701": "public/funiture/노트북3-270.png",
"노트북3-901": "public/funiture/노트북3-90.png",
"녹색 침대-02": "public/funiture/녹색 침대-0.png",
"녹색 침대-1802": "public/funiture/녹색 침대-180.png",
"녹색 침대-2702": "public/funiture/녹색 침대-270.png",
"녹색 침대-902": "public/funiture/녹색 침대-90.png",
"녹색 탁자1": "public/funiture/녹색 탁자.png",
"미니 냉장고-01": "public/funiture/미니 냉장고-0.png",
"미니 냉장고-1801": "public/funiture/미니 냉장고-180.png",
"미니 냉장고-901": "public/funiture/미니 냉장고-90.png",
"박스-01": "public/funiture/박스-0.png",
"박스-901": "public/funiture/박스-90.png",
"분홍색 탁자1": "public/funiture/분홍색 탁자.png",
"빨간 침대-02": "public/funiture/빨간 침대-0.png",
"빨간 침대-1802": "public/funiture/빨간 침대-180.png",
"빨간 침대-2702": "public/funiture/빨간 침대-270.png",
"빨간 침대-902": "public/funiture/빨간 침대-90.png",
"선반-01": "public/funiture/선반-0.png",
"선반-1801": "public/funiture/선반-180.png",
"선반-2701": "public/funiture/선반-270.png",
"선반-901": "public/funiture/선반-90.png",
"소파-02": "public/funiture/소파-0.png",
"소파-1802": "public/funiture/소파-180.png",
"소파-2702": "public/funiture/소파-270.png",
"소파-902": "public/funiture/소파-90.png",
"쓰레기통 닫힘1": "public/funiture/쓰레기통닫힘.png",
"쓰레기통 열림1": "public/funiture/쓰레기통열림.png",
"어항-01": "public/funiture/어항-0.png",
"어항-1801": "public/funiture/어항-180.png",
"어항-2701": "public/funiture/어항-270.png",
"어항-901": "public/funiture/어항-90.png",
"음료 냉장고-01": "public/funiture/음료 냉장고-0.png",
"음료 냉장고-1801": "public/funiture/음료 냉장고-180.png",
"음료 냉장고-2701": "public/funiture/음료 냉장고-270.png",
"음료 냉장고-901": "public/funiture/음료 냉장고-90.png",
"의자-01": "public/funiture/의자-0.png",
"의자-1801": "public/funiture/의자-180.png",
"의자-2701": "public/funiture/의자-270.png",
"의자-901": "public/funiture/의자-90.png",
"작은 선반-01": "public/funiture/작은 선반-0.png",
"작은 선반-1801": "public/funiture/작은 선반-180.png",
"작은 선반-2701": "public/funiture/작은 선반-270.png",
"작은 선반-901": "public/funiture/작은 선반-90.png",
"작은 식물1": "public/funiture/작은 식물.png",
"주황 침대-02": "public/funiture/주황 침대-0.png",
"주황 침대-1802": "public/funiture/주황 침대-180.png",
"주황 침대-2702": "public/funiture/주황 침대-270.png",
"주황 침대-902": "public/funiture/주황 침대-90.png",
"책장-01": "public/funiture/책장-0.png",
"책장-1801": "public/funiture/책장-180.png",
"책장-2701": "public/funiture/책장-270.png",
"책장-901": "public/funiture/책장-90.png",
"큰 식물1": "public/funiture/큰 식물.png",
"티비-01": "public/funiture/티비-0.png",
"티비-1801": "public/funiture/티비-180.png",
"티비-2701": "public/funiture/티비-270.png",
"티비-901": "public/funiture/티비-90.png",
"파란 침대-02": "public/funiture/파란 침대-0.png",
"파란 침대-1802": "public/funiture/파란 침대-180.png",
"파란 침대-2702": "public/funiture/파란 침대-270.png",
"파란 침대-902": "public/funiture/파란 침대-90.png",
"파란색 탁자1": "public/funiture/파란색 탁자.png",
"회색 탁자1": "public/funiture/회색 탁자.png",
"흰 노트북1-01": "public/funiture/흰 노트북1-0.png",
"흰 노트북1-1801": "public/funiture/흰 노트북1-180.png",
"흰 노트북1-2701": "public/funiture/흰 노트북1-270.png",
"흰 노트북1-901": "public/funiture/흰 노트북1-90.png",
"흰 노트북2-01": "public/funiture/흰 노트북2-0.png",
"흰 노트북2-1801": "public/funiture/흰 노트북2-180.png",
"흰 노트북2-2701": "public/funiture/흰 노트북2-270.png",
"흰 노트북2-901": "public/funiture/흰 노트북2-90.png",
"흰 노트북3-01": "public/funiture/흰 노트북3-0.png",
"흰 노트북3-1801": "public/funiture/흰 노트북3-180.png",
"흰 노트북3-2701": "public/funiture/흰 노트북3-270.png",
"흰 노트북3-901": "public/funiture/흰 노트북3-90.png",
"흰색 선반-01": "public/funiture/흰색 선반-0.png",
"흰색 선반-1801": "public/funiture/흰색 선반-180.png",
"흰색 선반-2701": "public/funiture/흰색 선반-270.png",
"흰색 선반-901": "public/funiture/흰색 선반-90.png",
"흰색 작은 선반-01": "public/funiture/흰색 작은 선반-0.png",
"흰색 작은 선반-1801": "public/funiture/흰색 작은 선반-180.png",
"흰색 작은 선반-2701": "public/funiture/흰색 작은 선반-270.png",
"흰색 작은 선반-901": "public/funiture/흰색 작은 선반-90.png",
"흰색 탁자1": "public/funiture/흰색 탁자.png",
"1": "1",
}
class Room:
def __init__(self, id: int, user_id: int, room_name: str, room_type: RoomTypes):
self.id = id
self.user_id = user_id
self.room_name = room_name
self.room_type = room_type
def to_response(self) -> RoomResponse:
return RoomResponse(
id=self.id,
user_id=self.user_id,
room_name=self.room_name,
room_type=self.room_type,
room_image_path=room_path[self.room_type],
)

93
Backend/schemas/user.py Normal file
View file

@ -0,0 +1,93 @@
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
import hashlib
import secrets
from fastapi import Form
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
@classmethod
def as_form(
cls,
username: str = Form(...),
email: EmailStr = Form(...),
password: str = Form(...),
) -> "UserCreate":
return cls(username=username, email=email, password=password)
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
password: Optional[str] = None
@classmethod
def as_form(
cls,
email: Optional[EmailStr] = Form(default=None),
password: Optional[str] = Form(default=None),
) -> "UserUpdate":
return cls(email=email, password=password)
class UserLogin(BaseModel):
username: str
password: str
class UserResponse(BaseModel):
id: int
username: str
email: str
created_at: datetime
profile_image_path: str
is_active: bool
class User:
def __init__(
self,
id: int,
username: str,
email: str,
password_hash: str,
salt: str,
created_at: datetime,
profile_image_path: str,
is_active: bool = True,
):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.salt = salt
self.created_at = created_at
self.profile_image_path = profile_image_path
self.is_active = is_active
@staticmethod
def hash_password(password: str, salt: Optional[str] = None) -> tuple[str, str]:
if salt is None:
salt = secrets.token_hex(32)
password_hash = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), salt.encode("utf-8"), 100000
)
return password_hash.hex(), salt
def verify_password(self, password: str) -> bool:
password_hash, _ = self.hash_password(password, self.salt)
return password_hash == self.password_hash
def to_response(self) -> UserResponse:
return UserResponse(
id=self.id,
username=self.username,
email=self.email,
created_at=self.created_at,
profile_image_path=self.profile_image_path,
is_active=self.is_active,
)

View file

View file

@ -0,0 +1,74 @@
from typing import Optional
from ..schemas.avatar import (
AvatarUpdate,
Avatar,
AvatarOptions,
AvatarType,
TopClothesType,
BottomClothesType,
)
from ..utils.db import execute, fetch_one
from ..utils.queries.avatar import AvatarQueries
class AvatarService:
@staticmethod
async def init_db():
await execute(AvatarQueries.CREATE_TABLE)
async def get_or_create_avatar(self, user_id: int) -> Avatar:
avatar = await self.get_user_avatar(user_id)
if not avatar:
avatar = await self.create_default_avatar(user_id)
return avatar
async def get_user_avatar(self, user_id: int) -> Optional[Avatar]:
row = await fetch_one(AvatarQueries.SELECT_USER_AVATAR, (user_id,))
if not row:
return None
return Avatar(**row)
async def create_default_avatar(self, user_id: int) -> Avatar:
await execute(
AvatarQueries.INSERT_AVATAR,
(
user_id,
AvatarType.MALE.value,
TopClothesType.ANA_CLOTHES.value,
BottomClothesType.JEANS.value,
),
)
return await self.get_user_avatar(user_id)
async def update_avatar(self, user_id: int, avatar_data: AvatarUpdate) -> Avatar:
update_fields = []
params = []
if avatar_data.avatar_type is not None:
update_fields.append("avatar_type = ?")
params.append(avatar_data.avatar_type.value)
if avatar_data.top_clothe_type is not None:
update_fields.append("top_clothe_type = ?")
params.append(avatar_data.top_clothe_type.value)
if avatar_data.bottom_clothe_type is not None:
update_fields.append("bottom_clothe_type = ?")
params.append(avatar_data.bottom_clothe_type.value)
if update_fields:
query = AvatarQueries.UPDATE_AVATAR.format(fields=", ".join(update_fields))
params.append(user_id)
await execute(query, tuple(params))
return await self.get_user_avatar(user_id)
async def get_avatar_options(self) -> AvatarOptions:
return AvatarOptions(
avatar_types=list(AvatarType),
top_clothe_types=list(TopClothesType),
bottom_clothe_types=list(BottomClothesType),
)
async def get_avatar_by_userId(self, user_id: int) -> Optional[Avatar]:
return await self.get_user_avatar(user_id)

View file

@ -0,0 +1,144 @@
import os
from datetime import datetime
from typing import List, Optional
from fastapi import UploadFile
from ..schemas.diary import DiaryCreate, DiaryUpdate, Diary
from ..utils.db import execute, fetch_one, fetch_all
from ..utils.image_processor import ImageProcessor
from ..utils.queries.diary import DiaryQueries
class DiaryService:
def __init__(self):
self.image_processor = ImageProcessor()
self.upload_dir = "uploads/diary"
os.makedirs(self.upload_dir, exist_ok=True)
@staticmethod
async def init_db():
await execute(DiaryQueries.CREATE_TABLE)
async def create_diary(
self, user_id: int, diary_data: DiaryCreate, files: List[UploadFile]
) -> Diary:
# image_path 는 ,로 구분 되어 있음
image_path = ""
if files is not None:
for file in files:
image_path += (
","
+ await self.image_processor.write_file_and_get_image_path(
file, self.upload_dir
)
)
image_path = image_path[1:]
query = DiaryQueries.INSERT_DIARY
created_at = datetime.now()
await execute(
query,
(
user_id,
diary_data.title,
diary_data.content,
image_path,
diary_data.category,
created_at,
False,
False,
),
)
row = await fetch_one(
DiaryQueries.SELECT_LATEST_USER_DIARY,
(user_id,),
)
return Diary(**row)
async def get_user_diaries(
self, user_id: int, skip: int = 0, limit: int = 20, category: str = None
) -> List[Diary]:
if category:
query = DiaryQueries.SELECT_USER_DIARIES_BY_CATEGORY
rows = await fetch_all(query, (user_id, category, limit, skip))
else:
query = DiaryQueries.SELECT_USER_DIARIES
rows = await fetch_all(query, (user_id, limit, skip))
return [Diary(**row) for row in rows]
async def get_diary_by_id(self, diary_id: int) -> Optional[Diary]:
query = DiaryQueries.SELECT_BY_ID
row = await fetch_one(query, (diary_id,))
if not row:
return None
return Diary(**row)
async def get_diary_with_user_id(
self, diary_id: int, user_id: int
) -> Optional[Diary]:
query = DiaryQueries.SELECT_BY_ID_WITH_USER_ID
row = await fetch_one(query, (diary_id, user_id))
if not row:
return None
return Diary(**row)
async def update_diary(
self,
diary_id: int,
user_id: int,
diary_data: DiaryUpdate,
files: List[UploadFile],
) -> Optional[Diary]:
diary = await self.get_diary_with_user_id(diary_id, user_id)
if not diary:
return None
update_fields = []
params = []
if diary_data.title is not None:
update_fields.append("title = ?")
params.append(diary_data.title)
if diary_data.content is not None:
update_fields.append("content = ?")
params.append(diary_data.content)
if diary_data.category is not None:
update_fields.append("category = ?")
params.append(diary_data.category)
if files is not None:
update_fields.append("images = ?")
image_paths = ""
for file in files:
image_paths += (
","
+ await self.image_processor.write_file_and_get_image_path(
file, self.upload_dir
)
)
image_paths = image_paths[1:]
params.append(image_paths)
if update_fields:
query = DiaryQueries.UPDATE_DIARY.format(fields=", ".join(update_fields))
params.extend([diary_id, user_id])
await execute(query, tuple(params))
return await self.get_diary_with_user_id(diary_id, user_id)
async def delete_diary(self, diary_id: int, user_id: int) -> bool:
try:
query = DiaryQueries.DELETE_DIARY
await execute(query, (diary_id, user_id))
return True
except Exception:
return False

View file

@ -0,0 +1,142 @@
from typing import List, Optional
from datetime import datetime
from ..schemas.friendship import Friendship, FriendshipResponse, FriendshipStatus
from ..utils.db import execute, fetch_one, fetch_all
from ..utils.queries.friendship import FriendshipQueries
class FriendshipService:
@staticmethod
async def init_db():
await execute(FriendshipQueries.CREATE_TABLE)
async def send_friendship_request(
self, user_id: int, friend_username: str
) -> FriendshipResponse:
friend_query = FriendshipQueries.SELECT_USER_BY_USERNAME
friend_row = await fetch_one(friend_query, (friend_username,))
if not friend_row:
raise ValueError("User not found")
friend_id = friend_row["id"]
if user_id == friend_id:
raise ValueError("Cannot send friendship request to yourself")
existing_query = FriendshipQueries.SELECT_EXISTING_FRIENDSHIP
existing_row = await fetch_one(
existing_query, (user_id, friend_id, friend_id, user_id)
)
if existing_row:
raise ValueError("Friendship request already exists")
created_at = datetime.now()
query = FriendshipQueries.INSERT_FRIENDSHIP
await execute(
query, (user_id, friend_id, FriendshipStatus.PENDING.value, created_at)
)
friendship_row = await fetch_one(
FriendshipQueries.SELECT_FRIENDSHIP_BY_IDS,
(user_id, friend_id),
)
if not friendship_row:
raise ValueError("Friendship not found after creation")
friendship = Friendship(
id=friendship_row["id"],
user_id=friendship_row["user_id"],
friend_id=friendship_row["friend_id"],
status=friendship_row["status"],
created_at=friendship_row["created_at"],
)
return friendship.to_response(friend_username)
async def accept_friendship_request(
self, friendship_id: int, user_id: int
) -> Optional[FriendshipResponse]:
friendship_query = FriendshipQueries.SELECT_FRIENDSHIP_FOR_ACCEPT
friendship_row = await fetch_one(
friendship_query,
(friendship_id, user_id, FriendshipStatus.PENDING.value),
)
if not friendship_row:
return None
update_query = FriendshipQueries.UPDATE_FRIENDSHIP_STATUS
await execute(update_query, (FriendshipStatus.ACCEPTED.value, friendship_id))
friendship = Friendship(
id=friendship_row["id"],
user_id=friendship_row["user_id"],
friend_id=friendship_row["friend_id"],
status=FriendshipStatus.ACCEPTED.value,
created_at=friendship_row["created_at"],
)
return friendship.to_response(friendship_row["username"])
async def get_user_friendships(
self, user_id: int, status: Optional[str] = None
) -> List[FriendshipResponse]:
if status:
query = FriendshipQueries.SELECT_USER_FRIENDSHIPS_BY_STATUS
rows = await fetch_all(query, (user_id, user_id, user_id, status))
else:
query = FriendshipQueries.SELECT_USER_FRIENDSHIPS
rows = await fetch_all(
query, (user_id, user_id, user_id, FriendshipStatus.ACCEPTED.value)
)
friendships = []
for row in rows:
friendship = Friendship(
id=row["id"],
user_id=row["user_id"],
friend_id=row["friend_id"],
status=row["status"],
created_at=row["created_at"],
)
friendships.append(friendship.to_response(row["username"]))
return friendships
async def delete_friendship(self, friendship_id: int, user_id: int) -> bool:
query = FriendshipQueries.DELETE_FRIENDSHIP
await execute(query, (friendship_id, user_id, user_id))
return True
async def get_pending_requests(self, user_id: int) -> List[FriendshipResponse]:
query = FriendshipQueries.SELECT_PENDING_REQUESTS
rows = await fetch_all(query, (user_id, FriendshipStatus.PENDING.value))
friendships = []
for row in rows:
friendship = Friendship(
id=row["id"],
user_id=row["user_id"],
friend_id=row["friend_id"],
status=row["status"],
created_at=row["created_at"],
)
friendships.append(friendship.to_response(row["username"]))
return friendships
async def check_friendship(self, user_id1: int, user_id2: int) -> bool:
friendship_query = FriendshipQueries.CHECK_FRIENDSHIP_STATUS
friendship_row = await fetch_one(
friendship_query, (user_id1, user_id2, user_id2, user_id1)
)
return friendship_row is not None

View file

@ -0,0 +1,110 @@
from datetime import datetime
from typing import List
from fastapi import HTTPException
from Backend.utils.db import execute, fetch_one, fetch_all
from Backend.utils.queries.guestbook import GuestBookQueries
from Backend.schemas.guestbook import GuestBook, GuestBookCreate, GuestbookResponse
from Backend.schemas.user import User
from Backend.utils.queries.user import UserQueries
class GuestbookService:
def __init__(self):
pass
@staticmethod
async def init_db():
await execute(GuestBookQueries.CREATE_TABLE)
async def create_guestbook(
self, data: GuestBookCreate, user: User
) -> GuestbookResponse:
user_exist = await fetch_one(UserQueries.SELECT_BY_ID, (data.target_user_id,))
if user_exist is None:
raise HTTPException(status_code=404, detail="User not found")
ex_row = await fetch_one(
GuestBookQueries.SELECT_GUEST_BOOK_BY_USER_ID, (user.id,)
)
created_at = datetime.now()
query = GuestBookQueries.INSERT_GUEST_BOOK
await execute(query, (data.target_user_id, user.id, data.content, created_at))
query = GuestBookQueries.SELECT_GUEST_BOOK_BY_USER_ID
row = await fetch_one(query, (user.id,))
if not (ex_row is None):
if row is None or ex_row["id"] == row["id"]:
raise HTTPException(
status_code=400, detail="Failed to create guest book"
)
return GuestbookResponse(
id=row["id"],
content=row["content"],
target_user_id=row["target_user_id"],
user_id=row["user_id"],
user_profile_path=user.profile_image_path,
username=user.username,
created_at=row["created_at"],
)
async def get_target_user_guestbooks(
self, target_user_id: int, limit: int = 20, offset: int = 0
) -> List[GuestbookResponse]:
query = GuestBookQueries.SELECT_TARGET_USER_GUEST_BOOKS
rows = await fetch_all(query, (target_user_id, limit, offset))
response_list = []
for row in rows:
user = await fetch_one(UserQueries.SELECT_BY_ID, (row["user_id"],))
response_list.append(
GuestbookResponse(
id=row["id"],
content=row["content"],
target_user_id=row["target_user_id"],
user_id=row["user_id"],
user_profile_path=user["profile_image_path"],
username=user["username"],
created_at=row["created_at"],
)
)
return response_list
async def update_guestbook_by_id(self, id: int, content: str) -> GuestbookResponse:
query = GuestBookQueries.SELECT_GUEST_BOOK_BY_ID
row = await fetch_one(query, (id,))
if row is None:
raise HTTPException(status_code=404, detail="Guest book not found")
query = GuestBookQueries.UPDATE_GUEST_BOOK_BY_ID
await execute(query, (content, id))
query = GuestBookQueries.SELECT_GUEST_BOOK_BY_ID
row = await fetch_one(query, (id,))
user = await fetch_one(UserQueries.SELECT_BY_ID, (row["user_id"],))
return GuestbookResponse(
id=row["id"],
content=row["content"],
target_user_id=row["target_user_id"],
user_id=row["user_id"],
user_profile_path=user["profile_image_path"],
username=user["username"],
created_at=row["created_at"],
)
async def delete_guestbook_by_id(self, id: int, user_id: int) -> bool:
try:
query = GuestBookQueries.DELETE_GUEST_BOOK
await execute(query, (id, user_id))
return True
except Exception:
return False

View file

@ -0,0 +1,81 @@
# from typing import List, Optional
# from ..schemas.letter import LetterCreate, Letter, EmailRequest
# from ..utils.db import execute, fetch_one, fetch_all
# from ..utils.default_queries import LetterQueries
# from ..utils.email_processor import EmailProcessor
#
#
# class LetterService:
# def __init__(self):
# self.email_processor = EmailProcessor()
#
# @staticmethod
# async def init_db():
# await execute(LetterQueries.CREATE_TABLE)
#
# async def create_letter(self, sender_id: int, letter_data: LetterCreate) -> Letter:
# query = LetterQueries.INSERT_LETTER
#
# await execute(
# query,
# (sender_id, letter_data.content),
# )
#
# row = await fetch_one(
# LetterQueries.SELECT_LATEST_USER_LETTER,
# (sender_id,),
# )
#
# return Letter(**row)
#
# async def get_user_letters(
# self, sender_id: int, skip: int = 0, limit: int = 20
# ) -> List[Letter]:
# query = LetterQueries.SELECT_USER_LETTERS
# rows = await fetch_all(query, (sender_id, limit, skip))
# return [Letter(**row) for row in rows]
#
# async def get_letter_by_id(
# self, letter_id: int, sender_id: int
# ) -> Optional[Letter]:
# query = LetterQueries.SELECT_LETTER_BY_ID
# row = await fetch_one(query, (letter_id, sender_id))
# if not row:
# return None
# return Letter(**row)
#
# async def delete_letter(self, letter_id: int, sender_id: int) -> bool:
# try:
# query = LetterQueries.DELETE_LETTER
# await execute(
# query,
# (letter_id, sender_id),
# )
# return True
# except Exception:
# return False
#
# async def update_letter(
# self, letter_id: int, sender_id: int, content: str
# ) -> Optional[Letter]:
# query = LetterQueries.UPDATE_LETTER
# await execute(
# query,
# (content, letter_id, sender_id),
# )
#
# row = await fetch_one(
# LetterQueries.SELECT_LETTER_BY_ID,
# (letter_id, sender_id),
# )
# if row is None:
# return None
#
# return Letter(**row)
#
# async def send_letter(self, letter: Letter, data: EmailRequest):
# subject = f"2025_SSF_LETTER_{data.sender_name}"
# content = letter.content
# await self.email_processor.send_email(
# subject, content, data.sender_email, data.sender_password
# )

View file

@ -0,0 +1,150 @@
# import os
# from datetime import datetime
# from typing import List
# from fastapi import UploadFile
# from ..schemas.photo import (
# PhotoUpload,
# Photo,
# PhotoComment,
# CommentCreate,
# CommentResponse,
# )
# from ..utils.db import execute, fetch_one, fetch_all
# from ..utils.default_queries import PhotoQueries
# from ..utils.image_processor import ImageProcessor
#
#
# class PhotoService:
# def __init__(self):
# self.image_processor = ImageProcessor()
# self.upload_dir = "uploads/photos"
# os.makedirs(self.upload_dir, exist_ok=True)
#
# @staticmethod
# async def init_db():
# await execute(PhotoQueries.CREATE_TABLE)
# await execute(PhotoQueries.CREATE_COMMENTS_TABLE)
#
# async def upload_photo(
# self, user_id: int, photo_data: PhotoUpload, file: UploadFile
# ) -> Photo:
# if not file.content_type.startswith("image/"):
# raise ValueError("File must be an image")
#
# self.image_processor.validate_image_file(file.filename, file.size)
#
# created_at = datetime.now()
#
# image_path = await self.image_processor.write_file_and_get_image_path(
# file, self.upload_dir
# )
#
# query = PhotoQueries.INSERT_PHOTO
#
# await execute(
# query,
# (user_id, photo_data.album_name, image_path, photo_data.title, created_at),
# )
#
# row = await fetch_one(
# PhotoQueries.SELECT_LATEST_USER_PHOTO,
# (user_id,),
# )
#
# return Photo(**row)
#
# async def get_user_photos(
# self, user_id: int, skip: int = 0, limit: int = 20, album_name: str = None
# ) -> List[Photo]:
# if album_name:
# query = PhotoQueries.SELECT_USER_PHOTOS_BY_ALBUM
# rows = await fetch_all(query, (user_id, album_name, limit, skip))
# else:
# query = PhotoQueries.SELECT_USER_PHOTOS
# rows = await fetch_all(query, (user_id, limit, skip))
#
# return [Photo(**row) for row in rows]
#
# async def check_friendship(self, user_id: int, photo_id: int) -> bool:
# photo_query = PhotoQueries.SELECT_PHOTO_OWNER
# photo_row = await fetch_one(photo_query, (photo_id,))
#
# if not photo_row:
# return False
#
# photo_owner_id = photo_row["user_id"]
#
# if user_id == photo_owner_id:
# return True
#
# from ..services.friendship_service import FriendshipService
#
# friendship_service = FriendshipService()
# return await friendship_service.check_friendship(user_id, photo_owner_id)
#
# async def add_comment(
# self, photo_id: int, user_id: int, comment_data: CommentCreate
# ) -> PhotoComment:
# if not await self.check_friendship(user_id, photo_id):
# raise ValueError("Cannot add comment before being friends")
# created_at = datetime.now()
#
# query = PhotoQueries.INSERT_COMMENT
#
# await execute(query, (photo_id, user_id, comment_data.content, created_at))
#
# row = await fetch_one(
# PhotoQueries.SELECT_LATEST_COMMENT,
# (photo_id, user_id),
# )
#
# return PhotoComment(**row)
#
# async def get_photo_comments(self, photo_id: int) -> List[CommentResponse]:
# query = PhotoQueries.SELECT_PHOTO_COMMENTS
#
# rows = await fetch_all(query, (photo_id,))
#
# return [CommentResponse(**row) for row in rows]
#
# async def apply_filter(
# self,
# photo_id: int,
# filter_type: str,
# user_id: int,
# cover: bool = False,
# title: str = None,
# ) -> str:
# photo_query = PhotoQueries.SELECT_PHOTO_BY_ID
# row = await fetch_one(photo_query, (photo_id, user_id))
#
# if not row:
# raise ValueError("Photo not found")
#
# original_path = row["image_path"]
# filtered_path = await self.image_processor.apply_filter(
# original_path, filter_type
# )
#
# if cover:
# photo_update_query = PhotoQueries.UPDATE_PHOTO_PATH
# await execute(photo_update_query, (filtered_path, photo_id, user_id))
# else:
# row = await fetch_one(
# PhotoQueries.SELECT_PHOTO_ALBUM_NAME, (photo_id, user_id)
# )
# photo_create_query = PhotoQueries.INSERT_PHOTO
# await execute(
# photo_create_query,
# (user_id, row["album_name"], filtered_path, title, datetime.now()),
# )
#
# return filtered_path
#
# async def delete_photo(self, photo_id: int, user_id: int) -> bool:
# try:
# query = PhotoQueries.DELETE_PHOTO
# await execute(query, (photo_id, user_id))
# return True
# except Exception:
# return False

View file

@ -0,0 +1,149 @@
from fastapi import HTTPException
from typing import List
from Backend.utils.queries.room import RoomQueries
from Backend.schemas.room import (
FurnitureItem,
RoomFurniturePlacement,
FurniturePlacementRequest,
Room,
RoomTypes,
RoomTypeResponse,
room_path,
Furniture,
furniture_path,
RoomFurnitureResponse,
FurniturePlacementResponse,
RoomResponse,
)
from Backend.utils.db import fetch_all, fetch_one, execute
class RoomService:
@staticmethod
async def init_db():
await execute(RoomQueries.CREATE_TABLE)
await execute(RoomQueries.CREATE_TABLE_ROOM_FURNITURE)
await execute(RoomQueries.CREATE_TABLE_USER_FURNITURE)
async def get_or_create_room(self, user_id: int) -> int:
row = await fetch_one(RoomQueries.SELECT_ROOM_ID_BY_USER_ID, (user_id,))
if row:
return row["id"]
await execute(RoomQueries.INSERT_ROOM, (user_id, "My Room", "room_1"))
new_row = await fetch_one(RoomQueries.SELECT_ROOM_ID_BY_USER_ID, (user_id,))
return new_row["id"]
async def get_room_by_id(self, id: int) -> Room:
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_ID, (id,))
if row is None:
raise HTTPException(status_code=404, detail="Room not found")
return Room(**row)
async def get_room_by_userId(self, id: int) -> Room:
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_USER_ID, (id,))
if row is None:
raise HTTPException(status_code=404, detail="Room not found")
return Room(**row)
async def get_room_types(self) -> List[RoomTypeResponse]:
return [
RoomTypeResponse(type=rt.value, image_path=room_path[rt.value])
for rt in RoomTypes
]
async def update_room_name(self, room_id: int, new_name: str):
await execute(RoomQueries.UPDATE_ROOM_NAME, (new_name, room_id))
async def update_room_type(self, room_id: int, new_type: RoomTypes):
query = RoomQueries.UPDATE_ROOM_TYPE
await execute(query, (new_type.value, room_id))
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_ID, (room_id,))
return Room(**row)
# furniture
async def get_furniture_catalog(self) -> List[FurnitureItem]:
furniture_list = []
for f in list(Furniture):
furniture_list.append(
FurnitureItem(
name=f,
image_path=furniture_path[f],
width=int(f[-1]),
)
)
return furniture_list
async def get_room_furnitures(self, room_id: int) -> RoomFurnitureResponse:
rows = await fetch_all(RoomQueries.SELECT_ROOM_FURNITURE, (room_id,))
furniture_placement_response = []
for row in rows:
furniture_placement_response.append(
FurniturePlacementResponse(
furniture_name=row["furniture_name"],
x=row["x"],
y=row["y"],
image_path=furniture_path[row["furniture_name"]],
)
)
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_ID, (room_id,))
return RoomFurnitureResponse(
furniture=furniture_placement_response,
room=RoomResponse(
id=row["id"],
user_id=row["user_id"],
room_name=row["room_name"],
room_type=row["room_type"],
room_image_path=room_path[row["room_type"]],
),
)
async def place_furniture(self, room_id: int, request: FurniturePlacementRequest):
is_oneone = furniture_path.get(request.furniture_name + "1") is not None
placed_furnitures = await fetch_all(
RoomQueries.SELECT_ROOM_FURNITURE, (room_id,)
)
for f in placed_furnitures:
if f["x"] == request.x and f["y"] == request.y:
raise HTTPException(status_code=409, detail="Furniture already placed")
await execute(
RoomQueries.INSERT_ROOM_FURNITURE,
(room_id, request.furniture_name, request.x, request.y),
)
if not is_oneone:
await execute(
RoomQueries.INSERT_ROOM_FURNITURE,
(room_id, "1", request.x - 1, request.y),
)
async def remove_furniture(self, room_id: int, x: int, y: int, furniture_name: str):
is_oneone = furniture_path.get(furniture_name + "1") is not None
await execute(RoomQueries.DELETE_FURNITURE, (room_id, x, y))
if not is_oneone:
await execute(RoomQueries.DELETE_FURNITURE, (room_id, x - 1, y))
async def add_furniture(self, user_id: int, furniture_name: str):
await execute(RoomQueries.INSERT_USER_FURNITURE, (user_id, furniture_name))
async def get_user_furniture(self, user_id: int):
rows = await fetch_all(RoomQueries.SELECT_USER_FURNITURE, (user_id,))
furniture_list = []
for row in rows:
furniture_list.append(
FurnitureItem(
name=row["furniture_name"],
image_path=furniture_path[row["furniture_name"]],
width=int(row["furniture_name"][-1]),
)
)
return furniture_list

View file

@ -0,0 +1,33 @@
import httpx
class StoreService:
def __init__(self):
self.SERVER_URL = "https://dotory.ana.st"
async def get_dotory_by_id(self, user_id: int):
async with httpx.AsyncClient() as client:
response = await client.get(f"{self.SERVER_URL}/")
response_json = response.json()
return
async def register_user(self, user_id: int):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.SERVER_URL}/", json={"user_id": user_id}
)
return
async def buy_product(self, product_id: int, user_id: int):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.SERVER_URL}/", json={"user_id": user_id}
)
return
async def update_user_dotory(self, user_id: int, dotoryNum: int):
async with httpx.AsyncClient() as client:
response = await client.put(
f"{self.SERVER_URL}/", json={"num": dotoryNum}
)
return

View file

@ -0,0 +1,178 @@
from datetime import datetime, timezone, timedelta
from typing import Optional, List
import os
from fastapi import UploadFile
from Backend.utils.db import execute, fetch_one, fetch_all
from Backend.utils.image_processor import ImageProcessor
from Backend.utils.queries.user import UserQueries
from Backend.schemas.user import User, UserCreate, UserResponse, UserUpdate
from Backend.services.store_service import StoreService
store_service = StoreService()
class UserService:
def __init__(self):
self.image_processor = ImageProcessor()
self.upload_dir = "uploads/profile"
os.makedirs(self.upload_dir, exist_ok=True)
@staticmethod
async def init_db():
await execute(UserQueries.CREATE_TABLE)
async def create_user(
self, user_data: UserCreate, profile_file: UploadFile = None
) -> User:
password_hash, salt = User.hash_password(user_data.password)
if profile_file is not None:
await self.image_processor.validate_image_file(
)
image_path = await self.image_processor.write_file_and_get_image_path(
profile_file, upload_dir=self.upload_dir
)
query = UserQueries.INSERT_USER_WITH_PROFILE
params = (
user_data.username,
user_data.email,
password_hash,
salt,
image_path,
)
else:
query = UserQueries.INSERT_USER_WITHOUT_PROFILE
params = (user_data.username, user_data.email, password_hash, salt)
await execute(
query,
params,
)
row = await fetch_one(
UserQueries.SELECT_BY_USERNAME,
(user_data.username,),
)
if row is None:
raise Exception("User creation failed")
row = dict(row)
if isinstance(row["created_at"], str):
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
row["is_active"] = bool(row["is_active"])
await store_service.register_user(row["id"])
return User(**row)
async def get_user_by_username(self, username: str) -> Optional[User]:
row = await fetch_one(
UserQueries.SELECT_BY_USERNAME,
(username,),
)
if row is None:
return None
row = dict(row)
if isinstance(row["created_at"], str):
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
row["is_active"] = bool(row["is_active"])
return User(**row)
async def get_user_by_email(self, email: str) -> Optional[User]:
row = await fetch_one(
UserQueries.SELECT_BY_EMAIL,
(email,),
)
if row is None:
return None
row = dict(row)
if isinstance(row["created_at"], str):
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
row["is_active"] = bool(row["is_active"])
return User(**row)
async def authenticate_user(self, username: str, password: str) -> Optional[User]:
user =
if user and :
return user
return None
async def delete_user(self, username: str) -> bool:
try:
query = UserQueries.DELETE_USER_BY_USERNAME
await execute(query, (username,))
return True
except Exception:
return False
async def find_user(self, username: str) -> List[UserResponse]:
query = UserQueries.SELECT_BY_USERNAME_LIKE
rows = await fetch_all(
query,
("%" + username + "%",),
)
return [User(**row).to_response() for row in rows]
async def get_user_by_id(self, user_id: int) -> Optional[User]:
row = await fetch_one(UserQueries.SELECT_BY_ID, (user_id,))
if row is None:
return None
row = dict(row)
if isinstance(row["created_at"], str):
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
row["is_active"] = bool(row["is_active"])
return User(**row)
async def update_user(
self, user: User, user_data: UserUpdate, profile_file: UploadFile = None
) -> User:
update_fields = {}
if user_data.email:
existing_user = await fetch_one(
UserQueries.SELECT_USER_BY_EMAIL_AND_NOT_ID,
(user_data.email, user.id),
)
if existing_user:
raise ValueError("Email already registered")
update_fields["email"] = user_data.email
if user_data.password:
password_hash, salt = User.hash_password(user_data.password)
update_fields["password_hash"] = password_hash
update_fields["salt"] = salt
if profile_file:
await self.image_processor.validate_image_file(
profile_file.filename, profile_file.size
)
image_path = await self.image_processor.write_file_and_get_image_path(
profile_file, upload_dir=self.upload_dir
)
update_fields["profile_image_path"] = image_path
if not update_fields:
return user
set_clause = ", ".join(f"{key} = ?" for key in update_fields.keys())
query = UserQueries.UPDATE_USER_BY_ID.format(set_clause)
params = list(update_fields.values())
params.append(user.id)
await execute(query, tuple(params))
return await self.get_user_by_id(user.id)

View file

81
Backend/tests/conftest.py Normal file
View file

@ -0,0 +1,81 @@
import pytest
from fastapi.testclient import TestClient
import sys
import os
# Add Backend to path
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from Backend import app
@pytest.fixture(scope="module")
def client():
with TestClient(app) as c:
yield c
@pytest.fixture
def authenticated_user(client):
user_data = {
"username": "testuser_authenticated",
"email": "test_auth@example.com",
"password": "testpassword123",
}
# Register user
response = client.post("/api/user/register", data=user_data)
# Login to get token
login_data = {"username": user_data["username"], "password": user_data["password"]}
response = client.post("/api/user/login", json=login_data)
token = response.json()["access_token"]
yield {"token": token, "user_data": user_data}
# Cleanup: delete user via API
headers = {"Authorization": f"Bearer {token}"}
client.delete(f"/api/user/{user_data['username']}", headers=headers)
@pytest.fixture
def two_authenticated_users(client):
user1_data = {
"username": "testuser1",
"email": "test1@example.com",
"password": "testpassword123",
}
user2_data = {
"username": "testuser2",
"email": "test2@example.com",
"password": "testpassword123",
}
# Register users
client.post("/api/user/register", data=user1_data)
client.post("/api/user/register", data=user2_data)
# Login users
login1_data = {
"username": user1_data["username"],
"password": user1_data["password"],
}
response1 = client.post("/api/user/login", json=login1_data)
token1 = response1.json()["access_token"]
login2_data = {
"username": user2_data["username"],
"password": user2_data["password"],
}
response2 = client.post("/api/user/login", json=login2_data)
token2 = response2.json()["access_token"]
yield {
"user1": {"token": token1, "username": user1_data["username"]},
"user2": {"token": token2, "username": user2_data["username"]},
}
# Cleanup
headers1 = {"Authorization": f"Bearer {token1}"}
headers2 = {"Authorization": f"Bearer {token2}"}
client.delete(f"/api/user/{user1_data['username']}", headers=headers1)
client.delete(f"/api/user/{user2_data['username']}", headers=headers2)

View file

@ -0,0 +1,71 @@
from fastapi.testclient import TestClient
from Backend.schemas.avatar import (
AvatarUpdate,
AvatarType,
TopClothesType,
BottomClothesType,
)
def test_get_my_avatar(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
response = client.get("/api/avatar", headers=headers)
assert response.status_code == 200
avatar_data = response.json()
assert "id" in avatar_data
assert "user_id" in avatar_data
assert avatar_data["avatar_type"]["name"] == AvatarType.MALE.value
def test_update_my_avatar(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
response = client.get("/api/avatar", headers=headers)
assert response.status_code == 200
update_data = AvatarUpdate(
avatar_type=AvatarType.FEMALE,
top_clothe_type=TopClothesType.SCHOOL_CLOTHES,
bottom_clothe_type=BottomClothesType.SCHOOL_CLOTHES_2,
)
response = client.put(
"/api/avatar", json=update_data.model_dump(mode="json"), headers=headers
)
assert response.status_code == 200
avatar_data = response.json()
assert avatar_data["avatar_type"]["name"] == update_data.avatar_type.value
assert avatar_data["top_clothe_type"]["name"] == update_data.top_clothe_type.value
assert (
avatar_data["bottom_clothe_type"]["name"]
== update_data.bottom_clothe_type.value
)
def test_get_avatar_options(client: TestClient):
response = client.get("/api/avatar/options")
assert response.status_code == 200
options = response.json()
assert "avatar_types" in options
assert "top_clothe_types" in options
assert "bottom_clothe_types" in options
assert all(isinstance(item, str) for item in options["avatar_types"])
assert all(isinstance(item, str) for item in options["top_clothe_types"])
assert all(isinstance(item, str) for item in options["bottom_clothe_types"])
def test_get_avatar_by_user_id(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
response = client.get("/api/avatar", headers=headers)
assert response.status_code == 200
user_id = response.json()["user_id"]
response = client.get(f"/api/avatar/{user_id}", headers=headers)
assert response.status_code == 200
avatar_data = response.json()
assert "id" in avatar_data
assert avatar_data["user_id"] == user_id
assert avatar_data["avatar_type"]["name"] == AvatarType.MALE.value
# Test for a non-existent user
response = client.get("/api/avatar/9999", headers=headers)
assert response.status_code == 404

View file

@ -0,0 +1,52 @@
from io import BytesIO
def test_diary_operations(client, authenticated_user):
token = authenticated_user["token"]
headers = {"Authorization": f"Bearer {token}"}
# Create Diary
diary_data = {
"title": "test title",
"content": "test content",
"category": "test category",
}
response = client.post(
"/api/diary",
data=diary_data,
files={"file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg")},
headers=headers,
)
assert response.status_code == 200
diary_id = response.json()["id"]
assert response.json()["title"] == "test title"
# Get Diary
response = client.get(f"/api/diary/{diary_id}", headers=headers)
assert response.status_code == 200
assert response.json()["id"] == diary_id
# List Diaries
response = client.get("/api/diary", headers=headers)
assert response.status_code == 200
assert len(response.json()) > 0
# Update Diary
updated_diary_data = {"title": "updated title", "content": "updated content"}
response = client.put(
f"/api/diary/{diary_id}",
data=updated_diary_data,
files={"file": ("b.jpg", BytesIO(b"bbb"), "image/jpeg")},
headers=headers,
)
assert response.status_code == 200
assert response.json()["title"] == "updated title"
# Delete Diary
response = client.delete(f"/api/diary/{diary_id}", headers=headers)
assert response.status_code == 200
assert response.json()["message"] == "Diary deleted successfully"
# Verify Deletion
response = client.get(f"/api/diary/{diary_id}", headers=headers)
assert response.status_code == 400

View file

@ -0,0 +1,45 @@
def test_friendship_flow(client, two_authenticated_users):
user1 = two_authenticated_users["user1"]
user2 = two_authenticated_users["user2"]
headers1 = {"Authorization": f"Bearer {user1['token']}"}
headers2 = {"Authorization": f"Bearer {user2['token']}"}
# User 1 sends friendship request to User 2
response = client.post(
"/api/friendship/request",
json={"friend_username": user2["username"]},
headers=headers1,
)
assert response.status_code == 200
friendship_id = response.json()["id"]
assert response.json()["status"] == "pending"
# User 2 accepts friendship request
response = client.put(f"/api/friendship/{friendship_id}/accept", headers=headers2)
assert response.status_code == 200
assert response.json()["status"] == "accepted"
# User 1 lists friends
response = client.get("/api/friendship", headers=headers1)
assert response.status_code == 200
assert any(f["id"] == friendship_id for f in response.json())
# User 2 lists friends
response = client.get("/api/friendship", headers=headers2)
assert response.status_code == 200
assert any(f["id"] == friendship_id for f in response.json())
# User 1 deletes friendship
response = client.delete(f"/api/friendship/{friendship_id}", headers=headers1)
assert response.status_code == 200
assert response.json()["message"] == "Friendship deleted successfully"
# Verify deletion for both users
response = client.get("/api/friendship", headers=headers1)
assert response.status_code == 200
assert not any(f["id"] == friendship_id for f in response.json())
response = client.get("/api/friendship", headers=headers2)
assert response.status_code == 200
assert not any(f["id"] == friendship_id for f in response.json())

View file

@ -0,0 +1,38 @@
def test_guest_book(client, authenticated_user):
token = authenticated_user["token"]
headers = {"Authorization": f"Bearer {token}"}
user_data = {
"username": "testtarget",
"email": "test@example.com",
"password": "testpassword123",
}
response = client.post("/api/user/register", data=user_data)
assert response.status_code == 201
user_id = response.json()["id"]
username = response.json()["username"]
response = client.post(
"/api/guestbook",
json={"target_user_id": user_id, "content": "test"},
headers=headers,
)
assert response.status_code == 201
assert response.json()["content"] == "test"
id = response.json()["id"]
response = client.get(f"/api/guestbook/{user_id}", headers=headers)
assert response.status_code == 200
assert response.json()[0]["content"] == "test"
response = client.put(
f"/api/guestbook/{id}", json={"content": "test2"}, headers=headers
)
assert response.status_code == 200
assert response.json()["content"] == "test2"
response = client.delete(f"/api/guestbook/{id}", headers=headers)
assert response.status_code == 200
client.delete(f"/api/user/{username}", headers=headers)
assert response.status_code == 200

View file

@ -0,0 +1,32 @@
# def test_letter_operations(client, authenticated_user):
# token = authenticated_user["token"]
# headers = {"Authorization": f"Bearer {token}"}
#
# # Create Letter
# letter_data = {"content": "test content"}
# response = client.post("/api/letter", json=letter_data, headers=headers)
# assert response.status_code == 200
# letter_id = response.json()["id"]
# assert response.json()["content"] == "test content"
#
# # Get Letter
# response = client.get(f"/api/letter/{letter_id}", headers=headers)
# assert response.status_code == 200
# assert response.json()["id"] == letter_id
#
# # Update Letter
# updated_letter_data = {"content": "updated content"}
# response = client.put(
# f"/api/letter/{letter_id}", json=updated_letter_data, headers=headers
# )
# assert response.status_code == 200
# assert response.json()["content"] == "updated content"
#
# # Delete Letter
# response = client.delete(f"/api/letter/{letter_id}", headers=headers)
# assert response.status_code == 200
# assert response.json()["detail"] == "Letter deleted"
#
# # Verify Deletion
# response = client.get(f"/api/letter/{letter_id}", headers=headers)
# assert response.status_code == 400

View file

@ -0,0 +1,94 @@
# from io import BytesIO
# import json
#
#
# def test_photo_upload_and_delete(client, authenticated_user):
# token = authenticated_user["token"]
# headers = {"Authorization": f"Bearer {token}"}
#
# # Upload Photo
# photo_data = {"album_name": "test_album", "title": "test_title"}
# photo_data_json = json.dumps(photo_data).encode("utf-8")
#
# response = client.post(
# "/api/photo/upload",
# files={
# "photo_data": (
# "photo_data.json",
# BytesIO(photo_data_json),
# "application/json",
# ),
# "file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg"),
# },
# headers=headers,
# )
# assert response.status_code == 200
# photo_id = response.json()["id"]
# assert response.json()["album_name"] == "test_album"
#
# # Delete Photo
# response = client.delete(f"/api/photo/{photo_id}", headers=headers)
# assert response.status_code == 200
# assert response.json()["message"] == "Photo deleted successfully"
#
#
# def test_photo_commenting(client, two_authenticated_users):
# user1 = two_authenticated_users["user1"]
# user2 = two_authenticated_users["user2"]
#
# headers1 = {"Authorization": f"Bearer {user1['token']}"}
# headers2 = {"Authorization": f"Bearer {user2['token']}"}
#
# # User 1 uploads a photo
# photo_data = {"album_name": "test_album", "title": "test_title"}
# photo_data_json = json.dumps(photo_data).encode("utf-8")
# response = client.post(
# "/api/photo/upload",
# files={
# "photo_data": (
# "photo_data.json",
# BytesIO(photo_data_json),
# "application/json",
# ),
# "file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg"),
# },
# headers=headers1,
# )
# assert response.status_code == 200
# photo_id = response.json()["id"]
#
# # User 2 cannot comment before being friends
# response = client.post(
# f"/api/photo/{photo_id}/comment",
# json={"content": "test comment"},
# headers=headers2,
# )
# assert response.status_code == 400
#
# # User 1 sends friendship request to User 2
# response = client.post(
# "/api/friendship/request",
# json={"friend_username": user2["username"]},
# headers=headers1,
# )
# assert response.status_code == 200
# friendship_id = response.json()["id"]
#
# # User 2 accepts friendship request
# response = client.put(f"/api/friendship/{friendship_id}/accept", headers=headers2)
# assert response.status_code == 200
#
# # User 2 can now comment
# response = client.post(
# f"/api/photo/{photo_id}/comment",
# json={"content": "test comment"},
# headers=headers2,
# )
# assert response.status_code == 200
# assert response.json()["content"] == "test comment"
#
# # User 1 can see the comment
# response = client.get(f"/api/photo/{photo_id}/comments", headers=headers1)
# assert response.status_code == 200
# assert len(response.json()) > 0
# assert response.json()[0]["content"] == "test comment"

161
Backend/tests/test_room.py Normal file
View file

@ -0,0 +1,161 @@
from fastapi.testclient import TestClient
from Backend.schemas.room import Furniture, RoomTypes
def test_get_furniture_catalog(client: TestClient):
response = client.get("/api/room/catalog")
assert response.status_code == 200
catalog = response.json()
assert isinstance(catalog, list)
assert len(catalog) > 0
for item in catalog:
assert "name" in item
assert "image_path" in item
assert "width" in item
def test_get_room_types(client: TestClient):
response = client.get("/api/room/types")
assert response.status_code == 200
types = response.json()
assert isinstance(types, list)
assert len(types) > 0
for room_type in types:
assert "type" in room_type
assert "image_path" in room_type
def test_get_my_room(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
response = client.get("/api/room/", headers=headers)
assert response.status_code == 200
room_data = response.json()
assert "id" in room_data
assert "room_name" in room_data
assert "room_type" in room_data
assert "room_image_path" in room_data
def test_update_room_name(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
response = client.get("/api/room/", headers=headers)
new_name = "My Awesome Room"
response = client.put("/api/room/", json={"new_name": new_name}, headers=headers)
assert response.status_code == 200
assert response.json() == {"message": "Room name updated successfully"}
# Verify the change
response = client.get("/api/room/", headers=headers)
assert response.status_code == 200
assert response.json()["room_name"] == new_name
def test_update_room_type(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
response = client.get("/api/room/", headers=headers)
new_type = RoomTypes.ROOM_2.value
response = client.patch("/api/room/", json={"type": new_type}, headers=headers)
assert response.status_code == 200
updated_room = response.json()
assert updated_room["room_type"] == new_type
def test_get_my_room_layout(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
response = client.get("/api/room/layout", headers=headers)
assert response.status_code == 200
layout_data = response.json()
assert "room" in layout_data
assert "furniture" in layout_data
assert isinstance(layout_data["furniture"], list)
def test_place_furniture(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
placement_data = {
"furniture_name": Furniture.SOFA_0.value,
"x": 5,
"y": 5,
}
response = client.post("/api/room/furniture", json=placement_data, headers=headers)
assert response.status_code == 200
assert response.json() == {"message": "Furniture placed successfully"}
# Verify placement
layout_response = client.get("/api/room/layout", headers=headers)
assert layout_response.status_code == 200
new_layout = layout_response.json()
assert any(
item["furniture_name"] == placement_data["furniture_name"]
and item["x"] == placement_data["x"]
and item["y"] == placement_data["y"]
for item in new_layout["furniture"]
)
# Cleanup
client.delete(
f"/api/room/furniture?x={placement_data['x']}&y={placement_data['y']}&furniture_name={placement_data['furniture_name']}",
headers=headers,
)
def test_remove_furniture(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
placement_data = {
"furniture_name": Furniture.CHAIR_0.value,
"x": 1,
"y": 1,
}
# Place furniture first
client.post("/api/room/furniture", json=placement_data, headers=headers)
# Remove furniture
response = client.delete(
f"/api/room/furniture?x={placement_data['x']}&y={placement_data['y']}&furniture_name={placement_data['furniture_name']}",
headers=headers,
)
assert response.status_code == 200
assert response.json() == {"message": "Furniture removed successfully"}
# Verify removal
final_layout_response = client.get("/api/room/layout", headers=headers)
assert final_layout_response.status_code == 200
final_layout = final_layout_response.json()
assert not any(
item["furniture_name"] == placement_data["furniture_name"]
and item["x"] == placement_data["x"]
and item["y"] == placement_data["y"]
for item in final_layout["furniture"]
)
def test_invalid_furniture_placement(client: TestClient, authenticated_user):
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
# Test with invalid coordinates (out of bounds)
invalid_coords_data = {
"furniture_name": Furniture.CHAIR_0.value,
"x": 11,
"y": -1,
}
response = client.post(
"/api/room/furniture", json=invalid_coords_data, headers=headers
)
assert response.status_code == 422 # Pydantic validation error
# Test collision
placement_data = {"furniture_name": Furniture.CHAIR_0.value, "x": 1, "y": 1}
# Place once
client.post("/api/room/furniture", json=placement_data, headers=headers)
# Try to place again in the same spot
response = client.post("/api/room/furniture", json=placement_data, headers=headers)
assert response.status_code == 400
assert "already placed" in response.json()["detail"]
# Cleanup
client.delete(
f"/api/room/furniture?x={placement_data['x']}&y={placement_data['y']}&furniture_name={placement_data['furniture_name']}",
headers=headers,
)

View file

@ -0,0 +1,68 @@
from io import BytesIO
def test_user_registration_and_login(client):
# Registration
user_data = {
"username": "testuser999",
"email": "test@example.com",
"password": "testpassword123",
}
response = client.post(
"/api/user/register",
data=user_data,
files={"file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg")},
)
assert response.status_code == 201
assert response.json()["username"] == "testuser999"
# Duplicate registration
response = client.post(
"/api/user/register",
data=user_data,
files={"file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg")},
)
assert response.status_code == 400
assert response.json()["detail"] == "Username already registered"
# Login
login_data = {
"username": "testuser999",
"password": "testpassword123",
}
response = client.post("/api/user/login", json=login_data)
assert response.status_code == 200
assert "access_token" in response.json()
# Login with wrong password
login_data["password"] = "wrongpassword"
response = client.post("/api/user/login", json=login_data)
assert response.status_code == 401
assert response.json()["detail"] == "Invalid username or password"
# delete
response = client.delete(f"/api/user/{user_data['username']}")
assert response.status_code == 200
def test_get_user_profile(client, authenticated_user):
token = authenticated_user["token"]
username = authenticated_user["user_data"]["username"]
headers = {"Authorization": f"Bearer {token}"}
response = client.get(f"/api/user/profile/{username}", headers=headers)
assert response.status_code == 200
assert response.json()["username"] == username
def test_user_delete(client, authenticated_user):
token = authenticated_user["token"]
username = authenticated_user["user_data"]["username"]
headers = {"Authorization": f"Bearer {token}"}
response = client.delete(f"/api/user/{username}", headers=headers)
assert response.status_code == 200
assert response.json()["detail"] == "User deleted"

View file

53
Backend/utils/db.py Normal file
View file

@ -0,0 +1,53 @@
import aiosqlite
from typing import Any, List, Tuple, Optional, Dict, Union
DB_PATH = "database.sqlite3"
# Generated by Github Copilot
# db 사용을 편하게 하기 위한 함수
async def get_db_connection(db_path: str = DB_PATH) -> aiosqlite.Connection:
return await aiosqlite.connect(db_path)
async def execute(
query: str,
params: Union[Tuple[Any, ...], Dict[str, Any]] = (),
db_path: str = DB_PATH,
) -> None:
async with aiosqlite.connect(db_path) as db:
await db.execute(query, params)
await db.commit()
async def fetch_one(
query: str,
params: Union[Tuple[Any, ...], Dict[str, Any]] = (),
db_path: str = DB_PATH,
) -> Optional[aiosqlite.Row]:
async with aiosqlite.connect(db_path) as db:
db.row_factory = aiosqlite.Row
async with db.execute(query, params) as cursor:
return await cursor.fetchone()
async def fetch_all(
query: str,
params: Union[Tuple[Any, ...], Dict[str, Any]] = (),
db_path: str = DB_PATH,
) -> List[aiosqlite.Row]:
async with aiosqlite.connect(db_path) as db:
db.row_factory = aiosqlite.Row
async with db.execute(query, params) as cursor:
return await cursor.fetchall()
async def executemany(
query: str,
seq_of_params: List[Union[Tuple[Any, ...], Dict[str, Any]]],
db_path: str = DB_PATH,
) -> None:
async with aiosqlite.connect(db_path) as db:
await db.executemany(query, seq_of_params)
await db.commit()

View file

@ -0,0 +1,168 @@
class PhotoQueries:
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
album_name TEXT NOT NULL,
image_path TEXT NOT NULL,
title TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
"""
CREATE_COMMENTS_TABLE = """
CREATE TABLE IF NOT EXISTS photo_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
photo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (photo_id) REFERENCES photos (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
"""
INSERT_PHOTO = """
INSERT INTO photos (user_id, album_name, image_path, title, created_at)
VALUES (?, ?, ?, ?, ?)
"""
SELECT_USER_PHOTOS = """
SELECT * FROM photos
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
"""
SELECT_USER_PHOTOS_BY_ALBUM = """
SELECT * FROM photos
WHERE user_id = ? AND album_name = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
"""
SELECT_LATEST_USER_PHOTO = """
SELECT * FROM photos WHERE user_id = ? ORDER BY id DESC LIMIT 1
"""
SELECT_PHOTO_OWNER = """
SELECT user_id FROM photos WHERE id = ?
"""
SELECT_PHOTO_ALBUM_NAME = """
SELECT album_name FROM photos WHERE id = ? AND user_id = ?
"""
INSERT_COMMENT = """
INSERT INTO photo_comments (photo_id, user_id, content, created_at)
VALUES (?, ?, ?, ?)
"""
SELECT_LATEST_COMMENT = """
SELECT * FROM photo_comments WHERE photo_id = ? AND user_id = ? ORDER BY id DESC LIMIT 1
"""
SELECT_PHOTO_COMMENTS = """
SELECT pc.*, u.username
FROM photo_comments pc
JOIN users u ON pc.user_id = u.id
WHERE pc.photo_id = ?
ORDER BY pc.created_at ASC
"""
SELECT_PHOTO_BY_ID = """
SELECT * FROM photos WHERE id = ? AND user_id = ?
"""
DELETE_PHOTO = """
DELETE FROM photos WHERE id = ? AND user_id = ?
"""
UPDATE_PHOTO_PATH = """
UPDATE photos SET image_path = ? WHERE id = ? AND user_id = ?
"""
class LetterQueries:
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS letters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_id INTEGER NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (sender_id) REFERENCES users (id) ON DELETE CASCADE
)
"""
INSERT_LETTER = """
INSERT INTO letters (sender_id, content)
VALUES (?, ?)
"""
SELECT_USER_LETTERS = """
SELECT * FROM letters
WHERE sender_id = ?
"""
SELECT_LATEST_USER_LETTER = """
SELECT * FROM letters WHERE sender_id = ? LIMIT 1
"""
SELECT_LETTER_BY_ID = """
SELECT * FROM letters WHERE id = ? AND sender_id = ?
"""
SELECT_LETTER_FOR_DELIVERY = """
SELECT * FROM letters WHERE id = ?
"""
SELECT_SENDER_USERNAME = """
SELECT username FROM users WHERE id = ?
"""
UPDATE_LETTER = """
UPDATE letters SET content = ? WHERE id = ? AND sender_id = ?
"""
DELETE_LETTER = """
DELETE FROM letters WHERE id = ? AND sender_id = ?
"""
class DatabaseIndexes:
USER_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
]
DIARY_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_diaries_user_id ON diaries(user_id)",
"CREATE INDEX IF NOT EXISTS idx_diaries_category ON diaries(category)",
"CREATE INDEX IF NOT EXISTS idx_diaries_created_at ON diaries(created_at)",
]
PHOTO_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_photos_user_id ON photos(user_id)",
"CREATE INDEX IF NOT EXISTS idx_photos_album ON photos(album_name)",
"CREATE INDEX IF NOT EXISTS idx_photo_comments_photo_id ON photo_comments(photo_id)",
]
FRIENDSHIP_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_friendships_user_id ON friendships(user_id)",
"CREATE INDEX IF NOT EXISTS idx_friendships_friend_id ON friendships(friend_id)",
"CREATE INDEX IF NOT EXISTS idx_friendships_status ON friendships(status)",
]
LETTER_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_letters_sender_id ON letters(sender_id)",
"CREATE INDEX IF NOT EXISTS idx_letters_sent_date ON letters(sent_date)",
]
AVATAR_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_avatars_user_id ON avatars(user_id)",
]
GUEST_BOOK_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_guest_books_user_id ON guest_books(user_id)",
"CREATE INDEX IF NOT EXISTS idx_guest_books_created_at ON guest_books(created_at)",
]

View file

@ -0,0 +1,75 @@
import aiosmtplib
from email.message import EmailMessage
from typing import Optional
import os
from pydantic import EmailStr
import ssl
# aiosmtplib을 사용한 비동기 이메일 전송
# EmailStr 사용
class EmailProcessor:
def __init__(self):
self.smtp_server = os.getenv("SMTP_SERVER", "smtp.gmail.com")
self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
self.sender_email = os.getenv("SENDER_EMAIL")
self.sender_password = os.getenv("SENDER_PASSWORD")
self.receiver_email = ""
async def send_email(
self,
subject: str,
content: str,
sender_email: EmailStr,
sender_password: str,
html_content: Optional[str] = None,
) -> bool:
if not self.sender_email or not self.sender_password:
print("Email credentials not configured, cannot send email.")
return False
if not sender_email or not sender_password:
print(
"Warning: Email credentials (SENDER_EMAIL, SENDER_PASSWORD) "
"are not configured in environment variables."
)
return False
message = EmailMessage()
message["Subject"] = subject
message["From"] = sender_email
message["To"] = self.receiver_email
message.set_content(content)
# HTML 내용이 있는 경우, 대체 콘텐츠로 추가
if html_content:
message.add_alternative(html_content, subtype="html")
try:
# 포트 465는 SMTPS (implicit TLS)를 사용하고, 포트 587은 STARTTLS를 사용합니다.
use_tls = self.smtp_port == 465
context = ssl.create_default_context() if use_tls else None
await aiosmtplib.send(
message,
hostname=self.smtp_server,
port=self.smtp_port,
username=sender_email,
password=sender_password,
use_tls=use_tls,
ssl_context=context,
timeout=10,
)
print(f"Email sent successfully to {self.receiver_email}")
return True
except aiosmtplib.SMTPException as e:
print(f"SMTP error occurred while sending to {self.receiver_email}: {e}")
return False
except Exception as e:
print(
f"An unexpected error occurred while sending email to {self.receiver_email}: {e}"
)
return False

View file

@ -0,0 +1,179 @@
import os
import aiofiles
from PIL import Image, ImageFilter, ImageEnhance
from datetime import datetime
from starlette.datastructures import UploadFile
class ImageProcessor:
def __init__(self):
self.filter_dir = "uploads/filtered"
os.makedirs(self.filter_dir, exist_ok=True)
async def apply_filter(self, image_path: str, filter_type: str) -> str:
"""Apply filter to image and return the path to filtered image"""
# Remove leading slash if exists for local file access
local_path = image_path.lstrip("/")
if not os.path.exists(local_path):
raise ValueError(f"Image file not found: {local_path}")
try:
with Image.open(local_path) as img:
# Convert to RGB if necessary
if img.mode != "RGB":
img = img.convert("RGB")
filtered_img = self._apply_filter_effect(img, filter_type)
# Generate filtered image filename
base_name = os.path.basename(image_path)
name, ext = os.path.splitext(base_name)
filtered_filename = f"{name}_{filter_type}{ext}"
filtered_path = os.path.join(self.filter_dir, filtered_filename)
# Save filtered image
filtered_img.save(filtered_path, quality=85, optimize=True)
return f"/uploads/filtered/{filtered_filename}"
except Exception as e:
raise ValueError(f"Failed to apply filter: {str(e)}")
def _apply_filter_effect(self, img: Image.Image, filter_type: str) -> Image.Image:
"""Apply specific filter effect to image"""
if filter_type == "none":
return img
elif filter_type == "vintage":
return self._apply_vintage_filter(img)
elif filter_type == "black_white":
return self._apply_black_white_filter(img)
elif filter_type == "sepia":
return self._apply_sepia_filter(img)
elif filter_type == "blur":
return self._apply_blur_filter(img)
elif filter_type == "sharpen":
return self._apply_sharpen_filter(img)
elif filter_type == "bright":
return self._apply_brightness_filter(img)
elif filter_type == "contrast":
return self._apply_contrast_filter(img)
else:
raise ValueError(f"Unknown filter type: {filter_type}")
def _apply_vintage_filter(self, img: Image.Image) -> Image.Image:
# Reduce saturation
enhancer = ImageEnhance.Color(img)
img = enhancer.enhance(0.7)
# Add slight warm tint
r, g, b = img.split()
r = ImageEnhance.Brightness(r).enhance(1.1)
g = ImageEnhance.Brightness(g).enhance(1.05)
b = ImageEnhance.Brightness(b).enhance(0.9)
return Image.merge("RGB", (r, g, b))
def _apply_black_white_filter(self, img: Image.Image) -> Image.Image:
return img.convert("L").convert("RGB")
def _apply_sepia_filter(self, img: Image.Image) -> Image.Image:
pixels = img.load()
width, height = img.size
for py in range(height):
for px in range(width):
r, g, b = pixels[px, py]
tr = int(0.393 * r + 0.769 * g + 0.189 * b)
tg = int(0.349 * r + 0.686 * g + 0.168 * b)
tb = int(0.272 * r + 0.534 * g + 0.131 * b)
pixels[px, py] = (min(255, tr), min(255, tg), min(255, tb))
return img
def _apply_blur_filter(self, img: Image.Image) -> Image.Image:
return img.filter(ImageFilter.GaussianBlur(radius=2))
def _apply_sharpen_filter(self, img: Image.Image) -> Image.Image:
return img.filter(ImageFilter.SHARPEN)
def _apply_brightness_filter(self, img: Image.Image) -> Image.Image:
enhancer = ImageEnhance.Brightness(img)
return enhancer.enhance(1.3)
def _apply_contrast_filter(self, img: Image.Image) -> Image.Image:
enhancer = ImageEnhance.Contrast(img)
return enhancer.enhance(1.2)
async def resize_image(
self, image_path: str, max_width: int = 800, max_height: int = 800
) -> str:
"""Resize image while maintaining aspect ratio"""
local_path = image_path.lstrip("/")
if not os.path.exists(local_path):
raise ValueError(f"Image file not found: {local_path}")
try:
with Image.open(local_path) as img:
# Calculate new size maintaining aspect ratio
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
# Generate resized image filename
base_name = os.path.basename(image_path)
name, ext = os.path.splitext(base_name)
resized_filename = f"{name}_resized{ext}"
resized_path = os.path.join(
os.path.dirname(local_path), resized_filename
)
# Save resized image
img.save(resized_path, quality=85, optimize=True)
return f"/{resized_path}"
except Exception as e:
raise ValueError(f"Failed to resize image: {str(e)}")
async def validate_image_file(self, filename: str, file_size: int):
"""Validate image file type and size"""
allowed_extensions = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
# Check file extension
_, ext = os.path.splitext(filename.lower())
if ext not in allowed_extensions:
raise ValueError("Unsupported file type")
if file_size > 20 * 1024 * 1024: # 20MB limit
raise ValueError("File size must be less than 20MB")
def get_safe_filename(self, filename: str) -> str:
"""Generate safe filename by removing dangerous characters"""
import re
# Keep only alphanumeric characters, dots, and hyphens
safe_name = re.sub(r"[^a-zA-Z0-9._-]", "_", filename)
# Add timestamp to avoid collisions
name, ext = os.path.splitext(safe_name)
timestamp = int(datetime.now().timestamp())
return f"{name}_{timestamp}{ext}"
async def write_file_and_get_image_path(
self, file: UploadFile, upload_dir: str
) -> str:
filename = self.get_safe_filename(file.filename)
file_path = os.path.join(upload_dir, filename)
async with aiofiles.open(file_path, "wb") as f:
content = await file.read()
await f.write(content)
return file_path

View file

View file

@ -0,0 +1,25 @@
class AvatarQueries:
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS avatars (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL,
avatar_type TEXT NOT NULL,
top_clothe_type TEXT,
bottom_clothe_type TEXT,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
"""
SELECT_USER_AVATAR = """
SELECT * FROM avatars WHERE user_id = ?
"""
INSERT_AVATAR = """
INSERT INTO avatars (user_id, avatar_type, top_clothe_type, bottom_clothe_type)
VALUES (?, ?, ?, ?)
"""
UPDATE_AVATAR = """
UPDATE avatars SET {fields} WHERE user_id = ?
"""

View file

@ -0,0 +1,71 @@
class DiaryQueries:
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS diaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
images TEXT,
category TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_submitted BOOLEAN DEFAULT FALSE,
email_sent BOOLEAN DEFAULT FALSE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
"""
INSERT_DIARY = """
INSERT INTO diaries (user_id, title, content, images, category, created_at, is_submitted, email_sent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"""
SELECT_USER_DIARIES = """
SELECT * FROM diaries
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
"""
SELECT_USER_DIARIES_BY_CATEGORY = """
SELECT * FROM diaries
WHERE user_id = ? AND category = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
"""
SELECT_BY_ID = """
SELECT * FROM diaries WHERE id = ?
"""
SELECT_BY_ID_WITH_USER_ID = """
SELECT * FROM diaries WHERE id = ? AND user_id = ?
"""
SELECT_LATEST_USER_DIARY = """
SELECT * FROM diaries WHERE user_id = ? ORDER BY id DESC LIMIT 1
"""
SELECT_IMAGES_BY_ID = """
SELECT images FROM diaries WHERE id = ?
"""
UPDATE_DIARY = """
UPDATE diaries SET {fields} WHERE id = ? AND user_id = ?
"""
DELETE_DIARY = """
DELETE FROM diaries WHERE id = ? AND user_id = ?
"""
UPDATE_SUBMISSION_STATUS = """
UPDATE diaries SET is_submitted = ? WHERE id = ? AND user_id = ?
"""
UPDATE_EMAIL_SENT = """
UPDATE diaries SET email_sent = ? WHERE id = ?
"""
UPDATE_DIARY_IMAGE_BY_ID = """
UPDATE diaries SET images = ? WHERE id = ?
"""

View file

@ -0,0 +1,89 @@
class FriendshipQueries:
"""친구 관계 관련 쿼리"""
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS friendships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
friend_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (friend_id) REFERENCES users (id) ON DELETE CASCADE,
UNIQUE(user_id, friend_id)
)
"""
SELECT_USER_BY_USERNAME = """
SELECT id FROM users WHERE username = ?
"""
SELECT_EXISTING_FRIENDSHIP = """
SELECT * FROM friendships
WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)
"""
INSERT_FRIENDSHIP = """
INSERT INTO friendships (user_id, friend_id, status, created_at)
VALUES (?, ?, ?, ?)
"""
SELECT_FRIENDSHIP_BY_IDS = """
SELECT * FROM friendships WHERE user_id = ? AND friend_id = ?
"""
SELECT_FRIENDSHIP_FOR_ACCEPT = """
SELECT f.*, u.username
FROM friendships f
JOIN users u ON f.user_id = u.id
WHERE f.id = ? AND f.friend_id = ? AND f.status = ?
"""
UPDATE_FRIENDSHIP_STATUS = """
UPDATE friendships SET status = ? WHERE id = ?
"""
SELECT_USER_FRIENDSHIPS = """
SELECT f.*, u.username
FROM friendships f
JOIN users u ON (
CASE
WHEN f.user_id = ? THEN f.friend_id = u.id
ELSE f.user_id = u.id
END
)
WHERE (f.user_id = ? OR f.friend_id = ?) AND f.status = ?
ORDER BY f.created_at DESC
"""
SELECT_USER_FRIENDSHIPS_BY_STATUS = """
SELECT f.*, u.username
FROM friendships f
JOIN users u ON (
CASE
WHEN f.user_id = ? THEN f.friend_id = u.id
ELSE f.user_id = u.id
END
)
WHERE (f.user_id = ? OR f.friend_id = ?) AND f.status = ?
ORDER BY f.created_at DESC
"""
DELETE_FRIENDSHIP = """
DELETE FROM friendships
WHERE id = ? AND (user_id = ? OR friend_id = ?)
"""
SELECT_PENDING_REQUESTS = """
SELECT f.*, u.username
FROM friendships f
JOIN users u ON f.user_id = u.id
WHERE f.friend_id = ? AND f.status = ?
ORDER BY f.created_at DESC
"""
CHECK_FRIENDSHIP_STATUS = """
SELECT * FROM friendships
WHERE ((user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?))
AND status = 'accepted'
"""

View file

@ -0,0 +1,40 @@
class GuestBookQueries:
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS guest_books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
target_user_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
"""
INSERT_GUEST_BOOK = """
INSERT INTO guest_books (target_user_id, user_id, content, created_at)
VALUES (?, ?, ?, ?)
"""
SELECT_TARGET_USER_GUEST_BOOKS = """
SELECT * FROM guest_books
WHERE target_user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
"""
SELECT_GUEST_BOOK_BY_ID = """
SELECT * FROM guest_books WHERE id = ?
"""
SELECT_GUEST_BOOK_BY_USER_ID = """
SELECT * FROM guest_books WHERE user_id = ? ORDER BY created_at DESC LIMIT 1
"""
UPDATE_GUEST_BOOK_BY_ID = """
UPDATE guest_books SET content = ?, updated_at=CURRENT_TIMESTAMP WHERE id = ?
"""
DELETE_GUEST_BOOK = """
DELETE FROM guest_books WHERE id = ? AND user_id = ?
"""

View file

@ -0,0 +1,81 @@
class RoomQueries:
"""마이 룸 관련 쿼리"""
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS rooms (
id INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE,
room_name TEXT,
room_type TEXT
)
"""
CREATE_TABLE_ROOM_FURNITURE = """
CREATE TABLE IF NOT EXISTS room_furnitures (
id INTEGER PRIMARY KEY AUTOINCREMENT,
room_id INTEGER NOT NULL,
furniture_name TEXT NOT NULL,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
)
"""
CREATE_TABLE_USER_FURNITURE = """
CREATE TABLE IF NOT EXISTS user_furnitures (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
furniture_name TEXT NOT NULL
)
"""
INSERT_USER_FURNITURE = """
INSERT INTO user_furnitures (user_id, furniture_name) VALUES (?, ?)
"""
INSERT_ROOM = """
INSERT INTO rooms (user_id, room_name, room_type) VALUES (?, ?, ?)
"""
INSERT_ROOM_FURNITURE = """
INSERT INTO room_furnitures (room_id, furniture_name, x, y) VALUES (?, ?, ?, ?)
"""
SELECT_ROOM_ID_BY_USER_ID = """
SELECT id FROM rooms WHERE user_id = ?
"""
SELECT_ROOM_BY_ID = """
SELECT * FROM rooms WHERE id = ?
"""
SELECT_FURNITURE = """
SELECT * FROM furnitures
"""
SELECT_ROOM_FURNITURE = """
SELECT id, furniture_name, x, y FROM room_furnitures WHERE room_id = ?
"""
SELECT_FURNITURE_ID_BY_X_Y = """
SELECT id FROM room_furnitures WHERE room_id = ? AND x = ? AND y = ?
"""
SELECT_USER_FURNITURE = """
SELECT * FROM user_furnitures WHERE user_id = ?
"""
UPDATE_ROOM_NAME = """
UPDATE rooms SET room_name = ? WHERE id = ?
"""
UPDATE_ROOM_TYPE = """
UPDATE rooms SET room_type = ? WHERE id = ?
"""
DELETE_FURNITURE = """
DELETE FROM room_furnitures WHERE room_id = ? AND x = ? AND y = ?
"""
SELECT_ROOM_BY_USER_ID = """
SELECT * FROM rooms WHERE user_id = ?
"""

View file

@ -0,0 +1,61 @@
class UserQueries:
"""사용자 관련 쿼리"""
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
profile_image_path TEXT DEFAULT 'upload/profile/default.jpg',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE
)
"""
INSERT_USER_WITHOUT_PROFILE = """
INSERT INTO users (username, email, password_hash, salt)
VALUES (?, ?, ?, ?)
"""
INSERT_USER_WITH_PROFILE = """
INSERT INTO users (username, email, password_hash, salt, profile_image_path)
VALUES (?, ?, ?, ?, ?)
"""
SELECT_BY_USERNAME = """
SELECT * FROM users WHERE username = ?
"""
SELECT_BY_EMAIL = """
SELECT * FROM users WHERE email = ?
"""
SELECT_BY_ID = """
SELECT * FROM users WHERE id = ?
"""
SELECT_BY_USERNAME_LIKE = """
SELECT * FROM users WHERE username LIKE ?
"""
DELETE_USER_BY_USERNAME = """
DELETE FROM users WHERE username = ?
"""
UPDATE_PROFILE_IMAGE_PATH_BY_USERNAME = """
UPDATE users SET profile_image_path = ? WHERE username = ?
"""
UPDATE_PROFILE_IMAGE_PATH_BY_ID = """
UPDATE users SET profile_image_path = ? WHERE id = ?
"""
UPDATE_USER_BY_ID = """
UPDATE users SET {} WHERE id = ?
"""
SELECT_USER_BY_EMAIL_AND_NOT_ID = """
SELECT * FROM users WHERE email = ? AND id != ?
"""

View file

@ -0,0 +1,64 @@
import sys
import os
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
# 프로젝트 루트를 Python 경로에 추가
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
app = FastAPI(
title="싸이월드 - 추억 속으로",
description="2000년대 감성의 소셜 네트워크 서비스",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
app.mount("/public", StaticFiles(directory="public"), name="public")
# 정적 파일 서빙
async def init_folders(app: FastAPI):
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
async def init_db():
for service in os.listdir("Backend/services"):
try:
if service.endswith(".py"):
module_name = "Backend.services." + service[:-3]
module = __import__(module_name, fromlist=[""])
service_class_name = service[:-3].split("_")[0] + "Service"
service_class_name = service_class_name.replace(
service_class_name[0], service_class_name[0].upper(), 1
)
service_class = getattr(module, service_class_name)
await service_class.init_db()
print(f"{service_class_name} : init_db")
except Exception as e:
print(f"failed to init_db {service}")
print(e)
async def startup_event():
await init_folders(app)
await init_db()
def init_FastAPI() -> FastAPI:
app.add_event_handler("startup", startup_event)
return app