a
68
.github/workflows/black-check.yml
vendored
Normal file
|
|
@ -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` 명령어를 사용해 주세요."
|
||||
})
|
||||
118
.github/workflows/comment-command.yml
vendored
Normal file
|
|
@ -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
|
||||
});
|
||||
}
|
||||
40
.github/workflows/issue_or_pr.yml.disabled
vendored
Normal file
|
|
@ -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]
|
||||
});
|
||||
55
.github/workflows/pr-merge-status.yml
vendored
Normal file
|
|
@ -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"]
|
||||
});
|
||||
66
.github/workflows/pr-review-status.yml
vendored
Normal file
|
|
@ -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]
|
||||
});
|
||||
}
|
||||
232
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
4
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
12
Backend/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from .router.router import router
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from Backend.utils.run_server import init_FastAPI
|
||||
|
||||
# 프로젝트 루트를 Python 경로에 추가
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
app = init_FastAPI()
|
||||
|
||||
app.include_router(router)
|
||||
78
Backend/core/security.py
Normal file
|
|
@ -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
|
||||
49
Backend/router/endpoints/avatar.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from ...schemas.avatar import (
|
||||
AvatarUpdate,
|
||||
AvatarResponse,
|
||||
AvatarOptions,
|
||||
)
|
||||
from ...services.avatar_service import AvatarService
|
||||
from ...core.security import get_current_user
|
||||
from ...schemas.user import User
|
||||
|
||||
router = APIRouter(prefix="/avatar", tags=["avatar"])
|
||||
avatar_service = AvatarService()
|
||||
|
||||
|
||||
@router.get("", response_model=AvatarResponse)
|
||||
async def get_my_avatar(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> AvatarResponse:
|
||||
try:
|
||||
avatar = await avatar_service.get_or_create_avatar(current_user.id)
|
||||
return avatar.to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("", response_model=AvatarResponse)
|
||||
async def update_avatar(
|
||||
avatar_data: AvatarUpdate, current_user: User = Depends(get_current_user)
|
||||
) -> AvatarResponse:
|
||||
try:
|
||||
avatar = await avatar_service.update_avatar(current_user.id, avatar_data)
|
||||
return avatar.to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/options", response_model=AvatarOptions)
|
||||
async def get_avatar_options() -> AvatarOptions:
|
||||
try:
|
||||
return await avatar_service.get_avatar_options()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.get("/{user_id}", response_model=AvatarResponse)
|
||||
async def get_avatar_by_userId(user_id: int) -> AvatarResponse:
|
||||
avatar = await avatar_service.get_avatar_by_userId(user_id)
|
||||
if not avatar:
|
||||
raise HTTPException(status_code=404, detail="Avatar not found")
|
||||
return avatar.to_response()
|
||||
102
Backend/router/endpoints/diary.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
from fastapi import (
|
||||
APIRouter,
|
||||
HTTPException,
|
||||
Depends,
|
||||
UploadFile,
|
||||
File,
|
||||
)
|
||||
from typing import List, Optional
|
||||
from ...schemas.diary import DiaryCreate, DiaryUpdate, DiaryResponse, Diary
|
||||
from ...services.diary_service import DiaryService
|
||||
from ...core.security import get_current_user
|
||||
from ...schemas.user import User
|
||||
|
||||
router = APIRouter(prefix="/diary", tags=["diary"])
|
||||
diary_service = DiaryService()
|
||||
|
||||
|
||||
@router.post("", response_model=DiaryResponse)
|
||||
async def create_diary(
|
||||
diary_data: DiaryCreate = Depends(DiaryCreate.as_form),
|
||||
file: List[UploadFile] = File(default=None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> DiaryResponse:
|
||||
try:
|
||||
diary = await diary_service.create_diary(current_user.id, diary_data, file)
|
||||
return diary.to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("", response_model=List[DiaryResponse])
|
||||
async def get_user_diaries(
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
category: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> List[DiaryResponse]:
|
||||
try:
|
||||
diaries = await diary_service.get_user_diaries(
|
||||
current_user.id, skip, limit, category
|
||||
)
|
||||
return [diary.to_response() for diary in diaries]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/w/{target_user_id}", response_model=List[DiaryResponse])
|
||||
async def get_target_user_diaries(
|
||||
target_user_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
category: Optional[str] = None,
|
||||
) -> List[DiaryResponse]:
|
||||
try:
|
||||
diaries = await diary_service.get_user_diaries(
|
||||
target_user_id, skip, limit, category
|
||||
)
|
||||
return [diary.to_response() for diary in diaries]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{diary_id}", response_model=DiaryResponse)
|
||||
async def get_diary(diary_id: int) -> DiaryResponse:
|
||||
try:
|
||||
diary = await diary_service.get_diary_by_id(diary_id)
|
||||
if not diary:
|
||||
raise HTTPException(status_code=404, detail="Diary not found")
|
||||
return diary.to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{diary_id}", response_model=DiaryResponse)
|
||||
async def update_diary(
|
||||
diary_id: int,
|
||||
diary_data: DiaryUpdate = Depends(DiaryUpdate.as_form),
|
||||
file: List[UploadFile] = File(default=None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> DiaryResponse:
|
||||
try:
|
||||
diary = await diary_service.update_diary(
|
||||
diary_id, current_user.id, diary_data, file
|
||||
)
|
||||
if not diary:
|
||||
raise HTTPException(status_code=404, detail="Diary not found")
|
||||
return diary.to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{diary_id}")
|
||||
async def delete_diary(
|
||||
diary_id: int, current_user: User = Depends(get_current_user)
|
||||
) -> dict:
|
||||
try:
|
||||
success = await diary_service.delete_diary(diary_id, current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Diary not found")
|
||||
return {"message": "Diary deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
81
Backend/router/endpoints/friendship.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import List, Optional
|
||||
from ...schemas.friendship import (
|
||||
FriendshipRequest,
|
||||
FriendshipResponse,
|
||||
)
|
||||
from ...services.friendship_service import FriendshipService
|
||||
from ...core.security import get_current_user
|
||||
from ...schemas.user import User
|
||||
|
||||
router = APIRouter(prefix="/friendship", tags=["friendship"])
|
||||
friendship_service = FriendshipService()
|
||||
|
||||
|
||||
@router.post("/request", response_model=FriendshipResponse)
|
||||
async def send_friendship_request(
|
||||
request_data: FriendshipRequest, current_user: User = Depends(get_current_user)
|
||||
) -> FriendshipResponse:
|
||||
try:
|
||||
friendship = await friendship_service.send_friendship_request(
|
||||
current_user.id, request_data.friend_username
|
||||
)
|
||||
return friendship
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{friendship_id}/accept", response_model=FriendshipResponse)
|
||||
async def accept_friendship_request(
|
||||
friendship_id: int, current_user: User = Depends(get_current_user)
|
||||
) -> FriendshipResponse:
|
||||
try:
|
||||
friendship = await friendship_service.accept_friendship_request(
|
||||
friendship_id, current_user.id
|
||||
)
|
||||
if not friendship:
|
||||
raise HTTPException(status_code=404, detail="Friendship request not found")
|
||||
return friendship
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("", response_model=List[FriendshipResponse])
|
||||
async def get_friendships(
|
||||
status: Optional[str] = None, current_user: User = Depends(get_current_user)
|
||||
) -> List[FriendshipResponse]:
|
||||
try:
|
||||
friendships = await friendship_service.get_user_friendships(
|
||||
current_user.id, status
|
||||
)
|
||||
return friendships
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{friendship_id}")
|
||||
async def delete_friendship(
|
||||
friendship_id: int, current_user: User = Depends(get_current_user)
|
||||
) -> dict:
|
||||
try:
|
||||
success = await friendship_service.delete_friendship(
|
||||
friendship_id, current_user.id
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Friendship not found")
|
||||
return {"message": "Friendship deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/pending", response_model=List[FriendshipResponse])
|
||||
async def get_pending_requests(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> List[FriendshipResponse]:
|
||||
try:
|
||||
pending_requests = await friendship_service.get_pending_requests(
|
||||
current_user.id
|
||||
)
|
||||
return pending_requests
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
63
Backend/router/endpoints/guestbook.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from typing import List
|
||||
from fastapi import APIRouter, HTTPException, Depends, status
|
||||
from Backend.core.security import get_current_user
|
||||
from Backend.schemas.guestbook import (
|
||||
GuestBookCreate,
|
||||
GuestbookUpdate,
|
||||
GuestbookResponse,
|
||||
)
|
||||
from Backend.schemas.user import User
|
||||
from Backend.services.guestbook_service import GuestbookService
|
||||
|
||||
router = APIRouter(prefix="/guestbook", tags=["guestbook"])
|
||||
|
||||
guestbook_service = GuestbookService()
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=GuestbookResponse)
|
||||
async def create_guestbook(
|
||||
guestbook: GuestBookCreate, current_user: User = Depends(get_current_user)
|
||||
) -> GuestbookResponse:
|
||||
try:
|
||||
response = await guestbook_service.create_guestbook(guestbook, current_user)
|
||||
return response
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{target_user_id}", response_model=List[GuestbookResponse])
|
||||
async def get_guestbook(target_user_id: int):
|
||||
try:
|
||||
response = await guestbook_service.get_target_user_guestbooks(target_user_id)
|
||||
response.reverse()
|
||||
return response
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{id}", response_model=GuestbookResponse)
|
||||
async def update_guestbook(
|
||||
id: int,
|
||||
guestbook: GuestbookUpdate,
|
||||
) -> GuestbookResponse:
|
||||
try:
|
||||
response = await guestbook_service.update_guestbook_by_id(id, guestbook.content)
|
||||
return response
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_guestbook(
|
||||
id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
try:
|
||||
is_success = await guestbook_service.delete_guestbook_by_id(id, user.id)
|
||||
if is_success:
|
||||
return {"detail": "success"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="guestbook not found")
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
79
Backend/router/endpoints/letter.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# from fastapi import APIRouter, HTTPException, Depends
|
||||
#
|
||||
# from ...schemas.letter import LetterCreate, LetterResponse, EmailRequest
|
||||
# from ...services.letter_service import LetterService
|
||||
# from ...core.security import get_current_user
|
||||
# from ...schemas.user import User
|
||||
#
|
||||
# router = APIRouter(prefix="/letter", tags=["letter"])
|
||||
# letter_service = LetterService()
|
||||
#
|
||||
#
|
||||
# @router.post("", response_model=LetterResponse)
|
||||
# async def create_letter(
|
||||
# letter_data: LetterCreate,
|
||||
# current_user: User = Depends(get_current_user),
|
||||
# ) -> LetterResponse:
|
||||
# try:
|
||||
# letter = await letter_service.create_letter(current_user.id, letter_data)
|
||||
# return letter.to_response()
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.get("/{letter_id}", response_model=LetterResponse)
|
||||
# async def get_letter(
|
||||
# letter_id: int, current_user: User = Depends(get_current_user)
|
||||
# ) -> LetterResponse:
|
||||
# try:
|
||||
# letter = await letter_service.get_letter_by_id(letter_id, current_user.id)
|
||||
# if not letter:
|
||||
# raise HTTPException(status_code=404, detail="Letter not found")
|
||||
# return letter.to_response()
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.delete("/{letter_id}")
|
||||
# async def delete_letter(letter_id: int, current_user: User = Depends(get_current_user)):
|
||||
# try:
|
||||
# is_success = await letter_service.delete_letter(letter_id, current_user.id)
|
||||
# if not is_success:
|
||||
# raise HTTPException(status_code=404, detail="Letter not found")
|
||||
# return {"detail": "Letter deleted"}
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.put("/{letter_id}", response_model=LetterResponse)
|
||||
# async def update_letter(
|
||||
# letter_id: int,
|
||||
# letter_data: LetterCreate,
|
||||
# current_user: User = Depends(get_current_user),
|
||||
# ):
|
||||
# try:
|
||||
# letter = await letter_service.update_letter(
|
||||
# letter_id, current_user.id, letter_data.content
|
||||
# )
|
||||
# if not letter:
|
||||
# raise HTTPException(status_code=404, detail="Letter not found")
|
||||
# return letter.to_response()
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.post("/{letter_id}/send")
|
||||
# async def send_letter(
|
||||
# letter_id: int,
|
||||
# letter_data: EmailRequest,
|
||||
# current_user: User = Depends(get_current_user),
|
||||
# ):
|
||||
# try:
|
||||
# letter = await letter_service.get_letter_by_id(letter_id, current_user.id)
|
||||
# if not letter:
|
||||
# raise HTTPException(status_code=404, detail="Letter not found")
|
||||
#
|
||||
# await letter_service.send_letter(letter, letter_data)
|
||||
# return {"message": "Email sent successfully!"}
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
117
Backend/router/endpoints/photo.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Query, Form
|
||||
# from typing import List
|
||||
# from ...schemas.photo import (
|
||||
# PhotoUpload,
|
||||
# PhotoResponse,
|
||||
# CommentCreate,
|
||||
# CommentResponse,
|
||||
# FilterRequest,
|
||||
# )
|
||||
# from ...services.photo_service import PhotoService
|
||||
# from ...core.security import get_current_user
|
||||
# from ...schemas.user import User
|
||||
# from pydantic import ValidationError
|
||||
#
|
||||
# router = APIRouter(prefix="/photo", tags=["photo"])
|
||||
# photo_service = PhotoService()
|
||||
#
|
||||
#
|
||||
# @router.post("/upload", response_model=PhotoResponse)
|
||||
# async def upload_photo(
|
||||
# photo_data: UploadFile = Form(...),
|
||||
# file: UploadFile = File(...),
|
||||
# current_user: User = Depends(get_current_user),
|
||||
# ) -> PhotoResponse:
|
||||
# import json
|
||||
#
|
||||
# try:
|
||||
# photo_data_bytes = await photo_data.read()
|
||||
# photo_info = json.loads(photo_data_bytes.decode("utf-8"))
|
||||
# photo_data = PhotoUpload(**photo_info)
|
||||
# except (json.JSONDecodeError, ValidationError) as e:
|
||||
# raise HTTPException(status_code=400, detail="Invalid photo data format")
|
||||
# try:
|
||||
# photo = await photo_service.upload_photo(current_user.id, photo_data, file)
|
||||
# return photo.to_response()
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.get("", response_model=List[PhotoResponse])
|
||||
# async def get_user_photos(
|
||||
# skip: int = 0,
|
||||
# limit: int = 20,
|
||||
# album_name: str = None,
|
||||
# current_user: User = Depends(get_current_user),
|
||||
# ) -> List[PhotoResponse]:
|
||||
# try:
|
||||
# photos = await photo_service.get_user_photos(
|
||||
# current_user.id, skip, limit, album_name
|
||||
# )
|
||||
# return [photo.to_response() for photo in photos]
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.post("/{photo_id}/comment", response_model=CommentResponse)
|
||||
# async def add_photo_comment(
|
||||
# photo_id: int,
|
||||
# comment_data: CommentCreate,
|
||||
# current_user: User = Depends(get_current_user),
|
||||
# ) -> CommentResponse:
|
||||
# try:
|
||||
# is_friend = await photo_service.check_friendship(current_user.id, photo_id)
|
||||
# if not is_friend:
|
||||
# raise HTTPException(
|
||||
# status_code=403, detail="Only friends can comment on photos"
|
||||
# )
|
||||
#
|
||||
# comment = await photo_service.add_comment(
|
||||
# photo_id, current_user.id, comment_data
|
||||
# )
|
||||
# return comment.to_response(current_user.username)
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.get("/{photo_id}/comments", response_model=List[CommentResponse])
|
||||
# async def get_photo_comments(
|
||||
# photo_id: int, current_user: User = Depends(get_current_user)
|
||||
# ) -> List[CommentResponse]:
|
||||
# try:
|
||||
# comments = await photo_service.get_photo_comments(photo_id)
|
||||
# return comments
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.post("/edit-filter")
|
||||
# async def apply_photo_filter(
|
||||
# filter_request: FilterRequest, current_user: User = Depends(get_current_user)
|
||||
# ) -> dict:
|
||||
# if filter_request.cover and filter_request.title is None:
|
||||
# raise HTTPException(status_code=400, detail="title must be Not Null")
|
||||
# try:
|
||||
# filtered_image_path = await photo_service.apply_filter(
|
||||
# filter_request.photo_id,
|
||||
# filter_request.filter_type,
|
||||
# current_user.id,
|
||||
# filter_request.cover,
|
||||
# filter_request.title,
|
||||
# )
|
||||
# return {"filtered_image_path": filtered_image_path}
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
#
|
||||
#
|
||||
# @router.delete("/{photo_id}")
|
||||
# async def delete_photo(
|
||||
# photo_id: int, current_user: User = Depends(get_current_user)
|
||||
# ) -> dict:
|
||||
# try:
|
||||
# success = await photo_service.delete_photo(photo_id, current_user.id)
|
||||
# if not success:
|
||||
# raise HTTPException(status_code=404, detail="Photo not found")
|
||||
# return {"message": "Photo deleted successfully"}
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=400, detail=str(e))
|
||||
110
Backend/router/endpoints/room.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import List
|
||||
from Backend.schemas.room import (
|
||||
FurnitureItem,
|
||||
RoomFurniturePlacement,
|
||||
FurniturePlacementRequest,
|
||||
RoomNameUpdateRequest,
|
||||
UpdateRoomTypeRequest,
|
||||
RoomResponse,
|
||||
RoomTypeResponse,
|
||||
RoomFurnitureResponse,
|
||||
)
|
||||
from Backend.schemas.user import User
|
||||
from Backend.core.security import get_current_user
|
||||
from Backend.services.room_service import RoomService
|
||||
|
||||
router = APIRouter(prefix="/room", tags=["room"])
|
||||
room_service = RoomService()
|
||||
|
||||
|
||||
@router.get("/catalog", response_model=List[FurnitureItem])
|
||||
async def get_furniture_catalog() -> List[FurnitureItem]:
|
||||
return await room_service.get_furniture_catalog()
|
||||
|
||||
|
||||
@router.get("/layout", response_model=RoomFurnitureResponse)
|
||||
async def get_my_room_layout(current_user: User = Depends(get_current_user)):
|
||||
room_id = await room_service.get_or_create_room(current_user.id)
|
||||
return await room_service.get_room_furnitures(room_id)
|
||||
|
||||
|
||||
@router.get("/layout/{user_id}", response_model=RoomFurnitureResponse)
|
||||
async def get_user_room_layout(user_id: int) -> RoomFurnitureResponse:
|
||||
room_id = await room_service.get_or_create_room(user_id)
|
||||
return await room_service.get_room_furnitures(room_id)
|
||||
|
||||
|
||||
@router.post("/furniture")
|
||||
async def place_furniture(
|
||||
request: FurniturePlacementRequest, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
room_id = await room_service.get_or_create_room(current_user.id)
|
||||
try:
|
||||
await room_service.place_furniture(room_id, request)
|
||||
return {"message": "Furniture placed successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/furniture")
|
||||
async def remove_furniture(
|
||||
x: int, y: int, furniture_name: str, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
room_id = await room_service.get_or_create_room(current_user.id)
|
||||
await room_service.remove_furniture(room_id, x, y, furniture_name)
|
||||
return {"message": "Furniture removed successfully"}
|
||||
|
||||
|
||||
@router.put("/")
|
||||
async def update_room_name(
|
||||
data: RoomNameUpdateRequest, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
try:
|
||||
room_id = await room_service.get_or_create_room(current_user.id)
|
||||
await room_service.update_room_name(room_id, data.new_name)
|
||||
return {"message": "Room name updated successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_my_room(current_user: User = Depends(get_current_user)) -> RoomResponse:
|
||||
try:
|
||||
room_id = await room_service.get_or_create_room(current_user.id)
|
||||
return (await room_service.get_room_by_id(room_id)).to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/types")
|
||||
async def get_room_types() -> List[RoomTypeResponse]:
|
||||
return await room_service.get_room_types()
|
||||
|
||||
|
||||
@router.patch("/")
|
||||
async def update_room_type(
|
||||
data: UpdateRoomTypeRequest, current_user: User = Depends(get_current_user)
|
||||
) -> RoomResponse:
|
||||
try:
|
||||
room_id = await room_service.get_or_create_room(current_user.id)
|
||||
return (await room_service.update_room_type(room_id, data.type)).to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/my")
|
||||
async def get_my_furniture(current_user: User = Depends(get_current_user)):
|
||||
try:
|
||||
list_ = await room_service.get_user_furniture(current_user.id)
|
||||
return list_
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.get("/{user_id}", response_model=RoomResponse)
|
||||
async def get_room_by_userId(user_id: int) -> RoomResponse:
|
||||
try:
|
||||
room = await room_service.get_room_by_userId(user_id)
|
||||
return room.to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
48
Backend/router/endpoints/store.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from ...core.security import get_current_user
|
||||
from ...schemas.user import User
|
||||
from Backend.services.store_service import StoreService
|
||||
from ...services.room_service import RoomService
|
||||
|
||||
router = APIRouter(prefix="/store", tags=["store"])
|
||||
store_service = StoreService()
|
||||
room_service = RoomService()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_dotory(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
dotory = await store_service.get_dotory_by_id(current_user.id)
|
||||
return {"dotory": dotory}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{product_id}")
|
||||
async def buy_product(
|
||||
product_id: int,
|
||||
product_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
response = await store_service.buy_product(product_id, current_user.id)
|
||||
if response["isSuccess"]:
|
||||
await room_service.add_furniture(current_user.id, product_name)
|
||||
return {"dotory": response["dotory"]}
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("")
|
||||
async def update_dotory(
|
||||
dotory_num: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
dotory = await store_service.update_user_dotory(current_user.id, dotory_num)
|
||||
return {"dotory": dotory}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
116
Backend/router/endpoints/user.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status, UploadFile, File, Depends
|
||||
from Backend.schemas.user import UserCreate, UserLogin, UserResponse, User, UserUpdate
|
||||
from Backend.services.user_service import UserService
|
||||
from Backend.core.security import create_access_token, get_current_user
|
||||
|
||||
router = APIRouter(prefix="/user", tags=["user"])
|
||||
|
||||
user_service = UserService()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def register_user(
|
||||
user_data: UserCreate = Depends(UserCreate.as_form),
|
||||
profile_file: UploadFile = File(default=None),
|
||||
) -> UserResponse:
|
||||
existing_user = await user_service.get_user_by_username
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already registered"
|
||||
)
|
||||
|
||||
existing_email = await user_service.get_user_by_email(user_data.email)
|
||||
if existing_email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already registered"
|
||||
)
|
||||
|
||||
try:
|
||||
user = await user_service.create_user(user_data, profile_file)
|
||||
return user.to_response()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to created user: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/login")
|
||||
async def login_user(login_data: UserLogin) -> dict:
|
||||
user = await user_service.authenticate_user(
|
||||
login_data.username, login_data.password
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=
|
||||
detail=
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=, detail=""
|
||||
)
|
||||
|
||||
access_token =
|
||||
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
|
||||
@router.get("/profile/{username}", response_model=UserResponse)
|
||||
async def get_user_profile(username: str) -> UserResponse:
|
||||
user = await user_service.get_user_by_username(username)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
|
||||
return user.to_response()
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_user_me(user: User = Depends(get_current_user)) -> UserResponse:
|
||||
return user.to_response()
|
||||
|
||||
|
||||
@router.delete("/{username}")
|
||||
async def delete_user(username: str) -> dict:
|
||||
is_success = await user_service.delete_user(username)
|
||||
if not is_success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
return {"detail": "User deleted"}
|
||||
|
||||
|
||||
@router.get("/find/{username}", response_model=List[UserResponse])
|
||||
async def find_user(username: str) -> List[UserResponse]:
|
||||
users = await user_service.find_user(username)
|
||||
return users
|
||||
|
||||
|
||||
@router.put("/", response_model=UserResponse)
|
||||
async def update_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
user_data: UserUpdate = Depends(UserUpdate.as_form),
|
||||
profile_file: UploadFile = File(default=None),
|
||||
) -> UserResponse:
|
||||
try:
|
||||
updated_user = await user_service.update_user(
|
||||
user=current_user, user_data=user_data, profile_file=profile_file
|
||||
)
|
||||
return updated_user.to_response()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update user: {str(e)}",
|
||||
)
|
||||
21
Backend/router/router.py
Normal file
|
|
@ -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()}")
|
||||
0
Backend/schemas/__init__.py
Normal file
106
Backend/schemas/avatar.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
from pydantic import BaseModel
|
||||
import enum
|
||||
|
||||
|
||||
class AvatarType(str, enum.Enum):
|
||||
MALE = "남성"
|
||||
FEMALE = "여성"
|
||||
|
||||
|
||||
class TopClothesType(str, enum.Enum):
|
||||
SCHOOL_CLOTHES = "교복"
|
||||
SCHOOL_CLOTHES_2 = "교복 조끼"
|
||||
ANA_CLOTHES = "AnA 동잠"
|
||||
SUSPENDERS_CLOTHES_1 = "멜빵 바지"
|
||||
SUSPENDERS_CLOTHES_2 = "멜빵 치마"
|
||||
RAINBOW_CLOTHES = "무지개 맨투맨"
|
||||
SANTA_CLOTHES = "산타"
|
||||
|
||||
|
||||
class BottomClothesType(str, enum.Enum):
|
||||
SCHOOL_CLOTHES = "교복 바지"
|
||||
SCHOOL_CLOTHES_2 = "교복 치마"
|
||||
SCHOOL_CLOTHES_3 = "교복 조끼 바지"
|
||||
SCHOOL_CLOTHES_4 = "교복 조끼 치마"
|
||||
SANTA_CLOTHES = "산타 바지"
|
||||
JEANS = "청바지"
|
||||
|
||||
|
||||
avatar_path_ = {"남성": "public/avatar/남자.png", "여성": "public/avatar/여자.png"}
|
||||
top_clothe_path_ = {
|
||||
"교복": "public/avatar/교복상의.png",
|
||||
"교복 조끼": "public/avatar/교복조끼상의.png",
|
||||
"AnA 동잠": "public/avatar/동잠상의.png",
|
||||
"멜빵 바지": "public/avatar/멜빵바지상의.png",
|
||||
"멜빵 치마": "public/avatar/멜빵치마상의.png",
|
||||
"무지개 맨투맨": "public/avatar/무지개맨투맨상의.png",
|
||||
"산타": "public/avatar/산타상의.png",
|
||||
}
|
||||
bottom_clothe_path_ = {
|
||||
"교복 바지": "public/avatar/교복하의남.png",
|
||||
"교복 치마": "public/avatar/교복하의여.png",
|
||||
"교복 조끼 바지": "public/avatar/교복조끼하의남.png",
|
||||
"교복 조끼 치마": "public/avatar/교복조끼하의여.png",
|
||||
"산타 바지": "public/avatar/산타하의.png",
|
||||
"청바지": "public/avatar/청바지하의.png",
|
||||
}
|
||||
|
||||
|
||||
class AvatarTypeResponse(BaseModel):
|
||||
name: str
|
||||
path: str
|
||||
|
||||
|
||||
class AvatarUpdate(BaseModel):
|
||||
avatar_type: AvatarType
|
||||
top_clothe_type: TopClothesType
|
||||
bottom_clothe_type: BottomClothesType
|
||||
|
||||
|
||||
class AvatarResponse(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
avatar_type: AvatarTypeResponse
|
||||
top_clothe_type: AvatarTypeResponse
|
||||
bottom_clothe_type: AvatarTypeResponse
|
||||
|
||||
|
||||
class AvatarOptions(BaseModel):
|
||||
avatar_types: list[str]
|
||||
top_clothe_types: list[str]
|
||||
bottom_clothe_types: list[str]
|
||||
|
||||
|
||||
|
||||
class Avatar:
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
user_id: int,
|
||||
avatar_type: AvatarType,
|
||||
top_clothe_type: TopClothesType,
|
||||
bottom_clothe_type: BottomClothesType,
|
||||
):
|
||||
self.id = id
|
||||
self.user_id = user_id
|
||||
self.avatar_type = avatar_type
|
||||
self.top_clothe_type = top_clothe_type
|
||||
self.bottom_clothe_type = bottom_clothe_type
|
||||
|
||||
def to_response(self) -> AvatarResponse:
|
||||
return AvatarResponse(
|
||||
id=self.id,
|
||||
user_id=self.user_id,
|
||||
avatar_type=AvatarTypeResponse(
|
||||
name=self.avatar_type,
|
||||
path=avatar_path_[self.avatar_type],
|
||||
),
|
||||
top_clothe_type=AvatarTypeResponse(
|
||||
name=self.top_clothe_type,
|
||||
path=top_clothe_path_[self.top_clothe_type],
|
||||
),
|
||||
bottom_clothe_type=AvatarTypeResponse(
|
||||
name=self.bottom_clothe_type,
|
||||
path=bottom_clothe_path_[self.bottom_clothe_type],
|
||||
),
|
||||
)
|
||||
109
Backend/schemas/diary.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
59
Backend/schemas/friendship.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from pydantic import BaseModel, field_validator
|
||||
from datetime import datetime
|
||||
import enum
|
||||
|
||||
|
||||
class FriendshipStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
ACCEPTED = "accepted"
|
||||
REJECTED = "rejected"
|
||||
|
||||
|
||||
class FriendshipRequest(BaseModel):
|
||||
friend_username: str
|
||||
|
||||
@field_validator("friend_username")
|
||||
@classmethod
|
||||
def validate_friend_username(cls, v):
|
||||
if len(v.strip()) < 1:
|
||||
raise ValueError("Friend username cannot be empty")
|
||||
return v.strip()
|
||||
|
||||
|
||||
class FriendshipResponse(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
friend_id: int
|
||||
friend_username: str
|
||||
status: FriendshipStatus
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class FriendshipUpdate(BaseModel):
|
||||
status: FriendshipStatus
|
||||
|
||||
|
||||
class Friendship:
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
user_id: int,
|
||||
friend_id: int,
|
||||
status: str,
|
||||
created_at: datetime,
|
||||
):
|
||||
self.id = id
|
||||
self.user_id = user_id
|
||||
self.friend_id = friend_id
|
||||
self.status = status
|
||||
self.created_at = created_at
|
||||
|
||||
def to_response(self, friend_username: str) -> FriendshipResponse:
|
||||
return FriendshipResponse(
|
||||
id=self.id,
|
||||
user_id=self.user_id,
|
||||
friend_id=self.friend_id,
|
||||
friend_username=friend_username,
|
||||
status=FriendshipStatus(self.status),
|
||||
created_at=self.created_at,
|
||||
)
|
||||
46
Backend/schemas/guestbook.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from datetime import datetime
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
class GuestBookCreate(BaseModel):
|
||||
content: str
|
||||
target_user_id: int
|
||||
|
||||
@field_validator("content")
|
||||
@classmethod
|
||||
def validate_content(cls, v):
|
||||
if len(v.strip()) < 1:
|
||||
raise ValueError("GuestBook content cannot be empty")
|
||||
if len(v) > 2000:
|
||||
raise ValueError("GuestBook content must be less than 2000 characters")
|
||||
return v.strip()
|
||||
|
||||
|
||||
class GuestbookUpdate(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
class GuestbookResponse(BaseModel):
|
||||
id: int
|
||||
content: str
|
||||
target_user_id: int
|
||||
user_id: int
|
||||
user_profile_path: str
|
||||
username: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class GuestBook:
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
target_user_id: int,
|
||||
user_id,
|
||||
content,
|
||||
created_at: datetime,
|
||||
):
|
||||
self.id = id
|
||||
self.target_user_id = target_user_id
|
||||
self.user_id = user_id
|
||||
self.content = content
|
||||
self.created_at = created_at
|
||||
47
Backend/schemas/letter.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# from pydantic import BaseModel, field_validator, EmailStr
|
||||
#
|
||||
# # email validator 삭제 및 EmailStr 사용
|
||||
#
|
||||
#
|
||||
# class LetterCreate(BaseModel):
|
||||
# content: str
|
||||
#
|
||||
# @field_validator("content")
|
||||
# @classmethod
|
||||
# def validate_content(cls, v):
|
||||
# if len(v.strip()) < 1:
|
||||
# raise ValueError("Letter content cannot be empty")
|
||||
# if len(v) > 2000:
|
||||
# raise ValueError("Letter content must be less than 2000 characters")
|
||||
# return v.strip()
|
||||
#
|
||||
#
|
||||
# class LetterResponse(BaseModel):
|
||||
# id: int
|
||||
# sender_id: int
|
||||
# content: str
|
||||
#
|
||||
#
|
||||
# class Letter:
|
||||
# def __init__(
|
||||
# self,
|
||||
# id: int,
|
||||
# sender_id: int,
|
||||
# content: str,
|
||||
# ):
|
||||
# self.id = id
|
||||
# self.sender_id = sender_id
|
||||
# self.content = content
|
||||
#
|
||||
# def to_response(self) -> LetterResponse:
|
||||
# return LetterResponse(
|
||||
# id=self.id,
|
||||
# sender_id=self.sender_id,
|
||||
# content=self.content,
|
||||
# )
|
||||
#
|
||||
#
|
||||
# class EmailRequest(BaseModel):
|
||||
# sender_email: EmailStr
|
||||
# sender_password: str
|
||||
# sender_name: str
|
||||
117
Backend/schemas/photo.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# from pydantic import BaseModel, field_validator
|
||||
# from datetime import datetime
|
||||
#
|
||||
#
|
||||
# class PhotoUpload(BaseModel):
|
||||
# album_name: str
|
||||
# title: str
|
||||
#
|
||||
# @field_validator("album_name")
|
||||
# @classmethod
|
||||
# def validate_album_name(cls, v):
|
||||
# if len(v.strip()) < 1:
|
||||
# raise ValueError("Album name cannot be empty")
|
||||
# if len(v) > 50:
|
||||
# raise ValueError("Album name must be less than 50 characters")
|
||||
# return v.strip()
|
||||
#
|
||||
# @field_validator("title")
|
||||
# @classmethod
|
||||
# def validate_title(cls, v):
|
||||
# if len(v.strip()) < 1:
|
||||
# raise ValueError("Title cannot be empty")
|
||||
# if len(v) > 100:
|
||||
# raise ValueError("Title must be less than 100 characters")
|
||||
# return v.strip()
|
||||
#
|
||||
#
|
||||
# class PhotoResponse(BaseModel):
|
||||
# id: int
|
||||
# user_id: int
|
||||
# album_name: str
|
||||
# image_path: str
|
||||
# title: str
|
||||
# created_at: datetime
|
||||
#
|
||||
#
|
||||
# class CommentCreate(BaseModel):
|
||||
# content: str
|
||||
#
|
||||
# @field_validator("content")
|
||||
# @classmethod
|
||||
# def validate_content(cls, v):
|
||||
# if len(v.strip()) < 1:
|
||||
# raise ValueError("Comment cannot be empty")
|
||||
# if len(v) > 500:
|
||||
# raise ValueError("Comment must be less than 500 characters")
|
||||
# return v.strip()
|
||||
#
|
||||
#
|
||||
# class CommentResponse(BaseModel):
|
||||
# id: int
|
||||
# photo_id: int
|
||||
# user_id: int
|
||||
# username: str
|
||||
# content: str
|
||||
# created_at: datetime
|
||||
#
|
||||
#
|
||||
# class FilterRequest(BaseModel):
|
||||
# photo_id: int
|
||||
# filter_type: str
|
||||
# cover: bool
|
||||
# title: str = None
|
||||
#
|
||||
#
|
||||
# class Photo:
|
||||
# def __init__(
|
||||
# self,
|
||||
# id: int,
|
||||
# user_id: int,
|
||||
# album_name: str,
|
||||
# image_path: str,
|
||||
# title: str,
|
||||
# created_at: datetime,
|
||||
# ):
|
||||
# self.id = id
|
||||
# self.user_id = user_id
|
||||
# self.album_name = album_name
|
||||
# self.image_path = image_path
|
||||
# self.title = title
|
||||
# self.created_at = created_at
|
||||
#
|
||||
# def to_response(self) -> PhotoResponse:
|
||||
# return PhotoResponse(
|
||||
# id=self.id,
|
||||
# user_id=self.user_id,
|
||||
# album_name=self.album_name,
|
||||
# image_path=self.image_path,
|
||||
# title=self.title,
|
||||
# created_at=self.created_at,
|
||||
# )
|
||||
#
|
||||
#
|
||||
# class PhotoComment:
|
||||
# def __init__(
|
||||
# self,
|
||||
# id: int,
|
||||
# photo_id: int,
|
||||
# user_id: int,
|
||||
# content: str,
|
||||
# created_at: datetime,
|
||||
# ):
|
||||
# self.id = id
|
||||
# self.photo_id = photo_id
|
||||
# self.user_id = user_id
|
||||
# self.content = content
|
||||
# self.created_at = created_at
|
||||
#
|
||||
# def to_response(self, username: str) -> CommentResponse:
|
||||
# return CommentResponse(
|
||||
# id=self.id,
|
||||
# photo_id=self.photo_id,
|
||||
# user_id=self.user_id,
|
||||
# username=username,
|
||||
# content=self.content,
|
||||
# created_at=self.created_at,
|
||||
# )
|
||||
296
Backend/schemas/room.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
import enum
|
||||
from pydantic import BaseModel, field_validator
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class RoomNameUpdateRequest(BaseModel):
|
||||
new_name: str
|
||||
|
||||
|
||||
class RoomTypes(enum.Enum):
|
||||
ROOM_1 = "room_1"
|
||||
ROOM_2 = "room_2"
|
||||
|
||||
|
||||
class UpdateRoomTypeRequest(BaseModel):
|
||||
type: RoomTypes
|
||||
|
||||
|
||||
class RoomTypeResponse(BaseModel):
|
||||
type: str
|
||||
image_path: str
|
||||
|
||||
|
||||
class RoomResponse(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
room_name: str
|
||||
room_type: RoomTypes
|
||||
room_image_path: str
|
||||
|
||||
|
||||
room_path = {
|
||||
"room_1": "public/room/room_1.png",
|
||||
"room_2": "public/room/room_2.png",
|
||||
}
|
||||
|
||||
|
||||
class FurnitureItem(BaseModel):
|
||||
name: str
|
||||
image_path: str
|
||||
width: int
|
||||
|
||||
|
||||
class Furniture(str, enum.Enum):
|
||||
BLACK_LAPTOP1_0 = "검정 노트북1-01"
|
||||
BLACK_LAPTOP1_180 = "검정 노트북1-1801"
|
||||
BLACK_LAPTOP1_270 = "검정 노트북1-2701"
|
||||
BLACK_LAPTOP1_90 = "검정 노트북1-901"
|
||||
BLACK_LAPTOP2_0 = "검정 노트북2-01"
|
||||
BLACK_LAPTOP2_180 = "검정 노트북2-1801"
|
||||
BLACK_LAPTOP2_270 = "검정 노트북2-2701"
|
||||
BLACK_LAPTOP2_90 = "검정 노트북2-901"
|
||||
BLACK_LAPTOP3_0 = "검정 노트북3-01"
|
||||
BLACK_LAPTOP3_180 = "검정 노트북3-1801"
|
||||
BLACK_LAPTOP3_270 = "검정 노트북3-2701"
|
||||
BLACK_LAPTOP3_90 = "검정 노트북3-901"
|
||||
WOODEN_TABLE_90 = "나무 탁자-901"
|
||||
WOODEN_TABLE_0 = "나무 탁자-01"
|
||||
LAPTOP1_0 = "노트북1-01"
|
||||
LAPTOP1_180 = "노트북1-1801"
|
||||
LAPTOP1_270 = "노트북1-2701"
|
||||
LAPTOP1_90 = "노트북1-901"
|
||||
LAPTOP2_0 = "노트북2-01"
|
||||
LAPTOP2_180 = "노트북2-1801"
|
||||
LAPTOP2_270 = "노트북2-2701"
|
||||
LAPTOP2_90 = "노트북2-901"
|
||||
LAPTOP3_0 = "노트북3-01"
|
||||
LAPTOP3_180 = "노트북3-1801"
|
||||
LAPTOP3_270 = "노트북3-2701"
|
||||
LAPTOP3_90 = "노트북3-901"
|
||||
GREEN_TABLE = "녹색 탁자1"
|
||||
MINI_FRIDGE_0 = "미니 냉장고-01"
|
||||
MINI_FRIDGE_180 = "미니 냉장고-1801"
|
||||
MINI_FRIDGE_90 = "미니 냉장고-901"
|
||||
BOX_0 = "박스-01"
|
||||
BOX_90 = "박스-901"
|
||||
PINK_TABLE = "분홍색 탁자1"
|
||||
SHELF_0 = "선반-01"
|
||||
SHELF_180 = "선반-1801"
|
||||
SHELF_270 = "선반-2701"
|
||||
SHELF_90 = "선반-901"
|
||||
TRASH_CAN_CLOSED = "쓰레기통 닫힘1"
|
||||
TRASH_CAN_OPEN = "쓰레기통 열림1"
|
||||
FISHBOWL_0 = "어항-01"
|
||||
FISHBOWL_180 = "어항-1801"
|
||||
FISHBOWL_270 = "어항-2701"
|
||||
FISHBOWL_90 = "어항-901"
|
||||
BEVERAGE_FRIDGE_0 = "음료 냉장고-01"
|
||||
BEVERAGE_FRIDGE_180 = "음료 냉장고-1801"
|
||||
BEVERAGE_FRIDGE_270 = "음료 냉장고-2701"
|
||||
BEVERAGE_FRIDGE_90 = "음료 냉장고-901"
|
||||
CHAIR_0 = "의자-01"
|
||||
CHAIR_180 = "의자-1801"
|
||||
CHAIR_270 = "의자-2701"
|
||||
CHAIR_90 = "의자-901"
|
||||
SMALL_SHELF_0 = "작은 선반-01"
|
||||
SMALL_SHELF_180 = "작은 선반-1801"
|
||||
SMALL_SHELF_270 = "작은 선반-2701"
|
||||
SMALL_SHELF_90 = "작은 선반-901"
|
||||
SMALL_PLANT = "작은 식물1"
|
||||
BOOKSHELF_0 = "책장-01"
|
||||
BOOKSHELF_180 = "책장-1801"
|
||||
BOOKSHELF_270 = "책장-2701"
|
||||
BOOKSHELF_90 = "책장-901"
|
||||
LARGE_PLANT = "큰 식물1"
|
||||
TV_0 = "티비-01"
|
||||
TV_180 = "티비-1801"
|
||||
TV_270 = "티비-2701"
|
||||
TV_90 = "티비-901"
|
||||
BLUE_TABLE = "파란색 탁자1"
|
||||
GRAY_TABLE = "회색 탁자1"
|
||||
WHITE_LAPTOP1_0 = "흰 노트북1-01"
|
||||
WHITE_LAPTOP1_180 = "흰 노트북1-1801"
|
||||
WHITE_LAPTOP1_270 = "흰 노트북1-2701"
|
||||
WHITE_LAPTOP1_90 = "흰 노트북1-901"
|
||||
WHITE_LAPTOP2_0 = "흰 노트북2-01"
|
||||
WHITE_LAPTOP2_180 = "흰 노트북2-1801"
|
||||
WHITE_LAPTOP2_270 = "흰 노트북2-2701"
|
||||
WHITE_LAPTOP2_90 = "흰 노트북2-901"
|
||||
WHITE_LAPTOP3_0 = "흰 노트북3-01"
|
||||
WHITE_LAPTOP3_180 = "흰 노트북3-1801"
|
||||
WHITE_LAPTOP3_270 = "흰 노트북3-2701"
|
||||
WHITE_LAPTOP3_90 = "흰 노트북3-901"
|
||||
WHITE_SHELF_0 = "흰색 선반-01"
|
||||
WHITE_SHELF_180 = "흰색 선반-1801"
|
||||
WHITE_SHELF_270 = "흰색 선반-2701"
|
||||
WHITE_SHELF_90 = "흰색 선반-901"
|
||||
WHITE_SMALL_SHELF_0 = "흰색 작은 선반-01"
|
||||
WHITE_SMALL_SHELF_180 = "흰색 작은 선반-1801"
|
||||
WHITE_SMALL_SHELF_270 = "흰색 작은 선반-2701"
|
||||
WHITE_SMALL_SHELF_90 = "흰색 작은 선반-901"
|
||||
WHITE_TABLE = "흰색 탁자1"
|
||||
EMPTY = "1"
|
||||
|
||||
|
||||
class RoomFurniturePlacement(BaseModel):
|
||||
id: int
|
||||
room_id: int
|
||||
furniture_name: Furniture
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
class FurniturePlacementRequest(BaseModel):
|
||||
furniture_name: Furniture
|
||||
x: int
|
||||
y: int
|
||||
|
||||
@field_validator("x", "y")
|
||||
@classmethod
|
||||
def validate_coordinates(cls, v):
|
||||
if v < 0 or v >= 10:
|
||||
raise ValueError("position must be between 0 and 10")
|
||||
return v
|
||||
|
||||
|
||||
class FurniturePlacementResponse(BaseModel):
|
||||
furniture_name: Furniture
|
||||
x: int
|
||||
y: int
|
||||
image_path: str
|
||||
|
||||
|
||||
class RoomFurnitureResponse(BaseModel):
|
||||
room: RoomResponse
|
||||
furniture: List[FurniturePlacementResponse]
|
||||
|
||||
|
||||
furniture_path = {
|
||||
"검정 노트북1-01": "public/funiture/검정 노트북1-0.png",
|
||||
"검정 노트북1-1801": "public/funiture/검정 노트북1-180.png",
|
||||
"검정 노트북1-2701": "public/funiture/검정 노트북1-270.png",
|
||||
"검정 노트북1-901": "public/funiture/검정 노트북1-90.png",
|
||||
"검정 노트북2-01": "public/funiture/검정 노트북2-0.png",
|
||||
"검정 노트북2-1801": "public/funiture/검정 노트북2-180.png",
|
||||
"검정 노트북2-2701": "public/funiture/검정 노트북2-270.png",
|
||||
"검정 노트북2-901": "public/funiture/검정 노트북2-90.png",
|
||||
"검정 노트북3-01": "public/funiture/검정 노트북3-0.png",
|
||||
"검정 노트북3-1801": "public/funiture/검정 노트북3-180.png",
|
||||
"검정 노트북3-2701": "public/funiture/검정 노트북3-270.png",
|
||||
"검정 노트북3-901": "public/funiture/검정 노트북3-90.png",
|
||||
"나무 탁자-901": "public/funiture/나무 탁자-90.png",
|
||||
"나무 탁자-01": "public/funiture/나무탁자-0.png",
|
||||
"노트북1-01": "public/funiture/노트북1-0.png",
|
||||
"노트북1-1801": "public/funiture/노트북1-180.png",
|
||||
"노트북1-2701": "public/funiture/노트북1-270.png",
|
||||
"노트북1-901": "public/funiture/노트북1-90.png",
|
||||
"노트북2-01": "public/funiture/노트북2-0.png",
|
||||
"노트북2-1801": "public/funiture/노트북2-180.png",
|
||||
"노트북2-2701": "public/funiture/노트북2-270.png",
|
||||
"노트북2-901": "public/funiture/노트북2-90.png",
|
||||
"노트북3-01": "public/funiture/노트북3-0.png",
|
||||
"노트북3-1801": "public/funiture/노트북3-180.png",
|
||||
"노트북3-2701": "public/funiture/노트북3-270.png",
|
||||
"노트북3-901": "public/funiture/노트북3-90.png",
|
||||
"녹색 침대-02": "public/funiture/녹색 침대-0.png",
|
||||
"녹색 침대-1802": "public/funiture/녹색 침대-180.png",
|
||||
"녹색 침대-2702": "public/funiture/녹색 침대-270.png",
|
||||
"녹색 침대-902": "public/funiture/녹색 침대-90.png",
|
||||
"녹색 탁자1": "public/funiture/녹색 탁자.png",
|
||||
"미니 냉장고-01": "public/funiture/미니 냉장고-0.png",
|
||||
"미니 냉장고-1801": "public/funiture/미니 냉장고-180.png",
|
||||
"미니 냉장고-901": "public/funiture/미니 냉장고-90.png",
|
||||
"박스-01": "public/funiture/박스-0.png",
|
||||
"박스-901": "public/funiture/박스-90.png",
|
||||
"분홍색 탁자1": "public/funiture/분홍색 탁자.png",
|
||||
"빨간 침대-02": "public/funiture/빨간 침대-0.png",
|
||||
"빨간 침대-1802": "public/funiture/빨간 침대-180.png",
|
||||
"빨간 침대-2702": "public/funiture/빨간 침대-270.png",
|
||||
"빨간 침대-902": "public/funiture/빨간 침대-90.png",
|
||||
"선반-01": "public/funiture/선반-0.png",
|
||||
"선반-1801": "public/funiture/선반-180.png",
|
||||
"선반-2701": "public/funiture/선반-270.png",
|
||||
"선반-901": "public/funiture/선반-90.png",
|
||||
"소파-02": "public/funiture/소파-0.png",
|
||||
"소파-1802": "public/funiture/소파-180.png",
|
||||
"소파-2702": "public/funiture/소파-270.png",
|
||||
"소파-902": "public/funiture/소파-90.png",
|
||||
"쓰레기통 닫힘1": "public/funiture/쓰레기통닫힘.png",
|
||||
"쓰레기통 열림1": "public/funiture/쓰레기통열림.png",
|
||||
"어항-01": "public/funiture/어항-0.png",
|
||||
"어항-1801": "public/funiture/어항-180.png",
|
||||
"어항-2701": "public/funiture/어항-270.png",
|
||||
"어항-901": "public/funiture/어항-90.png",
|
||||
"음료 냉장고-01": "public/funiture/음료 냉장고-0.png",
|
||||
"음료 냉장고-1801": "public/funiture/음료 냉장고-180.png",
|
||||
"음료 냉장고-2701": "public/funiture/음료 냉장고-270.png",
|
||||
"음료 냉장고-901": "public/funiture/음료 냉장고-90.png",
|
||||
"의자-01": "public/funiture/의자-0.png",
|
||||
"의자-1801": "public/funiture/의자-180.png",
|
||||
"의자-2701": "public/funiture/의자-270.png",
|
||||
"의자-901": "public/funiture/의자-90.png",
|
||||
"작은 선반-01": "public/funiture/작은 선반-0.png",
|
||||
"작은 선반-1801": "public/funiture/작은 선반-180.png",
|
||||
"작은 선반-2701": "public/funiture/작은 선반-270.png",
|
||||
"작은 선반-901": "public/funiture/작은 선반-90.png",
|
||||
"작은 식물1": "public/funiture/작은 식물.png",
|
||||
"주황 침대-02": "public/funiture/주황 침대-0.png",
|
||||
"주황 침대-1802": "public/funiture/주황 침대-180.png",
|
||||
"주황 침대-2702": "public/funiture/주황 침대-270.png",
|
||||
"주황 침대-902": "public/funiture/주황 침대-90.png",
|
||||
"책장-01": "public/funiture/책장-0.png",
|
||||
"책장-1801": "public/funiture/책장-180.png",
|
||||
"책장-2701": "public/funiture/책장-270.png",
|
||||
"책장-901": "public/funiture/책장-90.png",
|
||||
"큰 식물1": "public/funiture/큰 식물.png",
|
||||
"티비-01": "public/funiture/티비-0.png",
|
||||
"티비-1801": "public/funiture/티비-180.png",
|
||||
"티비-2701": "public/funiture/티비-270.png",
|
||||
"티비-901": "public/funiture/티비-90.png",
|
||||
"파란 침대-02": "public/funiture/파란 침대-0.png",
|
||||
"파란 침대-1802": "public/funiture/파란 침대-180.png",
|
||||
"파란 침대-2702": "public/funiture/파란 침대-270.png",
|
||||
"파란 침대-902": "public/funiture/파란 침대-90.png",
|
||||
"파란색 탁자1": "public/funiture/파란색 탁자.png",
|
||||
"회색 탁자1": "public/funiture/회색 탁자.png",
|
||||
"흰 노트북1-01": "public/funiture/흰 노트북1-0.png",
|
||||
"흰 노트북1-1801": "public/funiture/흰 노트북1-180.png",
|
||||
"흰 노트북1-2701": "public/funiture/흰 노트북1-270.png",
|
||||
"흰 노트북1-901": "public/funiture/흰 노트북1-90.png",
|
||||
"흰 노트북2-01": "public/funiture/흰 노트북2-0.png",
|
||||
"흰 노트북2-1801": "public/funiture/흰 노트북2-180.png",
|
||||
"흰 노트북2-2701": "public/funiture/흰 노트북2-270.png",
|
||||
"흰 노트북2-901": "public/funiture/흰 노트북2-90.png",
|
||||
"흰 노트북3-01": "public/funiture/흰 노트북3-0.png",
|
||||
"흰 노트북3-1801": "public/funiture/흰 노트북3-180.png",
|
||||
"흰 노트북3-2701": "public/funiture/흰 노트북3-270.png",
|
||||
"흰 노트북3-901": "public/funiture/흰 노트북3-90.png",
|
||||
"흰색 선반-01": "public/funiture/흰색 선반-0.png",
|
||||
"흰색 선반-1801": "public/funiture/흰색 선반-180.png",
|
||||
"흰색 선반-2701": "public/funiture/흰색 선반-270.png",
|
||||
"흰색 선반-901": "public/funiture/흰색 선반-90.png",
|
||||
"흰색 작은 선반-01": "public/funiture/흰색 작은 선반-0.png",
|
||||
"흰색 작은 선반-1801": "public/funiture/흰색 작은 선반-180.png",
|
||||
"흰색 작은 선반-2701": "public/funiture/흰색 작은 선반-270.png",
|
||||
"흰색 작은 선반-901": "public/funiture/흰색 작은 선반-90.png",
|
||||
"흰색 탁자1": "public/funiture/흰색 탁자.png",
|
||||
"1": "1",
|
||||
}
|
||||
|
||||
|
||||
class Room:
|
||||
def __init__(self, id: int, user_id: int, room_name: str, room_type: RoomTypes):
|
||||
self.id = id
|
||||
self.user_id = user_id
|
||||
self.room_name = room_name
|
||||
self.room_type = room_type
|
||||
|
||||
def to_response(self) -> RoomResponse:
|
||||
return RoomResponse(
|
||||
id=self.id,
|
||||
user_id=self.user_id,
|
||||
room_name=self.room_name,
|
||||
room_type=self.room_type,
|
||||
room_image_path=room_path[self.room_type],
|
||||
)
|
||||
93
Backend/schemas/user.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
0
Backend/services/__init__.py
Normal file
74
Backend/services/avatar_service.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
from typing import Optional
|
||||
from ..schemas.avatar import (
|
||||
AvatarUpdate,
|
||||
Avatar,
|
||||
AvatarOptions,
|
||||
AvatarType,
|
||||
TopClothesType,
|
||||
BottomClothesType,
|
||||
)
|
||||
from ..utils.db import execute, fetch_one
|
||||
from ..utils.queries.avatar import AvatarQueries
|
||||
|
||||
|
||||
class AvatarService:
|
||||
|
||||
@staticmethod
|
||||
async def init_db():
|
||||
await execute(AvatarQueries.CREATE_TABLE)
|
||||
|
||||
async def get_or_create_avatar(self, user_id: int) -> Avatar:
|
||||
avatar = await self.get_user_avatar(user_id)
|
||||
if not avatar:
|
||||
avatar = await self.create_default_avatar(user_id)
|
||||
return avatar
|
||||
|
||||
async def get_user_avatar(self, user_id: int) -> Optional[Avatar]:
|
||||
row = await fetch_one(AvatarQueries.SELECT_USER_AVATAR, (user_id,))
|
||||
if not row:
|
||||
return None
|
||||
return Avatar(**row)
|
||||
|
||||
async def create_default_avatar(self, user_id: int) -> Avatar:
|
||||
await execute(
|
||||
AvatarQueries.INSERT_AVATAR,
|
||||
(
|
||||
user_id,
|
||||
AvatarType.MALE.value,
|
||||
TopClothesType.ANA_CLOTHES.value,
|
||||
BottomClothesType.JEANS.value,
|
||||
),
|
||||
)
|
||||
return await self.get_user_avatar(user_id)
|
||||
|
||||
async def update_avatar(self, user_id: int, avatar_data: AvatarUpdate) -> Avatar:
|
||||
|
||||
update_fields = []
|
||||
params = []
|
||||
|
||||
if avatar_data.avatar_type is not None:
|
||||
update_fields.append("avatar_type = ?")
|
||||
params.append(avatar_data.avatar_type.value)
|
||||
if avatar_data.top_clothe_type is not None:
|
||||
update_fields.append("top_clothe_type = ?")
|
||||
params.append(avatar_data.top_clothe_type.value)
|
||||
if avatar_data.bottom_clothe_type is not None:
|
||||
update_fields.append("bottom_clothe_type = ?")
|
||||
params.append(avatar_data.bottom_clothe_type.value)
|
||||
|
||||
if update_fields:
|
||||
query = AvatarQueries.UPDATE_AVATAR.format(fields=", ".join(update_fields))
|
||||
params.append(user_id)
|
||||
await execute(query, tuple(params))
|
||||
|
||||
return await self.get_user_avatar(user_id)
|
||||
|
||||
async def get_avatar_options(self) -> AvatarOptions:
|
||||
return AvatarOptions(
|
||||
avatar_types=list(AvatarType),
|
||||
top_clothe_types=list(TopClothesType),
|
||||
bottom_clothe_types=list(BottomClothesType),
|
||||
)
|
||||
|
||||
async def get_avatar_by_userId(self, user_id: int) -> Optional[Avatar]:
|
||||
return await self.get_user_avatar(user_id)
|
||||
144
Backend/services/diary_service.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from fastapi import UploadFile
|
||||
from ..schemas.diary import DiaryCreate, DiaryUpdate, Diary
|
||||
from ..utils.db import execute, fetch_one, fetch_all
|
||||
from ..utils.image_processor import ImageProcessor
|
||||
from ..utils.queries.diary import DiaryQueries
|
||||
|
||||
|
||||
class DiaryService:
|
||||
def __init__(self):
|
||||
self.image_processor = ImageProcessor()
|
||||
self.upload_dir = "uploads/diary"
|
||||
os.makedirs(self.upload_dir, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
async def init_db():
|
||||
await execute(DiaryQueries.CREATE_TABLE)
|
||||
|
||||
async def create_diary(
|
||||
self, user_id: int, diary_data: DiaryCreate, files: List[UploadFile]
|
||||
) -> Diary:
|
||||
# image_path 는 ,로 구분 되어 있음
|
||||
image_path = ""
|
||||
if files is not None:
|
||||
for file in files:
|
||||
image_path += (
|
||||
","
|
||||
+ await self.image_processor.write_file_and_get_image_path(
|
||||
file, self.upload_dir
|
||||
)
|
||||
)
|
||||
image_path = image_path[1:]
|
||||
|
||||
query = DiaryQueries.INSERT_DIARY
|
||||
|
||||
created_at = datetime.now()
|
||||
await execute(
|
||||
query,
|
||||
(
|
||||
user_id,
|
||||
diary_data.title,
|
||||
diary_data.content,
|
||||
image_path,
|
||||
diary_data.category,
|
||||
created_at,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
)
|
||||
|
||||
row = await fetch_one(
|
||||
DiaryQueries.SELECT_LATEST_USER_DIARY,
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
return Diary(**row)
|
||||
|
||||
async def get_user_diaries(
|
||||
self, user_id: int, skip: int = 0, limit: int = 20, category: str = None
|
||||
) -> List[Diary]:
|
||||
if category:
|
||||
query = DiaryQueries.SELECT_USER_DIARIES_BY_CATEGORY
|
||||
rows = await fetch_all(query, (user_id, category, limit, skip))
|
||||
else:
|
||||
query = DiaryQueries.SELECT_USER_DIARIES
|
||||
rows = await fetch_all(query, (user_id, limit, skip))
|
||||
|
||||
return [Diary(**row) for row in rows]
|
||||
|
||||
async def get_diary_by_id(self, diary_id: int) -> Optional[Diary]:
|
||||
query = DiaryQueries.SELECT_BY_ID
|
||||
row = await fetch_one(query, (diary_id,))
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return Diary(**row)
|
||||
|
||||
async def get_diary_with_user_id(
|
||||
self, diary_id: int, user_id: int
|
||||
) -> Optional[Diary]:
|
||||
query = DiaryQueries.SELECT_BY_ID_WITH_USER_ID
|
||||
row = await fetch_one(query, (diary_id, user_id))
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return Diary(**row)
|
||||
|
||||
async def update_diary(
|
||||
self,
|
||||
diary_id: int,
|
||||
user_id: int,
|
||||
diary_data: DiaryUpdate,
|
||||
files: List[UploadFile],
|
||||
) -> Optional[Diary]:
|
||||
diary = await self.get_diary_with_user_id(diary_id, user_id)
|
||||
if not diary:
|
||||
return None
|
||||
|
||||
update_fields = []
|
||||
params = []
|
||||
|
||||
if diary_data.title is not None:
|
||||
update_fields.append("title = ?")
|
||||
params.append(diary_data.title)
|
||||
|
||||
if diary_data.content is not None:
|
||||
update_fields.append("content = ?")
|
||||
params.append(diary_data.content)
|
||||
|
||||
if diary_data.category is not None:
|
||||
update_fields.append("category = ?")
|
||||
params.append(diary_data.category)
|
||||
|
||||
if files is not None:
|
||||
update_fields.append("images = ?")
|
||||
image_paths = ""
|
||||
for file in files:
|
||||
image_paths += (
|
||||
","
|
||||
+ await self.image_processor.write_file_and_get_image_path(
|
||||
file, self.upload_dir
|
||||
)
|
||||
)
|
||||
image_paths = image_paths[1:]
|
||||
params.append(image_paths)
|
||||
|
||||
if update_fields:
|
||||
query = DiaryQueries.UPDATE_DIARY.format(fields=", ".join(update_fields))
|
||||
params.extend([diary_id, user_id])
|
||||
await execute(query, tuple(params))
|
||||
|
||||
return await self.get_diary_with_user_id(diary_id, user_id)
|
||||
|
||||
async def delete_diary(self, diary_id: int, user_id: int) -> bool:
|
||||
try:
|
||||
query = DiaryQueries.DELETE_DIARY
|
||||
await execute(query, (diary_id, user_id))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
142
Backend/services/friendship_service.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from ..schemas.friendship import Friendship, FriendshipResponse, FriendshipStatus
|
||||
from ..utils.db import execute, fetch_one, fetch_all
|
||||
from ..utils.queries.friendship import FriendshipQueries
|
||||
|
||||
|
||||
class FriendshipService:
|
||||
|
||||
@staticmethod
|
||||
async def init_db():
|
||||
await execute(FriendshipQueries.CREATE_TABLE)
|
||||
|
||||
async def send_friendship_request(
|
||||
self, user_id: int, friend_username: str
|
||||
) -> FriendshipResponse:
|
||||
friend_query = FriendshipQueries.SELECT_USER_BY_USERNAME
|
||||
friend_row = await fetch_one(friend_query, (friend_username,))
|
||||
|
||||
if not friend_row:
|
||||
raise ValueError("User not found")
|
||||
|
||||
friend_id = friend_row["id"]
|
||||
|
||||
if user_id == friend_id:
|
||||
raise ValueError("Cannot send friendship request to yourself")
|
||||
|
||||
existing_query = FriendshipQueries.SELECT_EXISTING_FRIENDSHIP
|
||||
existing_row = await fetch_one(
|
||||
existing_query, (user_id, friend_id, friend_id, user_id)
|
||||
)
|
||||
|
||||
if existing_row:
|
||||
raise ValueError("Friendship request already exists")
|
||||
|
||||
created_at = datetime.now()
|
||||
|
||||
query = FriendshipQueries.INSERT_FRIENDSHIP
|
||||
|
||||
await execute(
|
||||
query, (user_id, friend_id, FriendshipStatus.PENDING.value, created_at)
|
||||
)
|
||||
|
||||
friendship_row = await fetch_one(
|
||||
FriendshipQueries.SELECT_FRIENDSHIP_BY_IDS,
|
||||
(user_id, friend_id),
|
||||
)
|
||||
|
||||
if not friendship_row:
|
||||
raise ValueError("Friendship not found after creation")
|
||||
|
||||
friendship = Friendship(
|
||||
id=friendship_row["id"],
|
||||
user_id=friendship_row["user_id"],
|
||||
friend_id=friendship_row["friend_id"],
|
||||
status=friendship_row["status"],
|
||||
created_at=friendship_row["created_at"],
|
||||
)
|
||||
|
||||
return friendship.to_response(friend_username)
|
||||
|
||||
async def accept_friendship_request(
|
||||
self, friendship_id: int, user_id: int
|
||||
) -> Optional[FriendshipResponse]:
|
||||
friendship_query = FriendshipQueries.SELECT_FRIENDSHIP_FOR_ACCEPT
|
||||
|
||||
friendship_row = await fetch_one(
|
||||
friendship_query,
|
||||
(friendship_id, user_id, FriendshipStatus.PENDING.value),
|
||||
)
|
||||
|
||||
if not friendship_row:
|
||||
return None
|
||||
|
||||
update_query = FriendshipQueries.UPDATE_FRIENDSHIP_STATUS
|
||||
await execute(update_query, (FriendshipStatus.ACCEPTED.value, friendship_id))
|
||||
|
||||
friendship = Friendship(
|
||||
id=friendship_row["id"],
|
||||
user_id=friendship_row["user_id"],
|
||||
friend_id=friendship_row["friend_id"],
|
||||
status=FriendshipStatus.ACCEPTED.value,
|
||||
created_at=friendship_row["created_at"],
|
||||
)
|
||||
|
||||
return friendship.to_response(friendship_row["username"])
|
||||
|
||||
async def get_user_friendships(
|
||||
self, user_id: int, status: Optional[str] = None
|
||||
) -> List[FriendshipResponse]:
|
||||
if status:
|
||||
query = FriendshipQueries.SELECT_USER_FRIENDSHIPS_BY_STATUS
|
||||
rows = await fetch_all(query, (user_id, user_id, user_id, status))
|
||||
else:
|
||||
query = FriendshipQueries.SELECT_USER_FRIENDSHIPS
|
||||
rows = await fetch_all(
|
||||
query, (user_id, user_id, user_id, FriendshipStatus.ACCEPTED.value)
|
||||
)
|
||||
|
||||
friendships = []
|
||||
for row in rows:
|
||||
friendship = Friendship(
|
||||
id=row["id"],
|
||||
user_id=row["user_id"],
|
||||
friend_id=row["friend_id"],
|
||||
status=row["status"],
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
friendships.append(friendship.to_response(row["username"]))
|
||||
|
||||
return friendships
|
||||
|
||||
async def delete_friendship(self, friendship_id: int, user_id: int) -> bool:
|
||||
query = FriendshipQueries.DELETE_FRIENDSHIP
|
||||
await execute(query, (friendship_id, user_id, user_id))
|
||||
return True
|
||||
|
||||
async def get_pending_requests(self, user_id: int) -> List[FriendshipResponse]:
|
||||
query = FriendshipQueries.SELECT_PENDING_REQUESTS
|
||||
|
||||
rows = await fetch_all(query, (user_id, FriendshipStatus.PENDING.value))
|
||||
|
||||
friendships = []
|
||||
for row in rows:
|
||||
friendship = Friendship(
|
||||
id=row["id"],
|
||||
user_id=row["user_id"],
|
||||
friend_id=row["friend_id"],
|
||||
status=row["status"],
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
friendships.append(friendship.to_response(row["username"]))
|
||||
|
||||
return friendships
|
||||
|
||||
async def check_friendship(self, user_id1: int, user_id2: int) -> bool:
|
||||
friendship_query = FriendshipQueries.CHECK_FRIENDSHIP_STATUS
|
||||
friendship_row = await fetch_one(
|
||||
friendship_query, (user_id1, user_id2, user_id2, user_id1)
|
||||
)
|
||||
|
||||
return friendship_row is not None
|
||||
110
Backend/services/guestbook_service.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
from datetime import datetime
|
||||
from typing import List
|
||||
from fastapi import HTTPException
|
||||
from Backend.utils.db import execute, fetch_one, fetch_all
|
||||
from Backend.utils.queries.guestbook import GuestBookQueries
|
||||
from Backend.schemas.guestbook import GuestBook, GuestBookCreate, GuestbookResponse
|
||||
from Backend.schemas.user import User
|
||||
from Backend.utils.queries.user import UserQueries
|
||||
|
||||
|
||||
class GuestbookService:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def init_db():
|
||||
await execute(GuestBookQueries.CREATE_TABLE)
|
||||
|
||||
async def create_guestbook(
|
||||
self, data: GuestBookCreate, user: User
|
||||
) -> GuestbookResponse:
|
||||
user_exist = await fetch_one(UserQueries.SELECT_BY_ID, (data.target_user_id,))
|
||||
if user_exist is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
ex_row = await fetch_one(
|
||||
GuestBookQueries.SELECT_GUEST_BOOK_BY_USER_ID, (user.id,)
|
||||
)
|
||||
created_at = datetime.now()
|
||||
query = GuestBookQueries.INSERT_GUEST_BOOK
|
||||
await execute(query, (data.target_user_id, user.id, data.content, created_at))
|
||||
|
||||
query = GuestBookQueries.SELECT_GUEST_BOOK_BY_USER_ID
|
||||
row = await fetch_one(query, (user.id,))
|
||||
|
||||
if not (ex_row is None):
|
||||
if row is None or ex_row["id"] == row["id"]:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Failed to create guest book"
|
||||
)
|
||||
|
||||
return GuestbookResponse(
|
||||
id=row["id"],
|
||||
content=row["content"],
|
||||
target_user_id=row["target_user_id"],
|
||||
user_id=row["user_id"],
|
||||
user_profile_path=user.profile_image_path,
|
||||
username=user.username,
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
|
||||
async def get_target_user_guestbooks(
|
||||
self, target_user_id: int, limit: int = 20, offset: int = 0
|
||||
) -> List[GuestbookResponse]:
|
||||
query = GuestBookQueries.SELECT_TARGET_USER_GUEST_BOOKS
|
||||
|
||||
rows = await fetch_all(query, (target_user_id, limit, offset))
|
||||
|
||||
response_list = []
|
||||
for row in rows:
|
||||
user = await fetch_one(UserQueries.SELECT_BY_ID, (row["user_id"],))
|
||||
|
||||
response_list.append(
|
||||
GuestbookResponse(
|
||||
id=row["id"],
|
||||
content=row["content"],
|
||||
target_user_id=row["target_user_id"],
|
||||
user_id=row["user_id"],
|
||||
user_profile_path=user["profile_image_path"],
|
||||
username=user["username"],
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
)
|
||||
|
||||
return response_list
|
||||
|
||||
async def update_guestbook_by_id(self, id: int, content: str) -> GuestbookResponse:
|
||||
query = GuestBookQueries.SELECT_GUEST_BOOK_BY_ID
|
||||
|
||||
row = await fetch_one(query, (id,))
|
||||
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Guest book not found")
|
||||
|
||||
query = GuestBookQueries.UPDATE_GUEST_BOOK_BY_ID
|
||||
await execute(query, (content, id))
|
||||
|
||||
query = GuestBookQueries.SELECT_GUEST_BOOK_BY_ID
|
||||
|
||||
row = await fetch_one(query, (id,))
|
||||
|
||||
user = await fetch_one(UserQueries.SELECT_BY_ID, (row["user_id"],))
|
||||
|
||||
return GuestbookResponse(
|
||||
id=row["id"],
|
||||
content=row["content"],
|
||||
target_user_id=row["target_user_id"],
|
||||
user_id=row["user_id"],
|
||||
user_profile_path=user["profile_image_path"],
|
||||
username=user["username"],
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
|
||||
async def delete_guestbook_by_id(self, id: int, user_id: int) -> bool:
|
||||
try:
|
||||
query = GuestBookQueries.DELETE_GUEST_BOOK
|
||||
await execute(query, (id, user_id))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
81
Backend/services/letter_service.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# from typing import List, Optional
|
||||
# from ..schemas.letter import LetterCreate, Letter, EmailRequest
|
||||
# from ..utils.db import execute, fetch_one, fetch_all
|
||||
# from ..utils.default_queries import LetterQueries
|
||||
# from ..utils.email_processor import EmailProcessor
|
||||
#
|
||||
#
|
||||
# class LetterService:
|
||||
# def __init__(self):
|
||||
# self.email_processor = EmailProcessor()
|
||||
#
|
||||
# @staticmethod
|
||||
# async def init_db():
|
||||
# await execute(LetterQueries.CREATE_TABLE)
|
||||
#
|
||||
# async def create_letter(self, sender_id: int, letter_data: LetterCreate) -> Letter:
|
||||
# query = LetterQueries.INSERT_LETTER
|
||||
#
|
||||
# await execute(
|
||||
# query,
|
||||
# (sender_id, letter_data.content),
|
||||
# )
|
||||
#
|
||||
# row = await fetch_one(
|
||||
# LetterQueries.SELECT_LATEST_USER_LETTER,
|
||||
# (sender_id,),
|
||||
# )
|
||||
#
|
||||
# return Letter(**row)
|
||||
#
|
||||
# async def get_user_letters(
|
||||
# self, sender_id: int, skip: int = 0, limit: int = 20
|
||||
# ) -> List[Letter]:
|
||||
# query = LetterQueries.SELECT_USER_LETTERS
|
||||
# rows = await fetch_all(query, (sender_id, limit, skip))
|
||||
# return [Letter(**row) for row in rows]
|
||||
#
|
||||
# async def get_letter_by_id(
|
||||
# self, letter_id: int, sender_id: int
|
||||
# ) -> Optional[Letter]:
|
||||
# query = LetterQueries.SELECT_LETTER_BY_ID
|
||||
# row = await fetch_one(query, (letter_id, sender_id))
|
||||
# if not row:
|
||||
# return None
|
||||
# return Letter(**row)
|
||||
#
|
||||
# async def delete_letter(self, letter_id: int, sender_id: int) -> bool:
|
||||
# try:
|
||||
# query = LetterQueries.DELETE_LETTER
|
||||
# await execute(
|
||||
# query,
|
||||
# (letter_id, sender_id),
|
||||
# )
|
||||
# return True
|
||||
# except Exception:
|
||||
# return False
|
||||
#
|
||||
# async def update_letter(
|
||||
# self, letter_id: int, sender_id: int, content: str
|
||||
# ) -> Optional[Letter]:
|
||||
# query = LetterQueries.UPDATE_LETTER
|
||||
# await execute(
|
||||
# query,
|
||||
# (content, letter_id, sender_id),
|
||||
# )
|
||||
#
|
||||
# row = await fetch_one(
|
||||
# LetterQueries.SELECT_LETTER_BY_ID,
|
||||
# (letter_id, sender_id),
|
||||
# )
|
||||
# if row is None:
|
||||
# return None
|
||||
#
|
||||
# return Letter(**row)
|
||||
#
|
||||
# async def send_letter(self, letter: Letter, data: EmailRequest):
|
||||
# subject = f"2025_SSF_LETTER_{data.sender_name}"
|
||||
# content = letter.content
|
||||
# await self.email_processor.send_email(
|
||||
# subject, content, data.sender_email, data.sender_password
|
||||
# )
|
||||
150
Backend/services/photo_service.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# import os
|
||||
# from datetime import datetime
|
||||
# from typing import List
|
||||
# from fastapi import UploadFile
|
||||
# from ..schemas.photo import (
|
||||
# PhotoUpload,
|
||||
# Photo,
|
||||
# PhotoComment,
|
||||
# CommentCreate,
|
||||
# CommentResponse,
|
||||
# )
|
||||
# from ..utils.db import execute, fetch_one, fetch_all
|
||||
# from ..utils.default_queries import PhotoQueries
|
||||
# from ..utils.image_processor import ImageProcessor
|
||||
#
|
||||
#
|
||||
# class PhotoService:
|
||||
# def __init__(self):
|
||||
# self.image_processor = ImageProcessor()
|
||||
# self.upload_dir = "uploads/photos"
|
||||
# os.makedirs(self.upload_dir, exist_ok=True)
|
||||
#
|
||||
# @staticmethod
|
||||
# async def init_db():
|
||||
# await execute(PhotoQueries.CREATE_TABLE)
|
||||
# await execute(PhotoQueries.CREATE_COMMENTS_TABLE)
|
||||
#
|
||||
# async def upload_photo(
|
||||
# self, user_id: int, photo_data: PhotoUpload, file: UploadFile
|
||||
# ) -> Photo:
|
||||
# if not file.content_type.startswith("image/"):
|
||||
# raise ValueError("File must be an image")
|
||||
#
|
||||
# self.image_processor.validate_image_file(file.filename, file.size)
|
||||
#
|
||||
# created_at = datetime.now()
|
||||
#
|
||||
# image_path = await self.image_processor.write_file_and_get_image_path(
|
||||
# file, self.upload_dir
|
||||
# )
|
||||
#
|
||||
# query = PhotoQueries.INSERT_PHOTO
|
||||
#
|
||||
# await execute(
|
||||
# query,
|
||||
# (user_id, photo_data.album_name, image_path, photo_data.title, created_at),
|
||||
# )
|
||||
#
|
||||
# row = await fetch_one(
|
||||
# PhotoQueries.SELECT_LATEST_USER_PHOTO,
|
||||
# (user_id,),
|
||||
# )
|
||||
#
|
||||
# return Photo(**row)
|
||||
#
|
||||
# async def get_user_photos(
|
||||
# self, user_id: int, skip: int = 0, limit: int = 20, album_name: str = None
|
||||
# ) -> List[Photo]:
|
||||
# if album_name:
|
||||
# query = PhotoQueries.SELECT_USER_PHOTOS_BY_ALBUM
|
||||
# rows = await fetch_all(query, (user_id, album_name, limit, skip))
|
||||
# else:
|
||||
# query = PhotoQueries.SELECT_USER_PHOTOS
|
||||
# rows = await fetch_all(query, (user_id, limit, skip))
|
||||
#
|
||||
# return [Photo(**row) for row in rows]
|
||||
#
|
||||
# async def check_friendship(self, user_id: int, photo_id: int) -> bool:
|
||||
# photo_query = PhotoQueries.SELECT_PHOTO_OWNER
|
||||
# photo_row = await fetch_one(photo_query, (photo_id,))
|
||||
#
|
||||
# if not photo_row:
|
||||
# return False
|
||||
#
|
||||
# photo_owner_id = photo_row["user_id"]
|
||||
#
|
||||
# if user_id == photo_owner_id:
|
||||
# return True
|
||||
#
|
||||
# from ..services.friendship_service import FriendshipService
|
||||
#
|
||||
# friendship_service = FriendshipService()
|
||||
# return await friendship_service.check_friendship(user_id, photo_owner_id)
|
||||
#
|
||||
# async def add_comment(
|
||||
# self, photo_id: int, user_id: int, comment_data: CommentCreate
|
||||
# ) -> PhotoComment:
|
||||
# if not await self.check_friendship(user_id, photo_id):
|
||||
# raise ValueError("Cannot add comment before being friends")
|
||||
# created_at = datetime.now()
|
||||
#
|
||||
# query = PhotoQueries.INSERT_COMMENT
|
||||
#
|
||||
# await execute(query, (photo_id, user_id, comment_data.content, created_at))
|
||||
#
|
||||
# row = await fetch_one(
|
||||
# PhotoQueries.SELECT_LATEST_COMMENT,
|
||||
# (photo_id, user_id),
|
||||
# )
|
||||
#
|
||||
# return PhotoComment(**row)
|
||||
#
|
||||
# async def get_photo_comments(self, photo_id: int) -> List[CommentResponse]:
|
||||
# query = PhotoQueries.SELECT_PHOTO_COMMENTS
|
||||
#
|
||||
# rows = await fetch_all(query, (photo_id,))
|
||||
#
|
||||
# return [CommentResponse(**row) for row in rows]
|
||||
#
|
||||
# async def apply_filter(
|
||||
# self,
|
||||
# photo_id: int,
|
||||
# filter_type: str,
|
||||
# user_id: int,
|
||||
# cover: bool = False,
|
||||
# title: str = None,
|
||||
# ) -> str:
|
||||
# photo_query = PhotoQueries.SELECT_PHOTO_BY_ID
|
||||
# row = await fetch_one(photo_query, (photo_id, user_id))
|
||||
#
|
||||
# if not row:
|
||||
# raise ValueError("Photo not found")
|
||||
#
|
||||
# original_path = row["image_path"]
|
||||
# filtered_path = await self.image_processor.apply_filter(
|
||||
# original_path, filter_type
|
||||
# )
|
||||
#
|
||||
# if cover:
|
||||
# photo_update_query = PhotoQueries.UPDATE_PHOTO_PATH
|
||||
# await execute(photo_update_query, (filtered_path, photo_id, user_id))
|
||||
# else:
|
||||
# row = await fetch_one(
|
||||
# PhotoQueries.SELECT_PHOTO_ALBUM_NAME, (photo_id, user_id)
|
||||
# )
|
||||
# photo_create_query = PhotoQueries.INSERT_PHOTO
|
||||
# await execute(
|
||||
# photo_create_query,
|
||||
# (user_id, row["album_name"], filtered_path, title, datetime.now()),
|
||||
# )
|
||||
#
|
||||
# return filtered_path
|
||||
#
|
||||
# async def delete_photo(self, photo_id: int, user_id: int) -> bool:
|
||||
# try:
|
||||
# query = PhotoQueries.DELETE_PHOTO
|
||||
# await execute(query, (photo_id, user_id))
|
||||
# return True
|
||||
# except Exception:
|
||||
# return False
|
||||
149
Backend/services/room_service.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
from fastapi import HTTPException
|
||||
from typing import List
|
||||
from Backend.utils.queries.room import RoomQueries
|
||||
from Backend.schemas.room import (
|
||||
FurnitureItem,
|
||||
RoomFurniturePlacement,
|
||||
FurniturePlacementRequest,
|
||||
Room,
|
||||
RoomTypes,
|
||||
RoomTypeResponse,
|
||||
room_path,
|
||||
Furniture,
|
||||
furniture_path,
|
||||
RoomFurnitureResponse,
|
||||
FurniturePlacementResponse,
|
||||
RoomResponse,
|
||||
)
|
||||
from Backend.utils.db import fetch_all, fetch_one, execute
|
||||
|
||||
|
||||
class RoomService:
|
||||
|
||||
@staticmethod
|
||||
async def init_db():
|
||||
await execute(RoomQueries.CREATE_TABLE)
|
||||
await execute(RoomQueries.CREATE_TABLE_ROOM_FURNITURE)
|
||||
await execute(RoomQueries.CREATE_TABLE_USER_FURNITURE)
|
||||
|
||||
async def get_or_create_room(self, user_id: int) -> int:
|
||||
row = await fetch_one(RoomQueries.SELECT_ROOM_ID_BY_USER_ID, (user_id,))
|
||||
if row:
|
||||
return row["id"]
|
||||
|
||||
await execute(RoomQueries.INSERT_ROOM, (user_id, "My Room", "room_1"))
|
||||
new_row = await fetch_one(RoomQueries.SELECT_ROOM_ID_BY_USER_ID, (user_id,))
|
||||
|
||||
return new_row["id"]
|
||||
|
||||
async def get_room_by_id(self, id: int) -> Room:
|
||||
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_ID, (id,))
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
|
||||
return Room(**row)
|
||||
|
||||
async def get_room_by_userId(self, id: int) -> Room:
|
||||
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_USER_ID, (id,))
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
|
||||
return Room(**row)
|
||||
|
||||
async def get_room_types(self) -> List[RoomTypeResponse]:
|
||||
return [
|
||||
RoomTypeResponse(type=rt.value, image_path=room_path[rt.value])
|
||||
for rt in RoomTypes
|
||||
]
|
||||
|
||||
async def update_room_name(self, room_id: int, new_name: str):
|
||||
await execute(RoomQueries.UPDATE_ROOM_NAME, (new_name, room_id))
|
||||
|
||||
async def update_room_type(self, room_id: int, new_type: RoomTypes):
|
||||
query = RoomQueries.UPDATE_ROOM_TYPE
|
||||
await execute(query, (new_type.value, room_id))
|
||||
|
||||
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_ID, (room_id,))
|
||||
return Room(**row)
|
||||
|
||||
# furniture
|
||||
async def get_furniture_catalog(self) -> List[FurnitureItem]:
|
||||
furniture_list = []
|
||||
for f in list(Furniture):
|
||||
furniture_list.append(
|
||||
FurnitureItem(
|
||||
name=f,
|
||||
image_path=furniture_path[f],
|
||||
width=int(f[-1]),
|
||||
)
|
||||
)
|
||||
|
||||
return furniture_list
|
||||
|
||||
async def get_room_furnitures(self, room_id: int) -> RoomFurnitureResponse:
|
||||
rows = await fetch_all(RoomQueries.SELECT_ROOM_FURNITURE, (room_id,))
|
||||
furniture_placement_response = []
|
||||
for row in rows:
|
||||
furniture_placement_response.append(
|
||||
FurniturePlacementResponse(
|
||||
furniture_name=row["furniture_name"],
|
||||
x=row["x"],
|
||||
y=row["y"],
|
||||
image_path=furniture_path[row["furniture_name"]],
|
||||
)
|
||||
)
|
||||
row = await fetch_one(RoomQueries.SELECT_ROOM_BY_ID, (room_id,))
|
||||
return RoomFurnitureResponse(
|
||||
furniture=furniture_placement_response,
|
||||
room=RoomResponse(
|
||||
id=row["id"],
|
||||
user_id=row["user_id"],
|
||||
room_name=row["room_name"],
|
||||
room_type=row["room_type"],
|
||||
room_image_path=room_path[row["room_type"]],
|
||||
),
|
||||
)
|
||||
|
||||
async def place_furniture(self, room_id: int, request: FurniturePlacementRequest):
|
||||
is_oneone = furniture_path.get(request.furniture_name + "1") is not None
|
||||
|
||||
placed_furnitures = await fetch_all(
|
||||
RoomQueries.SELECT_ROOM_FURNITURE, (room_id,)
|
||||
)
|
||||
|
||||
for f in placed_furnitures:
|
||||
if f["x"] == request.x and f["y"] == request.y:
|
||||
raise HTTPException(status_code=409, detail="Furniture already placed")
|
||||
|
||||
await execute(
|
||||
RoomQueries.INSERT_ROOM_FURNITURE,
|
||||
(room_id, request.furniture_name, request.x, request.y),
|
||||
)
|
||||
if not is_oneone:
|
||||
await execute(
|
||||
RoomQueries.INSERT_ROOM_FURNITURE,
|
||||
(room_id, "1", request.x - 1, request.y),
|
||||
)
|
||||
|
||||
async def remove_furniture(self, room_id: int, x: int, y: int, furniture_name: str):
|
||||
is_oneone = furniture_path.get(furniture_name + "1") is not None
|
||||
await execute(RoomQueries.DELETE_FURNITURE, (room_id, x, y))
|
||||
if not is_oneone:
|
||||
await execute(RoomQueries.DELETE_FURNITURE, (room_id, x - 1, y))
|
||||
|
||||
async def add_furniture(self, user_id: int, furniture_name: str):
|
||||
await execute(RoomQueries.INSERT_USER_FURNITURE, (user_id, furniture_name))
|
||||
|
||||
async def get_user_furniture(self, user_id: int):
|
||||
rows = await fetch_all(RoomQueries.SELECT_USER_FURNITURE, (user_id,))
|
||||
furniture_list = []
|
||||
for row in rows:
|
||||
furniture_list.append(
|
||||
FurnitureItem(
|
||||
name=row["furniture_name"],
|
||||
image_path=furniture_path[row["furniture_name"]],
|
||||
width=int(row["furniture_name"][-1]),
|
||||
)
|
||||
)
|
||||
|
||||
return furniture_list
|
||||
33
Backend/services/store_service.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import httpx
|
||||
|
||||
|
||||
class StoreService:
|
||||
def __init__(self):
|
||||
self.SERVER_URL = "https://dotory.ana.st"
|
||||
|
||||
async def get_dotory_by_id(self, user_id: int):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{self.SERVER_URL}/")
|
||||
response_json = response.json()
|
||||
return
|
||||
|
||||
async def register_user(self, user_id: int):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.SERVER_URL}/", json={"user_id": user_id}
|
||||
)
|
||||
return
|
||||
|
||||
async def buy_product(self, product_id: int, user_id: int):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.SERVER_URL}/", json={"user_id": user_id}
|
||||
)
|
||||
return
|
||||
|
||||
async def update_user_dotory(self, user_id: int, dotoryNum: int):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(
|
||||
f"{self.SERVER_URL}/", json={"num": dotoryNum}
|
||||
)
|
||||
return
|
||||
178
Backend/services/user_service.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, List
|
||||
import os
|
||||
from fastapi import UploadFile
|
||||
from Backend.utils.db import execute, fetch_one, fetch_all
|
||||
from Backend.utils.image_processor import ImageProcessor
|
||||
from Backend.utils.queries.user import UserQueries
|
||||
from Backend.schemas.user import User, UserCreate, UserResponse, UserUpdate
|
||||
from Backend.services.store_service import StoreService
|
||||
|
||||
store_service = StoreService()
|
||||
|
||||
class UserService:
|
||||
def __init__(self):
|
||||
self.image_processor = ImageProcessor()
|
||||
self.upload_dir = "uploads/profile"
|
||||
os.makedirs(self.upload_dir, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
async def init_db():
|
||||
await execute(UserQueries.CREATE_TABLE)
|
||||
|
||||
async def create_user(
|
||||
self, user_data: UserCreate, profile_file: UploadFile = None
|
||||
) -> User:
|
||||
password_hash, salt = User.hash_password(user_data.password)
|
||||
|
||||
if profile_file is not None:
|
||||
await self.image_processor.validate_image_file(
|
||||
|
||||
)
|
||||
image_path = await self.image_processor.write_file_and_get_image_path(
|
||||
profile_file, upload_dir=self.upload_dir
|
||||
)
|
||||
query = UserQueries.INSERT_USER_WITH_PROFILE
|
||||
params = (
|
||||
user_data.username,
|
||||
user_data.email,
|
||||
password_hash,
|
||||
salt,
|
||||
image_path,
|
||||
)
|
||||
else:
|
||||
query = UserQueries.INSERT_USER_WITHOUT_PROFILE
|
||||
params = (user_data.username, user_data.email, password_hash, salt)
|
||||
|
||||
await execute(
|
||||
query,
|
||||
params,
|
||||
)
|
||||
|
||||
row = await fetch_one(
|
||||
UserQueries.SELECT_BY_USERNAME,
|
||||
(user_data.username,),
|
||||
)
|
||||
|
||||
if row is None:
|
||||
raise Exception("User creation failed")
|
||||
|
||||
row = dict(row)
|
||||
|
||||
if isinstance(row["created_at"], str):
|
||||
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
|
||||
|
||||
row["is_active"] = bool(row["is_active"])
|
||||
|
||||
await store_service.register_user(row["id"])
|
||||
|
||||
|
||||
return User(**row)
|
||||
|
||||
async def get_user_by_username(self, username: str) -> Optional[User]:
|
||||
row = await fetch_one(
|
||||
UserQueries.SELECT_BY_USERNAME,
|
||||
(username,),
|
||||
)
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
row = dict(row)
|
||||
|
||||
if isinstance(row["created_at"], str):
|
||||
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
|
||||
|
||||
row["is_active"] = bool(row["is_active"])
|
||||
|
||||
return User(**row)
|
||||
|
||||
async def get_user_by_email(self, email: str) -> Optional[User]:
|
||||
row = await fetch_one(
|
||||
UserQueries.SELECT_BY_EMAIL,
|
||||
(email,),
|
||||
)
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
row = dict(row)
|
||||
|
||||
if isinstance(row["created_at"], str):
|
||||
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
|
||||
|
||||
row["is_active"] = bool(row["is_active"])
|
||||
|
||||
return User(**row)
|
||||
|
||||
async def authenticate_user(self, username: str, password: str) -> Optional[User]:
|
||||
user =
|
||||
if user and :
|
||||
return user
|
||||
return None
|
||||
|
||||
async def delete_user(self, username: str) -> bool:
|
||||
try:
|
||||
query = UserQueries.DELETE_USER_BY_USERNAME
|
||||
await execute(query, (username,))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def find_user(self, username: str) -> List[UserResponse]:
|
||||
query = UserQueries.SELECT_BY_USERNAME_LIKE
|
||||
rows = await fetch_all(
|
||||
query,
|
||||
("%" + username + "%",),
|
||||
)
|
||||
|
||||
return [User(**row).to_response() for row in rows]
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
row = await fetch_one(UserQueries.SELECT_BY_ID, (user_id,))
|
||||
if row is None:
|
||||
return None
|
||||
row = dict(row)
|
||||
if isinstance(row["created_at"], str):
|
||||
datetime.fromisoformat(row["created_at"].replace("Z", "+09:00"))
|
||||
row["is_active"] = bool(row["is_active"])
|
||||
return User(**row)
|
||||
|
||||
async def update_user(
|
||||
self, user: User, user_data: UserUpdate, profile_file: UploadFile = None
|
||||
) -> User:
|
||||
update_fields = {}
|
||||
if user_data.email:
|
||||
existing_user = await fetch_one(
|
||||
UserQueries.SELECT_USER_BY_EMAIL_AND_NOT_ID,
|
||||
(user_data.email, user.id),
|
||||
)
|
||||
if existing_user:
|
||||
raise ValueError("Email already registered")
|
||||
update_fields["email"] = user_data.email
|
||||
|
||||
if user_data.password:
|
||||
password_hash, salt = User.hash_password(user_data.password)
|
||||
update_fields["password_hash"] = password_hash
|
||||
update_fields["salt"] = salt
|
||||
|
||||
if profile_file:
|
||||
await self.image_processor.validate_image_file(
|
||||
profile_file.filename, profile_file.size
|
||||
)
|
||||
image_path = await self.image_processor.write_file_and_get_image_path(
|
||||
profile_file, upload_dir=self.upload_dir
|
||||
)
|
||||
update_fields["profile_image_path"] = image_path
|
||||
|
||||
if not update_fields:
|
||||
return user
|
||||
|
||||
set_clause = ", ".join(f"{key} = ?" for key in update_fields.keys())
|
||||
query = UserQueries.UPDATE_USER_BY_ID.format(set_clause)
|
||||
params = list(update_fields.values())
|
||||
params.append(user.id)
|
||||
|
||||
await execute(query, tuple(params))
|
||||
|
||||
return await self.get_user_by_id(user.id)
|
||||
0
Backend/tests/__init__.py
Normal file
81
Backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add Backend to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from Backend import app
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_user(client):
|
||||
user_data = {
|
||||
"username": "testuser_authenticated",
|
||||
"email": "test_auth@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
# Register user
|
||||
response = client.post("/api/user/register", data=user_data)
|
||||
|
||||
# Login to get token
|
||||
login_data = {"username": user_data["username"], "password": user_data["password"]}
|
||||
response = client.post("/api/user/login", json=login_data)
|
||||
token = response.json()["access_token"]
|
||||
|
||||
yield {"token": token, "user_data": user_data}
|
||||
|
||||
# Cleanup: delete user via API
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
client.delete(f"/api/user/{user_data['username']}", headers=headers)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_authenticated_users(client):
|
||||
user1_data = {
|
||||
"username": "testuser1",
|
||||
"email": "test1@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
user2_data = {
|
||||
"username": "testuser2",
|
||||
"email": "test2@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
|
||||
# Register users
|
||||
client.post("/api/user/register", data=user1_data)
|
||||
client.post("/api/user/register", data=user2_data)
|
||||
|
||||
# Login users
|
||||
login1_data = {
|
||||
"username": user1_data["username"],
|
||||
"password": user1_data["password"],
|
||||
}
|
||||
response1 = client.post("/api/user/login", json=login1_data)
|
||||
token1 = response1.json()["access_token"]
|
||||
|
||||
login2_data = {
|
||||
"username": user2_data["username"],
|
||||
"password": user2_data["password"],
|
||||
}
|
||||
response2 = client.post("/api/user/login", json=login2_data)
|
||||
token2 = response2.json()["access_token"]
|
||||
|
||||
yield {
|
||||
"user1": {"token": token1, "username": user1_data["username"]},
|
||||
"user2": {"token": token2, "username": user2_data["username"]},
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
headers1 = {"Authorization": f"Bearer {token1}"}
|
||||
headers2 = {"Authorization": f"Bearer {token2}"}
|
||||
client.delete(f"/api/user/{user1_data['username']}", headers=headers1)
|
||||
client.delete(f"/api/user/{user2_data['username']}", headers=headers2)
|
||||
71
Backend/tests/test_avatar.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from fastapi.testclient import TestClient
|
||||
from Backend.schemas.avatar import (
|
||||
AvatarUpdate,
|
||||
AvatarType,
|
||||
TopClothesType,
|
||||
BottomClothesType,
|
||||
)
|
||||
|
||||
|
||||
def test_get_my_avatar(client: TestClient, authenticated_user):
|
||||
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
|
||||
response = client.get("/api/avatar", headers=headers)
|
||||
assert response.status_code == 200
|
||||
avatar_data = response.json()
|
||||
assert "id" in avatar_data
|
||||
assert "user_id" in avatar_data
|
||||
assert avatar_data["avatar_type"]["name"] == AvatarType.MALE.value
|
||||
|
||||
|
||||
def test_update_my_avatar(client: TestClient, authenticated_user):
|
||||
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
|
||||
|
||||
response = client.get("/api/avatar", headers=headers)
|
||||
assert response.status_code == 200
|
||||
update_data = AvatarUpdate(
|
||||
avatar_type=AvatarType.FEMALE,
|
||||
top_clothe_type=TopClothesType.SCHOOL_CLOTHES,
|
||||
bottom_clothe_type=BottomClothesType.SCHOOL_CLOTHES_2,
|
||||
)
|
||||
response = client.put(
|
||||
"/api/avatar", json=update_data.model_dump(mode="json"), headers=headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
avatar_data = response.json()
|
||||
assert avatar_data["avatar_type"]["name"] == update_data.avatar_type.value
|
||||
assert avatar_data["top_clothe_type"]["name"] == update_data.top_clothe_type.value
|
||||
assert (
|
||||
avatar_data["bottom_clothe_type"]["name"]
|
||||
== update_data.bottom_clothe_type.value
|
||||
)
|
||||
|
||||
|
||||
def test_get_avatar_options(client: TestClient):
|
||||
response = client.get("/api/avatar/options")
|
||||
assert response.status_code == 200
|
||||
options = response.json()
|
||||
assert "avatar_types" in options
|
||||
assert "top_clothe_types" in options
|
||||
assert "bottom_clothe_types" in options
|
||||
assert all(isinstance(item, str) for item in options["avatar_types"])
|
||||
assert all(isinstance(item, str) for item in options["top_clothe_types"])
|
||||
assert all(isinstance(item, str) for item in options["bottom_clothe_types"])
|
||||
|
||||
|
||||
def test_get_avatar_by_user_id(client: TestClient, authenticated_user):
|
||||
headers = {"Authorization": f"Bearer {authenticated_user['token']}"}
|
||||
response = client.get("/api/avatar", headers=headers)
|
||||
assert response.status_code == 200
|
||||
user_id = response.json()["user_id"]
|
||||
|
||||
response = client.get(f"/api/avatar/{user_id}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
avatar_data = response.json()
|
||||
assert "id" in avatar_data
|
||||
assert avatar_data["user_id"] == user_id
|
||||
assert avatar_data["avatar_type"]["name"] == AvatarType.MALE.value
|
||||
|
||||
# Test for a non-existent user
|
||||
response = client.get("/api/avatar/9999", headers=headers)
|
||||
assert response.status_code == 404
|
||||
52
Backend/tests/test_diary.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from io import BytesIO
|
||||
|
||||
|
||||
def test_diary_operations(client, authenticated_user):
|
||||
token = authenticated_user["token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# Create Diary
|
||||
diary_data = {
|
||||
"title": "test title",
|
||||
"content": "test content",
|
||||
"category": "test category",
|
||||
}
|
||||
response = client.post(
|
||||
"/api/diary",
|
||||
data=diary_data,
|
||||
files={"file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
diary_id = response.json()["id"]
|
||||
assert response.json()["title"] == "test title"
|
||||
|
||||
# Get Diary
|
||||
response = client.get(f"/api/diary/{diary_id}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == diary_id
|
||||
|
||||
# List Diaries
|
||||
response = client.get("/api/diary", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) > 0
|
||||
|
||||
# Update Diary
|
||||
updated_diary_data = {"title": "updated title", "content": "updated content"}
|
||||
response = client.put(
|
||||
f"/api/diary/{diary_id}",
|
||||
data=updated_diary_data,
|
||||
files={"file": ("b.jpg", BytesIO(b"bbb"), "image/jpeg")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "updated title"
|
||||
|
||||
# Delete Diary
|
||||
response = client.delete(f"/api/diary/{diary_id}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["message"] == "Diary deleted successfully"
|
||||
|
||||
# Verify Deletion
|
||||
response = client.get(f"/api/diary/{diary_id}", headers=headers)
|
||||
assert response.status_code == 400
|
||||
45
Backend/tests/test_friendship.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
def test_friendship_flow(client, two_authenticated_users):
|
||||
user1 = two_authenticated_users["user1"]
|
||||
user2 = two_authenticated_users["user2"]
|
||||
|
||||
headers1 = {"Authorization": f"Bearer {user1['token']}"}
|
||||
headers2 = {"Authorization": f"Bearer {user2['token']}"}
|
||||
|
||||
# User 1 sends friendship request to User 2
|
||||
response = client.post(
|
||||
"/api/friendship/request",
|
||||
json={"friend_username": user2["username"]},
|
||||
headers=headers1,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
friendship_id = response.json()["id"]
|
||||
assert response.json()["status"] == "pending"
|
||||
|
||||
# User 2 accepts friendship request
|
||||
response = client.put(f"/api/friendship/{friendship_id}/accept", headers=headers2)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "accepted"
|
||||
|
||||
# User 1 lists friends
|
||||
response = client.get("/api/friendship", headers=headers1)
|
||||
assert response.status_code == 200
|
||||
assert any(f["id"] == friendship_id for f in response.json())
|
||||
|
||||
# User 2 lists friends
|
||||
response = client.get("/api/friendship", headers=headers2)
|
||||
assert response.status_code == 200
|
||||
assert any(f["id"] == friendship_id for f in response.json())
|
||||
|
||||
# User 1 deletes friendship
|
||||
response = client.delete(f"/api/friendship/{friendship_id}", headers=headers1)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["message"] == "Friendship deleted successfully"
|
||||
|
||||
# Verify deletion for both users
|
||||
response = client.get("/api/friendship", headers=headers1)
|
||||
assert response.status_code == 200
|
||||
assert not any(f["id"] == friendship_id for f in response.json())
|
||||
|
||||
response = client.get("/api/friendship", headers=headers2)
|
||||
assert response.status_code == 200
|
||||
assert not any(f["id"] == friendship_id for f in response.json())
|
||||
38
Backend/tests/test_guestbook.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
def test_guest_book(client, authenticated_user):
|
||||
token = authenticated_user["token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
user_data = {
|
||||
"username": "testtarget",
|
||||
"email": "test@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
response = client.post("/api/user/register", data=user_data)
|
||||
assert response.status_code == 201
|
||||
user_id = response.json()["id"]
|
||||
username = response.json()["username"]
|
||||
|
||||
response = client.post(
|
||||
"/api/guestbook",
|
||||
json={"target_user_id": user_id, "content": "test"},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["content"] == "test"
|
||||
id = response.json()["id"]
|
||||
|
||||
response = client.get(f"/api/guestbook/{user_id}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()[0]["content"] == "test"
|
||||
|
||||
response = client.put(
|
||||
f"/api/guestbook/{id}", json={"content": "test2"}, headers=headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["content"] == "test2"
|
||||
|
||||
response = client.delete(f"/api/guestbook/{id}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
client.delete(f"/api/user/{username}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
32
Backend/tests/test_letter.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# def test_letter_operations(client, authenticated_user):
|
||||
# token = authenticated_user["token"]
|
||||
# headers = {"Authorization": f"Bearer {token}"}
|
||||
#
|
||||
# # Create Letter
|
||||
# letter_data = {"content": "test content"}
|
||||
# response = client.post("/api/letter", json=letter_data, headers=headers)
|
||||
# assert response.status_code == 200
|
||||
# letter_id = response.json()["id"]
|
||||
# assert response.json()["content"] == "test content"
|
||||
#
|
||||
# # Get Letter
|
||||
# response = client.get(f"/api/letter/{letter_id}", headers=headers)
|
||||
# assert response.status_code == 200
|
||||
# assert response.json()["id"] == letter_id
|
||||
#
|
||||
# # Update Letter
|
||||
# updated_letter_data = {"content": "updated content"}
|
||||
# response = client.put(
|
||||
# f"/api/letter/{letter_id}", json=updated_letter_data, headers=headers
|
||||
# )
|
||||
# assert response.status_code == 200
|
||||
# assert response.json()["content"] == "updated content"
|
||||
#
|
||||
# # Delete Letter
|
||||
# response = client.delete(f"/api/letter/{letter_id}", headers=headers)
|
||||
# assert response.status_code == 200
|
||||
# assert response.json()["detail"] == "Letter deleted"
|
||||
#
|
||||
# # Verify Deletion
|
||||
# response = client.get(f"/api/letter/{letter_id}", headers=headers)
|
||||
# assert response.status_code == 400
|
||||
94
Backend/tests/test_photo.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# from io import BytesIO
|
||||
# import json
|
||||
#
|
||||
#
|
||||
# def test_photo_upload_and_delete(client, authenticated_user):
|
||||
# token = authenticated_user["token"]
|
||||
# headers = {"Authorization": f"Bearer {token}"}
|
||||
#
|
||||
# # Upload Photo
|
||||
# photo_data = {"album_name": "test_album", "title": "test_title"}
|
||||
# photo_data_json = json.dumps(photo_data).encode("utf-8")
|
||||
#
|
||||
# response = client.post(
|
||||
# "/api/photo/upload",
|
||||
# files={
|
||||
# "photo_data": (
|
||||
# "photo_data.json",
|
||||
# BytesIO(photo_data_json),
|
||||
# "application/json",
|
||||
# ),
|
||||
# "file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg"),
|
||||
# },
|
||||
# headers=headers,
|
||||
# )
|
||||
# assert response.status_code == 200
|
||||
# photo_id = response.json()["id"]
|
||||
# assert response.json()["album_name"] == "test_album"
|
||||
#
|
||||
# # Delete Photo
|
||||
# response = client.delete(f"/api/photo/{photo_id}", headers=headers)
|
||||
# assert response.status_code == 200
|
||||
# assert response.json()["message"] == "Photo deleted successfully"
|
||||
#
|
||||
#
|
||||
# def test_photo_commenting(client, two_authenticated_users):
|
||||
# user1 = two_authenticated_users["user1"]
|
||||
# user2 = two_authenticated_users["user2"]
|
||||
#
|
||||
# headers1 = {"Authorization": f"Bearer {user1['token']}"}
|
||||
# headers2 = {"Authorization": f"Bearer {user2['token']}"}
|
||||
#
|
||||
# # User 1 uploads a photo
|
||||
# photo_data = {"album_name": "test_album", "title": "test_title"}
|
||||
# photo_data_json = json.dumps(photo_data).encode("utf-8")
|
||||
# response = client.post(
|
||||
# "/api/photo/upload",
|
||||
# files={
|
||||
# "photo_data": (
|
||||
# "photo_data.json",
|
||||
# BytesIO(photo_data_json),
|
||||
# "application/json",
|
||||
# ),
|
||||
# "file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg"),
|
||||
# },
|
||||
# headers=headers1,
|
||||
# )
|
||||
# assert response.status_code == 200
|
||||
# photo_id = response.json()["id"]
|
||||
#
|
||||
# # User 2 cannot comment before being friends
|
||||
# response = client.post(
|
||||
# f"/api/photo/{photo_id}/comment",
|
||||
# json={"content": "test comment"},
|
||||
# headers=headers2,
|
||||
# )
|
||||
# assert response.status_code == 400
|
||||
#
|
||||
# # User 1 sends friendship request to User 2
|
||||
# response = client.post(
|
||||
# "/api/friendship/request",
|
||||
# json={"friend_username": user2["username"]},
|
||||
# headers=headers1,
|
||||
# )
|
||||
# assert response.status_code == 200
|
||||
# friendship_id = response.json()["id"]
|
||||
#
|
||||
# # User 2 accepts friendship request
|
||||
# response = client.put(f"/api/friendship/{friendship_id}/accept", headers=headers2)
|
||||
# assert response.status_code == 200
|
||||
#
|
||||
# # User 2 can now comment
|
||||
# response = client.post(
|
||||
# f"/api/photo/{photo_id}/comment",
|
||||
# json={"content": "test comment"},
|
||||
# headers=headers2,
|
||||
# )
|
||||
# assert response.status_code == 200
|
||||
# assert response.json()["content"] == "test comment"
|
||||
#
|
||||
# # User 1 can see the comment
|
||||
# response = client.get(f"/api/photo/{photo_id}/comments", headers=headers1)
|
||||
# assert response.status_code == 200
|
||||
# assert len(response.json()) > 0
|
||||
# assert response.json()[0]["content"] == "test comment"
|
||||
161
Backend/tests/test_room.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
68
Backend/tests/test_user.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from io import BytesIO
|
||||
|
||||
|
||||
def test_user_registration_and_login(client):
|
||||
# Registration
|
||||
user_data = {
|
||||
"username": "testuser999",
|
||||
"email": "test@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
response = client.post(
|
||||
"/api/user/register",
|
||||
data=user_data,
|
||||
files={"file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg")},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["username"] == "testuser999"
|
||||
|
||||
# Duplicate registration
|
||||
response = client.post(
|
||||
"/api/user/register",
|
||||
data=user_data,
|
||||
files={"file": ("a.jpg", BytesIO(b"aaa"), "image/jpeg")},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Username already registered"
|
||||
|
||||
# Login
|
||||
login_data = {
|
||||
"username": "testuser999",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
response = client.post("/api/user/login", json=login_data)
|
||||
assert response.status_code == 200
|
||||
assert "access_token" in response.json()
|
||||
|
||||
# Login with wrong password
|
||||
login_data["password"] = "wrongpassword"
|
||||
response = client.post("/api/user/login", json=login_data)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Invalid username or password"
|
||||
|
||||
# delete
|
||||
response = client.delete(f"/api/user/{user_data['username']}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_get_user_profile(client, authenticated_user):
|
||||
token = authenticated_user["token"]
|
||||
username = authenticated_user["user_data"]["username"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = client.get(f"/api/user/profile/{username}", headers=headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["username"] == username
|
||||
|
||||
|
||||
def test_user_delete(client, authenticated_user):
|
||||
token = authenticated_user["token"]
|
||||
username = authenticated_user["user_data"]["username"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = client.delete(f"/api/user/{username}", headers=headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["detail"] == "User deleted"
|
||||
0
Backend/utils/__init__.py
Normal file
53
Backend/utils/db.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import aiosqlite
|
||||
from typing import Any, List, Tuple, Optional, Dict, Union
|
||||
|
||||
DB_PATH = "database.sqlite3"
|
||||
|
||||
# Generated by Github Copilot
|
||||
# db 사용을 편하게 하기 위한 함수
|
||||
|
||||
|
||||
async def get_db_connection(db_path: str = DB_PATH) -> aiosqlite.Connection:
|
||||
return await aiosqlite.connect(db_path)
|
||||
|
||||
|
||||
async def execute(
|
||||
query: str,
|
||||
params: Union[Tuple[Any, ...], Dict[str, Any]] = (),
|
||||
db_path: str = DB_PATH,
|
||||
) -> None:
|
||||
async with aiosqlite.connect(db_path) as db:
|
||||
await db.execute(query, params)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def fetch_one(
|
||||
query: str,
|
||||
params: Union[Tuple[Any, ...], Dict[str, Any]] = (),
|
||||
db_path: str = DB_PATH,
|
||||
) -> Optional[aiosqlite.Row]:
|
||||
async with aiosqlite.connect(db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(query, params) as cursor:
|
||||
return await cursor.fetchone()
|
||||
|
||||
|
||||
async def fetch_all(
|
||||
query: str,
|
||||
params: Union[Tuple[Any, ...], Dict[str, Any]] = (),
|
||||
db_path: str = DB_PATH,
|
||||
) -> List[aiosqlite.Row]:
|
||||
async with aiosqlite.connect(db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(query, params) as cursor:
|
||||
return await cursor.fetchall()
|
||||
|
||||
|
||||
async def executemany(
|
||||
query: str,
|
||||
seq_of_params: List[Union[Tuple[Any, ...], Dict[str, Any]]],
|
||||
db_path: str = DB_PATH,
|
||||
) -> None:
|
||||
async with aiosqlite.connect(db_path) as db:
|
||||
await db.executemany(query, seq_of_params)
|
||||
await db.commit()
|
||||
168
Backend/utils/default_queries.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
class PhotoQueries:
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
album_name TEXT NOT NULL,
|
||||
image_path TEXT NOT NULL,
|
||||
title TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
CREATE_COMMENTS_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS photo_comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
photo_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (photo_id) REFERENCES photos (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
INSERT_PHOTO = """
|
||||
INSERT INTO photos (user_id, album_name, image_path, title, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
SELECT_USER_PHOTOS = """
|
||||
SELECT * FROM photos
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
SELECT_USER_PHOTOS_BY_ALBUM = """
|
||||
SELECT * FROM photos
|
||||
WHERE user_id = ? AND album_name = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
SELECT_LATEST_USER_PHOTO = """
|
||||
SELECT * FROM photos WHERE user_id = ? ORDER BY id DESC LIMIT 1
|
||||
"""
|
||||
|
||||
SELECT_PHOTO_OWNER = """
|
||||
SELECT user_id FROM photos WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_PHOTO_ALBUM_NAME = """
|
||||
SELECT album_name FROM photos WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
INSERT_COMMENT = """
|
||||
INSERT INTO photo_comments (photo_id, user_id, content, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
SELECT_LATEST_COMMENT = """
|
||||
SELECT * FROM photo_comments WHERE photo_id = ? AND user_id = ? ORDER BY id DESC LIMIT 1
|
||||
"""
|
||||
|
||||
SELECT_PHOTO_COMMENTS = """
|
||||
SELECT pc.*, u.username
|
||||
FROM photo_comments pc
|
||||
JOIN users u ON pc.user_id = u.id
|
||||
WHERE pc.photo_id = ?
|
||||
ORDER BY pc.created_at ASC
|
||||
"""
|
||||
|
||||
SELECT_PHOTO_BY_ID = """
|
||||
SELECT * FROM photos WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
DELETE_PHOTO = """
|
||||
DELETE FROM photos WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
UPDATE_PHOTO_PATH = """
|
||||
UPDATE photos SET image_path = ? WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
|
||||
class LetterQueries:
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS letters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
FOREIGN KEY (sender_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
INSERT_LETTER = """
|
||||
INSERT INTO letters (sender_id, content)
|
||||
VALUES (?, ?)
|
||||
"""
|
||||
|
||||
SELECT_USER_LETTERS = """
|
||||
SELECT * FROM letters
|
||||
WHERE sender_id = ?
|
||||
"""
|
||||
|
||||
SELECT_LATEST_USER_LETTER = """
|
||||
SELECT * FROM letters WHERE sender_id = ? LIMIT 1
|
||||
"""
|
||||
|
||||
SELECT_LETTER_BY_ID = """
|
||||
SELECT * FROM letters WHERE id = ? AND sender_id = ?
|
||||
"""
|
||||
|
||||
SELECT_LETTER_FOR_DELIVERY = """
|
||||
SELECT * FROM letters WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_SENDER_USERNAME = """
|
||||
SELECT username FROM users WHERE id = ?
|
||||
"""
|
||||
|
||||
UPDATE_LETTER = """
|
||||
UPDATE letters SET content = ? WHERE id = ? AND sender_id = ?
|
||||
"""
|
||||
|
||||
DELETE_LETTER = """
|
||||
DELETE FROM letters WHERE id = ? AND sender_id = ?
|
||||
"""
|
||||
|
||||
|
||||
class DatabaseIndexes:
|
||||
USER_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
|
||||
]
|
||||
|
||||
DIARY_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_diaries_user_id ON diaries(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_diaries_category ON diaries(category)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_diaries_created_at ON diaries(created_at)",
|
||||
]
|
||||
|
||||
PHOTO_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_photos_user_id ON photos(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_photos_album ON photos(album_name)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_photo_comments_photo_id ON photo_comments(photo_id)",
|
||||
]
|
||||
|
||||
FRIENDSHIP_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_friendships_user_id ON friendships(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_friendships_friend_id ON friendships(friend_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_friendships_status ON friendships(status)",
|
||||
]
|
||||
|
||||
LETTER_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_letters_sender_id ON letters(sender_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_letters_sent_date ON letters(sent_date)",
|
||||
]
|
||||
|
||||
AVATAR_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_avatars_user_id ON avatars(user_id)",
|
||||
]
|
||||
|
||||
GUEST_BOOK_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_guest_books_user_id ON guest_books(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_guest_books_created_at ON guest_books(created_at)",
|
||||
]
|
||||
75
Backend/utils/email_processor.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import aiosmtplib
|
||||
from email.message import EmailMessage
|
||||
from typing import Optional
|
||||
import os
|
||||
from pydantic import EmailStr
|
||||
import ssl
|
||||
|
||||
# aiosmtplib을 사용한 비동기 이메일 전송
|
||||
# EmailStr 사용
|
||||
|
||||
|
||||
class EmailProcessor:
|
||||
def __init__(self):
|
||||
self.smtp_server = os.getenv("SMTP_SERVER", "smtp.gmail.com")
|
||||
self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
|
||||
self.sender_email = os.getenv("SENDER_EMAIL")
|
||||
self.sender_password = os.getenv("SENDER_PASSWORD")
|
||||
self.receiver_email = ""
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
subject: str,
|
||||
content: str,
|
||||
sender_email: EmailStr,
|
||||
sender_password: str,
|
||||
html_content: Optional[str] = None,
|
||||
) -> bool:
|
||||
if not self.sender_email or not self.sender_password:
|
||||
print("Email credentials not configured, cannot send email.")
|
||||
return False
|
||||
|
||||
if not sender_email or not sender_password:
|
||||
print(
|
||||
"Warning: Email credentials (SENDER_EMAIL, SENDER_PASSWORD) "
|
||||
"are not configured in environment variables."
|
||||
)
|
||||
return False
|
||||
|
||||
message = EmailMessage()
|
||||
message["Subject"] = subject
|
||||
message["From"] = sender_email
|
||||
message["To"] = self.receiver_email
|
||||
message.set_content(content)
|
||||
|
||||
# HTML 내용이 있는 경우, 대체 콘텐츠로 추가
|
||||
if html_content:
|
||||
message.add_alternative(html_content, subtype="html")
|
||||
|
||||
try:
|
||||
# 포트 465는 SMTPS (implicit TLS)를 사용하고, 포트 587은 STARTTLS를 사용합니다.
|
||||
use_tls = self.smtp_port == 465
|
||||
context = ssl.create_default_context() if use_tls else None
|
||||
|
||||
await aiosmtplib.send(
|
||||
message,
|
||||
hostname=self.smtp_server,
|
||||
port=self.smtp_port,
|
||||
username=sender_email,
|
||||
password=sender_password,
|
||||
use_tls=use_tls,
|
||||
ssl_context=context,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
print(f"Email sent successfully to {self.receiver_email}")
|
||||
return True
|
||||
|
||||
except aiosmtplib.SMTPException as e:
|
||||
print(f"SMTP error occurred while sending to {self.receiver_email}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(
|
||||
f"An unexpected error occurred while sending email to {self.receiver_email}: {e}"
|
||||
)
|
||||
return False
|
||||
179
Backend/utils/image_processor.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import os
|
||||
import aiofiles
|
||||
from PIL import Image, ImageFilter, ImageEnhance
|
||||
from datetime import datetime
|
||||
from starlette.datastructures import UploadFile
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
def __init__(self):
|
||||
self.filter_dir = "uploads/filtered"
|
||||
os.makedirs(self.filter_dir, exist_ok=True)
|
||||
|
||||
async def apply_filter(self, image_path: str, filter_type: str) -> str:
|
||||
"""Apply filter to image and return the path to filtered image"""
|
||||
|
||||
# Remove leading slash if exists for local file access
|
||||
local_path = image_path.lstrip("/")
|
||||
|
||||
if not os.path.exists(local_path):
|
||||
raise ValueError(f"Image file not found: {local_path}")
|
||||
|
||||
try:
|
||||
with Image.open(local_path) as img:
|
||||
# Convert to RGB if necessary
|
||||
if img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
|
||||
filtered_img = self._apply_filter_effect(img, filter_type)
|
||||
|
||||
# Generate filtered image filename
|
||||
base_name = os.path.basename(image_path)
|
||||
name, ext = os.path.splitext(base_name)
|
||||
filtered_filename = f"{name}_{filter_type}{ext}"
|
||||
filtered_path = os.path.join(self.filter_dir, filtered_filename)
|
||||
|
||||
# Save filtered image
|
||||
filtered_img.save(filtered_path, quality=85, optimize=True)
|
||||
|
||||
return f"/uploads/filtered/{filtered_filename}"
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to apply filter: {str(e)}")
|
||||
|
||||
def _apply_filter_effect(self, img: Image.Image, filter_type: str) -> Image.Image:
|
||||
"""Apply specific filter effect to image"""
|
||||
if filter_type == "none":
|
||||
return img
|
||||
elif filter_type == "vintage":
|
||||
return self._apply_vintage_filter(img)
|
||||
elif filter_type == "black_white":
|
||||
return self._apply_black_white_filter(img)
|
||||
elif filter_type == "sepia":
|
||||
return self._apply_sepia_filter(img)
|
||||
elif filter_type == "blur":
|
||||
return self._apply_blur_filter(img)
|
||||
elif filter_type == "sharpen":
|
||||
return self._apply_sharpen_filter(img)
|
||||
elif filter_type == "bright":
|
||||
return self._apply_brightness_filter(img)
|
||||
elif filter_type == "contrast":
|
||||
return self._apply_contrast_filter(img)
|
||||
else:
|
||||
raise ValueError(f"Unknown filter type: {filter_type}")
|
||||
|
||||
def _apply_vintage_filter(self, img: Image.Image) -> Image.Image:
|
||||
# Reduce saturation
|
||||
enhancer = ImageEnhance.Color(img)
|
||||
img = enhancer.enhance(0.7)
|
||||
|
||||
# Add slight warm tint
|
||||
r, g, b = img.split()
|
||||
r = ImageEnhance.Brightness(r).enhance(1.1)
|
||||
g = ImageEnhance.Brightness(g).enhance(1.05)
|
||||
b = ImageEnhance.Brightness(b).enhance(0.9)
|
||||
|
||||
return Image.merge("RGB", (r, g, b))
|
||||
|
||||
def _apply_black_white_filter(self, img: Image.Image) -> Image.Image:
|
||||
return img.convert("L").convert("RGB")
|
||||
|
||||
def _apply_sepia_filter(self, img: Image.Image) -> Image.Image:
|
||||
pixels = img.load()
|
||||
width, height = img.size
|
||||
|
||||
for py in range(height):
|
||||
for px in range(width):
|
||||
r, g, b = pixels[px, py]
|
||||
|
||||
tr = int(0.393 * r + 0.769 * g + 0.189 * b)
|
||||
tg = int(0.349 * r + 0.686 * g + 0.168 * b)
|
||||
tb = int(0.272 * r + 0.534 * g + 0.131 * b)
|
||||
|
||||
pixels[px, py] = (min(255, tr), min(255, tg), min(255, tb))
|
||||
|
||||
return img
|
||||
|
||||
def _apply_blur_filter(self, img: Image.Image) -> Image.Image:
|
||||
return img.filter(ImageFilter.GaussianBlur(radius=2))
|
||||
|
||||
def _apply_sharpen_filter(self, img: Image.Image) -> Image.Image:
|
||||
return img.filter(ImageFilter.SHARPEN)
|
||||
|
||||
def _apply_brightness_filter(self, img: Image.Image) -> Image.Image:
|
||||
enhancer = ImageEnhance.Brightness(img)
|
||||
return enhancer.enhance(1.3)
|
||||
|
||||
def _apply_contrast_filter(self, img: Image.Image) -> Image.Image:
|
||||
enhancer = ImageEnhance.Contrast(img)
|
||||
return enhancer.enhance(1.2)
|
||||
|
||||
async def resize_image(
|
||||
self, image_path: str, max_width: int = 800, max_height: int = 800
|
||||
) -> str:
|
||||
"""Resize image while maintaining aspect ratio"""
|
||||
|
||||
local_path = image_path.lstrip("/")
|
||||
|
||||
if not os.path.exists(local_path):
|
||||
raise ValueError(f"Image file not found: {local_path}")
|
||||
|
||||
try:
|
||||
with Image.open(local_path) as img:
|
||||
# Calculate new size maintaining aspect ratio
|
||||
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Generate resized image filename
|
||||
base_name = os.path.basename(image_path)
|
||||
name, ext = os.path.splitext(base_name)
|
||||
resized_filename = f"{name}_resized{ext}"
|
||||
resized_path = os.path.join(
|
||||
os.path.dirname(local_path), resized_filename
|
||||
)
|
||||
|
||||
# Save resized image
|
||||
img.save(resized_path, quality=85, optimize=True)
|
||||
|
||||
return f"/{resized_path}"
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to resize image: {str(e)}")
|
||||
|
||||
async def validate_image_file(self, filename: str, file_size: int):
|
||||
"""Validate image file type and size"""
|
||||
|
||||
allowed_extensions = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
|
||||
|
||||
# Check file extension
|
||||
_, ext = os.path.splitext(filename.lower())
|
||||
if ext not in allowed_extensions:
|
||||
raise ValueError("Unsupported file type")
|
||||
|
||||
if file_size > 20 * 1024 * 1024: # 20MB limit
|
||||
raise ValueError("File size must be less than 20MB")
|
||||
|
||||
def get_safe_filename(self, filename: str) -> str:
|
||||
"""Generate safe filename by removing dangerous characters"""
|
||||
|
||||
import re
|
||||
|
||||
# Keep only alphanumeric characters, dots, and hyphens
|
||||
safe_name = re.sub(r"[^a-zA-Z0-9._-]", "_", filename)
|
||||
|
||||
# Add timestamp to avoid collisions
|
||||
name, ext = os.path.splitext(safe_name)
|
||||
timestamp = int(datetime.now().timestamp())
|
||||
|
||||
return f"{name}_{timestamp}{ext}"
|
||||
|
||||
async def write_file_and_get_image_path(
|
||||
self, file: UploadFile, upload_dir: str
|
||||
) -> str:
|
||||
filename = self.get_safe_filename(file.filename)
|
||||
file_path = os.path.join(upload_dir, filename)
|
||||
|
||||
async with aiofiles.open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
await f.write(content)
|
||||
|
||||
return file_path
|
||||
0
Backend/utils/queries/__init__.py
Normal file
25
Backend/utils/queries/avatar.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
class AvatarQueries:
|
||||
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS avatars (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
avatar_type TEXT NOT NULL,
|
||||
top_clothe_type TEXT,
|
||||
bottom_clothe_type TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
SELECT_USER_AVATAR = """
|
||||
SELECT * FROM avatars WHERE user_id = ?
|
||||
"""
|
||||
|
||||
INSERT_AVATAR = """
|
||||
INSERT INTO avatars (user_id, avatar_type, top_clothe_type, bottom_clothe_type)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
UPDATE_AVATAR = """
|
||||
UPDATE avatars SET {fields} WHERE user_id = ?
|
||||
"""
|
||||
71
Backend/utils/queries/diary.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
class DiaryQueries:
|
||||
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS diaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
images TEXT,
|
||||
category TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_submitted BOOLEAN DEFAULT FALSE,
|
||||
email_sent BOOLEAN DEFAULT FALSE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
INSERT_DIARY = """
|
||||
INSERT INTO diaries (user_id, title, content, images, category, created_at, is_submitted, email_sent)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
SELECT_USER_DIARIES = """
|
||||
SELECT * FROM diaries
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
SELECT_USER_DIARIES_BY_CATEGORY = """
|
||||
SELECT * FROM diaries
|
||||
WHERE user_id = ? AND category = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
SELECT_BY_ID = """
|
||||
SELECT * FROM diaries WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_BY_ID_WITH_USER_ID = """
|
||||
SELECT * FROM diaries WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
SELECT_LATEST_USER_DIARY = """
|
||||
SELECT * FROM diaries WHERE user_id = ? ORDER BY id DESC LIMIT 1
|
||||
"""
|
||||
|
||||
SELECT_IMAGES_BY_ID = """
|
||||
SELECT images FROM diaries WHERE id = ?
|
||||
"""
|
||||
|
||||
UPDATE_DIARY = """
|
||||
UPDATE diaries SET {fields} WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
DELETE_DIARY = """
|
||||
DELETE FROM diaries WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
UPDATE_SUBMISSION_STATUS = """
|
||||
UPDATE diaries SET is_submitted = ? WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
|
||||
UPDATE_EMAIL_SENT = """
|
||||
UPDATE diaries SET email_sent = ? WHERE id = ?
|
||||
"""
|
||||
|
||||
UPDATE_DIARY_IMAGE_BY_ID = """
|
||||
UPDATE diaries SET images = ? WHERE id = ?
|
||||
"""
|
||||
89
Backend/utils/queries/friendship.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
class FriendshipQueries:
|
||||
"""친구 관계 관련 쿼리"""
|
||||
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS friendships (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
friend_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (friend_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
UNIQUE(user_id, friend_id)
|
||||
)
|
||||
"""
|
||||
|
||||
SELECT_USER_BY_USERNAME = """
|
||||
SELECT id FROM users WHERE username = ?
|
||||
"""
|
||||
|
||||
SELECT_EXISTING_FRIENDSHIP = """
|
||||
SELECT * FROM friendships
|
||||
WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)
|
||||
"""
|
||||
|
||||
INSERT_FRIENDSHIP = """
|
||||
INSERT INTO friendships (user_id, friend_id, status, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
SELECT_FRIENDSHIP_BY_IDS = """
|
||||
SELECT * FROM friendships WHERE user_id = ? AND friend_id = ?
|
||||
"""
|
||||
|
||||
SELECT_FRIENDSHIP_FOR_ACCEPT = """
|
||||
SELECT f.*, u.username
|
||||
FROM friendships f
|
||||
JOIN users u ON f.user_id = u.id
|
||||
WHERE f.id = ? AND f.friend_id = ? AND f.status = ?
|
||||
"""
|
||||
|
||||
UPDATE_FRIENDSHIP_STATUS = """
|
||||
UPDATE friendships SET status = ? WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_USER_FRIENDSHIPS = """
|
||||
SELECT f.*, u.username
|
||||
FROM friendships f
|
||||
JOIN users u ON (
|
||||
CASE
|
||||
WHEN f.user_id = ? THEN f.friend_id = u.id
|
||||
ELSE f.user_id = u.id
|
||||
END
|
||||
)
|
||||
WHERE (f.user_id = ? OR f.friend_id = ?) AND f.status = ?
|
||||
ORDER BY f.created_at DESC
|
||||
"""
|
||||
|
||||
SELECT_USER_FRIENDSHIPS_BY_STATUS = """
|
||||
SELECT f.*, u.username
|
||||
FROM friendships f
|
||||
JOIN users u ON (
|
||||
CASE
|
||||
WHEN f.user_id = ? THEN f.friend_id = u.id
|
||||
ELSE f.user_id = u.id
|
||||
END
|
||||
)
|
||||
WHERE (f.user_id = ? OR f.friend_id = ?) AND f.status = ?
|
||||
ORDER BY f.created_at DESC
|
||||
"""
|
||||
|
||||
DELETE_FRIENDSHIP = """
|
||||
DELETE FROM friendships
|
||||
WHERE id = ? AND (user_id = ? OR friend_id = ?)
|
||||
"""
|
||||
|
||||
SELECT_PENDING_REQUESTS = """
|
||||
SELECT f.*, u.username
|
||||
FROM friendships f
|
||||
JOIN users u ON f.user_id = u.id
|
||||
WHERE f.friend_id = ? AND f.status = ?
|
||||
ORDER BY f.created_at DESC
|
||||
"""
|
||||
|
||||
CHECK_FRIENDSHIP_STATUS = """
|
||||
SELECT * FROM friendships
|
||||
WHERE ((user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?))
|
||||
AND status = 'accepted'
|
||||
"""
|
||||
40
Backend/utils/queries/guestbook.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
class GuestBookQueries:
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS guest_books (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_user_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
INSERT_GUEST_BOOK = """
|
||||
INSERT INTO guest_books (target_user_id, user_id, content, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
SELECT_TARGET_USER_GUEST_BOOKS = """
|
||||
SELECT * FROM guest_books
|
||||
WHERE target_user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
SELECT_GUEST_BOOK_BY_ID = """
|
||||
SELECT * FROM guest_books WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_GUEST_BOOK_BY_USER_ID = """
|
||||
SELECT * FROM guest_books WHERE user_id = ? ORDER BY created_at DESC LIMIT 1
|
||||
"""
|
||||
|
||||
UPDATE_GUEST_BOOK_BY_ID = """
|
||||
UPDATE guest_books SET content = ?, updated_at=CURRENT_TIMESTAMP WHERE id = ?
|
||||
"""
|
||||
|
||||
DELETE_GUEST_BOOK = """
|
||||
DELETE FROM guest_books WHERE id = ? AND user_id = ?
|
||||
"""
|
||||
81
Backend/utils/queries/room.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
class RoomQueries:
|
||||
"""마이 룸 관련 쿼리"""
|
||||
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER UNIQUE,
|
||||
room_name TEXT,
|
||||
room_type TEXT
|
||||
)
|
||||
"""
|
||||
CREATE_TABLE_ROOM_FURNITURE = """
|
||||
CREATE TABLE IF NOT EXISTS room_furnitures (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
furniture_name TEXT NOT NULL,
|
||||
x INTEGER NOT NULL,
|
||||
y INTEGER NOT NULL,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
CREATE_TABLE_USER_FURNITURE = """
|
||||
CREATE TABLE IF NOT EXISTS user_furnitures (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
furniture_name TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
INSERT_USER_FURNITURE = """
|
||||
INSERT INTO user_furnitures (user_id, furniture_name) VALUES (?, ?)
|
||||
"""
|
||||
|
||||
INSERT_ROOM = """
|
||||
INSERT INTO rooms (user_id, room_name, room_type) VALUES (?, ?, ?)
|
||||
"""
|
||||
|
||||
INSERT_ROOM_FURNITURE = """
|
||||
INSERT INTO room_furnitures (room_id, furniture_name, x, y) VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
SELECT_ROOM_ID_BY_USER_ID = """
|
||||
SELECT id FROM rooms WHERE user_id = ?
|
||||
"""
|
||||
|
||||
SELECT_ROOM_BY_ID = """
|
||||
SELECT * FROM rooms WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_FURNITURE = """
|
||||
SELECT * FROM furnitures
|
||||
"""
|
||||
|
||||
SELECT_ROOM_FURNITURE = """
|
||||
SELECT id, furniture_name, x, y FROM room_furnitures WHERE room_id = ?
|
||||
"""
|
||||
|
||||
SELECT_FURNITURE_ID_BY_X_Y = """
|
||||
SELECT id FROM room_furnitures WHERE room_id = ? AND x = ? AND y = ?
|
||||
"""
|
||||
|
||||
SELECT_USER_FURNITURE = """
|
||||
SELECT * FROM user_furnitures WHERE user_id = ?
|
||||
"""
|
||||
|
||||
UPDATE_ROOM_NAME = """
|
||||
UPDATE rooms SET room_name = ? WHERE id = ?
|
||||
"""
|
||||
|
||||
UPDATE_ROOM_TYPE = """
|
||||
UPDATE rooms SET room_type = ? WHERE id = ?
|
||||
"""
|
||||
|
||||
DELETE_FURNITURE = """
|
||||
DELETE FROM room_furnitures WHERE room_id = ? AND x = ? AND y = ?
|
||||
"""
|
||||
|
||||
SELECT_ROOM_BY_USER_ID = """
|
||||
SELECT * FROM rooms WHERE user_id = ?
|
||||
"""
|
||||
61
Backend/utils/queries/user.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
class UserQueries:
|
||||
"""사용자 관련 쿼리"""
|
||||
|
||||
CREATE_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
profile_image_path TEXT DEFAULT 'upload/profile/default.jpg',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
)
|
||||
"""
|
||||
|
||||
INSERT_USER_WITHOUT_PROFILE = """
|
||||
INSERT INTO users (username, email, password_hash, salt)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
INSERT_USER_WITH_PROFILE = """
|
||||
INSERT INTO users (username, email, password_hash, salt, profile_image_path)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
SELECT_BY_USERNAME = """
|
||||
SELECT * FROM users WHERE username = ?
|
||||
"""
|
||||
|
||||
SELECT_BY_EMAIL = """
|
||||
SELECT * FROM users WHERE email = ?
|
||||
"""
|
||||
|
||||
SELECT_BY_ID = """
|
||||
SELECT * FROM users WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_BY_USERNAME_LIKE = """
|
||||
SELECT * FROM users WHERE username LIKE ?
|
||||
"""
|
||||
|
||||
DELETE_USER_BY_USERNAME = """
|
||||
DELETE FROM users WHERE username = ?
|
||||
"""
|
||||
|
||||
UPDATE_PROFILE_IMAGE_PATH_BY_USERNAME = """
|
||||
UPDATE users SET profile_image_path = ? WHERE username = ?
|
||||
"""
|
||||
|
||||
UPDATE_PROFILE_IMAGE_PATH_BY_ID = """
|
||||
UPDATE users SET profile_image_path = ? WHERE id = ?
|
||||
"""
|
||||
|
||||
UPDATE_USER_BY_ID = """
|
||||
UPDATE users SET {} WHERE id = ?
|
||||
"""
|
||||
|
||||
SELECT_USER_BY_EMAIL_AND_NOT_ID = """
|
||||
SELECT * FROM users WHERE email = ? AND id != ?
|
||||
"""
|
||||
64
Backend/utils/run_server.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
# 프로젝트 루트를 Python 경로에 추가
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="싸이월드 - 추억 속으로",
|
||||
description="2000년대 감성의 소셜 네트워크 서비스",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||
app.mount("/public", StaticFiles(directory="public"), name="public")
|
||||
|
||||
|
||||
# 정적 파일 서빙
|
||||
async def init_folders(app: FastAPI):
|
||||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||
|
||||
|
||||
async def init_db():
|
||||
|
||||
for service in os.listdir("Backend/services"):
|
||||
try:
|
||||
if service.endswith(".py"):
|
||||
module_name = "Backend.services." + service[:-3]
|
||||
module = __import__(module_name, fromlist=[""])
|
||||
service_class_name = service[:-3].split("_")[0] + "Service"
|
||||
service_class_name = service_class_name.replace(
|
||||
service_class_name[0], service_class_name[0].upper(), 1
|
||||
)
|
||||
service_class = getattr(module, service_class_name)
|
||||
|
||||
await service_class.init_db()
|
||||
print(f"{service_class_name} : init_db")
|
||||
|
||||
except Exception as e:
|
||||
print(f"failed to init_db {service}")
|
||||
print(e)
|
||||
|
||||
|
||||
async def startup_event():
|
||||
await init_folders(app)
|
||||
await init_db()
|
||||
|
||||
|
||||
def init_FastAPI() -> FastAPI:
|
||||
app.add_event_handler("startup", startup_event)
|
||||
return app
|
||||
49
README.md
Normal file
|
|
@ -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
|
||||
|
||||
### 참고자료
|
||||
싸이월드 미니홈피 소개
|
||||
1
api.json
Normal file
BIN
public/avatar/교복상의.png
Normal file
|
After Width: | Height: | Size: 294 B |
BIN
public/avatar/교복조끼상의.png
Normal file
|
After Width: | Height: | Size: 297 B |
BIN
public/avatar/교복조끼하의남.png
Normal file
|
After Width: | Height: | Size: 183 B |
BIN
public/avatar/교복조끼하의여.png
Normal file
|
After Width: | Height: | Size: 165 B |
BIN
public/avatar/교복하의남.png
Normal file
|
After Width: | Height: | Size: 186 B |
BIN
public/avatar/교복하의여.png
Normal file
|
After Width: | Height: | Size: 165 B |
BIN
public/avatar/남자.png
Normal file
|
After Width: | Height: | Size: 441 B |
BIN
public/avatar/동잠상의.png
Normal file
|
After Width: | Height: | Size: 270 B |
BIN
public/avatar/멜빵바지상의 .png
Normal file
|
After Width: | Height: | Size: 309 B |
BIN
public/avatar/멜빵바지상의.png
Normal file
|
After Width: | Height: | Size: 309 B |
BIN
public/avatar/멜빵치마상의.png
Normal file
|
After Width: | Height: | Size: 303 B |
BIN
public/avatar/무지개맨투맨상의.png
Normal file
|
After Width: | Height: | Size: 297 B |
BIN
public/avatar/산타상의.png
Normal file
|
After Width: | Height: | Size: 288 B |
BIN
public/avatar/산타하의.png
Normal file
|
After Width: | Height: | Size: 189 B |
BIN
public/avatar/여자.png
Normal file
|
After Width: | Height: | Size: 549 B |
BIN
public/avatar/청바지하의.png
Normal file
|
After Width: | Height: | Size: 189 B |
BIN
public/funiture/검정 노트북1-0.png
Normal file
|
After Width: | Height: | Size: 244 B |
BIN
public/funiture/검정 노트북1-180.png
Normal file
|
After Width: | Height: | Size: 251 B |
BIN
public/funiture/검정 노트북1-270.png
Normal file
|
After Width: | Height: | Size: 245 B |
BIN
public/funiture/검정 노트북1-90.png
Normal file
|
After Width: | Height: | Size: 264 B |
BIN
public/funiture/검정 노트북2-0.png
Normal file
|
After Width: | Height: | Size: 244 B |
BIN
public/funiture/검정 노트북2-180.png
Normal file
|
After Width: | Height: | Size: 260 B |
BIN
public/funiture/검정 노트북2-270.png
Normal file
|
After Width: | Height: | Size: 248 B |
BIN
public/funiture/검정 노트북2-90.png
Normal file
|
After Width: | Height: | Size: 254 B |
BIN
public/funiture/검정 노트북3-0.png
Normal file
|
After Width: | Height: | Size: 244 B |
BIN
public/funiture/검정 노트북3-180.png
Normal file
|
After Width: | Height: | Size: 254 B |
BIN
public/funiture/검정 노트북3-270.png
Normal file
|
After Width: | Height: | Size: 250 B |
BIN
public/funiture/검정 노트북3-90.png
Normal file
|
After Width: | Height: | Size: 259 B |
BIN
public/funiture/나무 탁자-90.png
Normal file
|
After Width: | Height: | Size: 511 B |
BIN
public/funiture/나무탁자-0.png
Normal file
|
After Width: | Height: | Size: 506 B |
BIN
public/funiture/노트북1-0.png
Normal file
|
After Width: | Height: | Size: 303 B |
BIN
public/funiture/노트북1-180.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
public/funiture/노트북1-270.png
Normal file
|
After Width: | Height: | Size: 266 B |
BIN
public/funiture/노트북1-90.png
Normal file
|
After Width: | Height: | Size: 285 B |
BIN
public/funiture/노트북2-0.png
Normal file
|
After Width: | Height: | Size: 303 B |
BIN
public/funiture/노트북2-180.png
Normal file
|
After Width: | Height: | Size: 288 B |
BIN
public/funiture/노트북2-270.png
Normal file
|
After Width: | Height: | Size: 270 B |