commit 76a02076c93132338fb001ed281440e8270f9015 Author: janghanul090801 <25s100@sunrint.hs.kr> Date: Fri Sep 12 14:47:48 2025 +0900 2025 SSF Public diff --git a/.github/workflows/black-check.yml b/.github/workflows/black-check.yml new file mode 100644 index 0000000..cac0448 --- /dev/null +++ b/.github/workflows/black-check.yml @@ -0,0 +1,68 @@ +name: Black Formatter Check + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + black-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Get commit messages + id: commits + run: | + messages=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json commits --jq '.commits[].message') + if echo "$messages" | grep -q "chore: format code with black"; then + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + - name: Set up Python 3.x + if: steps.commits.outputs.skip == 'false' + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install black + if: steps.commits.outputs.skip == 'false' + run: pip install black + + - name: Run black --check + if: steps.commits.outputs.skip == 'false' + id: black-check + run: | + black . --check + continue-on-error: true + + - name: Comment on PR (success) + if: steps.commits.outputs.skip == 'false' && steps.black-check.outcome == 'success' + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "> [!NOTE]\n> ๐ŸŽ‰ Black ํฌ๋งคํŒ… ๊ฒ€์‚ฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์–ด์š”.\n`/review`๋กœ ๋ฆฌ๋ทฐ๋ฅผ ์š”์ฒญํ•˜์‹ค ์ˆ˜ ์žˆ์–ด์š”." + }) + + - name: Comment on PR (failure) + if: steps.commits.outputs.skip == 'false' && steps.black-check.outcome == 'failure' + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "> [!WARNING]\n> ๐Ÿ˜ข ์ฝ”๋“œ ํฌ๋งคํŒ…์ด ํ•„์š”ํ•ด์š”.\n`black .` ๋ช…๋ น์–ด๋กœ ์ฝ”๋“œ๋ฅผ ํฌ๋งคํŒ…ํ•˜๊ฑฐ๋‚˜ `/format` ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•ด ์ฃผ์„ธ์š”." + }) diff --git a/.github/workflows/comment-command.yml b/.github/workflows/comment-command.yml new file mode 100644 index 0000000..d6dccde --- /dev/null +++ b/.github/workflows/comment-command.yml @@ -0,0 +1,118 @@ +name: PR Comment Commands + +on: + issue_comment: + types: [created] + +jobs: + format: + if: > + startsWith(github.event.comment.body, '/format') && + github.event.issue.pull_request != null && + !contains(github.event.comment.body, '[!NOTE]') + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v3 + with: + ref: refs/pull/${{ github.event.issue.number }}/head + token: ${{ secrets.GH_TOKEN }} + + - name: Set up Python 3.x + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install black + run: pip install black + + - name: Run black + run: black . + + - name: Get PR branch name + id: get_pr_branch + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + core.setOutput('branch', pr.data.head.ref); + + - name: Commit & Push if changed + env: + PR_BRANCH: ${{ steps.get_pr_branch.outputs.branch }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if [ -n "$(git status --porcelain)" ]; then + git add . + git commit -m "chore: format code with black" + git push origin HEAD:${PR_BRANCH} + else + echo "No changes to commit." + fi + + - name: Comment on PR + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "> [!NOTE]\n> ์ฝ”๋“œ ํฌ๋งคํŒ…์ด ์™„๋ฃŒ๋˜์—ˆ์–ด์š”." + }) + + review: + if: > + startsWith(github.event.comment.body, '/review') && + github.event.issue.pull_request != null && + !contains(github.event.comment.body, '[!NOTE]') + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - name: Label PR as "review required" + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ["status: review required"] + }) + - name: Request reviewers + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + const submitter = pr.data.head.user.login; + const reviewers = ["norhu1130", "janghanul090801"].filter(r => r !== submitter); + if (reviewers.length > 0) { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + reviewers + }); + } diff --git a/.github/workflows/issue_or_pr.yml.disabled b/.github/workflows/issue_or_pr.yml.disabled new file mode 100644 index 0000000..e1d30db --- /dev/null +++ b/.github/workflows/issue_or_pr.yml.disabled @@ -0,0 +1,40 @@ +name: Issue or PR Label + +on: + pull_request: + types: [opened] + issues: + types: [opened] + +jobs: + add-label: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Check if issue or PR is opened + id: check + run: | + if [[ "${{ github.event_name }}" == "issues" ]]; then + echo "type=issue" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "type=pull_request" >> $GITHUB_OUTPUT + else + echo "type=unknown" >> $GITHUB_OUTPUT + fi + + - name: Add label to issue or PR + if: steps.check.outputs.type == 'issue' || steps.check.outputs.type == 'pull_request' + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const label = steps.check.outputs.type === 'issue' ? 'type: issue' : 'type: pull request'; + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [label] + }); \ No newline at end of file diff --git a/.github/workflows/pr-merge-status.yml b/.github/workflows/pr-merge-status.yml new file mode 100644 index 0000000..6b52705 --- /dev/null +++ b/.github/workflows/pr-merge-status.yml @@ -0,0 +1,55 @@ +name: PR Status - When Merged + +on: + pull_request: + types: [closed] + +jobs: + handle-merge: + # PR์ด closed ๋˜์—ˆ์„ ๋•Œ, merged ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ merged=true์ธ ๊ฒฝ์šฐ์—๋งŒ ์‹คํ–‰ + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Update status to done + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + const labelsToRemove = [ + "status: in progress", + "status: review required", + "status: change required", + "status: waiting merge" + ]; + + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + for (const labelName of labelsToRemove) { + if (currentLabels.some(l => l.name === labelName)) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: labelName + }).catch(err => { + if (err.status !== 404) throw err; + }); + } + } + + // ๋ณ‘ํ•ฉ์ด ์™„๋ฃŒ๋œ PR์— "status: done" ๋ผ๋ฒจ์„ ๋ถ€์ฐฉ + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ["status: done"] + }); \ No newline at end of file diff --git a/.github/workflows/pr-review-status.yml b/.github/workflows/pr-review-status.yml new file mode 100644 index 0000000..bfa0482 --- /dev/null +++ b/.github/workflows/pr-review-status.yml @@ -0,0 +1,66 @@ +name: PR Status - Review Outcome + +on: + pull_request_review: + types: [submitted] + +jobs: + handle-review-outcome: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Handle review outcomes + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + const labelInProgress = "status: in progress"; + const labelReviewReq = "status: review required"; + const labelChangeReq = "status: change required"; + const labelWaitingMerge = "status: waiting merge"; + + const labelsToRemove = [ + labelInProgress, + labelReviewReq, + labelChangeReq, + labelWaitingMerge + ]; + + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + for (const labelName of labelsToRemove) { + if (currentLabels.some(l => l.name === labelName)) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: labelName + }).catch(err => { + if (err.status !== 404) throw err; + }); + } + } + + if (context.payload.review.state === "approved") { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [labelWaitingMerge] + }); + } else if (context.payload.review.state === "changes_requested") { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [labelChangeReq] + }); + } \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e0a72e --- /dev/null +++ b/.gitignore @@ -0,0 +1,232 @@ +__pycache__# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +# End of https://www.toptal.com/developers/gitignore/api/macos diff --git a/Backend/__init__.py b/Backend/__init__.py new file mode 100644 index 0000000..5d3f802 --- /dev/null +++ b/Backend/__init__.py @@ -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) diff --git a/Backend/core/security.py b/Backend/core/security.py new file mode 100644 index 0000000..c5866cf --- /dev/null +++ b/Backend/core/security.py @@ -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 diff --git a/Backend/router/endpoints/avatar.py b/Backend/router/endpoints/avatar.py new file mode 100644 index 0000000..ee4b48e --- /dev/null +++ b/Backend/router/endpoints/avatar.py @@ -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() diff --git a/Backend/router/endpoints/diary.py b/Backend/router/endpoints/diary.py new file mode 100644 index 0000000..e01a508 --- /dev/null +++ b/Backend/router/endpoints/diary.py @@ -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)) diff --git a/Backend/router/endpoints/friendship.py b/Backend/router/endpoints/friendship.py new file mode 100644 index 0000000..a3b4beb --- /dev/null +++ b/Backend/router/endpoints/friendship.py @@ -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)) diff --git a/Backend/router/endpoints/guestbook.py b/Backend/router/endpoints/guestbook.py new file mode 100644 index 0000000..9246638 --- /dev/null +++ b/Backend/router/endpoints/guestbook.py @@ -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)) diff --git a/Backend/router/endpoints/letter.py b/Backend/router/endpoints/letter.py new file mode 100644 index 0000000..ee784e6 --- /dev/null +++ b/Backend/router/endpoints/letter.py @@ -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)) diff --git a/Backend/router/endpoints/photo.py b/Backend/router/endpoints/photo.py new file mode 100644 index 0000000..412ed79 --- /dev/null +++ b/Backend/router/endpoints/photo.py @@ -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)) diff --git a/Backend/router/endpoints/room.py b/Backend/router/endpoints/room.py new file mode 100644 index 0000000..d654ea3 --- /dev/null +++ b/Backend/router/endpoints/room.py @@ -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)) \ No newline at end of file diff --git a/Backend/router/endpoints/store.py b/Backend/router/endpoints/store.py new file mode 100644 index 0000000..b2f79fa --- /dev/null +++ b/Backend/router/endpoints/store.py @@ -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)) diff --git a/Backend/router/endpoints/user.py b/Backend/router/endpoints/user.py new file mode 100644 index 0000000..37078cd --- /dev/null +++ b/Backend/router/endpoints/user.py @@ -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 = # ์ž‘์„ฑ # + if existing_user: + raise HTTPException( + status_code=# ์ž‘์„ฑ # + detail="# ์ž‘์„ฑ #" + ) + + existing_email = # ์ž‘์„ฑ # + if existing_email: + raise HTTPException( + status_code=# ์ž‘์„ฑ # + detail="# ์ž‘์„ฑ #" + ) + + try: + user = # ์ž‘์„ฑ # + return user.to_response() + except Exception as e: + raise HTTPException( + status_code= + detail="# ์ž‘์„ฑ #" + ) + +@router.post("/login") +async def login_user(login_data: UserLogin) -> dict: + user = await user_service.authenticate_user( + + ) + + 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)}", + ) diff --git a/Backend/router/router.py b/Backend/router/router.py new file mode 100644 index 0000000..dadeafe --- /dev/null +++ b/Backend/router/router.py @@ -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()}") diff --git a/Backend/schemas/__init__.py b/Backend/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/schemas/avatar.py b/Backend/schemas/avatar.py new file mode 100644 index 0000000..5a4e3ad --- /dev/null +++ b/Backend/schemas/avatar.py @@ -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], + ), + ) diff --git a/Backend/schemas/diary.py b/Backend/schemas/diary.py new file mode 100644 index 0000000..cf2d9d1 --- /dev/null +++ b/Backend/schemas/diary.py @@ -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, + ) diff --git a/Backend/schemas/friendship.py b/Backend/schemas/friendship.py new file mode 100644 index 0000000..a747653 --- /dev/null +++ b/Backend/schemas/friendship.py @@ -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, + ) diff --git a/Backend/schemas/guestbook.py b/Backend/schemas/guestbook.py new file mode 100644 index 0000000..4472367 --- /dev/null +++ b/Backend/schemas/guestbook.py @@ -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 diff --git a/Backend/schemas/letter.py b/Backend/schemas/letter.py new file mode 100644 index 0000000..400366d --- /dev/null +++ b/Backend/schemas/letter.py @@ -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 diff --git a/Backend/schemas/photo.py b/Backend/schemas/photo.py new file mode 100644 index 0000000..234b1db --- /dev/null +++ b/Backend/schemas/photo.py @@ -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, +# ) diff --git a/Backend/schemas/room.py b/Backend/schemas/room.py new file mode 100644 index 0000000..c9381f6 --- /dev/null +++ b/Backend/schemas/room.py @@ -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], + ) diff --git a/Backend/schemas/user.py b/Backend/schemas/user.py new file mode 100644 index 0000000..1082521 --- /dev/null +++ b/Backend/schemas/user.py @@ -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, + ) diff --git a/Backend/services/__init__.py b/Backend/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/services/avatar_service.py b/Backend/services/avatar_service.py new file mode 100644 index 0000000..0869583 --- /dev/null +++ b/Backend/services/avatar_service.py @@ -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) \ No newline at end of file diff --git a/Backend/services/diary_service.py b/Backend/services/diary_service.py new file mode 100644 index 0000000..e8fd232 --- /dev/null +++ b/Backend/services/diary_service.py @@ -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 diff --git a/Backend/services/friendship_service.py b/Backend/services/friendship_service.py new file mode 100644 index 0000000..3ec4415 --- /dev/null +++ b/Backend/services/friendship_service.py @@ -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 diff --git a/Backend/services/guestbook_service.py b/Backend/services/guestbook_service.py new file mode 100644 index 0000000..d1648a2 --- /dev/null +++ b/Backend/services/guestbook_service.py @@ -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 diff --git a/Backend/services/letter_service.py b/Backend/services/letter_service.py new file mode 100644 index 0000000..fcc6422 --- /dev/null +++ b/Backend/services/letter_service.py @@ -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 +# ) diff --git a/Backend/services/photo_service.py b/Backend/services/photo_service.py new file mode 100644 index 0000000..43f11f8 --- /dev/null +++ b/Backend/services/photo_service.py @@ -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 diff --git a/Backend/services/room_service.py b/Backend/services/room_service.py new file mode 100644 index 0000000..122e4ba --- /dev/null +++ b/Backend/services/room_service.py @@ -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 \ No newline at end of file diff --git a/Backend/services/store_service.py b/Backend/services/store_service.py new file mode 100644 index 0000000..d393875 --- /dev/null +++ b/Backend/services/store_service.py @@ -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 \ No newline at end of file diff --git a/Backend/services/user_service.py b/Backend/services/user_service.py new file mode 100644 index 0000000..d85800e --- /dev/null +++ b/Backend/services/user_service.py @@ -0,0 +1,176 @@ +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 = + + if profile_file is not None: + await self.image_processor.validate_image_file( + + ) + image_path = + 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) diff --git a/Backend/tests/__init__.py b/Backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/tests/conftest.py b/Backend/tests/conftest.py new file mode 100644 index 0000000..312f139 --- /dev/null +++ b/Backend/tests/conftest.py @@ -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) diff --git a/Backend/tests/test_avatar.py b/Backend/tests/test_avatar.py new file mode 100644 index 0000000..13d41d8 --- /dev/null +++ b/Backend/tests/test_avatar.py @@ -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 \ No newline at end of file diff --git a/Backend/tests/test_diary.py b/Backend/tests/test_diary.py new file mode 100644 index 0000000..f5bd904 --- /dev/null +++ b/Backend/tests/test_diary.py @@ -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 diff --git a/Backend/tests/test_friendship.py b/Backend/tests/test_friendship.py new file mode 100644 index 0000000..781fc4a --- /dev/null +++ b/Backend/tests/test_friendship.py @@ -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()) diff --git a/Backend/tests/test_guestbook.py b/Backend/tests/test_guestbook.py new file mode 100644 index 0000000..c256f97 --- /dev/null +++ b/Backend/tests/test_guestbook.py @@ -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 diff --git a/Backend/tests/test_letter.py b/Backend/tests/test_letter.py new file mode 100644 index 0000000..014db38 --- /dev/null +++ b/Backend/tests/test_letter.py @@ -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 diff --git a/Backend/tests/test_photo.py b/Backend/tests/test_photo.py new file mode 100644 index 0000000..932b771 --- /dev/null +++ b/Backend/tests/test_photo.py @@ -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" diff --git a/Backend/tests/test_room.py b/Backend/tests/test_room.py new file mode 100644 index 0000000..509fafd --- /dev/null +++ b/Backend/tests/test_room.py @@ -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, + ) diff --git a/Backend/tests/test_user.py b/Backend/tests/test_user.py new file mode 100644 index 0000000..48394c9 --- /dev/null +++ b/Backend/tests/test_user.py @@ -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" diff --git a/Backend/utils/__init__.py b/Backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/utils/db.py b/Backend/utils/db.py new file mode 100644 index 0000000..863a9a7 --- /dev/null +++ b/Backend/utils/db.py @@ -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() diff --git a/Backend/utils/default_queries.py b/Backend/utils/default_queries.py new file mode 100644 index 0000000..e070ba7 --- /dev/null +++ b/Backend/utils/default_queries.py @@ -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)", + ] diff --git a/Backend/utils/email_processor.py b/Backend/utils/email_processor.py new file mode 100644 index 0000000..4d6c5c7 --- /dev/null +++ b/Backend/utils/email_processor.py @@ -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 diff --git a/Backend/utils/image_processor.py b/Backend/utils/image_processor.py new file mode 100644 index 0000000..e694725 --- /dev/null +++ b/Backend/utils/image_processor.py @@ -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 diff --git a/Backend/utils/queries/__init__.py b/Backend/utils/queries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/utils/queries/avatar.py b/Backend/utils/queries/avatar.py new file mode 100644 index 0000000..c6ebd95 --- /dev/null +++ b/Backend/utils/queries/avatar.py @@ -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 = ? + """ \ No newline at end of file diff --git a/Backend/utils/queries/diary.py b/Backend/utils/queries/diary.py new file mode 100644 index 0000000..04a38d1 --- /dev/null +++ b/Backend/utils/queries/diary.py @@ -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 = ? + """ diff --git a/Backend/utils/queries/friendship.py b/Backend/utils/queries/friendship.py new file mode 100644 index 0000000..f019f2a --- /dev/null +++ b/Backend/utils/queries/friendship.py @@ -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' + """ diff --git a/Backend/utils/queries/guestbook.py b/Backend/utils/queries/guestbook.py new file mode 100644 index 0000000..33e7d13 --- /dev/null +++ b/Backend/utils/queries/guestbook.py @@ -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 = ? + """ diff --git a/Backend/utils/queries/room.py b/Backend/utils/queries/room.py new file mode 100644 index 0000000..8c0f07b --- /dev/null +++ b/Backend/utils/queries/room.py @@ -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 = ? + """ diff --git a/Backend/utils/queries/user.py b/Backend/utils/queries/user.py new file mode 100644 index 0000000..96dbe04 --- /dev/null +++ b/Backend/utils/queries/user.py @@ -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 != ? + """ diff --git a/Backend/utils/run_server.py b/Backend/utils/run_server.py new file mode 100644 index 0000000..a42b550 --- /dev/null +++ b/Backend/utils/run_server.py @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbe88ba --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +## 2025 AnA SSF - ๋‚˜๋งŒ์˜ ์‹ธ์ด์›”๋“œ ๋งŒ๋“ค๊ธฐ + +### ์†Œ๊ฐœ +2000๋…„๋Œ€ ๊ฐ์„ฑ์„ ๋‹ด์€ ์†Œ์…œ ๋„คํŠธ์›Œํฌ ์„œ๋น„์Šค '์‹ธ์ด์›”๋“œ'๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๋Š” ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. +์‚ฌ์ง„ ์—…๋กœ๋“œ, ๋ฏธ๋‹ˆํ™ˆํ”ผ, ๋„ํ† ๋ฆฌ, ๋ฐฉ๋ช…๋ก ๋“ฑ ์ฃผ์š” ๊ธฐ๋Šฅ๋“ค์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + +### ๊ธฐ์ˆ  ์Šคํƒ + - ํ”„๋ก ํŠธ์—”๋“œ : Qwik-city, Tailwind CSS + - ๋ฐฑ์—”๋“œ : FastAPI + - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค : SQLite + - ๋ฐฐํฌ : git + +### ์ฃผ์š” ๊ธฐ๋Šฅ + - ํšŒ์›๊ฐ€์ž… / ๋กœ๊ทธ์ธ + - ํ”„๋กœํ•„ ์‚ฌ์ง„ ์—…๋กœ๋“œ ๋ฐ ๋ฐฉ ๊พธ๋ฏธ๊ธฐ + - ๋ฐฉ๋ช…๋ก ์ž‘์„ฑ + - ๋„ํ† ๋ฆฌ ์ƒ์  + +### ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ +2025-SSF-Internal\ +โ”œโ”€โ”€ Backend\ +โ”‚ โ”œโ”€โ”€ __init__.py\ +โ”‚ โ”œโ”€โ”€ models\ +โ”‚ โ”œโ”€โ”€ routes\ +โ”‚ โ””โ”€โ”€ ...\ +โ””โ”€โ”€ README.md + +### ์‹คํ–‰ ๋ฐฉ๋ฒ• +1. ํ”„๋ก ํŠธ์—”๋“œ\ +git clone https://github.com/sunrin-ana/2025-SSF-Frontend.git \ +npm install\ +npm run + +2. ๋ฐฑ์—”๋“œ\ +git clone https://github.com/sunrin-ana/2025-SSF.git \ +pip install -r requirements.txt\ +uvicorn Backed:app --reload + +### ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„ +2025-05-30 ~ 2025-09-12 + +### ํŒ€์› +์žฅํ•œ์šธ - diary, ~~photo~~, ~~letter~~, store\ +๊ณ ์œค - avatar, room\ +๊น€๊ฑด์šฐ - user, friendship\ +๊น€์ฃผ์˜ - dotory-manage server, frontend + +### ์ฐธ๊ณ ์ž๋ฃŒ +์‹ธ์ด์›”๋“œ ๋ฏธ๋‹ˆํ™ˆํ”ผ ์†Œ๊ฐœ diff --git a/api.json b/api.json new file mode 100644 index 0000000..9cda915 --- /dev/null +++ b/api.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/register":{"post":{"tags":["user"],"summary":"Register User","operationId":"register_user_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login":{"post":{"tags":["user"],"summary":"Login User","operationId":"login_user_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLogin"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/profile/{username}":{"get":{"tags":["user"],"summary":"Get User Profile","operationId":"get_user_profile_profile__username__get","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","title":"Username"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/friendship/request":{"post":{"tags":["friendship","friendship"],"summary":"Send Friendship Request","operationId":"send_friendship_request_friendship_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendshipRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendshipResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/friendship/{friendship_id}/accept":{"put":{"tags":["friendship","friendship"],"summary":"Accept Friendship Request","operationId":"accept_friendship_request_friendship__friendship_id__accept_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"friendship_id","in":"path","required":true,"schema":{"type":"integer","title":"Friendship Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendshipResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/friendship":{"get":{"tags":["friendship","friendship"],"summary":"Get Friendships","operationId":"get_friendships_friendship_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FriendshipResponse"},"title":"Response Get Friendships Friendship Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/friendship/{friendship_id}":{"delete":{"tags":["friendship","friendship"],"summary":"Delete Friendship","operationId":"delete_friendship_friendship__friendship_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"friendship_id","in":"path","required":true,"schema":{"type":"integer","title":"Friendship Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Friendship Friendship Friendship Id Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/friendship/pending":{"get":{"tags":["friendship","friendship"],"summary":"Get Pending Requests","operationId":"get_pending_requests_friendship_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/FriendshipResponse"},"type":"array","title":"Response Get Pending Requests Friendship Pending Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/diary":{"post":{"tags":["diary","diary"],"summary":"Create Diary","operationId":"create_diary_diary_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiaryCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiaryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["diary","diary"],"summary":"Get User Diaries","operationId":"get_user_diaries_diary_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skip","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DiaryResponse"},"title":"Response Get User Diaries Diary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/diary/{diary_id}":{"get":{"tags":["diary","diary"],"summary":"Get Diary","operationId":"get_diary_diary__diary_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"diary_id","in":"path","required":true,"schema":{"type":"integer","title":"Diary Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiaryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["diary","diary"],"summary":"Update Diary","operationId":"update_diary_diary__diary_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"diary_id","in":"path","required":true,"schema":{"type":"integer","title":"Diary Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiaryUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiaryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["diary","diary"],"summary":"Delete Diary","operationId":"delete_diary_diary__diary_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"diary_id","in":"path","required":true,"schema":{"type":"integer","title":"Diary Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Diary Diary Diary Id Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/diary/upload-image":{"post":{"tags":["diary","diary"],"summary":"Upload Diary Image","operationId":"upload_diary_image_diary_upload_image_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_diary_image_diary_upload_image_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Upload Diary Image Diary Upload Image Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/diary/{diary_id}/submit-for-event":{"post":{"tags":["diary","diary"],"summary":"Submit Diary For Event","operationId":"submit_diary_for_event_diary__diary_id__submit_for_event_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"diary_id","in":"path","required":true,"schema":{"type":"integer","title":"Diary Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Submit Diary For Event Diary Diary Id Submit For Event Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/avatar":{"get":{"tags":["avatar","avatar"],"summary":"Get Current Avatar","operationId":"get_current_avatar_avatar_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["avatar","avatar"],"summary":"Update Avatar","operationId":"update_avatar_avatar_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/avatar/options":{"get":{"tags":["avatar","avatar"],"summary":"Get Avatar Options","operationId":"get_avatar_options_avatar_options_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarOptions"}}}}}}},"/letter":{"post":{"tags":["letter","letter"],"summary":"Create Letter","operationId":"create_letter_letter_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LetterCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LetterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/letter/my":{"get":{"tags":["letter","letter"],"summary":"Get My Letters","operationId":"get_my_letters_letter_my_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skip","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LetterResponse"},"title":"Response Get My Letters Letter My Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/letter/{letter_id}":{"get":{"tags":["letter","letter"],"summary":"Get Letter","operationId":"get_letter_letter__letter_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"letter_id","in":"path","required":true,"schema":{"type":"integer","title":"Letter Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LetterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/photo/upload":{"post":{"tags":["photo","photo"],"summary":"Upload Photo","operationId":"upload_photo_photo_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_photo_photo_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/photo":{"get":{"tags":["photo","photo"],"summary":"Get User Photos","operationId":"get_user_photos_photo_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skip","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}},{"name":"album_name","in":"query","required":false,"schema":{"type":"string","title":"Album Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PhotoResponse"},"title":"Response Get User Photos Photo Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/photo/{photo_id}/comment":{"post":{"tags":["photo","photo"],"summary":"Add Photo Comment","operationId":"add_photo_comment_photo__photo_id__comment_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"photo_id","in":"path","required":true,"schema":{"type":"integer","title":"Photo Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/photo/{photo_id}/comments":{"get":{"tags":["photo","photo"],"summary":"Get Photo Comments","operationId":"get_photo_comments_photo__photo_id__comments_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"photo_id","in":"path","required":true,"schema":{"type":"integer","title":"Photo Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CommentResponse"},"title":"Response Get Photo Comments Photo Photo Id Comments Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/photo/edit-filter":{"post":{"tags":["photo","photo"],"summary":"Apply Photo Filter","operationId":"apply_photo_filter_photo_edit_filter_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FilterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Apply Photo Filter Photo Edit Filter Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/photo/{photo_id}":{"delete":{"tags":["photo","photo"],"summary":"Delete Photo","operationId":"delete_photo_photo__photo_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"photo_id","in":"path","required":true,"schema":{"type":"integer","title":"Photo Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Photo Photo Photo Id Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test":{"get":{"tags":["guestBook"],"summary":"Get Guest Book","operationId":"get_guest_book_test_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AvatarOptions":{"properties":{"face_types":{"items":{"type":"string"},"type":"array","title":"Face Types"},"hair_types":{"items":{"type":"string"},"type":"array","title":"Hair Types"},"clothes_types":{"items":{"type":"string"},"type":"array","title":"Clothes Types"},"background_types":{"items":{"type":"string"},"type":"array","title":"Background Types"}},"type":"object","required":["face_types","hair_types","clothes_types","background_types"],"title":"AvatarOptions"},"AvatarResponse":{"properties":{"id":{"type":"integer","title":"Id"},"user_id":{"type":"integer","title":"User Id"},"face_type":{"$ref":"#/components/schemas/FaceType"},"hair_type":{"$ref":"#/components/schemas/HairType"},"clothes_type":{"$ref":"#/components/schemas/ClothesType"},"background_type":{"$ref":"#/components/schemas/BackgroundType"},"position_x":{"type":"integer","title":"Position X"},"position_y":{"type":"integer","title":"Position Y"}},"type":"object","required":["id","user_id","face_type","hair_type","clothes_type","background_type","position_x","position_y"],"title":"AvatarResponse"},"AvatarUpdate":{"properties":{"face_type":{"anyOf":[{"$ref":"#/components/schemas/FaceType"},{"type":"null"}]},"hair_type":{"anyOf":[{"$ref":"#/components/schemas/HairType"},{"type":"null"}]},"clothes_type":{"anyOf":[{"$ref":"#/components/schemas/ClothesType"},{"type":"null"}]},"background_type":{"anyOf":[{"$ref":"#/components/schemas/BackgroundType"},{"type":"null"}]},"position_x":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position X"},"position_y":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position Y"}},"type":"object","title":"AvatarUpdate"},"BackgroundType":{"type":"string","enum":["room","garden","beach","city","space"],"title":"BackgroundType"},"Body_upload_diary_image_diary_upload_image_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_diary_image_diary_upload_image_post"},"Body_upload_photo_photo_upload_post":{"properties":{"photo_data":{"$ref":"#/components/schemas/PhotoUpload"},"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["photo_data","file"],"title":"Body_upload_photo_photo_upload_post"},"ClothesType":{"type":"string","enum":["casual","formal","sporty","vintage","cute"],"title":"ClothesType"},"CommentCreate":{"properties":{"content":{"type":"string","title":"Content"}},"type":"object","required":["content"],"title":"CommentCreate"},"CommentResponse":{"properties":{"id":{"type":"integer","title":"Id"},"photo_id":{"type":"integer","title":"Photo Id"},"user_id":{"type":"integer","title":"User Id"},"username":{"type":"string","title":"Username"},"content":{"type":"string","title":"Content"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","photo_id","user_id","username","content","created_at"],"title":"CommentResponse"},"DiaryCategory":{"type":"string","enum":["daily","travel","food","love","work","hobby"],"title":"DiaryCategory"},"DiaryCreate":{"properties":{"title":{"type":"string","title":"Title"},"content":{"type":"string","title":"Content"},"category":{"$ref":"#/components/schemas/DiaryCategory"},"images":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Images","default":[]}},"type":"object","required":["title","content","category"],"title":"DiaryCreate"},"DiaryResponse":{"properties":{"id":{"type":"integer","title":"Id"},"user_id":{"type":"integer","title":"User Id"},"title":{"type":"string","title":"Title"},"content":{"type":"string","title":"Content"},"images":{"items":{"type":"string"},"type":"array","title":"Images"},"category":{"$ref":"#/components/schemas/DiaryCategory"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_submitted":{"type":"boolean","title":"Is Submitted"},"email_sent":{"type":"boolean","title":"Email Sent"}},"type":"object","required":["id","user_id","title","content","images","category","created_at","is_submitted","email_sent"],"title":"DiaryResponse"},"DiaryUpdate":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content"},"category":{"anyOf":[{"$ref":"#/components/schemas/DiaryCategory"},{"type":"null"}]},"images":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Images"}},"type":"object","title":"DiaryUpdate"},"FaceType":{"type":"string","enum":["round","oval","square","heart"],"title":"FaceType"},"FilterRequest":{"properties":{"photo_id":{"type":"integer","title":"Photo Id"},"filter_type":{"type":"string","title":"Filter Type"}},"type":"object","required":["photo_id","filter_type"],"title":"FilterRequest"},"FriendshipRequest":{"properties":{"friend_username":{"type":"string","title":"Friend Username"}},"type":"object","required":["friend_username"],"title":"FriendshipRequest"},"FriendshipResponse":{"properties":{"id":{"type":"integer","title":"Id"},"user_id":{"type":"integer","title":"User Id"},"friend_id":{"type":"integer","title":"Friend Id"},"friend_username":{"type":"string","title":"Friend Username"},"status":{"$ref":"#/components/schemas/FriendshipStatus"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","user_id","friend_id","friend_username","status","created_at"],"title":"FriendshipResponse"},"FriendshipStatus":{"type":"string","enum":["pending","accepted","rejected"],"title":"FriendshipStatus"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HairType":{"type":"string","enum":["short","long","curly","ponytail","bob"],"title":"HairType"},"LetterCreate":{"properties":{"recipient_email":{"type":"string","title":"Recipient Email"},"content":{"type":"string","title":"Content"}},"type":"object","required":["recipient_email","content"],"title":"LetterCreate"},"LetterResponse":{"properties":{"id":{"type":"integer","title":"Id"},"sender_id":{"type":"integer","title":"Sender Id"},"recipient_email":{"type":"string","title":"Recipient Email"},"content":{"type":"string","title":"Content"},"sent_date":{"type":"string","format":"date-time","title":"Sent Date"},"is_sent":{"type":"boolean","title":"Is Sent"}},"type":"object","required":["id","sender_id","recipient_email","content","sent_date","is_sent"],"title":"LetterResponse"},"PhotoResponse":{"properties":{"id":{"type":"integer","title":"Id"},"user_id":{"type":"integer","title":"User Id"},"album_name":{"type":"string","title":"Album Name"},"image_path":{"type":"string","title":"Image Path"},"title":{"type":"string","title":"Title"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","user_id","album_name","image_path","title","created_at"],"title":"PhotoResponse"},"PhotoUpload":{"properties":{"album_name":{"type":"string","title":"Album Name"},"title":{"type":"string","title":"Title"}},"type":"object","required":["album_name","title"],"title":"PhotoUpload"},"UserCreate":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["username","email","password"],"title":"UserCreate"},"UserLogin":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"}},"type":"object","required":["username","password"],"title":"UserLogin"},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_active":{"type":"boolean","title":"Is Active"}},"type":"object","required":["id","username","email","created_at","is_active"],"title":"UserResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}} \ No newline at end of file diff --git a/database.sqlite3 b/database.sqlite3 new file mode 100644 index 0000000..9f8da7f Binary files /dev/null and b/database.sqlite3 differ diff --git a/public/avatar/๊ต๋ณต์ƒ์˜.png b/public/avatar/๊ต๋ณต์ƒ์˜.png new file mode 100644 index 0000000..62af167 Binary files /dev/null and b/public/avatar/๊ต๋ณต์ƒ์˜.png differ diff --git a/public/avatar/๊ต๋ณต์กฐ๋ผ์ƒ์˜.png b/public/avatar/๊ต๋ณต์กฐ๋ผ์ƒ์˜.png new file mode 100644 index 0000000..7e001cf Binary files /dev/null and b/public/avatar/๊ต๋ณต์กฐ๋ผ์ƒ์˜.png differ diff --git a/public/avatar/๊ต๋ณต์กฐ๋ผํ•˜์˜๋‚จ.png b/public/avatar/๊ต๋ณต์กฐ๋ผํ•˜์˜๋‚จ.png new file mode 100644 index 0000000..ac02326 Binary files /dev/null and b/public/avatar/๊ต๋ณต์กฐ๋ผํ•˜์˜๋‚จ.png differ diff --git a/public/avatar/๊ต๋ณต์กฐ๋ผํ•˜์˜์—ฌ.png b/public/avatar/๊ต๋ณต์กฐ๋ผํ•˜์˜์—ฌ.png new file mode 100644 index 0000000..1a581d2 Binary files /dev/null and b/public/avatar/๊ต๋ณต์กฐ๋ผํ•˜์˜์—ฌ.png differ diff --git a/public/avatar/๊ต๋ณตํ•˜์˜๋‚จ.png b/public/avatar/๊ต๋ณตํ•˜์˜๋‚จ.png new file mode 100644 index 0000000..ebb7ac2 Binary files /dev/null and b/public/avatar/๊ต๋ณตํ•˜์˜๋‚จ.png differ diff --git a/public/avatar/๊ต๋ณตํ•˜์˜์—ฌ.png b/public/avatar/๊ต๋ณตํ•˜์˜์—ฌ.png new file mode 100644 index 0000000..1a581d2 Binary files /dev/null and b/public/avatar/๊ต๋ณตํ•˜์˜์—ฌ.png differ diff --git a/public/avatar/๋‚จ์ž.png b/public/avatar/๋‚จ์ž.png new file mode 100644 index 0000000..b09caaa Binary files /dev/null and b/public/avatar/๋‚จ์ž.png differ diff --git a/public/avatar/๋™์ž ์ƒ์˜.png b/public/avatar/๋™์ž ์ƒ์˜.png new file mode 100644 index 0000000..61b4d6d Binary files /dev/null and b/public/avatar/๋™์ž ์ƒ์˜.png differ diff --git a/public/avatar/๋ฉœ๋นต๋ฐ”์ง€์ƒ์˜.png b/public/avatar/๋ฉœ๋นต๋ฐ”์ง€์ƒ์˜.png new file mode 100644 index 0000000..6266856 Binary files /dev/null and b/public/avatar/๋ฉœ๋นต๋ฐ”์ง€์ƒ์˜.png differ diff --git a/public/avatar/๋ฉœ๋นต์น˜๋งˆ์ƒ์˜.png b/public/avatar/๋ฉœ๋นต์น˜๋งˆ์ƒ์˜.png new file mode 100644 index 0000000..d12ba99 Binary files /dev/null and b/public/avatar/๋ฉœ๋นต์น˜๋งˆ์ƒ์˜.png differ diff --git a/public/avatar/๋ฌด์ง€๊ฐœ๋งจํˆฌ๋งจ์ƒ์˜.png b/public/avatar/๋ฌด์ง€๊ฐœ๋งจํˆฌ๋งจ์ƒ์˜.png new file mode 100644 index 0000000..ccd08e1 Binary files /dev/null and b/public/avatar/๋ฌด์ง€๊ฐœ๋งจํˆฌ๋งจ์ƒ์˜.png differ diff --git a/public/avatar/์‚ฐํƒ€์ƒ์˜.png b/public/avatar/์‚ฐํƒ€์ƒ์˜.png new file mode 100644 index 0000000..037abdd Binary files /dev/null and b/public/avatar/์‚ฐํƒ€์ƒ์˜.png differ diff --git a/public/avatar/์‚ฐํƒ€ํ•˜์˜.png b/public/avatar/์‚ฐํƒ€ํ•˜์˜.png new file mode 100644 index 0000000..99c1434 Binary files /dev/null and b/public/avatar/์‚ฐํƒ€ํ•˜์˜.png differ diff --git a/public/avatar/์—ฌ์ž.png b/public/avatar/์—ฌ์ž.png new file mode 100644 index 0000000..a916e81 Binary files /dev/null and b/public/avatar/์—ฌ์ž.png differ diff --git a/public/avatar/์ฒญ๋ฐ”์ง€ํ•˜์˜.png b/public/avatar/์ฒญ๋ฐ”์ง€ํ•˜์˜.png new file mode 100644 index 0000000..66473e8 Binary files /dev/null and b/public/avatar/์ฒญ๋ฐ”์ง€ํ•˜์˜.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-0.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-0.png new file mode 100644 index 0000000..da7660a Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-0.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-180.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-180.png new file mode 100644 index 0000000..7d30b16 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-180.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-270.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-270.png new file mode 100644 index 0000000..23e8994 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-270.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-90.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-90.png new file mode 100644 index 0000000..bf91e08 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ1-90.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-0.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-0.png new file mode 100644 index 0000000..da7660a Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-0.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-180.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-180.png new file mode 100644 index 0000000..7ea3580 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-180.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-270.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-270.png new file mode 100644 index 0000000..98db1d1 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-270.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-90.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-90.png new file mode 100644 index 0000000..e4378c8 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ2-90.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-0.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-0.png new file mode 100644 index 0000000..da7660a Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-0.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-180.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-180.png new file mode 100644 index 0000000..e4378c8 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-180.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-270.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-270.png new file mode 100644 index 0000000..d1f6253 Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-270.png differ diff --git a/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-90.png b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-90.png new file mode 100644 index 0000000..84d359a Binary files /dev/null and b/public/funiture/๊ฒ€์ • ๋…ธํŠธ๋ถ3-90.png differ diff --git a/public/funiture/๋‚˜๋ฌด ํƒ์ž-90.png b/public/funiture/๋‚˜๋ฌด ํƒ์ž-90.png new file mode 100644 index 0000000..6b419e1 Binary files /dev/null and b/public/funiture/๋‚˜๋ฌด ํƒ์ž-90.png differ diff --git a/public/funiture/๋‚˜๋ฌดํƒ์ž-0.png b/public/funiture/๋‚˜๋ฌดํƒ์ž-0.png new file mode 100644 index 0000000..9f55c5f Binary files /dev/null and b/public/funiture/๋‚˜๋ฌดํƒ์ž-0.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ1-0.png b/public/funiture/๋…ธํŠธ๋ถ1-0.png new file mode 100644 index 0000000..843e71d Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ1-0.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ1-180.png b/public/funiture/๋…ธํŠธ๋ถ1-180.png new file mode 100644 index 0000000..a137f10 Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ1-180.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ1-270.png b/public/funiture/๋…ธํŠธ๋ถ1-270.png new file mode 100644 index 0000000..fda776c Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ1-270.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ1-90.png b/public/funiture/๋…ธํŠธ๋ถ1-90.png new file mode 100644 index 0000000..891e8af Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ1-90.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ2-0.png b/public/funiture/๋…ธํŠธ๋ถ2-0.png new file mode 100644 index 0000000..843e71d Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ2-0.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ2-180.png b/public/funiture/๋…ธํŠธ๋ถ2-180.png new file mode 100644 index 0000000..48cb9b3 Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ2-180.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ2-270.png b/public/funiture/๋…ธํŠธ๋ถ2-270.png new file mode 100644 index 0000000..53d7068 Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ2-270.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ2-90.png b/public/funiture/๋…ธํŠธ๋ถ2-90.png new file mode 100644 index 0000000..891e8af Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ2-90.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ3-0.png b/public/funiture/๋…ธํŠธ๋ถ3-0.png new file mode 100644 index 0000000..843e71d Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ3-0.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ3-180.png b/public/funiture/๋…ธํŠธ๋ถ3-180.png new file mode 100644 index 0000000..fb6f6f9 Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ3-180.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ3-270.png b/public/funiture/๋…ธํŠธ๋ถ3-270.png new file mode 100644 index 0000000..c06c404 Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ3-270.png differ diff --git a/public/funiture/๋…ธํŠธ๋ถ3-90.png b/public/funiture/๋…ธํŠธ๋ถ3-90.png new file mode 100644 index 0000000..891e8af Binary files /dev/null and b/public/funiture/๋…ธํŠธ๋ถ3-90.png differ diff --git a/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-0.png b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-0.png new file mode 100644 index 0000000..ded87ee Binary files /dev/null and b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-0.png differ diff --git a/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-180.png b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-180.png new file mode 100644 index 0000000..cd5367b Binary files /dev/null and b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-180.png differ diff --git a/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-270.png b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-270.png new file mode 100644 index 0000000..4833be9 Binary files /dev/null and b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-270.png differ diff --git a/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-90.png b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-90.png new file mode 100644 index 0000000..a09a8e5 Binary files /dev/null and b/public/funiture/๋…น์ƒ‰ ์นจ๋Œ€-90.png differ diff --git a/public/funiture/๋…น์ƒ‰ ํƒ์ž.png b/public/funiture/๋…น์ƒ‰ ํƒ์ž.png new file mode 100644 index 0000000..ff88989 Binary files /dev/null and b/public/funiture/๋…น์ƒ‰ ํƒ์ž.png differ diff --git a/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -0.png b/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -0.png new file mode 100644 index 0000000..17dfebe Binary files /dev/null and b/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -0.png differ diff --git a/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -180.png b/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -180.png new file mode 100644 index 0000000..9537ba0 Binary files /dev/null and b/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -180.png differ diff --git a/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -90.png b/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -90.png new file mode 100644 index 0000000..e0a1940 Binary files /dev/null and b/public/funiture/๋ฏธ๋‹ˆ ๋ƒ‰์žฅ๊ณ -90.png differ diff --git a/public/funiture/๋ฐ•์Šค-0.png b/public/funiture/๋ฐ•์Šค-0.png new file mode 100644 index 0000000..244dccd Binary files /dev/null and b/public/funiture/๋ฐ•์Šค-0.png differ diff --git a/public/funiture/๋ฐ•์Šค-90.png b/public/funiture/๋ฐ•์Šค-90.png new file mode 100644 index 0000000..3770986 Binary files /dev/null and b/public/funiture/๋ฐ•์Šค-90.png differ diff --git a/public/funiture/๋ถ„ํ™์ƒ‰ ํƒ์ž.png b/public/funiture/๋ถ„ํ™์ƒ‰ ํƒ์ž.png new file mode 100644 index 0000000..8d530bb Binary files /dev/null and b/public/funiture/๋ถ„ํ™์ƒ‰ ํƒ์ž.png differ diff --git a/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-0.png b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-0.png new file mode 100644 index 0000000..5c6314b Binary files /dev/null and b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-0.png differ diff --git a/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-180.png b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-180.png new file mode 100644 index 0000000..c591e0c Binary files /dev/null and b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-180.png differ diff --git a/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-270.png b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-270.png new file mode 100644 index 0000000..efcd92c Binary files /dev/null and b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-270.png differ diff --git a/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-90.png b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-90.png new file mode 100644 index 0000000..b9dfa4b Binary files /dev/null and b/public/funiture/๋นจ๊ฐ„ ์นจ๋Œ€-90.png differ diff --git a/public/funiture/์„ ๋ฐ˜-0.png b/public/funiture/์„ ๋ฐ˜-0.png new file mode 100644 index 0000000..795d5fa Binary files /dev/null and b/public/funiture/์„ ๋ฐ˜-0.png differ diff --git a/public/funiture/์„ ๋ฐ˜-180.png b/public/funiture/์„ ๋ฐ˜-180.png new file mode 100644 index 0000000..0b1b7ae Binary files /dev/null and b/public/funiture/์„ ๋ฐ˜-180.png differ diff --git a/public/funiture/์„ ๋ฐ˜-270.png b/public/funiture/์„ ๋ฐ˜-270.png new file mode 100644 index 0000000..2f5b746 Binary files /dev/null and b/public/funiture/์„ ๋ฐ˜-270.png differ diff --git a/public/funiture/์„ ๋ฐ˜-90.png b/public/funiture/์„ ๋ฐ˜-90.png new file mode 100644 index 0000000..f826741 Binary files /dev/null and b/public/funiture/์„ ๋ฐ˜-90.png differ diff --git a/public/funiture/์†ŒํŒŒ-0.png b/public/funiture/์†ŒํŒŒ-0.png new file mode 100644 index 0000000..1d84855 Binary files /dev/null and b/public/funiture/์†ŒํŒŒ-0.png differ diff --git a/public/funiture/์†ŒํŒŒ-180.png b/public/funiture/์†ŒํŒŒ-180.png new file mode 100644 index 0000000..962f4a5 Binary files /dev/null and b/public/funiture/์†ŒํŒŒ-180.png differ diff --git a/public/funiture/์†ŒํŒŒ-270.png b/public/funiture/์†ŒํŒŒ-270.png new file mode 100644 index 0000000..194e15d Binary files /dev/null and b/public/funiture/์†ŒํŒŒ-270.png differ diff --git a/public/funiture/์†ŒํŒŒ-90.png b/public/funiture/์†ŒํŒŒ-90.png new file mode 100644 index 0000000..69eeee2 Binary files /dev/null and b/public/funiture/์†ŒํŒŒ-90.png differ diff --git a/public/funiture/์“ฐ๋ ˆ๊ธฐํ†ต๋‹ซํž˜.png b/public/funiture/์“ฐ๋ ˆ๊ธฐํ†ต๋‹ซํž˜.png new file mode 100644 index 0000000..54543ea Binary files /dev/null and b/public/funiture/์“ฐ๋ ˆ๊ธฐํ†ต๋‹ซํž˜.png differ diff --git a/public/funiture/์“ฐ๋ ˆ๊ธฐํ†ต์—ด๋ฆผ.png b/public/funiture/์“ฐ๋ ˆ๊ธฐํ†ต์—ด๋ฆผ.png new file mode 100644 index 0000000..a9613b0 Binary files /dev/null and b/public/funiture/์“ฐ๋ ˆ๊ธฐํ†ต์—ด๋ฆผ.png differ diff --git a/public/funiture/์–ดํ•ญ-0.png b/public/funiture/์–ดํ•ญ-0.png new file mode 100644 index 0000000..13eae39 Binary files /dev/null and b/public/funiture/์–ดํ•ญ-0.png differ diff --git a/public/funiture/์–ดํ•ญ-180.png b/public/funiture/์–ดํ•ญ-180.png new file mode 100644 index 0000000..a441953 Binary files /dev/null and b/public/funiture/์–ดํ•ญ-180.png differ diff --git a/public/funiture/์–ดํ•ญ-270.png b/public/funiture/์–ดํ•ญ-270.png new file mode 100644 index 0000000..2985ca2 Binary files /dev/null and b/public/funiture/์–ดํ•ญ-270.png differ diff --git a/public/funiture/์–ดํ•ญ-90.png b/public/funiture/์–ดํ•ญ-90.png new file mode 100644 index 0000000..0352670 Binary files /dev/null and b/public/funiture/์–ดํ•ญ-90.png differ diff --git a/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -0.png b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -0.png new file mode 100644 index 0000000..1e338da Binary files /dev/null and b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -0.png differ diff --git a/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -180.png b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -180.png new file mode 100644 index 0000000..19870d5 Binary files /dev/null and b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -180.png differ diff --git a/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -270.png b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -270.png new file mode 100644 index 0000000..19870d5 Binary files /dev/null and b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -270.png differ diff --git a/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -90.png b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -90.png new file mode 100644 index 0000000..77cdf13 Binary files /dev/null and b/public/funiture/์Œ๋ฃŒ ๋ƒ‰์žฅ๊ณ -90.png differ diff --git a/public/funiture/์˜์ž-0.png b/public/funiture/์˜์ž-0.png new file mode 100644 index 0000000..6072ef9 Binary files /dev/null and b/public/funiture/์˜์ž-0.png differ diff --git a/public/funiture/์˜์ž-180.png b/public/funiture/์˜์ž-180.png new file mode 100644 index 0000000..ab21c1d Binary files /dev/null and b/public/funiture/์˜์ž-180.png differ diff --git a/public/funiture/์˜์ž-270.png b/public/funiture/์˜์ž-270.png new file mode 100644 index 0000000..96ca92e Binary files /dev/null and b/public/funiture/์˜์ž-270.png differ diff --git a/public/funiture/์˜์ž-90.png b/public/funiture/์˜์ž-90.png new file mode 100644 index 0000000..41b8d57 Binary files /dev/null and b/public/funiture/์˜์ž-90.png differ diff --git a/public/funiture/์ž‘์€ ์„ ๋ฐ˜-0.png b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-0.png new file mode 100644 index 0000000..f377ed7 Binary files /dev/null and b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-0.png differ diff --git a/public/funiture/์ž‘์€ ์„ ๋ฐ˜-180.png b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-180.png new file mode 100644 index 0000000..b63b3ae Binary files /dev/null and b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-180.png differ diff --git a/public/funiture/์ž‘์€ ์„ ๋ฐ˜-270.png b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-270.png new file mode 100644 index 0000000..8537143 Binary files /dev/null and b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-270.png differ diff --git a/public/funiture/์ž‘์€ ์„ ๋ฐ˜-90.png b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-90.png new file mode 100644 index 0000000..5ecdfb2 Binary files /dev/null and b/public/funiture/์ž‘์€ ์„ ๋ฐ˜-90.png differ diff --git a/public/funiture/์ž‘์€ ์‹๋ฌผ.png b/public/funiture/์ž‘์€ ์‹๋ฌผ.png new file mode 100644 index 0000000..e509319 Binary files /dev/null and b/public/funiture/์ž‘์€ ์‹๋ฌผ.png differ diff --git a/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-0.png b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-0.png new file mode 100644 index 0000000..94dd63a Binary files /dev/null and b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-0.png differ diff --git a/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-180.png b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-180.png new file mode 100644 index 0000000..90c37a3 Binary files /dev/null and b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-180.png differ diff --git a/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-270.png b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-270.png new file mode 100644 index 0000000..af18d09 Binary files /dev/null and b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-270.png differ diff --git a/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-90.png b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-90.png new file mode 100644 index 0000000..026dff0 Binary files /dev/null and b/public/funiture/์ฃผํ™ฉ ์นจ๋Œ€-90.png differ diff --git a/public/funiture/์ฑ…์žฅ-0.png b/public/funiture/์ฑ…์žฅ-0.png new file mode 100644 index 0000000..6f318e4 Binary files /dev/null and b/public/funiture/์ฑ…์žฅ-0.png differ diff --git a/public/funiture/์ฑ…์žฅ-180.png b/public/funiture/์ฑ…์žฅ-180.png new file mode 100644 index 0000000..b9345df Binary files /dev/null and b/public/funiture/์ฑ…์žฅ-180.png differ diff --git a/public/funiture/์ฑ…์žฅ-270.png b/public/funiture/์ฑ…์žฅ-270.png new file mode 100644 index 0000000..f63d93e Binary files /dev/null and b/public/funiture/์ฑ…์žฅ-270.png differ diff --git a/public/funiture/์ฑ…์žฅ-90.png b/public/funiture/์ฑ…์žฅ-90.png new file mode 100644 index 0000000..6761598 Binary files /dev/null and b/public/funiture/์ฑ…์žฅ-90.png differ diff --git a/public/funiture/ํฐ ์‹๋ฌผ.png b/public/funiture/ํฐ ์‹๋ฌผ.png new file mode 100644 index 0000000..1c44583 Binary files /dev/null and b/public/funiture/ํฐ ์‹๋ฌผ.png differ diff --git a/public/funiture/ํ‹ฐ๋น„-0.png b/public/funiture/ํ‹ฐ๋น„-0.png new file mode 100644 index 0000000..797b7d0 Binary files /dev/null and b/public/funiture/ํ‹ฐ๋น„-0.png differ diff --git a/public/funiture/ํ‹ฐ๋น„-180.png b/public/funiture/ํ‹ฐ๋น„-180.png new file mode 100644 index 0000000..f8844bb Binary files /dev/null and b/public/funiture/ํ‹ฐ๋น„-180.png differ diff --git a/public/funiture/ํ‹ฐ๋น„-270.png b/public/funiture/ํ‹ฐ๋น„-270.png new file mode 100644 index 0000000..6198090 Binary files /dev/null and b/public/funiture/ํ‹ฐ๋น„-270.png differ diff --git a/public/funiture/ํ‹ฐ๋น„-90.png b/public/funiture/ํ‹ฐ๋น„-90.png new file mode 100644 index 0000000..d5c096d Binary files /dev/null and b/public/funiture/ํ‹ฐ๋น„-90.png differ diff --git a/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-0.png b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-0.png new file mode 100644 index 0000000..4430734 Binary files /dev/null and b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-0.png differ diff --git a/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-180.png b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-180.png new file mode 100644 index 0000000..ad0a1e5 Binary files /dev/null and b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-180.png differ diff --git a/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-270.png b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-270.png new file mode 100644 index 0000000..686012c Binary files /dev/null and b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-270.png differ diff --git a/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-90.png b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-90.png new file mode 100644 index 0000000..f46d19f Binary files /dev/null and b/public/funiture/ํŒŒ๋ž€ ์นจ๋Œ€-90.png differ diff --git a/public/funiture/ํŒŒ๋ž€์ƒ‰ ํƒ์ž.png b/public/funiture/ํŒŒ๋ž€์ƒ‰ ํƒ์ž.png new file mode 100644 index 0000000..de86821 Binary files /dev/null and b/public/funiture/ํŒŒ๋ž€์ƒ‰ ํƒ์ž.png differ diff --git a/public/funiture/ํšŒ์ƒ‰ ํƒ์ž.png b/public/funiture/ํšŒ์ƒ‰ ํƒ์ž.png new file mode 100644 index 0000000..bc1bb49 Binary files /dev/null and b/public/funiture/ํšŒ์ƒ‰ ํƒ์ž.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-0.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-0.png new file mode 100644 index 0000000..0fd0b4c Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-0.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-180.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-180.png new file mode 100644 index 0000000..588886a Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-180.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-270.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-270.png new file mode 100644 index 0000000..62a1876 Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-270.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-90.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-90.png new file mode 100644 index 0000000..eb057e7 Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ1-90.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-0.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-0.png new file mode 100644 index 0000000..0fd0b4c Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-0.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-180.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-180.png new file mode 100644 index 0000000..8e811a5 Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-180.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-270.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-270.png new file mode 100644 index 0000000..6226b7b Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-270.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-90.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-90.png new file mode 100644 index 0000000..eb057e7 Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ2-90.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-0.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-0.png new file mode 100644 index 0000000..0fd0b4c Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-0.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-180.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-180.png new file mode 100644 index 0000000..3a69bf5 Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-180.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-270.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-270.png new file mode 100644 index 0000000..9fdfe3e Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-270.png differ diff --git a/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-90.png b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-90.png new file mode 100644 index 0000000..eb057e7 Binary files /dev/null and b/public/funiture/ํฐ ๋…ธํŠธ๋ถ3-90.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-0.png b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-0.png new file mode 100644 index 0000000..c9bd902 Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-0.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-180.png b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-180.png new file mode 100644 index 0000000..37ba475 Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-180.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-270.png b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-270.png new file mode 100644 index 0000000..a8a219c Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-270.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-90.png b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-90.png new file mode 100644 index 0000000..40496d2 Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์„ ๋ฐ˜-90.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-0.png b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-0.png new file mode 100644 index 0000000..148dadd Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-0.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-180.png b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-180.png new file mode 100644 index 0000000..dd631af Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-180.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-270.png b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-270.png new file mode 100644 index 0000000..91d96b5 Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-270.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-90.png b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-90.png new file mode 100644 index 0000000..b210efc Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ์ž‘์€ ์„ ๋ฐ˜-90.png differ diff --git a/public/funiture/ํฐ์ƒ‰ ํƒ์ž.png b/public/funiture/ํฐ์ƒ‰ ํƒ์ž.png new file mode 100644 index 0000000..9becce8 Binary files /dev/null and b/public/funiture/ํฐ์ƒ‰ ํƒ์ž.png differ diff --git a/public/room/room_1.png b/public/room/room_1.png new file mode 100644 index 0000000..0aa105e Binary files /dev/null and b/public/room/room_1.png differ diff --git a/public/room/room_2.png b/public/room/room_2.png new file mode 100644 index 0000000..8003a19 Binary files /dev/null and b/public/room/room_2.png differ diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..bdfc0eb --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +pythonpath = . +asyncio_mode = auto diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..86ab168 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,28 @@ +annotated-types==0.7.0 +anyio==4.9.0 +black==25.1.0 +click==8.2.1 +fastapi==0.115.12 +idna==3.10 +mypy_extensions==1.1.0 +packaging==25.0 +pathspec==0.12.1 +platformdirs==4.3.8 +pydantic==2.11.5 +pydantic_core==2.33.2 +sniffio==1.3.1 +starlette==0.46.2 +typing-inspection==0.4.1 +typing_extensions==4.13.2 +httpx>=0.25.0 +Pillow>=9.0.0 +python-multipart>=0.0.5 +aiofiles>=22.0.0 +jinja2>=3.0.0 +python-jose>=3.3.0 +passlib>=1.7.0 +uvicorn>=0.18.0 +aiosqlite>=0.17.0 +pytest>=7.0.0 +pytest-asyncio>=0.18.0 +aiosmtplib>=1.1.0 diff --git a/uploads/profile/2___instar_1753881367.png b/uploads/profile/2___instar_1753881367.png new file mode 100644 index 0000000..ff8c32b Binary files /dev/null and b/uploads/profile/2___instar_1753881367.png differ diff --git a/uploads/profile/_____1755559451.jpg b/uploads/profile/_____1755559451.jpg new file mode 100644 index 0000000..c41146b Binary files /dev/null and b/uploads/profile/_____1755559451.jpg differ