a
This commit is contained in:
commit
40266cc6e5
191 changed files with 5022 additions and 0 deletions
68
.github/workflows/black-check.yml
vendored
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
0
Backend/schemas/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue