From 09d4999149661c6d3f1d6beb12249345a2367f02 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Thu, 3 Oct 2024 22:39:19 +0900 Subject: [PATCH 01/60] =?UTF-8?q?=F0=9F=9A=9A=20build:=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EB=B3=84=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/__init__.py | 0 src/app/badges/__init__.py | 0 src/app/badges/models/__init__.py | 0 src/app/badges/repositorys/__init__.py | 0 src/app/badges/router.py | 0 src/app/badges/schemas/__init__.py | 0 src/app/badges/services/__init__.py | 0 src/app/colors/__init__.py | 0 src/app/colors/models/__init__.py | 0 src/app/colors/repositorys/__init__.py | 0 src/app/colors/router.py | 0 src/app/colors/schemas/__init__.py | 0 src/app/colors/services/__init__.py | 0 src/app/items/__init__.py | 0 src/app/items/models/__init__.py | 0 src/app/items/repositorys/__init__.py | 0 src/app/items/router.py | 0 src/app/items/schemas/__init__.py | 0 src/app/items/services/__init__.py | 0 src/app/levels/__init__.py | 0 src/app/levels/models/__init__.py | 0 src/app/levels/repositorys/__init__.py | 0 src/app/levels/router.py | 0 src/app/levels/schemas/__init__.py | 0 src/app/levels/services/__init__.py | 0 src/app/missions/__init__.py | 0 src/app/missions/models/__init__.py | 0 src/app/missions/repositorys/__init__.py | 0 src/app/missions/router.py | 0 src/app/missions/schemas/__init__.py | 0 src/app/missions/services/__init__.py | 0 src/app/rewards/__init__.py | 0 src/app/rewards/models/__init__.py | 0 src/app/rewards/repositorys/__init__.py | 0 src/app/rewards/router.py | 0 src/app/rewards/schemas/__init__.py | 0 src/app/rewards/services/__init__.py | 0 src/app/teller_cards/__init__.py | 0 src/app/teller_cards/models/__init__.py | 0 src/app/teller_cards/repositorys/__init__.py | 0 src/app/teller_cards/schemas/__init__.py | 0 src/app/teller_cards/services/__init__.py | 0 src/app/users/__init__.py | 0 src/app/users/models/__init__.py | 0 src/app/users/repositorys/__init__.py | 0 src/app/users/schemas/__init__.py | 0 src/app/users/services/__init__.py | 0 src/common/__init__.py | 0 src/common/handlers/__init__.py | 0 src/common/post_construct.py | 0 src/core/__init__.py | 0 src/core/config.py | 0 src/core/database/__init__.py | 0 src/core/database/connection.py | 0 src/core/database/orm.py | 0 55 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/app/__init__.py create mode 100644 src/app/badges/__init__.py create mode 100644 src/app/badges/models/__init__.py create mode 100644 src/app/badges/repositorys/__init__.py create mode 100644 src/app/badges/router.py create mode 100644 src/app/badges/schemas/__init__.py create mode 100644 src/app/badges/services/__init__.py create mode 100644 src/app/colors/__init__.py create mode 100644 src/app/colors/models/__init__.py create mode 100644 src/app/colors/repositorys/__init__.py create mode 100644 src/app/colors/router.py create mode 100644 src/app/colors/schemas/__init__.py create mode 100644 src/app/colors/services/__init__.py create mode 100644 src/app/items/__init__.py create mode 100644 src/app/items/models/__init__.py create mode 100644 src/app/items/repositorys/__init__.py create mode 100644 src/app/items/router.py create mode 100644 src/app/items/schemas/__init__.py create mode 100644 src/app/items/services/__init__.py create mode 100644 src/app/levels/__init__.py create mode 100644 src/app/levels/models/__init__.py create mode 100644 src/app/levels/repositorys/__init__.py create mode 100644 src/app/levels/router.py create mode 100644 src/app/levels/schemas/__init__.py create mode 100644 src/app/levels/services/__init__.py create mode 100644 src/app/missions/__init__.py create mode 100644 src/app/missions/models/__init__.py create mode 100644 src/app/missions/repositorys/__init__.py create mode 100644 src/app/missions/router.py create mode 100644 src/app/missions/schemas/__init__.py create mode 100644 src/app/missions/services/__init__.py create mode 100644 src/app/rewards/__init__.py create mode 100644 src/app/rewards/models/__init__.py create mode 100644 src/app/rewards/repositorys/__init__.py create mode 100644 src/app/rewards/router.py create mode 100644 src/app/rewards/schemas/__init__.py create mode 100644 src/app/rewards/services/__init__.py create mode 100644 src/app/teller_cards/__init__.py create mode 100644 src/app/teller_cards/models/__init__.py create mode 100644 src/app/teller_cards/repositorys/__init__.py create mode 100644 src/app/teller_cards/schemas/__init__.py create mode 100644 src/app/teller_cards/services/__init__.py create mode 100644 src/app/users/__init__.py create mode 100644 src/app/users/models/__init__.py create mode 100644 src/app/users/repositorys/__init__.py create mode 100644 src/app/users/schemas/__init__.py create mode 100644 src/app/users/services/__init__.py create mode 100644 src/common/__init__.py create mode 100644 src/common/handlers/__init__.py create mode 100644 src/common/post_construct.py create mode 100644 src/core/__init__.py create mode 100644 src/core/config.py create mode 100644 src/core/database/__init__.py create mode 100644 src/core/database/connection.py create mode 100644 src/core/database/orm.py diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/badges/__init__.py b/src/app/badges/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/badges/models/__init__.py b/src/app/badges/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/badges/repositorys/__init__.py b/src/app/badges/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/badges/router.py b/src/app/badges/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/badges/schemas/__init__.py b/src/app/badges/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/badges/services/__init__.py b/src/app/badges/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/colors/__init__.py b/src/app/colors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/colors/models/__init__.py b/src/app/colors/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/colors/repositorys/__init__.py b/src/app/colors/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/colors/router.py b/src/app/colors/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/colors/schemas/__init__.py b/src/app/colors/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/colors/services/__init__.py b/src/app/colors/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/items/__init__.py b/src/app/items/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/items/models/__init__.py b/src/app/items/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/items/repositorys/__init__.py b/src/app/items/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/items/router.py b/src/app/items/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/items/schemas/__init__.py b/src/app/items/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/items/services/__init__.py b/src/app/items/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/levels/__init__.py b/src/app/levels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/levels/models/__init__.py b/src/app/levels/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/levels/repositorys/__init__.py b/src/app/levels/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/levels/router.py b/src/app/levels/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/levels/schemas/__init__.py b/src/app/levels/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/levels/services/__init__.py b/src/app/levels/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/missions/__init__.py b/src/app/missions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/missions/models/__init__.py b/src/app/missions/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/missions/repositorys/__init__.py b/src/app/missions/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/missions/router.py b/src/app/missions/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/missions/schemas/__init__.py b/src/app/missions/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/missions/services/__init__.py b/src/app/missions/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rewards/__init__.py b/src/app/rewards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rewards/models/__init__.py b/src/app/rewards/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rewards/repositorys/__init__.py b/src/app/rewards/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rewards/router.py b/src/app/rewards/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rewards/schemas/__init__.py b/src/app/rewards/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rewards/services/__init__.py b/src/app/rewards/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/teller_cards/__init__.py b/src/app/teller_cards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/teller_cards/models/__init__.py b/src/app/teller_cards/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/teller_cards/repositorys/__init__.py b/src/app/teller_cards/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/teller_cards/schemas/__init__.py b/src/app/teller_cards/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/teller_cards/services/__init__.py b/src/app/teller_cards/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/__init__.py b/src/app/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/models/__init__.py b/src/app/users/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/repositorys/__init__.py b/src/app/users/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/schemas/__init__.py b/src/app/users/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/services/__init__.py b/src/app/users/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/handlers/__init__.py b/src/common/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/post_construct.py b/src/common/post_construct.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/config.py b/src/core/config.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/database/__init__.py b/src/core/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/database/connection.py b/src/core/database/connection.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/database/orm.py b/src/core/database/orm.py new file mode 100644 index 0000000..e69de29 From 3b1dc8b6f374640bdd1c68733f55af4ac3d2d59c Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Fri, 11 Oct 2024 01:18:51 +0900 Subject: [PATCH 02/60] =?UTF-8?q?=F0=9F=9A=9A=20build:=20sql-alchemy,=20py?= =?UTF-8?q?mysql=20=EC=82=AD=EC=A0=9C,=20tortoise-orm=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20database=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- poetry.lock | 403 ++++++++++++------ pyproject.toml | 6 +- src/app/{badges => v2}/__init__.py | 0 .../{badges/models => v2/answers}/__init__.py | 0 .../answers/dtos}/__init__.py | 0 .../schemas => v2/answers/models}/__init__.py | 0 .../answers/repositorys}/__init__.py | 0 src/app/{badges => v2/answers}/router.py | 0 .../answers/services}/__init__.py | 0 .../{colors/models => v2/badges}/__init__.py | 0 .../badges/dtos}/__init__.py | 0 .../schemas => v2/badges/models}/__init__.py | 0 .../badges/repositorys}/__init__.py | 0 src/app/{colors => v2/badges}/router.py | 0 .../{items => v2/badges/services}/__init__.py | 0 .../{items/models => v2/colors}/__init__.py | 0 .../colors/dtos}/__init__.py | 0 .../schemas => v2/colors/models}/__init__.py | 0 .../colors/repositorys}/__init__.py | 0 src/app/{items => v2/colors}/router.py | 0 .../colors/services}/__init__.py | 0 .../{levels/models => v2/items}/__init__.py | 0 .../repositorys => v2/items/dtos}/__init__.py | 0 .../schemas => v2/items/models}/__init__.py | 0 .../items/repositorys}/__init__.py | 0 src/app/{levels => v2/items}/router.py | 0 .../items/services}/__init__.py | 0 .../models => v2/levels}/__init__.py | 0 .../levels/dtos}/__init__.py | 0 .../schemas => v2/levels/models}/__init__.py | 0 .../levels/repositorys}/__init__.py | 0 src/app/{missions => v2/levels}/router.py | 0 .../levels/services}/__init__.py | 0 .../models => v2/missions}/__init__.py | 0 .../missions/dtos}/__init__.py | 0 .../missions/models}/__init__.py | 0 .../missions/repositorys}/__init__.py | 0 src/app/{rewards => v2/missions}/router.py | 0 .../missions/services}/__init__.py | 0 .../models => v2/mobiles}/__init__.py | 0 src/app/v2/mobiles/router.py | 18 + .../repositorys => v2/payments}/__init__.py | 0 .../schemas => v2/payments/dtos}/__init__.py | 0 .../payments/models}/__init__.py | 0 .../payments/repositorys}/__init__.py | 0 .../__init__.py => v2/payments/router.py} | 0 .../payments/services}/__init__.py | 0 .../schemas => v2/purchases}/__init__.py | 0 .../purchases/dtos}/__init__.py | 0 .../v2/purchases/models/__init__.py} | 0 .../v2/purchases/repositorys/__init__.py} | 0 .../v2/purchases/services/__init__.py} | 0 src/app/v2/questions/__init__.py | 0 src/app/v2/questions/dtos/__init__.py | 0 src/app/v2/questions/dtos/responses.py | 0 src/app/v2/questions/models/__init__.py | 0 src/app/v2/questions/models/question.py | 13 + src/app/v2/questions/repositorys/__init__.py | 0 src/app/v2/questions/router.py | 13 + src/app/v2/questions/services/__init__.py | 0 src/app/v2/rewards/__init__.py | 0 src/app/v2/rewards/models/__init__.py | 0 src/app/v2/rewards/repositorys/__init__.py | 0 src/app/v2/rewards/router.py | 0 src/app/v2/rewards/schemas/__init__.py | 0 src/app/v2/rewards/services/__init__.py | 0 src/app/v2/teller_cards/__init__.py | 0 src/app/v2/teller_cards/models/__init__.py | 0 .../v2/teller_cards/repositorys/__init__.py | 0 src/app/v2/teller_cards/schemas/__init__.py | 0 src/app/v2/teller_cards/services/__init__.py | 0 src/app/v2/users/__init__.py | 0 src/app/v2/users/models/__init__.py | 0 src/app/v2/users/repositorys/__init__.py | 0 src/app/v2/users/schemas/__init__.py | 0 src/app/v2/users/services/__init__.py | 0 src/common/handlers/exception_handler.py | 2 + src/common/handlers/router_handler.py | 7 + src/common/post_construct.py | 9 + src/core/configs/__init__.py | 8 + src/core/configs/base_settings.py | 19 + src/core/database/database_settings.py | 44 ++ src/main.py | 4 + 84 files changed, 407 insertions(+), 141 deletions(-) rename src/app/{badges => v2}/__init__.py (100%) rename src/app/{badges/models => v2/answers}/__init__.py (100%) rename src/app/{badges/repositorys => v2/answers/dtos}/__init__.py (100%) rename src/app/{badges/schemas => v2/answers/models}/__init__.py (100%) rename src/app/{badges/services => v2/answers/repositorys}/__init__.py (100%) rename src/app/{badges => v2/answers}/router.py (100%) rename src/app/{colors => v2/answers/services}/__init__.py (100%) rename src/app/{colors/models => v2/badges}/__init__.py (100%) rename src/app/{colors/repositorys => v2/badges/dtos}/__init__.py (100%) rename src/app/{colors/schemas => v2/badges/models}/__init__.py (100%) rename src/app/{colors/services => v2/badges/repositorys}/__init__.py (100%) rename src/app/{colors => v2/badges}/router.py (100%) rename src/app/{items => v2/badges/services}/__init__.py (100%) rename src/app/{items/models => v2/colors}/__init__.py (100%) rename src/app/{items/repositorys => v2/colors/dtos}/__init__.py (100%) rename src/app/{items/schemas => v2/colors/models}/__init__.py (100%) rename src/app/{items/services => v2/colors/repositorys}/__init__.py (100%) rename src/app/{items => v2/colors}/router.py (100%) rename src/app/{levels => v2/colors/services}/__init__.py (100%) rename src/app/{levels/models => v2/items}/__init__.py (100%) rename src/app/{levels/repositorys => v2/items/dtos}/__init__.py (100%) rename src/app/{levels/schemas => v2/items/models}/__init__.py (100%) rename src/app/{levels/services => v2/items/repositorys}/__init__.py (100%) rename src/app/{levels => v2/items}/router.py (100%) rename src/app/{missions => v2/items/services}/__init__.py (100%) rename src/app/{missions/models => v2/levels}/__init__.py (100%) rename src/app/{missions/repositorys => v2/levels/dtos}/__init__.py (100%) rename src/app/{missions/schemas => v2/levels/models}/__init__.py (100%) rename src/app/{missions/services => v2/levels/repositorys}/__init__.py (100%) rename src/app/{missions => v2/levels}/router.py (100%) rename src/app/{rewards => v2/levels/services}/__init__.py (100%) rename src/app/{rewards/models => v2/missions}/__init__.py (100%) rename src/app/{rewards/repositorys => v2/missions/dtos}/__init__.py (100%) rename src/app/{rewards/schemas => v2/missions/models}/__init__.py (100%) rename src/app/{rewards/services => v2/missions/repositorys}/__init__.py (100%) rename src/app/{rewards => v2/missions}/router.py (100%) rename src/app/{teller_cards => v2/missions/services}/__init__.py (100%) rename src/app/{teller_cards/models => v2/mobiles}/__init__.py (100%) create mode 100644 src/app/v2/mobiles/router.py rename src/app/{teller_cards/repositorys => v2/payments}/__init__.py (100%) rename src/app/{teller_cards/schemas => v2/payments/dtos}/__init__.py (100%) rename src/app/{teller_cards/services => v2/payments/models}/__init__.py (100%) rename src/app/{users => v2/payments/repositorys}/__init__.py (100%) rename src/app/{users/models/__init__.py => v2/payments/router.py} (100%) rename src/app/{users/repositorys => v2/payments/services}/__init__.py (100%) rename src/app/{users/schemas => v2/purchases}/__init__.py (100%) rename src/app/{users/services => v2/purchases/dtos}/__init__.py (100%) rename src/{core/config.py => app/v2/purchases/models/__init__.py} (100%) rename src/{core/database/connection.py => app/v2/purchases/repositorys/__init__.py} (100%) rename src/{core/database/orm.py => app/v2/purchases/services/__init__.py} (100%) create mode 100644 src/app/v2/questions/__init__.py create mode 100644 src/app/v2/questions/dtos/__init__.py create mode 100644 src/app/v2/questions/dtos/responses.py create mode 100644 src/app/v2/questions/models/__init__.py create mode 100644 src/app/v2/questions/models/question.py create mode 100644 src/app/v2/questions/repositorys/__init__.py create mode 100644 src/app/v2/questions/router.py create mode 100644 src/app/v2/questions/services/__init__.py create mode 100644 src/app/v2/rewards/__init__.py create mode 100644 src/app/v2/rewards/models/__init__.py create mode 100644 src/app/v2/rewards/repositorys/__init__.py create mode 100644 src/app/v2/rewards/router.py create mode 100644 src/app/v2/rewards/schemas/__init__.py create mode 100644 src/app/v2/rewards/services/__init__.py create mode 100644 src/app/v2/teller_cards/__init__.py create mode 100644 src/app/v2/teller_cards/models/__init__.py create mode 100644 src/app/v2/teller_cards/repositorys/__init__.py create mode 100644 src/app/v2/teller_cards/schemas/__init__.py create mode 100644 src/app/v2/teller_cards/services/__init__.py create mode 100644 src/app/v2/users/__init__.py create mode 100644 src/app/v2/users/models/__init__.py create mode 100644 src/app/v2/users/repositorys/__init__.py create mode 100644 src/app/v2/users/schemas/__init__.py create mode 100644 src/app/v2/users/services/__init__.py create mode 100644 src/common/handlers/exception_handler.py create mode 100644 src/common/handlers/router_handler.py create mode 100644 src/core/configs/__init__.py create mode 100644 src/core/configs/base_settings.py create mode 100644 src/core/database/database_settings.py diff --git a/.gitignore b/.gitignore index 09a9681..81fb159 100644 --- a/.gitignore +++ b/.gitignore @@ -122,7 +122,7 @@ celerybeat.pid *.sage.py # Environments -.env +src/.env .venv env/ venv/ diff --git a/poetry.lock b/poetry.lock index d9c6e44..bd27ed9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,22 +1,18 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] -name = "aiomysql" -version = "0.2.0" -description = "MySQL driver for asyncio." +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a"}, - {file = "aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67"}, + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, ] [package.dependencies] -PyMySQL = ">=1.0" - -[package.extras] -rsa = ["PyMySQL[rsa] (>=1.0)"] -sa = ["sqlalchemy (>=1.3,<1.4)"] +typing_extensions = ">=3.7.2" [[package]] name = "annotated-types" @@ -49,6 +45,72 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "asyncmy" +version = "0.2.9" +description = "A fast asyncio MySQL driver" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "asyncmy-0.2.9-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d077eaee9a126f36bbe95e0412baa89e93172dd46193ef7bf7650a686e458e50"}, + {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83cf951a44294626df43c5a85cf328297c3bac63f25ede216f9706514dabb322"}, + {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8a1d63c1bb8e3a09c90767199954fd423c48084a1f6c0d956217bc2e48d37d6d"}, + {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ecad6826086e47596c6aa65dcbe221305f3d9232f0d4de11b8562ee2c55464a"}, + {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a664d58f9ebe4132f6cb3128206392be8ad71ad6fb09a5f4a990b04ec142024"}, + {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f2bbd7b75e2d751216f48c3b1b5092b812d70c2cd0053f8d2f50ec3f76a525a8"}, + {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:55e3bc41aa0d4ab410fc3a1d0c31b9cdb6688cd3b0cae6f2ee49c2e7f42968be"}, + {file = "asyncmy-0.2.9-cp310-cp310-win32.whl", hash = "sha256:ea44eefc965c62bcfebf34e9ef00f6e807edf51046046767c56914243e0737e4"}, + {file = "asyncmy-0.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:2b4a2a7cf0bd5051931756e765fefef3c9f9561550e0dd8b1e79308d048b710a"}, + {file = "asyncmy-0.2.9-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:e2b77f03a17a8db338d74311e38ca6dbd4ff9aacb07d2af6b9e0cac9cf1c7b87"}, + {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c19f27b7ff0e297f2981335a85599ffe1c9a8a35c97230203321d5d6e9e4cb30"}, + {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:bf18aef65ac98f5130ca588c55a83a56e74ae416cf0fe2c0757a2b597c4269d0"}, + {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef02186cc02cb767ee5d5cf9ab002d5c7910a1a9f4c16a666867a9325c9ec5e"}, + {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:696da0f71db0fe11e62fa58cd5a27d7c9d9a90699d13d82640755d0061da0624"}, + {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84d20745bb187ced05bd4072ae8b0bff4b4622efa23b79935519edb717174584"}, + {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea242364523f6205c4426435272bd57cbf593c20d5e5551efb28d44cfbd595c2"}, + {file = "asyncmy-0.2.9-cp311-cp311-win32.whl", hash = "sha256:47609d34e6b49fc5ad5bd2a2a593ca120e143e2a4f4206f27a543c5c598a18ca"}, + {file = "asyncmy-0.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d56df7342f7b5467a9d09a854f0e5602c8da09afdad8181ba40b0434d66d8a4"}, + {file = "asyncmy-0.2.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c2a98f225560f9a52d5bd0d2e58517639e209e5d996e9ab7470e661b39394d"}, + {file = "asyncmy-0.2.9-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:20ae3acc326b4b104949cc5e3a728a927e671f671c6f26266ad4a44f57ea9a5b"}, + {file = "asyncmy-0.2.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8171a64888453423a17ae507cd97d256541ea880b314bba16376ab9deffef6e8"}, + {file = "asyncmy-0.2.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c966de493928f26218e0bfaa284cfa609540e52841c423d7babf9ca97c9ff820"}, + {file = "asyncmy-0.2.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4321c4cb4c691689aa26a56354e3fa723d89dc2cac82751e8671b2a4e6441778"}, + {file = "asyncmy-0.2.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd7cde6759dbbfcc467c2af4ef3d75de0b756dde39a3d176383d8c6d9f8a34f3"}, + {file = "asyncmy-0.2.9-cp37-cp37m-win32.whl", hash = "sha256:7678d3641d5a19f20e7e19220c83405fe8616a3b437efbc494f34ad186cedcf0"}, + {file = "asyncmy-0.2.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e8f48d09adf3426e7a59066eaae3c7c84c318ec56cc2f20732d652056c7a3f62"}, + {file = "asyncmy-0.2.9-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:4c4f1dc0acbaac8c3f046215031bbf3ca3d2cd7716244365325496e4f6222b78"}, + {file = "asyncmy-0.2.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:901aac048e5342acc62e1f68f6dec5aa3ed272cb2b138dca38d1c74fc414285d"}, + {file = "asyncmy-0.2.9-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c2d4ad8817f99d9734912c2ff91c42e419031441f512b4aecd7e40a167908c1c"}, + {file = "asyncmy-0.2.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544d3736fd6682f0201a123e4f49335420b6abf6c245abe0487f5967021f1436"}, + {file = "asyncmy-0.2.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f0c606a55625146e189534cc39038540f7a8f2c680ea82845c1f4315a9ad2914"}, + {file = "asyncmy-0.2.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:625f96371d64769b94f7f7f699cfa5be56e669828aef3698cbf4f5bb0014ccb3"}, + {file = "asyncmy-0.2.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eeeb53fdd54eef54b9793a7a5c849c5f7a2fb2540a637f21585a996ef9dd8845"}, + {file = "asyncmy-0.2.9-cp38-cp38-win32.whl", hash = "sha256:2136b749ac489c25ab3aab4a81ae6e9dfb18fd0a5ebda96cd72788c5e4d46927"}, + {file = "asyncmy-0.2.9-cp38-cp38-win_amd64.whl", hash = "sha256:d08fb8722150a9c0645665cf777916335687bddb5f37a8e02af772e330be777b"}, + {file = "asyncmy-0.2.9-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:dbee276a9c8750b522aaad86315a6ed1ffbcb9145ce89070db77831c00dd2da1"}, + {file = "asyncmy-0.2.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8755248429f9bd3d7768c71494c9943fced18f9f526f768e96f5b9b3c727c84"}, + {file = "asyncmy-0.2.9-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:64bcd5110dca7a96cb411de85ab8f79fa867e864150939b8e76286a66eab28fc"}, + {file = "asyncmy-0.2.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a83e3895bed6d44aa334deb1c343d4ffc64b0def2215149f8df2e0e13499250"}, + {file = "asyncmy-0.2.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:beb3d0e434ce0bd9e609cf5341c3b82433ef544f89055d3792186e11fa2433d9"}, + {file = "asyncmy-0.2.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dc608ff331c5d1065e2d3566493d2d9e17f36e315bd5fad3c91c421eea306edb"}, + {file = "asyncmy-0.2.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02caedc00035b2bd0be5555ef61d83ee9cb356ab488ac40072630ba224af02b0"}, + {file = "asyncmy-0.2.9-cp39-cp39-win32.whl", hash = "sha256:5b944d9cdf7ce25b396cd1e0c9319ba24c6583bde7a5dd31157614f3b9cc5b2f"}, + {file = "asyncmy-0.2.9-cp39-cp39-win_amd64.whl", hash = "sha256:3ceb59b9307b5eb893f4d473fcbc43ac0321ffb0436e0115b20cc2e0baa44eb5"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9f1ca623517552a637900b90d65b5bafc9c67bebf96e3427eecb9359ffa24b1"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:49622dc4ec69b5a4cbddb3695a1e9249b31092c6f19604abb664b43dcb509b6f"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8412e825443ee876ef0d55ac4356b56173f5cb64ca8e4638974f8cf5c912a63"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4025db2a27b1d84d3c68b5d5aacecac17258b69f25ec8a8c350c5f666003a778"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7640f3357849b176364ed546908e28c8460701ddc0d23cc3fa7113ec52a076"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d2593717fa7a92a7d361444726292ce34edea76d5aa67d469b5efeee1c9b729e"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f22e13bd77277593b56de2e4b65c40c2e81b1a42c4845d062403c5c5bc52bc"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a4aa17cc6ac0f7bc6b72e08d112566e69a36e2e1ebebad43d699757b7b4ff028"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e6f5205722e67c910510e294ad483bdafa7e29d5cf455d49ffa4b819e55fd8"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1021796f1910a0c2ab2d878f8f5d56f939ef0681f9c1fe925b78161cad2f8297"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b1dd463bb054138bd1fd3fec9911eb618e92f54f61abb476658f863340394d1"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad06f3c02d455947e95087d29f7122411208f0eadaf8671772fe5bad97d9873a"}, + {file = "asyncmy-0.2.9.tar.gz", hash = "sha256:da188be013291d1f831d63cdd3614567f4c63bfdcde73631ddff8df00c56d614"}, +] + [[package]] name = "black" version = "24.8.0" @@ -104,6 +166,85 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "click" version = "8.1.7" @@ -129,6 +270,55 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "43.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "dnspython" version = "2.6.1" @@ -445,6 +635,17 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "iso8601" +version = "1.1.0" +description = "Simple module to parse ISO 8601 dates" +optional = false +python-versions = ">=3.6.2,<4.0" +files = [ + {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, + {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, +] + [[package]] name = "isort" version = "5.13.2" @@ -690,6 +891,17 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.9.2" @@ -849,20 +1061,16 @@ files = [ windows-terminal = ["colorama (>=0.4.6)"] [[package]] -name = "pymysql" -version = "1.1.1" -description = "Pure Python MySQL Driver" +name = "pypika-tortoise" +version = "0.1.6" +description = "Forked from pypika and streamline just for tortoise-orm" optional = false -python-versions = ">=3.7" +python-versions = ">=3.7,<4.0" files = [ - {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, - {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, + {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, + {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, ] -[package.extras] -ed25519 = ["PyNaCl (>=1.4.0)"] -rsa = ["cryptography"] - [[package]] name = "pytest" version = "8.3.3" @@ -908,6 +1116,17 @@ files = [ {file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"}, ] +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1011,136 +1230,48 @@ files = [ ] [[package]] -name = "sqlalchemy" -version = "2.0.35" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f021d334f2ca692523aaf7bbf7592ceff70c8594fad853416a81d66b35e3abf9"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05c3f58cf91683102f2f0265c0db3bd3892e9eedabe059720492dbaa4f922da1"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032d979ce77a6c2432653322ba4cbeabf5a6837f704d16fa38b5a05d8e21fa00"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2e795c2f7d7249b75bb5f479b432a51b59041580d20599d4e112b5f2046437a3"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:cc32b2990fc34380ec2f6195f33a76b6cdaa9eecf09f0c9404b74fc120aef36f"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-win32.whl", hash = "sha256:9509c4123491d0e63fb5e16199e09f8e262066e58903e84615c301dde8fa2e87"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-win_amd64.whl", hash = "sha256:3655af10ebcc0f1e4e06c5900bb33e080d6a1fa4228f502121f28a3b1753cde5"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4c31943b61ed8fdd63dfd12ccc919f2bf95eefca133767db6fbbd15da62078ec"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a62dd5d7cc8626a3634208df458c5fe4f21200d96a74d122c83bc2015b333bc1"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0630774b0977804fba4b6bbea6852ab56c14965a2b0c7fc7282c5f7d90a1ae72"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d625eddf7efeba2abfd9c014a22c0f6b3796e0ffb48f5d5ab106568ef01ff5a"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ada603db10bb865bbe591939de854faf2c60f43c9b763e90f653224138f910d9"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c41411e192f8d3ea39ea70e0fae48762cd11a2244e03751a98bd3c0ca9a4e936"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-win32.whl", hash = "sha256:d299797d75cd747e7797b1b41817111406b8b10a4f88b6e8fe5b5e59598b43b0"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-win_amd64.whl", hash = "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-win32.whl", hash = "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-win_amd64.whl", hash = "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f"}, - {file = "SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1"}, - {file = "sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f"}, -] - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - -[[package]] -name = "sqlalchemy-utils" -version = "0.41.2" -description = "Various utility functions for SQLAlchemy." +name = "starlette" +version = "0.38.6" +description = "The little ASGI library that shines." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, - {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"}, + {file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"}, + {file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"}, ] [package.dependencies] -SQLAlchemy = ">=1.3" +anyio = ">=3.4.0,<5" [package.extras] -arrow = ["arrow (>=0.3.4)"] -babel = ["Babel (>=1.3)"] -color = ["colour (>=0.0.4)"] -encrypted = ["cryptography (>=0.6)"] -intervals = ["intervals (>=0.7.1)"] -password = ["passlib (>=1.6,<2.0)"] -pendulum = ["pendulum (>=2.0.5)"] -phone = ["phonenumbers (>=5.9.2)"] -test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -timezone = ["python-dateutil"] -url = ["furl (>=0.4.1)"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] [[package]] -name = "starlette" -version = "0.38.6" -description = "The little ASGI library that shines." +name = "tortoise-orm" +version = "0.21.6" +description = "Easy async ORM for python, built with relations in mind" optional = false -python-versions = ">=3.8" +python-versions = "<4.0,>=3.8" files = [ - {file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"}, - {file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"}, + {file = "tortoise_orm-0.21.6-py3-none-any.whl", hash = "sha256:98fcf07dce3396075eac36b0d2b14d2267ff875d32455e03ee15e38de2f138df"}, + {file = "tortoise_orm-0.21.6.tar.gz", hash = "sha256:0fbc718001647bf282c01eaaa360f94f1432c9281701244180703d48d58a88ec"}, ] [package.dependencies] -anyio = ">=3.4.0,<5" +aiosqlite = ">=0.16.0,<0.18.0" +asyncmy = {version = ">=0.2.8,<0.3.0", optional = true, markers = "extra == \"asyncmy\""} +iso8601 = ">=1.0.2,<2.0.0" +pydantic = ">=2.0,<2.7.0 || >2.7.0,<3.0" +pypika-tortoise = ">=0.1.6,<0.2.0" +pytz = "*" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +accel = ["ciso8601", "orjson", "uvloop"] +aiomysql = ["aiomysql"] +asyncmy = ["asyncmy (>=0.2.8,<0.3.0)"] +asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] +asyncpg = ["asyncpg"] +psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] [[package]] name = "typer" @@ -1432,4 +1563,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "f8e5b5d5c5cb7aba3c1540a9af858829fc894bcfd66c2aba5f75d3cb9abd6ddb" +content-hash = "56dff9593f165d37446949d353911498715f6d0ce99c834e042ed8c0aaf8ea9e" diff --git a/pyproject.toml b/pyproject.toml index aa76bb9..f0f54fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,14 +9,12 @@ readme = "README.md" python = "^3.12" fastapi = {extras = ["standard"], version = "^0.115.0"} gunicorn = "^23.0.0" -sqlalchemy = "^2.0.35" -pymysql = "^1.1.1" -aiomysql = "^0.2.0" greenlet = "^3.1.1" -sqlalchemy-utils = "^0.41.2" python-multipart = "^0.0.12" pydantic-settings = "^2.5.2" python-dotenv = "^1.0.1" +tortoise-orm = {extras = ["asyncmy"], version = "^0.21.6"} +cryptography = "^43.0.1" [tool.poetry.group.dev.dependencies] diff --git a/src/app/badges/__init__.py b/src/app/v2/__init__.py similarity index 100% rename from src/app/badges/__init__.py rename to src/app/v2/__init__.py diff --git a/src/app/badges/models/__init__.py b/src/app/v2/answers/__init__.py similarity index 100% rename from src/app/badges/models/__init__.py rename to src/app/v2/answers/__init__.py diff --git a/src/app/badges/repositorys/__init__.py b/src/app/v2/answers/dtos/__init__.py similarity index 100% rename from src/app/badges/repositorys/__init__.py rename to src/app/v2/answers/dtos/__init__.py diff --git a/src/app/badges/schemas/__init__.py b/src/app/v2/answers/models/__init__.py similarity index 100% rename from src/app/badges/schemas/__init__.py rename to src/app/v2/answers/models/__init__.py diff --git a/src/app/badges/services/__init__.py b/src/app/v2/answers/repositorys/__init__.py similarity index 100% rename from src/app/badges/services/__init__.py rename to src/app/v2/answers/repositorys/__init__.py diff --git a/src/app/badges/router.py b/src/app/v2/answers/router.py similarity index 100% rename from src/app/badges/router.py rename to src/app/v2/answers/router.py diff --git a/src/app/colors/__init__.py b/src/app/v2/answers/services/__init__.py similarity index 100% rename from src/app/colors/__init__.py rename to src/app/v2/answers/services/__init__.py diff --git a/src/app/colors/models/__init__.py b/src/app/v2/badges/__init__.py similarity index 100% rename from src/app/colors/models/__init__.py rename to src/app/v2/badges/__init__.py diff --git a/src/app/colors/repositorys/__init__.py b/src/app/v2/badges/dtos/__init__.py similarity index 100% rename from src/app/colors/repositorys/__init__.py rename to src/app/v2/badges/dtos/__init__.py diff --git a/src/app/colors/schemas/__init__.py b/src/app/v2/badges/models/__init__.py similarity index 100% rename from src/app/colors/schemas/__init__.py rename to src/app/v2/badges/models/__init__.py diff --git a/src/app/colors/services/__init__.py b/src/app/v2/badges/repositorys/__init__.py similarity index 100% rename from src/app/colors/services/__init__.py rename to src/app/v2/badges/repositorys/__init__.py diff --git a/src/app/colors/router.py b/src/app/v2/badges/router.py similarity index 100% rename from src/app/colors/router.py rename to src/app/v2/badges/router.py diff --git a/src/app/items/__init__.py b/src/app/v2/badges/services/__init__.py similarity index 100% rename from src/app/items/__init__.py rename to src/app/v2/badges/services/__init__.py diff --git a/src/app/items/models/__init__.py b/src/app/v2/colors/__init__.py similarity index 100% rename from src/app/items/models/__init__.py rename to src/app/v2/colors/__init__.py diff --git a/src/app/items/repositorys/__init__.py b/src/app/v2/colors/dtos/__init__.py similarity index 100% rename from src/app/items/repositorys/__init__.py rename to src/app/v2/colors/dtos/__init__.py diff --git a/src/app/items/schemas/__init__.py b/src/app/v2/colors/models/__init__.py similarity index 100% rename from src/app/items/schemas/__init__.py rename to src/app/v2/colors/models/__init__.py diff --git a/src/app/items/services/__init__.py b/src/app/v2/colors/repositorys/__init__.py similarity index 100% rename from src/app/items/services/__init__.py rename to src/app/v2/colors/repositorys/__init__.py diff --git a/src/app/items/router.py b/src/app/v2/colors/router.py similarity index 100% rename from src/app/items/router.py rename to src/app/v2/colors/router.py diff --git a/src/app/levels/__init__.py b/src/app/v2/colors/services/__init__.py similarity index 100% rename from src/app/levels/__init__.py rename to src/app/v2/colors/services/__init__.py diff --git a/src/app/levels/models/__init__.py b/src/app/v2/items/__init__.py similarity index 100% rename from src/app/levels/models/__init__.py rename to src/app/v2/items/__init__.py diff --git a/src/app/levels/repositorys/__init__.py b/src/app/v2/items/dtos/__init__.py similarity index 100% rename from src/app/levels/repositorys/__init__.py rename to src/app/v2/items/dtos/__init__.py diff --git a/src/app/levels/schemas/__init__.py b/src/app/v2/items/models/__init__.py similarity index 100% rename from src/app/levels/schemas/__init__.py rename to src/app/v2/items/models/__init__.py diff --git a/src/app/levels/services/__init__.py b/src/app/v2/items/repositorys/__init__.py similarity index 100% rename from src/app/levels/services/__init__.py rename to src/app/v2/items/repositorys/__init__.py diff --git a/src/app/levels/router.py b/src/app/v2/items/router.py similarity index 100% rename from src/app/levels/router.py rename to src/app/v2/items/router.py diff --git a/src/app/missions/__init__.py b/src/app/v2/items/services/__init__.py similarity index 100% rename from src/app/missions/__init__.py rename to src/app/v2/items/services/__init__.py diff --git a/src/app/missions/models/__init__.py b/src/app/v2/levels/__init__.py similarity index 100% rename from src/app/missions/models/__init__.py rename to src/app/v2/levels/__init__.py diff --git a/src/app/missions/repositorys/__init__.py b/src/app/v2/levels/dtos/__init__.py similarity index 100% rename from src/app/missions/repositorys/__init__.py rename to src/app/v2/levels/dtos/__init__.py diff --git a/src/app/missions/schemas/__init__.py b/src/app/v2/levels/models/__init__.py similarity index 100% rename from src/app/missions/schemas/__init__.py rename to src/app/v2/levels/models/__init__.py diff --git a/src/app/missions/services/__init__.py b/src/app/v2/levels/repositorys/__init__.py similarity index 100% rename from src/app/missions/services/__init__.py rename to src/app/v2/levels/repositorys/__init__.py diff --git a/src/app/missions/router.py b/src/app/v2/levels/router.py similarity index 100% rename from src/app/missions/router.py rename to src/app/v2/levels/router.py diff --git a/src/app/rewards/__init__.py b/src/app/v2/levels/services/__init__.py similarity index 100% rename from src/app/rewards/__init__.py rename to src/app/v2/levels/services/__init__.py diff --git a/src/app/rewards/models/__init__.py b/src/app/v2/missions/__init__.py similarity index 100% rename from src/app/rewards/models/__init__.py rename to src/app/v2/missions/__init__.py diff --git a/src/app/rewards/repositorys/__init__.py b/src/app/v2/missions/dtos/__init__.py similarity index 100% rename from src/app/rewards/repositorys/__init__.py rename to src/app/v2/missions/dtos/__init__.py diff --git a/src/app/rewards/schemas/__init__.py b/src/app/v2/missions/models/__init__.py similarity index 100% rename from src/app/rewards/schemas/__init__.py rename to src/app/v2/missions/models/__init__.py diff --git a/src/app/rewards/services/__init__.py b/src/app/v2/missions/repositorys/__init__.py similarity index 100% rename from src/app/rewards/services/__init__.py rename to src/app/v2/missions/repositorys/__init__.py diff --git a/src/app/rewards/router.py b/src/app/v2/missions/router.py similarity index 100% rename from src/app/rewards/router.py rename to src/app/v2/missions/router.py diff --git a/src/app/teller_cards/__init__.py b/src/app/v2/missions/services/__init__.py similarity index 100% rename from src/app/teller_cards/__init__.py rename to src/app/v2/missions/services/__init__.py diff --git a/src/app/teller_cards/models/__init__.py b/src/app/v2/mobiles/__init__.py similarity index 100% rename from src/app/teller_cards/models/__init__.py rename to src/app/v2/mobiles/__init__.py diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py new file mode 100644 index 0000000..8912dc1 --- /dev/null +++ b/src/app/v2/mobiles/router.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter(prefix="/mobiles", tags=["Mobile"]) + + +@router.post("/main") +async def mobile_main_handler(): + pass + + +@router.get("/tellercard") +async def mobile_teller_card_handler(): + pass + + +@router.get("/mypage") +async def mobile_my_page_handler(): + pass diff --git a/src/app/teller_cards/repositorys/__init__.py b/src/app/v2/payments/__init__.py similarity index 100% rename from src/app/teller_cards/repositorys/__init__.py rename to src/app/v2/payments/__init__.py diff --git a/src/app/teller_cards/schemas/__init__.py b/src/app/v2/payments/dtos/__init__.py similarity index 100% rename from src/app/teller_cards/schemas/__init__.py rename to src/app/v2/payments/dtos/__init__.py diff --git a/src/app/teller_cards/services/__init__.py b/src/app/v2/payments/models/__init__.py similarity index 100% rename from src/app/teller_cards/services/__init__.py rename to src/app/v2/payments/models/__init__.py diff --git a/src/app/users/__init__.py b/src/app/v2/payments/repositorys/__init__.py similarity index 100% rename from src/app/users/__init__.py rename to src/app/v2/payments/repositorys/__init__.py diff --git a/src/app/users/models/__init__.py b/src/app/v2/payments/router.py similarity index 100% rename from src/app/users/models/__init__.py rename to src/app/v2/payments/router.py diff --git a/src/app/users/repositorys/__init__.py b/src/app/v2/payments/services/__init__.py similarity index 100% rename from src/app/users/repositorys/__init__.py rename to src/app/v2/payments/services/__init__.py diff --git a/src/app/users/schemas/__init__.py b/src/app/v2/purchases/__init__.py similarity index 100% rename from src/app/users/schemas/__init__.py rename to src/app/v2/purchases/__init__.py diff --git a/src/app/users/services/__init__.py b/src/app/v2/purchases/dtos/__init__.py similarity index 100% rename from src/app/users/services/__init__.py rename to src/app/v2/purchases/dtos/__init__.py diff --git a/src/core/config.py b/src/app/v2/purchases/models/__init__.py similarity index 100% rename from src/core/config.py rename to src/app/v2/purchases/models/__init__.py diff --git a/src/core/database/connection.py b/src/app/v2/purchases/repositorys/__init__.py similarity index 100% rename from src/core/database/connection.py rename to src/app/v2/purchases/repositorys/__init__.py diff --git a/src/core/database/orm.py b/src/app/v2/purchases/services/__init__.py similarity index 100% rename from src/core/database/orm.py rename to src/app/v2/purchases/services/__init__.py diff --git a/src/app/v2/questions/__init__.py b/src/app/v2/questions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/questions/dtos/__init__.py b/src/app/v2/questions/dtos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/questions/dtos/responses.py b/src/app/v2/questions/dtos/responses.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/questions/models/__init__.py b/src/app/v2/questions/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/questions/models/question.py b/src/app/v2/questions/models/question.py new file mode 100644 index 0000000..f3571d5 --- /dev/null +++ b/src/app/v2/questions/models/question.py @@ -0,0 +1,13 @@ +from tortoise import fields +from tortoise.models import Model + + +class Question(Model): + date = fields.DateField(pk=True) # 기본 키로 설정된 날짜 필드 + phrase = fields.CharField(max_length=255) + title = fields.CharField(max_length=255) + spare_phrase = fields.CharField(max_length=255) + spare_title = fields.CharField(max_length=255) + + class Meta: + table = "question" # 데이터베이스에서 매핑할 테이블 이름 diff --git a/src/app/v2/questions/repositorys/__init__.py b/src/app/v2/questions/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/questions/router.py b/src/app/v2/questions/router.py new file mode 100644 index 0000000..76ae3f6 --- /dev/null +++ b/src/app/v2/questions/router.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, HTTPException + +from app.v2.questions.models.question import Question + +router = APIRouter(prefix="/question", tags=["Question"]) + + +@router.get("/questions/{date}") +async def get_question_by_date(date: str): + question = await Question.get_or_none(date=date) + if question is None: + raise HTTPException(status_code=404, detail="Question not found") + return question diff --git a/src/app/v2/questions/services/__init__.py b/src/app/v2/questions/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/rewards/__init__.py b/src/app/v2/rewards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/rewards/models/__init__.py b/src/app/v2/rewards/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/rewards/repositorys/__init__.py b/src/app/v2/rewards/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/rewards/router.py b/src/app/v2/rewards/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/rewards/schemas/__init__.py b/src/app/v2/rewards/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/rewards/services/__init__.py b/src/app/v2/rewards/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/__init__.py b/src/app/v2/teller_cards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/models/__init__.py b/src/app/v2/teller_cards/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/repositorys/__init__.py b/src/app/v2/teller_cards/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/schemas/__init__.py b/src/app/v2/teller_cards/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/services/__init__.py b/src/app/v2/teller_cards/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/users/__init__.py b/src/app/v2/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/users/models/__init__.py b/src/app/v2/users/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/users/repositorys/__init__.py b/src/app/v2/users/repositorys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/users/schemas/__init__.py b/src/app/v2/users/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/users/services/__init__.py b/src/app/v2/users/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/handlers/exception_handler.py b/src/common/handlers/exception_handler.py new file mode 100644 index 0000000..0de1de3 --- /dev/null +++ b/src/common/handlers/exception_handler.py @@ -0,0 +1,2 @@ +def attach_exception_handlers(app): + pass \ No newline at end of file diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py new file mode 100644 index 0000000..d8e9cda --- /dev/null +++ b/src/common/handlers/router_handler.py @@ -0,0 +1,7 @@ +from app.v2.mobiles.router import router as mobile_router +from app.v2.questions.router import router as question_router + + +def attach_router_handlers(app): + app.include_router(router=mobile_router, prefix="/api/v2") + app.include_router(router=question_router, prefix="/api/v2") diff --git a/src/common/post_construct.py b/src/common/post_construct.py index e69de29..e616cef 100644 --- a/src/common/post_construct.py +++ b/src/common/post_construct.py @@ -0,0 +1,9 @@ +from common.handlers.router_handler import attach_router_handlers +from common.handlers.exception_handler import attach_exception_handlers +from core.database.database_settings import database_initialize + + +def post_construct(app): + attach_router_handlers(app) + attach_exception_handlers(app) + database_initialize(app) diff --git a/src/core/configs/__init__.py b/src/core/configs/__init__.py new file mode 100644 index 0000000..c6ef013 --- /dev/null +++ b/src/core/configs/__init__.py @@ -0,0 +1,8 @@ +from core.configs.base_settings import Settings + + +def get_settings() -> Settings: + return Settings(_env_file=".env", _env_file_encoding="utf-8") + + +settings = get_settings() diff --git a/src/core/configs/base_settings.py b/src/core/configs/base_settings.py new file mode 100644 index 0000000..8529883 --- /dev/null +++ b/src/core/configs/base_settings.py @@ -0,0 +1,19 @@ +from pydantic_settings import BaseSettings +from enum import StrEnum + + +class Env(StrEnum): + LOCAL = "local" + STAGE = "stage" + PROD = "prod" + + +class Settings(BaseSettings): + ENV: Env = Env.LOCAL + DB_HOST: str + DB_PORT: int + DB_USER: str + DB_PASSWORD: str + DB_NAME: str + DB_TIMEZONE: str = "Asia/Seoul" + DB_CHARSET: str = "utf8mb4" diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py new file mode 100644 index 0000000..f934f05 --- /dev/null +++ b/src/core/database/database_settings.py @@ -0,0 +1,44 @@ +from fastapi import FastAPI +from tortoise import Tortoise +from tortoise.contrib.fastapi import register_tortoise +from core.configs import settings + + +TORTOISE_APP_MODELS = [ + "app.v2.questions.models.question", +] + +TORTOISE_ORM = { + "connections": { + "default": { + "engine": "tortoise.backends.mysql", + "credentials": { + "host": settings.DB_HOST, + "port": settings.DB_PORT, + "user": settings.DB_USER, + "password": settings.DB_PASSWORD, + "database": settings.DB_NAME, + "connect_timeout": 5, + "charset": settings.DB_CHARSET, + }, + }, + }, + "apps": { + "models": { + "models": TORTOISE_APP_MODELS, + "default_connection": "default", + }, + }, + "use_tz": False, # 비동기 ORM에서 타임존 설정을 고려하지 않는다면 False + "timezone": settings.DB_TIMEZONE, # 타임존 설정 +} + + +def database_initialize(app: FastAPI) -> None: + Tortoise.init_models(TORTOISE_APP_MODELS, "models") + register_tortoise( + app, + config=TORTOISE_ORM, + generate_schemas=False, + add_exception_handlers=True, + ) diff --git a/src/main.py b/src/main.py index 6620dee..720eb4d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,11 @@ from fastapi import FastAPI +from common.post_construct import post_construct + app = FastAPI() +post_construct(app) + @app.get("/health_check") def health_check(): From 5a6fc971713977ca658c994230e70635d84073e7 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Fri, 11 Oct 2024 01:54:50 +0900 Subject: [PATCH 03/60] =?UTF-8?q?=F0=9F=93=9D=20docs:=20ci=20file=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/checks.yml | 44 ------------------------------------ 1 file changed, 44 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d756559..09d40a6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,6 +1,5 @@ name: Code Quality Checks -# 트리거 이벤트: push 또는 pull_request 시 CI가 실행됩니다. on: push: branches: @@ -9,77 +8,34 @@ on: jobs: ci: - # 가장 최신버전의 ubuntu를 OS 환경으로 설정합니다. runs-on: ubuntu-latest - # services 키워드를 사용하여 MySQL 서비스를 설정 - services: - db: - image: mysql:8.0 - ports: - - 3306:3306 - # Github Secrets에서 가져와서 env로 등록, MySQL 데이터베이스 연결 설정 - env: - MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} - MYSQL_DATABASE: ${{ secrets.MYSQL_DB }} - MYSQL_USER: ${{ secrets.MYSQL_USER }} - MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} - # MySQL 연결 상태를 확인. 10초 단위로 5번 재시도. 5초간 기다림. - options: >- - --health-cmd "mysqladmin ping -h localhost" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - # CI 환경에서 코드를 체크아웃합니다. - name: Checkout code uses: actions/checkout@v3 - # CI 환경에서 사용할 파이썬 버전을 지정합니다. - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.12' - # 환경 변수를 추가 -# env: - - - # Poetry를 설치합니다. - name: Install Poetry run: | curl -sSL https://install.python-poetry.org | python3 - echo "${HOME}/.local/bin" >> $GITHUB_PATH - # Poetry를 사용하여 의존성 패키지들을 설치합니다. - name: Install Packages & Libraries run: | poetry install - # isort를 사용하여 import 구문의 정렬 순서를 체크합니다. - name: Run isort (Import sorting) run: | poetry run isort . --check --diff - # black을 사용하여 PEP8 코드스타일을 준수했는지 체크합니다. - name: Run black (Code formatting) run: | poetry run black . --check - # mypy를 사용해서 타입힌팅 체크 - name: Run Mypy run: | poetry run mypy . - - # MySQL 연결을 테스트 - - name: Wait for MySQL - run: | - until mysqladmin ping -h localhost -u ${{ secrets.MYSQL_USER }} -p${{ secrets.MYSQL_PASSWORD }}; do - sleep 1 - done - - # FastAPI 테스트 실행 - - name: Run FastAPI tests with pytest - run: | - poetry run pytest From 3b349f1c7a427f319d51969a56a24d803a58845e Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 13 Oct 2024 23:57:38 +0900 Subject: [PATCH 04/60] =?UTF-8?q?=E2=9C=A8=20=20feat:=20raw=20query=20exec?= =?UTF-8?q?utor=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20mypage=20api=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/models/answer.py | 40 +++++++++++++ src/app/v2/badges/models/badge.py | 36 +++++++++++ src/app/v2/levels/dtos/level_dto.py | 6 ++ .../schemas => mobiles/dtos}/__init__.py | 0 src/app/v2/mobiles/dtos/responses.py | 14 +++++ src/app/v2/mobiles/router.py | 49 +++++++++++++-- src/app/v2/payments/models/cheese_manager.py | 9 +++ src/app/v2/teller_cards/models/teller_card.py | 11 ++++ src/app/v2/users/dtos/__init__.py | 0 src/app/v2/users/dtos/user_profile_dto.py | 10 ++++ src/app/v2/users/models/refresh_token.py | 14 +++++ src/app/v2/users/models/user.py | 60 +++++++++++++++++++ src/app/v2/users/models/user_mission.py | 13 ++++ src/common/base_models/__init__.py | 0 src/common/base_models/base_dtos/__init__.py | 0 .../base_models/base_dtos/base_response.py | 9 +++ .../base_models/custom_fields/__init__.py | 0 .../custom_fields/hex_binary_field.py | 39 ++++++++++++ src/common/query_executor.py | 30 ++++++++++ src/core/database/database_settings.py | 7 +++ 20 files changed, 343 insertions(+), 4 deletions(-) create mode 100644 src/app/v2/answers/models/answer.py create mode 100644 src/app/v2/badges/models/badge.py create mode 100644 src/app/v2/levels/dtos/level_dto.py rename src/app/v2/{users/schemas => mobiles/dtos}/__init__.py (100%) create mode 100644 src/app/v2/mobiles/dtos/responses.py create mode 100644 src/app/v2/payments/models/cheese_manager.py create mode 100644 src/app/v2/teller_cards/models/teller_card.py create mode 100644 src/app/v2/users/dtos/__init__.py create mode 100644 src/app/v2/users/dtos/user_profile_dto.py create mode 100644 src/app/v2/users/models/refresh_token.py create mode 100644 src/app/v2/users/models/user.py create mode 100644 src/app/v2/users/models/user_mission.py create mode 100644 src/common/base_models/__init__.py create mode 100644 src/common/base_models/base_dtos/__init__.py create mode 100644 src/common/base_models/base_dtos/base_response.py create mode 100644 src/common/base_models/custom_fields/__init__.py create mode 100644 src/common/base_models/custom_fields/hex_binary_field.py create mode 100644 src/common/query_executor.py diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py new file mode 100644 index 0000000..9894174 --- /dev/null +++ b/src/app/v2/answers/models/answer.py @@ -0,0 +1,40 @@ +from fastapi import Depends +from tortoise import fields, Tortoise +from tortoise.models import Model + +from common.query_executor import QueryExecutor + + +class Answer(Model): + answer_id = fields.BigIntField(pk=True) + content = fields.TextField(null=False) + created_time = fields.DatetimeField(null=True) + date = fields.DateField(null=False) + emotion = fields.IntField(null=False) + is_premium = fields.BooleanField(null=False) + is_public = fields.BooleanField(null=False) + modified_time = fields.DatetimeField(null=True) + user = fields.ForeignKeyField( + "models.User", related_name="answers", null=True, on_delete=fields.SET_NULL + ) # 외래 키 정의 + is_blind = fields.BooleanField(null=False) + blind_ended_at = fields.DatetimeField(null=True) + blind_started_at = fields.DatetimeField(null=True) + like_count = fields.IntField(null=False, default=0) + is_spare = fields.BooleanField(null=False) + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) + + class Meta: + table = "answer" + + @classmethod + async def get_answer_count_by_user_id( + cls, + uuid_bytes: bytes, + ) -> int: + hex_data = uuid_bytes.hex() # uuid_bytes를 16진수 문자열로 변환 + query = ( + f"SELECT COUNT(*) as answer_count FROM answer WHERE user_id = 0x{hex_data}" + ) + return await QueryExecutor.execute_query(query, fetch_type="single") diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py new file mode 100644 index 0000000..e6c4cc8 --- /dev/null +++ b/src/app/v2/badges/models/badge.py @@ -0,0 +1,36 @@ +from tortoise import fields +from tortoise.models import Model +from uuid import UUID + +from common.query_executor import QueryExecutor + + +class Badge(Model): + badge_id = fields.BigIntField(pk=True) # 자동 증가하는 기본 키 + badge_code = fields.CharField(max_length=255, null=True) # 배지 코드 + user = fields.ForeignKeyField("models.User", related_name="badges", null=True) + + class Meta: + table = "badge" + + @classmethod + async def get_badge_count_and_codes_by_user_id( + cls, + uuid_bytes: bytes, + ) -> dict: + hex_data = uuid_bytes.hex() + query = f""" + SELECT COUNT(*) as badge_count, GROUP_CONCAT(badge_code) as badge_codes + FROM badge + WHERE user_id = UNHEX('{hex_data}') + """ + + result = await QueryExecutor.execute_query(query, fetch_type="multiple") + print(result) + if result and len(result) > 0: + return ( + result[0].get("badge_count", 0), + result[0].get("badge_code", ""), + ) + + return {"badge_count": 0, "badge_codes": ""} diff --git a/src/app/v2/levels/dtos/level_dto.py b/src/app/v2/levels/dtos/level_dto.py new file mode 100644 index 0000000..581882b --- /dev/null +++ b/src/app/v2/levels/dtos/level_dto.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class LevelDto(BaseModel): + level: int + current_exp: int diff --git a/src/app/v2/users/schemas/__init__.py b/src/app/v2/mobiles/dtos/__init__.py similarity index 100% rename from src/app/v2/users/schemas/__init__.py rename to src/app/v2/mobiles/dtos/__init__.py diff --git a/src/app/v2/mobiles/dtos/responses.py b/src/app/v2/mobiles/dtos/responses.py new file mode 100644 index 0000000..2ac6b84 --- /dev/null +++ b/src/app/v2/mobiles/dtos/responses.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + +from app.v2.levels.dtos.level_dto import LevelDto +from app.v2.users.dtos.user_profile_dto import UserProfileDto +from common.base_models.base_dtos.base_response import BaseResponseDTO + + +class UserProfileWithLevel(BaseModel): + userProfile: UserProfileDto + level: LevelDto + + +class UserProfileWithLevelResponseDTO(BaseResponseDTO): + data: UserProfileWithLevel diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 8912dc1..7e674f5 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -1,6 +1,18 @@ -from fastapi import APIRouter +import asyncio -router = APIRouter(prefix="/mobiles", tags=["Mobile"]) +from fastapi import APIRouter, status + +from app.v2.answers.models.answer import Answer +from app.v2.badges.models.badge import Badge +from app.v2.levels.dtos.level_dto import LevelDto +from app.v2.mobiles.dtos.responses import ( + UserProfileWithLevelResponseDTO, + UserProfileWithLevel, +) +from app.v2.users.dtos.user_profile_dto import UserProfileDto +from app.v2.users.models.user import User + +router = APIRouter(prefix="/mobiles", tags=["모바일 화면용 컨트롤러"]) @router.post("/main") @@ -13,6 +25,35 @@ async def mobile_teller_card_handler(): pass -@router.get("/mypage") +@router.get( + "/mypage", + response_model=UserProfileWithLevelResponseDTO, + status_code=status.HTTP_200_OK, + deprecated="마이페이지 UI용 API입니다", +) async def mobile_my_page_handler(): - pass + uuid_bytes = b"\x18\nN@b\xf8F\xbe\xb1\xeb\xe7\xe3\xdd\x91\xcd\xdf" + + user, answer_count, (badge_count, badge_code) = await asyncio.gather( + User.get_by_user_id(uuid_bytes=uuid_bytes), + Answer.get_answer_count_by_user_id(uuid_bytes=uuid_bytes), + Badge.get_badge_count_and_codes_by_user_id(uuid_bytes=uuid_bytes), + ) + + # cheese Balance, level, current_exp 필요 -> Mission 쪽 + user_profile_data = UserProfileWithLevel( + userProfile=UserProfileDto( + nickname=user.nickname, + badgeCode=badge_code, + cheeseBalance=1000, + badgeCount=badge_count, + answerCount=answer_count, + premium=user.is_premium, + ), + level=LevelDto(level=1, current_exp=10), + ) + return UserProfileWithLevelResponseDTO( + code=status.HTTP_200_OK, + message="정상처리되었습니다", + data=user_profile_data, + ) diff --git a/src/app/v2/payments/models/cheese_manager.py b/src/app/v2/payments/models/cheese_manager.py new file mode 100644 index 0000000..b28600f --- /dev/null +++ b/src/app/v2/payments/models/cheese_manager.py @@ -0,0 +1,9 @@ +from tortoise import fields +from tortoise.models import Model + + +class CheeseManager(Model): + cheese_manager_id = fields.BigIntField(pk=True) + + class Meta: + table = "cheese_manager" diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py new file mode 100644 index 0000000..7936b4e --- /dev/null +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -0,0 +1,11 @@ +from tortoise import fields +from tortoise.models import Model + + +class TellerCard(Model): + teller_card_id = fields.BigIntField(pk=True) + activate_badge_code = fields.CharField(max_length=255, null=True) + activate_color_code = fields.CharField(max_length=255, null=True) + + class Meta: + table = "teller_card" diff --git a/src/app/v2/users/dtos/__init__.py b/src/app/v2/users/dtos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/users/dtos/user_profile_dto.py b/src/app/v2/users/dtos/user_profile_dto.py new file mode 100644 index 0000000..0d26729 --- /dev/null +++ b/src/app/v2/users/dtos/user_profile_dto.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class UserProfileDto(BaseModel): + nickname: str + badgeCode: str + cheeseBalance: int + badgeCount: int + answerCount: int + premium: bool diff --git a/src/app/v2/users/models/refresh_token.py b/src/app/v2/users/models/refresh_token.py new file mode 100644 index 0000000..fdeea95 --- /dev/null +++ b/src/app/v2/users/models/refresh_token.py @@ -0,0 +1,14 @@ +from tortoise import fields +from tortoise.models import Model + + +class RefreshToken(Model): + refresh_token_id = fields.BigIntField(pk=True) + access_token = fields.CharField(max_length=255) + refresh_token = fields.CharField(max_length=255) + user_id = fields.BinaryField( + max_length=16 + ) # foreign key로 사용되지는 않지만 binary field로 선언 + + class Meta: + table = "refresh_token" diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py new file mode 100644 index 0000000..b0e2018 --- /dev/null +++ b/src/app/v2/users/models/user.py @@ -0,0 +1,60 @@ +from tortoise import fields +from tortoise.models import Model + + +class User(Model): + user_id = fields.CharField(max_length=54, pk=True) + allow_notification = fields.BooleanField(null=True) + birth_date = fields.CharField(max_length=8, null=True) + created_time = fields.DatetimeField(auto_now_add=True) + gender = fields.CharField(max_length=16, null=True) + job = fields.IntField() + mbti = fields.CharField(max_length=8, null=True) + nickname = fields.CharField(max_length=16) + purpose = fields.CharField(max_length=16) + push_token = fields.CharField(max_length=255, null=True) + social_id = fields.CharField(max_length=255) + social_login_type = fields.CharField(max_length=16) + user_status = fields.BooleanField() + withdraw_period = fields.DatetimeField(null=True) + refresh_token = fields.ForeignKeyField( + "models.RefreshToken", + related_name="users", + db_column="refresh_token_id", + null=True, + ) + is_premium = fields.BooleanField() + profile_url = fields.CharField( + max_length=255, + default="https://miro.medium.com/v2/resize:fit:1400/format:webp/1*dh7Xy5tFvRj7n2wf1UweAw.png", + ) + premium_started_at = fields.DatetimeField(null=True) + user_exp = fields.IntField(null=True) + user_level = fields.IntField(null=True) + cheese_manager = fields.ForeignKeyField( + "models.CheeseManager", + related_name="users", + db_column="cheese_manager_id", + null=True, + ) + teller_card = fields.ForeignKeyField( + "models.TellerCard", + related_name="users", + db_column="teller_card_id", + null=True, + ) + + class Meta: + table = "user" + + @classmethod + async def get_by_user_id(cls, uuid_bytes: bytes) -> "User": + hex_data = uuid_bytes.hex() + print(hex_data) + users = await cls.raw( + f"SELECT * FROM user WHERE user_id = 0x{hex_data} LIMIT 1" + ) + if users: + return users[0] # 첫 번째 결과 반환 + else: + return None diff --git a/src/app/v2/users/models/user_mission.py b/src/app/v2/users/models/user_mission.py new file mode 100644 index 0000000..5ffdc52 --- /dev/null +++ b/src/app/v2/users/models/user_mission.py @@ -0,0 +1,13 @@ +from tortoise import fields +from tortoise.models import Model + + +class UserMission(Model): + user_mission_id = fields.BigIntField(pk=True) + is_completed = fields.BooleanField(null=True) + mission_code = fields.CharField(max_length=255, null=True) + progress_count = fields.IntField(null=True) + user_id = fields.ForeignKeyField("models.User", related_name="missions", null=True) + + class Meta: + table = "user_mission" diff --git a/src/common/base_models/__init__.py b/src/common/base_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/base_models/base_dtos/__init__.py b/src/common/base_models/base_dtos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/base_models/base_dtos/base_response.py b/src/common/base_models/base_dtos/base_response.py new file mode 100644 index 0000000..43bf9d8 --- /dev/null +++ b/src/common/base_models/base_dtos/base_response.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel +from typing import Any, Optional + + +# 공통 응답 모델 정의 +class BaseResponseDTO(BaseModel): + code: int + message: str + data: Optional[Any] = None diff --git a/src/common/base_models/custom_fields/__init__.py b/src/common/base_models/custom_fields/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/base_models/custom_fields/hex_binary_field.py b/src/common/base_models/custom_fields/hex_binary_field.py new file mode 100644 index 0000000..f0bc606 --- /dev/null +++ b/src/common/base_models/custom_fields/hex_binary_field.py @@ -0,0 +1,39 @@ +from tortoise import fields +import uuid +import binascii + + +class HexBinaryField(fields.BinaryField): + """ + 바이트 배열을 16진수로 변환하여 처리하는 커스텀 필드 + """ + + def to_db_value(self, value, instance): + """ + 데이터베이스로 저장될 때 바이트 배열로 변환 + """ + if isinstance(value, uuid.UUID): + return value.bytes # UUID 객체를 바이트 배열로 변환 + elif isinstance(value, str): + return uuid.UUID(value).bytes # 문자열을 UUID로 변환하여 바이트 배열로 변환 + elif isinstance(value, bytes): + return value # 이미 바이트 배열이면 그대로 반환 + raise ValueError(f"Unsupported value type: {type(value)}") + + def to_python_value(self, value): + """ + 데이터베이스에서 읽어올 때 바이트 배열을 16진수로 변환하여 반환 + """ + if isinstance(value, bytes): + return binascii.hexlify(value).decode( + "utf-8" + ) # 바이트 배열을 16진수로 변환하여 반환 + return value + + def get_db_value(self, value, instance): + """ + 쿼리에서 바이트 배열을 16진수로 변환하여 사용 + """ + if isinstance(value, bytes): + return f"0x{binascii.hexlify(value).decode('utf-8')}" # 바이트 배열을 16진수로 변환하여 쿼리에 사용 + return value diff --git a/src/common/query_executor.py b/src/common/query_executor.py new file mode 100644 index 0000000..93560c0 --- /dev/null +++ b/src/common/query_executor.py @@ -0,0 +1,30 @@ +from typing import List, Dict, Union +from tortoise import Tortoise + + +class QueryExecutor: + + @staticmethod + async def execute_query( + query: str, fetch_type: str = "multiple" + ) -> Union[int, List[Dict]]: + """ + SQL 쿼리를 실행하고 결과를 반환합니다. + + :param query: 실행할 SQL 쿼리 문자열 + :param fetch_type: "single"일 경우 단일 값을 반환하고, "multiple"일 경우 여러 값을 반환 + :return: 단일 값 또는 여러 값(딕셔너리 리스트) + """ + connection = Tortoise.get_connection("default") + try: + result = await connection.execute_query(query) + + if result and len(result[1]) > 0: + if fetch_type == "single": + return result[1][0].get(list(result[1][0].keys())[0], 0) + elif fetch_type == "multiple": + return result[1] + return 0 if fetch_type == "single" else [] + + finally: + await connection.close() diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index f934f05..abfce1a 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -6,6 +6,13 @@ TORTOISE_APP_MODELS = [ "app.v2.questions.models.question", + "app.v2.users.models.user", + "app.v2.users.models.user_mission", + "app.v2.users.models.refresh_token", + "app.v2.badges.models.badge", + "app.v2.answers.models.answer", + "app.v2.payments.models.cheese_manager", + "app.v2.teller_cards.models.teller_card", ] TORTOISE_ORM = { From 2ee2d08a4d8e3d1ef1bdeeef6ed5419a0646c8d2 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Mon, 14 Oct 2024 02:51:22 +0900 Subject: [PATCH 05/60] =?UTF-8?q?=E2=9C=A8=20=20feat:=20tellercard=20ui=20?= =?UTF-8?q?page=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/badges/dtos/badge_dto.py | 16 ++++++ src/app/v2/badges/dtos/response.py | 6 +++ src/app/v2/badges/models/badge.py | 33 ++++++++++++- src/app/v2/badges/router.py | 34 +++++++++++++ src/app/v2/colors/dtos/color_dto.py | 5 ++ src/app/v2/colors/models/color.py | 23 +++++++++ src/app/v2/levels/dtos/level_dto.py | 2 +- src/app/v2/mobiles/dtos/mypage_response.py | 14 ++++++ src/app/v2/mobiles/dtos/responses.py | 14 ------ .../v2/mobiles/dtos/teller_card_response.py | 20 ++++++++ src/app/v2/mobiles/router.py | 37 ++++++++++---- .../{schemas => dtos}/__init__.py | 0 .../v2/teller_cards/dtos/teller_card_dto.py | 8 +++ src/app/v2/users/dtos/user_info_dto.py | 9 ++++ src/app/v2/users/dtos/user_profile_dto.py | 2 +- src/app/v2/users/models/user.py | 49 +++++++++++++++++++ src/common/handlers/router_handler.py | 2 + src/core/database/database_settings.py | 1 + 18 files changed, 248 insertions(+), 27 deletions(-) create mode 100644 src/app/v2/badges/dtos/badge_dto.py create mode 100644 src/app/v2/badges/dtos/response.py create mode 100644 src/app/v2/colors/dtos/color_dto.py create mode 100644 src/app/v2/colors/models/color.py create mode 100644 src/app/v2/mobiles/dtos/mypage_response.py delete mode 100644 src/app/v2/mobiles/dtos/responses.py create mode 100644 src/app/v2/mobiles/dtos/teller_card_response.py rename src/app/v2/teller_cards/{schemas => dtos}/__init__.py (100%) create mode 100644 src/app/v2/teller_cards/dtos/teller_card_dto.py create mode 100644 src/app/v2/users/dtos/user_info_dto.py diff --git a/src/app/v2/badges/dtos/badge_dto.py b/src/app/v2/badges/dtos/badge_dto.py new file mode 100644 index 0000000..0d3d0e0 --- /dev/null +++ b/src/app/v2/badges/dtos/badge_dto.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel + + +class BadgeCodeDTO(BaseModel): + badgeCode: str + + +class BadgeDTO(BaseModel): + badgeCode: str + badgeName: str + badgeMiddleName: str + badgeCondition: str + + +class BadgeListDTO(BaseModel): + badges: list[BadgeDTO] diff --git a/src/app/v2/badges/dtos/response.py b/src/app/v2/badges/dtos/response.py new file mode 100644 index 0000000..0f1f22e --- /dev/null +++ b/src/app/v2/badges/dtos/response.py @@ -0,0 +1,6 @@ +from app.v2.badges.dtos.badge_dto import BadgeListDTO +from common.base_models.base_dtos.base_response import BaseResponseDTO + + +class BadgeListResponseDTO(BaseResponseDTO): + data: BadgeListDTO diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index e6c4cc8..3bf41c8 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -1,7 +1,7 @@ from tortoise import fields from tortoise.models import Model -from uuid import UUID +from app.v2.badges.dtos.badge_dto import BadgeCodeDTO from common.query_executor import QueryExecutor @@ -34,3 +34,34 @@ async def get_badge_count_and_codes_by_user_id( ) return {"badge_count": 0, "badge_codes": ""} + + @classmethod + async def get_badges_with_details_by_user_id( + cls, + uuid_bytes: bytes, + ) -> list: + hex_data = uuid_bytes.hex() # uuid_bytes를 16진수로 변환 + query = f""" + SELECT + b.badge_code, + bi.badge_name, + bi.badge_condition, + bi.badge_middle_name + FROM badge b + JOIN badge_inventory bi ON b.badge_code = bi.badge_code + WHERE b.user_id = UNHEX('{hex_data}') + """ + + result = await QueryExecutor.execute_query(query, fetch_type="multiple") + + if result and len(result) > 0: + return result # 배지 정보를 딕셔너리 리스트로 반환 + + return [] # 결과가 없을 경우 빈 리스트 반환 + + @classmethod + async def get_badge_codes_by_user_id(cls, uuid_bytes: bytes) -> list[BadgeCodeDTO]: + hex_data = uuid_bytes.hex() + query = f"SELECT badge_code FROM badge WHERE user_id = UNHEX('{hex_data}')" + badges = await QueryExecutor.execute_query(query, fetch_type="multiple") + return [BadgeCodeDTO(badgeCode=badge.get("badge_code")) for badge in badges] diff --git a/src/app/v2/badges/router.py b/src/app/v2/badges/router.py index e69de29..39f6bea 100644 --- a/src/app/v2/badges/router.py +++ b/src/app/v2/badges/router.py @@ -0,0 +1,34 @@ +from fastapi import APIRouter, status + +from app.v2.badges.dtos.badge_dto import BadgeDTO, BadgeListDTO +from app.v2.badges.dtos.response import BadgeListResponseDTO +from app.v2.badges.models.badge import Badge + +router = APIRouter(prefix="/user/badge", tags=["Badge"]) + + +@router.get( + "/", + response_model=BadgeListResponseDTO, + status_code=status.HTTP_200_OK, +) +async def get_user_badge_handler(): + uuid_bytes = b"\x18\nN@b\xf8F\xbe\xb1\xeb\xe7\xe3\xdd\x91\xcd\xdf" + + badge_list = await Badge.get_badges_with_details_by_user_id(uuid_bytes=uuid_bytes) + + badges = [ + BadgeDTO( + badgeCode=item.get("badge_code"), + badgeName=item.get("badge_name"), + badgeMiddleName=item.get("badge_middle_name"), + badgeCondition=item.get("badge_condition"), + ) + for item in badge_list + ] + + return BadgeListResponseDTO( + code=status.HTTP_200_OK, + message="보유 뱃지 정보 조회", + data=BadgeListDTO(badges=badges), + ) diff --git a/src/app/v2/colors/dtos/color_dto.py b/src/app/v2/colors/dtos/color_dto.py new file mode 100644 index 0000000..241f00c --- /dev/null +++ b/src/app/v2/colors/dtos/color_dto.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ColorCodeDTO(BaseModel): + colorCode: str diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py new file mode 100644 index 0000000..3340af1 --- /dev/null +++ b/src/app/v2/colors/models/color.py @@ -0,0 +1,23 @@ +from tortoise import fields +from tortoise.models import Model + +from app.v2.colors.dtos.color_dto import ColorCodeDTO +from common.query_executor import QueryExecutor + + +class Color(Model): + color_id = fields.BigIntField(pk=True) # 자동 증가하는 기본 키 + color_code = fields.CharField(max_length=255, null=True) # 색상 코드 + user = fields.ForeignKeyField( + "models.User", related_name="colors", null=True, on_delete=fields.SET_NULL + ) + + class Meta: + table = "color" + + @classmethod + async def get_color_codes_by_user_id(cls, uuid_bytes: bytes) -> list[ColorCodeDTO]: + hex_data = uuid_bytes.hex() + query = f"SELECT color_code FROM color WHERE user_id = UNHEX('{hex_data}')" + colors = await QueryExecutor.execute_query(query, fetch_type="multiple") + return [ColorCodeDTO(colorCode=color.get("color_code")) for color in colors] diff --git a/src/app/v2/levels/dtos/level_dto.py b/src/app/v2/levels/dtos/level_dto.py index 581882b..7d8aebf 100644 --- a/src/app/v2/levels/dtos/level_dto.py +++ b/src/app/v2/levels/dtos/level_dto.py @@ -1,6 +1,6 @@ from pydantic import BaseModel -class LevelDto(BaseModel): +class LevelDTO(BaseModel): level: int current_exp: int diff --git a/src/app/v2/mobiles/dtos/mypage_response.py b/src/app/v2/mobiles/dtos/mypage_response.py new file mode 100644 index 0000000..ab066fc --- /dev/null +++ b/src/app/v2/mobiles/dtos/mypage_response.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + +from app.v2.levels.dtos.level_dto import LevelDTO +from app.v2.users.dtos.user_profile_dto import UserProfileDTO +from common.base_models.base_dtos.base_response import BaseResponseDTO + + +class UserProfileWithLevel(BaseModel): + userProfile: UserProfileDTO + level: LevelDTO + + +class MyPageResponseDTO(BaseResponseDTO): + data: UserProfileWithLevel diff --git a/src/app/v2/mobiles/dtos/responses.py b/src/app/v2/mobiles/dtos/responses.py deleted file mode 100644 index 2ac6b84..0000000 --- a/src/app/v2/mobiles/dtos/responses.py +++ /dev/null @@ -1,14 +0,0 @@ -from pydantic import BaseModel - -from app.v2.levels.dtos.level_dto import LevelDto -from app.v2.users.dtos.user_profile_dto import UserProfileDto -from common.base_models.base_dtos.base_response import BaseResponseDTO - - -class UserProfileWithLevel(BaseModel): - userProfile: UserProfileDto - level: LevelDto - - -class UserProfileWithLevelResponseDTO(BaseResponseDTO): - data: UserProfileWithLevel diff --git a/src/app/v2/mobiles/dtos/teller_card_response.py b/src/app/v2/mobiles/dtos/teller_card_response.py new file mode 100644 index 0000000..eee34b5 --- /dev/null +++ b/src/app/v2/mobiles/dtos/teller_card_response.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel +from typing import List, Optional + +from app.v2.badges.dtos.badge_dto import BadgeCodeDTO +from app.v2.colors.dtos.color_dto import ColorCodeDTO +from app.v2.levels.dtos.level_dto import LevelDTO + +from app.v2.users.dtos.user_info_dto import UserInfoDTO + + +class DataDTO(BaseModel): + badges: List[BadgeCodeDTO] + colors: List[ColorCodeDTO] + userInfo: UserInfoDTO + level: LevelDTO + + +# 최종 응답 DTO +class TellerCardResponseDTO(BaseModel): + data: DataDTO diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 7e674f5..dd2708d 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -4,12 +4,15 @@ from app.v2.answers.models.answer import Answer from app.v2.badges.models.badge import Badge -from app.v2.levels.dtos.level_dto import LevelDto -from app.v2.mobiles.dtos.responses import ( - UserProfileWithLevelResponseDTO, +from app.v2.colors.models.color import Color +from app.v2.levels.dtos.level_dto import LevelDTO + +from app.v2.mobiles.dtos.mypage_response import ( UserProfileWithLevel, + MyPageResponseDTO, ) -from app.v2.users.dtos.user_profile_dto import UserProfileDto +from app.v2.mobiles.dtos.teller_card_response import DataDTO, TellerCardResponseDTO +from app.v2.users.dtos.user_profile_dto import UserProfileDTO from app.v2.users.models.user import User router = APIRouter(prefix="/mobiles", tags=["모바일 화면용 컨트롤러"]) @@ -22,14 +25,28 @@ async def mobile_main_handler(): @router.get("/tellercard") async def mobile_teller_card_handler(): - pass + uuid_bytes = b"\x18\nN@b\xf8F\xbe\xb1\xeb\xe7\xe3\xdd\x91\xcd\xdf" + # user_info -> cheese balance 처리만 하면 된다 + badges, colors, user_info, level_info = await asyncio.gather( + Badge.get_badge_codes_by_user_id(uuid_bytes), + Color.get_color_codes_by_user_id(uuid_bytes), + User.get_user_info_by_user_id(uuid_bytes), + User.get_level_info_by_user_id(uuid_bytes), + ) + + data = DataDTO(badges=badges, colors=colors, userInfo=user_info, level=level_info) + + return TellerCardResponseDTO( + code=status.HTTP_200_OK, + data=data, + message="teller_card ui page", + ) @router.get( "/mypage", - response_model=UserProfileWithLevelResponseDTO, + response_model=MyPageResponseDTO, status_code=status.HTTP_200_OK, - deprecated="마이페이지 UI용 API입니다", ) async def mobile_my_page_handler(): uuid_bytes = b"\x18\nN@b\xf8F\xbe\xb1\xeb\xe7\xe3\xdd\x91\xcd\xdf" @@ -42,7 +59,7 @@ async def mobile_my_page_handler(): # cheese Balance, level, current_exp 필요 -> Mission 쪽 user_profile_data = UserProfileWithLevel( - userProfile=UserProfileDto( + userProfile=UserProfileDTO( nickname=user.nickname, badgeCode=badge_code, cheeseBalance=1000, @@ -50,9 +67,9 @@ async def mobile_my_page_handler(): answerCount=answer_count, premium=user.is_premium, ), - level=LevelDto(level=1, current_exp=10), + level=LevelDTO(level=user.user_level, current_exp=user.user_exp), ) - return UserProfileWithLevelResponseDTO( + return MyPageResponseDTO( code=status.HTTP_200_OK, message="정상처리되었습니다", data=user_profile_data, diff --git a/src/app/v2/teller_cards/schemas/__init__.py b/src/app/v2/teller_cards/dtos/__init__.py similarity index 100% rename from src/app/v2/teller_cards/schemas/__init__.py rename to src/app/v2/teller_cards/dtos/__init__.py diff --git a/src/app/v2/teller_cards/dtos/teller_card_dto.py b/src/app/v2/teller_cards/dtos/teller_card_dto.py new file mode 100644 index 0000000..4b4c10b --- /dev/null +++ b/src/app/v2/teller_cards/dtos/teller_card_dto.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + + +class TellerCardDTO(BaseModel): + badgeCode: str + badgeName: str + badgeMiddleName: str + colorCode: str diff --git a/src/app/v2/users/dtos/user_info_dto.py b/src/app/v2/users/dtos/user_info_dto.py new file mode 100644 index 0000000..f60bae4 --- /dev/null +++ b/src/app/v2/users/dtos/user_info_dto.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + +from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO + + +class UserInfoDTO(BaseModel): + nickname: str + cheeseBalance: int + tellerCard: TellerCardDTO diff --git a/src/app/v2/users/dtos/user_profile_dto.py b/src/app/v2/users/dtos/user_profile_dto.py index 0d26729..bd51ebc 100644 --- a/src/app/v2/users/dtos/user_profile_dto.py +++ b/src/app/v2/users/dtos/user_profile_dto.py @@ -1,7 +1,7 @@ from pydantic import BaseModel -class UserProfileDto(BaseModel): +class UserProfileDTO(BaseModel): nickname: str badgeCode: str cheeseBalance: int diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index b0e2018..21e5d05 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -1,6 +1,11 @@ from tortoise import fields from tortoise.models import Model +from app.v2.levels.dtos.level_dto import LevelDTO +from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO +from app.v2.users.dtos.user_info_dto import UserInfoDTO +from common.query_executor import QueryExecutor + class User(Model): user_id = fields.CharField(max_length=54, pk=True) @@ -58,3 +63,47 @@ async def get_by_user_id(cls, uuid_bytes: bytes) -> "User": return users[0] # 첫 번째 결과 반환 else: return None + + @classmethod + async def get_user_info_by_user_id(cls, uuid_bytes: bytes) -> UserInfoDTO: + hex_data = uuid_bytes.hex() + query = f""" + SELECT + u.nickname, + u.cheese_balance, + tc.activate_badge_code AS badgeCode, + bi.badge_name AS badgeName, + bi.badge_middle_name AS badgeMiddleName, + tc.activate_color_code AS colorCode + FROM user u + JOIN teller_card tc ON u.teller_card_id = tc.teller_card_id + JOIN badge_inventory bi ON tc.activate_badge_code = bi.badge_code + WHERE u.user_id = UNHEX('{hex_data}') + """ + result = await QueryExecutor.execute_query(query, fetch_type="single") + if result: + user = result[0] + teller_card = TellerCardDTO( + badgeCode=user.get("badgeCode"), + badgeName=user.get("badgeName"), + badgeMiddleName=user.get("badgeMiddleName"), + colorCode=user.get("colorCode"), + ) + return UserInfoDTO( + nickname=user.get("nickname"), + cheeseBalance=user.get("cheese_balance"), + tellerCard=teller_card, + ) + return None + + @classmethod + async def get_level_info_by_user_id(cls, uuid_bytes: bytes) -> LevelDTO: + hex_data = uuid_bytes.hex() + query = f"SELECT user_level AS level, user_exp AS current_exp FROM user WHERE user_id = UNHEX('{hex_data}')" + result = await QueryExecutor.execute_query(query, fetch_type="multiple") + if result: + level_data = result[0] + return LevelDTO( + level=level_data.get("level"), current_exp=level_data.get("current_exp") + ) + return None diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index d8e9cda..e74d600 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -1,7 +1,9 @@ from app.v2.mobiles.router import router as mobile_router from app.v2.questions.router import router as question_router +from app.v2.badges.router import router as badge_router def attach_router_handlers(app): app.include_router(router=mobile_router, prefix="/api/v2") + app.include_router(router=badge_router, prefix="/api/v2") app.include_router(router=question_router, prefix="/api/v2") diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index abfce1a..4019e45 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -10,6 +10,7 @@ "app.v2.users.models.user_mission", "app.v2.users.models.refresh_token", "app.v2.badges.models.badge", + "app.v2.colors.models.color", "app.v2.answers.models.answer", "app.v2.payments.models.cheese_manager", "app.v2.teller_cards.models.teller_card", From c8009100c5b362a758d195407167309e73247549 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Tue, 15 Oct 2024 02:42:15 +0900 Subject: [PATCH 06/60] =?UTF-8?q?=E2=9C=A8=20=20feat:=20Query=20fomatter?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20raw=20query=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/models/answer.py | 20 +++-- .../{repositorys => querys}/__init__.py | 0 src/app/v2/answers/querys/answer_query.py | 5 ++ src/app/v2/badges/models/badge.py | 75 ++++++++---------- .../{repositorys => querys}/__init__.py | 0 src/app/v2/badges/querys/badge_query.py | 24 ++++++ src/app/v2/colors/models/color.py | 14 ++-- .../{repositorys => querys}/__init__.py | 0 src/app/v2/colors/querys/color_query.py | 7 ++ .../v2/mobiles/dtos/teller_card_response.py | 3 +- src/app/v2/mobiles/router.py | 51 ++++++++++--- src/app/v2/users/models/user.py | 76 +++++++------------ .../users/{repositorys => querys}/__init__.py | 0 src/app/v2/users/querys/user_query.py | 25 ++++++ .../base_models/base_dtos/base_response.py | 2 + src/common/utils/__init__.py | 0 src/common/{ => utils}/query_executor.py | 19 +++-- src/common/utils/query_formatter.py | 23 ++++++ src/main.py | 9 +++ 19 files changed, 225 insertions(+), 128 deletions(-) rename src/app/v2/answers/{repositorys => querys}/__init__.py (100%) create mode 100644 src/app/v2/answers/querys/answer_query.py rename src/app/v2/badges/{repositorys => querys}/__init__.py (100%) create mode 100644 src/app/v2/badges/querys/badge_query.py rename src/app/v2/colors/{repositorys => querys}/__init__.py (100%) create mode 100644 src/app/v2/colors/querys/color_query.py rename src/app/v2/users/{repositorys => querys}/__init__.py (100%) create mode 100644 src/app/v2/users/querys/user_query.py create mode 100644 src/common/utils/__init__.py rename src/common/{ => utils}/query_executor.py (57%) create mode 100644 src/common/utils/query_formatter.py diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py index 9894174..46d02d0 100644 --- a/src/app/v2/answers/models/answer.py +++ b/src/app/v2/answers/models/answer.py @@ -1,8 +1,8 @@ -from fastapi import Depends -from tortoise import fields, Tortoise +from tortoise import fields from tortoise.models import Model -from common.query_executor import QueryExecutor +from app.v2.answers.querys.answer_query import SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY +from common.utils.query_executor import QueryExecutor class Answer(Model): @@ -28,13 +28,11 @@ class Answer(Model): class Meta: table = "answer" + # 기존 get_answer_count_by_user_id 메서드 @classmethod - async def get_answer_count_by_user_id( - cls, - uuid_bytes: bytes, - ) -> int: - hex_data = uuid_bytes.hex() # uuid_bytes를 16진수 문자열로 변환 - query = ( - f"SELECT COUNT(*) as answer_count FROM answer WHERE user_id = 0x{hex_data}" + async def get_answer_count_by_user_id(cls, user_id: str) -> int: + query = SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="single" ) - return await QueryExecutor.execute_query(query, fetch_type="single") diff --git a/src/app/v2/answers/repositorys/__init__.py b/src/app/v2/answers/querys/__init__.py similarity index 100% rename from src/app/v2/answers/repositorys/__init__.py rename to src/app/v2/answers/querys/__init__.py diff --git a/src/app/v2/answers/querys/answer_query.py b/src/app/v2/answers/querys/answer_query.py new file mode 100644 index 0000000..c6cb9b7 --- /dev/null +++ b/src/app/v2/answers/querys/answer_query.py @@ -0,0 +1,5 @@ +from app.v2.users.querys.user_query import USER_ID_QUERY + +SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY = ( + f"SELECT COUNT(*) as answer_count FROM answer WHERE {USER_ID_QUERY}" +) diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index 3bf41c8..84f95f5 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -2,7 +2,13 @@ from tortoise.models import Model from app.v2.badges.dtos.badge_dto import BadgeCodeDTO -from common.query_executor import QueryExecutor +from app.v2.badges.querys.badge_query import ( + SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY, + SELECT_BADGE_BY_USER_UUID_QUERY, + SELECT_BADGE_CODE_BY_USER_UUID_QUERY, +) + +from common.utils.query_executor import QueryExecutor class Badge(Model): @@ -14,54 +20,35 @@ class Meta: table = "badge" @classmethod - async def get_badge_count_and_codes_by_user_id( - cls, - uuid_bytes: bytes, - ) -> dict: - hex_data = uuid_bytes.hex() - query = f""" - SELECT COUNT(*) as badge_count, GROUP_CONCAT(badge_code) as badge_codes - FROM badge - WHERE user_id = UNHEX('{hex_data}') - """ + async def get_badge_count_and_codes_by_user_id(cls, user_id: str) -> tuple: - result = await QueryExecutor.execute_query(query, fetch_type="multiple") - print(result) - if result and len(result) > 0: - return ( - result[0].get("badge_count", 0), - result[0].get("badge_code", ""), - ) + query = SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY + value = user_id + result = await QueryExecutor.execute_query( + query, values=value, fetch_type="multiple" + ) - return {"badge_count": 0, "badge_codes": ""} + return ( + (result[0].get("badge_count", 0), result[0].get("badge_code", "")) + if result and len(result) > 0 + else (0, "") + ) @classmethod - async def get_badges_with_details_by_user_id( - cls, - uuid_bytes: bytes, - ) -> list: - hex_data = uuid_bytes.hex() # uuid_bytes를 16진수로 변환 - query = f""" - SELECT - b.badge_code, - bi.badge_name, - bi.badge_condition, - bi.badge_middle_name - FROM badge b - JOIN badge_inventory bi ON b.badge_code = bi.badge_code - WHERE b.user_id = UNHEX('{hex_data}') - """ - - result = await QueryExecutor.execute_query(query, fetch_type="multiple") + async def get_badges_with_details_by_user_id(cls, user_id: str) -> list: + query = SELECT_BADGE_BY_USER_UUID_QUERY + value = user_id - if result and len(result) > 0: - return result # 배지 정보를 딕셔너리 리스트로 반환 + result = await QueryExecutor.execute_query( + query, values=value, fetch_type="multiple" + ) - return [] # 결과가 없을 경우 빈 리스트 반환 + return result if result else [] @classmethod - async def get_badge_codes_by_user_id(cls, uuid_bytes: bytes) -> list[BadgeCodeDTO]: - hex_data = uuid_bytes.hex() - query = f"SELECT badge_code FROM badge WHERE user_id = UNHEX('{hex_data}')" - badges = await QueryExecutor.execute_query(query, fetch_type="multiple") - return [BadgeCodeDTO(badgeCode=badge.get("badge_code")) for badge in badges] + async def get_badge_codes_by_user_id(cls, user_id: str) -> list[dict]: + query = SELECT_BADGE_CODE_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="multiple" + ) diff --git a/src/app/v2/badges/repositorys/__init__.py b/src/app/v2/badges/querys/__init__.py similarity index 100% rename from src/app/v2/badges/repositorys/__init__.py rename to src/app/v2/badges/querys/__init__.py diff --git a/src/app/v2/badges/querys/badge_query.py b/src/app/v2/badges/querys/badge_query.py new file mode 100644 index 0000000..17a9037 --- /dev/null +++ b/src/app/v2/badges/querys/badge_query.py @@ -0,0 +1,24 @@ +from app.v2.users.querys.user_query import USER_ID_QUERY + +SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY = f""" + SELECT COUNT(*) as badge_count, GROUP_CONCAT(badge_code) as badge_codes + FROM badge + WHERE {USER_ID_QUERY} +""" + +SELECT_BADGE_BY_USER_UUID_QUERY = f""" + SELECT + b.badge_code, + bi.badge_name, + bi.badge_condition, + bi.badge_middle_name + FROM badge b + JOIN badge_inventory bi ON b.badge_code = bi.badge_code + WHERE {USER_ID_QUERY} +""" + +SELECT_BADGE_CODE_BY_USER_UUID_QUERY = f""" + SELECT badge_code + FROM badge + WHERE {USER_ID_QUERY} +""" diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 3340af1..49d8989 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -2,7 +2,8 @@ from tortoise.models import Model from app.v2.colors.dtos.color_dto import ColorCodeDTO -from common.query_executor import QueryExecutor +from app.v2.colors.querys.color_query import SELECT_COLOR_CODE_BY_USER_UUID_QUERY +from common.utils.query_executor import QueryExecutor class Color(Model): @@ -16,8 +17,9 @@ class Meta: table = "color" @classmethod - async def get_color_codes_by_user_id(cls, uuid_bytes: bytes) -> list[ColorCodeDTO]: - hex_data = uuid_bytes.hex() - query = f"SELECT color_code FROM color WHERE user_id = UNHEX('{hex_data}')" - colors = await QueryExecutor.execute_query(query, fetch_type="multiple") - return [ColorCodeDTO(colorCode=color.get("color_code")) for color in colors] + async def get_color_codes_by_user_id(cls, user_id: str) -> list[dict]: + query = SELECT_COLOR_CODE_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="multiple" + ) diff --git a/src/app/v2/colors/repositorys/__init__.py b/src/app/v2/colors/querys/__init__.py similarity index 100% rename from src/app/v2/colors/repositorys/__init__.py rename to src/app/v2/colors/querys/__init__.py diff --git a/src/app/v2/colors/querys/color_query.py b/src/app/v2/colors/querys/color_query.py new file mode 100644 index 0000000..387dcea --- /dev/null +++ b/src/app/v2/colors/querys/color_query.py @@ -0,0 +1,7 @@ +from app.v2.users.querys.user_query import USER_ID_QUERY + +SELECT_COLOR_CODE_BY_USER_UUID_QUERY = f""" + SELECT color_code + FROM color + WHERE {USER_ID_QUERY} +""" diff --git a/src/app/v2/mobiles/dtos/teller_card_response.py b/src/app/v2/mobiles/dtos/teller_card_response.py index eee34b5..7fbd37b 100644 --- a/src/app/v2/mobiles/dtos/teller_card_response.py +++ b/src/app/v2/mobiles/dtos/teller_card_response.py @@ -6,6 +6,7 @@ from app.v2.levels.dtos.level_dto import LevelDTO from app.v2.users.dtos.user_info_dto import UserInfoDTO +from common.base_models.base_dtos.base_response import BaseResponseDTO class DataDTO(BaseModel): @@ -16,5 +17,5 @@ class DataDTO(BaseModel): # 최종 응답 DTO -class TellerCardResponseDTO(BaseModel): +class TellerCardResponseDTO(BaseResponseDTO): data: DataDTO diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index dd2708d..7ed0eab 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -1,9 +1,12 @@ import asyncio +import uuid from fastapi import APIRouter, status from app.v2.answers.models.answer import Answer +from app.v2.badges.dtos.badge_dto import BadgeCodeDTO from app.v2.badges.models.badge import Badge +from app.v2.colors.dtos.color_dto import ColorCodeDTO from app.v2.colors.models.color import Color from app.v2.levels.dtos.level_dto import LevelDTO @@ -12,6 +15,8 @@ MyPageResponseDTO, ) from app.v2.mobiles.dtos.teller_card_response import DataDTO, TellerCardResponseDTO +from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO +from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO from app.v2.users.models.user import User @@ -25,13 +30,39 @@ async def mobile_main_handler(): @router.get("/tellercard") async def mobile_teller_card_handler(): - uuid_bytes = b"\x18\nN@b\xf8F\xbe\xb1\xeb\xe7\xe3\xdd\x91\xcd\xdf" + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + # user_info -> cheese balance 처리만 하면 된다 - badges, colors, user_info, level_info = await asyncio.gather( - Badge.get_badge_codes_by_user_id(uuid_bytes), - Color.get_color_codes_by_user_id(uuid_bytes), - User.get_user_info_by_user_id(uuid_bytes), - User.get_level_info_by_user_id(uuid_bytes), + badges_raw, colors_raw, user_info_raw, level_info_raw = await asyncio.gather( + Badge.get_badge_codes_by_user_id(user_id=user_id), + Color.get_color_codes_by_user_id(user_id=user_id), + User.get_user_info_by_user_id(user_id=user_id), + User.get_level_info_by_user_id(user_id=user_id), + ) + + badges: list[BadgeCodeDTO] = [ + BadgeCodeDTO(badgeCode=badge.get("badge_code")) for badge in badges_raw + ] + colors: list[ColorCodeDTO] = [ + ColorCodeDTO(colorCode=color.get("color_code")) for color in colors_raw + ] + + user = user_info_raw + teller_card = TellerCardDTO( + badgeCode=user.get("badgeCode"), + badgeName=user.get("badgeName"), + badgeMiddleName=user.get("badgeMiddleName"), + colorCode=user.get("colorCode"), + ) + + user_info = UserInfoDTO( + nickname=user.get("nickname"), + cheeseBalance=user.get("cheese_balance"), + tellerCard=teller_card, + ) + + level_info: LevelDTO = LevelDTO( + level=level_info_raw.get("level"), current_exp=level_info_raw.get("current_exp") ) data = DataDTO(badges=badges, colors=colors, userInfo=user_info, level=level_info) @@ -49,12 +80,12 @@ async def mobile_teller_card_handler(): status_code=status.HTTP_200_OK, ) async def mobile_my_page_handler(): - uuid_bytes = b"\x18\nN@b\xf8F\xbe\xb1\xeb\xe7\xe3\xdd\x91\xcd\xdf" + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" user, answer_count, (badge_count, badge_code) = await asyncio.gather( - User.get_by_user_id(uuid_bytes=uuid_bytes), - Answer.get_answer_count_by_user_id(uuid_bytes=uuid_bytes), - Badge.get_badge_count_and_codes_by_user_id(uuid_bytes=uuid_bytes), + User.get_by_user_id(user_id=user_id), + Answer.get_answer_count_by_user_id(user_id=user_id), + Badge.get_badge_count_and_codes_by_user_id(user_id=user_id), ) # cheese Balance, level, current_exp 필요 -> Mission 쪽 diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index 21e5d05..cda2084 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -4,11 +4,17 @@ from app.v2.levels.dtos.level_dto import LevelDTO from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO from app.v2.users.dtos.user_info_dto import UserInfoDTO -from common.query_executor import QueryExecutor +from app.v2.users.querys.user_query import ( + SELECT_USER_BY_UUID_QUERY, + SELECT_USER_INFO_BY_USER_UUID_QUERY, + SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, +) +from common.utils.query_executor import QueryExecutor +from common.utils.query_formatter import QueryFormatter class User(Model): - user_id = fields.CharField(max_length=54, pk=True) + user_id = fields.BinaryField(pk=True) # BINARY(16)로 저장 allow_notification = fields.BooleanField(null=True) birth_date = fields.CharField(max_length=8, null=True) created_time = fields.DatetimeField(auto_now_add=True) @@ -53,57 +59,29 @@ class Meta: table = "user" @classmethod - async def get_by_user_id(cls, uuid_bytes: bytes) -> "User": - hex_data = uuid_bytes.hex() - print(hex_data) - users = await cls.raw( - f"SELECT * FROM user WHERE user_id = 0x{hex_data} LIMIT 1" + async def get_by_user_id(cls, user_id: str) -> "User": + query = QueryFormatter.format( + query_template=SELECT_USER_BY_UUID_QUERY, values=user_id ) + users = await cls.raw(query) if users: return users[0] # 첫 번째 결과 반환 - else: - return None + return None @classmethod - async def get_user_info_by_user_id(cls, uuid_bytes: bytes) -> UserInfoDTO: - hex_data = uuid_bytes.hex() - query = f""" - SELECT - u.nickname, - u.cheese_balance, - tc.activate_badge_code AS badgeCode, - bi.badge_name AS badgeName, - bi.badge_middle_name AS badgeMiddleName, - tc.activate_color_code AS colorCode - FROM user u - JOIN teller_card tc ON u.teller_card_id = tc.teller_card_id - JOIN badge_inventory bi ON tc.activate_badge_code = bi.badge_code - WHERE u.user_id = UNHEX('{hex_data}') - """ - result = await QueryExecutor.execute_query(query, fetch_type="single") - if result: - user = result[0] - teller_card = TellerCardDTO( - badgeCode=user.get("badgeCode"), - badgeName=user.get("badgeName"), - badgeMiddleName=user.get("badgeMiddleName"), - colorCode=user.get("colorCode"), - ) - return UserInfoDTO( - nickname=user.get("nickname"), - cheeseBalance=user.get("cheese_balance"), - tellerCard=teller_card, - ) - return None + async def get_user_info_by_user_id(cls, user_id: str) -> dict | None: + query = SELECT_USER_INFO_BY_USER_UUID_QUERY + value = user_id + result = await QueryExecutor.execute_query( + query, values=value, fetch_type="single" + ) + return result[0] if result else None @classmethod - async def get_level_info_by_user_id(cls, uuid_bytes: bytes) -> LevelDTO: - hex_data = uuid_bytes.hex() - query = f"SELECT user_level AS level, user_exp AS current_exp FROM user WHERE user_id = UNHEX('{hex_data}')" - result = await QueryExecutor.execute_query(query, fetch_type="multiple") - if result: - level_data = result[0] - return LevelDTO( - level=level_data.get("level"), current_exp=level_data.get("current_exp") - ) - return None + async def get_level_info_by_user_id(cls, user_id: str) -> dict | None: + query = SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY + value = user_id + result = await QueryExecutor.execute_query( + query, values=value, fetch_type="multiple" + ) + return result[0] if result else None diff --git a/src/app/v2/users/repositorys/__init__.py b/src/app/v2/users/querys/__init__.py similarity index 100% rename from src/app/v2/users/repositorys/__init__.py rename to src/app/v2/users/querys/__init__.py diff --git a/src/app/v2/users/querys/user_query.py b/src/app/v2/users/querys/user_query.py new file mode 100644 index 0000000..86e4acf --- /dev/null +++ b/src/app/v2/users/querys/user_query.py @@ -0,0 +1,25 @@ +USER_ID_QUERY = "user_id = UNHEX(REPLACE(%s, '-', ''))" + +SELECT_USER_BY_UUID_QUERY = f"SELECT * FROM user WHERE {USER_ID_QUERY} LIMIT 1" + +SELECT_USER_INFO_BY_USER_UUID_QUERY = f""" + SELECT + u.nickname, + u.cheese_balance, + tc.activate_badge_code AS badgeCode, + bi.badge_name AS badgeName, + bi.badge_middle_name AS badgeMiddleName, + tc.activate_color_code AS colorCode + FROM user u + JOIN teller_card tc ON u.teller_card_id = tc.teller_card_id + JOIN badge_inventory bi ON tc.activate_badge_code = bi.badge_code + WHERE {USER_ID_QUERY} +""" + +SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY = f""" + SELECT + user_level AS level, + user_exp AS current_exp + FROM user + WHERE {USER_ID_QUERY} +""" diff --git a/src/common/base_models/base_dtos/base_response.py b/src/common/base_models/base_dtos/base_response.py index 43bf9d8..552e537 100644 --- a/src/common/base_models/base_dtos/base_response.py +++ b/src/common/base_models/base_dtos/base_response.py @@ -7,3 +7,5 @@ class BaseResponseDTO(BaseModel): code: int message: str data: Optional[Any] = None + + # @classmethod() diff --git a/src/common/utils/__init__.py b/src/common/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/query_executor.py b/src/common/utils/query_executor.py similarity index 57% rename from src/common/query_executor.py rename to src/common/utils/query_executor.py index 93560c0..6158af4 100644 --- a/src/common/query_executor.py +++ b/src/common/utils/query_executor.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Union +from typing import Union from tortoise import Tortoise @@ -6,24 +6,29 @@ class QueryExecutor: @staticmethod async def execute_query( - query: str, fetch_type: str = "multiple" - ) -> Union[int, List[Dict]]: + query: str, values: Union[tuple, str] = (), fetch_type: str = "multiple" + ) -> Union[int, list[dict]]: """ SQL 쿼리를 실행하고 결과를 반환합니다. :param query: 실행할 SQL 쿼리 문자열 + :param values: 쿼리에 바인딩할 값들 (tuple 또는 단일 값) :param fetch_type: "single"일 경우 단일 값을 반환하고, "multiple"일 경우 여러 값을 반환 :return: 단일 값 또는 여러 값(딕셔너리 리스트) """ connection = Tortoise.get_connection("default") + + if not isinstance(values, tuple): + values = (values,) + try: - result = await connection.execute_query(query) + result = await connection.execute_query_dict(query, values) - if result and len(result[1]) > 0: + if result and len(result) > 0: if fetch_type == "single": - return result[1][0].get(list(result[1][0].keys())[0], 0) + return result[0].get(list(result[0].keys())[0], 0) elif fetch_type == "multiple": - return result[1] + return result return 0 if fetch_type == "single" else [] finally: diff --git a/src/common/utils/query_formatter.py b/src/common/utils/query_formatter.py new file mode 100644 index 0000000..4b7d746 --- /dev/null +++ b/src/common/utils/query_formatter.py @@ -0,0 +1,23 @@ +from typing import Union + + +class QueryFormatter: + @staticmethod + def format(query_template: str, values: Union[str, list, tuple]) -> str: + """ + 쿼리 템플릿과 단일 값 또는 리스트/튜플 형태의 값을 받아 SQL 쿼리를 포맷팅하는 메서드 (%s 사용) + + :param query_template: SQL 쿼리 템플릿 + :param values: 단일 값 또는 리스트/튜플 + :return: 포맷팅된 SQL 쿼리 + """ + # 단일 값일 경우 문자열 포맷팅 처리 + if isinstance(values, (list, tuple)): + formatted_values = tuple( + f"'{value}'" if isinstance(value, str) else value for value in values + ) + return query_template % formatted_values + else: + # 단일 문자열일 경우 따옴표 추가 + formatted_value = f"'{values}'" if isinstance(values, str) else values + return query_template % formatted_value diff --git a/src/main.py b/src/main.py index 720eb4d..6fff125 100644 --- a/src/main.py +++ b/src/main.py @@ -2,6 +2,15 @@ from common.post_construct import post_construct +import logging + +# 로깅 설정 +logging.basicConfig(level=logging.DEBUG) + +# Tortoise ORM의 쿼리 로깅 활성화 +db_client_logger = logging.getLogger("tortoise.db_client") +db_client_logger.setLevel(logging.DEBUG) + app = FastAPI() post_construct(app) From dce88894a0998d51a416b6d527555b701af553af Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 19 Oct 2024 17:52:11 +0900 Subject: [PATCH 07/60] =?UTF-8?q?=E2=9C=A8=20feat:=20teller=20card=20&=20m?= =?UTF-8?q?ypage=20Ui=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/badges/models/badge.py | 1 - .../__init__.py | 0 .../models}/__init__.py | 0 .../cheese_managers/models/cheese_manager.py | 44 +++++++++++++ .../cheese_managers/models/cheese_status.py | 7 ++ .../querys}/__init__.py | 0 src/app/v2/levels/models/level.py | 24 +++++++ src/app/v2/levels/querys/__init__.py | 0 src/app/v2/levels/querys/level_query.py | 12 ++++ src/app/v2/mobiles/router.py | 65 ++++++++++++------- src/app/v2/payments/querys/__init__.py | 0 src/app/v2/teller_cards/models/teller_card.py | 13 ++++ src/app/v2/teller_cards/querys/__init__.py | 0 .../teller_cards/querys/teller_card_query.py | 16 +++++ src/app/v2/users/models/user.py | 39 ++++------- src/app/v2/users/querys/user_query.py | 19 ++---- src/common/utils/query_executor.py | 20 +++--- src/core/database/database_settings.py | 6 +- 18 files changed, 190 insertions(+), 76 deletions(-) rename src/app/v2/{levels/repositorys => cheese_managers}/__init__.py (100%) rename src/app/v2/{payments/repositorys => cheese_managers/models}/__init__.py (100%) create mode 100644 src/app/v2/cheese_managers/models/cheese_manager.py create mode 100644 src/app/v2/cheese_managers/models/cheese_status.py rename src/app/v2/{teller_cards/repositorys => cheese_managers/querys}/__init__.py (100%) create mode 100644 src/app/v2/levels/models/level.py create mode 100644 src/app/v2/levels/querys/__init__.py create mode 100644 src/app/v2/levels/querys/level_query.py create mode 100644 src/app/v2/payments/querys/__init__.py create mode 100644 src/app/v2/teller_cards/querys/__init__.py create mode 100644 src/app/v2/teller_cards/querys/teller_card_query.py diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index 84f95f5..308b632 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -1,7 +1,6 @@ from tortoise import fields from tortoise.models import Model -from app.v2.badges.dtos.badge_dto import BadgeCodeDTO from app.v2.badges.querys.badge_query import ( SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY, SELECT_BADGE_BY_USER_UUID_QUERY, diff --git a/src/app/v2/levels/repositorys/__init__.py b/src/app/v2/cheese_managers/__init__.py similarity index 100% rename from src/app/v2/levels/repositorys/__init__.py rename to src/app/v2/cheese_managers/__init__.py diff --git a/src/app/v2/payments/repositorys/__init__.py b/src/app/v2/cheese_managers/models/__init__.py similarity index 100% rename from src/app/v2/payments/repositorys/__init__.py rename to src/app/v2/cheese_managers/models/__init__.py diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py new file mode 100644 index 0000000..1dc7bc5 --- /dev/null +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -0,0 +1,44 @@ +from tortoise import fields +from tortoise.expressions import Q +from tortoise.functions import Sum +from tortoise.models import Model + +from app.v2.cheese_managers.models.cheese_status import CheeseStatus + + +class CheeseManager(Model): + cheese_manager_id = fields.BigIntField(pk=True) # BIGINT auto_increment equivalent + + class Meta: + table = "cheese_manager" # Database table name + + async def get_total_cheese_amount_by_manager(cheese_manager_id: int): + result = ( + await CheeseHistory.filter( + Q(status=CheeseStatus.CAN_USE) | Q(status=CheeseStatus.USING), + cheese_manager_id=cheese_manager_id, + ) + .annotate(total_cheese_amount=Sum("current_amount")) + .values("total_cheese_amount") + ) + + return result[0].get("total_cheese_amount", 0) + + +class CheeseHistory(Model): + cheese_history_id = fields.BigIntField(pk=True) + status = fields.CharEnumField(CheeseStatus, max_length=50, null=True) # Enum Field + current_amount = fields.IntField() + starting_amount = fields.IntField() + cheese_manager = fields.ForeignKeyField( + "models.CheeseManager", + related_name="histories", + null=True, + on_delete=fields.CASCADE, + ) + + class Meta: + table = "cheese_history" + + class Meta: + table = "cheese_history" # Database table name diff --git a/src/app/v2/cheese_managers/models/cheese_status.py b/src/app/v2/cheese_managers/models/cheese_status.py new file mode 100644 index 0000000..653694e --- /dev/null +++ b/src/app/v2/cheese_managers/models/cheese_status.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class CheeseStatus(Enum): + CAN_USE = "CAN_USE" # 치즈 사용 가능 + USING = "USING" # 치즈 사용 중 + ALREADY_USED = "ALREADY_USED" # 치즈 사용 완료 diff --git a/src/app/v2/teller_cards/repositorys/__init__.py b/src/app/v2/cheese_managers/querys/__init__.py similarity index 100% rename from src/app/v2/teller_cards/repositorys/__init__.py rename to src/app/v2/cheese_managers/querys/__init__.py diff --git a/src/app/v2/levels/models/level.py b/src/app/v2/levels/models/level.py new file mode 100644 index 0000000..2bf1705 --- /dev/null +++ b/src/app/v2/levels/models/level.py @@ -0,0 +1,24 @@ +from tortoise import fields +from tortoise.models import Model + +from app.v2.levels.querys.level_query import ( + SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, +) +from common.utils.query_executor import QueryExecutor + + +class Level(Model): + level_id = fields.BigIntField(pk=True) # BIGINT auto_increment equivalent + user_exp = fields.IntField() # Experience points field + user_level = fields.IntField() # User level field + + class Meta: + table = "level" + + @classmethod + async def get_level_info_by_user_id(cls, user_id: str) -> dict | None: + query = SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="single" + ) diff --git a/src/app/v2/levels/querys/__init__.py b/src/app/v2/levels/querys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/levels/querys/level_query.py b/src/app/v2/levels/querys/level_query.py new file mode 100644 index 0000000..279b231 --- /dev/null +++ b/src/app/v2/levels/querys/level_query.py @@ -0,0 +1,12 @@ +from app.v2.users.querys.user_query import USER_ID_QUERY + +SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY = f""" + SELECT + l.user_exp AS level_exp, + l.user_level AS level_level + FROM + user u + JOIN + level l ON u.level_id = l.level_id + WHERE {USER_ID_QUERY} +""" diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 7ed0eab..8cd1b0b 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -1,14 +1,15 @@ import asyncio -import uuid -from fastapi import APIRouter, status +from fastapi import APIRouter, status, HTTPException from app.v2.answers.models.answer import Answer from app.v2.badges.dtos.badge_dto import BadgeCodeDTO from app.v2.badges.models.badge import Badge +from app.v2.cheese_managers.models.cheese_manager import CheeseManager from app.v2.colors.dtos.color_dto import ColorCodeDTO from app.v2.colors.models.color import Color from app.v2.levels.dtos.level_dto import LevelDTO +from app.v2.levels.models.level import Level from app.v2.mobiles.dtos.mypage_response import ( UserProfileWithLevel, @@ -16,6 +17,7 @@ ) from app.v2.mobiles.dtos.teller_card_response import DataDTO, TellerCardResponseDTO from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO +from app.v2.teller_cards.models.teller_card import TellerCard from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO from app.v2.users.models.user import User @@ -32,12 +34,21 @@ async def mobile_main_handler(): async def mobile_teller_card_handler(): user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - # user_info -> cheese balance 처리만 하면 된다 - badges_raw, colors_raw, user_info_raw, level_info_raw = await asyncio.gather( - Badge.get_badge_codes_by_user_id(user_id=user_id), - Color.get_color_codes_by_user_id(user_id=user_id), - User.get_user_info_by_user_id(user_id=user_id), - User.get_level_info_by_user_id(user_id=user_id), + try: + badges_raw, colors_raw, level_info_raw, teller_cards_raw, user_raw = ( + await asyncio.gather( + Badge.get_badge_codes_by_user_id(user_id=user_id), + Color.get_color_codes_by_user_id(user_id=user_id), + Level.get_level_info_by_user_id(user_id=user_id), + TellerCard.get_teller_card_info_by_user_id(user_id=user_id), + User.get_user_info_by_user_id(user_id=user_id), + ) + ) + except Exception as e: + raise HTTPException(status_code=500, detail="내부 서버 오류") + + cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager( + cheese_manager_id=user_raw["cheese_manager_id"] ) badges: list[BadgeCodeDTO] = [ @@ -47,22 +58,22 @@ async def mobile_teller_card_handler(): ColorCodeDTO(colorCode=color.get("color_code")) for color in colors_raw ] - user = user_info_raw teller_card = TellerCardDTO( - badgeCode=user.get("badgeCode"), - badgeName=user.get("badgeName"), - badgeMiddleName=user.get("badgeMiddleName"), - colorCode=user.get("colorCode"), + badgeCode=teller_cards_raw.get("badgeCode"), + badgeName=teller_cards_raw.get("badgeName"), + badgeMiddleName=teller_cards_raw.get("badgeMiddleName"), + colorCode=teller_cards_raw.get("colorCode"), ) user_info = UserInfoDTO( - nickname=user.get("nickname"), - cheeseBalance=user.get("cheese_balance"), + nickname=user_raw.get("nickname"), + cheeseBalance=cheese_amount, tellerCard=teller_card, ) level_info: LevelDTO = LevelDTO( - level=level_info_raw.get("level"), current_exp=level_info_raw.get("current_exp") + level=level_info_raw.get("level_level"), + current_exp=level_info_raw.get("level_exp"), ) data = DataDTO(badges=badges, colors=colors, userInfo=user_info, level=level_info) @@ -82,23 +93,29 @@ async def mobile_teller_card_handler(): async def mobile_my_page_handler(): user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - user, answer_count, (badge_count, badge_code) = await asyncio.gather( - User.get_by_user_id(user_id=user_id), + user, answer_count, (badge_count, badge_code), level = await asyncio.gather( + User.get_user_profile_by_user_id(user_id=user_id), Answer.get_answer_count_by_user_id(user_id=user_id), Badge.get_badge_count_and_codes_by_user_id(user_id=user_id), + Level.get_level_info_by_user_id(user_id=user_id), + ) + + cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager( + cheese_manager_id=user["cheese_manager_id"] ) - # cheese Balance, level, current_exp 필요 -> Mission 쪽 user_profile_data = UserProfileWithLevel( userProfile=UserProfileDTO( - nickname=user.nickname, + nickname=user["nickname"], badgeCode=badge_code, - cheeseBalance=1000, + cheeseBalance=cheese_amount, badgeCount=badge_count, - answerCount=answer_count, - premium=user.is_premium, + answerCount=answer_count["answer_count"], + premium=bool(user["is_premium"]), + ), + level=LevelDTO( + level=level.get("level_level"), current_exp=level.get("level_exp") ), - level=LevelDTO(level=user.user_level, current_exp=user.user_exp), ) return MyPageResponseDTO( code=status.HTTP_200_OK, diff --git a/src/app/v2/payments/querys/__init__.py b/src/app/v2/payments/querys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py index 7936b4e..0c51ffd 100644 --- a/src/app/v2/teller_cards/models/teller_card.py +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -1,6 +1,11 @@ from tortoise import fields from tortoise.models import Model +from app.v2.teller_cards.querys.teller_card_query import ( + SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY, +) +from common.utils.query_executor import QueryExecutor + class TellerCard(Model): teller_card_id = fields.BigIntField(pk=True) @@ -9,3 +14,11 @@ class TellerCard(Model): class Meta: table = "teller_card" + + @classmethod + async def get_teller_card_info_by_user_id(cls, user_id: str) -> dict | None: + query = SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="single" + ) diff --git a/src/app/v2/teller_cards/querys/__init__.py b/src/app/v2/teller_cards/querys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/querys/teller_card_query.py b/src/app/v2/teller_cards/querys/teller_card_query.py new file mode 100644 index 0000000..a05f52f --- /dev/null +++ b/src/app/v2/teller_cards/querys/teller_card_query.py @@ -0,0 +1,16 @@ +from app.v2.users.querys.user_query import USER_ID_QUERY + +SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY = f""" + SELECT + tc.activate_badge_code AS badgeCode, + bi.badge_name AS badgeName, + bi.badge_middle_name AS badgeMiddleName, + tc.activate_color_code AS colorCode + FROM teller_card tc + JOIN badge_inventory bi ON tc.activate_badge_code = bi.badge_code + WHERE tc.teller_card_id = ( + SELECT u.teller_card_id + FROM user u + WHERE {USER_ID_QUERY} + ) +""" diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index cda2084..7ee8b7a 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -1,16 +1,11 @@ from tortoise import fields from tortoise.models import Model -from app.v2.levels.dtos.level_dto import LevelDTO -from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO -from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.querys.user_query import ( - SELECT_USER_BY_UUID_QUERY, SELECT_USER_INFO_BY_USER_UUID_QUERY, - SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, + SELECT_USER_PROFILE_BY_USER_ID_QUERY, ) from common.utils.query_executor import QueryExecutor -from common.utils.query_formatter import QueryFormatter class User(Model): @@ -40,8 +35,6 @@ class User(Model): default="https://miro.medium.com/v2/resize:fit:1400/format:webp/1*dh7Xy5tFvRj7n2wf1UweAw.png", ) premium_started_at = fields.DatetimeField(null=True) - user_exp = fields.IntField(null=True) - user_level = fields.IntField(null=True) cheese_manager = fields.ForeignKeyField( "models.CheeseManager", related_name="users", @@ -54,34 +47,28 @@ class User(Model): db_column="teller_card_id", null=True, ) + level = fields.ForeignKeyField( + "models.Level", + related_name="users", + db_column="level_id", + null=True, + ) class Meta: table = "user" @classmethod - async def get_by_user_id(cls, user_id: str) -> "User": - query = QueryFormatter.format( - query_template=SELECT_USER_BY_UUID_QUERY, values=user_id + async def get_user_profile_by_user_id(cls, user_id: str) -> dict | None: + query = SELECT_USER_PROFILE_BY_USER_ID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="single" ) - users = await cls.raw(query) - if users: - return users[0] # 첫 번째 결과 반환 - return None @classmethod async def get_user_info_by_user_id(cls, user_id: str) -> dict | None: query = SELECT_USER_INFO_BY_USER_UUID_QUERY value = user_id - result = await QueryExecutor.execute_query( + return await QueryExecutor.execute_query( query, values=value, fetch_type="single" ) - return result[0] if result else None - - @classmethod - async def get_level_info_by_user_id(cls, user_id: str) -> dict | None: - query = SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY - value = user_id - result = await QueryExecutor.execute_query( - query, values=value, fetch_type="multiple" - ) - return result[0] if result else None diff --git a/src/app/v2/users/querys/user_query.py b/src/app/v2/users/querys/user_query.py index 86e4acf..ae44e39 100644 --- a/src/app/v2/users/querys/user_query.py +++ b/src/app/v2/users/querys/user_query.py @@ -2,24 +2,19 @@ SELECT_USER_BY_UUID_QUERY = f"SELECT * FROM user WHERE {USER_ID_QUERY} LIMIT 1" -SELECT_USER_INFO_BY_USER_UUID_QUERY = f""" +SELECT_USER_PROFILE_BY_USER_ID_QUERY = f""" SELECT u.nickname, - u.cheese_balance, - tc.activate_badge_code AS badgeCode, - bi.badge_name AS badgeName, - bi.badge_middle_name AS badgeMiddleName, - tc.activate_color_code AS colorCode + u.is_premium, + u.cheese_manager_id FROM user u - JOIN teller_card tc ON u.teller_card_id = tc.teller_card_id - JOIN badge_inventory bi ON tc.activate_badge_code = bi.badge_code WHERE {USER_ID_QUERY} """ -SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY = f""" +SELECT_USER_INFO_BY_USER_UUID_QUERY = f""" SELECT - user_level AS level, - user_exp AS current_exp - FROM user + u.nickname, + u.cheese_manager_id + FROM user u WHERE {USER_ID_QUERY} """ diff --git a/src/common/utils/query_executor.py b/src/common/utils/query_executor.py index 6158af4..3d9e643 100644 --- a/src/common/utils/query_executor.py +++ b/src/common/utils/query_executor.py @@ -21,15 +21,11 @@ async def execute_query( if not isinstance(values, tuple): values = (values,) - try: - result = await connection.execute_query_dict(query, values) - - if result and len(result) > 0: - if fetch_type == "single": - return result[0].get(list(result[0].keys())[0], 0) - elif fetch_type == "multiple": - return result - return 0 if fetch_type == "single" else [] - - finally: - await connection.close() + result = await connection.execute_query_dict(query, values) + + if result and len(result) > 0: + if fetch_type == "single": + return result[0] + elif fetch_type == "multiple": + return result + return 0 if fetch_type == "single" else [] diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index 4019e45..34115da 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -12,8 +12,10 @@ "app.v2.badges.models.badge", "app.v2.colors.models.color", "app.v2.answers.models.answer", - "app.v2.payments.models.cheese_manager", + # "app.v2.payments.models.cheese_manager", "app.v2.teller_cards.models.teller_card", + "app.v2.levels.models.level", + "app.v2.cheese_managers.models.cheese_manager", ] TORTOISE_ORM = { @@ -28,6 +30,8 @@ "database": settings.DB_NAME, "connect_timeout": 5, "charset": settings.DB_CHARSET, + "minsize": 5, # 커넥션 풀 최소 크기 + "maxsize": 10, # 커넥션 풀 최대 크기 }, }, }, From 3208163ad759f0f8eb73b7972195cbe18eb574b8 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 19 Oct 2024 18:54:18 +0900 Subject: [PATCH 08/60] feat: teller_card patch api added --- src/app/v2/badges/models/badge.py | 5 +-- src/app/v2/badges/router.py | 4 +- src/app/v2/questions/router.py | 12 +++--- src/app/v2/teller_cards/dtos/request.py | 6 +++ src/app/v2/teller_cards/dtos/response.py | 6 +++ src/app/v2/teller_cards/models/teller_card.py | 9 +++++ .../teller_cards/querys/teller_card_query.py | 23 ++++++++++++ src/app/v2/teller_cards/router.py | 37 +++++++++++++++++++ src/app/v2/users/models/user.py | 9 +++++ src/app/v2/users/querys/user_query.py | 7 ++++ src/common/handlers/router_handler.py | 2 + src/common/utils/query_executor.py | 4 +- 12 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/app/v2/teller_cards/dtos/request.py create mode 100644 src/app/v2/teller_cards/dtos/response.py create mode 100644 src/app/v2/teller_cards/router.py diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index 308b632..9b9be17 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -37,13 +37,10 @@ async def get_badge_count_and_codes_by_user_id(cls, user_id: str) -> tuple: async def get_badges_with_details_by_user_id(cls, user_id: str) -> list: query = SELECT_BADGE_BY_USER_UUID_QUERY value = user_id - - result = await QueryExecutor.execute_query( + return await QueryExecutor.execute_query( query, values=value, fetch_type="multiple" ) - return result if result else [] - @classmethod async def get_badge_codes_by_user_id(cls, user_id: str) -> list[dict]: query = SELECT_BADGE_CODE_BY_USER_UUID_QUERY diff --git a/src/app/v2/badges/router.py b/src/app/v2/badges/router.py index 39f6bea..05e58a0 100644 --- a/src/app/v2/badges/router.py +++ b/src/app/v2/badges/router.py @@ -13,9 +13,9 @@ status_code=status.HTTP_200_OK, ) async def get_user_badge_handler(): - uuid_bytes = b"\x18\nN@b\xf8F\xbe\xb1\xeb\xe7\xe3\xdd\x91\xcd\xdf" + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - badge_list = await Badge.get_badges_with_details_by_user_id(uuid_bytes=uuid_bytes) + badge_list = await Badge.get_badges_with_details_by_user_id(user_id=user_id) badges = [ BadgeDTO( diff --git a/src/app/v2/questions/router.py b/src/app/v2/questions/router.py index 76ae3f6..0f789d9 100644 --- a/src/app/v2/questions/router.py +++ b/src/app/v2/questions/router.py @@ -5,9 +5,9 @@ router = APIRouter(prefix="/question", tags=["Question"]) -@router.get("/questions/{date}") -async def get_question_by_date(date: str): - question = await Question.get_or_none(date=date) - if question is None: - raise HTTPException(status_code=404, detail="Question not found") - return question +# @router.get("/questions/{date}") +# async def get_question_by_date(date: str): +# question = await Question.get_or_none(date=date) +# if question is None: +# raise HTTPException(status_code=404, detail="Question not found") +# return question diff --git a/src/app/v2/teller_cards/dtos/request.py b/src/app/v2/teller_cards/dtos/request.py new file mode 100644 index 0000000..6bda2a9 --- /dev/null +++ b/src/app/v2/teller_cards/dtos/request.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class TellerCardRequestDTO(BaseModel): + colorCode: str + badgeCode: str diff --git a/src/app/v2/teller_cards/dtos/response.py b/src/app/v2/teller_cards/dtos/response.py new file mode 100644 index 0000000..2418d2a --- /dev/null +++ b/src/app/v2/teller_cards/dtos/response.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class TellerCardResponseDTO(BaseModel): + colorCode: str + badgeCode: str diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py index 0c51ffd..475b2ad 100644 --- a/src/app/v2/teller_cards/models/teller_card.py +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -3,6 +3,7 @@ from app.v2.teller_cards.querys.teller_card_query import ( SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY, + PATCH_TELLER_CARD_BY_USER_UUID_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -22,3 +23,11 @@ async def get_teller_card_info_by_user_id(cls, user_id: str) -> dict | None: return await QueryExecutor.execute_query( query, values=value, fetch_type="single" ) + + @classmethod + async def patch_teller_card_info_by_user_id( + cls, user_id: str, badge_code: str, color_code: str + ) -> None: + query = PATCH_TELLER_CARD_BY_USER_UUID_QUERY + values = (badge_code, color_code, user_id) + await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/teller_cards/querys/teller_card_query.py b/src/app/v2/teller_cards/querys/teller_card_query.py index a05f52f..cf4039f 100644 --- a/src/app/v2/teller_cards/querys/teller_card_query.py +++ b/src/app/v2/teller_cards/querys/teller_card_query.py @@ -14,3 +14,26 @@ WHERE {USER_ID_QUERY} ) """ + +PATCH_TELLER_CARD_QUERY = """ + UPDATE teller_card + SET activate_badge_code = %s, activate_color_code = %s + WHERE teller_card_id = ( + SELECT u.teller_card_id + FROM user u + WHERE {USER_ID_QUERY} + ) +""" + +GET_UPDATED_TELLER_CARD_QUERY = """ + SELECT activate_badge_code AS badgeCode, activate_color_code AS colorCode + FROM teller_card + WHERE teller_card_id = ( + SELECT u.teller_card_id + FROM user u + WHERE {USER_ID_QUERY} + ) +""" +PATCH_TELLER_CARD_BY_USER_UUID_QUERY = PATCH_TELLER_CARD_QUERY.format( + USER_ID_QUERY=USER_ID_QUERY +) diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py new file mode 100644 index 0000000..3327140 --- /dev/null +++ b/src/app/v2/teller_cards/router.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, status + +from app.v2.badges.models.badge import Badge +from app.v2.teller_cards.dtos.response import TellerCardResponseDTO +from app.v2.teller_cards.dtos.request import TellerCardRequestDTO +from app.v2.teller_cards.models.teller_card import TellerCard + + +router = APIRouter(prefix="/tellercard", tags=["TellerCard"]) + + +@router.patch( + "/", + response_model=TellerCardResponseDTO, + status_code=status.HTTP_200_OK, +) +async def patch_teller_card_handler( + body: TellerCardRequestDTO, +) -> TellerCardResponseDTO: + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + badge_code = (body.badgeCode,) + color_code = (body.colorCode,) + + await TellerCard.patch_teller_card_info_by_user_id( + user_id=user_id, + badge_code=badge_code, + color_code=color_code, + ) + + teller_card = await TellerCard.get_teller_card_info_by_user_id(user_id=user_id) + print(teller_card) + + await Badge.get_badge_count_and_codes_by_user_id(user_id) + return TellerCardResponseDTO( + colorCode=body.colorCode, + badgeCode=body.badgeCode, + ) diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index 7ee8b7a..0c92544 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -4,6 +4,7 @@ from app.v2.users.querys.user_query import ( SELECT_USER_INFO_BY_USER_UUID_QUERY, SELECT_USER_PROFILE_BY_USER_ID_QUERY, + SELECT_USER_TELLER_CARD_ID_BY_USER_UUID_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -72,3 +73,11 @@ async def get_user_info_by_user_id(cls, user_id: str) -> dict | None: return await QueryExecutor.execute_query( query, values=value, fetch_type="single" ) + + @classmethod + async def get_teller_card_id_by_user_id(cls, user_id: str) -> int | None: + query = SELECT_USER_TELLER_CARD_ID_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="single" + ) diff --git a/src/app/v2/users/querys/user_query.py b/src/app/v2/users/querys/user_query.py index ae44e39..2883ad1 100644 --- a/src/app/v2/users/querys/user_query.py +++ b/src/app/v2/users/querys/user_query.py @@ -18,3 +18,10 @@ FROM user u WHERE {USER_ID_QUERY} """ + +SELECT_USER_TELLER_CARD_ID_BY_USER_UUID_QUERY = f""" + SELECT + u.teller_card_id + FROM user u + WHERE {USER_ID_QUERY} +""" diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index e74d600..e9c9492 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -1,9 +1,11 @@ from app.v2.mobiles.router import router as mobile_router from app.v2.questions.router import router as question_router from app.v2.badges.router import router as badge_router +from app.v2.teller_cards.router import router as teller_card_router def attach_router_handlers(app): app.include_router(router=mobile_router, prefix="/api/v2") app.include_router(router=badge_router, prefix="/api/v2") app.include_router(router=question_router, prefix="/api/v2") + app.include_router(router=teller_card_router, prefix="/api/v2") diff --git a/src/common/utils/query_executor.py b/src/common/utils/query_executor.py index 3d9e643..178698d 100644 --- a/src/common/utils/query_executor.py +++ b/src/common/utils/query_executor.py @@ -18,7 +18,9 @@ async def execute_query( """ connection = Tortoise.get_connection("default") - if not isinstance(values, tuple): + if isinstance(values, tuple): + values = tuple(v[0] if isinstance(v, tuple) else v for v in values) + else: values = (values,) result = await connection.execute_query_dict(query, values) From f2fc7b388c273345864b95d43e09959c93674e31 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 19 Oct 2024 20:33:37 +0900 Subject: [PATCH 09/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=97=B0=EC=86=8D=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9D=BC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/dtos/answer_dto.py | 10 +++++++ src/app/v2/answers/models/answer.py | 18 +++++++++++- src/app/v2/answers/querys/answer_query.py | 7 +++++ src/app/v2/answers/router.py | 13 +++++++++ src/app/v2/answers/services/answer_service.py | 28 +++++++++++++++++++ src/app/v2/badges/router.py | 2 +- src/app/v2/missions/router.py | 8 ++++++ src/app/v2/mobiles/router.py | 8 +++--- src/app/v2/payments/dtos/request.py | 5 ++++ src/app/v2/payments/models/cheese_manager.py | 9 ------ src/app/v2/payments/router.py | 14 ++++++++++ src/app/v2/purchases/router.py | 0 .../teller_cards/querys/teller_card_query.py | 8 +++--- src/app/v2/teller_cards/router.py | 10 ++++--- src/common/handlers/router_handler.py | 6 ++++ 15 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 src/app/v2/answers/dtos/answer_dto.py create mode 100644 src/app/v2/answers/services/answer_service.py create mode 100644 src/app/v2/payments/dtos/request.py delete mode 100644 src/app/v2/payments/models/cheese_manager.py create mode 100644 src/app/v2/purchases/router.py diff --git a/src/app/v2/answers/dtos/answer_dto.py b/src/app/v2/answers/dtos/answer_dto.py new file mode 100644 index 0000000..931a8fb --- /dev/null +++ b/src/app/v2/answers/dtos/answer_dto.py @@ -0,0 +1,10 @@ +class RecordDto: + def __init__(self, count: int): + self.count = count + + @classmethod + def builder(cls, count: int): + return cls(count) + + def build(self): + return self diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py index 46d02d0..7808bb2 100644 --- a/src/app/v2/answers/models/answer.py +++ b/src/app/v2/answers/models/answer.py @@ -1,7 +1,12 @@ +from datetime import datetime + from tortoise import fields from tortoise.models import Model -from app.v2.answers.querys.answer_query import SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY +from app.v2.answers.querys.answer_query import ( + SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY, + SELECT_ANSWER_BY_USER_UUID_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -36,3 +41,14 @@ async def get_answer_count_by_user_id(cls, user_id: str) -> int: return await QueryExecutor.execute_query( query, values=value, fetch_type="single" ) + + @classmethod + async def find_all_by_user( + cls, user_id: str, start_date: datetime, end_date: datetime + ) -> list[dict] | None: + query = SELECT_ANSWER_BY_USER_UUID_QUERY + values = (user_id, start_date, end_date) + + return await QueryExecutor.execute_query( + query, values=values, fetch_type="multiple" + ) diff --git a/src/app/v2/answers/querys/answer_query.py b/src/app/v2/answers/querys/answer_query.py index c6cb9b7..8d95447 100644 --- a/src/app/v2/answers/querys/answer_query.py +++ b/src/app/v2/answers/querys/answer_query.py @@ -3,3 +3,10 @@ SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY = ( f"SELECT COUNT(*) as answer_count FROM answer WHERE {USER_ID_QUERY}" ) + +SELECT_ANSWER_BY_USER_UUID_QUERY = f""" + SELECT * FROM answer + WHERE {USER_ID_QUERY} + AND date BETWEEN %s AND %s + ORDER BY date DESC + """ diff --git a/src/app/v2/answers/router.py b/src/app/v2/answers/router.py index e69de29..6afeed7 100644 --- a/src/app/v2/answers/router.py +++ b/src/app/v2/answers/router.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from app.v2.answers.services.answer_service import AnswerService + +router = APIRouter(prefix="/answer", tags=["Test용"]) + + +# FastAPI 비동기 뷰 +@router.get("") +async def get_answer_record_view(): + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + record_dto = await AnswerService.get_answer_record(user_id) + return {"user_id": user_id, "consecutive_answer_days": record_dto.count} diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py new file mode 100644 index 0000000..9d5e16a --- /dev/null +++ b/src/app/v2/answers/services/answer_service.py @@ -0,0 +1,28 @@ +from datetime import datetime, timedelta + +from app.v2.answers.dtos.answer_dto import RecordDto +from app.v2.answers.models.answer import Answer + + +class AnswerService: + + @classmethod + async def get_answer_record(cls, user_id: str) -> "RecordDto": + end_date = datetime.now() + start_date = end_date - timedelta(days=100) + + all_answers = await Answer.find_all_by_user(user_id, start_date, end_date) + + record = 0 + target_date = end_date + + if all_answers: + for answer in all_answers: + answer_date = answer["date"] # 답변의 날짜를 가져옴 + if answer_date == target_date: + record += 1 + target_date = target_date - timedelta(days=1) + else: + break + + return RecordDto.builder(count=record).build() diff --git a/src/app/v2/badges/router.py b/src/app/v2/badges/router.py index 05e58a0..f4f3100 100644 --- a/src/app/v2/badges/router.py +++ b/src/app/v2/badges/router.py @@ -8,7 +8,7 @@ @router.get( - "/", + "", response_model=BadgeListResponseDTO, status_code=status.HTTP_200_OK, ) diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index e69de29..528bad1 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter(prefix="/mission", tags=["Mission"]) + + +@router.post("/level") +def level_up_handler(): + pass diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 8cd1b0b..7f2df3f 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -59,10 +59,10 @@ async def mobile_teller_card_handler(): ] teller_card = TellerCardDTO( - badgeCode=teller_cards_raw.get("badgeCode"), - badgeName=teller_cards_raw.get("badgeName"), - badgeMiddleName=teller_cards_raw.get("badgeMiddleName"), - colorCode=teller_cards_raw.get("colorCode"), + badgeCode=teller_cards_raw.get("activate_badge_code"), + badgeName=teller_cards_raw.get("badge_name"), + badgeMiddleName=teller_cards_raw.get("badge_middle_name"), + colorCode=teller_cards_raw.get("activate_color_code"), ) user_info = UserInfoDTO( diff --git a/src/app/v2/payments/dtos/request.py b/src/app/v2/payments/dtos/request.py new file mode 100644 index 0000000..ff79865 --- /dev/null +++ b/src/app/v2/payments/dtos/request.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class PaymentRequestDTO(BaseModel): + productId: str diff --git a/src/app/v2/payments/models/cheese_manager.py b/src/app/v2/payments/models/cheese_manager.py deleted file mode 100644 index b28600f..0000000 --- a/src/app/v2/payments/models/cheese_manager.py +++ /dev/null @@ -1,9 +0,0 @@ -from tortoise import fields -from tortoise.models import Model - - -class CheeseManager(Model): - cheese_manager_id = fields.BigIntField(pk=True) - - class Meta: - table = "cheese_manager" diff --git a/src/app/v2/payments/router.py b/src/app/v2/payments/router.py index e69de29..7b4909f 100644 --- a/src/app/v2/payments/router.py +++ b/src/app/v2/payments/router.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter + +from app.v2.payments.dtos.request import PaymentRequestDTO + +router = APIRouter(prefix="/payment", tags=["Payment"]) + + +@router.post( + "", +) +async def payment_item_handler( + body: PaymentRequestDTO, +): + prodict_id = body.productId diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/teller_cards/querys/teller_card_query.py b/src/app/v2/teller_cards/querys/teller_card_query.py index cf4039f..e8bd424 100644 --- a/src/app/v2/teller_cards/querys/teller_card_query.py +++ b/src/app/v2/teller_cards/querys/teller_card_query.py @@ -2,10 +2,10 @@ SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY = f""" SELECT - tc.activate_badge_code AS badgeCode, - bi.badge_name AS badgeName, - bi.badge_middle_name AS badgeMiddleName, - tc.activate_color_code AS colorCode + tc.activate_badge_code, + bi.badge_name, + bi.badge_middle_name, + tc.activate_color_code FROM teller_card tc JOIN badge_inventory bi ON tc.activate_badge_code = bi.badge_code WHERE tc.teller_card_id = ( diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py index 3327140..69d8b85 100644 --- a/src/app/v2/teller_cards/router.py +++ b/src/app/v2/teller_cards/router.py @@ -10,7 +10,7 @@ @router.patch( - "/", + "", response_model=TellerCardResponseDTO, status_code=status.HTTP_200_OK, ) @@ -28,10 +28,12 @@ async def patch_teller_card_handler( ) teller_card = await TellerCard.get_teller_card_info_by_user_id(user_id=user_id) - print(teller_card) + + activate_badge_code = teller_card["activate_badge_code"] + activate_color_code = teller_card["activate_color_code"] await Badge.get_badge_count_and_codes_by_user_id(user_id) return TellerCardResponseDTO( - colorCode=body.colorCode, - badgeCode=body.badgeCode, + colorCode=activate_color_code, + badgeCode=activate_badge_code, ) diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index e9c9492..dd2f34b 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -2,6 +2,9 @@ from app.v2.questions.router import router as question_router from app.v2.badges.router import router as badge_router from app.v2.teller_cards.router import router as teller_card_router +from app.v2.payments.router import router as payment_router +from app.v2.missions.router import router as mission_router +from app.v2.answers.router import router as answer_router def attach_router_handlers(app): @@ -9,3 +12,6 @@ def attach_router_handlers(app): app.include_router(router=badge_router, prefix="/api/v2") app.include_router(router=question_router, prefix="/api/v2") app.include_router(router=teller_card_router, prefix="/api/v2") + app.include_router(router=payment_router, prefix="/api/v2") + app.include_router(router=mission_router, prefix="/api/v2") + app.include_router(router=answer_router, prefix="/test") From cfc29de41b4fe49439bec1769a18a260a2f8843f Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 20 Oct 2024 17:02:24 +0900 Subject: [PATCH 10/60] =?UTF-8?q?feat:=20level=20up=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/router.py | 9 +++ src/app/v2/badges/dtos/badge_dto.py | 4 ++ src/app/v2/badges/services/badge_service.py | 9 +++ src/app/v2/cheese_managers/dtos/__init__.py | 0 src/app/v2/cheese_managers/dtos/cheese_dto.py | 9 +++ src/app/v2/cheese_managers/router.py | 16 +++++ .../v2/cheese_managers/services/__init__.py | 0 .../services/cheese_service.py | 10 +++ src/app/v2/colors/dtos/color_dto.py | 4 ++ src/app/v2/colors/services/color_service.py | 9 +++ src/app/v2/levels/dtos/level_dto.py | 4 ++ src/app/v2/levels/models/level.py | 18 +++++ src/app/v2/levels/querys/level_query.py | 16 +++++ src/app/v2/levels/services/level_service.py | 52 +++++++++++++++ src/app/v2/mobiles/router.py | 65 ++++++++----------- .../v2/teller_cards/dtos/teller_card_dto.py | 9 +++ .../services/teller_card_service.py | 11 ++++ src/app/v2/users/dtos/user_info_dto.py | 11 +++- src/app/v2/users/services/user_service.py | 14 ++++ src/common/handlers/router_handler.py | 2 + 20 files changed, 232 insertions(+), 40 deletions(-) create mode 100644 src/app/v2/badges/services/badge_service.py create mode 100644 src/app/v2/cheese_managers/dtos/__init__.py create mode 100644 src/app/v2/cheese_managers/dtos/cheese_dto.py create mode 100644 src/app/v2/cheese_managers/router.py create mode 100644 src/app/v2/cheese_managers/services/__init__.py create mode 100644 src/app/v2/cheese_managers/services/cheese_service.py create mode 100644 src/app/v2/colors/services/color_service.py create mode 100644 src/app/v2/levels/services/level_service.py create mode 100644 src/app/v2/teller_cards/services/teller_card_service.py create mode 100644 src/app/v2/users/services/user_service.py diff --git a/src/app/v2/answers/router.py b/src/app/v2/answers/router.py index 6afeed7..94612b0 100644 --- a/src/app/v2/answers/router.py +++ b/src/app/v2/answers/router.py @@ -2,6 +2,8 @@ from app.v2.answers.services.answer_service import AnswerService +from app.v2.levels.services.level_service import LevelService + router = APIRouter(prefix="/answer", tags=["Test용"]) @@ -11,3 +13,10 @@ async def get_answer_record_view(): user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" record_dto = await AnswerService.get_answer_record(user_id) return {"user_id": user_id, "consecutive_answer_days": record_dto.count} + + +@router.get("/level-up") +async def level_up_handler(): + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + result = await LevelService.level_up(user_id=user_id) + return result diff --git a/src/app/v2/badges/dtos/badge_dto.py b/src/app/v2/badges/dtos/badge_dto.py index 0d3d0e0..8026267 100644 --- a/src/app/v2/badges/dtos/badge_dto.py +++ b/src/app/v2/badges/dtos/badge_dto.py @@ -4,6 +4,10 @@ class BadgeCodeDTO(BaseModel): badgeCode: str + @classmethod + def builder(cls, badge_raw: dict) -> "BadgeCodeDTO": + return cls(badgeCode=badge_raw.get("badge_code")) + class BadgeDTO(BaseModel): badgeCode: str diff --git a/src/app/v2/badges/services/badge_service.py b/src/app/v2/badges/services/badge_service.py new file mode 100644 index 0000000..042e996 --- /dev/null +++ b/src/app/v2/badges/services/badge_service.py @@ -0,0 +1,9 @@ +from app.v2.badges.dtos.badge_dto import BadgeCodeDTO +from app.v2.badges.models.badge import Badge + + +class BadgeService: + @classmethod + async def get_badges(cls, user_id: str) -> list[BadgeCodeDTO]: + badges_raw = await Badge.get_badge_codes_by_user_id(user_id=user_id) + return [BadgeCodeDTO.builder(badge) for badge in badges_raw] diff --git a/src/app/v2/cheese_managers/dtos/__init__.py b/src/app/v2/cheese_managers/dtos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/cheese_managers/dtos/cheese_dto.py b/src/app/v2/cheese_managers/dtos/cheese_dto.py new file mode 100644 index 0000000..5e7d44a --- /dev/null +++ b/src/app/v2/cheese_managers/dtos/cheese_dto.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class CheeseResponseDTO(BaseModel): + cheeseBalance: int + + @classmethod + def builder(cls, cheese_balance: int) -> "CheeseResponseDTO": + return cls(cheeseBalance=cheese_balance) diff --git a/src/app/v2/cheese_managers/router.py b/src/app/v2/cheese_managers/router.py new file mode 100644 index 0000000..f75d0f9 --- /dev/null +++ b/src/app/v2/cheese_managers/router.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, status + +from app.v2.cheese_managers.dtos.cheese_dto import CheeseResponseDTO +from app.v2.cheese_managers.services.cheese_service import CheeseService +from app.v2.users.services.user_service import UserService + +router = APIRouter(prefix="/cheese", tags=["Cheese"]) + + +@router.get("", response_model=CheeseResponseDTO, status_code=status.HTTP_200_OK) +async def get_cheese_handler(): + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + user = await UserService.get_user_info(user_id=user_id) + cheese_amount = await CheeseService.get_cheese_balance(user["cheese_manager_id"]) + + return CheeseResponseDTO.builder(cheese_balance=cheese_amount) diff --git a/src/app/v2/cheese_managers/services/__init__.py b/src/app/v2/cheese_managers/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/cheese_managers/services/cheese_service.py b/src/app/v2/cheese_managers/services/cheese_service.py new file mode 100644 index 0000000..a7a2f58 --- /dev/null +++ b/src/app/v2/cheese_managers/services/cheese_service.py @@ -0,0 +1,10 @@ +from app.v2.cheese_managers.models.cheese_manager import CheeseManager + + +class CheeseService: + + @classmethod + async def get_cheese_balance(cls, cheese_manager_id: str) -> int: + return await CheeseManager.get_total_cheese_amount_by_manager( + cheese_manager_id=cheese_manager_id + ) diff --git a/src/app/v2/colors/dtos/color_dto.py b/src/app/v2/colors/dtos/color_dto.py index 241f00c..e153cc0 100644 --- a/src/app/v2/colors/dtos/color_dto.py +++ b/src/app/v2/colors/dtos/color_dto.py @@ -3,3 +3,7 @@ class ColorCodeDTO(BaseModel): colorCode: str + + @classmethod + def builder(cls, color_raw: dict) -> "ColorCodeDTO": + return cls(colorCode=color_raw.get("color_code")) diff --git a/src/app/v2/colors/services/color_service.py b/src/app/v2/colors/services/color_service.py new file mode 100644 index 0000000..120b67f --- /dev/null +++ b/src/app/v2/colors/services/color_service.py @@ -0,0 +1,9 @@ +from app.v2.colors.dtos.color_dto import ColorCodeDTO +from app.v2.colors.models.color import Color + + +class ColorService: + @classmethod + async def get_colors(cls, user_id: str) -> list[ColorCodeDTO]: + colors_raw = await Color.get_color_codes_by_user_id(user_id=user_id) + return [ColorCodeDTO.builder(color) for color in colors_raw] diff --git a/src/app/v2/levels/dtos/level_dto.py b/src/app/v2/levels/dtos/level_dto.py index 7d8aebf..f512950 100644 --- a/src/app/v2/levels/dtos/level_dto.py +++ b/src/app/v2/levels/dtos/level_dto.py @@ -4,3 +4,7 @@ class LevelDTO(BaseModel): level: int current_exp: int + + @classmethod + def builder(cls, level: int, current_exp: int) -> "LevelDTO": + return cls(level=level, current_exp=current_exp) diff --git a/src/app/v2/levels/models/level.py b/src/app/v2/levels/models/level.py index 2bf1705..b19cf08 100644 --- a/src/app/v2/levels/models/level.py +++ b/src/app/v2/levels/models/level.py @@ -3,6 +3,8 @@ from app.v2.levels.querys.level_query import ( SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, + SELECT_USER_EXP_QUERY, + UPDATE_USER_LEVEL_AND_EXP_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -22,3 +24,19 @@ async def get_level_info_by_user_id(cls, user_id: str) -> dict | None: return await QueryExecutor.execute_query( query, values=value, fetch_type="single" ) + + @classmethod + async def get_required_exp_by_user_id(cls, user_id: str) -> dict | None: + query = SELECT_USER_EXP_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=(value,), fetch_type="single" + ) + + @classmethod + async def update_level_and_exp( + cls, user_id: str, new_level: int, new_exp: int + ) -> None: + query = UPDATE_USER_LEVEL_AND_EXP_QUERY + values = (new_level, new_exp, user_id) + await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/levels/querys/level_query.py b/src/app/v2/levels/querys/level_query.py index 279b231..33d04fb 100644 --- a/src/app/v2/levels/querys/level_query.py +++ b/src/app/v2/levels/querys/level_query.py @@ -10,3 +10,19 @@ level l ON u.level_id = l.level_id WHERE {USER_ID_QUERY} """ + +SELECT_USER_EXP_QUERY = f""" +SELECT li.required_exp +FROM user u +JOIN level l ON u.level_id = l.level_id +JOIN level_inventory li ON l.user_level = li.level +WHERE {USER_ID_QUERY} +LIMIT 1; +""" + +UPDATE_USER_LEVEL_AND_EXP_QUERY = f""" + UPDATE level l + JOIN user u ON u.level_id = l.level_id + SET l.user_level = %s, l.user_exp = %s + WHERE {USER_ID_QUERY}; +""" diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py new file mode 100644 index 0000000..3ae204e --- /dev/null +++ b/src/app/v2/levels/services/level_service.py @@ -0,0 +1,52 @@ +import asyncio + +from app.v2.levels.dtos.level_dto import LevelDTO +from app.v2.levels.models.level import Level + + +class LevelService: + @classmethod + async def get_level_info(cls, user_id: str) -> LevelDTO: + # 레벨 정보를 조회하는 로직 + level_info_raw = await Level.get_level_info_by_user_id(user_id=user_id) + return LevelDTO.builder( + level=level_info_raw.get("level_level"), + current_exp=level_info_raw.get("level_exp"), + ) + + @classmethod + async def level_up(cls, user_id: str) -> dict: + """ + 유저가 레벨업 가능한지 확인 후, 레벨업 처리 + """ + level_dto, required_exp_raw = await asyncio.gather( + cls.get_level_info(user_id=user_id), + Level.get_required_exp_by_user_id(user_id=user_id), + ) + + level = level_dto.level + current_exp = level_dto.current_exp + required_exp = required_exp_raw["required_exp"] + + if current_exp >= required_exp: + new_exp = current_exp - required_exp + new_level = level + 1 + + await Level.update_level_and_exp( + user_id=user_id, new_level=new_level, new_exp=new_exp + ) + + return { + "status": "success", + "message": "레벨업 성공", + "new_level": new_level, + "remaining_exp": new_exp, + } + + return { + "status": "failure", + "message": "레벨업에 필요한 경험치가 부족합니다", + "current_level": level, + "current_exp": current_exp, + "required_exp": required_exp, + } diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 7f2df3f..85faa70 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -3,24 +3,28 @@ from fastapi import APIRouter, status, HTTPException from app.v2.answers.models.answer import Answer -from app.v2.badges.dtos.badge_dto import BadgeCodeDTO + from app.v2.badges.models.badge import Badge +from app.v2.badges.services.badge_service import BadgeService from app.v2.cheese_managers.models.cheese_manager import CheeseManager -from app.v2.colors.dtos.color_dto import ColorCodeDTO -from app.v2.colors.models.color import Color +from app.v2.cheese_managers.services.cheese_service import CheeseService + +from app.v2.colors.services.color_service import ColorService from app.v2.levels.dtos.level_dto import LevelDTO from app.v2.levels.models.level import Level +from app.v2.levels.services.level_service import LevelService from app.v2.mobiles.dtos.mypage_response import ( UserProfileWithLevel, MyPageResponseDTO, ) from app.v2.mobiles.dtos.teller_card_response import DataDTO, TellerCardResponseDTO -from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO -from app.v2.teller_cards.models.teller_card import TellerCard + +from app.v2.teller_cards.services.teller_card_service import TellerCardService from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO from app.v2.users.models.user import User +from app.v2.users.services.user_service import UserService router = APIRouter(prefix="/mobiles", tags=["모바일 화면용 컨트롤러"]) @@ -30,50 +34,33 @@ async def mobile_main_handler(): pass -@router.get("/tellercard") +@router.get( + "/tellercard", + response_model=TellerCardResponseDTO, + status_code=status.HTTP_200_OK, +) async def mobile_teller_card_handler(): user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" try: - badges_raw, colors_raw, level_info_raw, teller_cards_raw, user_raw = ( - await asyncio.gather( - Badge.get_badge_codes_by_user_id(user_id=user_id), - Color.get_color_codes_by_user_id(user_id=user_id), - Level.get_level_info_by_user_id(user_id=user_id), - TellerCard.get_teller_card_info_by_user_id(user_id=user_id), - User.get_user_info_by_user_id(user_id=user_id), - ) + badges_task = BadgeService.get_badges(user_id) + colors_task = ColorService.get_colors(user_id) + level_info_task = LevelService.get_level_info(user_id) # LevelService 추가 + teller_card_task = TellerCardService.get_teller_card(user_id) + user_info_task = UserService.get_user_info(user_id) + + badges, colors, level_info, teller_card, user_raw = await asyncio.gather( + badges_task, colors_task, level_info_task, teller_card_task, user_info_task ) except Exception as e: raise HTTPException(status_code=500, detail="내부 서버 오류") - cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager( - cheese_manager_id=user_raw["cheese_manager_id"] - ) - - badges: list[BadgeCodeDTO] = [ - BadgeCodeDTO(badgeCode=badge.get("badge_code")) for badge in badges_raw - ] - colors: list[ColorCodeDTO] = [ - ColorCodeDTO(colorCode=color.get("color_code")) for color in colors_raw - ] - - teller_card = TellerCardDTO( - badgeCode=teller_cards_raw.get("activate_badge_code"), - badgeName=teller_cards_raw.get("badge_name"), - badgeMiddleName=teller_cards_raw.get("badge_middle_name"), - colorCode=teller_cards_raw.get("activate_color_code"), - ) - - user_info = UserInfoDTO( - nickname=user_raw.get("nickname"), - cheeseBalance=cheese_amount, - tellerCard=teller_card, + cheese_amount = await CheeseService.get_cheese_balance( + user_raw["cheese_manager_id"] ) - level_info: LevelDTO = LevelDTO( - level=level_info_raw.get("level_level"), - current_exp=level_info_raw.get("level_exp"), + user_info = UserInfoDTO.builder( + user_raw, cheeseBalance=cheese_amount, tellerCard=teller_card ) data = DataDTO(badges=badges, colors=colors, userInfo=user_info, level=level_info) diff --git a/src/app/v2/teller_cards/dtos/teller_card_dto.py b/src/app/v2/teller_cards/dtos/teller_card_dto.py index 4b4c10b..d29463b 100644 --- a/src/app/v2/teller_cards/dtos/teller_card_dto.py +++ b/src/app/v2/teller_cards/dtos/teller_card_dto.py @@ -6,3 +6,12 @@ class TellerCardDTO(BaseModel): badgeName: str badgeMiddleName: str colorCode: str + + @classmethod + def builder(cls, teller_card_raw: dict) -> "TellerCardDTO": + return cls( + badgeCode=teller_card_raw.get("activate_badge_code"), + badgeName=teller_card_raw.get("badge_name"), + badgeMiddleName=teller_card_raw.get("badge_middle_name"), + colorCode=teller_card_raw.get("activate_color_code"), + ) diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py new file mode 100644 index 0000000..2d9de44 --- /dev/null +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -0,0 +1,11 @@ +from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO +from app.v2.teller_cards.models.teller_card import TellerCard + + +class TellerCardService: + @classmethod + async def get_teller_card(cls, user_id: str) -> TellerCardDTO: + teller_cards_raw = await TellerCard.get_teller_card_info_by_user_id( + user_id=user_id + ) + return TellerCardDTO.builder(teller_cards_raw) diff --git a/src/app/v2/users/dtos/user_info_dto.py b/src/app/v2/users/dtos/user_info_dto.py index f60bae4..1866a1a 100644 --- a/src/app/v2/users/dtos/user_info_dto.py +++ b/src/app/v2/users/dtos/user_info_dto.py @@ -1,5 +1,4 @@ from pydantic import BaseModel - from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO @@ -7,3 +6,13 @@ class UserInfoDTO(BaseModel): nickname: str cheeseBalance: int tellerCard: TellerCardDTO + + @classmethod + def builder( + cls, user_raw: dict, cheeseBalance: int, tellerCard: TellerCardDTO + ) -> "UserInfoDTO": + return cls( + nickname=user_raw.get("nickname"), + cheeseBalance=cheeseBalance, + tellerCard=tellerCard, + ) diff --git a/src/app/v2/users/services/user_service.py b/src/app/v2/users/services/user_service.py new file mode 100644 index 0000000..2d77ae0 --- /dev/null +++ b/src/app/v2/users/services/user_service.py @@ -0,0 +1,14 @@ +from app.v2.cheese_managers.models.cheese_manager import CheeseManager +from app.v2.users.models.user import User + + +class UserService: + @classmethod + async def get_user_info(cls, user_id: str) -> dict: + return await User.get_user_info_by_user_id(user_id=user_id) + + @classmethod + async def get_cheese_balance(cls, cheese_manager_id: str) -> int: + return await CheeseManager.get_total_cheese_amount_by_manager( + cheese_manager_id=cheese_manager_id + ) diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index dd2f34b..aceeff5 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -5,6 +5,7 @@ from app.v2.payments.router import router as payment_router from app.v2.missions.router import router as mission_router from app.v2.answers.router import router as answer_router +from app.v2.cheese_managers.router import router as cheese_router def attach_router_handlers(app): @@ -14,4 +15,5 @@ def attach_router_handlers(app): app.include_router(router=teller_card_router, prefix="/api/v2") app.include_router(router=payment_router, prefix="/api/v2") app.include_router(router=mission_router, prefix="/api/v2") + app.include_router(router=cheese_router, prefix="/api/v2") app.include_router(router=answer_router, prefix="/test") From 38d4bb209468d34dbe4a4880c52b3a6cb01a2202 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Mon, 28 Oct 2024 00:15:59 +0900 Subject: [PATCH 11/60] =?UTF-8?q?=20=E2=9C=A8=20feat:=20color=20&=20emotio?= =?UTF-8?q?n=20=EA=B2=B0=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 치즈, 색상 같은 소모성 상품 결제 로직 추가 - 이미 보유중인 상품 검증 로직 필요 --- src/app/v2/badges/models/badge.py | 7 ++ src/app/v2/badges/querys/badge_query.py | 6 ++ src/app/v2/badges/services/badge_service.py | 4 + .../cheese_managers/models/cheese_manager.py | 45 +++++++++- src/app/v2/colors/models/color.py | 13 ++- src/app/v2/colors/querys/color_query.py | 7 ++ src/app/v2/colors/services/color_service.py | 4 + src/app/v2/emotions/__init__.py | 0 src/app/v2/emotions/dtos/__init__.py | 0 src/app/v2/emotions/models/__init__.py | 0 src/app/v2/emotions/models/emotion.py | 43 +++++++++ src/app/v2/emotions/querys/__init__.py | 0 src/app/v2/emotions/querys/emotion_query.py | 15 ++++ src/app/v2/emotions/services/__init__.py | 0 .../v2/emotions/services/emotion_service.py | 15 ++++ src/app/v2/items/dtos/item_dto.py | 22 +++++ src/app/v2/items/models/item.py | 36 ++++++++ src/app/v2/mobiles/router.py | 4 +- src/app/v2/payments/router.py | 40 +++++++-- .../v2/payments/services/payment_service.py | 89 +++++++++++++++++++ src/app/v2/purchases/router.py | 60 +++++++++++++ .../v2/purchases/services/purchase_service.py | 26 ++++++ src/app/v2/users/services/user_service.py | 4 +- src/common/handlers/router_handler.py | 2 + src/core/database/database_settings.py | 1 + 25 files changed, 424 insertions(+), 19 deletions(-) create mode 100644 src/app/v2/emotions/__init__.py create mode 100644 src/app/v2/emotions/dtos/__init__.py create mode 100644 src/app/v2/emotions/models/__init__.py create mode 100644 src/app/v2/emotions/models/emotion.py create mode 100644 src/app/v2/emotions/querys/__init__.py create mode 100644 src/app/v2/emotions/querys/emotion_query.py create mode 100644 src/app/v2/emotions/services/__init__.py create mode 100644 src/app/v2/emotions/services/emotion_service.py create mode 100644 src/app/v2/items/dtos/item_dto.py create mode 100644 src/app/v2/items/models/item.py create mode 100644 src/app/v2/payments/services/payment_service.py create mode 100644 src/app/v2/purchases/services/purchase_service.py diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index 9b9be17..0761b33 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -5,6 +5,7 @@ SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY, SELECT_BADGE_BY_USER_UUID_QUERY, SELECT_BADGE_CODE_BY_USER_UUID_QUERY, + INSERT_BADGE_CODE_FOR_USER_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -48,3 +49,9 @@ async def get_badge_codes_by_user_id(cls, user_id: str) -> list[dict]: return await QueryExecutor.execute_query( query, values=value, fetch_type="multiple" ) + + @classmethod + async def add_badge(cls, user_id: str, badge_code: str) -> None: + query = INSERT_BADGE_CODE_FOR_USER_QUERY + values = (badge_code, user_id) + await QueryExecutor.execute_query(query, values=values) diff --git a/src/app/v2/badges/querys/badge_query.py b/src/app/v2/badges/querys/badge_query.py index 17a9037..6c01ae9 100644 --- a/src/app/v2/badges/querys/badge_query.py +++ b/src/app/v2/badges/querys/badge_query.py @@ -22,3 +22,9 @@ FROM badge WHERE {USER_ID_QUERY} """ +INSERT_BADGE_CODE_FOR_USER_QUERY = f""" + INSERT INTO badge (badge_code, user_id) + SELECT %s, user_id + FROM user + WHERE {USER_ID_QUERY} + """ diff --git a/src/app/v2/badges/services/badge_service.py b/src/app/v2/badges/services/badge_service.py index 042e996..acb94cf 100644 --- a/src/app/v2/badges/services/badge_service.py +++ b/src/app/v2/badges/services/badge_service.py @@ -7,3 +7,7 @@ class BadgeService: async def get_badges(cls, user_id: str) -> list[BadgeCodeDTO]: badges_raw = await Badge.get_badge_codes_by_user_id(user_id=user_id) return [BadgeCodeDTO.builder(badge) for badge in badges_raw] + + @classmethod + async def add_badge(cls, user_id: str, badge_code: str) -> None: + await Badge.add_badge(user_id=user_id, badge_code=badge_code) diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py index 1dc7bc5..9955ee7 100644 --- a/src/app/v2/cheese_managers/models/cheese_manager.py +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -2,6 +2,7 @@ from tortoise.expressions import Q from tortoise.functions import Sum from tortoise.models import Model +from tortoise.transactions import atomic from app.v2.cheese_managers.models.cheese_status import CheeseStatus @@ -12,6 +13,7 @@ class CheeseManager(Model): class Meta: table = "cheese_manager" # Database table name + @staticmethod async def get_total_cheese_amount_by_manager(cheese_manager_id: int): result = ( await CheeseHistory.filter( @@ -24,6 +26,46 @@ async def get_total_cheese_amount_by_manager(cheese_manager_id: int): return result[0].get("total_cheese_amount", 0) + @staticmethod + async def use_cheese(cheese_manager_id: int, amount: int): + using_cheese = await CheeseHistory.filter( + status=CheeseStatus.USING, cheese_manager_id=cheese_manager_id + ).order_by("cheese_history_id") + + remaining_amount = amount + + for cheese in using_cheese: + if cheese.current_amount >= remaining_amount: + cheese.current_amount -= remaining_amount + if cheese.current_amount == 0: + cheese.status = CheeseStatus.ALREADY_USED + await cheese.save() + return + + remaining_amount -= cheese.current_amount + cheese.current_amount = 0 + cheese.status = CheeseStatus.ALREADY_USED + await cheese.save() + + can_use_cheese = await CheeseHistory.filter( + status=CheeseStatus.CAN_USE, cheese_manager_id=cheese_manager_id + ).order_by("cheese_history_id") + + for cheese in can_use_cheese: + if cheese.current_amount >= remaining_amount: + cheese.current_amount -= remaining_amount + cheese.status = CheeseStatus.USING + await cheese.save() + return + + remaining_amount -= cheese.current_amount + cheese.current_amount = 0 + cheese.status = CheeseStatus.ALREADY_USED + await cheese.save() + + if remaining_amount > 0: + raise ValueError("Not enough cheese to complete the transaction") + class CheeseHistory(Model): cheese_history_id = fields.BigIntField(pk=True) @@ -39,6 +81,3 @@ class CheeseHistory(Model): class Meta: table = "cheese_history" - - class Meta: - table = "cheese_history" # Database table name diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 49d8989..1966878 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -2,7 +2,10 @@ from tortoise.models import Model from app.v2.colors.dtos.color_dto import ColorCodeDTO -from app.v2.colors.querys.color_query import SELECT_COLOR_CODE_BY_USER_UUID_QUERY +from app.v2.colors.querys.color_query import ( + SELECT_COLOR_CODE_BY_USER_UUID_QUERY, + INSERT_COLOR_CODE_FOR_USER_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -23,3 +26,11 @@ async def get_color_codes_by_user_id(cls, user_id: str) -> list[dict]: return await QueryExecutor.execute_query( query, values=value, fetch_type="multiple" ) + + @classmethod + async def add_color_code_for_user(cls, user_id: str, color_code: str) -> dict: + query = INSERT_COLOR_CODE_FOR_USER_QUERY + values = (color_code, user_id) + return await QueryExecutor.execute_query( + query, values=values, fetch_type="single" + ) diff --git a/src/app/v2/colors/querys/color_query.py b/src/app/v2/colors/querys/color_query.py index 387dcea..9dd2c1a 100644 --- a/src/app/v2/colors/querys/color_query.py +++ b/src/app/v2/colors/querys/color_query.py @@ -5,3 +5,10 @@ FROM color WHERE {USER_ID_QUERY} """ + +INSERT_COLOR_CODE_FOR_USER_QUERY = f""" + INSERT INTO color (color_code, user_id) + SELECT %s, user_id + FROM user + WHERE {USER_ID_QUERY} +""" diff --git a/src/app/v2/colors/services/color_service.py b/src/app/v2/colors/services/color_service.py index 120b67f..4d8cacc 100644 --- a/src/app/v2/colors/services/color_service.py +++ b/src/app/v2/colors/services/color_service.py @@ -7,3 +7,7 @@ class ColorService: async def get_colors(cls, user_id: str) -> list[ColorCodeDTO]: colors_raw = await Color.get_color_codes_by_user_id(user_id=user_id) return [ColorCodeDTO.builder(color) for color in colors_raw] + + @classmethod + async def add_color(cls, user_id: str, color_code: str) -> None: + await Color.add_color_code_for_user(user_id=user_id, color_code=color_code) diff --git a/src/app/v2/emotions/__init__.py b/src/app/v2/emotions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/emotions/dtos/__init__.py b/src/app/v2/emotions/dtos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/emotions/models/__init__.py b/src/app/v2/emotions/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/emotions/models/emotion.py b/src/app/v2/emotions/models/emotion.py new file mode 100644 index 0000000..355dd1a --- /dev/null +++ b/src/app/v2/emotions/models/emotion.py @@ -0,0 +1,43 @@ +from tortoise import fields, models + +from app.v2.emotions.querys.emotion_query import ( + INSERT_EMOTION_CODE_FOR_USER_QUERY, + SELECT_EMOTION_CODE_BY_USER_UUID_QUERY, +) +from common.utils.query_executor import QueryExecutor + + +class Emotion(models.Model): + emotion_id = fields.BigIntField(pk=True) + emotion_code = fields.CharField(max_length=255, unique=True) + user = fields.ForeignKeyField("models.User", related_name="emotions") + + class Meta: + table = "emotion" + + @classmethod + async def get_emotions_with_details_by_user_id(cls, user_id: str) -> list[dict]: + query = SELECT_EMOTION_CODE_BY_USER_UUID_QUERY + values = user_id + return await QueryExecutor.execute_query( + query, values=values, fetch_type="multiple" + ) + + @classmethod + async def add_emotion(cls, user_id: str, emotion_code: str) -> None: + query = INSERT_EMOTION_CODE_FOR_USER_QUERY + values = (emotion_code, user_id) + await QueryExecutor.execute_query(query, values=values) + + +class EmotionInventory(models.Model): + emotion_inventory_id = fields.BigIntField(pk=True) + emotion_code = fields.CharField(max_length=255, unique=True) + emotion_name = fields.CharField(max_length=255) + + class Meta: + table = "emotion_inventory" + + @classmethod + async def get_emotion_inventory(cls) -> list[dict]: + return cls.all().values("emotion_code", "emotion_name") diff --git a/src/app/v2/emotions/querys/__init__.py b/src/app/v2/emotions/querys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/emotions/querys/emotion_query.py b/src/app/v2/emotions/querys/emotion_query.py new file mode 100644 index 0000000..d1c7703 --- /dev/null +++ b/src/app/v2/emotions/querys/emotion_query.py @@ -0,0 +1,15 @@ +from app.v2.users.querys.user_query import USER_ID_QUERY + + +SELECT_EMOTION_CODE_BY_USER_UUID_QUERY = f""" + SELECT emotion_code + FROM emotion + WHERE {USER_ID_QUERY} + """ + +INSERT_EMOTION_CODE_FOR_USER_QUERY = f""" + INSERT INTO emotion (emotion_code, user_id) + SELECT %s, user_id + FROM user + WHERE {USER_ID_QUERY} + """ diff --git a/src/app/v2/emotions/services/__init__.py b/src/app/v2/emotions/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/emotions/services/emotion_service.py b/src/app/v2/emotions/services/emotion_service.py new file mode 100644 index 0000000..bd960d1 --- /dev/null +++ b/src/app/v2/emotions/services/emotion_service.py @@ -0,0 +1,15 @@ +from app.v2.emotions.models.emotion import Emotion, EmotionInventory + + +class EmotionService: + @classmethod + async def get_emotions(cls, user_id: str) -> list[dict]: + return await Emotion.get_emotions_with_details_by_user_id(user_id=user_id) + + @classmethod + async def add_emotion(cls, user_id: str, emotion_code: str) -> None: + await Emotion.add_emotion(user_id=user_id, emotion_code=emotion_code) + + @classmethod + async def get_emotion_inventory(cls) -> list[dict]: + return await EmotionInventory.get_emotion_inventory() diff --git a/src/app/v2/items/dtos/item_dto.py b/src/app/v2/items/dtos/item_dto.py new file mode 100644 index 0000000..8d7336c --- /dev/null +++ b/src/app/v2/items/dtos/item_dto.py @@ -0,0 +1,22 @@ +# schemas.py +from pydantic import BaseModel +from typing import Optional + + +class ItemInventorySchema(BaseModel): + item_category: Optional[str] + item_code: Optional[str] + + +class ProductInventorySchema(BaseModel): + price: Optional[float] + product_category: Optional[str] + product_code: Optional[str] + transaction_currency: Optional[str] + + +class ItemInventoryProductInventorySchema(BaseModel): + quantity: int + item_measurement: Optional[str] + item_inventory_id: int + product_inventory_id: int diff --git a/src/app/v2/items/models/item.py b/src/app/v2/items/models/item.py new file mode 100644 index 0000000..ae7a5e6 --- /dev/null +++ b/src/app/v2/items/models/item.py @@ -0,0 +1,36 @@ +from tortoise import fields, models + + +class ItemInventory(models.Model): + item_id = fields.BigIntField(pk=True) + item_category = fields.CharField(max_length=255, null=True) + item_code = fields.CharField(max_length=255, null=True) + + class Meta: + table = "item_inventory" + + +class ProductInventory(models.Model): + product_id = fields.BigIntField(pk=True) + price = fields.FloatField(null=True) + product_category = fields.CharField(max_length=255, null=True) + product_code = fields.CharField(max_length=255, null=True) + transaction_currency = fields.CharField(max_length=255, null=True) + + class Meta: + table = "product_inventory" + + +class ItemInventoryProductInventory(models.Model): + item_inventory_product_inventory_id = fields.BigIntField(pk=True) + quantity = fields.IntField() + item_inventory = fields.ForeignKeyField( + "models.ItemInventory", related_name="product_inventories" + ) + product_inventory = fields.ForeignKeyField( + "models.ProductInventory", related_name="item_inventories" + ) + item_measurement = fields.CharField(max_length=255, null=True) + + class Meta: + table = "item_inventory_product_inventory" diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 85faa70..f7add59 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -87,9 +87,7 @@ async def mobile_my_page_handler(): Level.get_level_info_by_user_id(user_id=user_id), ) - cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager( - cheese_manager_id=user["cheese_manager_id"] - ) + cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager() user_profile_data = UserProfileWithLevel( userProfile=UserProfileDTO( diff --git a/src/app/v2/payments/router.py b/src/app/v2/payments/router.py index 7b4909f..884d62f 100644 --- a/src/app/v2/payments/router.py +++ b/src/app/v2/payments/router.py @@ -1,14 +1,36 @@ -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException +from tortoise.exceptions import DoesNotExist -from app.v2.payments.dtos.request import PaymentRequestDTO +from app.v2.items.models.item import ( + ProductInventory, + ItemInventoryProductInventory, + ItemInventory, +) +from app.v2.payments.services.payment_service import PaymentService +from app.v2.users.services.user_service import UserService router = APIRouter(prefix="/payment", tags=["Payment"]) -@router.post( - "", -) -async def payment_item_handler( - body: PaymentRequestDTO, -): - prodict_id = body.productId +# current_user: User = Depends(get_current_user) +@router.post("") +async def process_payment(product_code: str): + try: + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + + product, item_inventory_products = await PaymentService.validate_payment( + product_code + ) + + user = await UserService.get_user_info(user_id=user_id) + + await PaymentService.process_cheese_payment( + product, item_inventory_products, user_id, user["cheese_manager_id"] + ) + + return {"message": "Payment successful", "product": product} + + except HTTPException as e: + raise e + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/src/app/v2/payments/services/payment_service.py b/src/app/v2/payments/services/payment_service.py new file mode 100644 index 0000000..03e8c8e --- /dev/null +++ b/src/app/v2/payments/services/payment_service.py @@ -0,0 +1,89 @@ +from fastapi import HTTPException +from tortoise.exceptions import DoesNotExist +from tortoise.transactions import atomic + +from app.v2.badges.services.badge_service import BadgeService +from app.v2.cheese_managers.models.cheese_manager import CheeseManager +from app.v2.colors.services.color_service import ColorService +from app.v2.emotions.services.emotion_service import EmotionService +from app.v2.items.models.item import ( + ItemInventory, + ProductInventory, + ItemInventoryProductInventory, +) +from app.v2.users.services.user_service import UserService + + +class PaymentService: + @staticmethod + async def validate_payment(product_code: str): + try: + product = await ProductInventory.get(product_code=product_code) + + if product.transaction_currency != "CHEESE": + raise HTTPException( + status_code=400, detail="Invalid transaction currency for payment." + ) + + item_inventory_products = await ItemInventoryProductInventory.filter( + product_inventory_id=product.product_id + ).all() + + if not item_inventory_products: + raise HTTPException( + status_code=404, detail="No inventory found for this product." + ) + + return product, item_inventory_products + + except DoesNotExist: + raise HTTPException(status_code=404, detail="Product not found.") + + @classmethod + @atomic() + async def process_cheese_payment( + cls, + product: ProductInventory, + item_inventory_products: list[ItemInventoryProductInventory], + user_id: str, + cheese_manager_id: str, + ): + total_cheese = await CheeseManager.get_total_cheese_amount_by_manager( + cheese_manager_id=cheese_manager_id + ) + + total_required_cheese = product.price + + if total_cheese < total_required_cheese: + raise HTTPException( + status_code=400, detail="Not enough cheese for this purchase" + ) + + try: + await CheeseManager.use_cheese(cheese_manager_id, total_required_cheese) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + for item_inventory_product in item_inventory_products: + item: ItemInventory = await item_inventory_product.item_inventory + quantity = item_inventory_product.quantity + + if item.item_category == "BADGE": + for _ in range(quantity): + await BadgeService.add_badge( + user_id=user_id, badge_code=item.item_code + ) + elif item.item_category == "COLOR": + for _ in range(quantity): + await ColorService.add_color( + user_id=user_id, color_code=item.item_code + ) + elif item.item_category == "EMOTION": + for _ in range(quantity): + await EmotionService.add_emotion( + user_id=user_id, emotion_code=item.item_code + ) + else: + raise ValueError( + f"Invalid item category for cheese payment: {item.item_category}" + ) diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index e69de29..b5079ec 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -0,0 +1,60 @@ +from fastapi import HTTPException, APIRouter +from tortoise.exceptions import DoesNotExist + +from app.v2.items.models.item import ( + ProductInventory, + ItemInventoryProductInventory, + ItemInventory, +) +from app.v2.purchases.services.purchase_service import PurchaseService + +router = APIRouter(prefix="/purchase", tags=["Purchase"]) + + +@router.post("") +async def process_purchase(product_code: str): + try: + user_id = user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + + product = await ProductInventory.get(product_code=product_code) + + if product.transaction_currency not in ["KRW", "CHEESE"]: + raise HTTPException( + status_code=400, detail="Invalid transaction currency for purchase." + ) + + item_inventory_products = await ItemInventoryProductInventory.filter( + product_inventory_id=product.product_id + ).all() + + if not item_inventory_products: + raise HTTPException( + status_code=404, detail="No inventory found for this product." + ) + + for item_inventory_product in item_inventory_products: + item: ItemInventory = await item_inventory_product.item_inventory + quantity = item_inventory_product.quantity + + if product.transaction_currency == "KRW": + await PurchaseService.process_krw_payment(product, quantity) + + if item.item_category == "SUBSCRIPTION": + await PurchaseService.process_subscription( + item, quantity, item_inventory_product.item_measurement + ) + elif item.item_category == "POINT": + await PurchaseService.add_points(user_id, quantity) + elif item.item_category == "CHEESE": + await PurchaseService.add_cheese(user_id, quantity) + else: + raise ValueError( + f"Invalid item category for purchase: {item.item_category}" + ) + + return {"message": "Purchase successful", "product": product} + + except DoesNotExist: + raise HTTPException(status_code=404, detail="Product not found.") + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py new file mode 100644 index 0000000..9f6124f --- /dev/null +++ b/src/app/v2/purchases/services/purchase_service.py @@ -0,0 +1,26 @@ +from app.v2.items.models.item import ProductInventory, ItemInventory +from app.v2.users.services.user_service import UserService + + +class PurchaseService: + @staticmethod + async def process_krw_payment(product: ProductInventory, quantity: int): + print(f"Processing KRW payment: {product.price * quantity} KRW") + # 여기에 실제 KRW 결제 처리 로직 구현 + + @staticmethod + async def process_subscription( + item: ItemInventory, quantity: int, measurement: str + ): + print(f"Activating subscription: {item.item_code} for {quantity} {measurement}") + # 구독 활성화 로직 구현 + + @staticmethod + async def add_points(user_id: str, quantity: int): + print(f"Adding {quantity} points to user's balance") + await UserService.add_points(user_id, quantity) + + @staticmethod + async def add_cheese(user_id: str, quantity: int): + print(f"Adding {quantity} cheese to user's balance") + await UserService.add_cheese(user_id, quantity) diff --git a/src/app/v2/users/services/user_service.py b/src/app/v2/users/services/user_service.py index 2d77ae0..2043a48 100644 --- a/src/app/v2/users/services/user_service.py +++ b/src/app/v2/users/services/user_service.py @@ -9,6 +9,4 @@ async def get_user_info(cls, user_id: str) -> dict: @classmethod async def get_cheese_balance(cls, cheese_manager_id: str) -> int: - return await CheeseManager.get_total_cheese_amount_by_manager( - cheese_manager_id=cheese_manager_id - ) + return await CheeseManager.get_total_cheese_amount_by_manager() diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index aceeff5..628c064 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -3,6 +3,7 @@ from app.v2.badges.router import router as badge_router from app.v2.teller_cards.router import router as teller_card_router from app.v2.payments.router import router as payment_router +from app.v2.purchases.router import router as purchase_router from app.v2.missions.router import router as mission_router from app.v2.answers.router import router as answer_router from app.v2.cheese_managers.router import router as cheese_router @@ -14,6 +15,7 @@ def attach_router_handlers(app): app.include_router(router=question_router, prefix="/api/v2") app.include_router(router=teller_card_router, prefix="/api/v2") app.include_router(router=payment_router, prefix="/api/v2") + app.include_router(router=purchase_router, prefix="/api/v2") app.include_router(router=mission_router, prefix="/api/v2") app.include_router(router=cheese_router, prefix="/api/v2") app.include_router(router=answer_router, prefix="/test") diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index 34115da..0e34f1e 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -16,6 +16,7 @@ "app.v2.teller_cards.models.teller_card", "app.v2.levels.models.level", "app.v2.cheese_managers.models.cheese_manager", + "app.v2.items.models.item", ] TORTOISE_ORM = { From 678962b6ad6941e7d0390b66a0f538627f4b8e47 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Mon, 28 Oct 2024 23:01:09 +0900 Subject: [PATCH 12/60] =?UTF-8?q?=20=E2=9C=A8=20feat:=20mission=20api=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B5=AC=EC=A1=B0=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - condition_type에 맞춰 로직 수행 - reward logic 및 dummy data 필요 --- src/app/v2/levels/models/level.py | 6 +++ src/app/v2/missions/dtos/request.py | 6 +++ src/app/v2/missions/dtos/response.py | 22 +++++++++ src/app/v2/missions/models/mission.py | 20 ++++++++ src/app/v2/missions/router.py | 68 +++++++++++++++++++++++++- src/app/v2/payments/router.py | 7 +-- src/app/v2/users/models/user.py | 15 +++++- src/app/v2/users/querys/user_query.py | 1 + src/common/utils/get_user_id.py | 2 + src/core/database/database_settings.py | 1 + 10 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 src/app/v2/missions/dtos/request.py create mode 100644 src/app/v2/missions/dtos/response.py create mode 100644 src/app/v2/missions/models/mission.py create mode 100644 src/common/utils/get_user_id.py diff --git a/src/app/v2/levels/models/level.py b/src/app/v2/levels/models/level.py index b19cf08..64989bf 100644 --- a/src/app/v2/levels/models/level.py +++ b/src/app/v2/levels/models/level.py @@ -40,3 +40,9 @@ async def update_level_and_exp( query = UPDATE_USER_LEVEL_AND_EXP_QUERY values = (new_level, new_exp, user_id) await QueryExecutor.execute_query(query, values=values, fetch_type="single") + + +class LevelInventory(Model): + level_inventory_id = fields.BigIntField(pk=True) + level = fields.IntField(null=True) + required_exp = fields.IntField(null=True) diff --git a/src/app/v2/missions/dtos/request.py b/src/app/v2/missions/dtos/request.py new file mode 100644 index 0000000..026bed1 --- /dev/null +++ b/src/app/v2/missions/dtos/request.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class MissionProgressRequest(BaseModel): + mission_code: str + progress_count: int diff --git a/src/app/v2/missions/dtos/response.py b/src/app/v2/missions/dtos/response.py new file mode 100644 index 0000000..46ed7df --- /dev/null +++ b/src/app/v2/missions/dtos/response.py @@ -0,0 +1,22 @@ +# 응답 모델 정의 +from pydantic import BaseModel + + +class MissionProgressResponse(BaseModel): + mission_code: str + progress_count: int + is_completed: bool + mission_name: str + mission_description: str + target_count: int + + +class UserLevelResponse(BaseModel): + user_level: int + user_exp: int + level_up: bool + + +class ApiResponse(BaseModel): + mission_progress: MissionProgressResponse + user_level_info: UserLevelResponse diff --git a/src/app/v2/missions/models/mission.py b/src/app/v2/missions/models/mission.py new file mode 100644 index 0000000..b8802d9 --- /dev/null +++ b/src/app/v2/missions/models/mission.py @@ -0,0 +1,20 @@ +from tortoise import fields +from tortoise.models import Model + + +class UserMission(Model): + user_mission_id = fields.BigIntField(pk=True) + is_completed = fields.BooleanField(default=False) + mission_code = fields.CharField(max_length=255) + progress_count = fields.IntField(default=0) + user = fields.ForeignKeyField("models.User", related_name="missions") + + +class MissionInventory(Model): + mission_inventory_id = fields.BigIntField(pk=True) + condition_type = fields.CharField(max_length=255) + mission_code = fields.CharField(max_length=255) + mission_description = fields.CharField(max_length=255) + mission_name = fields.CharField(max_length=255) + reward_code = fields.CharField(max_length=255) + target_count = fields.IntField() diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 528bad1..83285ef 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -1,4 +1,16 @@ -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException, Depends + +from tortoise.expressions import F + +from app.v2.missions.dtos.request import MissionProgressRequest +from app.v2.missions.dtos.response import ( + ApiResponse, + MissionProgressResponse, + UserLevelResponse, +) +from app.v2.missions.models.mission import MissionInventory, UserMission +from app.v2.users.models.user import User +from common.utils.get_user_id import get_user_id router = APIRouter(prefix="/mission", tags=["Mission"]) @@ -6,3 +18,57 @@ @router.post("/level") def level_up_handler(): pass + + +@router.post("/update-mission-progress", response_model=ApiResponse) +async def update_mission_progress( + request: MissionProgressRequest, user_id: str = Depends(get_user_id) +): + user: User = await User.get_user_by_uuid(user_id) + + if user is None: + raise HTTPException(status_code=404, detail="User not found") + + # 미션 정보를 가져옵니다. + mission = await MissionInventory.get_or_none(mission_code=request.mission_code) + if not mission: + raise HTTPException(status_code=404, detail="Mission not found") + + user_mission, created = await UserMission.get_or_create( + user=user, + mission_code=request.mission_code, + defaults={"progress_count": 0, "is_completed": False}, + ) + + level_up = False + if not user_mission.is_completed: + # 미션 진행 상황을 업데이트합니다. + user_mission.progress_count = F("progress_count") + request.progress_count + await user_mission.save() + await user_mission.refresh_from_db() + + # 미션 완료 여부를 확인합니다. + if user_mission.progress_count >= mission.target_count: + user_mission.is_completed = True + await user_mission.save() + + # 레벨업 미션인 경우 처리 + if mission.condition_type == "LEVEL_UP": + user.level.user_level += 1 + await user.level.save() + level_up = True + # 여기에 레벨업 보상 로직을 추가할 수 있습니다. + + return ApiResponse( + mission_progress=MissionProgressResponse( + mission_code=mission.mission_code, + progress_count=user_mission.progress_count, + is_completed=user_mission.is_completed, + mission_name=mission.mission_name, + mission_description=mission.mission_description, + target_count=mission.target_count, + ), + user_level_info=UserLevelResponse( + user_level=user.level.user_level, level_up=level_up + ), + ) diff --git a/src/app/v2/payments/router.py b/src/app/v2/payments/router.py index 884d62f..83e79c2 100644 --- a/src/app/v2/payments/router.py +++ b/src/app/v2/payments/router.py @@ -1,11 +1,6 @@ from fastapi import APIRouter, HTTPException -from tortoise.exceptions import DoesNotExist -from app.v2.items.models.item import ( - ProductInventory, - ItemInventoryProductInventory, - ItemInventory, -) + from app.v2.payments.services.payment_service import PaymentService from app.v2.users.services.user_service import UserService diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index 0c92544..f3e365d 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -1,10 +1,14 @@ -from tortoise import fields +from typing import Optional, Any + +from fastapi import Depends +from tortoise import fields, connections from tortoise.models import Model from app.v2.users.querys.user_query import ( SELECT_USER_INFO_BY_USER_UUID_QUERY, SELECT_USER_PROFILE_BY_USER_ID_QUERY, SELECT_USER_TELLER_CARD_ID_BY_USER_UUID_QUERY, + SELECT_USER_BY_UUID_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -81,3 +85,12 @@ async def get_teller_card_id_by_user_id(cls, user_id: str) -> int | None: return await QueryExecutor.execute_query( query, values=value, fetch_type="single" ) + + @classmethod + async def get_user_by_uuid(cls, user_id: str) -> Optional[dict[str, Any]]: + conn = connections.get("default") + result = await conn.execute_query_dict(SELECT_USER_BY_UUID_QUERY, [user_id]) + if not result: + return None + user = result[0] + return user diff --git a/src/app/v2/users/querys/user_query.py b/src/app/v2/users/querys/user_query.py index 2883ad1..36a7dca 100644 --- a/src/app/v2/users/querys/user_query.py +++ b/src/app/v2/users/querys/user_query.py @@ -2,6 +2,7 @@ SELECT_USER_BY_UUID_QUERY = f"SELECT * FROM user WHERE {USER_ID_QUERY} LIMIT 1" + SELECT_USER_PROFILE_BY_USER_ID_QUERY = f""" SELECT u.nickname, diff --git a/src/common/utils/get_user_id.py b/src/common/utils/get_user_id.py new file mode 100644 index 0000000..fb66ade --- /dev/null +++ b/src/common/utils/get_user_id.py @@ -0,0 +1,2 @@ +async def get_user_id(): + return "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index 0e34f1e..69cd324 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -17,6 +17,7 @@ "app.v2.levels.models.level", "app.v2.cheese_managers.models.cheese_manager", "app.v2.items.models.item", + "app.v2.missions.models.mission", ] TORTOISE_ORM = { From 10a66592c2832f678f5761bdf07766e509cab1c9 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 3 Nov 2024 02:40:19 +0900 Subject: [PATCH 13/60] =?UTF-8?q?=20=E2=9C=A8=20feat:=20mission=20service?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 레벨업의 경우 level, mission 모델 둘다 이용 - 보상 로직 필요 --- src/app/v2/answers/models/answer.py | 2 +- src/app/v2/answers/services/answer_service.py | 4 ++ src/app/v2/badges/dtos/badge_dto.py | 11 +++- src/app/v2/badges/dtos/response.py | 4 +- src/app/v2/badges/models/badge.py | 16 ++--- src/app/v2/badges/querys/badge_query.py | 4 +- src/app/v2/badges/router.py | 16 +---- src/app/v2/badges/services/badge_service.py | 12 +++- src/app/v2/levels/dtos/level_dto.py | 7 +- src/app/v2/levels/services/level_service.py | 2 + src/app/v2/missions/dtos/mission_dto.py | 23 +++++++ src/app/v2/missions/models/mission.py | 32 +++++++++ .../{repositorys => querys}/__init__.py | 0 src/app/v2/missions/querys/mission_query.py | 15 +++++ src/app/v2/missions/router.py | 23 +++++++ .../v2/missions/services/mission_service.py | 65 +++++++++++++++++++ src/app/v2/mobiles/dtos/mypage_response.py | 11 ++++ .../v2/mobiles/dtos/teller_card_response.py | 19 +++++- src/app/v2/mobiles/router.py | 45 +++++++------ src/app/v2/teller_cards/router.py | 12 ++-- .../services/teller_card_service.py | 8 +++ src/app/v2/users/dtos/user_profile_dto.py | 19 ++++++ src/app/v2/users/services/user_service.py | 4 ++ 23 files changed, 289 insertions(+), 65 deletions(-) create mode 100644 src/app/v2/missions/dtos/mission_dto.py rename src/app/v2/missions/{repositorys => querys}/__init__.py (100%) create mode 100644 src/app/v2/missions/querys/mission_query.py create mode 100644 src/app/v2/missions/services/mission_service.py diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py index 7808bb2..cc5b062 100644 --- a/src/app/v2/answers/models/answer.py +++ b/src/app/v2/answers/models/answer.py @@ -35,7 +35,7 @@ class Meta: # 기존 get_answer_count_by_user_id 메서드 @classmethod - async def get_answer_count_by_user_id(cls, user_id: str) -> int: + async def get_answer_count_by_user_id(cls, user_id: str) -> dict: query = SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query( diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index 9d5e16a..cfba347 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -5,6 +5,10 @@ class AnswerService: + @classmethod + async def get_answer_count(cls, user_id: str): + answer_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) + return answer_count_raw["answer_count"] @classmethod async def get_answer_record(cls, user_id: str) -> "RecordDto": diff --git a/src/app/v2/badges/dtos/badge_dto.py b/src/app/v2/badges/dtos/badge_dto.py index 8026267..4262ad4 100644 --- a/src/app/v2/badges/dtos/badge_dto.py +++ b/src/app/v2/badges/dtos/badge_dto.py @@ -15,6 +15,11 @@ class BadgeDTO(BaseModel): badgeMiddleName: str badgeCondition: str - -class BadgeListDTO(BaseModel): - badges: list[BadgeDTO] + @classmethod + def builder(cls, badge_raw: dict) -> "BadgeDTO": + return cls( + badgeCode=badge_raw.get("badge_code"), + badgeName=badge_raw.get("badge_name"), + badgeMiddleName=badge_raw.get("badge_middle_name"), + badgeCondition=badge_raw.get("badge_condition"), + ) diff --git a/src/app/v2/badges/dtos/response.py b/src/app/v2/badges/dtos/response.py index 0f1f22e..efba691 100644 --- a/src/app/v2/badges/dtos/response.py +++ b/src/app/v2/badges/dtos/response.py @@ -1,6 +1,6 @@ -from app.v2.badges.dtos.badge_dto import BadgeListDTO +from app.v2.badges.dtos.badge_dto import BadgeDTO from common.base_models.base_dtos.base_response import BaseResponseDTO class BadgeListResponseDTO(BaseResponseDTO): - data: BadgeListDTO + data: list[BadgeDTO] diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index 0761b33..f88e15e 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -2,10 +2,10 @@ from tortoise.models import Model from app.v2.badges.querys.badge_query import ( - SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY, SELECT_BADGE_BY_USER_UUID_QUERY, SELECT_BADGE_CODE_BY_USER_UUID_QUERY, INSERT_BADGE_CODE_FOR_USER_QUERY, + SELECT_BADGE_COUNT_BY_USER_UUID_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -20,18 +20,12 @@ class Meta: table = "badge" @classmethod - async def get_badge_count_and_codes_by_user_id(cls, user_id: str) -> tuple: + async def get_badge_count_by_user_id(cls, user_id: str) -> dict: - query = SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY + query = SELECT_BADGE_COUNT_BY_USER_UUID_QUERY value = user_id - result = await QueryExecutor.execute_query( - query, values=value, fetch_type="multiple" - ) - - return ( - (result[0].get("badge_count", 0), result[0].get("badge_code", "")) - if result and len(result) > 0 - else (0, "") + return await QueryExecutor.execute_query( + query, values=value, fetch_type="single" ) @classmethod diff --git a/src/app/v2/badges/querys/badge_query.py b/src/app/v2/badges/querys/badge_query.py index 6c01ae9..2dae63e 100644 --- a/src/app/v2/badges/querys/badge_query.py +++ b/src/app/v2/badges/querys/badge_query.py @@ -1,7 +1,7 @@ from app.v2.users.querys.user_query import USER_ID_QUERY -SELECT_BADGE_COUNT_AND_CODES_BY_USER_UUID_QUERY = f""" - SELECT COUNT(*) as badge_count, GROUP_CONCAT(badge_code) as badge_codes +SELECT_BADGE_COUNT_BY_USER_UUID_QUERY = f""" + SELECT COUNT(*) as badge_count FROM badge WHERE {USER_ID_QUERY} """ diff --git a/src/app/v2/badges/router.py b/src/app/v2/badges/router.py index f4f3100..e72223b 100644 --- a/src/app/v2/badges/router.py +++ b/src/app/v2/badges/router.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, status -from app.v2.badges.dtos.badge_dto import BadgeDTO, BadgeListDTO from app.v2.badges.dtos.response import BadgeListResponseDTO from app.v2.badges.models.badge import Badge +from app.v2.badges.services.badge_service import BadgeService router = APIRouter(prefix="/user/badge", tags=["Badge"]) @@ -15,20 +15,10 @@ async def get_user_badge_handler(): user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - badge_list = await Badge.get_badges_with_details_by_user_id(user_id=user_id) - - badges = [ - BadgeDTO( - badgeCode=item.get("badge_code"), - badgeName=item.get("badge_name"), - badgeMiddleName=item.get("badge_middle_name"), - badgeCondition=item.get("badge_condition"), - ) - for item in badge_list - ] + badges = await BadgeService.get_badges_with_details_by_user_id(user_id) return BadgeListResponseDTO( code=status.HTTP_200_OK, message="보유 뱃지 정보 조회", - data=BadgeListDTO(badges=badges), + data=badges, ) diff --git a/src/app/v2/badges/services/badge_service.py b/src/app/v2/badges/services/badge_service.py index acb94cf..73ab316 100644 --- a/src/app/v2/badges/services/badge_service.py +++ b/src/app/v2/badges/services/badge_service.py @@ -1,4 +1,4 @@ -from app.v2.badges.dtos.badge_dto import BadgeCodeDTO +from app.v2.badges.dtos.badge_dto import BadgeCodeDTO, BadgeDTO from app.v2.badges.models.badge import Badge @@ -11,3 +11,13 @@ async def get_badges(cls, user_id: str) -> list[BadgeCodeDTO]: @classmethod async def add_badge(cls, user_id: str, badge_code: str) -> None: await Badge.add_badge(user_id=user_id, badge_code=badge_code) + + @classmethod + async def get_badges_with_details_by_user_id(cls, user_id: str) -> list[dict]: + badges_raw = await Badge.get_badges_with_details_by_user_id(user_id=user_id) + return [BadgeDTO.builder(badge) for badge in badges_raw] + + @classmethod + async def get_badge_count(cls, user_id: str) -> int: + badge_count_raw = await Badge.get_badge_count_by_user_id(user_id=user_id) + return badge_count_raw.get("badge_count", 0) diff --git a/src/app/v2/levels/dtos/level_dto.py b/src/app/v2/levels/dtos/level_dto.py index f512950..9be7a61 100644 --- a/src/app/v2/levels/dtos/level_dto.py +++ b/src/app/v2/levels/dtos/level_dto.py @@ -4,7 +4,10 @@ class LevelDTO(BaseModel): level: int current_exp: int + required_exp: int | None = None @classmethod - def builder(cls, level: int, current_exp: int) -> "LevelDTO": - return cls(level=level, current_exp=current_exp) + def builder( + cls, level: int, current_exp: int, required_exp: int | None = None + ) -> "LevelDTO": + return cls(level=level, current_exp=current_exp, required_exp=required_exp) diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index 3ae204e..dcd1861 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -9,9 +9,11 @@ class LevelService: async def get_level_info(cls, user_id: str) -> LevelDTO: # 레벨 정보를 조회하는 로직 level_info_raw = await Level.get_level_info_by_user_id(user_id=user_id) + required_exp_raw = await Level.get_required_exp_by_user_id(user_id=user_id) return LevelDTO.builder( level=level_info_raw.get("level_level"), current_exp=level_info_raw.get("level_exp"), + required_exp=required_exp_raw.get("required_exp"), ) @classmethod diff --git a/src/app/v2/missions/dtos/mission_dto.py b/src/app/v2/missions/dtos/mission_dto.py new file mode 100644 index 0000000..e7cea50 --- /dev/null +++ b/src/app/v2/missions/dtos/mission_dto.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel + + +class UserMissionDTO(BaseModel): + user_mission_id: int + is_completed: bool + mission_code: str + progress_count: int + + @classmethod + def builder(cls, user_mission) -> "UserMissionDTO": + is_completed_raw = user_mission.get("is_completed") + is_completed = ( + bool(int.from_bytes(is_completed_raw, byteorder="big")) + if isinstance(is_completed_raw, bytes) + else bool(is_completed_raw) + ) + return cls( + user_mission_id=user_mission.get("user_mission_id"), + is_completed=is_completed, + mission_code=user_mission.get("mission_code"), + progress_count=user_mission.get("progress_count"), + ) diff --git a/src/app/v2/missions/models/mission.py b/src/app/v2/missions/models/mission.py index b8802d9..c11e6be 100644 --- a/src/app/v2/missions/models/mission.py +++ b/src/app/v2/missions/models/mission.py @@ -1,6 +1,12 @@ from tortoise import fields from tortoise.models import Model +from app.v2.missions.querys.mission_query import ( + SELECT_USER_MISSIONS_BY_CONDITION_TYPE_QUERY, + UPDATE_USER_MISSION_PROGRESS_QUERY, +) +from common.utils.query_executor import QueryExecutor + class UserMission(Model): user_mission_id = fields.BigIntField(pk=True) @@ -9,6 +15,29 @@ class UserMission(Model): progress_count = fields.IntField(default=0) user = fields.ForeignKeyField("models.User", related_name="missions") + @classmethod + async def get_user_missions_by_condition_type( + cls, user_id: str, condition_type: str + ): + # 유저 ID와 미션 조건 타입에 따른 미션 필터링 + query = SELECT_USER_MISSIONS_BY_CONDITION_TYPE_QUERY + values = (user_id, condition_type) + return await QueryExecutor.execute_query( + query, values=values, fetch_type="multiple" + ) + + @classmethod + async def update_user_mission_progress( + cls, + user_id: str, + mission_code: str, + new_progress_count: int, + is_completed: bool, + ): + query = UPDATE_USER_MISSION_PROGRESS_QUERY + values = (new_progress_count, int(is_completed), user_id, mission_code) + await QueryExecutor.execute_query(query, values=values, fetch_type="single") + class MissionInventory(Model): mission_inventory_id = fields.BigIntField(pk=True) @@ -18,3 +47,6 @@ class MissionInventory(Model): mission_name = fields.CharField(max_length=255) reward_code = fields.CharField(max_length=255) target_count = fields.IntField() + + class Meta: + table = "mission_inventory" diff --git a/src/app/v2/missions/repositorys/__init__.py b/src/app/v2/missions/querys/__init__.py similarity index 100% rename from src/app/v2/missions/repositorys/__init__.py rename to src/app/v2/missions/querys/__init__.py diff --git a/src/app/v2/missions/querys/mission_query.py b/src/app/v2/missions/querys/mission_query.py new file mode 100644 index 0000000..703dfb2 --- /dev/null +++ b/src/app/v2/missions/querys/mission_query.py @@ -0,0 +1,15 @@ +from app.v2.users.querys.user_query import USER_ID_QUERY + +SELECT_USER_MISSIONS_BY_CONDITION_TYPE_QUERY = f""" + SELECT um.* + FROM user_mission um + JOIN mission_inventory mi ON um.mission_code = mi.mission_code + WHERE um.{USER_ID_QUERY} AND mi.condition_type = %s +""" + +UPDATE_USER_MISSION_PROGRESS_QUERY = """ + UPDATE user_mission + SET progress_count = %s, is_completed = %s + WHERE user_id = UNHEX(REPLACE(%s, '-', '')) + AND mission_code = %s +""" diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 83285ef..9a7775f 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -9,6 +9,7 @@ UserLevelResponse, ) from app.v2.missions.models.mission import MissionInventory, UserMission +from app.v2.missions.services.mission_service import MissionService from app.v2.users.models.user import User from common.utils.get_user_id import get_user_id @@ -72,3 +73,25 @@ async def update_mission_progress( user_level=user.level.user_level, level_up=level_up ), ) + + +@router.get("/get-user-missions") +async def mission_test_handler(user_id: str = Depends(get_user_id)): + + mission_service = MissionService() + + # 1. 글 작성 미션 진행도 업데이트 + await mission_service.update_mission_progress( + user_id=user_id, condition_type="post", increment=1 + ) + + # 2. 레벨업 미션 진행도 업데이트 + await MissionService().update_mission_progress( + user_id=user_id, condition_type="level-up", increment=0 + ) + + action_data = {"time_check": "1", "post": "1", "level_up": "1"} + # 3. 특정 시간대 미션도 체크하여 진행 + # if "time_check" in action_data: + # await update_mission_progress(user_id, action_data["time_check"], increment=1) + # pass diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py new file mode 100644 index 0000000..322fc37 --- /dev/null +++ b/src/app/v2/missions/services/mission_service.py @@ -0,0 +1,65 @@ +import asyncio + +from app.v2.missions.dtos.mission_dto import UserMissionDTO +from app.v2.missions.models.mission import UserMission, MissionInventory + + +class MissionService: + @staticmethod + async def get_user_missions(user_id, condition_type): + user_mission_raw = await UserMission.get_user_missions_by_condition_type( + user_id, condition_type + ) + return [ + UserMissionDTO.builder(user_mission) for user_mission in user_mission_raw + ] + + @staticmethod + async def update_user_mission_progress( + user_id: str, + mission_code: str, + new_progress_count: int, + is_completed: bool, + ): + await UserMission.update_user_mission_progress( + user_id=user_id, + mission_code=mission_code, + new_progress_count=new_progress_count, + is_completed=is_completed, + ) + + async def update_mission_progress(self, user_id, condition_type, increment=1): + # 1. 해당 조건 유형의 미션을 조회하고 진행 상황 업데이트 + user_missions = await self.get_user_missions(user_id, condition_type) + missions = await MissionInventory.filter(condition_type=condition_type).all() + + mission_dict = {mission.mission_code: mission for mission in missions} + + for user_mission in user_missions: + if user_mission.is_completed: + continue + + # 2. 진행 상황 증가 + user_mission.progress_count += increment + + mission = mission_dict.get(user_mission.mission_code) + if not mission: + continue + + # 3. 목표 도달 여부 확인 + if user_mission.progress_count >= mission.target_count: + user_mission.is_completed = True # 미션 완료 처리 + # await self.reward_user_for_mission( + # user_id, user_mission.mission.reward_code + # ) # 보상 지급 + + await self.update_user_mission_progress( + user_id=user_id, + mission_code=user_mission.mission_code, + new_progress_count=user_mission.progress_count, + is_completed=user_mission.is_completed, + ) + + @staticmethod + async def reward_user_for_mission(user_id, reward_code): + print(f"Reward {reward_code} granted to user {user_id}") diff --git a/src/app/v2/mobiles/dtos/mypage_response.py b/src/app/v2/mobiles/dtos/mypage_response.py index ab066fc..c5153b3 100644 --- a/src/app/v2/mobiles/dtos/mypage_response.py +++ b/src/app/v2/mobiles/dtos/mypage_response.py @@ -9,6 +9,17 @@ class UserProfileWithLevel(BaseModel): userProfile: UserProfileDTO level: LevelDTO + @classmethod + def builder( + cls, + userProfile: UserProfileDTO, + level: LevelDTO, + ) -> "UserProfileWithLevel": + return cls( + userProfile=userProfile, + level=level, + ) + class MyPageResponseDTO(BaseResponseDTO): data: UserProfileWithLevel diff --git a/src/app/v2/mobiles/dtos/teller_card_response.py b/src/app/v2/mobiles/dtos/teller_card_response.py index 7fbd37b..acd1d9a 100644 --- a/src/app/v2/mobiles/dtos/teller_card_response.py +++ b/src/app/v2/mobiles/dtos/teller_card_response.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from typing import List, Optional -from app.v2.badges.dtos.badge_dto import BadgeCodeDTO +from app.v2.badges.dtos.badge_dto import BadgeDTO from app.v2.colors.dtos.color_dto import ColorCodeDTO from app.v2.levels.dtos.level_dto import LevelDTO @@ -10,11 +10,26 @@ class DataDTO(BaseModel): - badges: List[BadgeCodeDTO] + badges: List[BadgeDTO] colors: List[ColorCodeDTO] userInfo: UserInfoDTO level: LevelDTO + @classmethod + def builder( + cls, + badges: List[BadgeDTO], + colors: List[ColorCodeDTO], + userInfo: UserInfoDTO, + level: LevelDTO, + ) -> "DataDTO": + return cls( + badges=badges, + colors=colors, + userInfo=userInfo, + level=level, + ) + # 최종 응답 DTO class TellerCardResponseDTO(BaseResponseDTO): diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index f7add59..8b0f14a 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -2,7 +2,8 @@ from fastapi import APIRouter, status, HTTPException -from app.v2.answers.models.answer import Answer + +from app.v2.answers.services.answer_service import AnswerService from app.v2.badges.models.badge import Badge from app.v2.badges.services.badge_service import BadgeService @@ -10,8 +11,7 @@ from app.v2.cheese_managers.services.cheese_service import CheeseService from app.v2.colors.services.color_service import ColorService -from app.v2.levels.dtos.level_dto import LevelDTO -from app.v2.levels.models.level import Level + from app.v2.levels.services.level_service import LevelService from app.v2.mobiles.dtos.mypage_response import ( @@ -20,6 +20,7 @@ ) from app.v2.mobiles.dtos.teller_card_response import DataDTO, TellerCardResponseDTO + from app.v2.teller_cards.services.teller_card_service import TellerCardService from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO @@ -43,9 +44,9 @@ async def mobile_teller_card_handler(): user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" try: - badges_task = BadgeService.get_badges(user_id) + badges_task = BadgeService.get_badges_with_details_by_user_id(user_id) colors_task = ColorService.get_colors(user_id) - level_info_task = LevelService.get_level_info(user_id) # LevelService 추가 + level_info_task = LevelService.get_level_info(user_id) teller_card_task = TellerCardService.get_teller_card(user_id) user_info_task = UserService.get_user_info(user_id) @@ -53,7 +54,7 @@ async def mobile_teller_card_handler(): badges_task, colors_task, level_info_task, teller_card_task, user_info_task ) except Exception as e: - raise HTTPException(status_code=500, detail="내부 서버 오류") + raise HTTPException(status_code=500, detail=str(e)) cheese_amount = await CheeseService.get_cheese_balance( user_raw["cheese_manager_id"] @@ -63,7 +64,9 @@ async def mobile_teller_card_handler(): user_raw, cheeseBalance=cheese_amount, tellerCard=teller_card ) - data = DataDTO(badges=badges, colors=colors, userInfo=user_info, level=level_info) + data = DataDTO.builder( + badges=badges, colors=colors, userInfo=user_info, level=level_info + ) return TellerCardResponseDTO( code=status.HTTP_200_OK, @@ -80,28 +83,30 @@ async def mobile_teller_card_handler(): async def mobile_my_page_handler(): user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - user, answer_count, (badge_count, badge_code), level = await asyncio.gather( - User.get_user_profile_by_user_id(user_id=user_id), - Answer.get_answer_count_by_user_id(user_id=user_id), - Badge.get_badge_count_and_codes_by_user_id(user_id=user_id), - Level.get_level_info_by_user_id(user_id=user_id), + user, answer_count, badge_count, teller_card, level = await asyncio.gather( + UserService.get_user_profile(user_id=user_id), + AnswerService.get_answer_count(user_id=user_id), + BadgeService.get_badge_count(user_id=user_id), + TellerCardService.get_teller_card(user_id=user_id), + LevelService.get_level_info(user_id), ) - cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager() + cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager( + cheese_manager_id=user["cheese_manager_id"] + ) - user_profile_data = UserProfileWithLevel( - userProfile=UserProfileDTO( + user_profile_data = UserProfileWithLevel.builder( + userProfile=UserProfileDTO.builder( nickname=user["nickname"], - badgeCode=badge_code, cheeseBalance=cheese_amount, + badgeCode=teller_card.badgeCode, badgeCount=badge_count, - answerCount=answer_count["answer_count"], + answerCount=answer_count, premium=bool(user["is_premium"]), ), - level=LevelDTO( - level=level.get("level_level"), current_exp=level.get("level_exp") - ), + level=level, ) + return MyPageResponseDTO( code=status.HTTP_200_OK, message="정상처리되었습니다", diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py index 69d8b85..d54d0d8 100644 --- a/src/app/v2/teller_cards/router.py +++ b/src/app/v2/teller_cards/router.py @@ -1,10 +1,9 @@ from fastapi import APIRouter, status -from app.v2.badges.models.badge import Badge from app.v2.teller_cards.dtos.response import TellerCardResponseDTO from app.v2.teller_cards.dtos.request import TellerCardRequestDTO -from app.v2.teller_cards.models.teller_card import TellerCard +from app.v2.teller_cards.services.teller_card_service import TellerCardService router = APIRouter(prefix="/tellercard", tags=["TellerCard"]) @@ -21,18 +20,15 @@ async def patch_teller_card_handler( badge_code = (body.badgeCode,) color_code = (body.colorCode,) - await TellerCard.patch_teller_card_info_by_user_id( - user_id=user_id, - badge_code=badge_code, - color_code=color_code, + await TellerCardService.patch_teller_card( + user_id=user_id, badge_code=badge_code, color_code=color_code ) - teller_card = await TellerCard.get_teller_card_info_by_user_id(user_id=user_id) + teller_card = await TellerCardService.get_teller_card(user_id=user_id) activate_badge_code = teller_card["activate_badge_code"] activate_color_code = teller_card["activate_color_code"] - await Badge.get_badge_count_and_codes_by_user_id(user_id) return TellerCardResponseDTO( colorCode=activate_color_code, badgeCode=activate_badge_code, diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py index 2d9de44..634b4af 100644 --- a/src/app/v2/teller_cards/services/teller_card_service.py +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -9,3 +9,11 @@ async def get_teller_card(cls, user_id: str) -> TellerCardDTO: user_id=user_id ) return TellerCardDTO.builder(teller_cards_raw) + + @classmethod + async def patch_teller_card( + cls, user_id: str, badge_code: str, color_code: str + ) -> None: + await TellerCard.patch_teller_card_info_by_user_id( + user_id=user_id, badge_code=badge_code, color_code=color_code + ) \ No newline at end of file diff --git a/src/app/v2/users/dtos/user_profile_dto.py b/src/app/v2/users/dtos/user_profile_dto.py index bd51ebc..65dec0f 100644 --- a/src/app/v2/users/dtos/user_profile_dto.py +++ b/src/app/v2/users/dtos/user_profile_dto.py @@ -8,3 +8,22 @@ class UserProfileDTO(BaseModel): badgeCount: int answerCount: int premium: bool + + @classmethod + def builder( + cls, + nickname: str, + badgeCode: str, + cheeseBalance: int, + badgeCount: int, + answerCount: int, + premium: bool, + ) -> "UserProfileDTO": + return cls( + nickname=nickname, + badgeCode=badgeCode, + cheeseBalance=cheeseBalance, + badgeCount=badgeCount, + answerCount=answerCount, + premium=premium, + ) diff --git a/src/app/v2/users/services/user_service.py b/src/app/v2/users/services/user_service.py index 2043a48..940bd59 100644 --- a/src/app/v2/users/services/user_service.py +++ b/src/app/v2/users/services/user_service.py @@ -10,3 +10,7 @@ async def get_user_info(cls, user_id: str) -> dict: @classmethod async def get_cheese_balance(cls, cheese_manager_id: str) -> int: return await CheeseManager.get_total_cheese_amount_by_manager() + + @classmethod + async def get_user_profile(cls, user_id: str) -> dict: + return await User.get_user_profile_by_user_id(user_id=user_id) From 1bb48998ae94b429af169da3fc3d7a7a6541b3d5 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Mon, 4 Nov 2024 02:14:14 +0900 Subject: [PATCH 14/60] =?UTF-8?q?=20=E2=9C=A8=20feat:=20Mission=20reward?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20+=20=EA=B2=BD=ED=97=98=EC=B9=98=20+=20?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=EC=97=85=20=EB=A7=88=EB=AC=B4=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/models/answer.py | 10 +- src/app/v2/answers/querys/answer_query.py | 8 + .../cheese_managers/models/cheese_manager.py | 7 + .../services/cheese_service.py | 6 + src/app/v2/items/models/item.py | 32 +++ src/app/v2/levels/services/level_service.py | 41 +-- src/app/v2/{rewards => likes}/__init__.py | 0 .../v2/{rewards => likes}/models/__init__.py | 0 src/app/v2/likes/models/like.py | 34 +++ .../repositorys => likes/querys}/__init__.py | 0 src/app/v2/likes/querys/like_query.py | 6 + src/app/v2/missions/models/mission.py | 11 +- src/app/v2/missions/querys/mission_query.py | 5 +- src/app/v2/missions/router.py | 13 +- .../v2/missions/services/mission_service.py | 236 ++++++++++++++++-- .../v2/payments/services/payment_service.py | 2 +- src/app/v2/rewards/router.py | 0 src/app/v2/rewards/schemas/__init__.py | 0 src/app/v2/rewards/services/__init__.py | 0 src/core/database/database_settings.py | 1 + 20 files changed, 357 insertions(+), 55 deletions(-) rename src/app/v2/{rewards => likes}/__init__.py (100%) rename src/app/v2/{rewards => likes}/models/__init__.py (100%) create mode 100644 src/app/v2/likes/models/like.py rename src/app/v2/{rewards/repositorys => likes/querys}/__init__.py (100%) create mode 100644 src/app/v2/likes/querys/like_query.py delete mode 100644 src/app/v2/rewards/router.py delete mode 100644 src/app/v2/rewards/schemas/__init__.py delete mode 100644 src/app/v2/rewards/services/__init__.py diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py index cc5b062..0320e6b 100644 --- a/src/app/v2/answers/models/answer.py +++ b/src/app/v2/answers/models/answer.py @@ -5,7 +5,7 @@ from app.v2.answers.querys.answer_query import ( SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY, - SELECT_ANSWER_BY_USER_UUID_QUERY, + SELECT_ANSWER_BY_USER_UUID_QUERY, SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -52,3 +52,11 @@ async def find_all_by_user( return await QueryExecutor.execute_query( query, values=values, fetch_type="multiple" ) + + @classmethod + async def get_most_recent_answer_by_user_id(cls, user_id: str) -> dict | None: + query = SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query( + query, values=value, fetch_type="single" + ) \ No newline at end of file diff --git a/src/app/v2/answers/querys/answer_query.py b/src/app/v2/answers/querys/answer_query.py index 8d95447..c43f29b 100644 --- a/src/app/v2/answers/querys/answer_query.py +++ b/src/app/v2/answers/querys/answer_query.py @@ -10,3 +10,11 @@ AND date BETWEEN %s AND %s ORDER BY date DESC """ + +SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY = f""" + SELECT * + FROM answer + WHERE {USER_ID_QUERY} + ORDER BY created_at DESC + LIMIT 1 +""" diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py index 9955ee7..942c0e2 100644 --- a/src/app/v2/cheese_managers/models/cheese_manager.py +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -66,6 +66,13 @@ async def use_cheese(cheese_manager_id: int, amount: int): if remaining_amount > 0: raise ValueError("Not enough cheese to complete the transaction") + async def add_cheese(self, amount: int): + await CheeseHistory.create( + status=CheeseStatus.CAN_USE, + current_amount=amount, + starting_amount=amount, + cheese_manager_id=self.cheese_manager_id, + ) class CheeseHistory(Model): cheese_history_id = fields.BigIntField(pk=True) diff --git a/src/app/v2/cheese_managers/services/cheese_service.py b/src/app/v2/cheese_managers/services/cheese_service.py index a7a2f58..a3ca0f5 100644 --- a/src/app/v2/cheese_managers/services/cheese_service.py +++ b/src/app/v2/cheese_managers/services/cheese_service.py @@ -8,3 +8,9 @@ async def get_cheese_balance(cls, cheese_manager_id: str) -> int: return await CheeseManager.get_total_cheese_amount_by_manager( cheese_manager_id=cheese_manager_id ) + + @classmethod + async def add_cheese(cls, cheese_manager_id: str, amount: int): + await CheeseManager.add_cheese( + cheese_manager_id=cheese_manager_id, amount=amount + ) \ No newline at end of file diff --git a/src/app/v2/items/models/item.py b/src/app/v2/items/models/item.py index ae7a5e6..a70a05f 100644 --- a/src/app/v2/items/models/item.py +++ b/src/app/v2/items/models/item.py @@ -34,3 +34,35 @@ class ItemInventoryProductInventory(models.Model): class Meta: table = "item_inventory_product_inventory" + + +class RewardInventory(models.Model): + reward_inventory_id = fields.BigIntField(pk=True) + item_code = fields.CharField(max_length=255, null=True) + reward_code = fields.CharField(max_length=255, null=True) + reward_description = fields.CharField(max_length=255, null=True) + reward_name = fields.CharField(max_length=255, null=True) + + class Meta: + table = "reward_inventory" + + +class ItemInventoryRewardInventory(models.Model): + item_inventory_reward_invnetory_id = fields.BigIntField(pk=True) + quantity = fields.IntField() + item_inventory = fields.ForeignKeyField( + "models.ItemInventory", + related_name="reward_inventories", + on_delete=fields.CASCADE, + db_column="item_inventory_id", + ) + reward_inventory = fields.ForeignKeyField( + "models.RewardInventory", + related_name="item_inventories", + on_delete=fields.CASCADE, + db_column="reward_inventory_id", + ) + item_measurement = fields.CharField(max_length=255, null=True) + + class Meta: + table = "item_inventory_reward_inventory" diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index dcd1861..3b52ea5 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -17,7 +17,7 @@ async def get_level_info(cls, user_id: str) -> LevelDTO: ) @classmethod - async def level_up(cls, user_id: str) -> dict: + async def level_up(cls, user_id: str) -> int: """ 유저가 레벨업 가능한지 확인 후, 레벨업 처리 """ @@ -37,18 +37,29 @@ async def level_up(cls, user_id: str) -> dict: await Level.update_level_and_exp( user_id=user_id, new_level=new_level, new_exp=new_exp ) + return 1 + return 0 + # return { + # "status": "success", + # "message": "레벨업 성공", + # "new_level": new_level, + # "remaining_exp": new_exp, + # } + # + # return { + # "status": "failure", + # "message": "레벨업에 필요한 경험치가 부족합니다", + # "current_level": level, + # "current_exp": current_exp, + # "required_exp": required_exp, + # } - return { - "status": "success", - "message": "레벨업 성공", - "new_level": new_level, - "remaining_exp": new_exp, - } - - return { - "status": "failure", - "message": "레벨업에 필요한 경험치가 부족합니다", - "current_level": level, - "current_exp": current_exp, - "required_exp": required_exp, - } + async def add_exp(self, user_id: str, exp: int) -> None: + level_dto = await self.get_level_info(user_id=user_id) + + current_exp = level_dto.current_exp + new_exp = current_exp + exp + + await Level.update_level_and_exp( + user_id=user_id, new_level=level_dto.level, new_exp=new_exp + ) diff --git a/src/app/v2/rewards/__init__.py b/src/app/v2/likes/__init__.py similarity index 100% rename from src/app/v2/rewards/__init__.py rename to src/app/v2/likes/__init__.py diff --git a/src/app/v2/rewards/models/__init__.py b/src/app/v2/likes/models/__init__.py similarity index 100% rename from src/app/v2/rewards/models/__init__.py rename to src/app/v2/likes/models/__init__.py diff --git a/src/app/v2/likes/models/like.py b/src/app/v2/likes/models/like.py new file mode 100644 index 0000000..deacd61 --- /dev/null +++ b/src/app/v2/likes/models/like.py @@ -0,0 +1,34 @@ +from tortoise import fields +from tortoise.models import Model + +from app.v2.likes.querys.like_query import SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY +from common.utils.query_executor import QueryExecutor + + +class Like(Model): + likes_id = fields.BigIntField(pk=True) + answer = fields.ForeignKeyField( + "models.Answer", related_name="likes", on_delete=fields.CASCADE + ) + user = fields.ForeignKeyField( + "models.User", related_name="likes", on_delete=fields.CASCADE + ) + created_time = fields.DatetimeField(null=True) + modified_time = fields.DatetimeField(null=True) + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) + + class Meta: + table = "likes" + indexes = [ + ("answer_id",), + ("user_id",), + ] + + @staticmethod + async def get_unique_likes_today(user_id: str) -> dict: + query = SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY + values = (user_id,) + return await QueryExecutor.execute_query( + query, values=values, fetch_type="single" + ) diff --git a/src/app/v2/rewards/repositorys/__init__.py b/src/app/v2/likes/querys/__init__.py similarity index 100% rename from src/app/v2/rewards/repositorys/__init__.py rename to src/app/v2/likes/querys/__init__.py diff --git a/src/app/v2/likes/querys/like_query.py b/src/app/v2/likes/querys/like_query.py new file mode 100644 index 0000000..84ce2de --- /dev/null +++ b/src/app/v2/likes/querys/like_query.py @@ -0,0 +1,6 @@ +SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY = """ + SELECT COUNT(DISTINCT answer_id) AS unique_likes + FROM likes + WHERE user_id = UNHEX(REPLACE(%s, '-', '')) + AND DATE(created_time) = CURDATE(); +""" \ No newline at end of file diff --git a/src/app/v2/missions/models/mission.py b/src/app/v2/missions/models/mission.py index c11e6be..e542357 100644 --- a/src/app/v2/missions/models/mission.py +++ b/src/app/v2/missions/models/mission.py @@ -2,8 +2,7 @@ from tortoise.models import Model from app.v2.missions.querys.mission_query import ( - SELECT_USER_MISSIONS_BY_CONDITION_TYPE_QUERY, - UPDATE_USER_MISSION_PROGRESS_QUERY, + UPDATE_USER_MISSION_PROGRESS_QUERY, SELECT_USER_MISSIONS_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -16,12 +15,10 @@ class UserMission(Model): user = fields.ForeignKeyField("models.User", related_name="missions") @classmethod - async def get_user_missions_by_condition_type( - cls, user_id: str, condition_type: str - ): + async def get_user_missions_by_condition_type(cls, user_id: str): # 유저 ID와 미션 조건 타입에 따른 미션 필터링 - query = SELECT_USER_MISSIONS_BY_CONDITION_TYPE_QUERY - values = (user_id, condition_type) + query = SELECT_USER_MISSIONS_QUERY + values = (user_id,) return await QueryExecutor.execute_query( query, values=values, fetch_type="multiple" ) diff --git a/src/app/v2/missions/querys/mission_query.py b/src/app/v2/missions/querys/mission_query.py index 703dfb2..40f5671 100644 --- a/src/app/v2/missions/querys/mission_query.py +++ b/src/app/v2/missions/querys/mission_query.py @@ -1,12 +1,13 @@ from app.v2.users.querys.user_query import USER_ID_QUERY -SELECT_USER_MISSIONS_BY_CONDITION_TYPE_QUERY = f""" +SELECT_USER_MISSIONS_QUERY = """ SELECT um.* FROM user_mission um JOIN mission_inventory mi ON um.mission_code = mi.mission_code - WHERE um.{USER_ID_QUERY} AND mi.condition_type = %s + WHERE um.user_id = UNHEX(REPLACE(%s, '-', '')) """ + UPDATE_USER_MISSION_PROGRESS_QUERY = """ UPDATE user_mission SET progress_count = %s, is_completed = %s diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 9a7775f..f4e1ceb 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -2,6 +2,8 @@ from tortoise.expressions import F +from app.v2.items.models.item import RewardInventory +from app.v2.likes.models.like import Like from app.v2.missions.dtos.request import MissionProgressRequest from app.v2.missions.dtos.response import ( ApiResponse, @@ -80,17 +82,10 @@ async def mission_test_handler(user_id: str = Depends(get_user_id)): mission_service = MissionService() - # 1. 글 작성 미션 진행도 업데이트 - await mission_service.update_mission_progress( - user_id=user_id, condition_type="post", increment=1 - ) + await mission_service.update_mission_progress(user_id=user_id) - # 2. 레벨업 미션 진행도 업데이트 - await MissionService().update_mission_progress( - user_id=user_id, condition_type="level-up", increment=0 - ) + await mission_service.validate_reward(reward_code="RW_FIRST_POST") - action_data = {"time_check": "1", "post": "1", "level_up": "1"} # 3. 특정 시간대 미션도 체크하여 진행 # if "time_check" in action_data: # await update_mission_progress(user_id, action_data["time_check"], increment=1) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 322fc37..b82ffbe 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -1,21 +1,38 @@ -import asyncio +from datetime import datetime, timezone, timedelta +from fastapi import HTTPException +from tortoise.exceptions import DoesNotExist +from tortoise.transactions import atomic + +from app.v2.answers.models.answer import Answer +from app.v2.answers.services.answer_service import AnswerService +from app.v2.badges.services.badge_service import BadgeService +from app.v2.cheese_managers.services.cheese_service import CheeseService +from app.v2.colors.services.color_service import ColorService +from app.v2.items.models.item import ( + RewardInventory, + ItemInventoryRewardInventory, + ItemInventory, +) +from app.v2.levels.services.level_service import LevelService +from app.v2.likes.models.like import Like from app.v2.missions.dtos.mission_dto import UserMissionDTO from app.v2.missions.models.mission import UserMission, MissionInventory +from app.v2.users.services.user_service import UserService class MissionService: @staticmethod - async def get_user_missions(user_id, condition_type): + async def get_user_missions(user_id: str): user_mission_raw = await UserMission.get_user_missions_by_condition_type( - user_id, condition_type + user_id ) return [ UserMissionDTO.builder(user_mission) for user_mission in user_mission_raw ] @staticmethod - async def update_user_mission_progress( + async def _update_user_mission_progress( user_id: str, mission_code: str, new_progress_count: int, @@ -28,38 +45,217 @@ async def update_user_mission_progress( is_completed=is_completed, ) - async def update_mission_progress(self, user_id, condition_type, increment=1): - # 1. 해당 조건 유형의 미션을 조회하고 진행 상황 업데이트 - user_missions = await self.get_user_missions(user_id, condition_type) - missions = await MissionInventory.filter(condition_type=condition_type).all() - + async def update_mission_progress(self, user_id: str): + user = await UserService.get_user_info(user_id=user_id) + user_missions = await self.get_user_missions(user_id) + missions = await MissionInventory.all() mission_dict = {mission.mission_code: mission for mission in missions} for user_mission in user_missions: if user_mission.is_completed: continue - # 2. 진행 상황 증가 - user_mission.progress_count += increment - mission = mission_dict.get(user_mission.mission_code) + if not mission: continue - # 3. 목표 도달 여부 확인 + # 조건에 따른 진행도 업데이트 + increment = await self.evaluate_mission_condition( + user_id, user_mission.mission_code + ) + user_mission.progress_count += increment + + # 3. 목표 도달 여부 확인 & 4.보상 처리 if user_mission.progress_count >= mission.target_count: - user_mission.is_completed = True # 미션 완료 처리 - # await self.reward_user_for_mission( - # user_id, user_mission.mission.reward_code - # ) # 보상 지급 + user_mission.is_completed = True + await self.reward_user_for_mission( + user_id=user_id, + reward_code=mission.reward_code, + cheese_manager_id=user["cheese_manager_id"], + ) - await self.update_user_mission_progress( + # 5. 미션 진행도 업데이트 + await self._update_user_mission_progress( user_id=user_id, mission_code=user_mission.mission_code, new_progress_count=user_mission.progress_count, is_completed=user_mission.is_completed, ) + async def evaluate_mission_condition(self, user_id, mission_code): + user_level_data = await LevelService.get_level_info(user_id) + current_level = user_level_data.level + + if mission_code == "MS_POST_FIRST" and await self.check_first_post(user_id): + return 1 + elif mission_code == "MS_POST_2_5" and await self.check_post_count_range( + user_id, 2, 5 + ): + return 1 + elif mission_code == "MS_POST_280_CHAR" and await self.check_long_answer( + user_id + ): + return 1 + elif mission_code == "MS_POST_GENERAL" and await self.check_post_count_min( + user_id, 6 + ): + return 1 + elif ( + mission_code == "MS_POST_CONSECUTIVE_7" + and await self.check_consecutive_days(user_id) + ): + return 1 + elif mission_code == "MS_POST_EARLY_3" and await self.check_early_morning_posts( + user_id + ): + return 1 + elif mission_code == "MS_CHEESE_TOTAL_50" and await self.check_cheese_total( + user_id + ): + return 1 + elif mission_code == "MS_CHRISTMAS" and await self.check_christmas_period(): + return 1 + + elif ( + mission_code == "MS_LIKE_3_DIFF_POST" + and await self.check_three_likes_different_posts(user_id) + ): + return 1 + elif mission_code == f"MS_LV_{current_level}" and await LevelService.level_up( + user_id=user_id + ): + return 1 + + return 0 + + @staticmethod + async def check_first_post(user_id) -> bool: + post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) + return post_count_raw.get("answer_count", 0) > 0 + + @staticmethod + async def check_post_count_range(user_id, min_count, max_count): + post_count = await Answer.get_answer_count_by_user_id(user_id=user_id) + return min_count <= post_count <= max_count + + @staticmethod + async def check_post_count_min(user_id, min_count): + post_count = await Answer.get_answer_count_by_user_id(user_id=user_id) + return post_count >= min_count + + @staticmethod + async def check_long_answer(user_id) -> bool: + recent_answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) + return len(recent_answer["content"]) >= 280 if recent_answer else False + + @staticmethod + async def check_consecutive_days(user_id) -> bool: + record_dto = await AnswerService.get_answer_record(user_id) + consecutive_days = record_dto.count + return consecutive_days >= 7 + + @staticmethod + async def check_early_morning_posts(user_id) -> bool: + recent_answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) + if recent_answer: + answer_time = recent_answer.get("created_time") + return 0 <= answer_time.hour <= 5 if answer_time else False + return False + + @staticmethod + async def check_cheese_total(user_id) -> bool: + user = await UserService.get_user_info(user_id=user_id) + cheese_amount = await CheeseService.get_cheese_balance( + user["cheese_manager_id"] + ) + + return cheese_amount >= 50 + + @staticmethod + async def check_christmas_period() -> bool: + # 현재 시간 한국 시간(KST) 기준 + now = datetime.now(timezone(timedelta(hours=9))) + + start_date = datetime(2024, 12, 23, 6, 0, tzinfo=timezone(timedelta(hours=9))) + end_date = datetime(2024, 12, 28, 5, 59, tzinfo=timezone(timedelta(hours=9))) + + return start_date <= now <= end_date + @staticmethod - async def reward_user_for_mission(user_id, reward_code): - print(f"Reward {reward_code} granted to user {user_id}") + async def check_three_likes_different_posts(user_id) -> bool: + like_raw = await Like.get_unique_likes_today(user_id) + like_count = like_raw.get("unique_likes", 0) + return like_count >= 3 + + async def reward_user_for_mission( + self, + user_id: str, + reward_code: str, + cheese_manager_id: str, + ): + + # 보상 검증 + reward, item_inventory_rewards = await self.validate_reward( + reward_code=reward_code + ) + + # 보상 지급 + await self.process_reward( + reward_code=reward.reward_code, + item_inventory_rewards=item_inventory_rewards, + user_id=user_id, + cheese_manager_id=cheese_manager_id, + ) + + @staticmethod + async def validate_reward(reward_code: str): + try: + # reward_code를 기반으로 RewardInventory에서 보상 조회 + reward = await RewardInventory.get(reward_code=reward_code) + + # 해당 보상에 연결된 ItemInventoryRewardInventory 항목 조회 + item_inventory_rewards = await reward.item_inventories.all() + + if not item_inventory_rewards: + raise HTTPException( + status_code=404, detail="No inventory found for this reward." + ) + return reward, item_inventory_rewards + + except DoesNotExist: + raise HTTPException(status_code=404, detail="Reward not found.") + + @classmethod + @atomic() + async def process_reward( + cls, + item_inventory_rewards: list[ItemInventoryRewardInventory], + user_id: str, + cheese_manager_id: str, + ): + + for item_inventory_reward in item_inventory_rewards: + item: ItemInventory = await item_inventory_reward.item_inventory + quantity = item_inventory_reward.quantity + + if item.item_category == "BADGE": + for _ in range(quantity): + await BadgeService.add_badge( + user_id=user_id, badge_code=item.item_code + ) + elif item.item_category == "COLOR": + for _ in range(quantity): + await ColorService.add_color( + user_id=user_id, color_code=item.item_code + ) + elif item.item_category == "CHEESE": + await CheeseService.add_cheese( + cheese_manager_id=cheese_manager_id, amount=quantity + ) + elif item.item_category == "POINT": + await LevelService.add_exp(user_id=user_id, point=quantity) + else: + raise ValueError( + f"Invalid item category for reward: {item.item_category}" + ) diff --git a/src/app/v2/payments/services/payment_service.py b/src/app/v2/payments/services/payment_service.py index 03e8c8e..491c887 100644 --- a/src/app/v2/payments/services/payment_service.py +++ b/src/app/v2/payments/services/payment_service.py @@ -11,7 +11,6 @@ ProductInventory, ItemInventoryProductInventory, ) -from app.v2.users.services.user_service import UserService class PaymentService: @@ -87,3 +86,4 @@ async def process_cheese_payment( raise ValueError( f"Invalid item category for cheese payment: {item.item_category}" ) + diff --git a/src/app/v2/rewards/router.py b/src/app/v2/rewards/router.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/v2/rewards/schemas/__init__.py b/src/app/v2/rewards/schemas/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/v2/rewards/services/__init__.py b/src/app/v2/rewards/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index 69cd324..b32c32d 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -18,6 +18,7 @@ "app.v2.cheese_managers.models.cheese_manager", "app.v2.items.models.item", "app.v2.missions.models.mission", + "app.v2.likes.models.like", ] TORTOISE_ORM = { From 668e4d9d9d8ab361524b03859c6aded995b2bb00 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 9 Nov 2024 18:05:54 +0900 Subject: [PATCH 15/60] =?UTF-8?q?=20=E2=9C=A8=20feat:=20purchase=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=EC=B2=98=EB=A6=AC=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 20 ++- pyproject.toml | 2 + src/app/v2/badges/router.py | 1 - src/app/v2/levels/services/level_service.py | 14 -- src/app/v2/missions/router.py | 75 +--------- .../v2/missions/services/mission_service.py | 1 - src/app/v2/mobiles/router.py | 2 - src/app/v2/purchases/dtos/requests.py | 6 + .../v2/purchases/models/purchase_history.py | 18 +++ src/app/v2/purchases/router.py | 65 ++++----- .../v2/purchases/services/purchase_service.py | 138 ++++++++++++++++-- src/app/v2/users/models/user.py | 28 +--- src/app/v2/users/querys/user_query.py | 7 + src/app/v2/users/services/user_service.py | 12 +- 14 files changed, 219 insertions(+), 170 deletions(-) create mode 100644 src/app/v2/purchases/dtos/requests.py create mode 100644 src/app/v2/purchases/models/purchase_history.py diff --git a/poetry.lock b/poetry.lock index bd27ed9..0eefbd0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1091,6 +1091,24 @@ pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1563,4 +1581,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "56dff9593f165d37446949d353911498715f6d0ce99c834e042ed8c0aaf8ea9e" +content-hash = "176e0a053b68d9eb52eb4de3d6ab403821bfabf070c8e387161ed9733bc99d27" diff --git a/pyproject.toml b/pyproject.toml index f0f54fd..a14cb6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ pydantic-settings = "^2.5.2" python-dotenv = "^1.0.1" tortoise-orm = {extras = ["asyncmy"], version = "^0.21.6"} cryptography = "^43.0.1" +httpx = "^0.27.2" [tool.poetry.group.dev.dependencies] @@ -22,6 +23,7 @@ isort = "^5.13.2" mypy = "^1.11.2" black = "^24.8.0" pytest = "^8.3.3" +pytest-asyncio = "^0.24.0" [build-system] requires = ["poetry-core"] diff --git a/src/app/v2/badges/router.py b/src/app/v2/badges/router.py index e72223b..dfeb5b3 100644 --- a/src/app/v2/badges/router.py +++ b/src/app/v2/badges/router.py @@ -1,7 +1,6 @@ from fastapi import APIRouter, status from app.v2.badges.dtos.response import BadgeListResponseDTO -from app.v2.badges.models.badge import Badge from app.v2.badges.services.badge_service import BadgeService router = APIRouter(prefix="/user/badge", tags=["Badge"]) diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index 3b52ea5..802426d 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -39,20 +39,6 @@ async def level_up(cls, user_id: str) -> int: ) return 1 return 0 - # return { - # "status": "success", - # "message": "레벨업 성공", - # "new_level": new_level, - # "remaining_exp": new_exp, - # } - # - # return { - # "status": "failure", - # "message": "레벨업에 필요한 경험치가 부족합니다", - # "current_level": level, - # "current_exp": current_exp, - # "required_exp": required_exp, - # } async def add_exp(self, user_id: str, exp: int) -> None: level_dto = await self.get_level_info(user_id=user_id) diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index f4e1ceb..4ab1e6f 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -1,82 +1,11 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, Depends -from tortoise.expressions import F - -from app.v2.items.models.item import RewardInventory -from app.v2.likes.models.like import Like -from app.v2.missions.dtos.request import MissionProgressRequest -from app.v2.missions.dtos.response import ( - ApiResponse, - MissionProgressResponse, - UserLevelResponse, -) -from app.v2.missions.models.mission import MissionInventory, UserMission from app.v2.missions.services.mission_service import MissionService -from app.v2.users.models.user import User from common.utils.get_user_id import get_user_id router = APIRouter(prefix="/mission", tags=["Mission"]) -@router.post("/level") -def level_up_handler(): - pass - - -@router.post("/update-mission-progress", response_model=ApiResponse) -async def update_mission_progress( - request: MissionProgressRequest, user_id: str = Depends(get_user_id) -): - user: User = await User.get_user_by_uuid(user_id) - - if user is None: - raise HTTPException(status_code=404, detail="User not found") - - # 미션 정보를 가져옵니다. - mission = await MissionInventory.get_or_none(mission_code=request.mission_code) - if not mission: - raise HTTPException(status_code=404, detail="Mission not found") - - user_mission, created = await UserMission.get_or_create( - user=user, - mission_code=request.mission_code, - defaults={"progress_count": 0, "is_completed": False}, - ) - - level_up = False - if not user_mission.is_completed: - # 미션 진행 상황을 업데이트합니다. - user_mission.progress_count = F("progress_count") + request.progress_count - await user_mission.save() - await user_mission.refresh_from_db() - - # 미션 완료 여부를 확인합니다. - if user_mission.progress_count >= mission.target_count: - user_mission.is_completed = True - await user_mission.save() - - # 레벨업 미션인 경우 처리 - if mission.condition_type == "LEVEL_UP": - user.level.user_level += 1 - await user.level.save() - level_up = True - # 여기에 레벨업 보상 로직을 추가할 수 있습니다. - - return ApiResponse( - mission_progress=MissionProgressResponse( - mission_code=mission.mission_code, - progress_count=user_mission.progress_count, - is_completed=user_mission.is_completed, - mission_name=mission.mission_name, - mission_description=mission.mission_description, - target_count=mission.target_count, - ), - user_level_info=UserLevelResponse( - user_level=user.level.user_level, level_up=level_up - ), - ) - - @router.get("/get-user-missions") async def mission_test_handler(user_id: str = Depends(get_user_id)): @@ -84,7 +13,7 @@ async def mission_test_handler(user_id: str = Depends(get_user_id)): await mission_service.update_mission_progress(user_id=user_id) - await mission_service.validate_reward(reward_code="RW_FIRST_POST") + # await mission_service.validate_reward(reward_code="RW_FIRST_POST") # 3. 특정 시간대 미션도 체크하여 진행 # if "time_check" in action_data: diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index b82ffbe..63e377e 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -234,7 +234,6 @@ async def process_reward( user_id: str, cheese_manager_id: str, ): - for item_inventory_reward in item_inventory_rewards: item: ItemInventory = await item_inventory_reward.item_inventory quantity = item_inventory_reward.quantity diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 8b0f14a..9f3e5c2 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -5,7 +5,6 @@ from app.v2.answers.services.answer_service import AnswerService -from app.v2.badges.models.badge import Badge from app.v2.badges.services.badge_service import BadgeService from app.v2.cheese_managers.models.cheese_manager import CheeseManager from app.v2.cheese_managers.services.cheese_service import CheeseService @@ -24,7 +23,6 @@ from app.v2.teller_cards.services.teller_card_service import TellerCardService from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO -from app.v2.users.models.user import User from app.v2.users.services.user_service import UserService router = APIRouter(prefix="/mobiles", tags=["모바일 화면용 컨트롤러"]) diff --git a/src/app/v2/purchases/dtos/requests.py b/src/app/v2/purchases/dtos/requests.py new file mode 100644 index 0000000..21d211c --- /dev/null +++ b/src/app/v2/purchases/dtos/requests.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class ReceiptRequest(BaseModel): + receipt_data: str + user_id: str diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py new file mode 100644 index 0000000..b7e410d --- /dev/null +++ b/src/app/v2/purchases/models/purchase_history.py @@ -0,0 +1,18 @@ +# models.py +from tortoise.models import Model +from tortoise import fields + + +# models.py (계속) +class PurchaseHistory(Model): + purchase_history_id = fields.BigIntField(pk=True) + product_code = fields.CharField(max_length=255) + status = fields.CharField(max_length=255) + receipt_id = fields.CharField(max_length=255, unique=True) # 영수증 중복 방지 + user = fields.ForeignKeyField("models.User", related_name="purchase_histories") + + class Meta: + table = "purchase_history" + unique_together = ("receipt_id",) + indexes = ["user_id"] + diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index b5079ec..8c93d7c 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -6,53 +6,40 @@ ItemInventoryProductInventory, ItemInventory, ) +from app.v2.purchases.dtos.requests import ReceiptRequest from app.v2.purchases.services.purchase_service import PurchaseService +from app.v2.users.services.user_service import UserService router = APIRouter(prefix="/purchase", tags=["Purchase"]) +@router.post("/process-receipt/") +async def process_receipt(receipt: ReceiptRequest): + if not receipt.receipt_data or not receipt.user_id: + raise HTTPException(status_code=400, detail="Missing data") + purchase_service = PurchaseService() + # Apple 서버에서 영수증 검증으로 이동 + return await purchase_service.validate_receipt( + receipt.receipt_data, receipt.user_id + ) + + @router.post("") async def process_purchase(product_code: str): try: - user_id = user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - - product = await ProductInventory.get(product_code=product_code) - - if product.transaction_currency not in ["KRW", "CHEESE"]: - raise HTTPException( - status_code=400, detail="Invalid transaction currency for purchase." - ) - - item_inventory_products = await ItemInventoryProductInventory.filter( - product_inventory_id=product.product_id - ).all() - - if not item_inventory_products: - raise HTTPException( - status_code=404, detail="No inventory found for this product." - ) - - for item_inventory_product in item_inventory_products: - item: ItemInventory = await item_inventory_product.item_inventory - quantity = item_inventory_product.quantity - - if product.transaction_currency == "KRW": - await PurchaseService.process_krw_payment(product, quantity) - - if item.item_category == "SUBSCRIPTION": - await PurchaseService.process_subscription( - item, quantity, item_inventory_product.item_measurement - ) - elif item.item_category == "POINT": - await PurchaseService.add_points(user_id, quantity) - elif item.item_category == "CHEESE": - await PurchaseService.add_cheese(user_id, quantity) - else: - raise ValueError( - f"Invalid item category for purchase: {item.item_category}" - ) - - return {"message": "Purchase successful", "product": product} + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + + item_inventory_products = await PurchaseService.validate_purchase(product_code) + + user = await UserService.get_user_info(user_id=user_id) + + await PurchaseService.process_purchase( + item_inventory_products=item_inventory_products, + user_id=user_id, + cheese_manager_id=user["cheese_manager_id"], + ) + + return {"message": "Purchase successful", "product": product_code} except DoesNotExist: raise HTTPException(status_code=404, detail="Product not found.") diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 9f6124f..148eca2 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,5 +1,15 @@ -from app.v2.items.models.item import ProductInventory, ItemInventory +from tortoise.exceptions import DoesNotExist + +from app.v2.cheese_managers.services.cheese_service import CheeseService +from app.v2.items.models.item import ( + ProductInventory, + ItemInventory, + ItemInventoryProductInventory, +) +from app.v2.purchases.models.purchase_history import PurchaseHistory from app.v2.users.services.user_service import UserService +import httpx +from fastapi import HTTPException class PurchaseService: @@ -8,19 +18,121 @@ async def process_krw_payment(product: ProductInventory, quantity: int): print(f"Processing KRW payment: {product.price * quantity} KRW") # 여기에 실제 KRW 결제 처리 로직 구현 + async def validate_receipt(self, receipt_data: str, user_id: str): + url = "https://buy.itunes.apple.com/verifyReceipt" # sandbox: "https://sandbox.itunes.apple.com/verifyReceipt" + payload = { + "receipt-data": receipt_data, + "password": "YOUR_APP_SHARED_SECRET", # Apple에서 제공받은 앱의 공유 비밀번호 + } + + async with httpx.AsyncClient() as client: + response = await client.post(url, json=payload) + + if response.status_code == 200: + return await self._handle_receipt_response(response.json(), user_id) + else: + raise HTTPException( + status_code=500, detail="Failed to connect to Apple server" + ) + + async def _handle_receipt_response(self, response_data: dict, user_id: str): + if response_data.get("status") == 0: + in_app_purchase = response_data.get("receipt", {}).get("in_app", []) + if in_app_purchase: + purchase_info = in_app_purchase[0] + return await self._save_purchase_history(user_id, purchase_info) + else: + raise HTTPException(status_code=400, detail="No in-app purchase found") + else: + raise HTTPException(status_code=400, detail="Invalid receipt") + + async def _save_purchase_history(user_id: bytes, purchase_info: dict): + receipt_id = purchase_info.get("transaction_id") + product_code = purchase_info.get("product_id") + status = "completed" + + existing_purchase = await PurchaseHistory.filter(receipt_id=receipt_id).exists() + if existing_purchase: + raise HTTPException(status_code=400, detail="Duplicate receipt") + + purchase_history = await PurchaseHistory.create( + product_code=product_code, + status=status, + receipt_id=receipt_id, + user_id=user_id, + ) + return { + "message": "Purchase history saved", + "purchase_history_id": purchase_history.purchase_history_id, + } + @staticmethod - async def process_subscription( - item: ItemInventory, quantity: int, measurement: str + async def validate_purchase(product_code: str): + try: + product = await ProductInventory.get(product_code=product_code) + + if product.transaction_currency not in ["KRW", "CHEESE"]: + raise HTTPException( + status_code=400, detail="Invalid transaction currency for purchase." + ) + + item_inventory_products = await ItemInventoryProductInventory.filter( + product_inventory_id=product.product_id + ).all() + + if not item_inventory_products: + raise HTTPException( + status_code=404, detail="No inventory found for this product." + ) + return item_inventory_products + except DoesNotExist: + raise HTTPException(status_code=404, detail="Product not found.") + + @classmethod + async def process_purchase( + cls, + item_inventory_products: list[ItemInventoryProductInventory], + user_id: str, + cheese_manager_id: str, ): - print(f"Activating subscription: {item.item_code} for {quantity} {measurement}") - # 구독 활성화 로직 구현 + for item_inventory_product in item_inventory_products: + item: ItemInventory = await item_inventory_product.item_inventory + quantity = item_inventory_product.quantity - @staticmethod - async def add_points(user_id: str, quantity: int): - print(f"Adding {quantity} points to user's balance") - await UserService.add_points(user_id, quantity) + if item.item_category == "SUBSCRIPTION": + await UserService.set_is_premium(user_id=user_id, is_premium=True) + elif item.item_category == "CHEESE": + await CheeseService.add_cheese( + cheese_manager_id=cheese_manager_id, amount=quantity + ) + else: + raise ValueError( + f"Invalid item category for purchase: {item.item_category}" + ) - @staticmethod - async def add_cheese(user_id: str, quantity: int): - print(f"Adding {quantity} cheese to user's balance") - await UserService.add_cheese(user_id, quantity) + # purchase_info = { + # "transaction_id": "1000000654000000", # 고유한 거래 ID (영수증 ID로 사용 가능) + # "product_id": "com.example.app.product1", # 구매한 상품의 ID + # "purchase_date": "2024-11-01T10:30:00Z", # 구매한 날짜와 시간 + # "original_transaction_id": "1000000654000000", # 원래 거래 ID (구독 갱신 시 동일) + # "quantity": "1", # 구매 수량 + # "expires_date": "2024-12-01T10:30:00Z", # 구독 만료 날짜 (구독형 상품인 경우) + # "is_trial_period": "false", # 무료 체험 기간 여부 + # "is_in_intro_offer_period": "false" # 소개 할인 기간 여부 + # } + + # 소모성 + # purchase_info = { + # "transaction_id": "1000000654000000", + # "product_id": "com.example.app.product1", + # "purchase_date": "2024-11-01T10:30:00Z", + # } + + # 구독 + # purchase_info = { + # "transaction_id": "1000000654000000", + # "product_id": "com.example.app.product1", + # "purchase_date": "2024-11-01T10:30:00Z", + # "expires_date": "2024-12-01T10:30:00Z", + # "original_transaction_id": "1000000654000000" + # } diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index f3e365d..c747b42 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -1,14 +1,12 @@ -from typing import Optional, Any +from datetime import datetime -from fastapi import Depends -from tortoise import fields, connections +from tortoise import fields from tortoise.models import Model from app.v2.users.querys.user_query import ( SELECT_USER_INFO_BY_USER_UUID_QUERY, SELECT_USER_PROFILE_BY_USER_ID_QUERY, - SELECT_USER_TELLER_CARD_ID_BY_USER_UUID_QUERY, - SELECT_USER_BY_UUID_QUERY, + UPDATE_PREMIUM_STATUS_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -79,18 +77,8 @@ async def get_user_info_by_user_id(cls, user_id: str) -> dict | None: ) @classmethod - async def get_teller_card_id_by_user_id(cls, user_id: str) -> int | None: - query = SELECT_USER_TELLER_CARD_ID_BY_USER_UUID_QUERY - value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) - - @classmethod - async def get_user_by_uuid(cls, user_id: str) -> Optional[dict[str, Any]]: - conn = connections.get("default") - result = await conn.execute_query_dict(SELECT_USER_BY_UUID_QUERY, [user_id]) - if not result: - return None - user = result[0] - return user + async def set_is_premium(cls, user_id: str, is_premium: bool) -> None: + query = UPDATE_PREMIUM_STATUS_QUERY + current_time = datetime.now() + values = (int(is_premium), current_time, user_id) + await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/users/querys/user_query.py b/src/app/v2/users/querys/user_query.py index 36a7dca..df963e3 100644 --- a/src/app/v2/users/querys/user_query.py +++ b/src/app/v2/users/querys/user_query.py @@ -26,3 +26,10 @@ FROM user u WHERE {USER_ID_QUERY} """ + +UPDATE_PREMIUM_STATUS_QUERY = f""" + UPDATE `user` + SET `is_premium` = %s, + `premium_started_at` = %s + WHERE {USER_ID_QUERY} + """ diff --git a/src/app/v2/users/services/user_service.py b/src/app/v2/users/services/user_service.py index 940bd59..273b0a9 100644 --- a/src/app/v2/users/services/user_service.py +++ b/src/app/v2/users/services/user_service.py @@ -3,14 +3,14 @@ class UserService: - @classmethod - async def get_user_info(cls, user_id: str) -> dict: + @staticmethod + async def get_user_info(user_id: str) -> dict: return await User.get_user_info_by_user_id(user_id=user_id) - @classmethod - async def get_cheese_balance(cls, cheese_manager_id: str) -> int: - return await CheeseManager.get_total_cheese_amount_by_manager() - @classmethod async def get_user_profile(cls, user_id: str) -> dict: return await User.get_user_profile_by_user_id(user_id=user_id) + + @staticmethod + async def set_is_premium(user_id: str, is_premium: bool) -> None: + await User.set_is_premium(user_id=user_id, is_premium=is_premium) \ No newline at end of file From b8e345177c2a9a309998289b3e20e15cdfbad831 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 10 Nov 2024 23:40:06 +0900 Subject: [PATCH 16/60] =?UTF-8?q?=20=20=F0=9F=90=9B=20fix:=20java=20server?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20Request,=20Response=20format?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/badges/router.py | 3 +-- src/app/v2/cheese_managers/dtos/cheese_dto.py | 17 ++++++++++++++-- src/app/v2/cheese_managers/router.py | 4 ++-- src/app/v2/missions/router.py | 5 ++--- src/app/v2/mobiles/router.py | 7 ++----- src/app/v2/payments/dtos/request.py | 3 ++- src/app/v2/payments/dtos/response.py | 20 +++++++++++++++++++ src/app/v2/payments/router.py | 20 +++++++++++-------- src/app/v2/purchases/dtos/requests.py | 5 +++++ src/app/v2/purchases/router.py | 12 ++++------- src/app/v2/teller_cards/dtos/request.py | 1 + src/app/v2/teller_cards/dtos/response.py | 18 ++++++++++++++++- src/app/v2/teller_cards/models/teller_card.py | 4 ++-- .../teller_cards/querys/teller_card_query.py | 7 ++----- src/app/v2/teller_cards/router.py | 12 +++-------- .../services/teller_card_service.py | 2 +- 16 files changed, 91 insertions(+), 49 deletions(-) create mode 100644 src/app/v2/payments/dtos/response.py diff --git a/src/app/v2/badges/router.py b/src/app/v2/badges/router.py index dfeb5b3..fcf0f77 100644 --- a/src/app/v2/badges/router.py +++ b/src/app/v2/badges/router.py @@ -11,8 +11,7 @@ response_model=BadgeListResponseDTO, status_code=status.HTTP_200_OK, ) -async def get_user_badge_handler(): - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" +async def get_user_badge_handler(user_id: str): badges = await BadgeService.get_badges_with_details_by_user_id(user_id) diff --git a/src/app/v2/cheese_managers/dtos/cheese_dto.py b/src/app/v2/cheese_managers/dtos/cheese_dto.py index 5e7d44a..8834f9d 100644 --- a/src/app/v2/cheese_managers/dtos/cheese_dto.py +++ b/src/app/v2/cheese_managers/dtos/cheese_dto.py @@ -1,9 +1,22 @@ from pydantic import BaseModel +from common.base_models.base_dtos.base_response import BaseResponseDTO -class CheeseResponseDTO(BaseModel): +class CheeseDTO(BaseModel): cheeseBalance: int @classmethod - def builder(cls, cheese_balance: int) -> "CheeseResponseDTO": + def builder(cls, cheese_balance: int) -> "CheeseDTO": return cls(cheeseBalance=cheese_balance) + + +class CheeseResponseDTO(BaseResponseDTO): + data: CheeseDTO + + @classmethod + def builder(cls, cheese_balance: int) -> "CheeseResponseDTO": + return cls( + code=200, + message="success", + data=CheeseDTO.builder(cheese_balance=cheese_balance), + ) diff --git a/src/app/v2/cheese_managers/router.py b/src/app/v2/cheese_managers/router.py index f75d0f9..cd5e602 100644 --- a/src/app/v2/cheese_managers/router.py +++ b/src/app/v2/cheese_managers/router.py @@ -8,8 +8,8 @@ @router.get("", response_model=CheeseResponseDTO, status_code=status.HTTP_200_OK) -async def get_cheese_handler(): - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" +async def get_cheese_handler(user_id: str): + user = await UserService.get_user_info(user_id=user_id) cheese_amount = await CheeseService.get_cheese_balance(user["cheese_manager_id"]) diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 4ab1e6f..9443878 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -1,13 +1,12 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter from app.v2.missions.services.mission_service import MissionService -from common.utils.get_user_id import get_user_id router = APIRouter(prefix="/mission", tags=["Mission"]) @router.get("/get-user-missions") -async def mission_test_handler(user_id: str = Depends(get_user_id)): +async def mission_test_handler(user_id: str): mission_service = MissionService() diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 9f3e5c2..127ec49 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -38,9 +38,7 @@ async def mobile_main_handler(): response_model=TellerCardResponseDTO, status_code=status.HTTP_200_OK, ) -async def mobile_teller_card_handler(): - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - +async def mobile_teller_card_handler(user_id: str): try: badges_task = BadgeService.get_badges_with_details_by_user_id(user_id) colors_task = ColorService.get_colors(user_id) @@ -78,8 +76,7 @@ async def mobile_teller_card_handler(): response_model=MyPageResponseDTO, status_code=status.HTTP_200_OK, ) -async def mobile_my_page_handler(): - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" +async def mobile_my_page_handler(user_id: str): user, answer_count, badge_count, teller_card, level = await asyncio.gather( UserService.get_user_profile(user_id=user_id), diff --git a/src/app/v2/payments/dtos/request.py b/src/app/v2/payments/dtos/request.py index ff79865..fb55b02 100644 --- a/src/app/v2/payments/dtos/request.py +++ b/src/app/v2/payments/dtos/request.py @@ -2,4 +2,5 @@ class PaymentRequestDTO(BaseModel): - productId: str + user_id: str + productCode: str diff --git a/src/app/v2/payments/dtos/response.py b/src/app/v2/payments/dtos/response.py new file mode 100644 index 0000000..e8dc89b --- /dev/null +++ b/src/app/v2/payments/dtos/response.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel +from common.base_models.base_dtos.base_response import BaseResponseDTO + + +class ProductDTO(BaseModel): + product_code: str + + +class PaymentResponseDTO(BaseResponseDTO): + data: ProductDTO + + @classmethod + def builder(cls, product_code: str) -> "PaymentResponseDTO": + return cls( + code=200, + message="Payment successful", + data=ProductDTO( + product_code=product_code, + ), + ) diff --git a/src/app/v2/payments/router.py b/src/app/v2/payments/router.py index 83e79c2..fbd3a56 100644 --- a/src/app/v2/payments/router.py +++ b/src/app/v2/payments/router.py @@ -1,17 +1,22 @@ -from fastapi import APIRouter, HTTPException - +from fastapi import APIRouter, HTTPException, status +from app.v2.payments.dtos.request import PaymentRequestDTO +from app.v2.payments.dtos.response import PaymentResponseDTO from app.v2.payments.services.payment_service import PaymentService from app.v2.users.services.user_service import UserService router = APIRouter(prefix="/payment", tags=["Payment"]) -# current_user: User = Depends(get_current_user) -@router.post("") -async def process_payment(product_code: str): +@router.post( + "", + response_model=PaymentResponseDTO, + status_code=status.HTTP_200_OK, +) +async def process_payment(request: PaymentRequestDTO): try: - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + user_id = request.user_id + product_code = request.productCode product, item_inventory_products = await PaymentService.validate_payment( product_code @@ -22,8 +27,7 @@ async def process_payment(product_code: str): await PaymentService.process_cheese_payment( product, item_inventory_products, user_id, user["cheese_manager_id"] ) - - return {"message": "Payment successful", "product": product} + return PaymentResponseDTO.builder(product_code=product.product_code) except HTTPException as e: raise e diff --git a/src/app/v2/purchases/dtos/requests.py b/src/app/v2/purchases/dtos/requests.py index 21d211c..0a89136 100644 --- a/src/app/v2/purchases/dtos/requests.py +++ b/src/app/v2/purchases/dtos/requests.py @@ -4,3 +4,8 @@ class ReceiptRequest(BaseModel): receipt_data: str user_id: str + + +class PurchaseRequest(BaseModel): + user_id: str + product_code: str diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 8c93d7c..ed4a36f 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -1,12 +1,7 @@ from fastapi import HTTPException, APIRouter from tortoise.exceptions import DoesNotExist -from app.v2.items.models.item import ( - ProductInventory, - ItemInventoryProductInventory, - ItemInventory, -) -from app.v2.purchases.dtos.requests import ReceiptRequest +from app.v2.purchases.dtos.requests import ReceiptRequest, PurchaseRequest from app.v2.purchases.services.purchase_service import PurchaseService from app.v2.users.services.user_service import UserService @@ -25,9 +20,10 @@ async def process_receipt(receipt: ReceiptRequest): @router.post("") -async def process_purchase(product_code: str): +async def process_purchase(request: PurchaseRequest): try: - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + user_id = request.user_id + product_code = request.product_code item_inventory_products = await PurchaseService.validate_purchase(product_code) diff --git a/src/app/v2/teller_cards/dtos/request.py b/src/app/v2/teller_cards/dtos/request.py index 6bda2a9..479ec16 100644 --- a/src/app/v2/teller_cards/dtos/request.py +++ b/src/app/v2/teller_cards/dtos/request.py @@ -2,5 +2,6 @@ class TellerCardRequestDTO(BaseModel): + user_id: str colorCode: str badgeCode: str diff --git a/src/app/v2/teller_cards/dtos/response.py b/src/app/v2/teller_cards/dtos/response.py index 2418d2a..e6087ca 100644 --- a/src/app/v2/teller_cards/dtos/response.py +++ b/src/app/v2/teller_cards/dtos/response.py @@ -1,6 +1,22 @@ from pydantic import BaseModel +from common.base_models.base_dtos.base_response import BaseResponseDTO -class TellerCardResponseDTO(BaseModel): + +class TellerCardDTO(BaseModel): colorCode: str badgeCode: str + + +class TellerCardResponseDTO(BaseResponseDTO): + data: TellerCardDTO + + @classmethod + def builder(cls, teller_card: TellerCardDTO) -> "TellerCardResponseDTO": + return cls( + code=200, + message="success", + data=TellerCardDTO( + colorCode=teller_card.colorCode, badgeCode=teller_card.badgeCode + ), + ) diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py index 475b2ad..b88cf6a 100644 --- a/src/app/v2/teller_cards/models/teller_card.py +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -3,7 +3,7 @@ from app.v2.teller_cards.querys.teller_card_query import ( SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY, - PATCH_TELLER_CARD_BY_USER_UUID_QUERY, + PATCH_TELLER_CARD_QUERY, ) from common.utils.query_executor import QueryExecutor @@ -28,6 +28,6 @@ async def get_teller_card_info_by_user_id(cls, user_id: str) -> dict | None: async def patch_teller_card_info_by_user_id( cls, user_id: str, badge_code: str, color_code: str ) -> None: - query = PATCH_TELLER_CARD_BY_USER_UUID_QUERY + query = PATCH_TELLER_CARD_QUERY values = (badge_code, color_code, user_id) await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/teller_cards/querys/teller_card_query.py b/src/app/v2/teller_cards/querys/teller_card_query.py index e8bd424..7d8ed85 100644 --- a/src/app/v2/teller_cards/querys/teller_card_query.py +++ b/src/app/v2/teller_cards/querys/teller_card_query.py @@ -21,7 +21,7 @@ WHERE teller_card_id = ( SELECT u.teller_card_id FROM user u - WHERE {USER_ID_QUERY} + WHERE u.user_id = UNHEX(REPLACE(%s, '-', '')) ) """ @@ -31,9 +31,6 @@ WHERE teller_card_id = ( SELECT u.teller_card_id FROM user u - WHERE {USER_ID_QUERY} + WHERE u.user_id = UNHEX(REPLACE(%s, '-', '')) ) """ -PATCH_TELLER_CARD_BY_USER_UUID_QUERY = PATCH_TELLER_CARD_QUERY.format( - USER_ID_QUERY=USER_ID_QUERY -) diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py index d54d0d8..4bcf61b 100644 --- a/src/app/v2/teller_cards/router.py +++ b/src/app/v2/teller_cards/router.py @@ -8,7 +8,7 @@ router = APIRouter(prefix="/tellercard", tags=["TellerCard"]) -@router.patch( +@router.post( "", response_model=TellerCardResponseDTO, status_code=status.HTTP_200_OK, @@ -16,7 +16,7 @@ async def patch_teller_card_handler( body: TellerCardRequestDTO, ) -> TellerCardResponseDTO: - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + user_id = body.user_id badge_code = (body.badgeCode,) color_code = (body.colorCode,) @@ -26,10 +26,4 @@ async def patch_teller_card_handler( teller_card = await TellerCardService.get_teller_card(user_id=user_id) - activate_badge_code = teller_card["activate_badge_code"] - activate_color_code = teller_card["activate_color_code"] - - return TellerCardResponseDTO( - colorCode=activate_color_code, - badgeCode=activate_badge_code, - ) + return TellerCardResponseDTO.builder(teller_card=teller_card) diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py index 634b4af..74b21fc 100644 --- a/src/app/v2/teller_cards/services/teller_card_service.py +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -16,4 +16,4 @@ async def patch_teller_card( ) -> None: await TellerCard.patch_teller_card_info_by_user_id( user_id=user_id, badge_code=badge_code, color_code=color_code - ) \ No newline at end of file + ) From a701bdf64a4c6056612e89fa41a1e4712714c93b Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Mon, 11 Nov 2024 23:45:58 +0900 Subject: [PATCH 17/60] =?UTF-8?q?=20=F0=9F=90=9B=20fix:=20mypy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 2 +- pyproject.toml | 7 +++ src/app/v2/answers/dtos/answer_dto.py | 5 +- src/app/v2/answers/models/answer.py | 7 +-- src/app/v2/answers/router.py | 5 +- src/app/v2/answers/services/answer_service.py | 4 +- src/app/v2/badges/models/badge.py | 7 +-- src/app/v2/badges/router.py | 2 +- src/app/v2/badges/services/badge_service.py | 2 +- src/app/v2/cheese_managers/dtos/cheese_dto.py | 1 + .../cheese_managers/models/cheese_manager.py | 7 ++- src/app/v2/cheese_managers/router.py | 2 +- .../services/cheese_service.py | 4 +- src/app/v2/colors/models/color.py | 4 +- src/app/v2/emotions/models/emotion.py | 6 +- src/app/v2/emotions/querys/emotion_query.py | 1 - src/app/v2/items/dtos/item_dto.py | 3 +- src/app/v2/levels/models/level.py | 6 +- src/app/v2/levels/services/level_service.py | 5 +- src/app/v2/likes/models/like.py | 3 +- src/app/v2/likes/querys/like_query.py | 2 +- src/app/v2/missions/dtos/mission_dto.py | 2 +- src/app/v2/missions/models/mission.py | 7 +-- src/app/v2/missions/router.py | 3 +- .../v2/missions/services/mission_service.py | 57 +++++++++---------- .../v2/mobiles/dtos/teller_card_response.py | 4 +- src/app/v2/mobiles/router.py | 24 +++----- src/app/v2/payments/dtos/response.py | 1 + src/app/v2/payments/router.py | 2 +- .../v2/payments/services/payment_service.py | 17 +++--- .../v2/purchases/models/purchase_history.py | 3 +- src/app/v2/purchases/router.py | 8 +-- .../v2/purchases/services/purchase_service.py | 27 ++++----- src/app/v2/teller_cards/models/teller_card.py | 6 +- src/app/v2/teller_cards/router.py | 7 +-- src/app/v2/users/dtos/user_info_dto.py | 1 + src/app/v2/users/models/user.py | 10 ++-- src/app/v2/users/services/user_service.py | 2 +- .../base_models/base_dtos/base_response.py | 3 +- .../custom_fields/hex_binary_field.py | 39 ------------- src/common/handlers/exception_handler.py | 7 ++- src/common/handlers/router_handler.py | 16 +++--- src/common/post_construct.py | 6 +- src/common/utils/get_user_id.py | 2 +- src/common/utils/query_executor.py | 7 ++- src/core/configs/base_settings.py | 3 +- src/core/database/database_settings.py | 2 +- src/main.py | 6 +- test.sh | 23 ++++++++ 49 files changed, 178 insertions(+), 202 deletions(-) delete mode 100644 src/common/base_models/custom_fields/hex_binary_field.py create mode 100755 test.sh diff --git a/poetry.lock b/poetry.lock index 0eefbd0..5f7e3f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1581,4 +1581,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "176e0a053b68d9eb52eb4de3d6ab403821bfabf070c8e387161ed9733bc99d27" +content-hash = "72d9120536852950d43796677f4f8c8e80ee7ea17591bb86db6048900c27dfb8" diff --git a/pyproject.toml b/pyproject.toml index a14cb6a..538455d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,13 @@ mypy = "^1.11.2" black = "^24.8.0" pytest = "^8.3.3" pytest-asyncio = "^0.24.0" +mypy-extensions = "^1.0.0" + +[tool.mypy] +files = "src" # 타입 체크할 파일 경로 +disallow_untyped_calls = true # 타입이 없는 함수 호출 금지 +disallow_untyped_defs = true # 타입이 없는 함수 정의 금지 +ignore_missing_imports = true # 누락된 import 무시 [build-system] requires = ["poetry-core"] diff --git a/src/app/v2/answers/dtos/answer_dto.py b/src/app/v2/answers/dtos/answer_dto.py index 931a8fb..12a8a33 100644 --- a/src/app/v2/answers/dtos/answer_dto.py +++ b/src/app/v2/answers/dtos/answer_dto.py @@ -3,8 +3,5 @@ def __init__(self, count: int): self.count = count @classmethod - def builder(cls, count: int): + def builder(cls, count: int) -> "RecordDto": return cls(count) - - def build(self): - return self diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py index 0320e6b..5f029cd 100644 --- a/src/app/v2/answers/models/answer.py +++ b/src/app/v2/answers/models/answer.py @@ -4,9 +4,8 @@ from tortoise.models import Model from app.v2.answers.querys.answer_query import ( - SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY, - SELECT_ANSWER_BY_USER_UUID_QUERY, SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY, -) + SELECT_ANSWER_BY_USER_UUID_QUERY, SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY, + SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY) from common.utils.query_executor import QueryExecutor @@ -59,4 +58,4 @@ async def get_most_recent_answer_by_user_id(cls, user_id: str) -> dict | None: value = user_id return await QueryExecutor.execute_query( query, values=value, fetch_type="single" - ) \ No newline at end of file + ) diff --git a/src/app/v2/answers/router.py b/src/app/v2/answers/router.py index 94612b0..f5e6fcb 100644 --- a/src/app/v2/answers/router.py +++ b/src/app/v2/answers/router.py @@ -1,7 +1,6 @@ from fastapi import APIRouter from app.v2.answers.services.answer_service import AnswerService - from app.v2.levels.services.level_service import LevelService router = APIRouter(prefix="/answer", tags=["Test용"]) @@ -9,14 +8,14 @@ # FastAPI 비동기 뷰 @router.get("") -async def get_answer_record_view(): +async def get_answer_record_view() -> dict: user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" record_dto = await AnswerService.get_answer_record(user_id) return {"user_id": user_id, "consecutive_answer_days": record_dto.count} @router.get("/level-up") -async def level_up_handler(): +async def level_up_handler() -> int: user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" result = await LevelService.level_up(user_id=user_id) return result diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index cfba347..2802486 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -6,7 +6,7 @@ class AnswerService: @classmethod - async def get_answer_count(cls, user_id: str): + async def get_answer_count(cls, user_id: str) -> int: answer_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) return answer_count_raw["answer_count"] @@ -29,4 +29,4 @@ async def get_answer_record(cls, user_id: str) -> "RecordDto": else: break - return RecordDto.builder(count=record).build() + return RecordDto.builder(count=record) diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index f88e15e..57b41cc 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -2,12 +2,9 @@ from tortoise.models import Model from app.v2.badges.querys.badge_query import ( - SELECT_BADGE_BY_USER_UUID_QUERY, + INSERT_BADGE_CODE_FOR_USER_QUERY, SELECT_BADGE_BY_USER_UUID_QUERY, SELECT_BADGE_CODE_BY_USER_UUID_QUERY, - INSERT_BADGE_CODE_FOR_USER_QUERY, - SELECT_BADGE_COUNT_BY_USER_UUID_QUERY, -) - + SELECT_BADGE_COUNT_BY_USER_UUID_QUERY) from common.utils.query_executor import QueryExecutor diff --git a/src/app/v2/badges/router.py b/src/app/v2/badges/router.py index fcf0f77..a3ec584 100644 --- a/src/app/v2/badges/router.py +++ b/src/app/v2/badges/router.py @@ -11,7 +11,7 @@ response_model=BadgeListResponseDTO, status_code=status.HTTP_200_OK, ) -async def get_user_badge_handler(user_id: str): +async def get_user_badge_handler(user_id: str) -> BadgeListResponseDTO: badges = await BadgeService.get_badges_with_details_by_user_id(user_id) diff --git a/src/app/v2/badges/services/badge_service.py b/src/app/v2/badges/services/badge_service.py index 73ab316..7d8ba3d 100644 --- a/src/app/v2/badges/services/badge_service.py +++ b/src/app/v2/badges/services/badge_service.py @@ -13,7 +13,7 @@ async def add_badge(cls, user_id: str, badge_code: str) -> None: await Badge.add_badge(user_id=user_id, badge_code=badge_code) @classmethod - async def get_badges_with_details_by_user_id(cls, user_id: str) -> list[dict]: + async def get_badges_with_details_by_user_id(cls, user_id: str) -> list[BadgeDTO]: badges_raw = await Badge.get_badges_with_details_by_user_id(user_id=user_id) return [BadgeDTO.builder(badge) for badge in badges_raw] diff --git a/src/app/v2/cheese_managers/dtos/cheese_dto.py b/src/app/v2/cheese_managers/dtos/cheese_dto.py index 8834f9d..427b4c6 100644 --- a/src/app/v2/cheese_managers/dtos/cheese_dto.py +++ b/src/app/v2/cheese_managers/dtos/cheese_dto.py @@ -1,4 +1,5 @@ from pydantic import BaseModel + from common.base_models.base_dtos.base_response import BaseResponseDTO diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py index 942c0e2..9e3b9de 100644 --- a/src/app/v2/cheese_managers/models/cheese_manager.py +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -14,7 +14,7 @@ class Meta: table = "cheese_manager" # Database table name @staticmethod - async def get_total_cheese_amount_by_manager(cheese_manager_id: int): + async def get_total_cheese_amount_by_manager(cheese_manager_id: int) -> int: result = ( await CheeseHistory.filter( Q(status=CheeseStatus.CAN_USE) | Q(status=CheeseStatus.USING), @@ -27,7 +27,7 @@ async def get_total_cheese_amount_by_manager(cheese_manager_id: int): return result[0].get("total_cheese_amount", 0) @staticmethod - async def use_cheese(cheese_manager_id: int, amount: int): + async def use_cheese(cheese_manager_id: int, amount: int) -> None: using_cheese = await CheeseHistory.filter( status=CheeseStatus.USING, cheese_manager_id=cheese_manager_id ).order_by("cheese_history_id") @@ -66,7 +66,7 @@ async def use_cheese(cheese_manager_id: int, amount: int): if remaining_amount > 0: raise ValueError("Not enough cheese to complete the transaction") - async def add_cheese(self, amount: int): + async def add_cheese(self, amount: int) -> None: await CheeseHistory.create( status=CheeseStatus.CAN_USE, current_amount=amount, @@ -74,6 +74,7 @@ async def add_cheese(self, amount: int): cheese_manager_id=self.cheese_manager_id, ) + class CheeseHistory(Model): cheese_history_id = fields.BigIntField(pk=True) status = fields.CharEnumField(CheeseStatus, max_length=50, null=True) # Enum Field diff --git a/src/app/v2/cheese_managers/router.py b/src/app/v2/cheese_managers/router.py index cd5e602..d784b0a 100644 --- a/src/app/v2/cheese_managers/router.py +++ b/src/app/v2/cheese_managers/router.py @@ -8,7 +8,7 @@ @router.get("", response_model=CheeseResponseDTO, status_code=status.HTTP_200_OK) -async def get_cheese_handler(user_id: str): +async def get_cheese_handler(user_id: str) -> CheeseResponseDTO: user = await UserService.get_user_info(user_id=user_id) cheese_amount = await CheeseService.get_cheese_balance(user["cheese_manager_id"]) diff --git a/src/app/v2/cheese_managers/services/cheese_service.py b/src/app/v2/cheese_managers/services/cheese_service.py index a3ca0f5..23f0418 100644 --- a/src/app/v2/cheese_managers/services/cheese_service.py +++ b/src/app/v2/cheese_managers/services/cheese_service.py @@ -10,7 +10,7 @@ async def get_cheese_balance(cls, cheese_manager_id: str) -> int: ) @classmethod - async def add_cheese(cls, cheese_manager_id: str, amount: int): + async def add_cheese(cls, cheese_manager_id: str, amount: int) -> None: await CheeseManager.add_cheese( cheese_manager_id=cheese_manager_id, amount=amount - ) \ No newline at end of file + ) diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 1966878..678328c 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -3,9 +3,7 @@ from app.v2.colors.dtos.color_dto import ColorCodeDTO from app.v2.colors.querys.color_query import ( - SELECT_COLOR_CODE_BY_USER_UUID_QUERY, - INSERT_COLOR_CODE_FOR_USER_QUERY, -) + INSERT_COLOR_CODE_FOR_USER_QUERY, SELECT_COLOR_CODE_BY_USER_UUID_QUERY) from common.utils.query_executor import QueryExecutor diff --git a/src/app/v2/emotions/models/emotion.py b/src/app/v2/emotions/models/emotion.py index 355dd1a..e373147 100644 --- a/src/app/v2/emotions/models/emotion.py +++ b/src/app/v2/emotions/models/emotion.py @@ -1,9 +1,7 @@ from tortoise import fields, models from app.v2.emotions.querys.emotion_query import ( - INSERT_EMOTION_CODE_FOR_USER_QUERY, - SELECT_EMOTION_CODE_BY_USER_UUID_QUERY, -) + INSERT_EMOTION_CODE_FOR_USER_QUERY, SELECT_EMOTION_CODE_BY_USER_UUID_QUERY) from common.utils.query_executor import QueryExecutor @@ -40,4 +38,4 @@ class Meta: @classmethod async def get_emotion_inventory(cls) -> list[dict]: - return cls.all().values("emotion_code", "emotion_name") + return await cls.all().values("emotion_code", "emotion_name") diff --git a/src/app/v2/emotions/querys/emotion_query.py b/src/app/v2/emotions/querys/emotion_query.py index d1c7703..813d727 100644 --- a/src/app/v2/emotions/querys/emotion_query.py +++ b/src/app/v2/emotions/querys/emotion_query.py @@ -1,6 +1,5 @@ from app.v2.users.querys.user_query import USER_ID_QUERY - SELECT_EMOTION_CODE_BY_USER_UUID_QUERY = f""" SELECT emotion_code FROM emotion diff --git a/src/app/v2/items/dtos/item_dto.py b/src/app/v2/items/dtos/item_dto.py index 8d7336c..554b2be 100644 --- a/src/app/v2/items/dtos/item_dto.py +++ b/src/app/v2/items/dtos/item_dto.py @@ -1,7 +1,8 @@ # schemas.py -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + class ItemInventorySchema(BaseModel): item_category: Optional[str] diff --git a/src/app/v2/levels/models/level.py b/src/app/v2/levels/models/level.py index 64989bf..315e059 100644 --- a/src/app/v2/levels/models/level.py +++ b/src/app/v2/levels/models/level.py @@ -2,10 +2,8 @@ from tortoise.models import Model from app.v2.levels.querys.level_query import ( - SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, - SELECT_USER_EXP_QUERY, - UPDATE_USER_LEVEL_AND_EXP_QUERY, -) + SELECT_USER_EXP_QUERY, SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, + UPDATE_USER_LEVEL_AND_EXP_QUERY) from common.utils.query_executor import QueryExecutor diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index 802426d..e36648d 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -40,8 +40,9 @@ async def level_up(cls, user_id: str) -> int: return 1 return 0 - async def add_exp(self, user_id: str, exp: int) -> None: - level_dto = await self.get_level_info(user_id=user_id) + @classmethod + async def add_exp(cls, user_id: str, exp: int) -> None: + level_dto = await cls.get_level_info(user_id=user_id) current_exp = level_dto.current_exp new_exp = current_exp + exp diff --git a/src/app/v2/likes/models/like.py b/src/app/v2/likes/models/like.py index deacd61..5a87711 100644 --- a/src/app/v2/likes/models/like.py +++ b/src/app/v2/likes/models/like.py @@ -1,7 +1,8 @@ from tortoise import fields from tortoise.models import Model -from app.v2.likes.querys.like_query import SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY +from app.v2.likes.querys.like_query import \ + SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY from common.utils.query_executor import QueryExecutor diff --git a/src/app/v2/likes/querys/like_query.py b/src/app/v2/likes/querys/like_query.py index 84ce2de..a8780dd 100644 --- a/src/app/v2/likes/querys/like_query.py +++ b/src/app/v2/likes/querys/like_query.py @@ -3,4 +3,4 @@ FROM likes WHERE user_id = UNHEX(REPLACE(%s, '-', '')) AND DATE(created_time) = CURDATE(); -""" \ No newline at end of file +""" diff --git a/src/app/v2/missions/dtos/mission_dto.py b/src/app/v2/missions/dtos/mission_dto.py index e7cea50..2968973 100644 --- a/src/app/v2/missions/dtos/mission_dto.py +++ b/src/app/v2/missions/dtos/mission_dto.py @@ -8,7 +8,7 @@ class UserMissionDTO(BaseModel): progress_count: int @classmethod - def builder(cls, user_mission) -> "UserMissionDTO": + def builder(cls, user_mission: dict) -> "UserMissionDTO": is_completed_raw = user_mission.get("is_completed") is_completed = ( bool(int.from_bytes(is_completed_raw, byteorder="big")) diff --git a/src/app/v2/missions/models/mission.py b/src/app/v2/missions/models/mission.py index e542357..7c1ad3b 100644 --- a/src/app/v2/missions/models/mission.py +++ b/src/app/v2/missions/models/mission.py @@ -2,8 +2,7 @@ from tortoise.models import Model from app.v2.missions.querys.mission_query import ( - UPDATE_USER_MISSION_PROGRESS_QUERY, SELECT_USER_MISSIONS_QUERY, -) + SELECT_USER_MISSIONS_QUERY, UPDATE_USER_MISSION_PROGRESS_QUERY) from common.utils.query_executor import QueryExecutor @@ -15,7 +14,7 @@ class UserMission(Model): user = fields.ForeignKeyField("models.User", related_name="missions") @classmethod - async def get_user_missions_by_condition_type(cls, user_id: str): + async def get_user_missions_by_condition_type(cls, user_id: str) -> dict: # 유저 ID와 미션 조건 타입에 따른 미션 필터링 query = SELECT_USER_MISSIONS_QUERY values = (user_id,) @@ -30,7 +29,7 @@ async def update_user_mission_progress( mission_code: str, new_progress_count: int, is_completed: bool, - ): + ) -> None: query = UPDATE_USER_MISSION_PROGRESS_QUERY values = (new_progress_count, int(is_completed), user_id, mission_code) await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 9443878..86681a3 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -1,12 +1,13 @@ from fastapi import APIRouter +from app.v2.answers.models.answer import Answer from app.v2.missions.services.mission_service import MissionService router = APIRouter(prefix="/mission", tags=["Mission"]) @router.get("/get-user-missions") -async def mission_test_handler(user_id: str): +async def mission_test_handler(user_id: str) -> None: mission_service = MissionService() diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 63e377e..7047b3d 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from fastapi import HTTPException from tortoise.exceptions import DoesNotExist @@ -9,21 +9,19 @@ from app.v2.badges.services.badge_service import BadgeService from app.v2.cheese_managers.services.cheese_service import CheeseService from app.v2.colors.services.color_service import ColorService -from app.v2.items.models.item import ( - RewardInventory, - ItemInventoryRewardInventory, - ItemInventory, -) +from app.v2.items.models.item import (ItemInventory, + ItemInventoryRewardInventory, + RewardInventory) from app.v2.levels.services.level_service import LevelService from app.v2.likes.models.like import Like from app.v2.missions.dtos.mission_dto import UserMissionDTO -from app.v2.missions.models.mission import UserMission, MissionInventory +from app.v2.missions.models.mission import MissionInventory, UserMission from app.v2.users.services.user_service import UserService class MissionService: @staticmethod - async def get_user_missions(user_id: str): + async def get_user_missions(user_id: str) -> list[UserMissionDTO]: user_mission_raw = await UserMission.get_user_missions_by_condition_type( user_id ) @@ -37,7 +35,7 @@ async def _update_user_mission_progress( mission_code: str, new_progress_count: int, is_completed: bool, - ): + ) -> None: await UserMission.update_user_mission_progress( user_id=user_id, mission_code=mission_code, @@ -45,7 +43,7 @@ async def _update_user_mission_progress( is_completed=is_completed, ) - async def update_mission_progress(self, user_id: str): + async def update_mission_progress(self, user_id: str) -> None: user = await UserService.get_user_info(user_id=user_id) user_missions = await self.get_user_missions(user_id) missions = await MissionInventory.all() @@ -83,7 +81,7 @@ async def update_mission_progress(self, user_id: str): is_completed=user_mission.is_completed, ) - async def evaluate_mission_condition(self, user_id, mission_code): + async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> int: user_level_data = await LevelService.get_level_info(user_id) current_level = user_level_data.level @@ -130,33 +128,35 @@ async def evaluate_mission_condition(self, user_id, mission_code): return 0 @staticmethod - async def check_first_post(user_id) -> bool: + async def check_first_post(user_id: str) -> bool: post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) return post_count_raw.get("answer_count", 0) > 0 @staticmethod - async def check_post_count_range(user_id, min_count, max_count): + async def check_post_count_range( + user_id: str, min_count: int, max_count: int + ) -> bool: post_count = await Answer.get_answer_count_by_user_id(user_id=user_id) - return min_count <= post_count <= max_count + return min_count <= post_count.get("answer_count", 0) <= max_count @staticmethod - async def check_post_count_min(user_id, min_count): + async def check_post_count_min(user_id: str, min_count: int) -> bool: post_count = await Answer.get_answer_count_by_user_id(user_id=user_id) - return post_count >= min_count + return post_count.get("answer_count", 0) >= min_count @staticmethod - async def check_long_answer(user_id) -> bool: + async def check_long_answer(user_id: str) -> bool: recent_answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) return len(recent_answer["content"]) >= 280 if recent_answer else False @staticmethod - async def check_consecutive_days(user_id) -> bool: + async def check_consecutive_days(user_id: str) -> bool: record_dto = await AnswerService.get_answer_record(user_id) consecutive_days = record_dto.count return consecutive_days >= 7 @staticmethod - async def check_early_morning_posts(user_id) -> bool: + async def check_early_morning_posts(user_id: str) -> bool: recent_answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) if recent_answer: answer_time = recent_answer.get("created_time") @@ -164,7 +164,7 @@ async def check_early_morning_posts(user_id) -> bool: return False @staticmethod - async def check_cheese_total(user_id) -> bool: + async def check_cheese_total(user_id: str) -> bool: user = await UserService.get_user_info(user_id=user_id) cheese_amount = await CheeseService.get_cheese_balance( user["cheese_manager_id"] @@ -183,7 +183,7 @@ async def check_christmas_period() -> bool: return start_date <= now <= end_date @staticmethod - async def check_three_likes_different_posts(user_id) -> bool: + async def check_three_likes_different_posts(user_id: str) -> bool: like_raw = await Like.get_unique_likes_today(user_id) like_count = like_raw.get("unique_likes", 0) return like_count >= 3 @@ -193,23 +193,20 @@ async def reward_user_for_mission( user_id: str, reward_code: str, cheese_manager_id: str, - ): + ) -> None: # 보상 검증 - reward, item_inventory_rewards = await self.validate_reward( - reward_code=reward_code - ) + item_inventory_rewards = await self.validate_reward(reward_code=reward_code) # 보상 지급 await self.process_reward( - reward_code=reward.reward_code, item_inventory_rewards=item_inventory_rewards, user_id=user_id, cheese_manager_id=cheese_manager_id, ) @staticmethod - async def validate_reward(reward_code: str): + async def validate_reward(reward_code: str) -> list[ItemInventoryRewardInventory]: try: # reward_code를 기반으로 RewardInventory에서 보상 조회 reward = await RewardInventory.get(reward_code=reward_code) @@ -221,7 +218,7 @@ async def validate_reward(reward_code: str): raise HTTPException( status_code=404, detail="No inventory found for this reward." ) - return reward, item_inventory_rewards + return item_inventory_rewards except DoesNotExist: raise HTTPException(status_code=404, detail="Reward not found.") @@ -233,7 +230,7 @@ async def process_reward( item_inventory_rewards: list[ItemInventoryRewardInventory], user_id: str, cheese_manager_id: str, - ): + ) -> None: for item_inventory_reward in item_inventory_rewards: item: ItemInventory = await item_inventory_reward.item_inventory quantity = item_inventory_reward.quantity @@ -253,7 +250,7 @@ async def process_reward( cheese_manager_id=cheese_manager_id, amount=quantity ) elif item.item_category == "POINT": - await LevelService.add_exp(user_id=user_id, point=quantity) + await LevelService.add_exp(user_id=user_id, exp=quantity) else: raise ValueError( f"Invalid item category for reward: {item.item_category}" diff --git a/src/app/v2/mobiles/dtos/teller_card_response.py b/src/app/v2/mobiles/dtos/teller_card_response.py index acd1d9a..c603526 100644 --- a/src/app/v2/mobiles/dtos/teller_card_response.py +++ b/src/app/v2/mobiles/dtos/teller_card_response.py @@ -1,10 +1,10 @@ -from pydantic import BaseModel from typing import List, Optional +from pydantic import BaseModel + from app.v2.badges.dtos.badge_dto import BadgeDTO from app.v2.colors.dtos.color_dto import ColorCodeDTO from app.v2.levels.dtos.level_dto import LevelDTO - from app.v2.users.dtos.user_info_dto import UserInfoDTO from common.base_models.base_dtos.base_response import BaseResponseDTO diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 127ec49..8e1e91b 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -1,25 +1,17 @@ import asyncio -from fastapi import APIRouter, status, HTTPException - +from fastapi import APIRouter, HTTPException, status from app.v2.answers.services.answer_service import AnswerService - from app.v2.badges.services.badge_service import BadgeService from app.v2.cheese_managers.models.cheese_manager import CheeseManager from app.v2.cheese_managers.services.cheese_service import CheeseService - from app.v2.colors.services.color_service import ColorService - from app.v2.levels.services.level_service import LevelService - -from app.v2.mobiles.dtos.mypage_response import ( - UserProfileWithLevel, - MyPageResponseDTO, -) -from app.v2.mobiles.dtos.teller_card_response import DataDTO, TellerCardResponseDTO - - +from app.v2.mobiles.dtos.mypage_response import (MyPageResponseDTO, + UserProfileWithLevel) +from app.v2.mobiles.dtos.teller_card_response import (DataDTO, + TellerCardResponseDTO) from app.v2.teller_cards.services.teller_card_service import TellerCardService from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO @@ -29,7 +21,7 @@ @router.post("/main") -async def mobile_main_handler(): +async def mobile_main_handler() -> None: pass @@ -38,7 +30,7 @@ async def mobile_main_handler(): response_model=TellerCardResponseDTO, status_code=status.HTTP_200_OK, ) -async def mobile_teller_card_handler(user_id: str): +async def mobile_teller_card_handler(user_id: str) -> TellerCardResponseDTO: try: badges_task = BadgeService.get_badges_with_details_by_user_id(user_id) colors_task = ColorService.get_colors(user_id) @@ -76,7 +68,7 @@ async def mobile_teller_card_handler(user_id: str): response_model=MyPageResponseDTO, status_code=status.HTTP_200_OK, ) -async def mobile_my_page_handler(user_id: str): +async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: user, answer_count, badge_count, teller_card, level = await asyncio.gather( UserService.get_user_profile(user_id=user_id), diff --git a/src/app/v2/payments/dtos/response.py b/src/app/v2/payments/dtos/response.py index e8dc89b..490deb4 100644 --- a/src/app/v2/payments/dtos/response.py +++ b/src/app/v2/payments/dtos/response.py @@ -1,4 +1,5 @@ from pydantic import BaseModel + from common.base_models.base_dtos.base_response import BaseResponseDTO diff --git a/src/app/v2/payments/router.py b/src/app/v2/payments/router.py index fbd3a56..16b71c4 100644 --- a/src/app/v2/payments/router.py +++ b/src/app/v2/payments/router.py @@ -13,7 +13,7 @@ response_model=PaymentResponseDTO, status_code=status.HTTP_200_OK, ) -async def process_payment(request: PaymentRequestDTO): +async def process_payment(request: PaymentRequestDTO) -> PaymentResponseDTO: try: user_id = request.user_id product_code = request.productCode diff --git a/src/app/v2/payments/services/payment_service.py b/src/app/v2/payments/services/payment_service.py index 491c887..2f8ec79 100644 --- a/src/app/v2/payments/services/payment_service.py +++ b/src/app/v2/payments/services/payment_service.py @@ -6,16 +6,16 @@ from app.v2.cheese_managers.models.cheese_manager import CheeseManager from app.v2.colors.services.color_service import ColorService from app.v2.emotions.services.emotion_service import EmotionService -from app.v2.items.models.item import ( - ItemInventory, - ProductInventory, - ItemInventoryProductInventory, -) +from app.v2.items.models.item import (ItemInventory, + ItemInventoryProductInventory, + ProductInventory) class PaymentService: @staticmethod - async def validate_payment(product_code: str): + async def validate_payment( + product_code: str, + ) -> tuple[ProductInventory, list[ItemInventoryProductInventory]]: try: product = await ProductInventory.get(product_code=product_code) @@ -45,8 +45,8 @@ async def process_cheese_payment( product: ProductInventory, item_inventory_products: list[ItemInventoryProductInventory], user_id: str, - cheese_manager_id: str, - ): + cheese_manager_id: int, + ) -> None: total_cheese = await CheeseManager.get_total_cheese_amount_by_manager( cheese_manager_id=cheese_manager_id ) @@ -86,4 +86,3 @@ async def process_cheese_payment( raise ValueError( f"Invalid item category for cheese payment: {item.item_category}" ) - diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py index b7e410d..16cf433 100644 --- a/src/app/v2/purchases/models/purchase_history.py +++ b/src/app/v2/purchases/models/purchase_history.py @@ -1,6 +1,6 @@ # models.py -from tortoise.models import Model from tortoise import fields +from tortoise.models import Model # models.py (계속) @@ -15,4 +15,3 @@ class Meta: table = "purchase_history" unique_together = ("receipt_id",) indexes = ["user_id"] - diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index ed4a36f..9d71e23 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -1,7 +1,7 @@ -from fastapi import HTTPException, APIRouter +from fastapi import APIRouter, HTTPException from tortoise.exceptions import DoesNotExist -from app.v2.purchases.dtos.requests import ReceiptRequest, PurchaseRequest +from app.v2.purchases.dtos.requests import PurchaseRequest, ReceiptRequest from app.v2.purchases.services.purchase_service import PurchaseService from app.v2.users.services.user_service import UserService @@ -9,7 +9,7 @@ @router.post("/process-receipt/") -async def process_receipt(receipt: ReceiptRequest): +async def process_receipt(receipt: ReceiptRequest) -> dict: if not receipt.receipt_data or not receipt.user_id: raise HTTPException(status_code=400, detail="Missing data") purchase_service = PurchaseService() @@ -20,7 +20,7 @@ async def process_receipt(receipt: ReceiptRequest): @router.post("") -async def process_purchase(request: PurchaseRequest): +async def process_purchase(request: PurchaseRequest) -> dict[str, str]: try: user_id = request.user_id product_code = request.product_code diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 148eca2..9153bc6 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,24 +1,22 @@ +import httpx +from fastapi import HTTPException from tortoise.exceptions import DoesNotExist from app.v2.cheese_managers.services.cheese_service import CheeseService -from app.v2.items.models.item import ( - ProductInventory, - ItemInventory, - ItemInventoryProductInventory, -) +from app.v2.items.models.item import (ItemInventory, + ItemInventoryProductInventory, + ProductInventory) from app.v2.purchases.models.purchase_history import PurchaseHistory from app.v2.users.services.user_service import UserService -import httpx -from fastapi import HTTPException class PurchaseService: @staticmethod - async def process_krw_payment(product: ProductInventory, quantity: int): + async def process_krw_payment(product: ProductInventory, quantity: int) -> None: print(f"Processing KRW payment: {product.price * quantity} KRW") # 여기에 실제 KRW 결제 처리 로직 구현 - async def validate_receipt(self, receipt_data: str, user_id: str): + async def validate_receipt(self, receipt_data: str, user_id: str) -> dict: url = "https://buy.itunes.apple.com/verifyReceipt" # sandbox: "https://sandbox.itunes.apple.com/verifyReceipt" payload = { "receipt-data": receipt_data, @@ -35,7 +33,7 @@ async def validate_receipt(self, receipt_data: str, user_id: str): status_code=500, detail="Failed to connect to Apple server" ) - async def _handle_receipt_response(self, response_data: dict, user_id: str): + async def _handle_receipt_response(self, response_data: dict, user_id: str) -> dict: if response_data.get("status") == 0: in_app_purchase = response_data.get("receipt", {}).get("in_app", []) if in_app_purchase: @@ -46,7 +44,8 @@ async def _handle_receipt_response(self, response_data: dict, user_id: str): else: raise HTTPException(status_code=400, detail="Invalid receipt") - async def _save_purchase_history(user_id: bytes, purchase_info: dict): + @classmethod + async def _save_purchase_history(cls, user_id: str, purchase_info: dict) -> dict: receipt_id = purchase_info.get("transaction_id") product_code = purchase_info.get("product_id") status = "completed" @@ -67,7 +66,9 @@ async def _save_purchase_history(user_id: bytes, purchase_info: dict): } @staticmethod - async def validate_purchase(product_code: str): + async def validate_purchase( + product_code: str, + ) -> list[ItemInventoryProductInventory]: try: product = await ProductInventory.get(product_code=product_code) @@ -94,7 +95,7 @@ async def process_purchase( item_inventory_products: list[ItemInventoryProductInventory], user_id: str, cheese_manager_id: str, - ): + ) -> None: for item_inventory_product in item_inventory_products: item: ItemInventory = await item_inventory_product.item_inventory quantity = item_inventory_product.quantity diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py index b88cf6a..a276d1d 100644 --- a/src/app/v2/teller_cards/models/teller_card.py +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -2,9 +2,7 @@ from tortoise.models import Model from app.v2.teller_cards.querys.teller_card_query import ( - SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY, - PATCH_TELLER_CARD_QUERY, -) + PATCH_TELLER_CARD_QUERY, SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY) from common.utils.query_executor import QueryExecutor @@ -17,7 +15,7 @@ class Meta: table = "teller_card" @classmethod - async def get_teller_card_info_by_user_id(cls, user_id: str) -> dict | None: + async def get_teller_card_info_by_user_id(cls, user_id: str) -> dict: query = SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query( diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py index 4bcf61b..9305d74 100644 --- a/src/app/v2/teller_cards/router.py +++ b/src/app/v2/teller_cards/router.py @@ -1,8 +1,7 @@ from fastapi import APIRouter, status -from app.v2.teller_cards.dtos.response import TellerCardResponseDTO from app.v2.teller_cards.dtos.request import TellerCardRequestDTO - +from app.v2.teller_cards.dtos.response import TellerCardResponseDTO from app.v2.teller_cards.services.teller_card_service import TellerCardService router = APIRouter(prefix="/tellercard", tags=["TellerCard"]) @@ -17,8 +16,8 @@ async def patch_teller_card_handler( body: TellerCardRequestDTO, ) -> TellerCardResponseDTO: user_id = body.user_id - badge_code = (body.badgeCode,) - color_code = (body.colorCode,) + badge_code = body.badgeCode + color_code = body.colorCode await TellerCardService.patch_teller_card( user_id=user_id, badge_code=badge_code, color_code=color_code diff --git a/src/app/v2/users/dtos/user_info_dto.py b/src/app/v2/users/dtos/user_info_dto.py index 1866a1a..f0f1969 100644 --- a/src/app/v2/users/dtos/user_info_dto.py +++ b/src/app/v2/users/dtos/user_info_dto.py @@ -1,4 +1,5 @@ from pydantic import BaseModel + from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index c747b42..527c2ac 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -4,10 +4,8 @@ from tortoise.models import Model from app.v2.users.querys.user_query import ( - SELECT_USER_INFO_BY_USER_UUID_QUERY, - SELECT_USER_PROFILE_BY_USER_ID_QUERY, - UPDATE_PREMIUM_STATUS_QUERY, -) + SELECT_USER_INFO_BY_USER_UUID_QUERY, SELECT_USER_PROFILE_BY_USER_ID_QUERY, + UPDATE_PREMIUM_STATUS_QUERY) from common.utils.query_executor import QueryExecutor @@ -61,7 +59,7 @@ class Meta: table = "user" @classmethod - async def get_user_profile_by_user_id(cls, user_id: str) -> dict | None: + async def get_user_profile_by_user_id(cls, user_id: str) -> dict: query = SELECT_USER_PROFILE_BY_USER_ID_QUERY value = user_id return await QueryExecutor.execute_query( @@ -69,7 +67,7 @@ async def get_user_profile_by_user_id(cls, user_id: str) -> dict | None: ) @classmethod - async def get_user_info_by_user_id(cls, user_id: str) -> dict | None: + async def get_user_info_by_user_id(cls, user_id: str) -> dict: query = SELECT_USER_INFO_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query( diff --git a/src/app/v2/users/services/user_service.py b/src/app/v2/users/services/user_service.py index 273b0a9..b2ae2a9 100644 --- a/src/app/v2/users/services/user_service.py +++ b/src/app/v2/users/services/user_service.py @@ -13,4 +13,4 @@ async def get_user_profile(cls, user_id: str) -> dict: @staticmethod async def set_is_premium(user_id: str, is_premium: bool) -> None: - await User.set_is_premium(user_id=user_id, is_premium=is_premium) \ No newline at end of file + await User.set_is_premium(user_id=user_id, is_premium=is_premium) diff --git a/src/common/base_models/base_dtos/base_response.py b/src/common/base_models/base_dtos/base_response.py index 552e537..a24e34b 100644 --- a/src/common/base_models/base_dtos/base_response.py +++ b/src/common/base_models/base_dtos/base_response.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel from typing import Any, Optional +from pydantic import BaseModel + # 공통 응답 모델 정의 class BaseResponseDTO(BaseModel): diff --git a/src/common/base_models/custom_fields/hex_binary_field.py b/src/common/base_models/custom_fields/hex_binary_field.py deleted file mode 100644 index f0bc606..0000000 --- a/src/common/base_models/custom_fields/hex_binary_field.py +++ /dev/null @@ -1,39 +0,0 @@ -from tortoise import fields -import uuid -import binascii - - -class HexBinaryField(fields.BinaryField): - """ - 바이트 배열을 16진수로 변환하여 처리하는 커스텀 필드 - """ - - def to_db_value(self, value, instance): - """ - 데이터베이스로 저장될 때 바이트 배열로 변환 - """ - if isinstance(value, uuid.UUID): - return value.bytes # UUID 객체를 바이트 배열로 변환 - elif isinstance(value, str): - return uuid.UUID(value).bytes # 문자열을 UUID로 변환하여 바이트 배열로 변환 - elif isinstance(value, bytes): - return value # 이미 바이트 배열이면 그대로 반환 - raise ValueError(f"Unsupported value type: {type(value)}") - - def to_python_value(self, value): - """ - 데이터베이스에서 읽어올 때 바이트 배열을 16진수로 변환하여 반환 - """ - if isinstance(value, bytes): - return binascii.hexlify(value).decode( - "utf-8" - ) # 바이트 배열을 16진수로 변환하여 반환 - return value - - def get_db_value(self, value, instance): - """ - 쿼리에서 바이트 배열을 16진수로 변환하여 사용 - """ - if isinstance(value, bytes): - return f"0x{binascii.hexlify(value).decode('utf-8')}" # 바이트 배열을 16진수로 변환하여 쿼리에 사용 - return value diff --git a/src/common/handlers/exception_handler.py b/src/common/handlers/exception_handler.py index 0de1de3..8b99401 100644 --- a/src/common/handlers/exception_handler.py +++ b/src/common/handlers/exception_handler.py @@ -1,2 +1,5 @@ -def attach_exception_handlers(app): - pass \ No newline at end of file +from fastapi import FastAPI + + +def attach_exception_handlers(app: FastAPI) -> None: + pass diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index 628c064..7599207 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -1,15 +1,17 @@ -from app.v2.mobiles.router import router as mobile_router -from app.v2.questions.router import router as question_router +from fastapi import FastAPI + +from app.v2.answers.router import router as answer_router from app.v2.badges.router import router as badge_router -from app.v2.teller_cards.router import router as teller_card_router +from app.v2.cheese_managers.router import router as cheese_router +from app.v2.missions.router import router as mission_router +from app.v2.mobiles.router import router as mobile_router from app.v2.payments.router import router as payment_router from app.v2.purchases.router import router as purchase_router -from app.v2.missions.router import router as mission_router -from app.v2.answers.router import router as answer_router -from app.v2.cheese_managers.router import router as cheese_router +from app.v2.questions.router import router as question_router +from app.v2.teller_cards.router import router as teller_card_router -def attach_router_handlers(app): +def attach_router_handlers(app: FastAPI) -> None: app.include_router(router=mobile_router, prefix="/api/v2") app.include_router(router=badge_router, prefix="/api/v2") app.include_router(router=question_router, prefix="/api/v2") diff --git a/src/common/post_construct.py b/src/common/post_construct.py index e616cef..b4ebb17 100644 --- a/src/common/post_construct.py +++ b/src/common/post_construct.py @@ -1,9 +1,11 @@ -from common.handlers.router_handler import attach_router_handlers +from fastapi import FastAPI + from common.handlers.exception_handler import attach_exception_handlers +from common.handlers.router_handler import attach_router_handlers from core.database.database_settings import database_initialize -def post_construct(app): +def post_construct(app: FastAPI) -> None: attach_router_handlers(app) attach_exception_handlers(app) database_initialize(app) diff --git a/src/common/utils/get_user_id.py b/src/common/utils/get_user_id.py index fb66ade..612b804 100644 --- a/src/common/utils/get_user_id.py +++ b/src/common/utils/get_user_id.py @@ -1,2 +1,2 @@ -async def get_user_id(): +async def get_user_id() -> str: return "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" diff --git a/src/common/utils/query_executor.py b/src/common/utils/query_executor.py index 178698d..3e78339 100644 --- a/src/common/utils/query_executor.py +++ b/src/common/utils/query_executor.py @@ -1,4 +1,5 @@ from typing import Union + from tortoise import Tortoise @@ -7,7 +8,7 @@ class QueryExecutor: @staticmethod async def execute_query( query: str, values: Union[tuple, str] = (), fetch_type: str = "multiple" - ) -> Union[int, list[dict]]: + ) -> Union[dict, list[dict], int]: """ SQL 쿼리를 실행하고 결과를 반환합니다. @@ -23,11 +24,11 @@ async def execute_query( else: values = (values,) - result = await connection.execute_query_dict(query, values) + result = await connection.execute_query_dict(query, values) # type: ignore if result and len(result) > 0: if fetch_type == "single": - return result[0] + return result[0] # type: ignore elif fetch_type == "multiple": return result return 0 if fetch_type == "single" else [] diff --git a/src/core/configs/base_settings.py b/src/core/configs/base_settings.py index 8529883..cbff778 100644 --- a/src/core/configs/base_settings.py +++ b/src/core/configs/base_settings.py @@ -1,6 +1,7 @@ -from pydantic_settings import BaseSettings from enum import StrEnum +from pydantic_settings import BaseSettings + class Env(StrEnum): LOCAL = "local" diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index b32c32d..2d7f572 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -1,8 +1,8 @@ from fastapi import FastAPI from tortoise import Tortoise from tortoise.contrib.fastapi import register_tortoise -from core.configs import settings +from core.configs import settings TORTOISE_APP_MODELS = [ "app.v2.questions.models.question", diff --git a/src/main.py b/src/main.py index 6fff125..8df357b 100644 --- a/src/main.py +++ b/src/main.py @@ -1,9 +1,9 @@ +import logging + from fastapi import FastAPI from common.post_construct import post_construct -import logging - # 로깅 설정 logging.basicConfig(level=logging.DEBUG) @@ -17,7 +17,7 @@ @app.get("/health_check") -def health_check(): +def health_check() -> dict[str, str]: return {"message": "Hello World"} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..780ed8a --- /dev/null +++ b/test.sh @@ -0,0 +1,23 @@ +set -eo pipefail + +COLOR_GREEN=`tput setaf 2;` +COLOR_NC=`tput sgr0;` # No Color + +echo "Starting black" +poetry run black . +echo "OK" + +echo "Starting isort" +poetry run isort . +echo "OK" + +echo "Starting mypy" +poetry run mypy . +echo "OK" + +echo "Starting pytest with coverage" +poetry run coverage run -m pytest +poetry run coverage report -m +poetry run coverage html + +echo "${COLOR_GREEN}All tests passed successfully!${COLOR_NC}" \ No newline at end of file From 9e68f1a28cf58eca12a5ef16c6987000914df499 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 16 Nov 2024 16:32:12 +0900 Subject: [PATCH 18/60] =?UTF-8?q?=E2=9C=A8=20feat:=20level=20info=20dto=20?= =?UTF-8?q?added?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/dtos/answer_dto.py | 7 --- src/app/v2/answers/router.py | 12 ++-- src/app/v2/answers/services/answer_service.py | 10 ++- src/app/v2/levels/dtos/level_dto.py | 22 +++++-- src/app/v2/levels/models/level.py | 20 ++---- src/app/v2/levels/querys/level_query.py | 16 +++++ src/app/v2/levels/services/level_service.py | 63 ++++++++++++++----- .../v2/missions/services/mission_service.py | 3 +- src/app/v2/mobiles/dtos/mypage_response.py | 6 +- .../v2/mobiles/dtos/teller_card_response.py | 8 +-- src/app/v2/mobiles/router.py | 6 +- 11 files changed, 112 insertions(+), 61 deletions(-) diff --git a/src/app/v2/answers/dtos/answer_dto.py b/src/app/v2/answers/dtos/answer_dto.py index 12a8a33..e69de29 100644 --- a/src/app/v2/answers/dtos/answer_dto.py +++ b/src/app/v2/answers/dtos/answer_dto.py @@ -1,7 +0,0 @@ -class RecordDto: - def __init__(self, count: int): - self.count = count - - @classmethod - def builder(cls, count: int) -> "RecordDto": - return cls(count) diff --git a/src/app/v2/answers/router.py b/src/app/v2/answers/router.py index f5e6fcb..821fb95 100644 --- a/src/app/v2/answers/router.py +++ b/src/app/v2/answers/router.py @@ -1,17 +1,11 @@ from fastapi import APIRouter -from app.v2.answers.services.answer_service import AnswerService from app.v2.levels.services.level_service import LevelService router = APIRouter(prefix="/answer", tags=["Test용"]) # FastAPI 비동기 뷰 -@router.get("") -async def get_answer_record_view() -> dict: - user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" - record_dto = await AnswerService.get_answer_record(user_id) - return {"user_id": user_id, "consecutive_answer_days": record_dto.count} @router.get("/level-up") @@ -19,3 +13,9 @@ async def level_up_handler() -> int: user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" result = await LevelService.level_up(user_id=user_id) return result + + +@router.get("/add-exp") +async def add_exp_handler() -> None: + user_id = "180a4e40-62f8-46be-b1eb-e7e3dd91cddf" + await LevelService.add_exp(user_id=user_id, exp=100) diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index 2802486..c8fabc4 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -1,6 +1,5 @@ from datetime import datetime, timedelta -from app.v2.answers.dtos.answer_dto import RecordDto from app.v2.answers.models.answer import Answer @@ -11,7 +10,7 @@ async def get_answer_count(cls, user_id: str) -> int: return answer_count_raw["answer_count"] @classmethod - async def get_answer_record(cls, user_id: str) -> "RecordDto": + async def get_answer_record(cls, user_id: str) -> int: end_date = datetime.now() start_date = end_date - timedelta(days=100) @@ -29,4 +28,9 @@ async def get_answer_record(cls, user_id: str) -> "RecordDto": else: break - return RecordDto.builder(count=record) + return record + + @classmethod + async def calculate_consecutive_answer_points(cls, user_id: str) -> int: + consecutive_days = await cls.get_answer_record(user_id=user_id) + return min(consecutive_days, 10) diff --git a/src/app/v2/levels/dtos/level_dto.py b/src/app/v2/levels/dtos/level_dto.py index 9be7a61..3c4c8a7 100644 --- a/src/app/v2/levels/dtos/level_dto.py +++ b/src/app/v2/levels/dtos/level_dto.py @@ -7,7 +7,21 @@ class LevelDTO(BaseModel): required_exp: int | None = None @classmethod - def builder( - cls, level: int, current_exp: int, required_exp: int | None = None - ) -> "LevelDTO": - return cls(level=level, current_exp=current_exp, required_exp=required_exp) + def builder(cls, level: dict) -> "LevelDTO": + return cls( + level=level["level_level"], + current_exp=level["level_exp"], + required_exp=level["required_exp"], + ) + + +class LevelInfoDTO(BaseModel): + level_dto: LevelDTO + days_to_level_up: int + + @classmethod + def builder(cls, level_dto: LevelDTO, days_to_level_up: int) -> "LevelInfoDTO": + return cls( + level_dto=level_dto, + days_to_level_up=days_to_level_up, + ) diff --git a/src/app/v2/levels/models/level.py b/src/app/v2/levels/models/level.py index 315e059..824838b 100644 --- a/src/app/v2/levels/models/level.py +++ b/src/app/v2/levels/models/level.py @@ -3,29 +3,21 @@ from app.v2.levels.querys.level_query import ( SELECT_USER_EXP_QUERY, SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, - UPDATE_USER_LEVEL_AND_EXP_QUERY) + SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY, UPDATE_USER_LEVEL_AND_EXP_QUERY) from common.utils.query_executor import QueryExecutor class Level(Model): - level_id = fields.BigIntField(pk=True) # BIGINT auto_increment equivalent - user_exp = fields.IntField() # Experience points field - user_level = fields.IntField() # User level field + level_id = fields.BigIntField(pk=True) + user_exp = fields.IntField() + user_level = fields.IntField() class Meta: table = "level" @classmethod - async def get_level_info_by_user_id(cls, user_id: str) -> dict | None: - query = SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY - value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) - - @classmethod - async def get_required_exp_by_user_id(cls, user_id: str) -> dict | None: - query = SELECT_USER_EXP_QUERY + async def get_level_info(cls, user_id: str) -> dict | None: + query = SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY value = user_id return await QueryExecutor.execute_query( query, values=(value,), fetch_type="single" diff --git a/src/app/v2/levels/querys/level_query.py b/src/app/v2/levels/querys/level_query.py index 33d04fb..e92b0fe 100644 --- a/src/app/v2/levels/querys/level_query.py +++ b/src/app/v2/levels/querys/level_query.py @@ -26,3 +26,19 @@ SET l.user_level = %s, l.user_exp = %s WHERE {USER_ID_QUERY}; """ + +SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY = f""" + SELECT + l.user_exp AS level_exp, + l.user_level AS level_level, + li.required_exp AS required_exp + FROM + user u + JOIN + level l ON u.level_id = l.level_id + JOIN + level_inventory li ON l.user_level = li.level + WHERE + {USER_ID_QUERY} + LIMIT 1; +""" diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index e36648d..cae9df0 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -1,19 +1,24 @@ -import asyncio - -from app.v2.levels.dtos.level_dto import LevelDTO +from app.v2.answers.services.answer_service import AnswerService +from app.v2.levels.dtos.level_dto import LevelDTO, LevelInfoDTO from app.v2.levels.models.level import Level class LevelService: @classmethod async def get_level_info(cls, user_id: str) -> LevelDTO: - # 레벨 정보를 조회하는 로직 - level_info_raw = await Level.get_level_info_by_user_id(user_id=user_id) - required_exp_raw = await Level.get_required_exp_by_user_id(user_id=user_id) - return LevelDTO.builder( - level=level_info_raw.get("level_level"), - current_exp=level_info_raw.get("level_exp"), - required_exp=required_exp_raw.get("required_exp"), + return LevelDTO.builder(level=await Level.get_level_info(user_id=user_id)) + + @classmethod + async def get_level_info_add_answer_days(cls, user_id: str) -> LevelDTO: + level_dto = await cls.get_level_info(user_id=user_id) + needs_to_level_up = await cls.calculate_days_to_level_up( + user_id=user_id, + current_exp=level_dto.current_exp, + required_exp=level_dto.required_exp, + ) + return LevelInfoDTO.builder( + level_dto=await cls.get_level_info(user_id=user_id), + days_to_level_up=needs_to_level_up, ) @classmethod @@ -21,14 +26,11 @@ async def level_up(cls, user_id: str) -> int: """ 유저가 레벨업 가능한지 확인 후, 레벨업 처리 """ - level_dto, required_exp_raw = await asyncio.gather( - cls.get_level_info(user_id=user_id), - Level.get_required_exp_by_user_id(user_id=user_id), - ) + level_dto = await cls.get_level_info(user_id=user_id) level = level_dto.level current_exp = level_dto.current_exp - required_exp = required_exp_raw["required_exp"] + required_exp = level_dto.required_exp if current_exp >= required_exp: new_exp = current_exp - required_exp @@ -50,3 +52,34 @@ async def add_exp(cls, user_id: str, exp: int) -> None: await Level.update_level_and_exp( user_id=user_id, new_level=level_dto.level, new_exp=new_exp ) + + @classmethod + async def calculate_bonus_points(cls, user_id: str) -> int: + return await AnswerService.calculate_consecutive_answer_points(user_id=user_id) + + @classmethod + async def calculate_days_to_level_up( + cls, user_id: str, current_exp: int, required_exp: int + ) -> int: + remaining_exp = required_exp - current_exp + days_needed = 0 + + answer_count = await AnswerService.get_answer_count(user_id=user_id) + bonus_points = await cls.calculate_bonus_points(user_id=user_id) + + while remaining_exp > 0: + if answer_count == 1: + calculated_points = 10 + bonus_points + elif 2 <= answer_count <= 5: + calculated_points = 5 + bonus_points + else: + calculated_points = 1 + bonus_points + + remaining_exp -= calculated_points + + days_needed += 1 + + answer_count += 1 + bonus_points = min(bonus_points + 1, 10) + + return days_needed diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 7047b3d..31451e6 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -151,8 +151,7 @@ async def check_long_answer(user_id: str) -> bool: @staticmethod async def check_consecutive_days(user_id: str) -> bool: - record_dto = await AnswerService.get_answer_record(user_id) - consecutive_days = record_dto.count + consecutive_days = await AnswerService.get_answer_record(user_id) return consecutive_days >= 7 @staticmethod diff --git a/src/app/v2/mobiles/dtos/mypage_response.py b/src/app/v2/mobiles/dtos/mypage_response.py index c5153b3..4d492aa 100644 --- a/src/app/v2/mobiles/dtos/mypage_response.py +++ b/src/app/v2/mobiles/dtos/mypage_response.py @@ -1,19 +1,19 @@ from pydantic import BaseModel -from app.v2.levels.dtos.level_dto import LevelDTO +from app.v2.levels.dtos.level_dto import LevelInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO from common.base_models.base_dtos.base_response import BaseResponseDTO class UserProfileWithLevel(BaseModel): userProfile: UserProfileDTO - level: LevelDTO + level: LevelInfoDTO @classmethod def builder( cls, userProfile: UserProfileDTO, - level: LevelDTO, + level: LevelInfoDTO, ) -> "UserProfileWithLevel": return cls( userProfile=userProfile, diff --git a/src/app/v2/mobiles/dtos/teller_card_response.py b/src/app/v2/mobiles/dtos/teller_card_response.py index c603526..67fbc79 100644 --- a/src/app/v2/mobiles/dtos/teller_card_response.py +++ b/src/app/v2/mobiles/dtos/teller_card_response.py @@ -4,7 +4,7 @@ from app.v2.badges.dtos.badge_dto import BadgeDTO from app.v2.colors.dtos.color_dto import ColorCodeDTO -from app.v2.levels.dtos.level_dto import LevelDTO +from app.v2.levels.dtos.level_dto import LevelDTO, LevelInfoDTO from app.v2.users.dtos.user_info_dto import UserInfoDTO from common.base_models.base_dtos.base_response import BaseResponseDTO @@ -13,7 +13,7 @@ class DataDTO(BaseModel): badges: List[BadgeDTO] colors: List[ColorCodeDTO] userInfo: UserInfoDTO - level: LevelDTO + levelInfo: LevelInfoDTO @classmethod def builder( @@ -21,13 +21,13 @@ def builder( badges: List[BadgeDTO], colors: List[ColorCodeDTO], userInfo: UserInfoDTO, - level: LevelDTO, + levelInfo: LevelInfoDTO, ) -> "DataDTO": return cls( badges=badges, colors=colors, userInfo=userInfo, - level=level, + levelInfo=levelInfo, ) diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 8e1e91b..aee0f24 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -34,7 +34,7 @@ async def mobile_teller_card_handler(user_id: str) -> TellerCardResponseDTO: try: badges_task = BadgeService.get_badges_with_details_by_user_id(user_id) colors_task = ColorService.get_colors(user_id) - level_info_task = LevelService.get_level_info(user_id) + level_info_task = LevelService.get_level_info_add_answer_days(user_id) teller_card_task = TellerCardService.get_teller_card(user_id) user_info_task = UserService.get_user_info(user_id) @@ -53,7 +53,7 @@ async def mobile_teller_card_handler(user_id: str) -> TellerCardResponseDTO: ) data = DataDTO.builder( - badges=badges, colors=colors, userInfo=user_info, level=level_info + badges=badges, colors=colors, userInfo=user_info, levelInfo=level_info ) return TellerCardResponseDTO( @@ -75,7 +75,7 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: AnswerService.get_answer_count(user_id=user_id), BadgeService.get_badge_count(user_id=user_id), TellerCardService.get_teller_card(user_id=user_id), - LevelService.get_level_info(user_id), + LevelService.get_level_info_add_answer_days(user_id), ) cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager( From 92114de74fe6d242736df0f043d844c18ebf96e2 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 16 Nov 2024 16:40:57 +0900 Subject: [PATCH 19/60] =?UTF-8?q?build:=20tortoise=20orm=20db=20driver=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 145 ++++++++++++++++++++----------------------------- pyproject.toml | 2 +- 2 files changed, 59 insertions(+), 88 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5f7e3f5..98c19f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,18 +1,40 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "aiomysql" +version = "0.2.0" +description = "MySQL driver for asyncio." +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a"}, + {file = "aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67"}, +] + +[package.dependencies] +PyMySQL = ">=1.0" + +[package.extras] +rsa = ["PyMySQL[rsa] (>=1.0)"] +sa = ["sqlalchemy (>=1.3,<1.4)"] + [[package]] name = "aiosqlite" -version = "0.17.0" +version = "0.20.0" description = "asyncio bridge to the standard sqlite3 module" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, - {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, ] [package.dependencies] -typing_extensions = ">=3.7.2" +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] [[package]] name = "annotated-types" @@ -45,72 +67,6 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] -[[package]] -name = "asyncmy" -version = "0.2.9" -description = "A fast asyncio MySQL driver" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "asyncmy-0.2.9-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d077eaee9a126f36bbe95e0412baa89e93172dd46193ef7bf7650a686e458e50"}, - {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83cf951a44294626df43c5a85cf328297c3bac63f25ede216f9706514dabb322"}, - {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8a1d63c1bb8e3a09c90767199954fd423c48084a1f6c0d956217bc2e48d37d6d"}, - {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ecad6826086e47596c6aa65dcbe221305f3d9232f0d4de11b8562ee2c55464a"}, - {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a664d58f9ebe4132f6cb3128206392be8ad71ad6fb09a5f4a990b04ec142024"}, - {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f2bbd7b75e2d751216f48c3b1b5092b812d70c2cd0053f8d2f50ec3f76a525a8"}, - {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:55e3bc41aa0d4ab410fc3a1d0c31b9cdb6688cd3b0cae6f2ee49c2e7f42968be"}, - {file = "asyncmy-0.2.9-cp310-cp310-win32.whl", hash = "sha256:ea44eefc965c62bcfebf34e9ef00f6e807edf51046046767c56914243e0737e4"}, - {file = "asyncmy-0.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:2b4a2a7cf0bd5051931756e765fefef3c9f9561550e0dd8b1e79308d048b710a"}, - {file = "asyncmy-0.2.9-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:e2b77f03a17a8db338d74311e38ca6dbd4ff9aacb07d2af6b9e0cac9cf1c7b87"}, - {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c19f27b7ff0e297f2981335a85599ffe1c9a8a35c97230203321d5d6e9e4cb30"}, - {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:bf18aef65ac98f5130ca588c55a83a56e74ae416cf0fe2c0757a2b597c4269d0"}, - {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef02186cc02cb767ee5d5cf9ab002d5c7910a1a9f4c16a666867a9325c9ec5e"}, - {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:696da0f71db0fe11e62fa58cd5a27d7c9d9a90699d13d82640755d0061da0624"}, - {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84d20745bb187ced05bd4072ae8b0bff4b4622efa23b79935519edb717174584"}, - {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea242364523f6205c4426435272bd57cbf593c20d5e5551efb28d44cfbd595c2"}, - {file = "asyncmy-0.2.9-cp311-cp311-win32.whl", hash = "sha256:47609d34e6b49fc5ad5bd2a2a593ca120e143e2a4f4206f27a543c5c598a18ca"}, - {file = "asyncmy-0.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d56df7342f7b5467a9d09a854f0e5602c8da09afdad8181ba40b0434d66d8a4"}, - {file = "asyncmy-0.2.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c2a98f225560f9a52d5bd0d2e58517639e209e5d996e9ab7470e661b39394d"}, - {file = "asyncmy-0.2.9-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:20ae3acc326b4b104949cc5e3a728a927e671f671c6f26266ad4a44f57ea9a5b"}, - {file = "asyncmy-0.2.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8171a64888453423a17ae507cd97d256541ea880b314bba16376ab9deffef6e8"}, - {file = "asyncmy-0.2.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c966de493928f26218e0bfaa284cfa609540e52841c423d7babf9ca97c9ff820"}, - {file = "asyncmy-0.2.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4321c4cb4c691689aa26a56354e3fa723d89dc2cac82751e8671b2a4e6441778"}, - {file = "asyncmy-0.2.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd7cde6759dbbfcc467c2af4ef3d75de0b756dde39a3d176383d8c6d9f8a34f3"}, - {file = "asyncmy-0.2.9-cp37-cp37m-win32.whl", hash = "sha256:7678d3641d5a19f20e7e19220c83405fe8616a3b437efbc494f34ad186cedcf0"}, - {file = "asyncmy-0.2.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e8f48d09adf3426e7a59066eaae3c7c84c318ec56cc2f20732d652056c7a3f62"}, - {file = "asyncmy-0.2.9-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:4c4f1dc0acbaac8c3f046215031bbf3ca3d2cd7716244365325496e4f6222b78"}, - {file = "asyncmy-0.2.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:901aac048e5342acc62e1f68f6dec5aa3ed272cb2b138dca38d1c74fc414285d"}, - {file = "asyncmy-0.2.9-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c2d4ad8817f99d9734912c2ff91c42e419031441f512b4aecd7e40a167908c1c"}, - {file = "asyncmy-0.2.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544d3736fd6682f0201a123e4f49335420b6abf6c245abe0487f5967021f1436"}, - {file = "asyncmy-0.2.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f0c606a55625146e189534cc39038540f7a8f2c680ea82845c1f4315a9ad2914"}, - {file = "asyncmy-0.2.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:625f96371d64769b94f7f7f699cfa5be56e669828aef3698cbf4f5bb0014ccb3"}, - {file = "asyncmy-0.2.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eeeb53fdd54eef54b9793a7a5c849c5f7a2fb2540a637f21585a996ef9dd8845"}, - {file = "asyncmy-0.2.9-cp38-cp38-win32.whl", hash = "sha256:2136b749ac489c25ab3aab4a81ae6e9dfb18fd0a5ebda96cd72788c5e4d46927"}, - {file = "asyncmy-0.2.9-cp38-cp38-win_amd64.whl", hash = "sha256:d08fb8722150a9c0645665cf777916335687bddb5f37a8e02af772e330be777b"}, - {file = "asyncmy-0.2.9-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:dbee276a9c8750b522aaad86315a6ed1ffbcb9145ce89070db77831c00dd2da1"}, - {file = "asyncmy-0.2.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8755248429f9bd3d7768c71494c9943fced18f9f526f768e96f5b9b3c727c84"}, - {file = "asyncmy-0.2.9-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:64bcd5110dca7a96cb411de85ab8f79fa867e864150939b8e76286a66eab28fc"}, - {file = "asyncmy-0.2.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a83e3895bed6d44aa334deb1c343d4ffc64b0def2215149f8df2e0e13499250"}, - {file = "asyncmy-0.2.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:beb3d0e434ce0bd9e609cf5341c3b82433ef544f89055d3792186e11fa2433d9"}, - {file = "asyncmy-0.2.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dc608ff331c5d1065e2d3566493d2d9e17f36e315bd5fad3c91c421eea306edb"}, - {file = "asyncmy-0.2.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02caedc00035b2bd0be5555ef61d83ee9cb356ab488ac40072630ba224af02b0"}, - {file = "asyncmy-0.2.9-cp39-cp39-win32.whl", hash = "sha256:5b944d9cdf7ce25b396cd1e0c9319ba24c6583bde7a5dd31157614f3b9cc5b2f"}, - {file = "asyncmy-0.2.9-cp39-cp39-win_amd64.whl", hash = "sha256:3ceb59b9307b5eb893f4d473fcbc43ac0321ffb0436e0115b20cc2e0baa44eb5"}, - {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9f1ca623517552a637900b90d65b5bafc9c67bebf96e3427eecb9359ffa24b1"}, - {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:49622dc4ec69b5a4cbddb3695a1e9249b31092c6f19604abb664b43dcb509b6f"}, - {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8412e825443ee876ef0d55ac4356b56173f5cb64ca8e4638974f8cf5c912a63"}, - {file = "asyncmy-0.2.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4025db2a27b1d84d3c68b5d5aacecac17258b69f25ec8a8c350c5f666003a778"}, - {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7640f3357849b176364ed546908e28c8460701ddc0d23cc3fa7113ec52a076"}, - {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d2593717fa7a92a7d361444726292ce34edea76d5aa67d469b5efeee1c9b729e"}, - {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f22e13bd77277593b56de2e4b65c40c2e81b1a42c4845d062403c5c5bc52bc"}, - {file = "asyncmy-0.2.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a4aa17cc6ac0f7bc6b72e08d112566e69a36e2e1ebebad43d699757b7b4ff028"}, - {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e6f5205722e67c910510e294ad483bdafa7e29d5cf455d49ffa4b819e55fd8"}, - {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1021796f1910a0c2ab2d878f8f5d56f939ef0681f9c1fe925b78161cad2f8297"}, - {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b1dd463bb054138bd1fd3fec9911eb618e92f54f61abb476658f863340394d1"}, - {file = "asyncmy-0.2.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad06f3c02d455947e95087d29f7122411208f0eadaf8671772fe5bad97d9873a"}, - {file = "asyncmy-0.2.9.tar.gz", hash = "sha256:da188be013291d1f831d63cdd3614567f4c63bfdcde73631ddff8df00c56d614"}, -] - [[package]] name = "black" version = "24.8.0" @@ -637,13 +593,13 @@ files = [ [[package]] name = "iso8601" -version = "1.1.0" +version = "2.1.0" description = "Simple module to parse ISO 8601 dates" optional = false -python-versions = ">=3.6.2,<4.0" +python-versions = ">=3.7,<4.0" files = [ - {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, - {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, + {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, + {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, ] [[package]] @@ -1060,15 +1016,30 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymysql" +version = "1.1.1" +description = "Pure Python MySQL Driver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, + {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, +] + +[package.extras] +ed25519 = ["PyNaCl (>=1.4.0)"] +rsa = ["cryptography"] + [[package]] name = "pypika-tortoise" -version = "0.1.6" +version = "0.2.1" description = "Forked from pypika and streamline just for tortoise-orm" optional = false -python-versions = ">=3.7,<4.0" +python-versions = "<4.0,>=3.7" files = [ - {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, - {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, + {file = "pypika_tortoise-0.2.1-py3-none-any.whl", hash = "sha256:e91a1c5a78c6753ead1a9ba1aa169a1f1282c5035170e5462f0073564bc18886"}, + {file = "pypika_tortoise-0.2.1.tar.gz", hash = "sha256:979bbb9d60fe9f6e4129a25c44ee008aab4a4e97b296350be9983dcaa2766354"}, ] [[package]] @@ -1266,21 +1237,21 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7 [[package]] name = "tortoise-orm" -version = "0.21.6" +version = "0.21.7" description = "Easy async ORM for python, built with relations in mind" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "tortoise_orm-0.21.6-py3-none-any.whl", hash = "sha256:98fcf07dce3396075eac36b0d2b14d2267ff875d32455e03ee15e38de2f138df"}, - {file = "tortoise_orm-0.21.6.tar.gz", hash = "sha256:0fbc718001647bf282c01eaaa360f94f1432c9281701244180703d48d58a88ec"}, + {file = "tortoise_orm-0.21.7-py3-none-any.whl", hash = "sha256:2229925885461f424673223ea1875bd5e6961384c766833af55a1ea11a9b25eb"}, + {file = "tortoise_orm-0.21.7.tar.gz", hash = "sha256:8a790a931828aa37ac364b344c561e603422aced2af5e403f6790575da0f19c5"}, ] [package.dependencies] -aiosqlite = ">=0.16.0,<0.18.0" -asyncmy = {version = ">=0.2.8,<0.3.0", optional = true, markers = "extra == \"asyncmy\""} -iso8601 = ">=1.0.2,<2.0.0" +aiomysql = {version = "*", optional = true, markers = "extra == \"aiomysql\""} +aiosqlite = ">=0.16.0,<0.21.0" +iso8601 = ">=2.1.0,<3.0.0" pydantic = ">=2.0,<2.7.0 || >2.7.0,<3.0" -pypika-tortoise = ">=0.1.6,<0.2.0" +pypika-tortoise = ">=0.2.1,<0.3.0" pytz = "*" [package.extras] @@ -1581,4 +1552,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "72d9120536852950d43796677f4f8c8e80ee7ea17591bb86db6048900c27dfb8" +content-hash = "dacd69fbcd1b4925712d2147b2078376e07a6aff377222b18642efabe5d4dabc" diff --git a/pyproject.toml b/pyproject.toml index 538455d..7e61aea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,9 @@ greenlet = "^3.1.1" python-multipart = "^0.0.12" pydantic-settings = "^2.5.2" python-dotenv = "^1.0.1" -tortoise-orm = {extras = ["asyncmy"], version = "^0.21.6"} cryptography = "^43.0.1" httpx = "^0.27.2" +tortoise-orm = {version = "^0.21.6", extras = ["aiomysql"]} [tool.poetry.group.dev.dependencies] From 50ef848485822d3d0ff70b4f987df159fd7c900d Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Wed, 20 Nov 2024 21:50:30 +0900 Subject: [PATCH 20/60] chore: mypy setting --- .gitignore | 2 +- pyproject.toml | 8 + src/app/v2/answers/models/answer.py | 22 +-- src/app/v2/answers/querys/answer_query.py | 4 +- src/app/v2/answers/services/answer_service.py | 5 +- src/app/v2/badges/models/badge.py | 18 +- .../cheese_managers/models/cheese_manager.py | 5 +- .../services/cheese_service.py | 8 +- src/app/v2/colors/models/color.py | 15 +- src/app/v2/emotions/models/emotion.py | 8 +- src/app/v2/items/models/item.py | 8 +- src/app/v2/levels/models/level.py | 15 +- src/app/v2/levels/services/level_service.py | 21 +-- src/app/v2/likes/models/like.py | 15 +- src/app/v2/missions/models/mission.py | 8 +- src/app/v2/missions/router.py | 1 - .../v2/missions/services/mission_service.py | 167 ++++++++---------- src/app/v2/mobiles/router.py | 22 +-- src/app/v2/payments/router.py | 4 +- .../v2/payments/services/payment_service.py | 36 +--- src/app/v2/purchases/router.py | 4 +- .../v2/purchases/services/purchase_service.py | 24 +-- src/app/v2/teller_cards/dtos/response.py | 4 +- src/app/v2/teller_cards/models/teller_card.py | 12 +- src/app/v2/teller_cards/router.py | 4 +- .../services/teller_card_service.py | 8 +- src/app/v2/users/dtos/user_info_dto.py | 4 +- src/app/v2/users/models/refresh_token.py | 4 +- src/app/v2/users/models/user.py | 14 +- src/common/utils/query_formatter.py | 4 +- src/core/configs/__init__.py | 2 +- src/core/configs/base_settings.py | 15 +- src/main.py | 3 +- 33 files changed, 183 insertions(+), 311 deletions(-) diff --git a/.gitignore b/.gitignore index 81fb159..2733f73 100644 --- a/.gitignore +++ b/.gitignore @@ -122,7 +122,7 @@ celerybeat.pid *.sage.py # Environments -src/.env +src/.env.local .venv env/ venv/ diff --git a/pyproject.toml b/pyproject.toml index 7e61aea..97fb3c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,11 @@ ignore_missing_imports = true # 누락된 import 무시 [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 120 + + +[tool.isort] +profile = "black" +line_length = 120 diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py index 5f029cd..57612d7 100644 --- a/src/app/v2/answers/models/answer.py +++ b/src/app/v2/answers/models/answer.py @@ -4,8 +4,10 @@ from tortoise.models import Model from app.v2.answers.querys.answer_query import ( - SELECT_ANSWER_BY_USER_UUID_QUERY, SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY, - SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY) + SELECT_ANSWER_BY_USER_UUID_QUERY, + SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY, + SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -37,25 +39,17 @@ class Meta: async def get_answer_count_by_user_id(cls, user_id: str) -> dict: query = SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod - async def find_all_by_user( - cls, user_id: str, start_date: datetime, end_date: datetime - ) -> list[dict] | None: + async def find_all_by_user(cls, user_id: str, start_date: datetime, end_date: datetime) -> list[dict] | None: query = SELECT_ANSWER_BY_USER_UUID_QUERY values = (user_id, start_date, end_date) - return await QueryExecutor.execute_query( - query, values=values, fetch_type="multiple" - ) + return await QueryExecutor.execute_query(query, values=values, fetch_type="multiple") @classmethod async def get_most_recent_answer_by_user_id(cls, user_id: str) -> dict | None: query = SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="single") diff --git a/src/app/v2/answers/querys/answer_query.py b/src/app/v2/answers/querys/answer_query.py index c43f29b..6c61924 100644 --- a/src/app/v2/answers/querys/answer_query.py +++ b/src/app/v2/answers/querys/answer_query.py @@ -1,8 +1,6 @@ from app.v2.users.querys.user_query import USER_ID_QUERY -SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY = ( - f"SELECT COUNT(*) as answer_count FROM answer WHERE {USER_ID_QUERY}" -) +SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY = f"SELECT COUNT(*) as answer_count FROM answer WHERE {USER_ID_QUERY}" SELECT_ANSWER_BY_USER_UUID_QUERY = f""" SELECT * FROM answer diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index c8fabc4..774742a 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -21,7 +21,7 @@ async def get_answer_record(cls, user_id: str) -> int: if all_answers: for answer in all_answers: - answer_date = answer["date"] # 답변의 날짜를 가져옴 + answer_date = answer["date"] if answer_date == target_date: record += 1 target_date = target_date - timedelta(days=1) @@ -32,5 +32,4 @@ async def get_answer_record(cls, user_id: str) -> int: @classmethod async def calculate_consecutive_answer_points(cls, user_id: str) -> int: - consecutive_days = await cls.get_answer_record(user_id=user_id) - return min(consecutive_days, 10) + return min(await cls.get_answer_record(user_id=user_id), 10) diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index 57b41cc..a302e01 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -2,9 +2,11 @@ from tortoise.models import Model from app.v2.badges.querys.badge_query import ( - INSERT_BADGE_CODE_FOR_USER_QUERY, SELECT_BADGE_BY_USER_UUID_QUERY, + INSERT_BADGE_CODE_FOR_USER_QUERY, + SELECT_BADGE_BY_USER_UUID_QUERY, SELECT_BADGE_CODE_BY_USER_UUID_QUERY, - SELECT_BADGE_COUNT_BY_USER_UUID_QUERY) + SELECT_BADGE_COUNT_BY_USER_UUID_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -21,25 +23,19 @@ async def get_badge_count_by_user_id(cls, user_id: str) -> dict: query = SELECT_BADGE_COUNT_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod async def get_badges_with_details_by_user_id(cls, user_id: str) -> list: query = SELECT_BADGE_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="multiple" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") @classmethod async def get_badge_codes_by_user_id(cls, user_id: str) -> list[dict]: query = SELECT_BADGE_CODE_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="multiple" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") @classmethod async def add_badge(cls, user_id: str, badge_code: str) -> None: diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py index 9e3b9de..5c6ee64 100644 --- a/src/app/v2/cheese_managers/models/cheese_manager.py +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -66,12 +66,13 @@ async def use_cheese(cheese_manager_id: int, amount: int) -> None: if remaining_amount > 0: raise ValueError("Not enough cheese to complete the transaction") - async def add_cheese(self, amount: int) -> None: + @staticmethod + async def add_cheese(cheese_manager_id: int, amount: int) -> None: await CheeseHistory.create( status=CheeseStatus.CAN_USE, current_amount=amount, starting_amount=amount, - cheese_manager_id=self.cheese_manager_id, + cheese_manager_id=cheese_manager_id, ) diff --git a/src/app/v2/cheese_managers/services/cheese_service.py b/src/app/v2/cheese_managers/services/cheese_service.py index 23f0418..c8f5056 100644 --- a/src/app/v2/cheese_managers/services/cheese_service.py +++ b/src/app/v2/cheese_managers/services/cheese_service.py @@ -5,12 +5,8 @@ class CheeseService: @classmethod async def get_cheese_balance(cls, cheese_manager_id: str) -> int: - return await CheeseManager.get_total_cheese_amount_by_manager( - cheese_manager_id=cheese_manager_id - ) + return await CheeseManager.get_total_cheese_amount_by_manager(cheese_manager_id=cheese_manager_id) @classmethod async def add_cheese(cls, cheese_manager_id: str, amount: int) -> None: - await CheeseManager.add_cheese( - cheese_manager_id=cheese_manager_id, amount=amount - ) + await CheeseManager.add_cheese(cheese_manager_id=cheese_manager_id, amount=amount) diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 678328c..a899b7a 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -2,17 +2,14 @@ from tortoise.models import Model from app.v2.colors.dtos.color_dto import ColorCodeDTO -from app.v2.colors.querys.color_query import ( - INSERT_COLOR_CODE_FOR_USER_QUERY, SELECT_COLOR_CODE_BY_USER_UUID_QUERY) +from app.v2.colors.querys.color_query import INSERT_COLOR_CODE_FOR_USER_QUERY, SELECT_COLOR_CODE_BY_USER_UUID_QUERY from common.utils.query_executor import QueryExecutor class Color(Model): color_id = fields.BigIntField(pk=True) # 자동 증가하는 기본 키 color_code = fields.CharField(max_length=255, null=True) # 색상 코드 - user = fields.ForeignKeyField( - "models.User", related_name="colors", null=True, on_delete=fields.SET_NULL - ) + user = fields.ForeignKeyField("models.User", related_name="colors", null=True, on_delete=fields.SET_NULL) class Meta: table = "color" @@ -21,14 +18,10 @@ class Meta: async def get_color_codes_by_user_id(cls, user_id: str) -> list[dict]: query = SELECT_COLOR_CODE_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="multiple" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") @classmethod async def add_color_code_for_user(cls, user_id: str, color_code: str) -> dict: query = INSERT_COLOR_CODE_FOR_USER_QUERY values = (color_code, user_id) - return await QueryExecutor.execute_query( - query, values=values, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/emotions/models/emotion.py b/src/app/v2/emotions/models/emotion.py index e373147..23338e4 100644 --- a/src/app/v2/emotions/models/emotion.py +++ b/src/app/v2/emotions/models/emotion.py @@ -1,7 +1,9 @@ from tortoise import fields, models from app.v2.emotions.querys.emotion_query import ( - INSERT_EMOTION_CODE_FOR_USER_QUERY, SELECT_EMOTION_CODE_BY_USER_UUID_QUERY) + INSERT_EMOTION_CODE_FOR_USER_QUERY, + SELECT_EMOTION_CODE_BY_USER_UUID_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -17,9 +19,7 @@ class Meta: async def get_emotions_with_details_by_user_id(cls, user_id: str) -> list[dict]: query = SELECT_EMOTION_CODE_BY_USER_UUID_QUERY values = user_id - return await QueryExecutor.execute_query( - query, values=values, fetch_type="multiple" - ) + return await QueryExecutor.execute_query(query, values=values, fetch_type="multiple") @classmethod async def add_emotion(cls, user_id: str, emotion_code: str) -> None: diff --git a/src/app/v2/items/models/item.py b/src/app/v2/items/models/item.py index a70a05f..592f286 100644 --- a/src/app/v2/items/models/item.py +++ b/src/app/v2/items/models/item.py @@ -24,12 +24,8 @@ class Meta: class ItemInventoryProductInventory(models.Model): item_inventory_product_inventory_id = fields.BigIntField(pk=True) quantity = fields.IntField() - item_inventory = fields.ForeignKeyField( - "models.ItemInventory", related_name="product_inventories" - ) - product_inventory = fields.ForeignKeyField( - "models.ProductInventory", related_name="item_inventories" - ) + item_inventory = fields.ForeignKeyField("models.ItemInventory", related_name="product_inventories") + product_inventory = fields.ForeignKeyField("models.ProductInventory", related_name="item_inventories") item_measurement = fields.CharField(max_length=255, null=True) class Meta: diff --git a/src/app/v2/levels/models/level.py b/src/app/v2/levels/models/level.py index 824838b..6492b88 100644 --- a/src/app/v2/levels/models/level.py +++ b/src/app/v2/levels/models/level.py @@ -2,8 +2,11 @@ from tortoise.models import Model from app.v2.levels.querys.level_query import ( - SELECT_USER_EXP_QUERY, SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, - SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY, UPDATE_USER_LEVEL_AND_EXP_QUERY) + SELECT_USER_EXP_QUERY, + SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, + SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY, + UPDATE_USER_LEVEL_AND_EXP_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -19,14 +22,10 @@ class Meta: async def get_level_info(cls, user_id: str) -> dict | None: query = SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=(value,), fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=(value,), fetch_type="single") @classmethod - async def update_level_and_exp( - cls, user_id: str, new_level: int, new_exp: int - ) -> None: + async def update_level_and_exp(cls, user_id: str, new_level: int, new_exp: int) -> None: query = UPDATE_USER_LEVEL_AND_EXP_QUERY values = (new_level, new_exp, user_id) await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index cae9df0..bf669da 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -23,9 +23,6 @@ async def get_level_info_add_answer_days(cls, user_id: str) -> LevelDTO: @classmethod async def level_up(cls, user_id: str) -> int: - """ - 유저가 레벨업 가능한지 확인 후, 레벨업 처리 - """ level_dto = await cls.get_level_info(user_id=user_id) level = level_dto.level @@ -36,9 +33,7 @@ async def level_up(cls, user_id: str) -> int: new_exp = current_exp - required_exp new_level = level + 1 - await Level.update_level_and_exp( - user_id=user_id, new_level=new_level, new_exp=new_exp - ) + await Level.update_level_and_exp(user_id=user_id, new_level=new_level, new_exp=new_exp) return 1 return 0 @@ -49,23 +44,15 @@ async def add_exp(cls, user_id: str, exp: int) -> None: current_exp = level_dto.current_exp new_exp = current_exp + exp - await Level.update_level_and_exp( - user_id=user_id, new_level=level_dto.level, new_exp=new_exp - ) - - @classmethod - async def calculate_bonus_points(cls, user_id: str) -> int: - return await AnswerService.calculate_consecutive_answer_points(user_id=user_id) + await Level.update_level_and_exp(user_id=user_id, new_level=level_dto.level, new_exp=new_exp) @classmethod - async def calculate_days_to_level_up( - cls, user_id: str, current_exp: int, required_exp: int - ) -> int: + async def calculate_days_to_level_up(cls, user_id: str, current_exp: int, required_exp: int) -> int: remaining_exp = required_exp - current_exp days_needed = 0 answer_count = await AnswerService.get_answer_count(user_id=user_id) - bonus_points = await cls.calculate_bonus_points(user_id=user_id) + bonus_points = await AnswerService.calculate_consecutive_answer_points(user_id=user_id) while remaining_exp > 0: if answer_count == 1: diff --git a/src/app/v2/likes/models/like.py b/src/app/v2/likes/models/like.py index 5a87711..6ed2dae 100644 --- a/src/app/v2/likes/models/like.py +++ b/src/app/v2/likes/models/like.py @@ -1,19 +1,14 @@ from tortoise import fields from tortoise.models import Model -from app.v2.likes.querys.like_query import \ - SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY +from app.v2.likes.querys.like_query import SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY from common.utils.query_executor import QueryExecutor class Like(Model): likes_id = fields.BigIntField(pk=True) - answer = fields.ForeignKeyField( - "models.Answer", related_name="likes", on_delete=fields.CASCADE - ) - user = fields.ForeignKeyField( - "models.User", related_name="likes", on_delete=fields.CASCADE - ) + answer = fields.ForeignKeyField("models.Answer", related_name="likes", on_delete=fields.CASCADE) + user = fields.ForeignKeyField("models.User", related_name="likes", on_delete=fields.CASCADE) created_time = fields.DatetimeField(null=True) modified_time = fields.DatetimeField(null=True) created_at = fields.DatetimeField(auto_now_add=True) @@ -30,6 +25,4 @@ class Meta: async def get_unique_likes_today(user_id: str) -> dict: query = SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY values = (user_id,) - return await QueryExecutor.execute_query( - query, values=values, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/missions/models/mission.py b/src/app/v2/missions/models/mission.py index 7c1ad3b..3438c54 100644 --- a/src/app/v2/missions/models/mission.py +++ b/src/app/v2/missions/models/mission.py @@ -1,8 +1,7 @@ from tortoise import fields from tortoise.models import Model -from app.v2.missions.querys.mission_query import ( - SELECT_USER_MISSIONS_QUERY, UPDATE_USER_MISSION_PROGRESS_QUERY) +from app.v2.missions.querys.mission_query import SELECT_USER_MISSIONS_QUERY, UPDATE_USER_MISSION_PROGRESS_QUERY from common.utils.query_executor import QueryExecutor @@ -15,12 +14,9 @@ class UserMission(Model): @classmethod async def get_user_missions_by_condition_type(cls, user_id: str) -> dict: - # 유저 ID와 미션 조건 타입에 따른 미션 필터링 query = SELECT_USER_MISSIONS_QUERY values = (user_id,) - return await QueryExecutor.execute_query( - query, values=values, fetch_type="multiple" - ) + return await QueryExecutor.execute_query(query, values=values, fetch_type="multiple") @classmethod async def update_user_mission_progress( diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 86681a3..ea9fd18 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -1,6 +1,5 @@ from fastapi import APIRouter -from app.v2.answers.models.answer import Answer from app.v2.missions.services.mission_service import MissionService router = APIRouter(prefix="/mission", tags=["Mission"]) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 31451e6..5c7fac9 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -1,3 +1,4 @@ +import asyncio from datetime import datetime, timedelta, timezone from fastapi import HTTPException @@ -9,9 +10,7 @@ from app.v2.badges.services.badge_service import BadgeService from app.v2.cheese_managers.services.cheese_service import CheeseService from app.v2.colors.services.color_service import ColorService -from app.v2.items.models.item import (ItemInventory, - ItemInventoryRewardInventory, - RewardInventory) +from app.v2.items.models.item import ItemInventory, ItemInventoryRewardInventory, RewardInventory from app.v2.levels.services.level_service import LevelService from app.v2.likes.models.like import Like from app.v2.missions.dtos.mission_dto import UserMissionDTO @@ -22,12 +21,8 @@ class MissionService: @staticmethod async def get_user_missions(user_id: str) -> list[UserMissionDTO]: - user_mission_raw = await UserMission.get_user_missions_by_condition_type( - user_id - ) - return [ - UserMissionDTO.builder(user_mission) for user_mission in user_mission_raw - ] + user_mission_raw = await UserMission.get_user_missions_by_condition_type(user_id) + return [UserMissionDTO.builder(user_mission) for user_mission in user_mission_raw] @staticmethod async def _update_user_mission_progress( @@ -44,87 +39,77 @@ async def _update_user_mission_progress( ) async def update_mission_progress(self, user_id: str) -> None: - user = await UserService.get_user_info(user_id=user_id) - user_missions = await self.get_user_missions(user_id) - missions = await MissionInventory.all() + + user, user_missions, missions = await asyncio.gather( + UserService.get_user_info(user_id=user_id), self.get_user_missions(user_id=user_id), MissionInventory.all() + ) + + cheese_manager_id = user["cheese_manager_id"] mission_dict = {mission.mission_code: mission for mission in missions} + level_up_mission = None for user_mission in user_missions: - if user_mission.is_completed: - continue + if user_mission.mission_code == "MS_LV_UP": + level_up_mission = user_mission + else: + await self._process_single_mission( + user_mission, mission_dict, cheese_manager_id=cheese_manager_id, user_id=user_id + ) - mission = mission_dict.get(user_mission.mission_code) + if level_up_mission: + await self._process_single_mission( + level_up_mission, mission_dict, cheese_manager_id=cheese_manager_id, user_id=user_id + ) - if not mission: - continue + async def _process_single_mission( + self, user_mission: UserMissionDTO, mission_dict: dict, cheese_manager_id: dict, user_id: str + ) -> None: + mission = mission_dict.get(user_mission.mission_code) - # 조건에 따른 진행도 업데이트 - increment = await self.evaluate_mission_condition( - user_id, user_mission.mission_code - ) - user_mission.progress_count += increment - - # 3. 목표 도달 여부 확인 & 4.보상 처리 - if user_mission.progress_count >= mission.target_count: - user_mission.is_completed = True - await self.reward_user_for_mission( - user_id=user_id, - reward_code=mission.reward_code, - cheese_manager_id=user["cheese_manager_id"], - ) + if user_mission.is_completed or not mission: + return - # 5. 미션 진행도 업데이트 - await self._update_user_mission_progress( + increment = await self.evaluate_mission_condition(user_id, user_mission.mission_code) + user_mission.progress_count += increment + + if user_mission.progress_count >= mission.target_count: + user_mission.is_completed = True + await self.reward_user_for_mission( user_id=user_id, + reward_code=mission.reward_code, + cheese_manager_id=cheese_manager_id, mission_code=user_mission.mission_code, - new_progress_count=user_mission.progress_count, - is_completed=user_mission.is_completed, ) - async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> int: - user_level_data = await LevelService.get_level_info(user_id) - current_level = user_level_data.level + # 진행도 업데이트 + await self._update_user_mission_progress( + user_id=user_id, + mission_code=user_mission.mission_code, + new_progress_count=user_mission.progress_count, + is_completed=user_mission.is_completed, + ) - if mission_code == "MS_POST_FIRST" and await self.check_first_post(user_id): + async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> int: + if mission_code == "MS_BADGE_POST_FIRST" and await self.check_first_post(user_id): return 1 - elif mission_code == "MS_POST_2_5" and await self.check_post_count_range( - user_id, 2, 5 - ): + elif mission_code == "MS_SINGLE_POST_2_5" and await self.check_post_count_range(user_id, 2, 5): return 1 - elif mission_code == "MS_POST_280_CHAR" and await self.check_long_answer( - user_id - ): + elif mission_code == "MS_BADGE_POST_280_CHAR" and await self.check_long_answer(user_id): return 1 - elif mission_code == "MS_POST_GENERAL" and await self.check_post_count_min( - user_id, 6 - ): + elif mission_code == "MS_DAILY_POST_GENERAL" and await self.check_post_count_min(user_id, 6): return 1 - elif ( - mission_code == "MS_POST_CONSECUTIVE_7" - and await self.check_consecutive_days(user_id) - ): + elif mission_code == "MS_BADGE_POST_CONSECUTIVE_7" and await self.check_consecutive_days(user_id): return 1 - elif mission_code == "MS_POST_EARLY_3" and await self.check_early_morning_posts( - user_id - ): + elif mission_code == "MS_BADGE_POST_EARLY_3" and await self.check_early_morning_posts(user_id): return 1 - elif mission_code == "MS_CHEESE_TOTAL_50" and await self.check_cheese_total( - user_id - ): + elif mission_code == "MS_BADGE_CHEESE_TOTAL_50" and await self.check_cheese_total(user_id): return 1 - elif mission_code == "MS_CHRISTMAS" and await self.check_christmas_period(): + elif mission_code == "MS_BADGE_CHRISTMAS" and await self.check_christmas_period(): return 1 - - elif ( - mission_code == "MS_LIKE_3_DIFF_POST" - and await self.check_three_likes_different_posts(user_id) - ): + elif mission_code == "MS_DAILY_LIKE_3_PER_DAY" and await self.check_three_likes_different_posts(user_id): return 1 - elif mission_code == f"MS_LV_{current_level}" and await LevelService.level_up( - user_id=user_id - ): + elif mission_code == f"MS_LV_UP" and await LevelService.level_up(user_id=user_id): return 1 - return 0 @staticmethod @@ -133,9 +118,7 @@ async def check_first_post(user_id: str) -> bool: return post_count_raw.get("answer_count", 0) > 0 @staticmethod - async def check_post_count_range( - user_id: str, min_count: int, max_count: int - ) -> bool: + async def check_post_count_range(user_id: str, min_count: int, max_count: int) -> bool: post_count = await Answer.get_answer_count_by_user_id(user_id=user_id) return min_count <= post_count.get("answer_count", 0) <= max_count @@ -165,9 +148,7 @@ async def check_early_morning_posts(user_id: str) -> bool: @staticmethod async def check_cheese_total(user_id: str) -> bool: user = await UserService.get_user_info(user_id=user_id) - cheese_amount = await CheeseService.get_cheese_balance( - user["cheese_manager_id"] - ) + cheese_amount = await CheeseService.get_cheese_balance(user["cheese_manager_id"]) return cheese_amount >= 50 @@ -192,31 +173,30 @@ async def reward_user_for_mission( user_id: str, reward_code: str, cheese_manager_id: str, + mission_code: str, ) -> None: - - # 보상 검증 item_inventory_rewards = await self.validate_reward(reward_code=reward_code) - # 보상 지급 await self.process_reward( item_inventory_rewards=item_inventory_rewards, user_id=user_id, cheese_manager_id=cheese_manager_id, + mission_code=mission_code, ) @staticmethod async def validate_reward(reward_code: str) -> list[ItemInventoryRewardInventory]: try: - # reward_code를 기반으로 RewardInventory에서 보상 조회 - reward = await RewardInventory.get(reward_code=reward_code) + reward = await RewardInventory.filter(reward_code=reward_code).prefetch_related("item_inventories").first() + + if not reward: + raise HTTPException(status_code=404, detail="Reward not found.") - # 해당 보상에 연결된 ItemInventoryRewardInventory 항목 조회 - item_inventory_rewards = await reward.item_inventories.all() + item_inventory_rewards = reward.item_inventories if not item_inventory_rewards: - raise HTTPException( - status_code=404, detail="No inventory found for this reward." - ) + raise HTTPException(status_code=404, detail="No inventory found for this reward.") + return item_inventory_rewards except DoesNotExist: @@ -229,6 +209,7 @@ async def process_reward( item_inventory_rewards: list[ItemInventoryRewardInventory], user_id: str, cheese_manager_id: str, + mission_code: str, ) -> None: for item_inventory_reward in item_inventory_rewards: item: ItemInventory = await item_inventory_reward.item_inventory @@ -236,21 +217,15 @@ async def process_reward( if item.item_category == "BADGE": for _ in range(quantity): - await BadgeService.add_badge( - user_id=user_id, badge_code=item.item_code - ) + await BadgeService.add_badge(user_id=user_id, badge_code=item.item_code) elif item.item_category == "COLOR": for _ in range(quantity): - await ColorService.add_color( - user_id=user_id, color_code=item.item_code - ) + await ColorService.add_color(user_id=user_id, color_code=item.item_code) elif item.item_category == "CHEESE": - await CheeseService.add_cheese( - cheese_manager_id=cheese_manager_id, amount=quantity - ) + await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) elif item.item_category == "POINT": + if mission_code != "MS_DAILY_LIKE_3_PER_DAY": + quantity += await AnswerService.calculate_consecutive_answer_points(user_id=user_id) await LevelService.add_exp(user_id=user_id, exp=quantity) else: - raise ValueError( - f"Invalid item category for reward: {item.item_category}" - ) + raise ValueError(f"Invalid item category for reward: {item.item_category}") diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index aee0f24..72d7c2e 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -8,10 +8,8 @@ from app.v2.cheese_managers.services.cheese_service import CheeseService from app.v2.colors.services.color_service import ColorService from app.v2.levels.services.level_service import LevelService -from app.v2.mobiles.dtos.mypage_response import (MyPageResponseDTO, - UserProfileWithLevel) -from app.v2.mobiles.dtos.teller_card_response import (DataDTO, - TellerCardResponseDTO) +from app.v2.mobiles.dtos.mypage_response import MyPageResponseDTO, UserProfileWithLevel +from app.v2.mobiles.dtos.teller_card_response import DataDTO, TellerCardResponseDTO from app.v2.teller_cards.services.teller_card_service import TellerCardService from app.v2.users.dtos.user_info_dto import UserInfoDTO from app.v2.users.dtos.user_profile_dto import UserProfileDTO @@ -44,17 +42,11 @@ async def mobile_teller_card_handler(user_id: str) -> TellerCardResponseDTO: except Exception as e: raise HTTPException(status_code=500, detail=str(e)) - cheese_amount = await CheeseService.get_cheese_balance( - user_raw["cheese_manager_id"] - ) + cheese_amount = await CheeseService.get_cheese_balance(user_raw["cheese_manager_id"]) - user_info = UserInfoDTO.builder( - user_raw, cheeseBalance=cheese_amount, tellerCard=teller_card - ) + user_info = UserInfoDTO.builder(user_raw, cheeseBalance=cheese_amount, tellerCard=teller_card) - data = DataDTO.builder( - badges=badges, colors=colors, userInfo=user_info, levelInfo=level_info - ) + data = DataDTO.builder(badges=badges, colors=colors, userInfo=user_info, levelInfo=level_info) return TellerCardResponseDTO( code=status.HTTP_200_OK, @@ -78,9 +70,7 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: LevelService.get_level_info_add_answer_days(user_id), ) - cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager( - cheese_manager_id=user["cheese_manager_id"] - ) + cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager(cheese_manager_id=user["cheese_manager_id"]) user_profile_data = UserProfileWithLevel.builder( userProfile=UserProfileDTO.builder( diff --git a/src/app/v2/payments/router.py b/src/app/v2/payments/router.py index 16b71c4..bb3d71c 100644 --- a/src/app/v2/payments/router.py +++ b/src/app/v2/payments/router.py @@ -18,9 +18,7 @@ async def process_payment(request: PaymentRequestDTO) -> PaymentResponseDTO: user_id = request.user_id product_code = request.productCode - product, item_inventory_products = await PaymentService.validate_payment( - product_code - ) + product, item_inventory_products = await PaymentService.validate_payment(product_code) user = await UserService.get_user_info(user_id=user_id) diff --git a/src/app/v2/payments/services/payment_service.py b/src/app/v2/payments/services/payment_service.py index 2f8ec79..c250ea9 100644 --- a/src/app/v2/payments/services/payment_service.py +++ b/src/app/v2/payments/services/payment_service.py @@ -6,9 +6,7 @@ from app.v2.cheese_managers.models.cheese_manager import CheeseManager from app.v2.colors.services.color_service import ColorService from app.v2.emotions.services.emotion_service import EmotionService -from app.v2.items.models.item import (ItemInventory, - ItemInventoryProductInventory, - ProductInventory) +from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory class PaymentService: @@ -20,18 +18,14 @@ async def validate_payment( product = await ProductInventory.get(product_code=product_code) if product.transaction_currency != "CHEESE": - raise HTTPException( - status_code=400, detail="Invalid transaction currency for payment." - ) + raise HTTPException(status_code=400, detail="Invalid transaction currency for payment.") item_inventory_products = await ItemInventoryProductInventory.filter( product_inventory_id=product.product_id ).all() if not item_inventory_products: - raise HTTPException( - status_code=404, detail="No inventory found for this product." - ) + raise HTTPException(status_code=404, detail="No inventory found for this product.") return product, item_inventory_products @@ -47,16 +41,12 @@ async def process_cheese_payment( user_id: str, cheese_manager_id: int, ) -> None: - total_cheese = await CheeseManager.get_total_cheese_amount_by_manager( - cheese_manager_id=cheese_manager_id - ) + total_cheese = await CheeseManager.get_total_cheese_amount_by_manager(cheese_manager_id=cheese_manager_id) total_required_cheese = product.price if total_cheese < total_required_cheese: - raise HTTPException( - status_code=400, detail="Not enough cheese for this purchase" - ) + raise HTTPException(status_code=400, detail="Not enough cheese for this purchase") try: await CheeseManager.use_cheese(cheese_manager_id, total_required_cheese) @@ -69,20 +59,12 @@ async def process_cheese_payment( if item.item_category == "BADGE": for _ in range(quantity): - await BadgeService.add_badge( - user_id=user_id, badge_code=item.item_code - ) + await BadgeService.add_badge(user_id=user_id, badge_code=item.item_code) elif item.item_category == "COLOR": for _ in range(quantity): - await ColorService.add_color( - user_id=user_id, color_code=item.item_code - ) + await ColorService.add_color(user_id=user_id, color_code=item.item_code) elif item.item_category == "EMOTION": for _ in range(quantity): - await EmotionService.add_emotion( - user_id=user_id, emotion_code=item.item_code - ) + await EmotionService.add_emotion(user_id=user_id, emotion_code=item.item_code) else: - raise ValueError( - f"Invalid item category for cheese payment: {item.item_category}" - ) + raise ValueError(f"Invalid item category for cheese payment: {item.item_category}") diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 9d71e23..8b410d5 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -14,9 +14,7 @@ async def process_receipt(receipt: ReceiptRequest) -> dict: raise HTTPException(status_code=400, detail="Missing data") purchase_service = PurchaseService() # Apple 서버에서 영수증 검증으로 이동 - return await purchase_service.validate_receipt( - receipt.receipt_data, receipt.user_id - ) + return await purchase_service.validate_receipt(receipt.receipt_data, receipt.user_id) @router.post("") diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 9153bc6..9d46a86 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -3,9 +3,7 @@ from tortoise.exceptions import DoesNotExist from app.v2.cheese_managers.services.cheese_service import CheeseService -from app.v2.items.models.item import (ItemInventory, - ItemInventoryProductInventory, - ProductInventory) +from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory from app.v2.purchases.models.purchase_history import PurchaseHistory from app.v2.users.services.user_service import UserService @@ -29,9 +27,7 @@ async def validate_receipt(self, receipt_data: str, user_id: str) -> dict: if response.status_code == 200: return await self._handle_receipt_response(response.json(), user_id) else: - raise HTTPException( - status_code=500, detail="Failed to connect to Apple server" - ) + raise HTTPException(status_code=500, detail="Failed to connect to Apple server") async def _handle_receipt_response(self, response_data: dict, user_id: str) -> dict: if response_data.get("status") == 0: @@ -73,18 +69,14 @@ async def validate_purchase( product = await ProductInventory.get(product_code=product_code) if product.transaction_currency not in ["KRW", "CHEESE"]: - raise HTTPException( - status_code=400, detail="Invalid transaction currency for purchase." - ) + raise HTTPException(status_code=400, detail="Invalid transaction currency for purchase.") item_inventory_products = await ItemInventoryProductInventory.filter( product_inventory_id=product.product_id ).all() if not item_inventory_products: - raise HTTPException( - status_code=404, detail="No inventory found for this product." - ) + raise HTTPException(status_code=404, detail="No inventory found for this product.") return item_inventory_products except DoesNotExist: raise HTTPException(status_code=404, detail="Product not found.") @@ -103,13 +95,9 @@ async def process_purchase( if item.item_category == "SUBSCRIPTION": await UserService.set_is_premium(user_id=user_id, is_premium=True) elif item.item_category == "CHEESE": - await CheeseService.add_cheese( - cheese_manager_id=cheese_manager_id, amount=quantity - ) + await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) else: - raise ValueError( - f"Invalid item category for purchase: {item.item_category}" - ) + raise ValueError(f"Invalid item category for purchase: {item.item_category}") # purchase_info = { # "transaction_id": "1000000654000000", # 고유한 거래 ID (영수증 ID로 사용 가능) diff --git a/src/app/v2/teller_cards/dtos/response.py b/src/app/v2/teller_cards/dtos/response.py index e6087ca..789888d 100644 --- a/src/app/v2/teller_cards/dtos/response.py +++ b/src/app/v2/teller_cards/dtos/response.py @@ -16,7 +16,5 @@ def builder(cls, teller_card: TellerCardDTO) -> "TellerCardResponseDTO": return cls( code=200, message="success", - data=TellerCardDTO( - colorCode=teller_card.colorCode, badgeCode=teller_card.badgeCode - ), + data=TellerCardDTO(colorCode=teller_card.colorCode, badgeCode=teller_card.badgeCode), ) diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py index a276d1d..d3f69ee 100644 --- a/src/app/v2/teller_cards/models/teller_card.py +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -2,7 +2,9 @@ from tortoise.models import Model from app.v2.teller_cards.querys.teller_card_query import ( - PATCH_TELLER_CARD_QUERY, SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY) + PATCH_TELLER_CARD_QUERY, + SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -18,14 +20,10 @@ class Meta: async def get_teller_card_info_by_user_id(cls, user_id: str) -> dict: query = SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod - async def patch_teller_card_info_by_user_id( - cls, user_id: str, badge_code: str, color_code: str - ) -> None: + async def patch_teller_card_info_by_user_id(cls, user_id: str, badge_code: str, color_code: str) -> None: query = PATCH_TELLER_CARD_QUERY values = (badge_code, color_code, user_id) await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py index 9305d74..93cd170 100644 --- a/src/app/v2/teller_cards/router.py +++ b/src/app/v2/teller_cards/router.py @@ -19,9 +19,7 @@ async def patch_teller_card_handler( badge_code = body.badgeCode color_code = body.colorCode - await TellerCardService.patch_teller_card( - user_id=user_id, badge_code=badge_code, color_code=color_code - ) + await TellerCardService.patch_teller_card(user_id=user_id, badge_code=badge_code, color_code=color_code) teller_card = await TellerCardService.get_teller_card(user_id=user_id) diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py index 74b21fc..f2b906e 100644 --- a/src/app/v2/teller_cards/services/teller_card_service.py +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -5,15 +5,11 @@ class TellerCardService: @classmethod async def get_teller_card(cls, user_id: str) -> TellerCardDTO: - teller_cards_raw = await TellerCard.get_teller_card_info_by_user_id( - user_id=user_id - ) + teller_cards_raw = await TellerCard.get_teller_card_info_by_user_id(user_id=user_id) return TellerCardDTO.builder(teller_cards_raw) @classmethod - async def patch_teller_card( - cls, user_id: str, badge_code: str, color_code: str - ) -> None: + async def patch_teller_card(cls, user_id: str, badge_code: str, color_code: str) -> None: await TellerCard.patch_teller_card_info_by_user_id( user_id=user_id, badge_code=badge_code, color_code=color_code ) diff --git a/src/app/v2/users/dtos/user_info_dto.py b/src/app/v2/users/dtos/user_info_dto.py index f0f1969..06a61f7 100644 --- a/src/app/v2/users/dtos/user_info_dto.py +++ b/src/app/v2/users/dtos/user_info_dto.py @@ -9,9 +9,7 @@ class UserInfoDTO(BaseModel): tellerCard: TellerCardDTO @classmethod - def builder( - cls, user_raw: dict, cheeseBalance: int, tellerCard: TellerCardDTO - ) -> "UserInfoDTO": + def builder(cls, user_raw: dict, cheeseBalance: int, tellerCard: TellerCardDTO) -> "UserInfoDTO": return cls( nickname=user_raw.get("nickname"), cheeseBalance=cheeseBalance, diff --git a/src/app/v2/users/models/refresh_token.py b/src/app/v2/users/models/refresh_token.py index fdeea95..15d5e03 100644 --- a/src/app/v2/users/models/refresh_token.py +++ b/src/app/v2/users/models/refresh_token.py @@ -6,9 +6,7 @@ class RefreshToken(Model): refresh_token_id = fields.BigIntField(pk=True) access_token = fields.CharField(max_length=255) refresh_token = fields.CharField(max_length=255) - user_id = fields.BinaryField( - max_length=16 - ) # foreign key로 사용되지는 않지만 binary field로 선언 + user_id = fields.BinaryField(max_length=16) # foreign key로 사용되지는 않지만 binary field로 선언 class Meta: table = "refresh_token" diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index 527c2ac..2151da0 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -4,8 +4,10 @@ from tortoise.models import Model from app.v2.users.querys.user_query import ( - SELECT_USER_INFO_BY_USER_UUID_QUERY, SELECT_USER_PROFILE_BY_USER_ID_QUERY, - UPDATE_PREMIUM_STATUS_QUERY) + SELECT_USER_INFO_BY_USER_UUID_QUERY, + SELECT_USER_PROFILE_BY_USER_ID_QUERY, + UPDATE_PREMIUM_STATUS_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -62,17 +64,13 @@ class Meta: async def get_user_profile_by_user_id(cls, user_id: str) -> dict: query = SELECT_USER_PROFILE_BY_USER_ID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod async def get_user_info_by_user_id(cls, user_id: str) -> dict: query = SELECT_USER_INFO_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query( - query, values=value, fetch_type="single" - ) + return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod async def set_is_premium(cls, user_id: str, is_premium: bool) -> None: diff --git a/src/common/utils/query_formatter.py b/src/common/utils/query_formatter.py index 4b7d746..0471832 100644 --- a/src/common/utils/query_formatter.py +++ b/src/common/utils/query_formatter.py @@ -13,9 +13,7 @@ def format(query_template: str, values: Union[str, list, tuple]) -> str: """ # 단일 값일 경우 문자열 포맷팅 처리 if isinstance(values, (list, tuple)): - formatted_values = tuple( - f"'{value}'" if isinstance(value, str) else value for value in values - ) + formatted_values = tuple(f"'{value}'" if isinstance(value, str) else value for value in values) return query_template % formatted_values else: # 단일 문자열일 경우 따옴표 추가 diff --git a/src/core/configs/__init__.py b/src/core/configs/__init__.py index c6ef013..8110387 100644 --- a/src/core/configs/__init__.py +++ b/src/core/configs/__init__.py @@ -2,7 +2,7 @@ def get_settings() -> Settings: - return Settings(_env_file=".env", _env_file_encoding="utf-8") + return Settings() settings = get_settings() diff --git a/src/core/configs/base_settings.py b/src/core/configs/base_settings.py index cbff778..2ad9426 100644 --- a/src/core/configs/base_settings.py +++ b/src/core/configs/base_settings.py @@ -1,3 +1,4 @@ +import os from enum import StrEnum from pydantic_settings import BaseSettings @@ -11,10 +12,14 @@ class Env(StrEnum): class Settings(BaseSettings): ENV: Env = Env.LOCAL - DB_HOST: str - DB_PORT: int - DB_USER: str - DB_PASSWORD: str - DB_NAME: str + DB_HOST: str = "localhost" + DB_PORT: int = 3306 + DB_USER: str = "root" + DB_PASSWORD: str = "password" + DB_NAME: str = "database_name" DB_TIMEZONE: str = "Asia/Seoul" DB_CHARSET: str = "utf8mb4" + + class Config: + env_file = f".env.{os.getenv('ENV', 'local')}" + env_file_encoding = "utf-8" diff --git a/src/main.py b/src/main.py index 8df357b..793ea23 100644 --- a/src/main.py +++ b/src/main.py @@ -4,10 +4,9 @@ from common.post_construct import post_construct -# 로깅 설정 + logging.basicConfig(level=logging.DEBUG) -# Tortoise ORM의 쿼리 로깅 활성화 db_client_logger = logging.getLogger("tortoise.db_client") db_client_logger.setLevel(logging.DEBUG) From 492d6b748511dc485ae2c9342e42f8ff44e68f03 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Fri, 22 Nov 2024 00:13:43 +0900 Subject: [PATCH 21/60] =?UTF-8?q?fix=20:=20cheese=20manager=20get=5Fcheese?= =?UTF-8?q?=5Fbalance=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/cheese_managers/models/cheese_manager.py | 1 - src/app/v2/cheese_managers/router.py | 1 + src/app/v2/cheese_managers/services/cheese_service.py | 2 +- src/app/v2/mobiles/router.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py index 5c6ee64..01094ff 100644 --- a/src/app/v2/cheese_managers/models/cheese_manager.py +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -23,7 +23,6 @@ async def get_total_cheese_amount_by_manager(cheese_manager_id: int) -> int: .annotate(total_cheese_amount=Sum("current_amount")) .values("total_cheese_amount") ) - return result[0].get("total_cheese_amount", 0) @staticmethod diff --git a/src/app/v2/cheese_managers/router.py b/src/app/v2/cheese_managers/router.py index d784b0a..2589c81 100644 --- a/src/app/v2/cheese_managers/router.py +++ b/src/app/v2/cheese_managers/router.py @@ -12,5 +12,6 @@ async def get_cheese_handler(user_id: str) -> CheeseResponseDTO: user = await UserService.get_user_info(user_id=user_id) cheese_amount = await CheeseService.get_cheese_balance(user["cheese_manager_id"]) + print(cheese_amount) return CheeseResponseDTO.builder(cheese_balance=cheese_amount) diff --git a/src/app/v2/cheese_managers/services/cheese_service.py b/src/app/v2/cheese_managers/services/cheese_service.py index c8f5056..724e5cc 100644 --- a/src/app/v2/cheese_managers/services/cheese_service.py +++ b/src/app/v2/cheese_managers/services/cheese_service.py @@ -5,7 +5,7 @@ class CheeseService: @classmethod async def get_cheese_balance(cls, cheese_manager_id: str) -> int: - return await CheeseManager.get_total_cheese_amount_by_manager(cheese_manager_id=cheese_manager_id) + return await CheeseManager.get_total_cheese_amount_by_manager(cheese_manager_id=cheese_manager_id) or 0 @classmethod async def add_cheese(cls, cheese_manager_id: str, amount: int) -> None: diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 72d7c2e..e1207f1 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -70,7 +70,7 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: LevelService.get_level_info_add_answer_days(user_id), ) - cheese_amount = await CheeseManager.get_total_cheese_amount_by_manager(cheese_manager_id=user["cheese_manager_id"]) + cheese_amount = await CheeseService.get_cheese_balance(cheese_manager_id=user["cheese_manager_id"]) user_profile_data = UserProfileWithLevel.builder( userProfile=UserProfileDTO.builder( From 318cd5c02ce1b379e11e957754f2abdc36fa6b2f Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 23 Nov 2024 14:38:14 +0900 Subject: [PATCH 22/60] =?UTF-8?q?fix:=20=EB=AF=B8=EC=85=98=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/missions/router.py | 2 +- src/app/v2/missions/services/mission_service.py | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index ea9fd18..20bb0ba 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -5,7 +5,7 @@ router = APIRouter(prefix="/mission", tags=["Mission"]) -@router.get("/get-user-missions") +@router.get("") async def mission_test_handler(user_id: str) -> None: mission_service = MissionService() diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 5c7fac9..5b6b9e5 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -78,7 +78,6 @@ async def _process_single_mission( user_id=user_id, reward_code=mission.reward_code, cheese_manager_id=cheese_manager_id, - mission_code=user_mission.mission_code, ) # 진행도 업데이트 @@ -96,8 +95,8 @@ async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> i return 1 elif mission_code == "MS_BADGE_POST_280_CHAR" and await self.check_long_answer(user_id): return 1 - elif mission_code == "MS_DAILY_POST_GENERAL" and await self.check_post_count_min(user_id, 6): - return 1 + # elif mission_code == "MS_DAILY_POST_GENERAL" and await self.check_post_count_min(user_id, 6): + # return 1 elif mission_code == "MS_BADGE_POST_CONSECUTIVE_7" and await self.check_consecutive_days(user_id): return 1 elif mission_code == "MS_BADGE_POST_EARLY_3" and await self.check_early_morning_posts(user_id): @@ -173,15 +172,11 @@ async def reward_user_for_mission( user_id: str, reward_code: str, cheese_manager_id: str, - mission_code: str, ) -> None: item_inventory_rewards = await self.validate_reward(reward_code=reward_code) await self.process_reward( - item_inventory_rewards=item_inventory_rewards, - user_id=user_id, - cheese_manager_id=cheese_manager_id, - mission_code=mission_code, + item_inventory_rewards=item_inventory_rewards, user_id=user_id, cheese_manager_id=cheese_manager_id ) @staticmethod @@ -209,7 +204,6 @@ async def process_reward( item_inventory_rewards: list[ItemInventoryRewardInventory], user_id: str, cheese_manager_id: str, - mission_code: str, ) -> None: for item_inventory_reward in item_inventory_rewards: item: ItemInventory = await item_inventory_reward.item_inventory @@ -224,8 +218,6 @@ async def process_reward( elif item.item_category == "CHEESE": await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) elif item.item_category == "POINT": - if mission_code != "MS_DAILY_LIKE_3_PER_DAY": - quantity += await AnswerService.calculate_consecutive_answer_points(user_id=user_id) await LevelService.add_exp(user_id=user_id, exp=quantity) else: raise ValueError(f"Invalid item category for reward: {item.item_category}") From 3941958f9c604b9b1d4feef3fbc5e0c7be6b9811 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 24 Nov 2024 16:57:33 +0900 Subject: [PATCH 23/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=B3=B4=EC=9C=A0=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/colors/dtos/color_dto.py | 14 +++++++++++++ src/app/v2/colors/dtos/response.py | 6 ++++++ src/app/v2/colors/models/color.py | 13 ++++++++++-- src/app/v2/colors/querys/color_query.py | 10 +++++++++ src/app/v2/colors/router.py | 23 +++++++++++++++++++++ src/app/v2/colors/services/color_service.py | 7 ++++++- src/app/v2/missions/router.py | 12 ++++++----- src/common/handlers/router_handler.py | 2 ++ 8 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/app/v2/colors/dtos/response.py diff --git a/src/app/v2/colors/dtos/color_dto.py b/src/app/v2/colors/dtos/color_dto.py index e153cc0..5436ce3 100644 --- a/src/app/v2/colors/dtos/color_dto.py +++ b/src/app/v2/colors/dtos/color_dto.py @@ -7,3 +7,17 @@ class ColorCodeDTO(BaseModel): @classmethod def builder(cls, color_raw: dict) -> "ColorCodeDTO": return cls(colorCode=color_raw.get("color_code")) + + +class ColorDTO(BaseModel): + colorCode: str + colorName: str + colorHexCode: str + + @classmethod + def builder(cls, color_raw: dict) -> "ColorDTO": + return cls( + colorCode=color_raw.get("color_code"), + colorName=color_raw.get("color_name"), + colorHexCode=color_raw.get("color_hex_code"), + ) diff --git a/src/app/v2/colors/dtos/response.py b/src/app/v2/colors/dtos/response.py new file mode 100644 index 0000000..7a0459e --- /dev/null +++ b/src/app/v2/colors/dtos/response.py @@ -0,0 +1,6 @@ +from app.v2.colors.dtos.color_dto import ColorDTO +from common.base_models.base_dtos.base_response import BaseResponseDTO + + +class ColorListResponseDTO(BaseResponseDTO): + data: list[ColorDTO] diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index a899b7a..4f2ec47 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -1,8 +1,11 @@ from tortoise import fields from tortoise.models import Model -from app.v2.colors.dtos.color_dto import ColorCodeDTO -from app.v2.colors.querys.color_query import INSERT_COLOR_CODE_FOR_USER_QUERY, SELECT_COLOR_CODE_BY_USER_UUID_QUERY +from app.v2.colors.querys.color_query import ( + INSERT_COLOR_CODE_FOR_USER_QUERY, + SELECT_COLOR_CODE_BY_USER_UUID_QUERY, + SELECT_COLOR_BY_USER_UUID_QUERY, +) from common.utils.query_executor import QueryExecutor @@ -25,3 +28,9 @@ async def add_color_code_for_user(cls, user_id: str, color_code: str) -> dict: query = INSERT_COLOR_CODE_FOR_USER_QUERY values = (color_code, user_id) return await QueryExecutor.execute_query(query, values=values, fetch_type="single") + + @classmethod + async def get_colors_with_details_by_user_id(cls, user_id: str) -> list[dict]: + query = SELECT_COLOR_BY_USER_UUID_QUERY + value = user_id + return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") diff --git a/src/app/v2/colors/querys/color_query.py b/src/app/v2/colors/querys/color_query.py index 9dd2c1a..0121d8d 100644 --- a/src/app/v2/colors/querys/color_query.py +++ b/src/app/v2/colors/querys/color_query.py @@ -12,3 +12,13 @@ FROM user WHERE {USER_ID_QUERY} """ + +SELECT_COLOR_BY_USER_UUID_QUERY = f""" + SELECT + c.color_code, + ci.color_name, + ci.color_hex_code + FROM color c + JOIN color_inventory ci on c.color_code = ci.color_code + WHERE {USER_ID_QUERY} +""" diff --git a/src/app/v2/colors/router.py b/src/app/v2/colors/router.py index e69de29..9d89f8f 100644 --- a/src/app/v2/colors/router.py +++ b/src/app/v2/colors/router.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, status + + +from app.v2.colors.dtos.response import ColorListResponseDTO +from app.v2.colors.services.color_service import ColorService + +router = APIRouter(prefix="/user/color", tags=["Color"]) + + +@router.get( + "", + response_model=ColorListResponseDTO, + status_code=status.HTTP_200_OK, +) +async def get_user_badge_handler(user_id: str): + + colors = await ColorService.get_colors_with_details_by_user_id(user_id=user_id) + + return ColorListResponseDTO( + code=status.HTTP_200_OK, + message="보유 색상 정보 조회", + data=colors, + ) diff --git a/src/app/v2/colors/services/color_service.py b/src/app/v2/colors/services/color_service.py index 4d8cacc..69f36f3 100644 --- a/src/app/v2/colors/services/color_service.py +++ b/src/app/v2/colors/services/color_service.py @@ -1,4 +1,4 @@ -from app.v2.colors.dtos.color_dto import ColorCodeDTO +from app.v2.colors.dtos.color_dto import ColorCodeDTO, ColorDTO from app.v2.colors.models.color import Color @@ -11,3 +11,8 @@ async def get_colors(cls, user_id: str) -> list[ColorCodeDTO]: @classmethod async def add_color(cls, user_id: str, color_code: str) -> None: await Color.add_color_code_for_user(user_id=user_id, color_code=color_code) + + @classmethod + async def get_colors_with_details_by_user_id(cls, user_id: str) -> list[ColorDTO]: + colors_raw = await Color.get_colors_with_details_by_user_id(user_id=user_id) + return [ColorDTO.builder(color) for color in colors_raw] \ No newline at end of file diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 20bb0ba..f1b174f 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -1,16 +1,18 @@ -from fastapi import APIRouter +from fastapi import APIRouter, BackgroundTasks from app.v2.missions.services.mission_service import MissionService router = APIRouter(prefix="/mission", tags=["Mission"]) -@router.get("") -async def mission_test_handler(user_id: str) -> None: - +async def process_mission_in_background(user_id: str): mission_service = MissionService() + await mission_service.update_mission_progress(user_id) - await mission_service.update_mission_progress(user_id=user_id) + +@router.get("") +async def mission_handler(user_id: str, background_tasks: BackgroundTasks) -> None: + background_tasks.add_task(process_mission_in_background, user_id) # await mission_service.validate_reward(reward_code="RW_FIRST_POST") diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index 7599207..1f61c9e 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -9,11 +9,13 @@ from app.v2.purchases.router import router as purchase_router from app.v2.questions.router import router as question_router from app.v2.teller_cards.router import router as teller_card_router +from app.v2.colors.router import router as color_router def attach_router_handlers(app: FastAPI) -> None: app.include_router(router=mobile_router, prefix="/api/v2") app.include_router(router=badge_router, prefix="/api/v2") + app.include_router(router=color_router, prefix="/api/v2") app.include_router(router=question_router, prefix="/api/v2") app.include_router(router=teller_card_router, prefix="/api/v2") app.include_router(router=payment_router, prefix="/api/v2") From 81a4c68b3c99b294c7e6c9b2b23584d3329e01c7 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Thu, 28 Nov 2024 18:15:39 +0900 Subject: [PATCH 24/60] =?UTF-8?q?=F0=9F=92=A1=20chore:=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/colors/models/color.py | 2 +- src/app/v2/colors/router.py | 1 - src/app/v2/colors/services/color_service.py | 2 +- src/common/handlers/router_handler.py | 2 +- src/core/configs/base_settings.py | 2 ++ src/main.py | 1 - 6 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 4f2ec47..1b3a4da 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -3,8 +3,8 @@ from app.v2.colors.querys.color_query import ( INSERT_COLOR_CODE_FOR_USER_QUERY, - SELECT_COLOR_CODE_BY_USER_UUID_QUERY, SELECT_COLOR_BY_USER_UUID_QUERY, + SELECT_COLOR_CODE_BY_USER_UUID_QUERY, ) from common.utils.query_executor import QueryExecutor diff --git a/src/app/v2/colors/router.py b/src/app/v2/colors/router.py index 9d89f8f..4a887ce 100644 --- a/src/app/v2/colors/router.py +++ b/src/app/v2/colors/router.py @@ -1,6 +1,5 @@ from fastapi import APIRouter, status - from app.v2.colors.dtos.response import ColorListResponseDTO from app.v2.colors.services.color_service import ColorService diff --git a/src/app/v2/colors/services/color_service.py b/src/app/v2/colors/services/color_service.py index 69f36f3..c9bfe57 100644 --- a/src/app/v2/colors/services/color_service.py +++ b/src/app/v2/colors/services/color_service.py @@ -15,4 +15,4 @@ async def add_color(cls, user_id: str, color_code: str) -> None: @classmethod async def get_colors_with_details_by_user_id(cls, user_id: str) -> list[ColorDTO]: colors_raw = await Color.get_colors_with_details_by_user_id(user_id=user_id) - return [ColorDTO.builder(color) for color in colors_raw] \ No newline at end of file + return [ColorDTO.builder(color) for color in colors_raw] diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index 1f61c9e..10b5fc2 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -3,13 +3,13 @@ from app.v2.answers.router import router as answer_router from app.v2.badges.router import router as badge_router from app.v2.cheese_managers.router import router as cheese_router +from app.v2.colors.router import router as color_router from app.v2.missions.router import router as mission_router from app.v2.mobiles.router import router as mobile_router from app.v2.payments.router import router as payment_router from app.v2.purchases.router import router as purchase_router from app.v2.questions.router import router as question_router from app.v2.teller_cards.router import router as teller_card_router -from app.v2.colors.router import router as color_router def attach_router_handlers(app: FastAPI) -> None: diff --git a/src/core/configs/base_settings.py b/src/core/configs/base_settings.py index 2ad9426..247b705 100644 --- a/src/core/configs/base_settings.py +++ b/src/core/configs/base_settings.py @@ -19,6 +19,8 @@ class Settings(BaseSettings): DB_NAME: str = "database_name" DB_TIMEZONE: str = "Asia/Seoul" DB_CHARSET: str = "utf8mb4" + APPLE_URL: str = "https://sandbox.itunes.apple.com/verifyReceipt" + APPLE_SHARED_SECRET: str = "YOUR_SHARED_SECRET" class Config: env_file = f".env.{os.getenv('ENV', 'local')}" diff --git a/src/main.py b/src/main.py index 793ea23..e17ccb1 100644 --- a/src/main.py +++ b/src/main.py @@ -4,7 +4,6 @@ from common.post_construct import post_construct - logging.basicConfig(level=logging.DEBUG) db_client_logger = logging.getLogger("tortoise.db_client") From f5b75569eb774dfb9db2928225e94b30ad84f041 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Thu, 28 Nov 2024 19:22:36 +0900 Subject: [PATCH 25/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=95=A0=ED=94=8C=20?= =?UTF-8?q?=EC=9D=B8=EC=95=B1=20=EC=98=81=EC=88=98=EC=A6=9D=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/dtos/requests.py | 4 +- src/app/v2/purchases/router.py | 17 +++-- .../v2/purchases/services/purchase_service.py | 67 ++++++++++++++++++- 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/app/v2/purchases/dtos/requests.py b/src/app/v2/purchases/dtos/requests.py index 0a89136..a2f87fc 100644 --- a/src/app/v2/purchases/dtos/requests.py +++ b/src/app/v2/purchases/dtos/requests.py @@ -1,8 +1,8 @@ from pydantic import BaseModel -class ReceiptRequest(BaseModel): - receipt_data: str +class ReceiptRequestDTO(BaseModel): + receiptData: str user_id: str diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 8b410d5..dcdfe5c 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -1,20 +1,25 @@ from fastapi import APIRouter, HTTPException from tortoise.exceptions import DoesNotExist -from app.v2.purchases.dtos.requests import PurchaseRequest, ReceiptRequest +from app.v2.purchases.dtos.requests import PurchaseRequest, ReceiptRequestDTO from app.v2.purchases.services.purchase_service import PurchaseService from app.v2.users.services.user_service import UserService router = APIRouter(prefix="/purchase", tags=["Purchase"]) -@router.post("/process-receipt/") -async def process_receipt(receipt: ReceiptRequest) -> dict: - if not receipt.receipt_data or not receipt.user_id: +@router.post("/process-receipt") +async def process_receipt(receipt: ReceiptRequestDTO) -> dict: + if not receipt.receiptData or not receipt.user_id: raise HTTPException(status_code=400, detail="Missing data") purchase_service = PurchaseService() - # Apple 서버에서 영수증 검증으로 이동 - return await purchase_service.validate_receipt(receipt.receipt_data, receipt.user_id) + return await purchase_service.validate_receipt(receipt.receiptData, receipt.user_id) + + +@router.get("/receipt-test") +async def receipt_test() -> dict: + purchase_service = PurchaseService() + return await purchase_service.receipt_test() @router.post("") diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 9d46a86..42d52f5 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,3 +1,6 @@ +import base64 +import re + import httpx from fastapi import HTTPException from tortoise.exceptions import DoesNotExist @@ -6,6 +9,7 @@ from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory from app.v2.purchases.models.purchase_history import PurchaseHistory from app.v2.users.services.user_service import UserService +from core.configs import settings class PurchaseService: @@ -15,20 +19,42 @@ async def process_krw_payment(product: ProductInventory, quantity: int) -> None: # 여기에 실제 KRW 결제 처리 로직 구현 async def validate_receipt(self, receipt_data: str, user_id: str) -> dict: - url = "https://buy.itunes.apple.com/verifyReceipt" # sandbox: "https://sandbox.itunes.apple.com/verifyReceipt" + + url = settings.APPLE_URL + payload = { "receipt-data": receipt_data, - "password": "YOUR_APP_SHARED_SECRET", # Apple에서 제공받은 앱의 공유 비밀번호 + "password": settings.APPLE_SHARED_SECRET, } async with httpx.AsyncClient() as client: response = await client.post(url, json=payload) - + print(response.json()) if response.status_code == 200: return await self._handle_receipt_response(response.json(), user_id) else: raise HTTPException(status_code=500, detail="Failed to connect to Apple server") + async def receipt_test(self) -> dict: + file_path = "/Users/gimtaeu/workspace/tellingme-python-server/1개월 구독한 영수증 data.txt/TXT.rtf" + base64_data = self.extract_base64_from_rtf(file_path) + + url = settings.APPLE_URL + + payload = { + "receipt-data": base64_data, + "password": settings.APPLE_SHARED_SECRET, + } + + async with httpx.AsyncClient() as client: + response = await client.post(url, json=payload) + response_data = response.json() + print(response_data) + if response.status_code != 200 or response_data.get("status") != 0: + raise HTTPException(status_code=400, detail="Receipt verification failed") + + return response_data + async def _handle_receipt_response(self, response_data: dict, user_id: str) -> dict: if response_data.get("status") == 0: in_app_purchase = response_data.get("receipt", {}).get("in_app", []) @@ -125,3 +151,38 @@ async def process_purchase( # "expires_date": "2024-12-01T10:30:00Z", # "original_transaction_id": "1000000654000000" # } + + import re + import base64 + + @staticmethod + def extract_base64_from_rtf(file_path): + # 파일 읽기 + try: + with open(file_path, "r", encoding="utf-8") as file: + content = file.read() + except FileNotFoundError: + raise ValueError(f"File not found: {file_path}") + except Exception as e: + raise ValueError(f"Failed to read the file: {str(e)}") + + # Base64 데이터 추출 (MII로 시작하는 패턴만) + base64_pattern = r"MII[A-Za-z0-9+/=]+" + matches = re.findall(base64_pattern, content) + + if not matches: + raise ValueError("No valid Base64 data found in the file.") + + # 가장 긴 Base64 데이터 선택 + base64_data = max(matches, key=len) + + # Base64 데이터 유효성 검증 + try: + base64.b64decode(base64_data, validate=True) + except Exception: + raise ValueError("Extracted data is not valid Base64.") + + # 디버깅 로그 출력 + print(f"Extracted Base64 data length: {len(base64_data)}") + + return base64_data From 849078f639a57b49284e1afce049fd88545b62db Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Thu, 28 Nov 2024 20:00:10 +0900 Subject: [PATCH 26/60] fix : purchase_api response --- src/app/v2/purchases/services/purchase_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 42d52f5..7399293 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -31,7 +31,8 @@ async def validate_receipt(self, receipt_data: str, user_id: str) -> dict: response = await client.post(url, json=payload) print(response.json()) if response.status_code == 200: - return await self._handle_receipt_response(response.json(), user_id) + return response.json() + # return await self._handle_receipt_response(response.json(), user_id) else: raise HTTPException(status_code=500, detail="Failed to connect to Apple server") @@ -42,6 +43,7 @@ async def receipt_test(self) -> dict: url = settings.APPLE_URL payload = { + "exclude-old-transactions": True, "receipt-data": base64_data, "password": settings.APPLE_SHARED_SECRET, } From 7a9288eebac1122627f09a9d10eab17562c6c5f4 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Thu, 28 Nov 2024 20:09:59 +0900 Subject: [PATCH 27/60] =?UTF-8?q?=F0=9F=92=A1=20chore:=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/router.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index dcdfe5c..48b9558 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, status from tortoise.exceptions import DoesNotExist from app.v2.purchases.dtos.requests import PurchaseRequest, ReceiptRequestDTO @@ -13,7 +13,12 @@ async def process_receipt(receipt: ReceiptRequestDTO) -> dict: if not receipt.receiptData or not receipt.user_id: raise HTTPException(status_code=400, detail="Missing data") purchase_service = PurchaseService() - return await purchase_service.validate_receipt(receipt.receiptData, receipt.user_id) + data = await purchase_service.validate_receipt(receipt.receiptData, receipt.user_id) + return { + "code": status.HTTP_200_OK, + "message": "Receipt verified successfully", + "data": data, + } @router.get("/receipt-test") From 37fc9a3332dc2851c27dc3ef7206d7d1d7958744 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Thu, 28 Nov 2024 20:17:02 +0900 Subject: [PATCH 28/60] =?UTF-8?q?=F0=9F=92=A1=20chore:=20=EC=98=A4?= =?UTF-8?q?=ED=83=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.py b/src/main.py index e17ccb1..424ef86 100644 --- a/src/main.py +++ b/src/main.py @@ -3,6 +3,7 @@ from fastapi import FastAPI from common.post_construct import post_construct +from core.configs import settings logging.basicConfig(level=logging.DEBUG) @@ -10,6 +11,8 @@ db_client_logger.setLevel(logging.DEBUG) app = FastAPI() +print(settings.APPLE_URL) +print(settings.APPLE_SHARED_SECRET) post_construct(app) From 0501aadeb1e96e1385621220ec2278efa289acb1 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Fri, 29 Nov 2024 23:50:47 +0900 Subject: [PATCH 29/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=85=94=EB=9F=AC?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/badges/models/badge.py | 10 ++++++++++ src/app/v2/colors/models/color.py | 9 +++++++++ src/app/v2/teller_cards/router.py | 1 + .../teller_cards/services/teller_card_service.py | 14 ++++++++++++++ src/common/handlers/exception_handler.py | 14 ++++++++++++-- 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index a302e01..0bf911e 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -42,3 +42,13 @@ async def add_badge(cls, user_id: str, badge_code: str) -> None: query = INSERT_BADGE_CODE_FOR_USER_QUERY values = (badge_code, user_id) await QueryExecutor.execute_query(query, values=values) + + +class BadgeInventory(Model): + badge_code = fields.CharField(max_length=255, primary_key=True) + badge_name = fields.CharField(max_length=255, null=True) + badge_condition = fields.CharField(max_length=255, null=True) + badge_middle_name = fields.CharField(max_length=255, null=True) + + class Meta: + table = "badge_inventory" diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 1b3a4da..8cded62 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -34,3 +34,12 @@ async def get_colors_with_details_by_user_id(cls, user_id: str) -> list[dict]: query = SELECT_COLOR_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") + + +class ColorInventory(Model): + color_code = fields.CharField(max_length=255, primary_key=True) + color_name = fields.CharField(max_length=255, null=True) + color_hex_code = fields.CharField(max_length=255, null=True) + + class Meta: + table = "color_inventory" # 테이블 이름을 명시 diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py index 93cd170..00e5db9 100644 --- a/src/app/v2/teller_cards/router.py +++ b/src/app/v2/teller_cards/router.py @@ -18,6 +18,7 @@ async def patch_teller_card_handler( user_id = body.user_id badge_code = body.badgeCode color_code = body.colorCode + await TellerCardService.validate_teller_card(badge_code=badge_code, color_code=color_code) await TellerCardService.patch_teller_card(user_id=user_id, badge_code=badge_code, color_code=color_code) diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py index f2b906e..4ce4d0c 100644 --- a/src/app/v2/teller_cards/services/teller_card_service.py +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -1,3 +1,5 @@ +from app.v2.badges.models.badge import BadgeInventory +from app.v2.colors.models.color import ColorInventory from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO from app.v2.teller_cards.models.teller_card import TellerCard @@ -13,3 +15,15 @@ async def patch_teller_card(cls, user_id: str, badge_code: str, color_code: str) await TellerCard.patch_teller_card_info_by_user_id( user_id=user_id, badge_code=badge_code, color_code=color_code ) + + @classmethod + async def validate_teller_card(cls, badge_code: str, color_code: str) -> None: + badge_code_list = await BadgeInventory.all().values("badge_code") + color_code_list = await ColorInventory.all().values("color_code") + badge_codes = [badge["badge_code"] for badge in badge_code_list] + color_codes = [color["color_code"] for color in color_code_list] + + if badge_code not in badge_codes: + raise ValueError("Invalid badge code") + if color_code not in color_codes: + raise ValueError("Invalid color code") diff --git a/src/common/handlers/exception_handler.py b/src/common/handlers/exception_handler.py index 8b99401..b3f43c3 100644 --- a/src/common/handlers/exception_handler.py +++ b/src/common/handlers/exception_handler.py @@ -1,5 +1,15 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request +from starlette.responses import JSONResponse def attach_exception_handlers(app: FastAPI) -> None: - pass + @app.exception_handler(Exception) + async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse: + return JSONResponse( + status_code=500, + content={ + "code": 500, + "data": str(exc), + "message": "An unexpected error occurred. Please try again later.", + }, + ) From 4189d3a199d0104170baf19e467b23e146eab979 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 30 Nov 2024 15:06:50 +0900 Subject: [PATCH 30/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=B3=B4=EC=9C=A0=20?= =?UTF-8?q?=EA=B0=90=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20api=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/colors/router.py | 2 +- src/app/v2/emotions/dtos/response.py | 15 +++++++++++++ src/app/v2/emotions/router.py | 19 +++++++++++++++++ .../v2/emotions/services/emotion_service.py | 21 +++++++++++++++++++ .../v2/purchases/services/purchase_service.py | 2 +- .../base_models/base_dtos/base_response.py | 2 -- src/common/handlers/router_handler.py | 2 ++ src/core/database/database_settings.py | 1 + 8 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/app/v2/emotions/dtos/response.py create mode 100644 src/app/v2/emotions/router.py diff --git a/src/app/v2/colors/router.py b/src/app/v2/colors/router.py index 4a887ce..93a9d09 100644 --- a/src/app/v2/colors/router.py +++ b/src/app/v2/colors/router.py @@ -11,7 +11,7 @@ response_model=ColorListResponseDTO, status_code=status.HTTP_200_OK, ) -async def get_user_badge_handler(user_id: str): +async def get_user_color_handler(user_id: str): colors = await ColorService.get_colors_with_details_by_user_id(user_id=user_id) diff --git a/src/app/v2/emotions/dtos/response.py b/src/app/v2/emotions/dtos/response.py new file mode 100644 index 0000000..3fcac1e --- /dev/null +++ b/src/app/v2/emotions/dtos/response.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + +from common.base_models.base_dtos.base_response import BaseResponseDTO + + +class EmotionDTO(BaseModel): + emotionList: list[int] + + @classmethod + def build(cls, emotion_list: list[int]) -> "EmotionDTO": + return cls(emotionList=emotion_list) + + +class EmotionListResponseDTO(BaseResponseDTO): + data: EmotionDTO diff --git a/src/app/v2/emotions/router.py b/src/app/v2/emotions/router.py new file mode 100644 index 0000000..3fea988 --- /dev/null +++ b/src/app/v2/emotions/router.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, status + +from app.v2.emotions.dtos.response import EmotionListResponseDTO +from app.v2.emotions.services.emotion_service import EmotionService + +router = APIRouter(prefix="/user/emotion", tags=["Emotion"]) + + +@router.get( + "", + response_model=EmotionListResponseDTO, + status_code=status.HTTP_200_OK, +) +async def get_user_emotion_handler(user_id: str) -> EmotionListResponseDTO: + return EmotionListResponseDTO( + data=await EmotionService.mapping_emotion_list(user_id=user_id), + code=status.HTTP_200_OK, + message="보유 감정 정보 조회", + ) diff --git a/src/app/v2/emotions/services/emotion_service.py b/src/app/v2/emotions/services/emotion_service.py index bd960d1..10890d3 100644 --- a/src/app/v2/emotions/services/emotion_service.py +++ b/src/app/v2/emotions/services/emotion_service.py @@ -1,5 +1,21 @@ +from app.v2.emotions.dtos.response import EmotionDTO, EmotionListResponseDTO from app.v2.emotions.models.emotion import Emotion, EmotionInventory +emotion_mapping = { + "EM_HAPPY": 1, + "EM_PROUD": 2, + "EM_OKAY": 3, + "EM_TIRED": 4, + "EM_SAD": 5, + "EM_ANGRY": 6, + "EM_EXCITED": 7, + "EM_FUN": 8, + "EM_RELAXED": 9, + "EM_APATHETIC": 10, + "EM_LONELY": 11, + "EM_COMPLEX": 12, +} + class EmotionService: @classmethod @@ -13,3 +29,8 @@ async def add_emotion(cls, user_id: str, emotion_code: str) -> None: @classmethod async def get_emotion_inventory(cls) -> list[dict]: return await EmotionInventory.get_emotion_inventory() + + @classmethod + async def mapping_emotion_list(cls, user_id: str) -> EmotionDTO: + emotions = await cls.get_emotions(user_id=user_id) + return EmotionDTO.build(emotion_list=[emotion_mapping[emotion["emotion_code"]] for emotion in emotions]) diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 7399293..f73274b 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -154,8 +154,8 @@ async def process_purchase( # "original_transaction_id": "1000000654000000" # } - import re import base64 + import re @staticmethod def extract_base64_from_rtf(file_path): diff --git a/src/common/base_models/base_dtos/base_response.py b/src/common/base_models/base_dtos/base_response.py index a24e34b..13d2463 100644 --- a/src/common/base_models/base_dtos/base_response.py +++ b/src/common/base_models/base_dtos/base_response.py @@ -8,5 +8,3 @@ class BaseResponseDTO(BaseModel): code: int message: str data: Optional[Any] = None - - # @classmethod() diff --git a/src/common/handlers/router_handler.py b/src/common/handlers/router_handler.py index 10b5fc2..ff6b67e 100644 --- a/src/common/handlers/router_handler.py +++ b/src/common/handlers/router_handler.py @@ -4,6 +4,7 @@ from app.v2.badges.router import router as badge_router from app.v2.cheese_managers.router import router as cheese_router from app.v2.colors.router import router as color_router +from app.v2.emotions.router import router as emotion_router from app.v2.missions.router import router as mission_router from app.v2.mobiles.router import router as mobile_router from app.v2.payments.router import router as payment_router @@ -23,3 +24,4 @@ def attach_router_handlers(app: FastAPI) -> None: app.include_router(router=mission_router, prefix="/api/v2") app.include_router(router=cheese_router, prefix="/api/v2") app.include_router(router=answer_router, prefix="/test") + app.include_router(router=emotion_router, prefix="/api/v2") diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index 2d7f572..430f9c9 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -19,6 +19,7 @@ "app.v2.items.models.item", "app.v2.missions.models.mission", "app.v2.likes.models.like", + "app.v2.emotions.models.emotion", ] TORTOISE_ORM = { From 095105e06793baa9a1f579bd04df48ab540e46d8 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 30 Nov 2024 16:25:52 +0900 Subject: [PATCH 31/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20level=20dto=20?= =?UTF-8?q?=EC=8A=A4=EB=84=A4=EC=9D=B4=ED=81=AC=20->=20=EC=B9=B4=EB=A9=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/levels/dtos/level_dto.py | 16 ++++++++-------- src/app/v2/levels/services/level_service.py | 4 ++-- src/app/v2/mobiles/router.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/v2/levels/dtos/level_dto.py b/src/app/v2/levels/dtos/level_dto.py index 3c4c8a7..7b103f3 100644 --- a/src/app/v2/levels/dtos/level_dto.py +++ b/src/app/v2/levels/dtos/level_dto.py @@ -3,25 +3,25 @@ class LevelDTO(BaseModel): level: int - current_exp: int - required_exp: int | None = None + currentExp: int + requiredExp: int | None = None @classmethod def builder(cls, level: dict) -> "LevelDTO": return cls( level=level["level_level"], - current_exp=level["level_exp"], - required_exp=level["required_exp"], + currentExp=level["level_exp"], + requiredExp=level["required_exp"], ) class LevelInfoDTO(BaseModel): - level_dto: LevelDTO - days_to_level_up: int + levelDto: LevelDTO + daysToLevelUp: int @classmethod def builder(cls, level_dto: LevelDTO, days_to_level_up: int) -> "LevelInfoDTO": return cls( - level_dto=level_dto, - days_to_level_up=days_to_level_up, + levelDto=level_dto, + daysToLevelUp=days_to_level_up, ) diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index bf669da..f828ab0 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -13,8 +13,8 @@ async def get_level_info_add_answer_days(cls, user_id: str) -> LevelDTO: level_dto = await cls.get_level_info(user_id=user_id) needs_to_level_up = await cls.calculate_days_to_level_up( user_id=user_id, - current_exp=level_dto.current_exp, - required_exp=level_dto.required_exp, + current_exp=level_dto.currentExp, + required_exp=level_dto.requiredExp, ) return LevelInfoDTO.builder( level_dto=await cls.get_level_info(user_id=user_id), diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index e1207f1..1740429 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -4,7 +4,7 @@ from app.v2.answers.services.answer_service import AnswerService from app.v2.badges.services.badge_service import BadgeService -from app.v2.cheese_managers.models.cheese_manager import CheeseManager + from app.v2.cheese_managers.services.cheese_service import CheeseService from app.v2.colors.services.color_service import ColorService from app.v2.levels.services.level_service import LevelService From a1a4d79cc79b16d434dde271ce7c530be66c969c Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 30 Nov 2024 16:47:07 +0900 Subject: [PATCH 32/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20teller=5Fcard=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=97=B0=EC=86=8D=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/services/answer_service.py | 3 ++- .../v2/mobiles/dtos/teller_card_response.py | 3 +++ src/app/v2/mobiles/router.py | 27 ++++++++++--------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index 774742a..3a6a6ce 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -22,7 +22,8 @@ async def get_answer_record(cls, user_id: str) -> int: if all_answers: for answer in all_answers: answer_date = answer["date"] - if answer_date == target_date: + + if answer_date == target_date.date(): # 날짜만 비교 record += 1 target_date = target_date - timedelta(days=1) else: diff --git a/src/app/v2/mobiles/dtos/teller_card_response.py b/src/app/v2/mobiles/dtos/teller_card_response.py index 67fbc79..22d1d2c 100644 --- a/src/app/v2/mobiles/dtos/teller_card_response.py +++ b/src/app/v2/mobiles/dtos/teller_card_response.py @@ -14,6 +14,7 @@ class DataDTO(BaseModel): colors: List[ColorCodeDTO] userInfo: UserInfoDTO levelInfo: LevelInfoDTO + recordCount: int = 0 @classmethod def builder( @@ -22,12 +23,14 @@ def builder( colors: List[ColorCodeDTO], userInfo: UserInfoDTO, levelInfo: LevelInfoDTO, + recordCount: Optional[int] = None, ) -> "DataDTO": return cls( badges=badges, colors=colors, userInfo=userInfo, levelInfo=levelInfo, + recordCount=recordCount if recordCount is not None else 0, ) diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 1740429..e35915a 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -29,24 +29,25 @@ async def mobile_main_handler() -> None: status_code=status.HTTP_200_OK, ) async def mobile_teller_card_handler(user_id: str) -> TellerCardResponseDTO: - try: - badges_task = BadgeService.get_badges_with_details_by_user_id(user_id) - colors_task = ColorService.get_colors(user_id) - level_info_task = LevelService.get_level_info_add_answer_days(user_id) - teller_card_task = TellerCardService.get_teller_card(user_id) - user_info_task = UserService.get_user_info(user_id) - - badges, colors, level_info, teller_card, user_raw = await asyncio.gather( - badges_task, colors_task, level_info_task, teller_card_task, user_info_task - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + + badges_task = BadgeService.get_badges_with_details_by_user_id(user_id) + colors_task = ColorService.get_colors(user_id) + level_info_task = LevelService.get_level_info_add_answer_days(user_id) + teller_card_task = TellerCardService.get_teller_card(user_id) + user_info_task = UserService.get_user_info(user_id) + record_answer_task = AnswerService.get_answer_record(user_id=user_id) + + badges, colors, level_info, teller_card, user_raw, record_count = await asyncio.gather( + badges_task, colors_task, level_info_task, teller_card_task, user_info_task, record_answer_task + ) cheese_amount = await CheeseService.get_cheese_balance(user_raw["cheese_manager_id"]) user_info = UserInfoDTO.builder(user_raw, cheeseBalance=cheese_amount, tellerCard=teller_card) - data = DataDTO.builder(badges=badges, colors=colors, userInfo=user_info, levelInfo=level_info) + data = DataDTO.builder( + badges=badges, colors=colors, userInfo=user_info, levelInfo=level_info, recordCount=record_count + ) return TellerCardResponseDTO( code=status.HTTP_200_OK, From 7613f93e457aaa0992e2ca66e4b0c79f54237467 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 1 Dec 2024 13:28:41 +0900 Subject: [PATCH 33/60] =?UTF-8?q?=F0=9F=92=A1=20chore:=20type=20hinting=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 76 +++++++++++- pyproject.toml | 10 +- src/app/v2/answers/models/answer.py | 17 +-- src/app/v2/answers/services/answer_service.py | 4 +- src/app/v2/badges/dtos/badge_dto.py | 14 +-- src/app/v2/badges/models/badge.py | 17 +-- src/app/v2/badges/services/badge_service.py | 4 +- src/app/v2/cheese_managers/dtos/cheese_dto.py | 6 + .../cheese_managers/models/cheese_manager.py | 15 ++- .../services/cheese_service.py | 4 +- src/app/v2/colors/dtos/color_dto.py | 12 +- src/app/v2/colors/models/color.py | 18 ++- src/app/v2/colors/router.py | 2 +- src/app/v2/emotions/models/emotion.py | 10 +- .../v2/emotions/services/emotion_service.py | 6 +- src/app/v2/items/models/item.py | 15 ++- src/app/v2/levels/dtos/level_dto.py | 4 +- src/app/v2/levels/models/level.py | 11 +- src/app/v2/levels/services/level_service.py | 22 +++- src/app/v2/likes/models/like.py | 15 ++- src/app/v2/missions/dtos/mission_dto.py | 14 ++- src/app/v2/missions/models/mission.py | 8 +- src/app/v2/missions/router.py | 2 +- .../v2/missions/services/mission_service.py | 36 +++--- src/app/v2/mobiles/router.py | 1 - .../v2/payments/services/payment_service.py | 47 +++---- .../v2/purchases/models/purchase_history.py | 5 +- src/app/v2/purchases/router.py | 12 +- .../v2/purchases/services/purchase_service.py | 115 +++++++++--------- src/app/v2/teller_cards/dtos/response.py | 3 +- .../v2/teller_cards/dtos/teller_card_dto.py | 12 +- src/app/v2/teller_cards/models/teller_card.py | 6 +- .../services/teller_card_service.py | 2 +- src/app/v2/users/dtos/user_info_dto.py | 4 +- src/app/v2/users/models/user.py | 23 ++-- src/app/v2/users/models/user_mission.py | 5 +- src/app/v2/users/services/user_service.py | 6 +- src/common/exceptions/__init__.py | 0 src/common/exceptions/custom_exception.py | 16 +++ src/common/exceptions/error_code.py | 24 ++++ src/common/handlers/exception_handler.py | 10 ++ src/common/utils/query_executor.py | 17 +-- src/common/utils/query_formatter.py | 2 +- src/main.py | 3 - 44 files changed, 437 insertions(+), 218 deletions(-) create mode 100644 src/common/exceptions/__init__.py create mode 100644 src/common/exceptions/custom_exception.py create mode 100644 src/common/exceptions/error_code.py diff --git a/poetry.lock b/poetry.lock index 98c19f4..456513c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -226,6 +226,80 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.6.8" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, +] + +[package.extras] +toml = ["tomli"] + [[package]] name = "cryptography" version = "43.0.1" @@ -1552,4 +1626,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "dacd69fbcd1b4925712d2147b2078376e07a6aff377222b18642efabe5d4dabc" +content-hash = "950fb3bce727b2cbb3b28751c57055bd378128b68f5797d380396e34c0ba1913" diff --git a/pyproject.toml b/pyproject.toml index 97fb3c5..f7a96de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,12 +25,14 @@ black = "^24.8.0" pytest = "^8.3.3" pytest-asyncio = "^0.24.0" mypy-extensions = "^1.0.0" +coverage = "^7.6.8" [tool.mypy] -files = "src" # 타입 체크할 파일 경로 -disallow_untyped_calls = true # 타입이 없는 함수 호출 금지 -disallow_untyped_defs = true # 타입이 없는 함수 정의 금지 -ignore_missing_imports = true # 누락된 import 무시 +files = "src" +strict = true +#disallow_untyped_calls = true # 타입이 없는 함수 호출 금지 +#disallow_untyped_defs = true # 타입이 없는 함수 정의 금지 +#ignore_missing_imports = true # 누락된 import 무시 [build-system] requires = ["poetry-core"] diff --git a/src/app/v2/answers/models/answer.py b/src/app/v2/answers/models/answer.py index 57612d7..2437b90 100644 --- a/src/app/v2/answers/models/answer.py +++ b/src/app/v2/answers/models/answer.py @@ -1,6 +1,8 @@ from datetime import datetime +from typing import Any from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model from app.v2.answers.querys.answer_query import ( @@ -8,6 +10,7 @@ SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY, SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY, ) +from app.v2.users.models.user import User from common.utils.query_executor import QueryExecutor @@ -20,9 +23,6 @@ class Answer(Model): is_premium = fields.BooleanField(null=False) is_public = fields.BooleanField(null=False) modified_time = fields.DatetimeField(null=True) - user = fields.ForeignKeyField( - "models.User", related_name="answers", null=True, on_delete=fields.SET_NULL - ) # 외래 키 정의 is_blind = fields.BooleanField(null=False) blind_ended_at = fields.DatetimeField(null=True) blind_started_at = fields.DatetimeField(null=True) @@ -31,25 +31,28 @@ class Answer(Model): created_at = fields.DatetimeField(auto_now_add=True) updated_at = fields.DatetimeField(auto_now=True) + user: ForeignKeyRelation[User] = fields.ForeignKeyField( + "models.User", related_name="answers", on_delete=fields.CASCADE + ) + class Meta: table = "answer" # 기존 get_answer_count_by_user_id 메서드 @classmethod - async def get_answer_count_by_user_id(cls, user_id: str) -> dict: + async def get_answer_count_by_user_id(cls, user_id: str) -> Any: query = SELECT_ANSWER_COUNT_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod - async def find_all_by_user(cls, user_id: str, start_date: datetime, end_date: datetime) -> list[dict] | None: + async def find_all_by_user(cls, user_id: str, start_date: datetime, end_date: datetime) -> Any: query = SELECT_ANSWER_BY_USER_UUID_QUERY values = (user_id, start_date, end_date) - return await QueryExecutor.execute_query(query, values=values, fetch_type="multiple") @classmethod - async def get_most_recent_answer_by_user_id(cls, user_id: str) -> dict | None: + async def get_most_recent_answer_by_user_id(cls, user_id: str) -> Any: query = SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="single") diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index 3a6a6ce..0db0a7f 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -7,7 +7,9 @@ class AnswerService: @classmethod async def get_answer_count(cls, user_id: str) -> int: answer_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) - return answer_count_raw["answer_count"] + if answer_count_raw is None: + return 0 + return int(answer_count_raw.get("answer_count", 0)) @classmethod async def get_answer_record(cls, user_id: str) -> int: diff --git a/src/app/v2/badges/dtos/badge_dto.py b/src/app/v2/badges/dtos/badge_dto.py index 4262ad4..30772de 100644 --- a/src/app/v2/badges/dtos/badge_dto.py +++ b/src/app/v2/badges/dtos/badge_dto.py @@ -5,8 +5,8 @@ class BadgeCodeDTO(BaseModel): badgeCode: str @classmethod - def builder(cls, badge_raw: dict) -> "BadgeCodeDTO": - return cls(badgeCode=badge_raw.get("badge_code")) + def builder(cls, badge_raw: dict[str, str]) -> "BadgeCodeDTO": + return cls(badgeCode=badge_raw.get("badge_code", "")) class BadgeDTO(BaseModel): @@ -16,10 +16,10 @@ class BadgeDTO(BaseModel): badgeCondition: str @classmethod - def builder(cls, badge_raw: dict) -> "BadgeDTO": + def builder(cls, badge_raw: dict[str, str]) -> "BadgeDTO": return cls( - badgeCode=badge_raw.get("badge_code"), - badgeName=badge_raw.get("badge_name"), - badgeMiddleName=badge_raw.get("badge_middle_name"), - badgeCondition=badge_raw.get("badge_condition"), + badgeCode=badge_raw.get("badge_code", ""), + badgeName=badge_raw.get("badge_name", ""), + badgeMiddleName=badge_raw.get("badge_middle_name", ""), + badgeCondition=badge_raw.get("badge_condition", ""), ) diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index 0bf911e..bc8170a 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -1,4 +1,7 @@ +from typing import Any + from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model from app.v2.badges.querys.badge_query import ( @@ -7,32 +10,32 @@ SELECT_BADGE_CODE_BY_USER_UUID_QUERY, SELECT_BADGE_COUNT_BY_USER_UUID_QUERY, ) +from app.v2.users.models.user import User from common.utils.query_executor import QueryExecutor class Badge(Model): - badge_id = fields.BigIntField(pk=True) # 자동 증가하는 기본 키 - badge_code = fields.CharField(max_length=255, null=True) # 배지 코드 - user = fields.ForeignKeyField("models.User", related_name="badges", null=True) + badge_id = fields.BigIntField(pk=True) + badge_code = fields.CharField(max_length=255) + user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="badges") class Meta: table = "badge" @classmethod - async def get_badge_count_by_user_id(cls, user_id: str) -> dict: - + async def get_badge_count_by_user_id(cls, user_id: str) -> Any: query = SELECT_BADGE_COUNT_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod - async def get_badges_with_details_by_user_id(cls, user_id: str) -> list: + async def get_badges_with_details_by_user_id(cls, user_id: str) -> Any: query = SELECT_BADGE_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") @classmethod - async def get_badge_codes_by_user_id(cls, user_id: str) -> list[dict]: + async def get_badge_codes_by_user_id(cls, user_id: str) -> Any: query = SELECT_BADGE_CODE_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") diff --git a/src/app/v2/badges/services/badge_service.py b/src/app/v2/badges/services/badge_service.py index 7d8ba3d..d8f716b 100644 --- a/src/app/v2/badges/services/badge_service.py +++ b/src/app/v2/badges/services/badge_service.py @@ -20,4 +20,6 @@ async def get_badges_with_details_by_user_id(cls, user_id: str) -> list[BadgeDTO @classmethod async def get_badge_count(cls, user_id: str) -> int: badge_count_raw = await Badge.get_badge_count_by_user_id(user_id=user_id) - return badge_count_raw.get("badge_count", 0) + if badge_count_raw is None: + return 0 + return int(badge_count_raw.get("badge_count", 0)) diff --git a/src/app/v2/cheese_managers/dtos/cheese_dto.py b/src/app/v2/cheese_managers/dtos/cheese_dto.py index 427b4c6..83dc866 100644 --- a/src/app/v2/cheese_managers/dtos/cheese_dto.py +++ b/src/app/v2/cheese_managers/dtos/cheese_dto.py @@ -1,8 +1,14 @@ +from typing import Optional, TypedDict + from pydantic import BaseModel from common.base_models.base_dtos.base_response import BaseResponseDTO +class CheeseAmountResult(TypedDict): + total_cheese_amount: Optional[int] + + class CheeseDTO(BaseModel): cheeseBalance: int diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py index 01094ff..124078a 100644 --- a/src/app/v2/cheese_managers/models/cheese_manager.py +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -1,9 +1,13 @@ +from typing import Any + from tortoise import fields from tortoise.expressions import Q +from tortoise.fields import ForeignKeyRelation from tortoise.functions import Sum from tortoise.models import Model from tortoise.transactions import atomic +from app.v2.cheese_managers.dtos.cheese_dto import CheeseAmountResult from app.v2.cheese_managers.models.cheese_status import CheeseStatus @@ -15,7 +19,7 @@ class Meta: @staticmethod async def get_total_cheese_amount_by_manager(cheese_manager_id: int) -> int: - result = ( + result: list[dict[str, Any]] = ( await CheeseHistory.filter( Q(status=CheeseStatus.CAN_USE) | Q(status=CheeseStatus.USING), cheese_manager_id=cheese_manager_id, @@ -23,7 +27,11 @@ async def get_total_cheese_amount_by_manager(cheese_manager_id: int) -> int: .annotate(total_cheese_amount=Sum("current_amount")) .values("total_cheese_amount") ) - return result[0].get("total_cheese_amount", 0) + if not result or result[0].get("total_cheese_amount") is None: + return 0 + + total_cheese_amount = result[0].get("total_cheese_amount") + return int(total_cheese_amount) if total_cheese_amount is not None else 0 @staticmethod async def use_cheese(cheese_manager_id: int, amount: int) -> None: @@ -80,10 +88,9 @@ class CheeseHistory(Model): status = fields.CharEnumField(CheeseStatus, max_length=50, null=True) # Enum Field current_amount = fields.IntField() starting_amount = fields.IntField() - cheese_manager = fields.ForeignKeyField( + cheese_manager: ForeignKeyRelation[CheeseManager] = fields.ForeignKeyField( "models.CheeseManager", related_name="histories", - null=True, on_delete=fields.CASCADE, ) diff --git a/src/app/v2/cheese_managers/services/cheese_service.py b/src/app/v2/cheese_managers/services/cheese_service.py index 724e5cc..d846548 100644 --- a/src/app/v2/cheese_managers/services/cheese_service.py +++ b/src/app/v2/cheese_managers/services/cheese_service.py @@ -4,9 +4,9 @@ class CheeseService: @classmethod - async def get_cheese_balance(cls, cheese_manager_id: str) -> int: + async def get_cheese_balance(cls, cheese_manager_id: int) -> int: return await CheeseManager.get_total_cheese_amount_by_manager(cheese_manager_id=cheese_manager_id) or 0 @classmethod - async def add_cheese(cls, cheese_manager_id: str, amount: int) -> None: + async def add_cheese(cls, cheese_manager_id: int, amount: int) -> None: await CheeseManager.add_cheese(cheese_manager_id=cheese_manager_id, amount=amount) diff --git a/src/app/v2/colors/dtos/color_dto.py b/src/app/v2/colors/dtos/color_dto.py index 5436ce3..2306d63 100644 --- a/src/app/v2/colors/dtos/color_dto.py +++ b/src/app/v2/colors/dtos/color_dto.py @@ -5,8 +5,8 @@ class ColorCodeDTO(BaseModel): colorCode: str @classmethod - def builder(cls, color_raw: dict) -> "ColorCodeDTO": - return cls(colorCode=color_raw.get("color_code")) + def builder(cls, color_raw: dict[str, str]) -> "ColorCodeDTO": + return cls(colorCode=color_raw.get("color_code", "")) class ColorDTO(BaseModel): @@ -15,9 +15,9 @@ class ColorDTO(BaseModel): colorHexCode: str @classmethod - def builder(cls, color_raw: dict) -> "ColorDTO": + def builder(cls, color_raw: dict[str, str]) -> "ColorDTO": return cls( - colorCode=color_raw.get("color_code"), - colorName=color_raw.get("color_name"), - colorHexCode=color_raw.get("color_hex_code"), + colorCode=color_raw.get("color_code", ""), + colorName=color_raw.get("color_name", ""), + colorHexCode=color_raw.get("color_hex_code", ""), ) diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 8cded62..1ccb5e6 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -1,4 +1,7 @@ +from typing import Any + from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model from app.v2.colors.querys.color_query import ( @@ -6,31 +9,34 @@ SELECT_COLOR_BY_USER_UUID_QUERY, SELECT_COLOR_CODE_BY_USER_UUID_QUERY, ) +from app.v2.users.models.user import User from common.utils.query_executor import QueryExecutor class Color(Model): - color_id = fields.BigIntField(pk=True) # 자동 증가하는 기본 키 - color_code = fields.CharField(max_length=255, null=True) # 색상 코드 - user = fields.ForeignKeyField("models.User", related_name="colors", null=True, on_delete=fields.SET_NULL) + color_id = fields.BigIntField(pk=True) + color_code = fields.CharField(max_length=255, null=True) + user: ForeignKeyRelation[User] = fields.ForeignKeyField( + "models.User", related_name="colors", on_delete=fields.CASCADE + ) class Meta: table = "color" @classmethod - async def get_color_codes_by_user_id(cls, user_id: str) -> list[dict]: + async def get_color_codes_by_user_id(cls, user_id: str) -> Any: query = SELECT_COLOR_CODE_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") @classmethod - async def add_color_code_for_user(cls, user_id: str, color_code: str) -> dict: + async def add_color_code_for_user(cls, user_id: str, color_code: str) -> Any: query = INSERT_COLOR_CODE_FOR_USER_QUERY values = (color_code, user_id) return await QueryExecutor.execute_query(query, values=values, fetch_type="single") @classmethod - async def get_colors_with_details_by_user_id(cls, user_id: str) -> list[dict]: + async def get_colors_with_details_by_user_id(cls, user_id: str) -> Any: query = SELECT_COLOR_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="multiple") diff --git a/src/app/v2/colors/router.py b/src/app/v2/colors/router.py index 93a9d09..0b11aa3 100644 --- a/src/app/v2/colors/router.py +++ b/src/app/v2/colors/router.py @@ -11,7 +11,7 @@ response_model=ColorListResponseDTO, status_code=status.HTTP_200_OK, ) -async def get_user_color_handler(user_id: str): +async def get_user_color_handler(user_id: str) -> ColorListResponseDTO: colors = await ColorService.get_colors_with_details_by_user_id(user_id=user_id) diff --git a/src/app/v2/emotions/models/emotion.py b/src/app/v2/emotions/models/emotion.py index 23338e4..58732fd 100644 --- a/src/app/v2/emotions/models/emotion.py +++ b/src/app/v2/emotions/models/emotion.py @@ -1,22 +1,26 @@ +from typing import Any + from tortoise import fields, models +from tortoise.fields import ForeignKeyRelation from app.v2.emotions.querys.emotion_query import ( INSERT_EMOTION_CODE_FOR_USER_QUERY, SELECT_EMOTION_CODE_BY_USER_UUID_QUERY, ) +from app.v2.users.models.user import User from common.utils.query_executor import QueryExecutor class Emotion(models.Model): emotion_id = fields.BigIntField(pk=True) emotion_code = fields.CharField(max_length=255, unique=True) - user = fields.ForeignKeyField("models.User", related_name="emotions") + user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="emotions") class Meta: table = "emotion" @classmethod - async def get_emotions_with_details_by_user_id(cls, user_id: str) -> list[dict]: + async def get_emotions_with_details_by_user_id(cls, user_id: str) -> Any: query = SELECT_EMOTION_CODE_BY_USER_UUID_QUERY values = user_id return await QueryExecutor.execute_query(query, values=values, fetch_type="multiple") @@ -37,5 +41,5 @@ class Meta: table = "emotion_inventory" @classmethod - async def get_emotion_inventory(cls) -> list[dict]: + async def get_emotion_inventory(cls) -> list[dict[str, str]]: return await cls.all().values("emotion_code", "emotion_name") diff --git a/src/app/v2/emotions/services/emotion_service.py b/src/app/v2/emotions/services/emotion_service.py index 10890d3..cffbb7d 100644 --- a/src/app/v2/emotions/services/emotion_service.py +++ b/src/app/v2/emotions/services/emotion_service.py @@ -1,3 +1,5 @@ +from typing import Any + from app.v2.emotions.dtos.response import EmotionDTO, EmotionListResponseDTO from app.v2.emotions.models.emotion import Emotion, EmotionInventory @@ -19,7 +21,7 @@ class EmotionService: @classmethod - async def get_emotions(cls, user_id: str) -> list[dict]: + async def get_emotions(cls, user_id: str) -> Any: return await Emotion.get_emotions_with_details_by_user_id(user_id=user_id) @classmethod @@ -27,7 +29,7 @@ async def add_emotion(cls, user_id: str, emotion_code: str) -> None: await Emotion.add_emotion(user_id=user_id, emotion_code=emotion_code) @classmethod - async def get_emotion_inventory(cls) -> list[dict]: + async def get_emotion_inventory(cls) -> list[dict[str, str]]: return await EmotionInventory.get_emotion_inventory() @classmethod diff --git a/src/app/v2/items/models/item.py b/src/app/v2/items/models/item.py index 592f286..5717827 100644 --- a/src/app/v2/items/models/item.py +++ b/src/app/v2/items/models/item.py @@ -1,4 +1,5 @@ from tortoise import fields, models +from tortoise.fields import ForeignKeyRelation class ItemInventory(models.Model): @@ -24,8 +25,12 @@ class Meta: class ItemInventoryProductInventory(models.Model): item_inventory_product_inventory_id = fields.BigIntField(pk=True) quantity = fields.IntField() - item_inventory = fields.ForeignKeyField("models.ItemInventory", related_name="product_inventories") - product_inventory = fields.ForeignKeyField("models.ProductInventory", related_name="item_inventories") + item_inventory: ForeignKeyRelation[ItemInventory] = fields.ForeignKeyField( + "models.ItemInventory", related_name="product_inventories" + ) + product_inventory: ForeignKeyRelation[ProductInventory] = fields.ForeignKeyField( + "models.ProductInventory", related_name="item_inventories" + ) item_measurement = fields.CharField(max_length=255, null=True) class Meta: @@ -39,6 +44,8 @@ class RewardInventory(models.Model): reward_description = fields.CharField(max_length=255, null=True) reward_name = fields.CharField(max_length=255, null=True) + item_inventories = fields.ReverseRelation["ItemInventoryRewardInventory"] + class Meta: table = "reward_inventory" @@ -46,13 +53,13 @@ class Meta: class ItemInventoryRewardInventory(models.Model): item_inventory_reward_invnetory_id = fields.BigIntField(pk=True) quantity = fields.IntField() - item_inventory = fields.ForeignKeyField( + item_inventory: ForeignKeyRelation[ItemInventory] = fields.ForeignKeyField( "models.ItemInventory", related_name="reward_inventories", on_delete=fields.CASCADE, db_column="item_inventory_id", ) - reward_inventory = fields.ForeignKeyField( + reward_inventory: ForeignKeyRelation[RewardInventory] = fields.ForeignKeyField( "models.RewardInventory", related_name="item_inventories", on_delete=fields.CASCADE, diff --git a/src/app/v2/levels/dtos/level_dto.py b/src/app/v2/levels/dtos/level_dto.py index 7b103f3..b9e8e25 100644 --- a/src/app/v2/levels/dtos/level_dto.py +++ b/src/app/v2/levels/dtos/level_dto.py @@ -1,3 +1,5 @@ +from typing import Any + from pydantic import BaseModel @@ -7,7 +9,7 @@ class LevelDTO(BaseModel): requiredExp: int | None = None @classmethod - def builder(cls, level: dict) -> "LevelDTO": + def builder(cls, level: dict[str, Any]) -> "LevelDTO": return cls( level=level["level_level"], currentExp=level["level_exp"], diff --git a/src/app/v2/levels/models/level.py b/src/app/v2/levels/models/level.py index 6492b88..8081d0f 100644 --- a/src/app/v2/levels/models/level.py +++ b/src/app/v2/levels/models/level.py @@ -1,12 +1,9 @@ +from typing import Any + from tortoise import fields from tortoise.models import Model -from app.v2.levels.querys.level_query import ( - SELECT_USER_EXP_QUERY, - SELECT_USER_LEVEL_AND_EXP_BY_USER_UUID_QUERY, - SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY, - UPDATE_USER_LEVEL_AND_EXP_QUERY, -) +from app.v2.levels.querys.level_query import SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY, UPDATE_USER_LEVEL_AND_EXP_QUERY from common.utils.query_executor import QueryExecutor @@ -19,7 +16,7 @@ class Meta: table = "level" @classmethod - async def get_level_info(cls, user_id: str) -> dict | None: + async def get_level_info(cls, user_id: str) -> Any: query = SELECT_USER_LEVEL_AND_REQUIRED_EXP_QUERY value = user_id return await QueryExecutor.execute_query(query, values=(value,), fetch_type="single") diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index f828ab0..6a59777 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -1,3 +1,5 @@ +from fastapi import HTTPException + from app.v2.answers.services.answer_service import AnswerService from app.v2.levels.dtos.level_dto import LevelDTO, LevelInfoDTO from app.v2.levels.models.level import Level @@ -6,11 +8,18 @@ class LevelService: @classmethod async def get_level_info(cls, user_id: str) -> LevelDTO: - return LevelDTO.builder(level=await Level.get_level_info(user_id=user_id)) + level_data = await Level.get_level_info(user_id=user_id) + if level_data is None: + raise HTTPException(status_code=404, detail="Level info not found") + return LevelDTO.builder(level=level_data) @classmethod - async def get_level_info_add_answer_days(cls, user_id: str) -> LevelDTO: + async def get_level_info_add_answer_days(cls, user_id: str) -> LevelInfoDTO: level_dto = await cls.get_level_info(user_id=user_id) + + if level_dto.requiredExp is None: + raise ValueError("Required experience cannot be None") # 예외를 던지는 방법 + needs_to_level_up = await cls.calculate_days_to_level_up( user_id=user_id, current_exp=level_dto.currentExp, @@ -26,8 +35,11 @@ async def level_up(cls, user_id: str) -> int: level_dto = await cls.get_level_info(user_id=user_id) level = level_dto.level - current_exp = level_dto.current_exp - required_exp = level_dto.required_exp + current_exp = level_dto.currentExp + required_exp = level_dto.requiredExp + + if current_exp is None or required_exp is None: + raise ValueError("Experience values cannot be None") if current_exp >= required_exp: new_exp = current_exp - required_exp @@ -41,7 +53,7 @@ async def level_up(cls, user_id: str) -> int: async def add_exp(cls, user_id: str, exp: int) -> None: level_dto = await cls.get_level_info(user_id=user_id) - current_exp = level_dto.current_exp + current_exp = level_dto.currentExp new_exp = current_exp + exp await Level.update_level_and_exp(user_id=user_id, new_level=level_dto.level, new_exp=new_exp) diff --git a/src/app/v2/likes/models/like.py b/src/app/v2/likes/models/like.py index 6ed2dae..8369c8c 100644 --- a/src/app/v2/likes/models/like.py +++ b/src/app/v2/likes/models/like.py @@ -1,14 +1,23 @@ +from typing import Any + from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model +from app.v2.answers.models.answer import Answer from app.v2.likes.querys.like_query import SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY +from app.v2.users.models.user import User from common.utils.query_executor import QueryExecutor class Like(Model): likes_id = fields.BigIntField(pk=True) - answer = fields.ForeignKeyField("models.Answer", related_name="likes", on_delete=fields.CASCADE) - user = fields.ForeignKeyField("models.User", related_name="likes", on_delete=fields.CASCADE) + answer: ForeignKeyRelation[Answer] = fields.ForeignKeyField( + "models.Answer", related_name="likes", on_delete=fields.CASCADE + ) + user: ForeignKeyRelation[User] = fields.ForeignKeyField( + "models.User", related_name="likes", on_delete=fields.CASCADE + ) created_time = fields.DatetimeField(null=True) modified_time = fields.DatetimeField(null=True) created_at = fields.DatetimeField(auto_now_add=True) @@ -22,7 +31,7 @@ class Meta: ] @staticmethod - async def get_unique_likes_today(user_id: str) -> dict: + async def get_unique_likes_today(user_id: str) -> Any: query = SELECT_UNIQUE_LIKES_COUNT_BY_USER_TODAY_QUERY values = (user_id,) return await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/missions/dtos/mission_dto.py b/src/app/v2/missions/dtos/mission_dto.py index 2968973..4d73b7a 100644 --- a/src/app/v2/missions/dtos/mission_dto.py +++ b/src/app/v2/missions/dtos/mission_dto.py @@ -1,3 +1,5 @@ +from typing import Any + from pydantic import BaseModel @@ -8,16 +10,16 @@ class UserMissionDTO(BaseModel): progress_count: int @classmethod - def builder(cls, user_mission: dict) -> "UserMissionDTO": + def builder(cls, user_mission: dict[str, Any]) -> "UserMissionDTO": is_completed_raw = user_mission.get("is_completed") is_completed = ( bool(int.from_bytes(is_completed_raw, byteorder="big")) - if isinstance(is_completed_raw, bytes) - else bool(is_completed_raw) + if isinstance(is_completed_raw, bytes) and is_completed_raw is not None + else bool(is_completed_raw) if is_completed_raw is not None else False ) return cls( - user_mission_id=user_mission.get("user_mission_id"), + user_mission_id=user_mission.get("user_mission_id", 0), # 기본값 0 설정 is_completed=is_completed, - mission_code=user_mission.get("mission_code"), - progress_count=user_mission.get("progress_count"), + mission_code=user_mission.get("mission_code", ""), # 기본값 빈 문자열 설정 + progress_count=user_mission.get("progress_count", 0), # 기본값 0 설정 ) diff --git a/src/app/v2/missions/models/mission.py b/src/app/v2/missions/models/mission.py index 3438c54..ca68deb 100644 --- a/src/app/v2/missions/models/mission.py +++ b/src/app/v2/missions/models/mission.py @@ -1,7 +1,11 @@ +from typing import Any + from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model from app.v2.missions.querys.mission_query import SELECT_USER_MISSIONS_QUERY, UPDATE_USER_MISSION_PROGRESS_QUERY +from app.v2.users.models.user import User from common.utils.query_executor import QueryExecutor @@ -10,10 +14,10 @@ class UserMission(Model): is_completed = fields.BooleanField(default=False) mission_code = fields.CharField(max_length=255) progress_count = fields.IntField(default=0) - user = fields.ForeignKeyField("models.User", related_name="missions") + user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="missions") @classmethod - async def get_user_missions_by_condition_type(cls, user_id: str) -> dict: + async def get_user_missions_by_condition_type(cls, user_id: str) -> Any: query = SELECT_USER_MISSIONS_QUERY values = (user_id,) return await QueryExecutor.execute_query(query, values=values, fetch_type="multiple") diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index f1b174f..77a91e1 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -5,7 +5,7 @@ router = APIRouter(prefix="/mission", tags=["Mission"]) -async def process_mission_in_background(user_id: str): +async def process_mission_in_background(user_id: str) -> None: mission_service = MissionService() await mission_service.update_mission_progress(user_id) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 5b6b9e5..a16840a 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -44,7 +44,7 @@ async def update_mission_progress(self, user_id: str) -> None: UserService.get_user_info(user_id=user_id), self.get_user_missions(user_id=user_id), MissionInventory.all() ) - cheese_manager_id = user["cheese_manager_id"] + cheese_manager_id: int = user["cheese_manager_id"] mission_dict = {mission.mission_code: mission for mission in missions} level_up_mission = None @@ -62,7 +62,11 @@ async def update_mission_progress(self, user_id: str) -> None: ) async def _process_single_mission( - self, user_mission: UserMissionDTO, mission_dict: dict, cheese_manager_id: dict, user_id: str + self, + user_mission: UserMissionDTO, + mission_dict: dict[str, MissionInventory], + cheese_manager_id: int, + user_id: str, ) -> None: mission = mission_dict.get(user_mission.mission_code) @@ -95,8 +99,6 @@ async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> i return 1 elif mission_code == "MS_BADGE_POST_280_CHAR" and await self.check_long_answer(user_id): return 1 - # elif mission_code == "MS_DAILY_POST_GENERAL" and await self.check_post_count_min(user_id, 6): - # return 1 elif mission_code == "MS_BADGE_POST_CONSECUTIVE_7" and await self.check_consecutive_days(user_id): return 1 elif mission_code == "MS_BADGE_POST_EARLY_3" and await self.check_early_morning_posts(user_id): @@ -114,17 +116,20 @@ async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> i @staticmethod async def check_first_post(user_id: str) -> bool: post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) - return post_count_raw.get("answer_count", 0) > 0 + post_count: int = post_count_raw.get("answer_count", 0) + return post_count > 0 @staticmethod async def check_post_count_range(user_id: str, min_count: int, max_count: int) -> bool: - post_count = await Answer.get_answer_count_by_user_id(user_id=user_id) - return min_count <= post_count.get("answer_count", 0) <= max_count + post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) + post_count: int = post_count_raw.get("answer_count", 0) + return min_count <= post_count <= max_count @staticmethod async def check_post_count_min(user_id: str, min_count: int) -> bool: - post_count = await Answer.get_answer_count_by_user_id(user_id=user_id) - return post_count.get("answer_count", 0) >= min_count + post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) + post_count: int = post_count_raw.get("answer_count", 0) + return post_count >= min_count @staticmethod async def check_long_answer(user_id: str) -> bool: @@ -141,7 +146,7 @@ async def check_early_morning_posts(user_id: str) -> bool: recent_answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) if recent_answer: answer_time = recent_answer.get("created_time") - return 0 <= answer_time.hour <= 5 if answer_time else False + return 0 <= answer_time.hour <= 5 if isinstance(answer_time, datetime) else False return False @staticmethod @@ -164,14 +169,14 @@ async def check_christmas_period() -> bool: @staticmethod async def check_three_likes_different_posts(user_id: str) -> bool: like_raw = await Like.get_unique_likes_today(user_id) - like_count = like_raw.get("unique_likes", 0) + like_count: int = like_raw.get("unique_likes", 0) return like_count >= 3 async def reward_user_for_mission( self, user_id: str, reward_code: str, - cheese_manager_id: str, + cheese_manager_id: int, ) -> None: item_inventory_rewards = await self.validate_reward(reward_code=reward_code) @@ -180,7 +185,7 @@ async def reward_user_for_mission( ) @staticmethod - async def validate_reward(reward_code: str) -> list[ItemInventoryRewardInventory]: + async def validate_reward(reward_code: str): # type: ignore try: reward = await RewardInventory.filter(reward_code=reward_code).prefetch_related("item_inventories").first() @@ -189,9 +194,6 @@ async def validate_reward(reward_code: str) -> list[ItemInventoryRewardInventory item_inventory_rewards = reward.item_inventories - if not item_inventory_rewards: - raise HTTPException(status_code=404, detail="No inventory found for this reward.") - return item_inventory_rewards except DoesNotExist: @@ -203,7 +205,7 @@ async def process_reward( cls, item_inventory_rewards: list[ItemInventoryRewardInventory], user_id: str, - cheese_manager_id: str, + cheese_manager_id: int, ) -> None: for item_inventory_reward in item_inventory_rewards: item: ItemInventory = await item_inventory_reward.item_inventory diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index e35915a..2d0f3f3 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -4,7 +4,6 @@ from app.v2.answers.services.answer_service import AnswerService from app.v2.badges.services.badge_service import BadgeService - from app.v2.cheese_managers.services.cheese_service import CheeseService from app.v2.colors.services.color_service import ColorService from app.v2.levels.services.level_service import LevelService diff --git a/src/app/v2/payments/services/payment_service.py b/src/app/v2/payments/services/payment_service.py index c250ea9..b03d68c 100644 --- a/src/app/v2/payments/services/payment_service.py +++ b/src/app/v2/payments/services/payment_service.py @@ -1,5 +1,5 @@ from fastapi import HTTPException -from tortoise.exceptions import DoesNotExist +from tortoise.exceptions import DoesNotExist, IntegrityError from tortoise.transactions import atomic from app.v2.badges.services.badge_service import BadgeService @@ -7,6 +7,8 @@ from app.v2.colors.services.color_service import ColorService from app.v2.emotions.services.emotion_service import EmotionService from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory +from common.exceptions.custom_exception import CustomException +from common.exceptions.error_code import ErrorCode class PaymentService: @@ -18,19 +20,19 @@ async def validate_payment( product = await ProductInventory.get(product_code=product_code) if product.transaction_currency != "CHEESE": - raise HTTPException(status_code=400, detail="Invalid transaction currency for payment.") + raise CustomException(ErrorCode.INVALID_TRANSACTION_CURRENCY) item_inventory_products = await ItemInventoryProductInventory.filter( product_inventory_id=product.product_id ).all() if not item_inventory_products: - raise HTTPException(status_code=404, detail="No inventory found for this product.") + raise CustomException(ErrorCode.NO_INVENTORY_FOR_PRODUCT) return product, item_inventory_products except DoesNotExist: - raise HTTPException(status_code=404, detail="Product not found.") + raise CustomException(ErrorCode.PRODUCT_NOT_FOUND) @classmethod @atomic() @@ -46,25 +48,28 @@ async def process_cheese_payment( total_required_cheese = product.price if total_cheese < total_required_cheese: - raise HTTPException(status_code=400, detail="Not enough cheese for this purchase") + raise CustomException(ErrorCode.NOT_ENOUGH_CHEESE) try: - await CheeseManager.use_cheese(cheese_manager_id, total_required_cheese) + await CheeseManager.use_cheese(cheese_manager_id, int(total_required_cheese)) except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) + raise CustomException(ErrorCode.NOT_ENOUGH_CHEESE) - for item_inventory_product in item_inventory_products: - item: ItemInventory = await item_inventory_product.item_inventory - quantity = item_inventory_product.quantity + try: + for item_inventory_product in item_inventory_products: + item: ItemInventory = await item_inventory_product.item_inventory + quantity = item_inventory_product.quantity - if item.item_category == "BADGE": - for _ in range(quantity): - await BadgeService.add_badge(user_id=user_id, badge_code=item.item_code) - elif item.item_category == "COLOR": - for _ in range(quantity): - await ColorService.add_color(user_id=user_id, color_code=item.item_code) - elif item.item_category == "EMOTION": - for _ in range(quantity): - await EmotionService.add_emotion(user_id=user_id, emotion_code=item.item_code) - else: - raise ValueError(f"Invalid item category for cheese payment: {item.item_category}") + if item.item_category == "BADGE": + for _ in range(quantity): + await BadgeService.add_badge(user_id=user_id, badge_code=item.item_code) + elif item.item_category == "COLOR": + for _ in range(quantity): + await ColorService.add_color(user_id=user_id, color_code=item.item_code) + elif item.item_category == "EMOTION": + for _ in range(quantity): + await EmotionService.add_emotion(user_id=user_id, emotion_code=item.item_code) + else: + raise CustomException(ErrorCode.INVALID_ITEM_CATEGORY) + except IntegrityError: + raise CustomException(ErrorCode.DUPLICATE_PURCHASE) diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py index 16cf433..70287fa 100644 --- a/src/app/v2/purchases/models/purchase_history.py +++ b/src/app/v2/purchases/models/purchase_history.py @@ -1,7 +1,10 @@ # models.py from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model +from app.v2.users.models.user import User + # models.py (계속) class PurchaseHistory(Model): @@ -9,7 +12,7 @@ class PurchaseHistory(Model): product_code = fields.CharField(max_length=255) status = fields.CharField(max_length=255) receipt_id = fields.CharField(max_length=255, unique=True) # 영수증 중복 방지 - user = fields.ForeignKeyField("models.User", related_name="purchase_histories") + user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="purchase_histories") class Meta: table = "purchase_history" diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 48b9558..9cee12c 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -1,3 +1,5 @@ +from typing import Any + from fastapi import APIRouter, HTTPException, status from tortoise.exceptions import DoesNotExist @@ -9,7 +11,7 @@ @router.post("/process-receipt") -async def process_receipt(receipt: ReceiptRequestDTO) -> dict: +async def process_receipt(receipt: ReceiptRequestDTO) -> dict[str, Any]: if not receipt.receiptData or not receipt.user_id: raise HTTPException(status_code=400, detail="Missing data") purchase_service = PurchaseService() @@ -21,10 +23,10 @@ async def process_receipt(receipt: ReceiptRequestDTO) -> dict: } -@router.get("/receipt-test") -async def receipt_test() -> dict: - purchase_service = PurchaseService() - return await purchase_service.receipt_test() +# @router.get("/receipt-test") +# async def receipt_test() -> dict[str, Any]: +# purchase_service = PurchaseService() +# return await purchase_service.receipt_test() @router.post("") diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index f73274b..f77fade 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,5 +1,6 @@ import base64 import re +from typing import Any, cast import httpx from fastapi import HTTPException @@ -18,7 +19,7 @@ async def process_krw_payment(product: ProductInventory, quantity: int) -> None: print(f"Processing KRW payment: {product.price * quantity} KRW") # 여기에 실제 KRW 결제 처리 로직 구현 - async def validate_receipt(self, receipt_data: str, user_id: str) -> dict: + async def validate_receipt(self, receipt_data: str, user_id: str) -> dict[str, Any]: url = settings.APPLE_URL @@ -31,33 +32,33 @@ async def validate_receipt(self, receipt_data: str, user_id: str) -> dict: response = await client.post(url, json=payload) print(response.json()) if response.status_code == 200: - return response.json() + return cast(dict[str, Any], response.json()) # return await self._handle_receipt_response(response.json(), user_id) else: raise HTTPException(status_code=500, detail="Failed to connect to Apple server") - async def receipt_test(self) -> dict: - file_path = "/Users/gimtaeu/workspace/tellingme-python-server/1개월 구독한 영수증 data.txt/TXT.rtf" - base64_data = self.extract_base64_from_rtf(file_path) - - url = settings.APPLE_URL - - payload = { - "exclude-old-transactions": True, - "receipt-data": base64_data, - "password": settings.APPLE_SHARED_SECRET, - } - - async with httpx.AsyncClient() as client: - response = await client.post(url, json=payload) - response_data = response.json() - print(response_data) - if response.status_code != 200 or response_data.get("status") != 0: - raise HTTPException(status_code=400, detail="Receipt verification failed") - - return response_data - - async def _handle_receipt_response(self, response_data: dict, user_id: str) -> dict: + # async def receipt_test(self) -> dict[str, Any]: + # file_path = "/Users/gimtaeu/workspace/tellingme-python-server/1개월 구독한 영수증 data.txt/TXT.rtf" + # base64_data = self.extract_base64_from_rtf(file_path) + # + # url = settings.APPLE_URL + # + # payload = { + # "exclude-old-transactions": True, + # "receipt-data": base64_data, + # "password": settings.APPLE_SHARED_SECRET, + # } + # + # async with httpx.AsyncClient() as client: + # response = await client.post(url, json=payload) + # response_data = response.json() + # print(response_data) + # if response.status_code != 200 or response_data.get("status") != 0: + # raise HTTPException(status_code=400, detail="Receipt verification failed") + # + # return response_data + + async def _handle_receipt_response(self, response_data: dict[str, Any], user_id: str) -> dict[str, Any]: if response_data.get("status") == 0: in_app_purchase = response_data.get("receipt", {}).get("in_app", []) if in_app_purchase: @@ -69,7 +70,7 @@ async def _handle_receipt_response(self, response_data: dict, user_id: str) -> d raise HTTPException(status_code=400, detail="Invalid receipt") @classmethod - async def _save_purchase_history(cls, user_id: str, purchase_info: dict) -> dict: + async def _save_purchase_history(cls, user_id: str, purchase_info: dict[str, Any]) -> dict[str, Any]: receipt_id = purchase_info.get("transaction_id") product_code = purchase_info.get("product_id") status = "completed" @@ -114,7 +115,7 @@ async def process_purchase( cls, item_inventory_products: list[ItemInventoryProductInventory], user_id: str, - cheese_manager_id: str, + cheese_manager_id: int, ) -> None: for item_inventory_product in item_inventory_products: item: ItemInventory = await item_inventory_product.item_inventory @@ -157,34 +158,34 @@ async def process_purchase( import base64 import re - @staticmethod - def extract_base64_from_rtf(file_path): - # 파일 읽기 - try: - with open(file_path, "r", encoding="utf-8") as file: - content = file.read() - except FileNotFoundError: - raise ValueError(f"File not found: {file_path}") - except Exception as e: - raise ValueError(f"Failed to read the file: {str(e)}") - - # Base64 데이터 추출 (MII로 시작하는 패턴만) - base64_pattern = r"MII[A-Za-z0-9+/=]+" - matches = re.findall(base64_pattern, content) - - if not matches: - raise ValueError("No valid Base64 data found in the file.") - - # 가장 긴 Base64 데이터 선택 - base64_data = max(matches, key=len) - - # Base64 데이터 유효성 검증 - try: - base64.b64decode(base64_data, validate=True) - except Exception: - raise ValueError("Extracted data is not valid Base64.") - - # 디버깅 로그 출력 - print(f"Extracted Base64 data length: {len(base64_data)}") - - return base64_data + # @staticmethod + # def extract_base64_from_rtf(file_path: str) -> str: + # # 파일 읽기 + # try: + # with open(file_path, "r", encoding="utf-8") as file: + # content = file.read() + # except FileNotFoundError: + # raise ValueError(f"File not found: {file_path}") + # except Exception as e: + # raise ValueError(f"Failed to read the file: {str(e)}") + # + # # Base64 데이터 추출 (MII로 시작하는 패턴만) + # base64_pattern = r"MII[A-Za-z0-9+/=]+" + # matches = re.findall(base64_pattern, content) + # + # if not matches: + # raise ValueError("No valid Base64 data found in the file.") + # + # # 가장 긴 Base64 데이터 선택 + # base64_data = max(matches, key=len) + # + # # Base64 데이터 유효성 검증 + # try: + # base64.b64decode(base64_data, validate=True) + # except Exception: + # raise ValueError("Extracted data is not valid Base64.") + # + # # 디버깅 로그 출력 + # print(f"Extracted Base64 data length: {len(base64_data)}") + # + # return base64_data diff --git a/src/app/v2/teller_cards/dtos/response.py b/src/app/v2/teller_cards/dtos/response.py index 789888d..4df7b3e 100644 --- a/src/app/v2/teller_cards/dtos/response.py +++ b/src/app/v2/teller_cards/dtos/response.py @@ -1,5 +1,6 @@ from pydantic import BaseModel +from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO as TellerCardLogicDTO from common.base_models.base_dtos.base_response import BaseResponseDTO @@ -12,7 +13,7 @@ class TellerCardResponseDTO(BaseResponseDTO): data: TellerCardDTO @classmethod - def builder(cls, teller_card: TellerCardDTO) -> "TellerCardResponseDTO": + def builder(cls, teller_card: TellerCardLogicDTO) -> "TellerCardResponseDTO": return cls( code=200, message="success", diff --git a/src/app/v2/teller_cards/dtos/teller_card_dto.py b/src/app/v2/teller_cards/dtos/teller_card_dto.py index d29463b..a38e524 100644 --- a/src/app/v2/teller_cards/dtos/teller_card_dto.py +++ b/src/app/v2/teller_cards/dtos/teller_card_dto.py @@ -1,3 +1,5 @@ +from typing import Any + from pydantic import BaseModel @@ -8,10 +10,10 @@ class TellerCardDTO(BaseModel): colorCode: str @classmethod - def builder(cls, teller_card_raw: dict) -> "TellerCardDTO": + def builder(cls, teller_card_raw: dict[str, str]) -> "TellerCardDTO": return cls( - badgeCode=teller_card_raw.get("activate_badge_code"), - badgeName=teller_card_raw.get("badge_name"), - badgeMiddleName=teller_card_raw.get("badge_middle_name"), - colorCode=teller_card_raw.get("activate_color_code"), + badgeCode=teller_card_raw.get("activate_badge_code", ""), + badgeName=teller_card_raw.get("badge_name", ""), + badgeMiddleName=teller_card_raw.get("badge_middle_name", ""), + colorCode=teller_card_raw.get("activate_color_code", ""), ) diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py index d3f69ee..3eace96 100644 --- a/src/app/v2/teller_cards/models/teller_card.py +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -1,3 +1,5 @@ +from typing import Any + from tortoise import fields from tortoise.models import Model @@ -17,10 +19,10 @@ class Meta: table = "teller_card" @classmethod - async def get_teller_card_info_by_user_id(cls, user_id: str) -> dict: + async def get_teller_card_info_by_user_id(cls, user_id: str) -> Any: # type ignore query = SELECT_TELLER_CARD_INFO_BY_USER_UUID_QUERY value = user_id - return await QueryExecutor.execute_query(query, values=value, fetch_type="single") + return await QueryExecutor.execute_query(query, values=value, fetch_type="single") # type ignore @classmethod async def patch_teller_card_info_by_user_id(cls, user_id: str, badge_code: str, color_code: str) -> None: diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py index 4ce4d0c..10d9f17 100644 --- a/src/app/v2/teller_cards/services/teller_card_service.py +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -7,7 +7,7 @@ class TellerCardService: @classmethod async def get_teller_card(cls, user_id: str) -> TellerCardDTO: - teller_cards_raw = await TellerCard.get_teller_card_info_by_user_id(user_id=user_id) + teller_cards_raw: dict[str, str] = await TellerCard.get_teller_card_info_by_user_id(user_id=user_id) return TellerCardDTO.builder(teller_cards_raw) @classmethod diff --git a/src/app/v2/users/dtos/user_info_dto.py b/src/app/v2/users/dtos/user_info_dto.py index 06a61f7..4bc245d 100644 --- a/src/app/v2/users/dtos/user_info_dto.py +++ b/src/app/v2/users/dtos/user_info_dto.py @@ -9,9 +9,9 @@ class UserInfoDTO(BaseModel): tellerCard: TellerCardDTO @classmethod - def builder(cls, user_raw: dict, cheeseBalance: int, tellerCard: TellerCardDTO) -> "UserInfoDTO": + def builder(cls, user_raw: dict[str, str], cheeseBalance: int, tellerCard: TellerCardDTO) -> "UserInfoDTO": return cls( - nickname=user_raw.get("nickname"), + nickname=user_raw.get("nickname", ""), cheeseBalance=cheeseBalance, tellerCard=tellerCard, ) diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index 2151da0..219e63f 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -1,8 +1,14 @@ from datetime import datetime +from typing import Any, Optional from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model +from app.v2.cheese_managers.models.cheese_manager import CheeseManager +from app.v2.levels.models.level import Level +from app.v2.teller_cards.models.teller_card import TellerCard +from app.v2.users.models.refresh_token import RefreshToken from app.v2.users.querys.user_query import ( SELECT_USER_INFO_BY_USER_UUID_QUERY, SELECT_USER_PROFILE_BY_USER_ID_QUERY, @@ -26,7 +32,7 @@ class User(Model): social_login_type = fields.CharField(max_length=16) user_status = fields.BooleanField() withdraw_period = fields.DatetimeField(null=True) - refresh_token = fields.ForeignKeyField( + refresh_token: Optional[ForeignKeyRelation[RefreshToken]] = fields.ForeignKeyField( "models.RefreshToken", related_name="users", db_column="refresh_token_id", @@ -38,42 +44,39 @@ class User(Model): default="https://miro.medium.com/v2/resize:fit:1400/format:webp/1*dh7Xy5tFvRj7n2wf1UweAw.png", ) premium_started_at = fields.DatetimeField(null=True) - cheese_manager = fields.ForeignKeyField( + cheese_manager: ForeignKeyRelation[CheeseManager] = fields.ForeignKeyField( "models.CheeseManager", related_name="users", db_column="cheese_manager_id", - null=True, ) - teller_card = fields.ForeignKeyField( + teller_card: ForeignKeyRelation[TellerCard] = fields.ForeignKeyField( "models.TellerCard", related_name="users", db_column="teller_card_id", - null=True, ) - level = fields.ForeignKeyField( + level: ForeignKeyRelation[Level] = fields.ForeignKeyField( "models.Level", related_name="users", db_column="level_id", - null=True, ) class Meta: table = "user" @classmethod - async def get_user_profile_by_user_id(cls, user_id: str) -> dict: + async def get_user_profile_by_user_id(cls, user_id: str) -> Any: query = SELECT_USER_PROFILE_BY_USER_ID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod - async def get_user_info_by_user_id(cls, user_id: str) -> dict: + async def get_user_info_by_user_id(cls, user_id: str) -> Any: query = SELECT_USER_INFO_BY_USER_UUID_QUERY value = user_id return await QueryExecutor.execute_query(query, values=value, fetch_type="single") @classmethod - async def set_is_premium(cls, user_id: str, is_premium: bool) -> None: + async def set_is_premium(cls, user_id: str, is_premium: bool) -> Any: query = UPDATE_PREMIUM_STATUS_QUERY current_time = datetime.now() values = (int(is_premium), current_time, user_id) diff --git a/src/app/v2/users/models/user_mission.py b/src/app/v2/users/models/user_mission.py index 5ffdc52..eb1712c 100644 --- a/src/app/v2/users/models/user_mission.py +++ b/src/app/v2/users/models/user_mission.py @@ -1,13 +1,16 @@ from tortoise import fields +from tortoise.fields import ForeignKeyRelation from tortoise.models import Model +from app.v2.users.models.user import User + class UserMission(Model): user_mission_id = fields.BigIntField(pk=True) is_completed = fields.BooleanField(null=True) mission_code = fields.CharField(max_length=255, null=True) progress_count = fields.IntField(null=True) - user_id = fields.ForeignKeyField("models.User", related_name="missions", null=True) + user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="missions") class Meta: table = "user_mission" diff --git a/src/app/v2/users/services/user_service.py b/src/app/v2/users/services/user_service.py index b2ae2a9..74d751f 100644 --- a/src/app/v2/users/services/user_service.py +++ b/src/app/v2/users/services/user_service.py @@ -1,14 +1,16 @@ +from typing import Any + from app.v2.cheese_managers.models.cheese_manager import CheeseManager from app.v2.users.models.user import User class UserService: @staticmethod - async def get_user_info(user_id: str) -> dict: + async def get_user_info(user_id: str) -> Any: return await User.get_user_info_by_user_id(user_id=user_id) @classmethod - async def get_user_profile(cls, user_id: str) -> dict: + async def get_user_profile(cls, user_id: str) -> Any: return await User.get_user_profile_by_user_id(user_id=user_id) @staticmethod diff --git a/src/common/exceptions/__init__.py b/src/common/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/exceptions/custom_exception.py b/src/common/exceptions/custom_exception.py new file mode 100644 index 0000000..48e6fb3 --- /dev/null +++ b/src/common/exceptions/custom_exception.py @@ -0,0 +1,16 @@ +from typing import Any + +from common.exceptions.error_code import ErrorCode + + +class CustomException(Exception): + def __init__(self, error_code: ErrorCode) -> None: + self.error_code = error_code + super().__init__(error_code.message) + + def to_dict(self) -> dict[str, Any]: + return { + "code": self.error_code.code, + "message": self.error_code.message, + "data": None, + } diff --git a/src/common/exceptions/error_code.py b/src/common/exceptions/error_code.py new file mode 100644 index 0000000..a21be77 --- /dev/null +++ b/src/common/exceptions/error_code.py @@ -0,0 +1,24 @@ +from enum import Enum + + +class ErrorCode(Enum): + NOT_ENOUGH_CHEESE = (4003, "치즈가 부족하여 구매를 진행할 수 없습니다.") + INVALID_ITEM_CATEGORY = (4004, "치즈 결제에 유효하지 않은 아이템 카테고리입니다.") + INVALID_TRANSACTION_CURRENCY = (4001, "결제에 유효하지 않은 거래 통화입니다.") + DUPLICATE_PURCHASE = (4005, "이미 소유한 제품입니다.") + + # 404 Not Found + NO_INVENTORY_FOR_PRODUCT = (4041, "이 상품에 대한 재고가 없습니다.") + PRODUCT_NOT_FOUND = (4042, "해당 상품을 찾을 수 없습니다.") + + def __init__(self, code: int, message: str) -> None: + self._code = code + self._message = message + + @property + def code(self) -> int: + return self._code + + @property + def message(self) -> str: + return self._message diff --git a/src/common/handlers/exception_handler.py b/src/common/handlers/exception_handler.py index b3f43c3..84313d0 100644 --- a/src/common/handlers/exception_handler.py +++ b/src/common/handlers/exception_handler.py @@ -1,6 +1,8 @@ from fastapi import FastAPI, Request from starlette.responses import JSONResponse +from common.exceptions.custom_exception import CustomException + def attach_exception_handlers(app: FastAPI) -> None: @app.exception_handler(Exception) @@ -13,3 +15,11 @@ async def global_exception_handler(request: Request, exc: Exception) -> JSONResp "message": "An unexpected error occurred. Please try again later.", }, ) + + @app.exception_handler(CustomException) + async def custom_exception_handler(request: Request, exc: CustomException) -> JSONResponse: + status_code = exc.error_code.code // 10 + return JSONResponse( + status_code=status_code, + content=exc.to_dict(), + ) diff --git a/src/common/utils/query_executor.py b/src/common/utils/query_executor.py index 3e78339..a30494c 100644 --- a/src/common/utils/query_executor.py +++ b/src/common/utils/query_executor.py @@ -1,4 +1,5 @@ -from typing import Union +from datetime import datetime +from typing import Any, Union from tortoise import Tortoise @@ -7,8 +8,10 @@ class QueryExecutor: @staticmethod async def execute_query( - query: str, values: Union[tuple, str] = (), fetch_type: str = "multiple" - ) -> Union[dict, list[dict], int]: + query: str, + values: Union[tuple[Union[str, int, float, datetime], ...], str] = (), + fetch_type: str = "multiple", + ) -> Any: # type ignore """ SQL 쿼리를 실행하고 결과를 반환합니다. @@ -20,15 +23,15 @@ async def execute_query( connection = Tortoise.get_connection("default") if isinstance(values, tuple): - values = tuple(v[0] if isinstance(v, tuple) else v for v in values) + processed_values = tuple(v[0] if isinstance(v, tuple) else v for v in values) # type: ignore else: - values = (values,) + processed_values = (values,) - result = await connection.execute_query_dict(query, values) # type: ignore + result = await connection.execute_query_dict(query, processed_values) # type: ignore if result and len(result) > 0: if fetch_type == "single": - return result[0] # type: ignore + return result[0] elif fetch_type == "multiple": return result return 0 if fetch_type == "single" else [] diff --git a/src/common/utils/query_formatter.py b/src/common/utils/query_formatter.py index 0471832..5ddf3a1 100644 --- a/src/common/utils/query_formatter.py +++ b/src/common/utils/query_formatter.py @@ -3,7 +3,7 @@ class QueryFormatter: @staticmethod - def format(query_template: str, values: Union[str, list, tuple]) -> str: + def format(query_template: str, values: Union[str, list[Union[str, int]], tuple[Union[str, int], ...]]) -> str: """ 쿼리 템플릿과 단일 값 또는 리스트/튜플 형태의 값을 받아 SQL 쿼리를 포맷팅하는 메서드 (%s 사용) diff --git a/src/main.py b/src/main.py index 424ef86..e17ccb1 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,6 @@ from fastapi import FastAPI from common.post_construct import post_construct -from core.configs import settings logging.basicConfig(level=logging.DEBUG) @@ -11,8 +10,6 @@ db_client_logger.setLevel(logging.DEBUG) app = FastAPI() -print(settings.APPLE_URL) -print(settings.APPLE_SHARED_SECRET) post_construct(app) From d381bfe4d0b80e77b1ff4d33aa060b8efccfce30 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 1 Dec 2024 18:02:06 +0900 Subject: [PATCH 34/60] =?UTF-8?q?=E2=9C=A8=20feat:=20apple=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/purchases/models/purchase_history.py | 170 +++++++++++++- .../v2/purchases/models/purchase_status.py | 21 ++ src/app/v2/purchases/router.py | 60 ++--- .../v2/purchases/services/purchase_service.py | 213 ++++++++---------- src/app/v2/users/models/user.py | 2 +- src/common/utils/query_executor.py | 2 +- src/core/database/database_settings.py | 2 +- src/main.py | 1 - 8 files changed, 294 insertions(+), 177 deletions(-) create mode 100644 src/app/v2/purchases/models/purchase_status.py diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py index 70287fa..68a1625 100644 --- a/src/app/v2/purchases/models/purchase_history.py +++ b/src/app/v2/purchases/models/purchase_history.py @@ -1,20 +1,172 @@ -# models.py +from datetime import datetime +from typing import Optional + from tortoise import fields from tortoise.fields import ForeignKeyRelation from tortoise.models import Model +from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus from app.v2.users.models.user import User +from common.utils.query_executor import QueryExecutor + + +class Subscription(Model): + subscription_id = fields.BigIntField(pk=True, description="Primary key for the Subscription") + product_code = fields.CharField(max_length=255, null=False, description="Product code of the subscription") + status = fields.CharField(max_length=255, null=False, description="Status of the subscription") + auto_renew_status = fields.BooleanField(default=True, description="Whether auto-renewal is active") + current_transaction_id = fields.CharField(max_length=255, null=False, description="Current transaction ID") + expires_date = fields.DatetimeField(null=False, description="Expiration date of the subscription") + created_at = fields.DatetimeField(auto_now_add=True, description="When the subscription was created") + updated_at = fields.DatetimeField(auto_now=True, description="Last updated timestamp") + + user: ForeignKeyRelation["User"] = fields.ForeignKeyField( + "models.User", + related_name="subscriptions", + on_delete=fields.CASCADE, + description="User linked to the subscription", + ) + purchase_histories = fields.ReverseRelation["PurchaseHistory"] + + class Meta: + table = "subscription" + + @classmethod + async def get_subscription_by_user_id_and_product_code( + cls, user_id: str, product_code: str + ) -> Optional["Subscription"]: + query = """ + SELECT * FROM subscription + WHERE user_id = UNHEX(REPLACE(%s, '-', '')) AND product_code = %s + LIMIT 1; + """ + values = (user_id, product_code) + + result = await QueryExecutor.execute_query(query, values=values, fetch_type="single") + + if result: + return cls(**result) + return None + + @classmethod + async def create_or_update_subscription( + cls, + user_id: str, + product_code: str, + transaction_id: str, + expires_date_ms: int, + status: SubscriptionStatus, + ) -> "Subscription": + query = """ + INSERT INTO subscription (user_id, product_code, status, current_transaction_id, expires_date, auto_renew_status) + VALUES (UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, FROM_UNIXTIME(%s / 1000), %s) + ON DUPLICATE KEY UPDATE + current_transaction_id = VALUES(current_transaction_id), + expires_date = VALUES(expires_date), + status = VALUES(status); + """ + auto_renew_status = True + values = (user_id, product_code, status, transaction_id, expires_date_ms, auto_renew_status) + + await QueryExecutor.execute_query(query, values=values, fetch_type="none") + + return cls( + user_id=user_id, + product_code=product_code, + status=status, + current_transaction_id=transaction_id, + expires_date=datetime.fromtimestamp(expires_date_ms / 1000), + ) + + @classmethod + async def update_subscription(cls, user_id: str, product_code: str, transaction_id: str, expires_date_ms: int): + expires_date = datetime.fromtimestamp(expires_date_ms / 1000.0) + + query = """ + UPDATE subscription + SET current_transaction_id = %s, + expires_date = FROM_UNIXTIME(%s / 1000), + status = %s + WHERE user_id = UNHEX(REPLACE(%s, '-', '')) + AND product_code = %s; + """ + values = (transaction_id, expires_date_ms, "active", user_id, product_code) + + await QueryExecutor.execute_query(query, values=values, fetch_type="single") -# models.py (계속) class PurchaseHistory(Model): - purchase_history_id = fields.BigIntField(pk=True) - product_code = fields.CharField(max_length=255) - status = fields.CharField(max_length=255) - receipt_id = fields.CharField(max_length=255, unique=True) # 영수증 중복 방지 - user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="purchase_histories") + purchase_history_id = fields.BigIntField(pk=True, description="Primary key for the Purchase History") + product_code = fields.CharField(max_length=255, null=False, description="Product code of the purchase") + transaction_id = fields.CharField(max_length=255, unique=True, null=False, description="Transaction ID") + original_transaction_id = fields.CharField(max_length=255, null=True, description="Original transaction ID") + status = fields.CharField(max_length=255, null=False, description="Purchase status") + expires_date = fields.DatetimeField(null=True, description="Expiration date of the purchase") + purchase_date = fields.DatetimeField(null=False, description="Date of the purchase") + quantity = fields.IntField(default=1, description="Quantity of items purchased") + is_refunded = fields.BooleanField(default=False, description="Whether the purchase was refunded") + refunded_at = fields.DatetimeField(null=True, description="When the purchase was refunded") + created_at = fields.DatetimeField(auto_now_add=True, description="When the purchase was made") + updated_at = fields.DatetimeField(auto_now=True, description="Last updated timestamp") + + user: ForeignKeyRelation["User"] = fields.ForeignKeyField( + "models.User", + related_name="purchase_histories", + on_delete=fields.CASCADE, + description="User linked to the purchase", + ) + + subscription: Optional[ForeignKeyRelation["Subscription"]] = fields.ForeignKeyField( + "models.Subscription", + related_name="purchase_histories", + null=True, + on_delete=fields.SET_NULL, + description="Linked subscription", + ) class Meta: table = "purchase_history" - unique_together = ("receipt_id",) - indexes = ["user_id"] + + @classmethod + async def create_purchase_history( + cls, + user_id: str, + subscription_id: Optional[int], + product_code: str, + transaction_id: str, + original_transaction_id: str, + status: PurchaseStatus, + expires_date_ms: Optional[int], + purchase_date_ms: int, + quantity: int = 1, + is_refunded: bool = False, + refunded_at: Optional[datetime] = None, + ) -> None: + query = """ + INSERT INTO purchase_history ( + user_id, subscription_id, product_code, transaction_id, + original_transaction_id, status, expires_date, purchase_date, + quantity, is_refunded, refunded_at, created_at, updated_at + ) + VALUES ( + UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, + %s, %s, FROM_UNIXTIME(%s / 1000), FROM_UNIXTIME(%s / 1000), + %s, %s, %s, NOW(), NOW() + ); + """ + + values = ( + user_id, + subscription_id, + product_code, + transaction_id, + original_transaction_id, + status, + expires_date_ms, + purchase_date_ms, + quantity, + is_refunded, + refunded_at, + ) + + await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/purchases/models/purchase_status.py b/src/app/v2/purchases/models/purchase_status.py new file mode 100644 index 0000000..f8789f7 --- /dev/null +++ b/src/app/v2/purchases/models/purchase_status.py @@ -0,0 +1,21 @@ +from enum import Enum + + +class PurchaseStatus(Enum): + AVAILABLE = "AVAILABLE" + CONSUMED = "CONSUMED" + EXPIRED = "EXPIRED" + REFUNDED = "REFUNDED" + CANCELED = "CANCELED" + + +class SubscriptionStatus(Enum): + ACTIVE = "ACTIVE" + EXPIRED = "EXPIRED" + CANCELED = "CANCELED" + + +purchase_mapping = { + "tellingme.plus.oneMonth": "PD_PLUS_MONTH_1_KR", + "tellingme.plus.oneYear": "PD_PLUS_YEAR_1_KR", +} diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 9cee12c..66937d0 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -1,53 +1,35 @@ from typing import Any -from fastapi import APIRouter, HTTPException, status -from tortoise.exceptions import DoesNotExist +from fastapi import APIRouter, Depends, status -from app.v2.purchases.dtos.requests import PurchaseRequest, ReceiptRequestDTO +from app.v2.purchases.dtos.requests import ReceiptRequestDTO from app.v2.purchases.services.purchase_service import PurchaseService -from app.v2.users.services.user_service import UserService router = APIRouter(prefix="/purchase", tags=["Purchase"]) -@router.post("/process-receipt") -async def process_receipt(receipt: ReceiptRequestDTO) -> dict[str, Any]: - if not receipt.receiptData or not receipt.user_id: - raise HTTPException(status_code=400, detail="Missing data") - purchase_service = PurchaseService() - data = await purchase_service.validate_receipt(receipt.receiptData, receipt.user_id) +@router.post( + "/apple", + status_code=status.HTTP_200_OK, + response_model=dict[str, Any], + summary="apple 결제 api", + description="apple 결제 api", +) +async def process_receipt( + receipt: ReceiptRequestDTO, + purchase_service: PurchaseService = Depends(), +) -> dict[str, Any]: + await purchase_service.process_apple_purchase(receipt_data=receipt.receiptData, user_id=receipt.user_id) return { "code": status.HTTP_200_OK, "message": "Receipt verified successfully", - "data": data, + "data": True, } -# @router.get("/receipt-test") -# async def receipt_test() -> dict[str, Any]: -# purchase_service = PurchaseService() -# return await purchase_service.receipt_test() - - -@router.post("") -async def process_purchase(request: PurchaseRequest) -> dict[str, str]: - try: - user_id = request.user_id - product_code = request.product_code - - item_inventory_products = await PurchaseService.validate_purchase(product_code) - - user = await UserService.get_user_info(user_id=user_id) - - await PurchaseService.process_purchase( - item_inventory_products=item_inventory_products, - user_id=user_id, - cheese_manager_id=user["cheese_manager_id"], - ) - - return {"message": "Purchase successful", "product": product_code} - - except DoesNotExist: - raise HTTPException(status_code=404, detail="Product not found.") - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) +@router.post("/receipt-test") +async def receipt_test( + receipt: ReceiptRequestDTO, + purchase_service: PurchaseService = Depends(), +) -> dict[str, Any]: + return await purchase_service._validate_apple_receipt(receipt_data=receipt.receiptData) diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index f77fade..ad33256 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,25 +1,62 @@ -import base64 -import re from typing import Any, cast import httpx from fastapi import HTTPException from tortoise.exceptions import DoesNotExist +from tortoise.transactions import atomic -from app.v2.cheese_managers.services.cheese_service import CheeseService from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory -from app.v2.purchases.models.purchase_history import PurchaseHistory +from app.v2.purchases.models.purchase_history import PurchaseHistory, Subscription +from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus, purchase_mapping from app.v2.users.services.user_service import UserService from core.configs import settings class PurchaseService: + @atomic() + async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: + response = await self._validate_apple_receipt(receipt_data=receipt_data) + + latest_receipt_info = self._extract_latest_receipt_info(response) + transaction_id = latest_receipt_info["transaction_id"] + original_transaction_id = latest_receipt_info["original_transaction_id"] + expires_date_ms = int(latest_receipt_info.get("expires_date_ms", 0)) + purchase_date_ms = int(latest_receipt_info.get("purchase_date_ms", 0)) + product_code = purchase_mapping.get(latest_receipt_info["product_id"], latest_receipt_info["product_id"]) + + await self._create_or_update_subscription( + user_id=user_id, + product_code=product_code, + transaction_id=transaction_id, + expires_date_ms=expires_date_ms, + ) + + subscription = await self._get_subscription(user_id, product_code) + + if subscription is None: + raise DoesNotExist("Subscription not found") + + await self._create_purchase_history( + user_id=user_id, + subscription=subscription, + product_code=product_code, + transaction_id=transaction_id, + original_transaction_id=original_transaction_id, + expires_date_ms=expires_date_ms, + purchase_date_ms=purchase_date_ms, + quantity=int(latest_receipt_info.get("quantity", 1)), + ) + + item_inventory_products = await self._validate_purchase(product_code=product_code) + + await self._process_purchase(user_id=user_id, item_inventory_products=item_inventory_products) + @staticmethod - async def process_krw_payment(product: ProductInventory, quantity: int) -> None: - print(f"Processing KRW payment: {product.price * quantity} KRW") - # 여기에 실제 KRW 결제 처리 로직 구현 + def _extract_latest_receipt_info(response: dict) -> dict: + return response.get("latest_receipt_info", [])[0] - async def validate_receipt(self, receipt_data: str, user_id: str) -> dict[str, Any]: + @staticmethod + async def _validate_apple_receipt(receipt_data: str) -> dict[str, Any]: url = settings.APPLE_URL @@ -30,68 +67,56 @@ async def validate_receipt(self, receipt_data: str, user_id: str) -> dict[str, A async with httpx.AsyncClient() as client: response = await client.post(url, json=payload) - print(response.json()) if response.status_code == 200: return cast(dict[str, Any], response.json()) - # return await self._handle_receipt_response(response.json(), user_id) else: raise HTTPException(status_code=500, detail="Failed to connect to Apple server") - # async def receipt_test(self) -> dict[str, Any]: - # file_path = "/Users/gimtaeu/workspace/tellingme-python-server/1개월 구독한 영수증 data.txt/TXT.rtf" - # base64_data = self.extract_base64_from_rtf(file_path) - # - # url = settings.APPLE_URL - # - # payload = { - # "exclude-old-transactions": True, - # "receipt-data": base64_data, - # "password": settings.APPLE_SHARED_SECRET, - # } - # - # async with httpx.AsyncClient() as client: - # response = await client.post(url, json=payload) - # response_data = response.json() - # print(response_data) - # if response.status_code != 200 or response_data.get("status") != 0: - # raise HTTPException(status_code=400, detail="Receipt verification failed") - # - # return response_data - - async def _handle_receipt_response(self, response_data: dict[str, Any], user_id: str) -> dict[str, Any]: - if response_data.get("status") == 0: - in_app_purchase = response_data.get("receipt", {}).get("in_app", []) - if in_app_purchase: - purchase_info = in_app_purchase[0] - return await self._save_purchase_history(user_id, purchase_info) - else: - raise HTTPException(status_code=400, detail="No in-app purchase found") - else: - raise HTTPException(status_code=400, detail="Invalid receipt") - - @classmethod - async def _save_purchase_history(cls, user_id: str, purchase_info: dict[str, Any]) -> dict[str, Any]: - receipt_id = purchase_info.get("transaction_id") - product_code = purchase_info.get("product_id") - status = "completed" + @staticmethod + async def _create_or_update_subscription( + user_id: str, product_code: str, transaction_id: str, expires_date_ms: int + ) -> None: + await Subscription.create_or_update_subscription( + user_id=user_id, + product_code=product_code, + transaction_id=transaction_id, + expires_date_ms=expires_date_ms, + status=SubscriptionStatus.ACTIVE, + ) - existing_purchase = await PurchaseHistory.filter(receipt_id=receipt_id).exists() - if existing_purchase: - raise HTTPException(status_code=400, detail="Duplicate receipt") + @staticmethod + async def _get_subscription(user_id: str, product_code: str) -> Subscription | None: + return await Subscription.get_subscription_by_user_id_and_product_code( + user_id=user_id, product_code=product_code + ) - purchase_history = await PurchaseHistory.create( - product_code=product_code, - status=status, - receipt_id=receipt_id, + @staticmethod + async def _create_purchase_history( + user_id: str, + subscription: Subscription, + product_code: str, + transaction_id: str, + original_transaction_id: str, + expires_date_ms: int, + purchase_date_ms: int, + quantity: int, + ) -> None: + await PurchaseHistory.create_purchase_history( user_id=user_id, + subscription_id=subscription.subscription_id if subscription else None, + product_code=product_code, + transaction_id=transaction_id, + original_transaction_id=original_transaction_id, + status=PurchaseStatus.AVAILABLE, + expires_date_ms=expires_date_ms, + purchase_date_ms=purchase_date_ms, + quantity=quantity, + is_refunded=False, + refunded_at=None, ) - return { - "message": "Purchase history saved", - "purchase_history_id": purchase_history.purchase_history_id, - } @staticmethod - async def validate_purchase( + async def _validate_purchase( product_code: str, ) -> list[ItemInventoryProductInventory]: try: @@ -111,11 +136,11 @@ async def validate_purchase( raise HTTPException(status_code=404, detail="Product not found.") @classmethod - async def process_purchase( + async def _process_purchase( cls, item_inventory_products: list[ItemInventoryProductInventory], user_id: str, - cheese_manager_id: int, + # cheese_manager_id: int, ) -> None: for item_inventory_product in item_inventory_products: item: ItemInventory = await item_inventory_product.item_inventory @@ -123,69 +148,7 @@ async def process_purchase( if item.item_category == "SUBSCRIPTION": await UserService.set_is_premium(user_id=user_id, is_premium=True) - elif item.item_category == "CHEESE": - await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) + # elif item.item_category == "CHEESE": + # await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) else: raise ValueError(f"Invalid item category for purchase: {item.item_category}") - - # purchase_info = { - # "transaction_id": "1000000654000000", # 고유한 거래 ID (영수증 ID로 사용 가능) - # "product_id": "com.example.app.product1", # 구매한 상품의 ID - # "purchase_date": "2024-11-01T10:30:00Z", # 구매한 날짜와 시간 - # "original_transaction_id": "1000000654000000", # 원래 거래 ID (구독 갱신 시 동일) - # "quantity": "1", # 구매 수량 - # "expires_date": "2024-12-01T10:30:00Z", # 구독 만료 날짜 (구독형 상품인 경우) - # "is_trial_period": "false", # 무료 체험 기간 여부 - # "is_in_intro_offer_period": "false" # 소개 할인 기간 여부 - # } - - # 소모성 - # purchase_info = { - # "transaction_id": "1000000654000000", - # "product_id": "com.example.app.product1", - # "purchase_date": "2024-11-01T10:30:00Z", - # } - - # 구독 - # purchase_info = { - # "transaction_id": "1000000654000000", - # "product_id": "com.example.app.product1", - # "purchase_date": "2024-11-01T10:30:00Z", - # "expires_date": "2024-12-01T10:30:00Z", - # "original_transaction_id": "1000000654000000" - # } - - import base64 - import re - - # @staticmethod - # def extract_base64_from_rtf(file_path: str) -> str: - # # 파일 읽기 - # try: - # with open(file_path, "r", encoding="utf-8") as file: - # content = file.read() - # except FileNotFoundError: - # raise ValueError(f"File not found: {file_path}") - # except Exception as e: - # raise ValueError(f"Failed to read the file: {str(e)}") - # - # # Base64 데이터 추출 (MII로 시작하는 패턴만) - # base64_pattern = r"MII[A-Za-z0-9+/=]+" - # matches = re.findall(base64_pattern, content) - # - # if not matches: - # raise ValueError("No valid Base64 data found in the file.") - # - # # 가장 긴 Base64 데이터 선택 - # base64_data = max(matches, key=len) - # - # # Base64 데이터 유효성 검증 - # try: - # base64.b64decode(base64_data, validate=True) - # except Exception: - # raise ValueError("Extracted data is not valid Base64.") - # - # # 디버깅 로그 출력 - # print(f"Extracted Base64 data length: {len(base64_data)}") - # - # return base64_data diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index 219e63f..f53e6e3 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -18,7 +18,7 @@ class User(Model): - user_id = fields.BinaryField(pk=True) # BINARY(16)로 저장 + user_id = fields.CharField(max_length=255, pk=True, description="Primary key for the User") allow_notification = fields.BooleanField(null=True) birth_date = fields.CharField(max_length=8, null=True) created_time = fields.DatetimeField(auto_now_add=True) diff --git a/src/common/utils/query_executor.py b/src/common/utils/query_executor.py index a30494c..b922713 100644 --- a/src/common/utils/query_executor.py +++ b/src/common/utils/query_executor.py @@ -9,7 +9,7 @@ class QueryExecutor: @staticmethod async def execute_query( query: str, - values: Union[tuple[Union[str, int, float, datetime], ...], str] = (), + values: Any = (), fetch_type: str = "multiple", ) -> Any: # type ignore """ diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index 430f9c9..dbd8101 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -12,7 +12,6 @@ "app.v2.badges.models.badge", "app.v2.colors.models.color", "app.v2.answers.models.answer", - # "app.v2.payments.models.cheese_manager", "app.v2.teller_cards.models.teller_card", "app.v2.levels.models.level", "app.v2.cheese_managers.models.cheese_manager", @@ -20,6 +19,7 @@ "app.v2.missions.models.mission", "app.v2.likes.models.like", "app.v2.emotions.models.emotion", + "app.v2.purchases.models.purchase_history", ] TORTOISE_ORM = { diff --git a/src/main.py b/src/main.py index e17ccb1..7c23be5 100644 --- a/src/main.py +++ b/src/main.py @@ -10,7 +10,6 @@ db_client_logger.setLevel(logging.DEBUG) app = FastAPI() - post_construct(app) From 0f2421686626f7572246d57b666e8a3e9636b90d Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 1 Dec 2024 18:47:04 +0900 Subject: [PATCH 35/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EA=B0=80#=20=F0=9F=90=9B=20fix:=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95,=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/purchases/models/purchase_history.py | 8 +++++--- .../v2/purchases/services/purchase_service.py | 18 +++++++++++++----- src/common/utils/query_executor.py | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py index 68a1625..b37b4d9 100644 --- a/src/app/v2/purchases/models/purchase_history.py +++ b/src/app/v2/purchases/models/purchase_history.py @@ -55,7 +55,7 @@ async def create_or_update_subscription( product_code: str, transaction_id: str, expires_date_ms: int, - status: SubscriptionStatus, + status: str, ) -> "Subscription": query = """ INSERT INTO subscription (user_id, product_code, status, current_transaction_id, expires_date, auto_renew_status) @@ -79,7 +79,9 @@ async def create_or_update_subscription( ) @classmethod - async def update_subscription(cls, user_id: str, product_code: str, transaction_id: str, expires_date_ms: int): + async def update_subscription( + cls, user_id: str, product_code: str, transaction_id: str, expires_date_ms: int + ) -> None: expires_date = datetime.fromtimestamp(expires_date_ms / 1000.0) query = """ @@ -135,7 +137,7 @@ async def create_purchase_history( product_code: str, transaction_id: str, original_transaction_id: str, - status: PurchaseStatus, + status: str, expires_date_ms: Optional[int], purchase_date_ms: int, quantity: int = 1, diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index ad33256..bd8bee0 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,4 +1,4 @@ -from typing import Any, cast +from typing import Any, Optional, cast import httpx from fastapi import HTTPException @@ -18,6 +18,10 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: response = await self._validate_apple_receipt(receipt_data=receipt_data) latest_receipt_info = self._extract_latest_receipt_info(response) + + if latest_receipt_info is None: + raise ValueError("No valid receipt information found.") + transaction_id = latest_receipt_info["transaction_id"] original_transaction_id = latest_receipt_info["original_transaction_id"] expires_date_ms = int(latest_receipt_info.get("expires_date_ms", 0)) @@ -52,8 +56,12 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: await self._process_purchase(user_id=user_id, item_inventory_products=item_inventory_products) @staticmethod - def _extract_latest_receipt_info(response: dict) -> dict: - return response.get("latest_receipt_info", [])[0] + def _extract_latest_receipt_info(response: dict[str, Any]) -> dict[str, Any] | None: + latest_receipt_info = response.get("latest_receipt_info") + + if isinstance(latest_receipt_info, list) and latest_receipt_info: + return latest_receipt_info[0] or {} + return None @staticmethod async def _validate_apple_receipt(receipt_data: str) -> dict[str, Any]: @@ -81,7 +89,7 @@ async def _create_or_update_subscription( product_code=product_code, transaction_id=transaction_id, expires_date_ms=expires_date_ms, - status=SubscriptionStatus.ACTIVE, + status=SubscriptionStatus.ACTIVE.value, ) @staticmethod @@ -107,7 +115,7 @@ async def _create_purchase_history( product_code=product_code, transaction_id=transaction_id, original_transaction_id=original_transaction_id, - status=PurchaseStatus.AVAILABLE, + status=PurchaseStatus.AVAILABLE.value, expires_date_ms=expires_date_ms, purchase_date_ms=purchase_date_ms, quantity=quantity, diff --git a/src/common/utils/query_executor.py b/src/common/utils/query_executor.py index b922713..cb10261 100644 --- a/src/common/utils/query_executor.py +++ b/src/common/utils/query_executor.py @@ -23,7 +23,7 @@ async def execute_query( connection = Tortoise.get_connection("default") if isinstance(values, tuple): - processed_values = tuple(v[0] if isinstance(v, tuple) else v for v in values) # type: ignore + processed_values = tuple(v[0] if isinstance(v, tuple) else v for v in values) else: processed_values = (values,) From f00d8345ea8d3a490aa22a377ee294de4dbe76c4 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 1 Dec 2024 23:15:11 +0900 Subject: [PATCH 36/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=98=81=EC=88=98?= =?UTF-8?q?=EC=A6=9D=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/models/purchase_history.py | 7 +++++-- src/app/v2/purchases/services/purchase_service.py | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py index b37b4d9..880b84d 100644 --- a/src/app/v2/purchases/models/purchase_history.py +++ b/src/app/v2/purchases/models/purchase_history.py @@ -108,6 +108,7 @@ class PurchaseHistory(Model): quantity = fields.IntField(default=1, description="Quantity of items purchased") is_refunded = fields.BooleanField(default=False, description="Whether the purchase was refunded") refunded_at = fields.DatetimeField(null=True, description="When the purchase was refunded") + receipt_data = fields.TextField(null=True, description="Raw receipt data from Apple") created_at = fields.DatetimeField(auto_now_add=True, description="When the purchase was made") updated_at = fields.DatetimeField(auto_now=True, description="Last updated timestamp") @@ -140,6 +141,7 @@ async def create_purchase_history( status: str, expires_date_ms: Optional[int], purchase_date_ms: int, + receipt_data: str, quantity: int = 1, is_refunded: bool = False, refunded_at: Optional[datetime] = None, @@ -148,12 +150,12 @@ async def create_purchase_history( INSERT INTO purchase_history ( user_id, subscription_id, product_code, transaction_id, original_transaction_id, status, expires_date, purchase_date, - quantity, is_refunded, refunded_at, created_at, updated_at + quantity, is_refunded, refunded_at, receipt_data, created_at, updated_at ) VALUES ( UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, %s, %s, FROM_UNIXTIME(%s / 1000), FROM_UNIXTIME(%s / 1000), - %s, %s, %s, NOW(), NOW() + %s, %s, %s, %s, NOW(), NOW() ); """ @@ -169,6 +171,7 @@ async def create_purchase_history( quantity, is_refunded, refunded_at, + receipt_data, ) await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index bd8bee0..b7730fc 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -49,6 +49,7 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: expires_date_ms=expires_date_ms, purchase_date_ms=purchase_date_ms, quantity=int(latest_receipt_info.get("quantity", 1)), + receipt_data=receipt_data, ) item_inventory_products = await self._validate_purchase(product_code=product_code) @@ -108,6 +109,7 @@ async def _create_purchase_history( expires_date_ms: int, purchase_date_ms: int, quantity: int, + receipt_data: str, ) -> None: await PurchaseHistory.create_purchase_history( user_id=user_id, @@ -121,6 +123,7 @@ async def _create_purchase_history( quantity=quantity, is_refunded=False, refunded_at=None, + receipt_data=receipt_data, ) @staticmethod From 633993aa800571f574ecaece014453e3b1d08df7 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Mon, 2 Dec 2024 11:14:51 +0900 Subject: [PATCH 37/60] =?UTF-8?q?=E2=9C=A8=20feat:=20celery=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 392 ++++++++++++++++++++++++++++++++++- pyproject.toml | 3 + src/celery_models.py | 13 ++ src/celery_settings.py | 86 ++++++++ src/common/post_construct.py | 2 + src/core/tasks/__init__.py | 0 src/core/tasks/scheduler.py | 31 +++ 7 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 src/celery_models.py create mode 100644 src/celery_settings.py create mode 100644 src/core/tasks/__init__.py create mode 100644 src/core/tasks/scheduler.py diff --git a/poetry.lock b/poetry.lock index 456513c..99bad8c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -36,6 +36,20 @@ typing_extensions = ">=4.0" dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] +[[package]] +name = "amqp" +version = "5.3.1" +description = "Low-level AMQP client for Python (fork of amqplib)." +optional = false +python-versions = ">=3.6" +files = [ + {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, + {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, +] + +[package.dependencies] +vine = ">=5.0.0,<6.0.0" + [[package]] name = "annotated-types" version = "0.7.0" @@ -67,6 +81,44 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "apscheduler" +version = "3.11.0" +description = "In-process task scheduler with Cron-like capabilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"}, + {file = "apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133"}, +] + +[package.dependencies] +tzlocal = ">=3.0" + +[package.extras] +doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"] +etcd = ["etcd3", "protobuf (<=3.21.0)"] +gevent = ["gevent"] +mongodb = ["pymongo (>=3.0)"] +redis = ["redis (>=3.0)"] +rethinkdb = ["rethinkdb (>=2.4.0)"] +sqlalchemy = ["sqlalchemy (>=1.4)"] +test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6", "anyio (>=4.5.2)", "gevent", "pytest", "pytz", "twisted"] +tornado = ["tornado (>=4.3)"] +twisted = ["twisted"] +zookeeper = ["kazoo"] + +[[package]] +name = "billiard" +version = "4.2.1" +description = "Python multiprocessing fork with improvements and bugfixes" +optional = false +python-versions = ">=3.7" +files = [ + {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, + {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, +] + [[package]] name = "black" version = "24.8.0" @@ -111,6 +163,63 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "celery" +version = "5.4.0" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.8" +files = [ + {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, + {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, +] + +[package.dependencies] +billiard = ">=4.2.0,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.3.4,<6.0" +python-dateutil = ">=2.8.2" +sqlalchemy = {version = ">=1.4.48,<2.1", optional = true, markers = "extra == \"sqlalchemy\""} +tzdata = ">=2022.7" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==42.0.5)"] +azureblockblob = ["azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb (==1.14.2)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] +eventlet = ["eventlet (>=0.32.0)"] +gcs = ["google-cloud-storage (>=2.10.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] +mongodb = ["pymongo[srv] (>=4.0.2)"] +msgpack = ["msgpack (==1.0.8)"] +pymemcache = ["python-memcached (>=1.61)"] +pyro = ["pyro4 (==4.82)"] +pytest = ["pytest-celery[all] (>=1.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem (==4.1.5)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.22.0)"] + [[package]] name = "certifi" version = "2024.8.30" @@ -215,6 +324,55 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-didyoumean" +version = "0.3.1" +description = "Enables git-like *did-you-mean* feature in click" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, +] + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +optional = false +python-versions = "*" +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +optional = false +python-versions = ">=3.6" +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] + [[package]] name = "colorama" version = "0.4.6" @@ -707,6 +865,39 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "kombu" +version = "5.4.2" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, + {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +tzdata = {version = "*", markers = "python_version >= \"3.9\""} +vine = "5.1.0" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2 (==0.1.5)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=4.1.1)"] +msgpack = ["msgpack (==1.1.0)"] +pyro = ["pyro4 (==4.82)"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -921,6 +1112,20 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "pycparser" version = "2.22" @@ -1154,6 +1359,20 @@ pytest = ">=8.2,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1252,6 +1471,21 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "redis" +version = "5.2.0" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.8" +files = [ + {file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"}, + {file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"}, +] + +[package.extras] +hiredis = ["hiredis (>=3.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] + [[package]] name = "rich" version = "13.8.1" @@ -1281,6 +1515,17 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1292,6 +1537,101 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.36" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, + {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, + {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "starlette" version = "0.38.6" @@ -1364,6 +1704,34 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "uvicorn" version = "0.30.6" @@ -1433,6 +1801,17 @@ files = [ docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +[[package]] +name = "vine" +version = "5.1.0" +description = "Python promises." +optional = false +python-versions = ">=3.6" +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + [[package]] name = "watchfiles" version = "0.24.0" @@ -1528,6 +1907,17 @@ files = [ [package.dependencies] anyio = ">=3.0.0" +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "websockets" version = "13.1" @@ -1626,4 +2016,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "950fb3bce727b2cbb3b28751c57055bd378128b68f5797d380396e34c0ba1913" +content-hash = "8de91580f01ea0c8d7d83151891614831cc5cf42e2b3521a6d1f9a37b14a26a5" diff --git a/pyproject.toml b/pyproject.toml index f7a96de..4fffb84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,9 @@ python-dotenv = "^1.0.1" cryptography = "^43.0.1" httpx = "^0.27.2" tortoise-orm = {version = "^0.21.6", extras = ["aiomysql"]} +celery = {extras = ["sqlalchemy"], version = "^5.4.0"} +apscheduler = "^3.11.0" +redis = "^5.2.0" [tool.poetry.group.dev.dependencies] diff --git a/src/celery_models.py b/src/celery_models.py new file mode 100644 index 0000000..f3571d5 --- /dev/null +++ b/src/celery_models.py @@ -0,0 +1,13 @@ +from tortoise import fields +from tortoise.models import Model + + +class Question(Model): + date = fields.DateField(pk=True) # 기본 키로 설정된 날짜 필드 + phrase = fields.CharField(max_length=255) + title = fields.CharField(max_length=255) + spare_phrase = fields.CharField(max_length=255) + spare_title = fields.CharField(max_length=255) + + class Meta: + table = "question" # 데이터베이스에서 매핑할 테이블 이름 diff --git a/src/celery_settings.py b/src/celery_settings.py new file mode 100644 index 0000000..4c352f6 --- /dev/null +++ b/src/celery_settings.py @@ -0,0 +1,86 @@ +import asyncio +import logging + +from celery import Celery +from tortoise import Tortoise, Model, fields + +from celery_models import Question +from core.configs import settings + + +TORTOISE_ORM = { + "connections": { + "default": { + "engine": "tortoise.backends.mysql", + "credentials": { + "host": settings.DB_HOST, + "port": settings.DB_PORT, + "user": settings.DB_USER, + "password": settings.DB_PASSWORD, + "database": settings.DB_NAME, + "connect_timeout": 5, + "charset": settings.DB_CHARSET, + "minsize": 5, # 커넥션 풀 최소 크기 + "maxsize": 10, # 커넥션 풀 최대 크기 + }, + }, + }, + "apps": { + "models": { + "models": ["app.celery_models"], + "default_connection": "default", + }, + }, + "use_tz": False, # 비동기 ORM에서 타임존 설정을 고려하지 않는다면 False + "timezone": settings.DB_TIMEZONE, # 타임존 설정 +} + +celery_app = Celery( + "telling-me-celery", + broker="redis://localhost:6379/0", # Redis를 브로커로 사용 + backend="redis://localhost:6379/0", # Redis를 결과 백엔드로 사용 +) + +celery_app.conf.update( + task_serializer="json", + result_serializer="json", + accept_content=["json"], + timezone="Asia/Seoul", + enable_utc=True, + result_expires=3600, # 작업 결과 만료 시간 (초 단위) +) + + +@celery_app.task(name="daily_task") +def daily_task() -> None: + asyncio.run(execute_async_daily_task()) + + +async def execute_async_daily_task() -> None: + await initialize_celery() + try: + print("Executing daily task!") + await Question.create( + date="2023-07-29", + title="Title for 2024-12-10", + phrase="Phrase", + spare_phrase="s", + spare_title="s", + ) + + finally: + await close_celery_connections() + + +async def initialize_celery(): + logger = logging.getLogger(__name__) + logger.info(f"Current path: 여기") + await Tortoise.init(config=TORTOISE_ORM) + + +async def close_celery_connections(): + await Tortoise.close_connections() + + +if __name__ == "__main__": + celery_app.start() diff --git a/src/common/post_construct.py b/src/common/post_construct.py index b4ebb17..a894aa2 100644 --- a/src/common/post_construct.py +++ b/src/common/post_construct.py @@ -1,5 +1,6 @@ from fastapi import FastAPI +from core.tasks.scheduler import start_scheduler from common.handlers.exception_handler import attach_exception_handlers from common.handlers.router_handler import attach_router_handlers from core.database.database_settings import database_initialize @@ -9,3 +10,4 @@ def post_construct(app: FastAPI) -> None: attach_router_handlers(app) attach_exception_handlers(app) database_initialize(app) + start_scheduler() diff --git a/src/core/tasks/__init__.py b/src/core/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/tasks/scheduler.py b/src/core/tasks/scheduler.py new file mode 100644 index 0000000..8768589 --- /dev/null +++ b/src/core/tasks/scheduler.py @@ -0,0 +1,31 @@ +from apscheduler.jobstores.redis import RedisJobStore +from apscheduler.schedulers.background import BackgroundScheduler + + +import logging + +from celery_settings import celery_app + +logger = logging.getLogger(__name__) + + +def execute_daily_task(): + celery_app.send_task("daily_task") + + +def start_scheduler(): + scheduler = BackgroundScheduler( + jobstores={"default": RedisJobStore(host="localhost", port=6379, db=0)}, + timezone="Asia/Seoul", + ) + scheduler.start() + logger.info("Scheduler started") + + scheduler.add_job( + func=execute_daily_task, + trigger="cron", + hour=11, + minute=9, + id="daily_task", + replace_existing=True, + ) From 75bf863977cba7657b6ac4fa3de72b393339a356 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Tue, 3 Dec 2024 10:35:48 +0900 Subject: [PATCH 38/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20mypage=20is=20premi?= =?UTF-8?q?um=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/mobiles/router.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 2d0f3f3..15093b9 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -71,6 +71,7 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: ) cheese_amount = await CheeseService.get_cheese_balance(cheese_manager_id=user["cheese_manager_id"]) + is_premium = user["is_premium"] != b"\x00" user_profile_data = UserProfileWithLevel.builder( userProfile=UserProfileDTO.builder( @@ -79,7 +80,7 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: badgeCode=teller_card.badgeCode, badgeCount=badge_count, answerCount=answer_count, - premium=bool(user["is_premium"]), + premium=is_premium, ), level=level, ) From 83e7390c65209c07d5cc7c11f76c225a7a482b76 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Tue, 3 Dec 2024 11:35:51 +0900 Subject: [PATCH 39/60] =?UTF-8?q?=20=F0=9F=90=9B=20fix:=20=ED=94=84?= =?UTF-8?q?=EB=A6=AC=EB=AF=B8=EC=97=84=20=EC=9C=A0=EC=A0=80=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=2012=EC=A2=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cheese_managers/models/cheese_manager.py | 2 -- .../v2/emotions/services/emotion_service.py | 19 +++++++++++-- src/app/v2/mobiles/router.py | 7 ++--- src/app/v2/users/dtos/user_dto.py | 28 +++++++++++++++++++ src/app/v2/users/services/user_service.py | 5 ++-- 5 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 src/app/v2/users/dtos/user_dto.py diff --git a/src/app/v2/cheese_managers/models/cheese_manager.py b/src/app/v2/cheese_managers/models/cheese_manager.py index 124078a..ea7dbfd 100644 --- a/src/app/v2/cheese_managers/models/cheese_manager.py +++ b/src/app/v2/cheese_managers/models/cheese_manager.py @@ -5,9 +5,7 @@ from tortoise.fields import ForeignKeyRelation from tortoise.functions import Sum from tortoise.models import Model -from tortoise.transactions import atomic -from app.v2.cheese_managers.dtos.cheese_dto import CheeseAmountResult from app.v2.cheese_managers.models.cheese_status import CheeseStatus diff --git a/src/app/v2/emotions/services/emotion_service.py b/src/app/v2/emotions/services/emotion_service.py index cffbb7d..bdcc9a6 100644 --- a/src/app/v2/emotions/services/emotion_service.py +++ b/src/app/v2/emotions/services/emotion_service.py @@ -2,6 +2,7 @@ from app.v2.emotions.dtos.response import EmotionDTO, EmotionListResponseDTO from app.v2.emotions.models.emotion import Emotion, EmotionInventory +from app.v2.users.services.user_service import UserService emotion_mapping = { "EM_HAPPY": 1, @@ -34,5 +35,19 @@ async def get_emotion_inventory(cls) -> list[dict[str, str]]: @classmethod async def mapping_emotion_list(cls, user_id: str) -> EmotionDTO: - emotions = await cls.get_emotions(user_id=user_id) - return EmotionDTO.build(emotion_list=[emotion_mapping[emotion["emotion_code"]] for emotion in emotions]) + user = await UserService.get_user_profile(user_id=user_id) + + if user.is_premium: + emotions = await cls.get_emotion_inventory() + else: + emotions = await cls.get_emotions(user_id=user_id) + + return EmotionDTO.build(emotion_list=await cls.get_mapped_emotions(emotions)) + + @classmethod + async def get_mapped_emotions(cls, emotions: list[dict[str, str]]) -> list[int]: + return [ + value + for value in (emotion_mapping.get(emotion["emotion_code"]) for emotion in emotions) + if value is not None + ] diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 15093b9..b18f31c 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -70,17 +70,16 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: LevelService.get_level_info_add_answer_days(user_id), ) - cheese_amount = await CheeseService.get_cheese_balance(cheese_manager_id=user["cheese_manager_id"]) - is_premium = user["is_premium"] != b"\x00" + cheese_amount = await CheeseService.get_cheese_balance(cheese_manager_id=user.cheese_manager_id) user_profile_data = UserProfileWithLevel.builder( userProfile=UserProfileDTO.builder( - nickname=user["nickname"], + nickname=user.nickname, cheeseBalance=cheese_amount, badgeCode=teller_card.badgeCode, badgeCount=badge_count, answerCount=answer_count, - premium=is_premium, + premium=user.is_premium, ), level=level, ) diff --git a/src/app/v2/users/dtos/user_dto.py b/src/app/v2/users/dtos/user_dto.py new file mode 100644 index 0000000..cc3ac20 --- /dev/null +++ b/src/app/v2/users/dtos/user_dto.py @@ -0,0 +1,28 @@ +from typing import Any, Optional + +from pydantic import BaseModel + + +class UserDTO(BaseModel): + user_id: Optional[str] = None + nickname: Optional[str] = None + profile_url: Optional[str] = None + is_premium: Optional[bool] = None + user_status: Optional[bool] = None + cheese_manager_id: Optional[int] = None + teller_card_id: Optional[int] = None + level_id: Optional[int] = None + + @classmethod + def build(cls, user: dict[str, Any]) -> "UserDTO": + is_premium = user.get("is_premium") != b"\x00" + return cls( + user_id=user.get("user_id", None), + nickname=user.get("nickname", None), + profile_url=user.get("profile_url", None), + is_premium=is_premium, + user_status=user.get("user_status", None), + cheese_manager_id=user.get("cheese_manager_id", None), + teller_card_id=user.get("teller_card_id", None), + level_id=user.get("level_id", None), + ) diff --git a/src/app/v2/users/services/user_service.py b/src/app/v2/users/services/user_service.py index 74d751f..95c3fb9 100644 --- a/src/app/v2/users/services/user_service.py +++ b/src/app/v2/users/services/user_service.py @@ -1,6 +1,7 @@ from typing import Any from app.v2.cheese_managers.models.cheese_manager import CheeseManager +from app.v2.users.dtos.user_dto import UserDTO from app.v2.users.models.user import User @@ -10,8 +11,8 @@ async def get_user_info(user_id: str) -> Any: return await User.get_user_info_by_user_id(user_id=user_id) @classmethod - async def get_user_profile(cls, user_id: str) -> Any: - return await User.get_user_profile_by_user_id(user_id=user_id) + async def get_user_profile(cls, user_id: str) -> UserDTO: + return UserDTO.build(await User.get_user_profile_by_user_id(user_id=user_id)) @staticmethod async def set_is_premium(user_id: str, is_premium: bool) -> None: From 6c5e5695d089bd6a93324160be412a3494497191 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Tue, 3 Dec 2024 11:56:48 +0900 Subject: [PATCH 40/60] =?UTF-8?q?=F0=9F=90=9Bfix:=20=ED=85=94=EB=9F=AC?= =?UTF-8?q?=EC=B9=B4=ED=8A=B8=20=EC=88=98=EC=A0=95=20api=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=EC=88=98=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/teller_cards/dtos/request.py | 6 ++++-- src/app/v2/teller_cards/models/teller_card.py | 6 ++++-- src/app/v2/teller_cards/querys/teller_card_query.py | 7 +++++-- src/app/v2/teller_cards/router.py | 1 + .../v2/teller_cards/services/teller_card_service.py | 11 ++++++++--- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/app/v2/teller_cards/dtos/request.py b/src/app/v2/teller_cards/dtos/request.py index 479ec16..fecc72e 100644 --- a/src/app/v2/teller_cards/dtos/request.py +++ b/src/app/v2/teller_cards/dtos/request.py @@ -1,7 +1,9 @@ +from typing import Optional + from pydantic import BaseModel class TellerCardRequestDTO(BaseModel): user_id: str - colorCode: str - badgeCode: str + colorCode: Optional[str] = None + badgeCode: Optional[str] = None diff --git a/src/app/v2/teller_cards/models/teller_card.py b/src/app/v2/teller_cards/models/teller_card.py index 3eace96..7314021 100644 --- a/src/app/v2/teller_cards/models/teller_card.py +++ b/src/app/v2/teller_cards/models/teller_card.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from tortoise import fields from tortoise.models import Model @@ -25,7 +25,9 @@ async def get_teller_card_info_by_user_id(cls, user_id: str) -> Any: # type ign return await QueryExecutor.execute_query(query, values=value, fetch_type="single") # type ignore @classmethod - async def patch_teller_card_info_by_user_id(cls, user_id: str, badge_code: str, color_code: str) -> None: + async def patch_teller_card_info_by_user_id( + cls, user_id: str, badge_code: Optional[str] = None, color_code: Optional[str] = None + ) -> None: query = PATCH_TELLER_CARD_QUERY values = (badge_code, color_code, user_id) await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/teller_cards/querys/teller_card_query.py b/src/app/v2/teller_cards/querys/teller_card_query.py index 7d8ed85..7e64673 100644 --- a/src/app/v2/teller_cards/querys/teller_card_query.py +++ b/src/app/v2/teller_cards/querys/teller_card_query.py @@ -17,14 +17,17 @@ PATCH_TELLER_CARD_QUERY = """ UPDATE teller_card - SET activate_badge_code = %s, activate_color_code = %s + SET + activate_badge_code = COALESCE(%s, activate_badge_code), + activate_color_code = COALESCE(%s, activate_color_code) WHERE teller_card_id = ( SELECT u.teller_card_id FROM user u WHERE u.user_id = UNHEX(REPLACE(%s, '-', '')) - ) +) """ + GET_UPDATED_TELLER_CARD_QUERY = """ SELECT activate_badge_code AS badgeCode, activate_color_code AS colorCode FROM teller_card diff --git a/src/app/v2/teller_cards/router.py b/src/app/v2/teller_cards/router.py index 00e5db9..a66c981 100644 --- a/src/app/v2/teller_cards/router.py +++ b/src/app/v2/teller_cards/router.py @@ -18,6 +18,7 @@ async def patch_teller_card_handler( user_id = body.user_id badge_code = body.badgeCode color_code = body.colorCode + await TellerCardService.validate_teller_card(badge_code=badge_code, color_code=color_code) await TellerCardService.patch_teller_card(user_id=user_id, badge_code=badge_code, color_code=color_code) diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py index 10d9f17..39e341a 100644 --- a/src/app/v2/teller_cards/services/teller_card_service.py +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from app.v2.badges.models.badge import BadgeInventory from app.v2.colors.models.color import ColorInventory from app.v2.teller_cards.dtos.teller_card_dto import TellerCardDTO @@ -11,7 +13,9 @@ async def get_teller_card(cls, user_id: str) -> TellerCardDTO: return TellerCardDTO.builder(teller_cards_raw) @classmethod - async def patch_teller_card(cls, user_id: str, badge_code: str, color_code: str) -> None: + async def patch_teller_card( + cls, user_id: str, badge_code: Optional[str] = None, color_code: Optional[str] = None + ) -> None: await TellerCard.patch_teller_card_info_by_user_id( user_id=user_id, badge_code=badge_code, color_code=color_code ) @@ -23,7 +27,8 @@ async def validate_teller_card(cls, badge_code: str, color_code: str) -> None: badge_codes = [badge["badge_code"] for badge in badge_code_list] color_codes = [color["color_code"] for color in color_code_list] - if badge_code not in badge_codes: + if badge_code and badge_code not in badge_codes: raise ValueError("Invalid badge code") - if color_code not in color_codes: + + if color_code and color_code not in color_codes: raise ValueError("Invalid color code") From 1db630d7d1067f924b75520933b80bb43d713797 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Thu, 5 Dec 2024 01:14:21 +0900 Subject: [PATCH 41/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=83=90=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20with=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EA=B5=AC=EB=8F=85=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EB=B0=8F=20=EB=AF=B8=EC=85=98=20=EB=A6=AC=EC=85=8B?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 17 +++- pyproject.toml | 4 + src/app/v2/missions/models/mission.py | 3 + src/app/v2/mobiles/router.py | 6 +- src/app/v2/purchases/dtos/purchase_dto.py | 32 ++++++ .../v2/purchases/models/purchase_history.py | 8 +- src/app/v2/purchases/router.py | 7 ++ .../v2/purchases/services/purchase_service.py | 98 +++++++++++++++---- .../services/teller_card_service.py | 2 +- src/app/v2/users/models/user_mission.py | 16 --- src/celery_models.py | 13 --- src/celery_settings.py | 86 ---------------- src/celery_worker.py | 3 + src/common/post_construct.py | 2 +- src/{core => common}/tasks/__init__.py | 0 src/common/tasks/mission_task.py | 7 ++ src/common/tasks/renew_subscription_task.py | 6 ++ src/{core/tasks => common/utils}/scheduler.py | 15 ++- src/core/configs/celery_settings.py | 56 +++++++++++ src/core/database/database_settings.py | 1 - 20 files changed, 230 insertions(+), 152 deletions(-) create mode 100644 src/app/v2/purchases/dtos/purchase_dto.py delete mode 100644 src/app/v2/users/models/user_mission.py delete mode 100644 src/celery_models.py delete mode 100644 src/celery_settings.py create mode 100644 src/celery_worker.py rename src/{core => common}/tasks/__init__.py (100%) create mode 100644 src/common/tasks/mission_task.py create mode 100644 src/common/tasks/renew_subscription_task.py rename src/{core/tasks => common/utils}/scheduler.py (72%) create mode 100644 src/core/configs/celery_settings.py diff --git a/poetry.lock b/poetry.lock index 99bad8c..f522317 100644 --- a/poetry.lock +++ b/poetry.lock @@ -220,6 +220,21 @@ yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.22.0)"] +[[package]] +name = "celery-stubs" +version = "0.1.3" +description = "celery stubs" +optional = false +python-versions = "*" +files = [ + {file = "celery-stubs-0.1.3.tar.gz", hash = "sha256:0fb5345820f8a2bd14e6ffcbef2d10181e12e40f8369f551d7acc99d8d514919"}, + {file = "celery_stubs-0.1.3-py3-none-any.whl", hash = "sha256:dfb9ad27614a8af028b2055bb4a4ae99ca5e9a8d871428a506646d62153218d7"}, +] + +[package.dependencies] +mypy = ">=0.950" +typing-extensions = ">=4.2.0" + [[package]] name = "certifi" version = "2024.8.30" @@ -2016,4 +2031,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "8de91580f01ea0c8d7d83151891614831cc5cf42e2b3521a6d1f9a37b14a26a5" +content-hash = "42b1f2c02091e30e76d806b1415db897e5feb02b3daf41250bbc809f76e05ea2" diff --git a/pyproject.toml b/pyproject.toml index 4fffb84..02211f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,13 @@ pytest = "^8.3.3" pytest-asyncio = "^0.24.0" mypy-extensions = "^1.0.0" coverage = "^7.6.8" +celery-stubs = "^0.1.3" [tool.mypy] files = "src" strict = true + + #disallow_untyped_calls = true # 타입이 없는 함수 호출 금지 #disallow_untyped_defs = true # 타입이 없는 함수 정의 금지 #ignore_missing_imports = true # 누락된 import 무시 @@ -48,3 +51,4 @@ line-length = 120 [tool.isort] profile = "black" line_length = 120 + diff --git a/src/app/v2/missions/models/mission.py b/src/app/v2/missions/models/mission.py index ca68deb..94f281f 100644 --- a/src/app/v2/missions/models/mission.py +++ b/src/app/v2/missions/models/mission.py @@ -16,6 +16,9 @@ class UserMission(Model): progress_count = fields.IntField(default=0) user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="missions") + class Meta: + table = "user_mission" + @classmethod async def get_user_missions_by_condition_type(cls, user_id: str) -> Any: query = SELECT_USER_MISSIONS_QUERY diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index b18f31c..746189c 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -70,16 +70,16 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: LevelService.get_level_info_add_answer_days(user_id), ) - cheese_amount = await CheeseService.get_cheese_balance(cheese_manager_id=user.cheese_manager_id) + cheese_amount = await CheeseService.get_cheese_balance(cheese_manager_id=user.cheese_manager_id) # type: ignore user_profile_data = UserProfileWithLevel.builder( userProfile=UserProfileDTO.builder( - nickname=user.nickname, + nickname=user.nickname, # type: ignore cheeseBalance=cheese_amount, badgeCode=teller_card.badgeCode, badgeCount=badge_count, answerCount=answer_count, - premium=user.is_premium, + premium=user.is_premium, # type: ignore ), level=level, ) diff --git a/src/app/v2/purchases/dtos/purchase_dto.py b/src/app/v2/purchases/dtos/purchase_dto.py new file mode 100644 index 0000000..0e2afcd --- /dev/null +++ b/src/app/v2/purchases/dtos/purchase_dto.py @@ -0,0 +1,32 @@ +from typing import Any + +from pydantic import BaseModel + +from app.v2.purchases.models.purchase_status import purchase_mapping + + +class ReceiptInfoDTO(BaseModel): + transaction_id: str + original_transaction_id: str + expires_date_ms: int + purchase_date_ms: int + product_code: str + quantity: int + + @classmethod + def build(cls, latest_receipt_info: dict[str, Any]) -> "ReceiptInfoDTO": + transaction_id = latest_receipt_info["transaction_id"] + original_transaction_id = latest_receipt_info["original_transaction_id"] + expires_date_ms = int(latest_receipt_info.get("expires_date_ms", 0)) + purchase_date_ms = int(latest_receipt_info.get("purchase_date_ms", 0)) + product_code = purchase_mapping.get(latest_receipt_info["product_id"], latest_receipt_info["product_id"]) + quantity = int(latest_receipt_info.get("quantity", 1)) + + return cls( + transaction_id=transaction_id, + original_transaction_id=original_transaction_id, + expires_date_ms=expires_date_ms, + purchase_date_ms=purchase_date_ms, + product_code=product_code, + quantity=quantity, + ) diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py index 880b84d..d41f97b 100644 --- a/src/app/v2/purchases/models/purchase_history.py +++ b/src/app/v2/purchases/models/purchase_history.py @@ -14,7 +14,6 @@ class Subscription(Model): subscription_id = fields.BigIntField(pk=True, description="Primary key for the Subscription") product_code = fields.CharField(max_length=255, null=False, description="Product code of the subscription") status = fields.CharField(max_length=255, null=False, description="Status of the subscription") - auto_renew_status = fields.BooleanField(default=True, description="Whether auto-renewal is active") current_transaction_id = fields.CharField(max_length=255, null=False, description="Current transaction ID") expires_date = fields.DatetimeField(null=False, description="Expiration date of the subscription") created_at = fields.DatetimeField(auto_now_add=True, description="When the subscription was created") @@ -58,15 +57,14 @@ async def create_or_update_subscription( status: str, ) -> "Subscription": query = """ - INSERT INTO subscription (user_id, product_code, status, current_transaction_id, expires_date, auto_renew_status) - VALUES (UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, FROM_UNIXTIME(%s / 1000), %s) + INSERT INTO subscription (user_id, product_code, status, current_transaction_id, expires_date) + VALUES (UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, FROM_UNIXTIME(%s / 1000)) ON DUPLICATE KEY UPDATE current_transaction_id = VALUES(current_transaction_id), expires_date = VALUES(expires_date), status = VALUES(status); """ - auto_renew_status = True - values = (user_id, product_code, status, transaction_id, expires_date_ms, auto_renew_status) + values = (user_id, product_code, status, transaction_id, expires_date_ms) await QueryExecutor.execute_query(query, values=values, fetch_type="none") diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 66937d0..cb60a3f 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -33,3 +33,10 @@ async def receipt_test( purchase_service: PurchaseService = Depends(), ) -> dict[str, Any]: return await purchase_service._validate_apple_receipt(receipt_data=receipt.receiptData) + + +@router.get("/renew-test") +async def renew_test( + purchase_service: PurchaseService = Depends(), +) -> dict[str, Any]: + return await purchase_service.renew_subscription() diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index b7730fc..70b6152 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,3 +1,5 @@ +import uuid +from datetime import datetime, timedelta, timezone from typing import Any, Optional, cast import httpx @@ -6,6 +8,7 @@ from tortoise.transactions import atomic from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory +from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO from app.v2.purchases.models.purchase_history import PurchaseHistory, Subscription from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus, purchase_mapping from app.v2.users.services.user_service import UserService @@ -22,20 +25,16 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: if latest_receipt_info is None: raise ValueError("No valid receipt information found.") - transaction_id = latest_receipt_info["transaction_id"] - original_transaction_id = latest_receipt_info["original_transaction_id"] - expires_date_ms = int(latest_receipt_info.get("expires_date_ms", 0)) - purchase_date_ms = int(latest_receipt_info.get("purchase_date_ms", 0)) - product_code = purchase_mapping.get(latest_receipt_info["product_id"], latest_receipt_info["product_id"]) + receipt_info = await self._parse_receipt_info(latest_receipt_info) await self._create_or_update_subscription( user_id=user_id, - product_code=product_code, - transaction_id=transaction_id, - expires_date_ms=expires_date_ms, + product_code=receipt_info.product_code, + transaction_id=receipt_info.transaction_id, + expires_date_ms=receipt_info.expires_date_ms, ) - subscription = await self._get_subscription(user_id, product_code) + subscription = await self._get_subscription(user_id, receipt_info.product_code) if subscription is None: raise DoesNotExist("Subscription not found") @@ -43,16 +42,16 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: await self._create_purchase_history( user_id=user_id, subscription=subscription, - product_code=product_code, - transaction_id=transaction_id, - original_transaction_id=original_transaction_id, - expires_date_ms=expires_date_ms, - purchase_date_ms=purchase_date_ms, - quantity=int(latest_receipt_info.get("quantity", 1)), + product_code=receipt_info.product_code, + transaction_id=receipt_info.transaction_id, + original_transaction_id=receipt_info.original_transaction_id, + expires_date_ms=receipt_info.expires_date_ms, + purchase_date_ms=receipt_info.purchase_date_ms, + quantity=receipt_info.quantity, receipt_data=receipt_data, ) - item_inventory_products = await self._validate_purchase(product_code=product_code) + item_inventory_products = await self._validate_purchase(product_code=receipt_info.product_code) await self._process_purchase(user_id=user_id, item_inventory_products=item_inventory_products) @@ -83,7 +82,10 @@ async def _validate_apple_receipt(receipt_data: str) -> dict[str, Any]: @staticmethod async def _create_or_update_subscription( - user_id: str, product_code: str, transaction_id: str, expires_date_ms: int + user_id: str, + product_code: str, + transaction_id: str, + expires_date_ms: int, ) -> None: await Subscription.create_or_update_subscription( user_id=user_id, @@ -163,3 +165,65 @@ async def _process_purchase( # await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) else: raise ValueError(f"Invalid item category for purchase: {item.item_category}") + + async def renew_subscription(self) -> dict[str, Any]: + today = datetime.now(timezone(timedelta(hours=9))) + + subscriptions_to_renew = await Subscription.filter( + expires_date__lte=today + timedelta(days=1), status="ACTIVE" + ).select_related("user") + + for subscription in subscriptions_to_renew: + + purchase_history = await PurchaseHistory.filter(transaction_id=subscription.current_transaction_id).first() + + if purchase_history: + response = await self._validate_apple_receipt(receipt_data=purchase_history.receipt_data) + + latest_receipt_info = self._extract_latest_receipt_info(response) + + if latest_receipt_info is None: + raise ValueError("No valid receipt information found.") + + receipt_data = await self._parse_receipt_info(latest_receipt_info) + + if not await self._check_auto_renewal(response.get("pending_renewal_info", [])): + continue + + await self._update_subscription_expiration( + subscription=subscription, expires_date_ms=receipt_data.expires_date_ms + ) + + await self._create_purchase_history( + user_id=str(uuid.UUID(bytes=subscription.user.user_id)), # type: ignore + subscription=subscription, + product_code=receipt_data.product_code, + transaction_id=receipt_data.transaction_id, + original_transaction_id=receipt_data.original_transaction_id, + expires_date_ms=receipt_data.expires_date_ms, + purchase_date_ms=receipt_data.purchase_date_ms, + quantity=receipt_data.quantity, + receipt_data=purchase_history.receipt_data, + ) + + return {"message": "Subscription renewal completed successfully"} + + @staticmethod + async def _update_subscription_expiration(subscription: Subscription, expires_date_ms: int) -> None: + new_expires_date = datetime.fromtimestamp(expires_date_ms / 1000) + subscription.expires_date = new_expires_date + await subscription.save() + + @staticmethod + async def _parse_receipt_info(latest_receipt_info: dict[str, Any]) -> ReceiptInfoDTO: + return ReceiptInfoDTO.build(latest_receipt_info) + + @staticmethod + async def _check_auto_renewal(pending_renewal_info: list[dict[str, Any]]) -> bool: + if pending_renewal_info: + auto_renew_status = pending_renewal_info[0].get("auto_renew_status") + expiration_intent = pending_renewal_info[0].get("expiration_intent") + + if auto_renew_status == "0" or expiration_intent == "1": + return False + return True diff --git a/src/app/v2/teller_cards/services/teller_card_service.py b/src/app/v2/teller_cards/services/teller_card_service.py index 39e341a..7e34904 100644 --- a/src/app/v2/teller_cards/services/teller_card_service.py +++ b/src/app/v2/teller_cards/services/teller_card_service.py @@ -21,7 +21,7 @@ async def patch_teller_card( ) @classmethod - async def validate_teller_card(cls, badge_code: str, color_code: str) -> None: + async def validate_teller_card(cls, badge_code: Optional[str], color_code: Optional[str]) -> None: badge_code_list = await BadgeInventory.all().values("badge_code") color_code_list = await ColorInventory.all().values("color_code") badge_codes = [badge["badge_code"] for badge in badge_code_list] diff --git a/src/app/v2/users/models/user_mission.py b/src/app/v2/users/models/user_mission.py deleted file mode 100644 index eb1712c..0000000 --- a/src/app/v2/users/models/user_mission.py +++ /dev/null @@ -1,16 +0,0 @@ -from tortoise import fields -from tortoise.fields import ForeignKeyRelation -from tortoise.models import Model - -from app.v2.users.models.user import User - - -class UserMission(Model): - user_mission_id = fields.BigIntField(pk=True) - is_completed = fields.BooleanField(null=True) - mission_code = fields.CharField(max_length=255, null=True) - progress_count = fields.IntField(null=True) - user: ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", related_name="missions") - - class Meta: - table = "user_mission" diff --git a/src/celery_models.py b/src/celery_models.py deleted file mode 100644 index f3571d5..0000000 --- a/src/celery_models.py +++ /dev/null @@ -1,13 +0,0 @@ -from tortoise import fields -from tortoise.models import Model - - -class Question(Model): - date = fields.DateField(pk=True) # 기본 키로 설정된 날짜 필드 - phrase = fields.CharField(max_length=255) - title = fields.CharField(max_length=255) - spare_phrase = fields.CharField(max_length=255) - spare_title = fields.CharField(max_length=255) - - class Meta: - table = "question" # 데이터베이스에서 매핑할 테이블 이름 diff --git a/src/celery_settings.py b/src/celery_settings.py deleted file mode 100644 index 4c352f6..0000000 --- a/src/celery_settings.py +++ /dev/null @@ -1,86 +0,0 @@ -import asyncio -import logging - -from celery import Celery -from tortoise import Tortoise, Model, fields - -from celery_models import Question -from core.configs import settings - - -TORTOISE_ORM = { - "connections": { - "default": { - "engine": "tortoise.backends.mysql", - "credentials": { - "host": settings.DB_HOST, - "port": settings.DB_PORT, - "user": settings.DB_USER, - "password": settings.DB_PASSWORD, - "database": settings.DB_NAME, - "connect_timeout": 5, - "charset": settings.DB_CHARSET, - "minsize": 5, # 커넥션 풀 최소 크기 - "maxsize": 10, # 커넥션 풀 최대 크기 - }, - }, - }, - "apps": { - "models": { - "models": ["app.celery_models"], - "default_connection": "default", - }, - }, - "use_tz": False, # 비동기 ORM에서 타임존 설정을 고려하지 않는다면 False - "timezone": settings.DB_TIMEZONE, # 타임존 설정 -} - -celery_app = Celery( - "telling-me-celery", - broker="redis://localhost:6379/0", # Redis를 브로커로 사용 - backend="redis://localhost:6379/0", # Redis를 결과 백엔드로 사용 -) - -celery_app.conf.update( - task_serializer="json", - result_serializer="json", - accept_content=["json"], - timezone="Asia/Seoul", - enable_utc=True, - result_expires=3600, # 작업 결과 만료 시간 (초 단위) -) - - -@celery_app.task(name="daily_task") -def daily_task() -> None: - asyncio.run(execute_async_daily_task()) - - -async def execute_async_daily_task() -> None: - await initialize_celery() - try: - print("Executing daily task!") - await Question.create( - date="2023-07-29", - title="Title for 2024-12-10", - phrase="Phrase", - spare_phrase="s", - spare_title="s", - ) - - finally: - await close_celery_connections() - - -async def initialize_celery(): - logger = logging.getLogger(__name__) - logger.info(f"Current path: 여기") - await Tortoise.init(config=TORTOISE_ORM) - - -async def close_celery_connections(): - await Tortoise.close_connections() - - -if __name__ == "__main__": - celery_app.start() diff --git a/src/celery_worker.py b/src/celery_worker.py new file mode 100644 index 0000000..7eeeeb6 --- /dev/null +++ b/src/celery_worker.py @@ -0,0 +1,3 @@ +from core.configs.celery_settings import celery_app + +__all__ = ("celery_app",) diff --git a/src/common/post_construct.py b/src/common/post_construct.py index a894aa2..f72dfb4 100644 --- a/src/common/post_construct.py +++ b/src/common/post_construct.py @@ -1,8 +1,8 @@ from fastapi import FastAPI -from core.tasks.scheduler import start_scheduler from common.handlers.exception_handler import attach_exception_handlers from common.handlers.router_handler import attach_router_handlers +from common.utils.scheduler import start_scheduler from core.database.database_settings import database_initialize diff --git a/src/core/tasks/__init__.py b/src/common/tasks/__init__.py similarity index 100% rename from src/core/tasks/__init__.py rename to src/common/tasks/__init__.py diff --git a/src/common/tasks/mission_task.py b/src/common/tasks/mission_task.py new file mode 100644 index 0000000..49c9ecc --- /dev/null +++ b/src/common/tasks/mission_task.py @@ -0,0 +1,7 @@ +from app.v2.missions.models.mission import UserMission + + +async def mission_reset_task() -> None: + await UserMission.filter(mission_code__in=["MS_LV_UP", "MS_DAILY_LIKE_3_PER_DAY", "MS_SINGLE_POST_2_5"]).update( + is_completed=False + ) diff --git a/src/common/tasks/renew_subscription_task.py b/src/common/tasks/renew_subscription_task.py new file mode 100644 index 0000000..83e0bbe --- /dev/null +++ b/src/common/tasks/renew_subscription_task.py @@ -0,0 +1,6 @@ +from app.v2.purchases.services.purchase_service import PurchaseService + + +async def renew_subscription_task() -> None: + purchase_service = PurchaseService() + await purchase_service.renew_subscription() diff --git a/src/core/tasks/scheduler.py b/src/common/utils/scheduler.py similarity index 72% rename from src/core/tasks/scheduler.py rename to src/common/utils/scheduler.py index 8768589..6d4571d 100644 --- a/src/core/tasks/scheduler.py +++ b/src/common/utils/scheduler.py @@ -1,15 +1,14 @@ -from apscheduler.jobstores.redis import RedisJobStore -from apscheduler.schedulers.background import BackgroundScheduler - - import logging -from celery_settings import celery_app +from apscheduler.jobstores.redis import RedisJobStore # type: ignore +from apscheduler.schedulers.background import BackgroundScheduler # type: ignore + +from core.configs.celery_settings import celery_app logger = logging.getLogger(__name__) -def execute_daily_task(): +def execute_daily_task() -> None: celery_app.send_task("daily_task") @@ -24,8 +23,8 @@ def start_scheduler(): scheduler.add_job( func=execute_daily_task, trigger="cron", - hour=11, - minute=9, + hour=1, + minute=11, id="daily_task", replace_existing=True, ) diff --git a/src/core/configs/celery_settings.py b/src/core/configs/celery_settings.py new file mode 100644 index 0000000..73db400 --- /dev/null +++ b/src/core/configs/celery_settings.py @@ -0,0 +1,56 @@ +import asyncio +import logging + +from celery import Celery +from tortoise import Tortoise + +from common.tasks.mission_task import mission_reset_task +from common.tasks.renew_subscription_task import renew_subscription_task +from core.database.database_settings import TORTOISE_ORM + +celery_app = Celery( + "telling-me-celery", + broker="redis://localhost:6379/0", # Redis를 브로커로 사용 + backend="redis://localhost:6379/0", # Redis를 결과 백엔드로 사용 +) + +celery_app.conf.update( + task_serializer="json", + result_serializer="json", + accept_content=["json"], + timezone="Asia/Seoul", + enable_utc=True, + result_expires=3600, # 작업 결과 만료 시간 (초 단위) +) + + +@celery_app.task(name="daily_task") +def daily_task() -> None: + asyncio.run(execute_async_daily_task()) + + +async def execute_async_daily_task() -> None: + await initialize_celery() + try: + await mission_reset_task() + await renew_subscription_task() + finally: + await close_celery_connections() + + +async def initialize_celery(): + logger = logging.getLogger(__name__) + logger.info(f"Current path: 여기") + logging.basicConfig(level=logging.DEBUG) + db_client_logger = logging.getLogger("tortoise.db_client") + db_client_logger.setLevel(logging.DEBUG) + + await Tortoise.init(config=TORTOISE_ORM) + + +async def close_celery_connections(): + await Tortoise.close_connections() + + +if __name__ == "__main__": + celery_app.start() diff --git a/src/core/database/database_settings.py b/src/core/database/database_settings.py index dbd8101..98477c4 100644 --- a/src/core/database/database_settings.py +++ b/src/core/database/database_settings.py @@ -7,7 +7,6 @@ TORTOISE_APP_MODELS = [ "app.v2.questions.models.question", "app.v2.users.models.user", - "app.v2.users.models.user_mission", "app.v2.users.models.refresh_token", "app.v2.badges.models.badge", "app.v2.colors.models.color", From 8b35ab5c30b49c75602a20a33faf1ef070c80ec1 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Fri, 6 Dec 2024 00:18:04 +0900 Subject: [PATCH 42/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=94=84=EB=A6=AC?= =?UTF-8?q?=EB=AF=B8=EC=97=84=20=EC=9C=A0=EC=A0=80=20=EB=A7=8C=EB=A3=8C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/router.py | 7 ++++++ .../v2/purchases/services/purchase_service.py | 25 +++++++++++++++++++ src/app/v2/users/models/user.py | 20 ++++++++++++++- src/common/tasks/renew_subscription_task.py | 5 ++++ src/common/utils/scheduler.py | 6 ++--- src/core/configs/celery_settings.py | 7 +++--- 6 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index cb60a3f..12cbede 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -40,3 +40,10 @@ async def renew_test( purchase_service: PurchaseService = Depends(), ) -> dict[str, Any]: return await purchase_service.renew_subscription() + + +@router.get("/expired-test") +async def expired_test( + purchase_service: PurchaseService = Depends(), +) -> None: + await purchase_service.expire_subscriptions() diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 70b6152..724cbbb 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -4,6 +4,7 @@ import httpx from fastapi import HTTPException +from tortoise import Tortoise from tortoise.exceptions import DoesNotExist from tortoise.transactions import atomic @@ -11,6 +12,7 @@ from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO from app.v2.purchases.models.purchase_history import PurchaseHistory, Subscription from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus, purchase_mapping +from app.v2.users.models.user import User from app.v2.users.services.user_service import UserService from core.configs import settings @@ -227,3 +229,26 @@ async def _check_auto_renewal(pending_renewal_info: list[dict[str, Any]]) -> boo if auto_renew_status == "0" or expiration_intent == "1": return False return True + + @staticmethod + async def expire_subscriptions() -> None: + today = datetime.now().date() + + expired_subscriptions = ( + await Subscription.filter( + status=SubscriptionStatus.ACTIVE.value, + expires_date__lt=today, + ) + .select_related("user") + .all() + ) + + for subscription in expired_subscriptions: + subscription.status = SubscriptionStatus.EXPIRED.value + + user_ids = [subscription.user.user_id for subscription in expired_subscriptions] + + if user_ids: + if expired_subscriptions: + await Subscription.bulk_update(expired_subscriptions, fields=["status"]) + await User.bulk_update_is_premium(user_ids) # type: ignore diff --git a/src/app/v2/users/models/user.py b/src/app/v2/users/models/user.py index f53e6e3..72212a9 100644 --- a/src/app/v2/users/models/user.py +++ b/src/app/v2/users/models/user.py @@ -1,7 +1,8 @@ +import uuid from datetime import datetime from typing import Any, Optional -from tortoise import fields +from tortoise import Tortoise, fields from tortoise.fields import ForeignKeyRelation from tortoise.models import Model @@ -81,3 +82,20 @@ async def set_is_premium(cls, user_id: str, is_premium: bool) -> Any: current_time = datetime.now() values = (int(is_premium), current_time, user_id) await QueryExecutor.execute_query(query, values=values, fetch_type="single") + + @classmethod + def format_user_id(cls, user_id_bytes: bytes) -> str: + return str(uuid.UUID(bytes=user_id_bytes)) + + @classmethod + def format_user_ids(cls, user_ids: list[bytes]) -> str: + return ", ".join([f"UNHEX(REPLACE('{str(uuid.UUID(bytes=user_id))}', '-', ''))" for user_id in user_ids]) + + @classmethod + async def bulk_update_is_premium(cls, user_ids: list[bytes]) -> None: + query = f""" + UPDATE user + SET is_premium = FALSE + WHERE user_id IN ({cls.format_user_ids(user_ids)}); + """ + await Tortoise.get_connection("default").execute_query(query) diff --git a/src/common/tasks/renew_subscription_task.py b/src/common/tasks/renew_subscription_task.py index 83e0bbe..5dcc569 100644 --- a/src/common/tasks/renew_subscription_task.py +++ b/src/common/tasks/renew_subscription_task.py @@ -4,3 +4,8 @@ async def renew_subscription_task() -> None: purchase_service = PurchaseService() await purchase_service.renew_subscription() + + +async def expire_subscription_task() -> None: + purchase_service = PurchaseService() + await purchase_service.expire_subscriptions() diff --git a/src/common/utils/scheduler.py b/src/common/utils/scheduler.py index 6d4571d..6f1f6c1 100644 --- a/src/common/utils/scheduler.py +++ b/src/common/utils/scheduler.py @@ -12,7 +12,7 @@ def execute_daily_task() -> None: celery_app.send_task("daily_task") -def start_scheduler(): +def start_scheduler() -> None: scheduler = BackgroundScheduler( jobstores={"default": RedisJobStore(host="localhost", port=6379, db=0)}, timezone="Asia/Seoul", @@ -23,8 +23,8 @@ def start_scheduler(): scheduler.add_job( func=execute_daily_task, trigger="cron", - hour=1, - minute=11, + hour=00, + minute=16, id="daily_task", replace_existing=True, ) diff --git a/src/core/configs/celery_settings.py b/src/core/configs/celery_settings.py index 73db400..d1d70cb 100644 --- a/src/core/configs/celery_settings.py +++ b/src/core/configs/celery_settings.py @@ -5,7 +5,7 @@ from tortoise import Tortoise from common.tasks.mission_task import mission_reset_task -from common.tasks.renew_subscription_task import renew_subscription_task +from common.tasks.renew_subscription_task import renew_subscription_task, expire_subscription_task from core.database.database_settings import TORTOISE_ORM celery_app = Celery( @@ -34,11 +34,12 @@ async def execute_async_daily_task() -> None: try: await mission_reset_task() await renew_subscription_task() + await expire_subscription_task() finally: await close_celery_connections() -async def initialize_celery(): +async def initialize_celery() -> None: logger = logging.getLogger(__name__) logger.info(f"Current path: 여기") logging.basicConfig(level=logging.DEBUG) @@ -48,7 +49,7 @@ async def initialize_celery(): await Tortoise.init(config=TORTOISE_ORM) -async def close_celery_connections(): +async def close_celery_connections() -> None: await Tortoise.close_connections() From b62ad19018d91feef4781c5cbf74ea06e38d726d Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 7 Dec 2024 15:25:39 +0900 Subject: [PATCH 43/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=99=98=EB=B6=88=20?= =?UTF-8?q?=EC=98=81=EC=88=98=EC=A6=9D=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/dtos/purchase_dto.py | 5 +- .../v2/purchases/models/purchase_history.py | 13 +- src/app/v2/purchases/router.py | 4 +- .../v2/purchases/services/purchase_service.py | 171 ++++++++++++------ src/common/exceptions/error_code.py | 2 + src/common/tasks/renew_subscription_task.py | 2 +- 6 files changed, 126 insertions(+), 71 deletions(-) diff --git a/src/app/v2/purchases/dtos/purchase_dto.py b/src/app/v2/purchases/dtos/purchase_dto.py index 0e2afcd..35d2db0 100644 --- a/src/app/v2/purchases/dtos/purchase_dto.py +++ b/src/app/v2/purchases/dtos/purchase_dto.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from pydantic import BaseModel @@ -12,6 +12,7 @@ class ReceiptInfoDTO(BaseModel): purchase_date_ms: int product_code: str quantity: int + cancellation_date_ms: Optional[int] = None @classmethod def build(cls, latest_receipt_info: dict[str, Any]) -> "ReceiptInfoDTO": @@ -21,6 +22,7 @@ def build(cls, latest_receipt_info: dict[str, Any]) -> "ReceiptInfoDTO": purchase_date_ms = int(latest_receipt_info.get("purchase_date_ms", 0)) product_code = purchase_mapping.get(latest_receipt_info["product_id"], latest_receipt_info["product_id"]) quantity = int(latest_receipt_info.get("quantity", 1)) + cancellation_date_ms = latest_receipt_info.get("cancellation_date_ms") # 환불일 (밀리초) return cls( transaction_id=transaction_id, @@ -29,4 +31,5 @@ def build(cls, latest_receipt_info: dict[str, Any]) -> "ReceiptInfoDTO": purchase_date_ms=purchase_date_ms, product_code=product_code, quantity=quantity, + cancellation_date_ms=cancellation_date_ms, ) diff --git a/src/app/v2/purchases/models/purchase_history.py b/src/app/v2/purchases/models/purchase_history.py index d41f97b..c5206b9 100644 --- a/src/app/v2/purchases/models/purchase_history.py +++ b/src/app/v2/purchases/models/purchase_history.py @@ -5,7 +5,6 @@ from tortoise.fields import ForeignKeyRelation from tortoise.models import Model -from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus from app.v2.users.models.user import User from common.utils.query_executor import QueryExecutor @@ -80,8 +79,6 @@ async def create_or_update_subscription( async def update_subscription( cls, user_id: str, product_code: str, transaction_id: str, expires_date_ms: int ) -> None: - expires_date = datetime.fromtimestamp(expires_date_ms / 1000.0) - query = """ UPDATE subscription SET current_transaction_id = %s, @@ -104,8 +101,6 @@ class PurchaseHistory(Model): expires_date = fields.DatetimeField(null=True, description="Expiration date of the purchase") purchase_date = fields.DatetimeField(null=False, description="Date of the purchase") quantity = fields.IntField(default=1, description="Quantity of items purchased") - is_refunded = fields.BooleanField(default=False, description="Whether the purchase was refunded") - refunded_at = fields.DatetimeField(null=True, description="When the purchase was refunded") receipt_data = fields.TextField(null=True, description="Raw receipt data from Apple") created_at = fields.DatetimeField(auto_now_add=True, description="When the purchase was made") updated_at = fields.DatetimeField(auto_now=True, description="Last updated timestamp") @@ -141,19 +136,17 @@ async def create_purchase_history( purchase_date_ms: int, receipt_data: str, quantity: int = 1, - is_refunded: bool = False, - refunded_at: Optional[datetime] = None, ) -> None: query = """ INSERT INTO purchase_history ( user_id, subscription_id, product_code, transaction_id, original_transaction_id, status, expires_date, purchase_date, - quantity, is_refunded, refunded_at, receipt_data, created_at, updated_at + quantity, receipt_data, created_at, updated_at ) VALUES ( UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, %s, %s, FROM_UNIXTIME(%s / 1000), FROM_UNIXTIME(%s / 1000), - %s, %s, %s, %s, NOW(), NOW() + %s, %s, NOW(), NOW() ); """ @@ -167,8 +160,6 @@ async def create_purchase_history( expires_date_ms, purchase_date_ms, quantity, - is_refunded, - refunded_at, receipt_data, ) diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 12cbede..1e36b7f 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -38,8 +38,8 @@ async def receipt_test( @router.get("/renew-test") async def renew_test( purchase_service: PurchaseService = Depends(), -) -> dict[str, Any]: - return await purchase_service.renew_subscription() +) -> None: + return await purchase_service.process_subscriptions_renewal() @router.get("/expired-test") diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 724cbbb..cceee82 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,19 +1,20 @@ import uuid -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone from typing import Any, Optional, cast import httpx from fastapi import HTTPException -from tortoise import Tortoise from tortoise.exceptions import DoesNotExist from tortoise.transactions import atomic from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO from app.v2.purchases.models.purchase_history import PurchaseHistory, Subscription -from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus, purchase_mapping +from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus from app.v2.users.models.user import User from app.v2.users.services.user_service import UserService +from common.exceptions.custom_exception import CustomException +from common.exceptions.error_code import ErrorCode from core.configs import settings @@ -25,15 +26,20 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: latest_receipt_info = self._extract_latest_receipt_info(response) if latest_receipt_info is None: - raise ValueError("No valid receipt information found.") + raise CustomException(ErrorCode.NO_VALID_RECEIPT) receipt_info = await self._parse_receipt_info(latest_receipt_info) + subscription_status = self.get_subscription_status(receipt_info.cancellation_date_ms) + + purchase_status = self.get_purchase_status(receipt_info.cancellation_date_ms) + await self._create_or_update_subscription( user_id=user_id, product_code=receipt_info.product_code, transaction_id=receipt_info.transaction_id, expires_date_ms=receipt_info.expires_date_ms, + status=subscription_status, ) subscription = await self._get_subscription(user_id, receipt_info.product_code) @@ -47,6 +53,7 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: product_code=receipt_info.product_code, transaction_id=receipt_info.transaction_id, original_transaction_id=receipt_info.original_transaction_id, + status=purchase_status, expires_date_ms=receipt_info.expires_date_ms, purchase_date_ms=receipt_info.purchase_date_ms, quantity=receipt_info.quantity, @@ -55,7 +62,11 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: item_inventory_products = await self._validate_purchase(product_code=receipt_info.product_code) - await self._process_purchase(user_id=user_id, item_inventory_products=item_inventory_products) + await self._process_purchase( + user_id=user_id, + item_inventory_products=item_inventory_products, + status=purchase_status, + ) @staticmethod def _extract_latest_receipt_info(response: dict[str, Any]) -> dict[str, Any] | None: @@ -65,18 +76,27 @@ def _extract_latest_receipt_info(response: dict[str, Any]) -> dict[str, Any] | N return latest_receipt_info[0] or {} return None - @staticmethod - async def _validate_apple_receipt(receipt_data: str) -> dict[str, Any]: - + async def _validate_apple_receipt(self, receipt_data: str) -> dict[str, Any]: url = settings.APPLE_URL + payload = self.create_receipt_validation_payload(receipt_data) + response = await self.send_receipt_validation_request(url, payload) + return await self.parse_apple_response(response) - payload = { + @staticmethod + def create_receipt_validation_payload(receipt_data: str) -> dict[str, Any]: + return { "receipt-data": receipt_data, "password": settings.APPLE_SHARED_SECRET, } + @staticmethod + async def send_receipt_validation_request(url: str, payload: dict[str, Any]) -> httpx.Response: async with httpx.AsyncClient() as client: response = await client.post(url, json=payload) + return response + + @staticmethod + async def parse_apple_response(response: httpx.Response) -> dict[str, Any]: if response.status_code == 200: return cast(dict[str, Any], response.json()) else: @@ -88,15 +108,28 @@ async def _create_or_update_subscription( product_code: str, transaction_id: str, expires_date_ms: int, + status: str, ) -> None: await Subscription.create_or_update_subscription( user_id=user_id, product_code=product_code, transaction_id=transaction_id, expires_date_ms=expires_date_ms, - status=SubscriptionStatus.ACTIVE.value, + status=status, ) + @staticmethod + def get_subscription_status(cancellation_date_ms: Optional[int]) -> str: + if cancellation_date_ms: + return SubscriptionStatus.CANCELED.value + return SubscriptionStatus.ACTIVE.value + + @staticmethod + def get_purchase_status(cancellation_date_ms: Optional[int]) -> str: + if cancellation_date_ms: + return PurchaseStatus.REFUNDED.value + return PurchaseStatus.AVAILABLE.value + @staticmethod async def _get_subscription(user_id: str, product_code: str) -> Subscription | None: return await Subscription.get_subscription_by_user_id_and_product_code( @@ -110,6 +143,7 @@ async def _create_purchase_history( product_code: str, transaction_id: str, original_transaction_id: str, + status: str, expires_date_ms: int, purchase_date_ms: int, quantity: int, @@ -121,12 +155,10 @@ async def _create_purchase_history( product_code=product_code, transaction_id=transaction_id, original_transaction_id=original_transaction_id, - status=PurchaseStatus.AVAILABLE.value, + status=status, expires_date_ms=expires_date_ms, purchase_date_ms=purchase_date_ms, quantity=quantity, - is_refunded=False, - refunded_at=None, receipt_data=receipt_data, ) @@ -155,6 +187,7 @@ async def _process_purchase( cls, item_inventory_products: list[ItemInventoryProductInventory], user_id: str, + status: str = "ACTIVE", # cheese_manager_id: int, ) -> None: for item_inventory_product in item_inventory_products: @@ -162,58 +195,77 @@ async def _process_purchase( quantity = item_inventory_product.quantity if item.item_category == "SUBSCRIPTION": - await UserService.set_is_premium(user_id=user_id, is_premium=True) + if status == SubscriptionStatus.ACTIVE.value: + await UserService.set_is_premium(user_id=user_id, is_premium=True) + if status == SubscriptionStatus.CANCELED.value: + await UserService.set_is_premium(user_id=user_id, is_premium=False) # elif item.item_category == "CHEESE": # await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) else: raise ValueError(f"Invalid item category for purchase: {item.item_category}") - async def renew_subscription(self) -> dict[str, Any]: - today = datetime.now(timezone(timedelta(hours=9))) + async def renew_subscription(self, subscription: Subscription) -> None: - subscriptions_to_renew = await Subscription.filter( - expires_date__lte=today + timedelta(days=1), status="ACTIVE" - ).select_related("user") + purchase_history = await PurchaseHistory.filter(transaction_id=subscription.current_transaction_id).first() - for subscription in subscriptions_to_renew: + if not purchase_history: + return - purchase_history = await PurchaseHistory.filter(transaction_id=subscription.current_transaction_id).first() + response = await self._validate_apple_receipt(receipt_data=purchase_history.receipt_data) + latest_receipt_info = self._extract_latest_receipt_info(response) + + if latest_receipt_info is None: + raise CustomException(ErrorCode.NO_VALID_RECEIPT) - if purchase_history: - response = await self._validate_apple_receipt(receipt_data=purchase_history.receipt_data) + receipt_data = await self._parse_receipt_info(latest_receipt_info) - latest_receipt_info = self._extract_latest_receipt_info(response) + purchase_status = self.get_purchase_status(receipt_data.cancellation_date_ms) - if latest_receipt_info is None: - raise ValueError("No valid receipt information found.") + if not await self._check_auto_renewal(response.get("pending_renewal_info", [])): + return - receipt_data = await self._parse_receipt_info(latest_receipt_info) + await self._update_subscription_expiration( + subscription=subscription, + expires_date_ms=receipt_data.expires_date_ms, + transaction_id=receipt_data.transaction_id, + ) - if not await self._check_auto_renewal(response.get("pending_renewal_info", [])): - continue + await self._create_purchase_history( + user_id=str(uuid.UUID(bytes=subscription.user.user_id)), # type: ignore + subscription=subscription, + product_code=receipt_data.product_code, + transaction_id=receipt_data.transaction_id, + original_transaction_id=receipt_data.original_transaction_id, + status=purchase_status, + expires_date_ms=receipt_data.expires_date_ms, + purchase_date_ms=receipt_data.purchase_date_ms, + quantity=receipt_data.quantity, + receipt_data=purchase_history.receipt_data, + ) - await self._update_subscription_expiration( - subscription=subscription, expires_date_ms=receipt_data.expires_date_ms - ) + @staticmethod + async def get_subscriptions_to_renew(today: datetime) -> list[Subscription]: + return ( + await Subscription.filter(expires_date__lte=today + timedelta(days=1), status="ACTIVE") + .select_related("user") + .all() + ) - await self._create_purchase_history( - user_id=str(uuid.UUID(bytes=subscription.user.user_id)), # type: ignore - subscription=subscription, - product_code=receipt_data.product_code, - transaction_id=receipt_data.transaction_id, - original_transaction_id=receipt_data.original_transaction_id, - expires_date_ms=receipt_data.expires_date_ms, - purchase_date_ms=receipt_data.purchase_date_ms, - quantity=receipt_data.quantity, - receipt_data=purchase_history.receipt_data, - ) + @atomic() + async def process_subscriptions_renewal(self) -> None: + today = datetime.now(timezone.utc) + timedelta(hours=9) + subscriptions_to_renew = await self.get_subscriptions_to_renew(today) - return {"message": "Subscription renewal completed successfully"} + for subscription in subscriptions_to_renew: + await self.renew_subscription(subscription) @staticmethod - async def _update_subscription_expiration(subscription: Subscription, expires_date_ms: int) -> None: + async def _update_subscription_expiration( + subscription: Subscription, expires_date_ms: int, transaction_id: str + ) -> None: new_expires_date = datetime.fromtimestamp(expires_date_ms / 1000) subscription.expires_date = new_expires_date + subscription.current_transaction_id = transaction_id await subscription.save() @staticmethod @@ -231,24 +283,31 @@ async def _check_auto_renewal(pending_renewal_info: list[dict[str, Any]]) -> boo return True @staticmethod - async def expire_subscriptions() -> None: - today = datetime.now().date() - - expired_subscriptions = ( - await Subscription.filter( - status=SubscriptionStatus.ACTIVE.value, - expires_date__lt=today, - ) + async def get_expired_subscriptions(today: date) -> list[Subscription]: + return ( + await Subscription.filter(status=SubscriptionStatus.ACTIVE.value, expires_date__lt=today) .select_related("user") .all() ) + @staticmethod + async def update_subscription_status(expired_subscriptions: list[Subscription]) -> None: for subscription in expired_subscriptions: subscription.status = SubscriptionStatus.EXPIRED.value + await Subscription.bulk_update(expired_subscriptions, fields=["status"]) + @staticmethod + async def update_user_premium_status(expired_subscriptions: list[Subscription]) -> None: user_ids = [subscription.user.user_id for subscription in expired_subscriptions] - if user_ids: - if expired_subscriptions: - await Subscription.bulk_update(expired_subscriptions, fields=["status"]) await User.bulk_update_is_premium(user_ids) # type: ignore + + @atomic() + async def expire_subscriptions(self) -> None: + today = date.today() + + expired_subscriptions = await self.get_expired_subscriptions(today) + + if expired_subscriptions: + await self.update_subscription_status(expired_subscriptions) + await self.update_user_premium_status(expired_subscriptions) diff --git a/src/common/exceptions/error_code.py b/src/common/exceptions/error_code.py index a21be77..22afa5a 100644 --- a/src/common/exceptions/error_code.py +++ b/src/common/exceptions/error_code.py @@ -11,6 +11,8 @@ class ErrorCode(Enum): NO_INVENTORY_FOR_PRODUCT = (4041, "이 상품에 대한 재고가 없습니다.") PRODUCT_NOT_FOUND = (4042, "해당 상품을 찾을 수 없습니다.") + NO_VALID_RECEIPT = (4006, "유효한 영수증이 없습니다.") + def __init__(self, code: int, message: str) -> None: self._code = code self._message = message diff --git a/src/common/tasks/renew_subscription_task.py b/src/common/tasks/renew_subscription_task.py index 5dcc569..eb71fb9 100644 --- a/src/common/tasks/renew_subscription_task.py +++ b/src/common/tasks/renew_subscription_task.py @@ -3,7 +3,7 @@ async def renew_subscription_task() -> None: purchase_service = PurchaseService() - await purchase_service.renew_subscription() + await purchase_service.process_subscriptions_renewal() async def expire_subscription_task() -> None: From 7829a7b7bc1dacf01c8e32a29da80fdcd8c74da6 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 7 Dec 2024 15:26:36 +0900 Subject: [PATCH 44/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=AF=B8=EC=85=98?= =?UTF-8?q?=20api=20backgroud=20task=20->=20celery=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/missions/router.py | 20 ++++---------------- src/common/utils/scheduler.py | 2 +- src/core/configs/celery_settings.py | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/app/v2/missions/router.py b/src/app/v2/missions/router.py index 77a91e1..37564a8 100644 --- a/src/app/v2/missions/router.py +++ b/src/app/v2/missions/router.py @@ -1,22 +1,10 @@ -from fastapi import APIRouter, BackgroundTasks +from fastapi import APIRouter -from app.v2.missions.services.mission_service import MissionService +from core.configs.celery_settings import process_mission_in_background router = APIRouter(prefix="/mission", tags=["Mission"]) -async def process_mission_in_background(user_id: str) -> None: - mission_service = MissionService() - await mission_service.update_mission_progress(user_id) - - @router.get("") -async def mission_handler(user_id: str, background_tasks: BackgroundTasks) -> None: - background_tasks.add_task(process_mission_in_background, user_id) - - # await mission_service.validate_reward(reward_code="RW_FIRST_POST") - - # 3. 특정 시간대 미션도 체크하여 진행 - # if "time_check" in action_data: - # await update_mission_progress(user_id, action_data["time_check"], increment=1) - # pass +async def mission_handler(user_id: str) -> None: + process_mission_in_background.delay(user_id) diff --git a/src/common/utils/scheduler.py b/src/common/utils/scheduler.py index 6f1f6c1..81c5739 100644 --- a/src/common/utils/scheduler.py +++ b/src/common/utils/scheduler.py @@ -24,7 +24,7 @@ def start_scheduler() -> None: func=execute_daily_task, trigger="cron", hour=00, - minute=16, + minute=28, id="daily_task", replace_existing=True, ) diff --git a/src/core/configs/celery_settings.py b/src/core/configs/celery_settings.py index d1d70cb..b20b4b6 100644 --- a/src/core/configs/celery_settings.py +++ b/src/core/configs/celery_settings.py @@ -4,8 +4,9 @@ from celery import Celery from tortoise import Tortoise +from app.v2.missions.services.mission_service import MissionService from common.tasks.mission_task import mission_reset_task -from common.tasks.renew_subscription_task import renew_subscription_task, expire_subscription_task +from common.tasks.renew_subscription_task import expire_subscription_task, renew_subscription_task from core.database.database_settings import TORTOISE_ORM celery_app = Celery( @@ -24,6 +25,11 @@ ) +@celery_app.task +def process_mission_in_background(user_id: str) -> None: + asyncio.run(execute_async_mission_task(user_id=user_id)) + + @celery_app.task(name="daily_task") def daily_task() -> None: asyncio.run(execute_async_daily_task()) @@ -39,6 +45,15 @@ async def execute_async_daily_task() -> None: await close_celery_connections() +async def execute_async_mission_task(user_id: str) -> None: + await initialize_celery() + try: + mission_service = MissionService() + await mission_service.update_mission_progress(user_id=user_id) + finally: + await close_celery_connections() + + async def initialize_celery() -> None: logger = logging.getLogger(__name__) logger.info(f"Current path: 여기") From 2880f4a9a881146a1deb60e317665f5e4626910a Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sat, 7 Dec 2024 18:23:04 +0900 Subject: [PATCH 45/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=83=90=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EC=9B=8C=EC=BB=A4=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20=EB=AF=B8=EC=85=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/tasks/mission_task.py | 9 +++++++-- src/common/utils/scheduler.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/common/tasks/mission_task.py b/src/common/tasks/mission_task.py index 49c9ecc..778e768 100644 --- a/src/common/tasks/mission_task.py +++ b/src/common/tasks/mission_task.py @@ -1,7 +1,12 @@ +import asyncio + +from tortoise.expressions import Q + from app.v2.missions.models.mission import UserMission async def mission_reset_task() -> None: - await UserMission.filter(mission_code__in=["MS_LV_UP", "MS_DAILY_LIKE_3_PER_DAY", "MS_SINGLE_POST_2_5"]).update( - is_completed=False + await asyncio.gather( + UserMission.filter(Q(mission_code="MS_SINGLE_POST_2_5", progress_count__lt=3)).update(is_completed=False), + UserMission.filter(mission_code__in=["MS_LV_UP", "MS_DAILY_LIKE_3_PER_DAY"]).update(is_completed=False), ) diff --git a/src/common/utils/scheduler.py b/src/common/utils/scheduler.py index 81c5739..30c1f07 100644 --- a/src/common/utils/scheduler.py +++ b/src/common/utils/scheduler.py @@ -24,7 +24,7 @@ def start_scheduler() -> None: func=execute_daily_task, trigger="cron", hour=00, - minute=28, + minute=00, id="daily_task", replace_existing=True, ) From dbf81cb2f5362245134f77f39d67fa1cb3b169cd Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 01:26:03 +0900 Subject: [PATCH 46/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EB=8B=AC=EC=84=B1=20=EC=8B=9C=20=EB=B3=B4=EC=83=81=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/missions/services/mission_service.py | 113 ++++++++++++------ src/app/v2/notices/__init__.py | 0 src/app/v2/notices/dtos/__init__.py | 0 src/app/v2/notices/models/__init__.py | 0 src/app/v2/notices/models/notice.py | 52 ++++++++ src/app/v2/notices/services/__init__.py | 0 src/app/v2/notices/services/notice_service.py | 50 ++++++++ src/common/tasks/mission_task.py | 5 +- 8 files changed, 183 insertions(+), 37 deletions(-) create mode 100644 src/app/v2/notices/__init__.py create mode 100644 src/app/v2/notices/dtos/__init__.py create mode 100644 src/app/v2/notices/models/__init__.py create mode 100644 src/app/v2/notices/models/notice.py create mode 100644 src/app/v2/notices/services/__init__.py create mode 100644 src/app/v2/notices/services/notice_service.py diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index a16840a..9b75f29 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone, date from fastapi import HTTPException from tortoise.exceptions import DoesNotExist @@ -15,6 +15,7 @@ from app.v2.likes.models.like import Like from app.v2.missions.dtos.mission_dto import UserMissionDTO from app.v2.missions.models.mission import MissionInventory, UserMission +from app.v2.notices.services.notice_service import NoticeService from app.v2.users.services.user_service import UserService @@ -38,59 +39,79 @@ async def _update_user_mission_progress( is_completed=is_completed, ) + @atomic() async def update_mission_progress(self, user_id: str) -> None: user, user_missions, missions = await asyncio.gather( - UserService.get_user_info(user_id=user_id), self.get_user_missions(user_id=user_id), MissionInventory.all() + UserService.get_user_info(user_id=user_id), + self.get_user_missions(user_id=user_id), + MissionInventory.all(), ) cheese_manager_id: int = user["cheese_manager_id"] mission_dict = {mission.mission_code: mission for mission in missions} - level_up_mission = None - for user_mission in user_missions: - if user_mission.mission_code == "MS_LV_UP": - level_up_mission = user_mission - else: - await self._process_single_mission( - user_mission, mission_dict, cheese_manager_id=cheese_manager_id, user_id=user_id - ) + special_mission_codes = ["MS_LV_UP"] + + regular_missions = [mission for mission in user_missions if mission.mission_code not in special_mission_codes] + special_missions = { + mission.mission_code: mission for mission in user_missions if mission.mission_code in special_mission_codes + } - if level_up_mission: - await self._process_single_mission( - level_up_mission, mission_dict, cheese_manager_id=cheese_manager_id, user_id=user_id + results = await asyncio.gather( + *[self._process_mission(mission, mission_dict, cheese_manager_id, user_id) for mission in regular_missions] + ) + + total_cheese, total_exp = map(sum, zip(*results)) + + if "MS_LV_UP" in special_missions: + cheese, exp = await self._process_mission( + special_missions["MS_LV_UP"], mission_dict, cheese_manager_id, user_id ) + total_cheese += cheese + total_exp += exp + + if total_cheese == 0 and total_exp == 0: + return + print(total_exp, total_cheese) + await NoticeService.create_reward_notice( + user_id=user_id, reward_type="DAILY_MISSION", total_cheese=total_cheese, total_exp=total_exp + ) - async def _process_single_mission( + async def _process_mission( self, user_mission: UserMissionDTO, mission_dict: dict[str, MissionInventory], cheese_manager_id: int, user_id: str, - ) -> None: + ) -> tuple[int, int]: mission = mission_dict.get(user_mission.mission_code) if user_mission.is_completed or not mission: - return + return 0, 0 increment = await self.evaluate_mission_condition(user_id, user_mission.mission_code) user_mission.progress_count += increment - if user_mission.progress_count >= mission.target_count: + if user_mission.progress_count >= mission.target_count and not user_mission.is_completed: user_mission.is_completed = True - await self.reward_user_for_mission( + await self._update_user_mission_progress( user_id=user_id, - reward_code=mission.reward_code, - cheese_manager_id=cheese_manager_id, + mission_code=user_mission.mission_code, + new_progress_count=user_mission.progress_count, + is_completed=user_mission.is_completed, ) - - # 진행도 업데이트 - await self._update_user_mission_progress( - user_id=user_id, - mission_code=user_mission.mission_code, - new_progress_count=user_mission.progress_count, - is_completed=user_mission.is_completed, - ) + if user_mission.mission_code == "MS_DAILY_POST_GENERAL": + return 0, await self.reward_daily_post( + user_id=user_id, + ) + else: + return await self.reward_user_for_mission( + user_id=user_id, + reward_code=mission.reward_code, + cheese_manager_id=cheese_manager_id, + ) + return 0, 0 async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> int: if mission_code == "MS_BADGE_POST_FIRST" and await self.check_first_post(user_id): @@ -111,6 +132,8 @@ async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> i return 1 elif mission_code == f"MS_LV_UP" and await LevelService.level_up(user_id=user_id): return 1 + elif mission_code == "MS_DAILY_POST_GENERAL" and await self.check_daily_post(user_id): + return 1 return 0 @staticmethod @@ -172,16 +195,23 @@ async def check_three_likes_different_posts(user_id: str) -> bool: like_count: int = like_raw.get("unique_likes", 0) return like_count >= 3 + @staticmethod + async def check_daily_post(user_id: str) -> bool: + answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) + answer_date = answer.get("date") + return answer_date == date.today() + async def reward_user_for_mission( self, user_id: str, reward_code: str, cheese_manager_id: int, - ) -> None: + ) -> tuple[int, int]: item_inventory_rewards = await self.validate_reward(reward_code=reward_code) - - await self.process_reward( - item_inventory_rewards=item_inventory_rewards, user_id=user_id, cheese_manager_id=cheese_manager_id + return await self.process_reward( + item_inventory_rewards=item_inventory_rewards, + user_id=user_id, + cheese_manager_id=cheese_manager_id, ) @staticmethod @@ -199,14 +229,15 @@ async def validate_reward(reward_code: str): # type: ignore except DoesNotExist: raise HTTPException(status_code=404, detail="Reward not found.") - @classmethod - @atomic() + @staticmethod async def process_reward( - cls, item_inventory_rewards: list[ItemInventoryRewardInventory], user_id: str, cheese_manager_id: int, - ) -> None: + ) -> tuple[int, int]: + total_cheese = 0 + total_exp = 0 + for item_inventory_reward in item_inventory_rewards: item: ItemInventory = await item_inventory_reward.item_inventory quantity = item_inventory_reward.quantity @@ -218,8 +249,18 @@ async def process_reward( for _ in range(quantity): await ColorService.add_color(user_id=user_id, color_code=item.item_code) elif item.item_category == "CHEESE": + total_cheese += quantity await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) elif item.item_category == "POINT": + total_exp += quantity await LevelService.add_exp(user_id=user_id, exp=quantity) else: raise ValueError(f"Invalid item category for reward: {item.item_category}") + + return total_cheese, total_exp + + @staticmethod + async def reward_daily_post(user_id: str) -> int: + exp = await AnswerService.calculate_consecutive_answer_points(user_id=user_id) + await LevelService.add_exp(user_id=user_id, exp=exp) + return exp diff --git a/src/app/v2/notices/__init__.py b/src/app/v2/notices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/notices/dtos/__init__.py b/src/app/v2/notices/dtos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/notices/models/__init__.py b/src/app/v2/notices/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/notices/models/notice.py b/src/app/v2/notices/models/notice.py new file mode 100644 index 0000000..a399a82 --- /dev/null +++ b/src/app/v2/notices/models/notice.py @@ -0,0 +1,52 @@ +from tortoise import fields +from tortoise.fields import ForeignKeyRelation +from tortoise.models import Model + +from app.v2.users.models.user import User +from common.utils.query_executor import QueryExecutor + + +class Notice(Model): + notice_id = fields.BigIntField(pk=True) + title = fields.CharField(max_length=255, null=False) + content = fields.TextField(null=True) + is_read = fields.BooleanField(default=False) + created_at = fields.DatetimeField(auto_now_add=True) + link = fields.CharField(max_length=255, null=True) + is_internal = fields.BooleanField(default=False) + answer_id = fields.BigIntField(null=True) + date = fields.DateField(null=True) + reward_type = fields.CharField(max_length=255, null=True) + + user: ForeignKeyRelation[User] = fields.ForeignKeyField( + "models.User", related_name="notices", null=True, on_delete=fields.SET_NULL + ) + + class Meta: + table = "notice" + + @classmethod + async def create_notice( + cls, + title: str, + content: str, + user_id: int, + link: str = None, + is_read: bool = False, + is_internal: bool = False, + answer_id: int = None, + date: str = None, + reward_type: str = None, + ) -> None: + query = """ + INSERT INTO notice ( + title, content, user_id, link, is_internal, is_read, answer_id, date, reward_type, + created_at + ) + VALUES ( + %s, %s, UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, %s, %s, %s, NOW() + ); + """ + values = (title, content, user_id, link, is_internal, is_read, answer_id, date, reward_type) + + await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/notices/services/__init__.py b/src/app/v2/notices/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/v2/notices/services/notice_service.py b/src/app/v2/notices/services/notice_service.py new file mode 100644 index 0000000..ff8e035 --- /dev/null +++ b/src/app/v2/notices/services/notice_service.py @@ -0,0 +1,50 @@ +from app.v2.notices.models.notice import Notice + + +class NoticeService: + + @classmethod + async def create_notice( + cls, + user_id: str, + title: str, + reward_type: str, + content: str, + ) -> Notice: + return await Notice.create_notice( + title=title, + content=content, + user_id=user_id, + reward_type=reward_type, + is_internal=True, + is_read=False, + ) + + @classmethod + async def create_reward_notice( + cls, + user_id: str, + reward_type: str, + total_cheese: int, + total_exp: int, + ) -> Notice: + if total_cheese == 0 and total_exp == 0: + return + + return await cls.create_notice( + user_id=user_id, + title="보상을 받았어요!", + content=cls.create_reward_message(total_cheese=total_cheese, total_exp=total_exp), + reward_type=reward_type, + ) + + @classmethod + def create_reward_message(cls, total_cheese: int, total_exp: int) -> str: + if total_cheese > 0 and total_exp > 0: + return f"치즈 {total_cheese}개와 경험치 {total_exp}P를 받았어요!" + elif total_cheese > 0: + return f"치즈 {total_cheese}개를 받았어요!" + elif total_exp > 0: + return f"경험치 {total_exp}P를 받았어요!" + else: + return "보상을 받지 못했어요!" diff --git a/src/common/tasks/mission_task.py b/src/common/tasks/mission_task.py index 778e768..f4844db 100644 --- a/src/common/tasks/mission_task.py +++ b/src/common/tasks/mission_task.py @@ -8,5 +8,8 @@ async def mission_reset_task() -> None: await asyncio.gather( UserMission.filter(Q(mission_code="MS_SINGLE_POST_2_5", progress_count__lt=3)).update(is_completed=False), - UserMission.filter(mission_code__in=["MS_LV_UP", "MS_DAILY_LIKE_3_PER_DAY"]).update(is_completed=False), + UserMission.filter(mission_code__in=["MS_LV_UP", "MS_DAILY_LIKE_3_PER_DAY", "MS_DAILY_POST_GENERAL"]).update( + is_completed=False, progress_count=0 + ), ) + # 좋아요, 일반은 progress count도 수정 From ab4ec1c2aa80b8e4ff2a756cd40054b7eb046207 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 15:01:27 +0900 Subject: [PATCH 47/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=20=EA=B5=AC=EB=8F=85=20api=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/missions/services/mission_service.py | 2 +- src/app/v2/purchases/dtos/purchase_dto.py | 27 +++++++++++++++++++ src/app/v2/purchases/router.py | 12 +++------ .../v2/purchases/services/purchase_service.py | 21 +++++---------- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 9b75f29..3c19ff6 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -73,7 +73,7 @@ async def update_mission_progress(self, user_id: str) -> None: if total_cheese == 0 and total_exp == 0: return - print(total_exp, total_cheese) + await NoticeService.create_reward_notice( user_id=user_id, reward_type="DAILY_MISSION", total_cheese=total_cheese, total_exp=total_exp ) diff --git a/src/app/v2/purchases/dtos/purchase_dto.py b/src/app/v2/purchases/dtos/purchase_dto.py index 35d2db0..e9e5800 100644 --- a/src/app/v2/purchases/dtos/purchase_dto.py +++ b/src/app/v2/purchases/dtos/purchase_dto.py @@ -3,6 +3,7 @@ from pydantic import BaseModel from app.v2.purchases.models.purchase_status import purchase_mapping +from common.base_models.base_dtos.base_response import BaseResponseDTO class ReceiptInfoDTO(BaseModel): @@ -33,3 +34,29 @@ def build(cls, latest_receipt_info: dict[str, Any]) -> "ReceiptInfoDTO": quantity=quantity, cancellation_date_ms=cancellation_date_ms, ) + + +class PurchaseDTO(BaseModel): + productCode: str + isPremium: bool + + @classmethod + def build(cls, product_code: str, is_premium: bool) -> "PurchaseDTO": + return cls( + productCode=product_code, + isPremium=is_premium, + ) + + +class PurchaseResponseDTO(BaseModel): + message: str + data: PurchaseDTO + code: int + + @classmethod + def build(cls, is_premium: bool, product_code: str) -> "PurchaseResponseDTO": + return cls( + code=200, + message="Purchase successful.", + data=PurchaseDTO.build(product_code=product_code, is_premium=is_premium), + ) diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 1e36b7f..7addd4a 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, status +from app.v2.purchases.dtos.purchase_dto import PurchaseResponseDTO from app.v2.purchases.dtos.requests import ReceiptRequestDTO from app.v2.purchases.services.purchase_service import PurchaseService @@ -11,20 +12,15 @@ @router.post( "/apple", status_code=status.HTTP_200_OK, - response_model=dict[str, Any], + response_model=PurchaseResponseDTO, summary="apple 결제 api", description="apple 결제 api", ) async def process_receipt( receipt: ReceiptRequestDTO, purchase_service: PurchaseService = Depends(), -) -> dict[str, Any]: - await purchase_service.process_apple_purchase(receipt_data=receipt.receiptData, user_id=receipt.user_id) - return { - "code": status.HTTP_200_OK, - "message": "Receipt verified successfully", - "data": True, - } +) -> PurchaseResponseDTO: + return await purchase_service.process_apple_purchase(receipt_data=receipt.receiptData, user_id=receipt.user_id) @router.post("/receipt-test") diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index cceee82..c9aae25 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -8,7 +8,7 @@ from tortoise.transactions import atomic from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory -from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO +from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO, PurchaseDTO, PurchaseResponseDTO from app.v2.purchases.models.purchase_history import PurchaseHistory, Subscription from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus from app.v2.users.models.user import User @@ -20,7 +20,7 @@ class PurchaseService: @atomic() - async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: + async def process_apple_purchase(self, receipt_data: str, user_id: str) -> str: response = await self._validate_apple_receipt(receipt_data=receipt_data) latest_receipt_info = self._extract_latest_receipt_info(response) @@ -47,19 +47,6 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: if subscription is None: raise DoesNotExist("Subscription not found") - await self._create_purchase_history( - user_id=user_id, - subscription=subscription, - product_code=receipt_info.product_code, - transaction_id=receipt_info.transaction_id, - original_transaction_id=receipt_info.original_transaction_id, - status=purchase_status, - expires_date_ms=receipt_info.expires_date_ms, - purchase_date_ms=receipt_info.purchase_date_ms, - quantity=receipt_info.quantity, - receipt_data=receipt_data, - ) - item_inventory_products = await self._validate_purchase(product_code=receipt_info.product_code) await self._process_purchase( @@ -68,6 +55,10 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> None: status=purchase_status, ) + user = await UserService.get_user_profile(user_id=user_id) + + return PurchaseResponseDTO.build(is_premium=user.is_premium, product_code=receipt_info.product_code) + @staticmethod def _extract_latest_receipt_info(response: dict[str, Any]) -> dict[str, Any] | None: latest_receipt_info = response.get("latest_receipt_info") From 6fa6fb05fc1747ce577c9718543b177ef46097b5 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 15:17:40 +0900 Subject: [PATCH 48/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/dtos/purchase_dto.py | 3 +++ src/app/v2/purchases/services/purchase_service.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/v2/purchases/dtos/purchase_dto.py b/src/app/v2/purchases/dtos/purchase_dto.py index e9e5800..a03b463 100644 --- a/src/app/v2/purchases/dtos/purchase_dto.py +++ b/src/app/v2/purchases/dtos/purchase_dto.py @@ -12,6 +12,7 @@ class ReceiptInfoDTO(BaseModel): expires_date_ms: int purchase_date_ms: int product_code: str + product_code_two: str quantity: int cancellation_date_ms: Optional[int] = None @@ -22,6 +23,7 @@ def build(cls, latest_receipt_info: dict[str, Any]) -> "ReceiptInfoDTO": expires_date_ms = int(latest_receipt_info.get("expires_date_ms", 0)) purchase_date_ms = int(latest_receipt_info.get("purchase_date_ms", 0)) product_code = purchase_mapping.get(latest_receipt_info["product_id"], latest_receipt_info["product_id"]) + product_code_two = latest_receipt_info["product_id"] quantity = int(latest_receipt_info.get("quantity", 1)) cancellation_date_ms = latest_receipt_info.get("cancellation_date_ms") # 환불일 (밀리초) @@ -31,6 +33,7 @@ def build(cls, latest_receipt_info: dict[str, Any]) -> "ReceiptInfoDTO": expires_date_ms=expires_date_ms, purchase_date_ms=purchase_date_ms, product_code=product_code, + product_code_two=product_code_two, quantity=quantity, cancellation_date_ms=cancellation_date_ms, ) diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index c9aae25..dcbb6f5 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -57,7 +57,7 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> str: user = await UserService.get_user_profile(user_id=user_id) - return PurchaseResponseDTO.build(is_premium=user.is_premium, product_code=receipt_info.product_code) + return PurchaseResponseDTO.build(is_premium=user.is_premium, product_code=receipt_info.product_code_two) @staticmethod def _extract_latest_receipt_info(response: dict[str, Any]) -> dict[str, Any] | None: From 4568c1831d922f547259b68bb150e1aa804c5228 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 15:34:34 +0900 Subject: [PATCH 49/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20mypage=20api=20allo?= =?UTF-8?q?w=20notification=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/mobiles/router.py | 1 + src/app/v2/users/dtos/user_dto.py | 3 +++ src/app/v2/users/dtos/user_profile_dto.py | 3 +++ src/app/v2/users/querys/user_query.py | 3 ++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 746189c..acabb0e 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -80,6 +80,7 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: badgeCount=badge_count, answerCount=answer_count, premium=user.is_premium, # type: ignore + allow_notification=user.allow_notification, ), level=level, ) diff --git a/src/app/v2/users/dtos/user_dto.py b/src/app/v2/users/dtos/user_dto.py index cc3ac20..4da00de 100644 --- a/src/app/v2/users/dtos/user_dto.py +++ b/src/app/v2/users/dtos/user_dto.py @@ -12,10 +12,12 @@ class UserDTO(BaseModel): cheese_manager_id: Optional[int] = None teller_card_id: Optional[int] = None level_id: Optional[int] = None + allow_notification: Optional[bool] = None @classmethod def build(cls, user: dict[str, Any]) -> "UserDTO": is_premium = user.get("is_premium") != b"\x00" + allow_notification = user.get("allow_notification") != b"\x00" return cls( user_id=user.get("user_id", None), nickname=user.get("nickname", None), @@ -25,4 +27,5 @@ def build(cls, user: dict[str, Any]) -> "UserDTO": cheese_manager_id=user.get("cheese_manager_id", None), teller_card_id=user.get("teller_card_id", None), level_id=user.get("level_id", None), + allow_notification=allow_notification, ) diff --git a/src/app/v2/users/dtos/user_profile_dto.py b/src/app/v2/users/dtos/user_profile_dto.py index 65dec0f..0aca6e3 100644 --- a/src/app/v2/users/dtos/user_profile_dto.py +++ b/src/app/v2/users/dtos/user_profile_dto.py @@ -8,6 +8,7 @@ class UserProfileDTO(BaseModel): badgeCount: int answerCount: int premium: bool + allowNotification: bool @classmethod def builder( @@ -18,6 +19,7 @@ def builder( badgeCount: int, answerCount: int, premium: bool, + allow_notification: bool, ) -> "UserProfileDTO": return cls( nickname=nickname, @@ -26,4 +28,5 @@ def builder( badgeCount=badgeCount, answerCount=answerCount, premium=premium, + allowNotification=allow_notification, ) diff --git a/src/app/v2/users/querys/user_query.py b/src/app/v2/users/querys/user_query.py index df963e3..9966e95 100644 --- a/src/app/v2/users/querys/user_query.py +++ b/src/app/v2/users/querys/user_query.py @@ -7,7 +7,8 @@ SELECT u.nickname, u.is_premium, - u.cheese_manager_id + u.cheese_manager_id, + u.allow_notification FROM user u WHERE {USER_ID_QUERY} """ From 16a6f347d4843c2b674dbeda1f007a6019fd65f7 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 16:43:46 +0900 Subject: [PATCH 50/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=20api=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/services/purchase_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index dcbb6f5..1ec6b73 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -52,7 +52,7 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> str: await self._process_purchase( user_id=user_id, item_inventory_products=item_inventory_products, - status=purchase_status, + status=subscription_status, ) user = await UserService.get_user_profile(user_id=user_id) From 12fdb214dd596f5f317bee8d2f45baa921341767 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 20:56:00 +0900 Subject: [PATCH 51/60] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=20=EC=98=81=EC=88=98=EC=A6=9D=20=EA=B5=AC=EB=8F=85=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/purchases/services/purchase_service.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 1ec6b73..7b2bbf8 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -1,3 +1,4 @@ +import time import uuid from datetime import date, datetime, timedelta, timezone from typing import Any, Optional, cast @@ -8,7 +9,7 @@ from tortoise.transactions import atomic from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory -from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO, PurchaseDTO, PurchaseResponseDTO +from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO, PurchaseResponseDTO from app.v2.purchases.models.purchase_history import PurchaseHistory, Subscription from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus from app.v2.users.models.user import User @@ -20,7 +21,7 @@ class PurchaseService: @atomic() - async def process_apple_purchase(self, receipt_data: str, user_id: str) -> str: + async def process_apple_purchase(self, receipt_data: str, user_id: str) -> PurchaseResponseDTO: response = await self._validate_apple_receipt(receipt_data=receipt_data) latest_receipt_info = self._extract_latest_receipt_info(response) @@ -30,9 +31,7 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> str: receipt_info = await self._parse_receipt_info(latest_receipt_info) - subscription_status = self.get_subscription_status(receipt_info.cancellation_date_ms) - - purchase_status = self.get_purchase_status(receipt_info.cancellation_date_ms) + subscription_status = self.get_subscription_status(receipt_info) await self._create_or_update_subscription( user_id=user_id, @@ -110,9 +109,15 @@ async def _create_or_update_subscription( ) @staticmethod - def get_subscription_status(cancellation_date_ms: Optional[int]) -> str: - if cancellation_date_ms: + def get_subscription_status(receipt: ReceiptInfoDTO) -> str: + current_time_ms = int(time.time() * 1000) + + if receipt.cancellation_date_ms: return SubscriptionStatus.CANCELED.value + + if receipt.expires_date_ms < current_time_ms: + return SubscriptionStatus.EXPIRED.value + return SubscriptionStatus.ACTIVE.value @staticmethod @@ -188,7 +193,7 @@ async def _process_purchase( if item.item_category == "SUBSCRIPTION": if status == SubscriptionStatus.ACTIVE.value: await UserService.set_is_premium(user_id=user_id, is_premium=True) - if status == SubscriptionStatus.CANCELED.value: + if status == SubscriptionStatus.CANCELED.value or status == SubscriptionStatus.EXPIRED.value: await UserService.set_is_premium(user_id=user_id, is_premium=False) # elif item.item_category == "CHEESE": # await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=quantity) From d85aea636d743c5a1bad0a8675f5ab1903bc1c07 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 21:18:55 +0900 Subject: [PATCH 52/60] =?UTF-8?q?=F0=9F=90=9Bfix:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EB=B0=B1=EA=B7=B8=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/missions/services/mission_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 3c19ff6..31c6dda 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -198,7 +198,12 @@ async def check_three_likes_different_posts(user_id: str) -> bool: @staticmethod async def check_daily_post(user_id: str) -> bool: answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) + + if answer == 0: + return False + answer_date = answer.get("date") + return answer_date == date.today() async def reward_user_for_mission( From a7c03201c269a5b4c9bb9a7a553a27a01ac4f2d8 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 21:32:33 +0900 Subject: [PATCH 53/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=98=81=EC=88=98?= =?UTF-8?q?=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20=ED=99=95=EC=9D=B8=EC=9A=A9=20?= =?UTF-8?q?api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/purchases/router.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/v2/purchases/router.py b/src/app/v2/purchases/router.py index 7addd4a..7cd7ba6 100644 --- a/src/app/v2/purchases/router.py +++ b/src/app/v2/purchases/router.py @@ -28,7 +28,12 @@ async def receipt_test( receipt: ReceiptRequestDTO, purchase_service: PurchaseService = Depends(), ) -> dict[str, Any]: - return await purchase_service._validate_apple_receipt(receipt_data=receipt.receiptData) + data = await purchase_service._validate_apple_receipt(receipt_data=receipt.receiptData) + return { + "code": 200, + "data": data, + "message": "정상처리되었습니다", + } @router.get("/renew-test") From 36a97e8648861c8832d1e8f24c340dcfc6339b19 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Sun, 8 Dec 2024 22:05:17 +0900 Subject: [PATCH 54/60] =?UTF-8?q?=E2=9C=A8=20fix:=20=EA=B5=AC=EB=8F=85=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=B2=98=EB=A6=AC=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A4=84=EB=9F=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/configs/celery_settings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/configs/celery_settings.py b/src/core/configs/celery_settings.py index b20b4b6..cf66aaa 100644 --- a/src/core/configs/celery_settings.py +++ b/src/core/configs/celery_settings.py @@ -6,7 +6,6 @@ from app.v2.missions.services.mission_service import MissionService from common.tasks.mission_task import mission_reset_task -from common.tasks.renew_subscription_task import expire_subscription_task, renew_subscription_task from core.database.database_settings import TORTOISE_ORM celery_app = Celery( @@ -39,8 +38,6 @@ async def execute_async_daily_task() -> None: await initialize_celery() try: await mission_reset_task() - await renew_subscription_task() - await expire_subscription_task() finally: await close_celery_connections() From fe06d4fe229437c4b47ffbcb010d8bfb545defff Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Wed, 11 Dec 2024 02:54:00 +0900 Subject: [PATCH 55/60] =?UTF-8?q?=E2=9C=A8=20fix:=20=EA=B2=BD=ED=97=98?= =?UTF-8?q?=EC=B9=98=20=EC=A7=80=EA=B8=89=20=EB=B0=8F=20=EB=B3=B4=EC=83=81?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/services/answer_service.py | 7 + src/app/v2/badges/models/badge.py | 4 + src/app/v2/badges/services/badge_service.py | 6 +- .../v2/missions/services/mission_service.py | 176 ++++++++++++------ src/app/v2/notices/services/notice_service.py | 32 +++- 5 files changed, 164 insertions(+), 61 deletions(-) diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index 0db0a7f..9974004 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -36,3 +36,10 @@ async def get_answer_record(cls, user_id: str) -> int: @classmethod async def calculate_consecutive_answer_points(cls, user_id: str) -> int: return min(await cls.get_answer_record(user_id=user_id), 10) + + @classmethod + async def get_most_recent_answer(cls, user_id: str): + answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) + if answer == 0: + return {} + return answer diff --git a/src/app/v2/badges/models/badge.py b/src/app/v2/badges/models/badge.py index bc8170a..3de92a8 100644 --- a/src/app/v2/badges/models/badge.py +++ b/src/app/v2/badges/models/badge.py @@ -55,3 +55,7 @@ class BadgeInventory(Model): class Meta: table = "badge_inventory" + + @property + def badge_full_name(self) -> str: + return f"{self.badge_middle_name} {self.badge_name}" diff --git a/src/app/v2/badges/services/badge_service.py b/src/app/v2/badges/services/badge_service.py index d8f716b..e611757 100644 --- a/src/app/v2/badges/services/badge_service.py +++ b/src/app/v2/badges/services/badge_service.py @@ -1,5 +1,5 @@ from app.v2.badges.dtos.badge_dto import BadgeCodeDTO, BadgeDTO -from app.v2.badges.models.badge import Badge +from app.v2.badges.models.badge import Badge, BadgeInventory class BadgeService: @@ -23,3 +23,7 @@ async def get_badge_count(cls, user_id: str) -> int: if badge_count_raw is None: return 0 return int(badge_count_raw.get("badge_count", 0)) + + @classmethod + async def get_badge_info_by_badge_code(cls, badge_code: str) -> BadgeInventory: + return await BadgeInventory.get(badge_code=badge_code) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 31c6dda..700dc54 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -51,32 +51,21 @@ async def update_mission_progress(self, user_id: str) -> None: cheese_manager_id: int = user["cheese_manager_id"] mission_dict = {mission.mission_code: mission for mission in missions} - special_mission_codes = ["MS_LV_UP"] + badge_missions, lv_up_mission, daily_missions = await self._classify_missions(user_missions) - regular_missions = [mission for mission in user_missions if mission.mission_code not in special_mission_codes] - special_missions = { - mission.mission_code: mission for mission in user_missions if mission.mission_code in special_mission_codes - } - - results = await asyncio.gather( - *[self._process_mission(mission, mission_dict, cheese_manager_id, user_id) for mission in regular_missions] + await asyncio.gather( + *[self._process_mission(mission, mission_dict, cheese_manager_id, user_id) for mission in badge_missions], + *[self._process_mission(mission, mission_dict, cheese_manager_id, user_id) for mission in daily_missions], ) - total_cheese, total_exp = map(sum, zip(*results)) - - if "MS_LV_UP" in special_missions: - cheese, exp = await self._process_mission( - special_missions["MS_LV_UP"], mission_dict, cheese_manager_id, user_id - ) - total_cheese += cheese - total_exp += exp + if lv_up_mission[0]: + await self._process_mission(lv_up_mission[0], mission_dict, cheese_manager_id, user_id) - if total_cheese == 0 and total_exp == 0: - return - - await NoticeService.create_reward_notice( - user_id=user_id, reward_type="DAILY_MISSION", total_cheese=total_cheese, total_exp=total_exp - ) + async def _classify_missions(self, user_missions): + badge_missions = [mission for mission in user_missions if mission.mission_code.startswith("MS_BADGE")] + lv_up_mission = [mission for mission in user_missions if mission.mission_code == "MS_LV_UP"] + daily_missions = [mission for mission in user_missions if mission.mission_code.startswith("MS_DAILY")] + return badge_missions, lv_up_mission, daily_missions async def _process_mission( self, @@ -84,7 +73,7 @@ async def _process_mission( mission_dict: dict[str, MissionInventory], cheese_manager_id: int, user_id: str, - ) -> tuple[int, int]: + ) -> None: mission = mission_dict.get(user_mission.mission_code) if user_mission.is_completed or not mission: @@ -102,22 +91,17 @@ async def _process_mission( is_completed=user_mission.is_completed, ) if user_mission.mission_code == "MS_DAILY_POST_GENERAL": - return 0, await self.reward_daily_post( - user_id=user_id, - ) + await self.reward_daily_post(user_id=user_id, cheese_manager_id=cheese_manager_id) else: return await self.reward_user_for_mission( user_id=user_id, reward_code=mission.reward_code, cheese_manager_id=cheese_manager_id, ) - return 0, 0 async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> int: if mission_code == "MS_BADGE_POST_FIRST" and await self.check_first_post(user_id): return 1 - elif mission_code == "MS_SINGLE_POST_2_5" and await self.check_post_count_range(user_id, 2, 5): - return 1 elif mission_code == "MS_BADGE_POST_280_CHAR" and await self.check_long_answer(user_id): return 1 elif mission_code == "MS_BADGE_POST_CONSECUTIVE_7" and await self.check_consecutive_days(user_id): @@ -138,25 +122,19 @@ async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> i @staticmethod async def check_first_post(user_id: str) -> bool: - post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) - post_count: int = post_count_raw.get("answer_count", 0) - return post_count > 0 + return await AnswerService.get_answer_count(user_id=user_id) > 0 @staticmethod - async def check_post_count_range(user_id: str, min_count: int, max_count: int) -> bool: - post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) - post_count: int = post_count_raw.get("answer_count", 0) - return min_count <= post_count <= max_count + async def get_answer_count(user_id: str) -> int: + return await AnswerService.get_answer_count(user_id=user_id) @staticmethod - async def check_post_count_min(user_id: str, min_count: int) -> bool: - post_count_raw = await Answer.get_answer_count_by_user_id(user_id=user_id) - post_count: int = post_count_raw.get("answer_count", 0) - return post_count >= min_count + async def check_post_count_range(answer_count: int, min_count: int, max_count: int) -> bool: + return min_count <= answer_count <= max_count @staticmethod async def check_long_answer(user_id: str) -> bool: - recent_answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) + recent_answer = await AnswerService.get_most_recent_answer(user_id=user_id) return len(recent_answer["content"]) >= 280 if recent_answer else False @staticmethod @@ -166,7 +144,7 @@ async def check_consecutive_days(user_id: str) -> bool: @staticmethod async def check_early_morning_posts(user_id: str) -> bool: - recent_answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) + recent_answer = await AnswerService.get_most_recent_answer(user_id=user_id) if recent_answer: answer_time = recent_answer.get("created_time") return 0 <= answer_time.hour <= 5 if isinstance(answer_time, datetime) else False @@ -197,10 +175,7 @@ async def check_three_likes_different_posts(user_id: str) -> bool: @staticmethod async def check_daily_post(user_id: str) -> bool: - answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) - - if answer == 0: - return False + answer = await AnswerService.get_most_recent_answer(user_id=user_id) answer_date = answer.get("date") @@ -234,14 +209,15 @@ async def validate_reward(reward_code: str): # type: ignore except DoesNotExist: raise HTTPException(status_code=404, detail="Reward not found.") - @staticmethod async def process_reward( + self, item_inventory_rewards: list[ItemInventoryRewardInventory], user_id: str, cheese_manager_id: int, - ) -> tuple[int, int]: + ) -> None: total_cheese = 0 total_exp = 0 + badge_info = [] for item_inventory_reward in item_inventory_rewards: item: ItemInventory = await item_inventory_reward.item_inventory @@ -250,6 +226,9 @@ async def process_reward( if item.item_category == "BADGE": for _ in range(quantity): await BadgeService.add_badge(user_id=user_id, badge_code=item.item_code) + badge = await BadgeService.get_badge_info_by_badge_code(badge_code=item.item_code) + badge_info.append(badge.badge_full_name) + elif item.item_category == "COLOR": for _ in range(quantity): await ColorService.add_color(user_id=user_id, color_code=item.item_code) @@ -262,10 +241,103 @@ async def process_reward( else: raise ValueError(f"Invalid item category for reward: {item.item_category}") - return total_cheese, total_exp + if badge_info: + await self._create_reward_notice( + user_id=user_id, + reward_type="BADGE_MISSION", + total_exp=total_exp, + total_cheese=total_cheese, + badge_info=badge_info[0], + ) + return + + if total_cheese > 0 or total_exp > 0: + await self._create_reward_notice( + user_id=user_id, + reward_type="DAILY_MISSION", + total_exp=total_exp, + total_cheese=total_cheese, + badge_info=None, + ) + + async def reward_daily_post(self, user_id: str, cheese_manager_id: int) -> None: + # 1. 경험치 및 치즈 계산 + exp, cheese, consecutive_date = await self._calculate_exp_and_cheese(user_id) + + # 2. 경험치와 치즈 추가 + await self._add_exp_and_cheese(user_id, cheese_manager_id, exp, cheese) + + # 3. 보상 알림 생성 + await self._create_reward_notice( + user_id=user_id, + reward_type="DAILY_MISSION", + total_exp=exp, + total_cheese=cheese, + badge_info=None, + ) + + async def _calculate_exp_and_cheese(self, user_id: str) -> tuple[int, int, int]: + # 1. 연속 답변 포인트 계산 + consecutive_date = await AnswerService.calculate_consecutive_answer_points(user_id=user_id) + + # 2. 경험치 계산 + exp = await self._calculate_exp(user_id, consecutive_date) + + # 3. 치즈 계산 + cheese = await self._calculate_cheese(consecutive_date) + + return exp, cheese, consecutive_date + + async def _calculate_exp(self, user_id: str, consecutive_date: int) -> int: + exp = 0 + + answer_count = await self.get_answer_count(user_id=user_id) + + if answer_count == 1: + exp += 10 + elif await self.check_post_count_range(answer_count, 2, 5): + exp += 5 + + exp += consecutive_date + + return exp @staticmethod - async def reward_daily_post(user_id: str) -> int: - exp = await AnswerService.calculate_consecutive_answer_points(user_id=user_id) + async def _calculate_cheese(consecutive_date: int) -> int: + cheese = 0 + + if consecutive_date == 0: + cheese = 0 + elif 2 <= consecutive_date <= 5: + cheese = 1 + elif 5 <= consecutive_date <= 8: + cheese = 2 + elif consecutive_date >= 9: + cheese = 3 + + return cheese + + @staticmethod + async def _add_exp_and_cheese(user_id: str, cheese_manager_id: int, exp: int, cheese: int) -> None: + # 경험치 추가 await LevelService.add_exp(user_id=user_id, exp=exp) - return exp + + # 치즈 추가 + await CheeseService.add_cheese(cheese_manager_id=cheese_manager_id, amount=cheese) + + @staticmethod + async def _create_reward_notice( + user_id: str, reward_type: str, total_exp: int, total_cheese: int, badge_info: str + ) -> None: + await NoticeService.create_reward_notice( + user_id=user_id, + reward_type=reward_type, + total_cheese=total_cheese, + total_exp=total_exp, + badge_info=badge_info, + ) + + async def _create_level_up_reward_notice(self, user_id: str, total_exp: int, total_cheese: int) -> None: + await NoticeService.create_reward_notice( + user_id=user_id, reward_type="LEVEL_UP", total_cheese=total_cheese, total_exp=total_exp + ) diff --git a/src/app/v2/notices/services/notice_service.py b/src/app/v2/notices/services/notice_service.py index ff8e035..46e39fc 100644 --- a/src/app/v2/notices/services/notice_service.py +++ b/src/app/v2/notices/services/notice_service.py @@ -25,26 +25,42 @@ async def create_reward_notice( cls, user_id: str, reward_type: str, - total_cheese: int, - total_exp: int, + total_cheese: int = 0, + total_exp: int = 0, + badge_info: str = None, ) -> Notice: - if total_cheese == 0 and total_exp == 0: + if not badge_info and total_cheese == 0 and total_exp == 0: return + # 1. 제목 생성 + title = cls.create_title(badge_info) + + # 2. 메시지 생성 + content = cls.create_reward_message(total_cheese, total_exp, badge_info) + + # 3. 알림 생성 return await cls.create_notice( user_id=user_id, - title="보상을 받았어요!", - content=cls.create_reward_message(total_cheese=total_cheese, total_exp=total_exp), + title=title, + content=content, reward_type=reward_type, ) @classmethod - def create_reward_message(cls, total_cheese: int, total_exp: int) -> str: + def create_title(cls, badge: str = None) -> str: + if badge: + return badge + return "보상을 받았어요!" + + @classmethod + def create_reward_message(cls, total_cheese: int, total_exp: int, badge: str = None) -> str: + if badge: + return f"선물로 치즈 {total_cheese}개를 드릴게요!" if total_cheese > 0 else "뱃지 획득을 축하드립니다!" + if total_cheese > 0 and total_exp > 0: return f"치즈 {total_cheese}개와 경험치 {total_exp}P를 받았어요!" elif total_cheese > 0: return f"치즈 {total_cheese}개를 받았어요!" elif total_exp > 0: return f"경험치 {total_exp}P를 받았어요!" - else: - return "보상을 받지 못했어요!" + return "보상을 받지 못했어요!" From 343628151104dc97d63490ef20c852d374667957 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Wed, 11 Dec 2024 12:20:18 +0900 Subject: [PATCH 56/60] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=EC=97=85=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/answers/services/answer_service.py | 3 +- src/app/v2/missions/dtos/reward_dto.py | 17 ++ .../v2/missions/services/mission_service.py | 151 ++++++++++++------ src/app/v2/mobiles/router.py | 2 +- src/app/v2/notices/models/notice.py | 18 ++- src/app/v2/notices/services/notice_service.py | 46 ++++-- .../v2/purchases/services/purchase_service.py | 4 +- 7 files changed, 173 insertions(+), 68 deletions(-) create mode 100644 src/app/v2/missions/dtos/reward_dto.py diff --git a/src/app/v2/answers/services/answer_service.py b/src/app/v2/answers/services/answer_service.py index 9974004..9b7f9c8 100644 --- a/src/app/v2/answers/services/answer_service.py +++ b/src/app/v2/answers/services/answer_service.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import Any from app.v2.answers.models.answer import Answer @@ -38,7 +39,7 @@ async def calculate_consecutive_answer_points(cls, user_id: str) -> int: return min(await cls.get_answer_record(user_id=user_id), 10) @classmethod - async def get_most_recent_answer(cls, user_id: str): + async def get_most_recent_answer(cls, user_id: str) -> Any: answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id) if answer == 0: return {} diff --git a/src/app/v2/missions/dtos/reward_dto.py b/src/app/v2/missions/dtos/reward_dto.py new file mode 100644 index 0000000..1a26c4a --- /dev/null +++ b/src/app/v2/missions/dtos/reward_dto.py @@ -0,0 +1,17 @@ +from typing import Any, Optional + +from pydantic import BaseModel + + +class RewardDTO(BaseModel): + total_cheese: int + total_exp: int + badge: Optional[str] = None + + @classmethod + async def build(cls, total_cheese: int, total_exp: int, badge: Optional[str] = None) -> "RewardDTO": + return cls( + total_cheese=total_cheese, + total_exp=total_exp, + badge=badge, + ) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index 700dc54..b357e62 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -1,5 +1,6 @@ import asyncio -from datetime import datetime, timedelta, timezone, date +from datetime import date, datetime, timedelta, timezone +from typing import Any, Optional from fastapi import HTTPException from tortoise.exceptions import DoesNotExist @@ -14,6 +15,7 @@ from app.v2.levels.services.level_service import LevelService from app.v2.likes.models.like import Like from app.v2.missions.dtos.mission_dto import UserMissionDTO +from app.v2.missions.dtos.reward_dto import RewardDTO from app.v2.missions.models.mission import MissionInventory, UserMission from app.v2.notices.services.notice_service import NoticeService from app.v2.users.services.user_service import UserService @@ -61,7 +63,9 @@ async def update_mission_progress(self, user_id: str) -> None: if lv_up_mission[0]: await self._process_mission(lv_up_mission[0], mission_dict, cheese_manager_id, user_id) - async def _classify_missions(self, user_missions): + async def _classify_missions( + self, user_missions: list[UserMissionDTO] + ) -> tuple[list[UserMissionDTO], list[UserMissionDTO], list[UserMissionDTO]]: badge_missions = [mission for mission in user_missions if mission.mission_code.startswith("MS_BADGE")] lv_up_mission = [mission for mission in user_missions if mission.mission_code == "MS_LV_UP"] daily_missions = [mission for mission in user_missions if mission.mission_code.startswith("MS_DAILY")] @@ -77,7 +81,7 @@ async def _process_mission( mission = mission_dict.get(user_mission.mission_code) if user_mission.is_completed or not mission: - return 0, 0 + return increment = await self.evaluate_mission_condition(user_id, user_mission.mission_code) user_mission.progress_count += increment @@ -90,14 +94,36 @@ async def _process_mission( new_progress_count=user_mission.progress_count, is_completed=user_mission.is_completed, ) - if user_mission.mission_code == "MS_DAILY_POST_GENERAL": - await self.reward_daily_post(user_id=user_id, cheese_manager_id=cheese_manager_id) - else: - return await self.reward_user_for_mission( - user_id=user_id, - reward_code=mission.reward_code, - cheese_manager_id=cheese_manager_id, - ) + await self._handle_mission_reward( + user_id=user_id, + mission_code=user_mission.mission_code, + reward_code=mission.reward_code, + cheese_manager_id=cheese_manager_id, + ) + + async def _handle_mission_reward( + self, + user_id: str, + mission_code: str, + reward_code: str, + cheese_manager_id: int, + ) -> None: + if mission_code == "MS_DAILY_POST_GENERAL": + await self.reward_daily_post(user_id=user_id, cheese_manager_id=cheese_manager_id) + elif mission_code == "MS_LV_UP": + await self.reward_level_up_mission( + user_id=user_id, cheese_manager_id=cheese_manager_id, reward_code=reward_code + ) + elif mission_code.startswith("MS_BADGE"): + await self.reward_badge_mission( + user_id=user_id, cheese_manager_id=cheese_manager_id, reward_code=reward_code + ) + else: + await self.reward_mission( + user_id=user_id, + cheese_manager_id=cheese_manager_id, + reward_code=reward_code, + ) async def evaluate_mission_condition(self, user_id: str, mission_code: str) -> int: if mission_code == "MS_BADGE_POST_FIRST" and await self.check_first_post(user_id): @@ -179,20 +205,7 @@ async def check_daily_post(user_id: str) -> bool: answer_date = answer.get("date") - return answer_date == date.today() - - async def reward_user_for_mission( - self, - user_id: str, - reward_code: str, - cheese_manager_id: int, - ) -> tuple[int, int]: - item_inventory_rewards = await self.validate_reward(reward_code=reward_code) - return await self.process_reward( - item_inventory_rewards=item_inventory_rewards, - user_id=user_id, - cheese_manager_id=cheese_manager_id, - ) + return answer_date == date.today() # type: ignore @staticmethod async def validate_reward(reward_code: str): # type: ignore @@ -214,7 +227,7 @@ async def process_reward( item_inventory_rewards: list[ItemInventoryRewardInventory], user_id: str, cheese_manager_id: int, - ) -> None: + ) -> RewardDTO: total_cheese = 0 total_exp = 0 badge_info = [] @@ -241,24 +254,9 @@ async def process_reward( else: raise ValueError(f"Invalid item category for reward: {item.item_category}") - if badge_info: - await self._create_reward_notice( - user_id=user_id, - reward_type="BADGE_MISSION", - total_exp=total_exp, - total_cheese=total_cheese, - badge_info=badge_info[0], - ) - return + badge_full_name = badge_info[0] if badge_info else None - if total_cheese > 0 or total_exp > 0: - await self._create_reward_notice( - user_id=user_id, - reward_type="DAILY_MISSION", - total_exp=total_exp, - total_cheese=total_cheese, - badge_info=None, - ) + return await RewardDTO.build(total_cheese=total_cheese, total_exp=total_exp, badge=badge_full_name) async def reward_daily_post(self, user_id: str, cheese_manager_id: int) -> None: # 1. 경험치 및 치즈 계산 @@ -327,7 +325,14 @@ async def _add_exp_and_cheese(user_id: str, cheese_manager_id: int, exp: int, ch @staticmethod async def _create_reward_notice( - user_id: str, reward_type: str, total_exp: int, total_cheese: int, badge_info: str + user_id: str, + reward_type: str, + total_exp: int, + total_cheese: int, + badge_info: Optional[str], + level_up: Optional[bool] = False, + nickname: Optional[str] = None, + new_level: Optional[int] = None, ) -> None: await NoticeService.create_reward_notice( user_id=user_id, @@ -335,9 +340,61 @@ async def _create_reward_notice( total_cheese=total_cheese, total_exp=total_exp, badge_info=badge_info, + level_up=level_up, + nickname=nickname, + new_level=new_level, ) - async def _create_level_up_reward_notice(self, user_id: str, total_exp: int, total_cheese: int) -> None: - await NoticeService.create_reward_notice( - user_id=user_id, reward_type="LEVEL_UP", total_cheese=total_cheese, total_exp=total_exp + async def reward_level_up_mission(self, user_id: str, cheese_manager_id: int, reward_code: str) -> None: + item_inventory_rewards = await self.validate_reward(reward_code=reward_code) + reward_dto = await self.process_reward( + item_inventory_rewards=item_inventory_rewards, + user_id=user_id, + cheese_manager_id=cheese_manager_id, + ) + level_info = await LevelService.get_level_info_add_answer_days(user_id) + user_info = await UserService.get_user_profile(user_id=user_id) + + nickname = user_info.nickname + level = level_info.levelDto.level + + await self._create_reward_notice( + user_id=user_id, + reward_type="LEVEL_UP", + total_exp=reward_dto.total_exp, + total_cheese=reward_dto.total_cheese, + badge_info=None, + level_up=True, + nickname=nickname, + new_level=level, + ) + + async def reward_badge_mission(self, user_id: str, cheese_manager_id: int, reward_code: str) -> None: + item_inventory_rewards = await self.validate_reward(reward_code=reward_code) + reward_dto = await self.process_reward( + item_inventory_rewards=item_inventory_rewards, + user_id=user_id, + cheese_manager_id=cheese_manager_id, + ) + await self._create_reward_notice( + user_id=user_id, + reward_type="BADGE_MISSION", + total_exp=reward_dto.total_exp, + total_cheese=reward_dto.total_cheese, + badge_info=reward_dto.badge, + ) + + async def reward_mission(self, user_id: str, cheese_manager_id: int, reward_code: str) -> None: + item_inventory_rewards = await self.validate_reward(reward_code=reward_code) + reward_dto = await self.process_reward( + item_inventory_rewards=item_inventory_rewards, + user_id=user_id, + cheese_manager_id=cheese_manager_id, + ) + await self._create_reward_notice( + user_id=user_id, + reward_type="DAILY_MISSION", + total_exp=reward_dto.total_exp, + total_cheese=reward_dto.total_cheese, + badge_info=None, ) diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index acabb0e..012f208 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -80,7 +80,7 @@ async def mobile_my_page_handler(user_id: str) -> MyPageResponseDTO: badgeCount=badge_count, answerCount=answer_count, premium=user.is_premium, # type: ignore - allow_notification=user.allow_notification, + allow_notification=user.allow_notification, # type: ignore ), level=level, ) diff --git a/src/app/v2/notices/models/notice.py b/src/app/v2/notices/models/notice.py index a399a82..b9b6a41 100644 --- a/src/app/v2/notices/models/notice.py +++ b/src/app/v2/notices/models/notice.py @@ -1,3 +1,5 @@ +from typing import Optional + from tortoise import fields from tortoise.fields import ForeignKeyRelation from tortoise.models import Model @@ -19,7 +21,7 @@ class Notice(Model): reward_type = fields.CharField(max_length=255, null=True) user: ForeignKeyRelation[User] = fields.ForeignKeyField( - "models.User", related_name="notices", null=True, on_delete=fields.SET_NULL + "models.User", related_name="notices", on_delete=fields.CASCADE ) class Meta: @@ -30,13 +32,13 @@ async def create_notice( cls, title: str, content: str, - user_id: int, - link: str = None, - is_read: bool = False, - is_internal: bool = False, - answer_id: int = None, - date: str = None, - reward_type: str = None, + user_id: str, + link: Optional[str] = None, + is_read: Optional[bool] = False, + is_internal: Optional[bool] = False, + answer_id: Optional[int] = None, + date: Optional[str] = None, + reward_type: Optional[str] = None, ) -> None: query = """ INSERT INTO notice ( diff --git a/src/app/v2/notices/services/notice_service.py b/src/app/v2/notices/services/notice_service.py index 46e39fc..0be2e0c 100644 --- a/src/app/v2/notices/services/notice_service.py +++ b/src/app/v2/notices/services/notice_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from app.v2.notices.models.notice import Notice @@ -10,7 +12,7 @@ async def create_notice( title: str, reward_type: str, content: str, - ) -> Notice: + ) -> None: return await Notice.create_notice( title=title, content=content, @@ -27,19 +29,32 @@ async def create_reward_notice( reward_type: str, total_cheese: int = 0, total_exp: int = 0, - badge_info: str = None, - ) -> Notice: - if not badge_info and total_cheese == 0 and total_exp == 0: + badge_info: Optional[str] = None, + level_up: Optional[bool] = False, + nickname: Optional[str] = None, + new_level: Optional[int] = None, + ) -> None: + if not badge_info and not level_up and total_cheese == 0 and total_exp == 0: return # 1. 제목 생성 - title = cls.create_title(badge_info) + title = cls.create_title( + badge=badge_info, + level_up=level_up, + nickname=nickname, + new_level=new_level, + ) # 2. 메시지 생성 - content = cls.create_reward_message(total_cheese, total_exp, badge_info) + content = cls.create_reward_message( + total_cheese=total_cheese, + total_exp=total_exp, + badge=badge_info, + level_up=level_up, + ) # 3. 알림 생성 - return await cls.create_notice( + await cls.create_notice( user_id=user_id, title=title, content=content, @@ -47,13 +62,26 @@ async def create_reward_notice( ) @classmethod - def create_title(cls, badge: str = None) -> str: + def create_title( + cls, + badge: Optional[str] = None, + level_up: Optional[bool] = False, + nickname: Optional[str] = None, + new_level: Optional[int] = None, + ) -> str: + if level_up and nickname and new_level is not None: + return f"{nickname} LV{new_level}로 레벨업!" if badge: return badge return "보상을 받았어요!" @classmethod - def create_reward_message(cls, total_cheese: int, total_exp: int, badge: str = None) -> str: + def create_reward_message( + cls, total_cheese: int, total_exp: int, badge: Optional[str] = None, level_up: Optional[bool] = False + ) -> str: + if level_up: + return f"선물로 치즈 {total_cheese}개를 드릴게요!" if total_cheese > 0 else "레벨업을 축하드립니다!" + if badge: return f"선물로 치즈 {total_cheese}개를 드릴게요!" if total_cheese > 0 else "뱃지 획득을 축하드립니다!" diff --git a/src/app/v2/purchases/services/purchase_service.py b/src/app/v2/purchases/services/purchase_service.py index 7b2bbf8..6420521 100644 --- a/src/app/v2/purchases/services/purchase_service.py +++ b/src/app/v2/purchases/services/purchase_service.py @@ -9,7 +9,7 @@ from tortoise.transactions import atomic from app.v2.items.models.item import ItemInventory, ItemInventoryProductInventory, ProductInventory -from app.v2.purchases.dtos.purchase_dto import ReceiptInfoDTO, PurchaseResponseDTO +from app.v2.purchases.dtos.purchase_dto import PurchaseResponseDTO, ReceiptInfoDTO from app.v2.purchases.models.purchase_history import PurchaseHistory, Subscription from app.v2.purchases.models.purchase_status import PurchaseStatus, SubscriptionStatus from app.v2.users.models.user import User @@ -56,7 +56,7 @@ async def process_apple_purchase(self, receipt_data: str, user_id: str) -> Purch user = await UserService.get_user_profile(user_id=user_id) - return PurchaseResponseDTO.build(is_premium=user.is_premium, product_code=receipt_info.product_code_two) + return PurchaseResponseDTO.build(is_premium=user.is_premium, product_code=receipt_info.product_code_two) # type: ignore @staticmethod def _extract_latest_receipt_info(response: dict[str, Any]) -> dict[str, Any] | None: From 4bf94d04596303e7e8e23998a15a418ba6b8bff7 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Wed, 11 Dec 2024 15:01:07 +0900 Subject: [PATCH 57/60] =?UTF-8?q?=F0=9F=92=A1=20chore:=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/levels/services/level_service.py | 2 +- src/common/tasks/mission_task.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/v2/levels/services/level_service.py b/src/app/v2/levels/services/level_service.py index 6a59777..1321bf6 100644 --- a/src/app/v2/levels/services/level_service.py +++ b/src/app/v2/levels/services/level_service.py @@ -18,7 +18,7 @@ async def get_level_info_add_answer_days(cls, user_id: str) -> LevelInfoDTO: level_dto = await cls.get_level_info(user_id=user_id) if level_dto.requiredExp is None: - raise ValueError("Required experience cannot be None") # 예외를 던지는 방법 + raise ValueError("Required experience cannot be None") needs_to_level_up = await cls.calculate_days_to_level_up( user_id=user_id, diff --git a/src/common/tasks/mission_task.py b/src/common/tasks/mission_task.py index f4844db..8e3ed33 100644 --- a/src/common/tasks/mission_task.py +++ b/src/common/tasks/mission_task.py @@ -1,15 +1,11 @@ import asyncio -from tortoise.expressions import Q - from app.v2.missions.models.mission import UserMission async def mission_reset_task() -> None: await asyncio.gather( - UserMission.filter(Q(mission_code="MS_SINGLE_POST_2_5", progress_count__lt=3)).update(is_completed=False), UserMission.filter(mission_code__in=["MS_LV_UP", "MS_DAILY_LIKE_3_PER_DAY", "MS_DAILY_POST_GENERAL"]).update( is_completed=False, progress_count=0 ), ) - # 좋아요, 일반은 progress count도 수정 From 2bde07ed3e19a8512dc9b0b2ef216a061083a18f Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Wed, 11 Dec 2024 17:34:58 +0900 Subject: [PATCH 58/60] =?UTF-8?q?=E2=9C=A8=20feat:=20user=5Fcolor=20api=20?= =?UTF-8?q?=ED=94=84=EB=A6=AC=EB=AF=B8=EC=97=84=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/colors/models/color.py | 4 ++++ src/app/v2/colors/services/color_service.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/v2/colors/models/color.py b/src/app/v2/colors/models/color.py index 1ccb5e6..acf8bc2 100644 --- a/src/app/v2/colors/models/color.py +++ b/src/app/v2/colors/models/color.py @@ -49,3 +49,7 @@ class ColorInventory(Model): class Meta: table = "color_inventory" # 테이블 이름을 명시 + + @classmethod + async def get_color_inventory(cls) -> list[dict[str, str]]: + return await cls.all().values("color_code", "color_name", "color_hex_code") diff --git a/src/app/v2/colors/services/color_service.py b/src/app/v2/colors/services/color_service.py index c9bfe57..5b03137 100644 --- a/src/app/v2/colors/services/color_service.py +++ b/src/app/v2/colors/services/color_service.py @@ -1,5 +1,6 @@ from app.v2.colors.dtos.color_dto import ColorCodeDTO, ColorDTO -from app.v2.colors.models.color import Color +from app.v2.colors.models.color import Color, ColorInventory +from app.v2.users.services.user_service import UserService class ColorService: @@ -14,5 +15,14 @@ async def add_color(cls, user_id: str, color_code: str) -> None: @classmethod async def get_colors_with_details_by_user_id(cls, user_id: str) -> list[ColorDTO]: - colors_raw = await Color.get_colors_with_details_by_user_id(user_id=user_id) + user = await UserService.get_user_profile(user_id=user_id) + if user.is_premium: + colors_raw = await ColorInventory.get_color_inventory() + else: + colors_raw = await Color.get_colors_with_details_by_user_id(user_id=user_id) + return [ColorDTO.builder(color) for color in colors_raw] + + @classmethod + async def get_color_inventory(cls) -> list[dict[str, str]]: + return await ColorInventory.get_color_inventory() From 1d0a397231fc42bb5cbec80138ea04f3ee4d0b94 Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Wed, 11 Dec 2024 22:11:08 +0900 Subject: [PATCH 59/60] =?UTF-8?q?=E2=9C=A8=20feat:=20notice=20=EB=B1=83?= =?UTF-8?q?=EC=A7=80=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/missions/dtos/reward_dto.py | 17 +++++++++-- .../v2/missions/services/mission_service.py | 21 ++++++++------ src/app/v2/notices/models/notice.py | 7 +++-- src/app/v2/notices/services/notice_service.py | 29 ++++++++++--------- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/app/v2/missions/dtos/reward_dto.py b/src/app/v2/missions/dtos/reward_dto.py index 1a26c4a..c0f46f9 100644 --- a/src/app/v2/missions/dtos/reward_dto.py +++ b/src/app/v2/missions/dtos/reward_dto.py @@ -6,12 +6,23 @@ class RewardDTO(BaseModel): total_cheese: int total_exp: int - badge: Optional[str] = None + badge_code: Optional[str] = None + badge_full_name: Optional[str] = None + + class META: + orm_mode = True @classmethod - async def build(cls, total_cheese: int, total_exp: int, badge: Optional[str] = None) -> "RewardDTO": + async def build( + cls, + total_cheese: int, + total_exp: int, + badge_code: Optional[str] = None, + badge_full_name: Optional[str] = None, + ) -> "RewardDTO": return cls( total_cheese=total_cheese, total_exp=total_exp, - badge=badge, + badge_code=badge_code, + badge_full_name=badge_full_name, ) diff --git a/src/app/v2/missions/services/mission_service.py b/src/app/v2/missions/services/mission_service.py index b357e62..ff23e19 100644 --- a/src/app/v2/missions/services/mission_service.py +++ b/src/app/v2/missions/services/mission_service.py @@ -240,7 +240,7 @@ async def process_reward( for _ in range(quantity): await BadgeService.add_badge(user_id=user_id, badge_code=item.item_code) badge = await BadgeService.get_badge_info_by_badge_code(badge_code=item.item_code) - badge_info.append(badge.badge_full_name) + badge_info.append(badge) elif item.item_category == "COLOR": for _ in range(quantity): @@ -254,9 +254,12 @@ async def process_reward( else: raise ValueError(f"Invalid item category for reward: {item.item_category}") - badge_full_name = badge_info[0] if badge_info else None + badge_full_name = badge_info[0].badge_full_name if badge_info else None + badge_code = badge_info[0].badge_code if badge_info else None - return await RewardDTO.build(total_cheese=total_cheese, total_exp=total_exp, badge=badge_full_name) + return await RewardDTO.build( + total_cheese=total_cheese, total_exp=total_exp, badge_full_name=badge_full_name, badge_code=badge_code + ) async def reward_daily_post(self, user_id: str, cheese_manager_id: int) -> None: # 1. 경험치 및 치즈 계산 @@ -271,7 +274,6 @@ async def reward_daily_post(self, user_id: str, cheese_manager_id: int) -> None: reward_type="DAILY_MISSION", total_exp=exp, total_cheese=cheese, - badge_info=None, ) async def _calculate_exp_and_cheese(self, user_id: str) -> tuple[int, int, int]: @@ -329,7 +331,8 @@ async def _create_reward_notice( reward_type: str, total_exp: int, total_cheese: int, - badge_info: Optional[str], + badge_full_name: Optional[str] = None, + badge_code: Optional[str] = None, level_up: Optional[bool] = False, nickname: Optional[str] = None, new_level: Optional[int] = None, @@ -339,7 +342,8 @@ async def _create_reward_notice( reward_type=reward_type, total_cheese=total_cheese, total_exp=total_exp, - badge_info=badge_info, + badge_full_name=badge_full_name, + badge_code=badge_code, level_up=level_up, nickname=nickname, new_level=new_level, @@ -363,7 +367,6 @@ async def reward_level_up_mission(self, user_id: str, cheese_manager_id: int, re reward_type="LEVEL_UP", total_exp=reward_dto.total_exp, total_cheese=reward_dto.total_cheese, - badge_info=None, level_up=True, nickname=nickname, new_level=level, @@ -381,7 +384,8 @@ async def reward_badge_mission(self, user_id: str, cheese_manager_id: int, rewar reward_type="BADGE_MISSION", total_exp=reward_dto.total_exp, total_cheese=reward_dto.total_cheese, - badge_info=reward_dto.badge, + badge_code=reward_dto.badge_code, + badge_full_name=reward_dto.badge_full_name, ) async def reward_mission(self, user_id: str, cheese_manager_id: int, reward_code: str) -> None: @@ -396,5 +400,4 @@ async def reward_mission(self, user_id: str, cheese_manager_id: int, reward_code reward_type="DAILY_MISSION", total_exp=reward_dto.total_exp, total_cheese=reward_dto.total_cheese, - badge_info=None, ) diff --git a/src/app/v2/notices/models/notice.py b/src/app/v2/notices/models/notice.py index b9b6a41..f189219 100644 --- a/src/app/v2/notices/models/notice.py +++ b/src/app/v2/notices/models/notice.py @@ -39,16 +39,17 @@ async def create_notice( answer_id: Optional[int] = None, date: Optional[str] = None, reward_type: Optional[str] = None, + badge_code: Optional[str] = None, ) -> None: query = """ INSERT INTO notice ( title, content, user_id, link, is_internal, is_read, answer_id, date, reward_type, - created_at + badge_code, created_at ) VALUES ( - %s, %s, UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, %s, %s, %s, NOW() + %s, %s, UNHEX(REPLACE(%s, '-', '')), %s, %s, %s, %s, %s, %s, %s, NOW() ); """ - values = (title, content, user_id, link, is_internal, is_read, answer_id, date, reward_type) + values = (title, content, user_id, link, is_internal, is_read, answer_id, date, reward_type, badge_code) await QueryExecutor.execute_query(query, values=values, fetch_type="single") diff --git a/src/app/v2/notices/services/notice_service.py b/src/app/v2/notices/services/notice_service.py index 0be2e0c..5c94902 100644 --- a/src/app/v2/notices/services/notice_service.py +++ b/src/app/v2/notices/services/notice_service.py @@ -1,5 +1,6 @@ from typing import Optional +from app.v2.badges.models.badge import BadgeInventory from app.v2.notices.models.notice import Notice @@ -12,6 +13,7 @@ async def create_notice( title: str, reward_type: str, content: str, + badge_code: Optional[str] = None, ) -> None: return await Notice.create_notice( title=title, @@ -20,6 +22,7 @@ async def create_notice( reward_type=reward_type, is_internal=True, is_read=False, + badge_code=badge_code, ) @classmethod @@ -29,17 +32,18 @@ async def create_reward_notice( reward_type: str, total_cheese: int = 0, total_exp: int = 0, - badge_info: Optional[str] = None, + badge_full_name: Optional[str] = None, + badge_code: Optional[str] = None, level_up: Optional[bool] = False, nickname: Optional[str] = None, new_level: Optional[int] = None, ) -> None: - if not badge_info and not level_up and total_cheese == 0 and total_exp == 0: + if not badge_code and not level_up and total_cheese == 0 and total_exp == 0: return # 1. 제목 생성 title = cls.create_title( - badge=badge_info, + badge_full_name=badge_full_name, level_up=level_up, nickname=nickname, new_level=new_level, @@ -49,40 +53,39 @@ async def create_reward_notice( content = cls.create_reward_message( total_cheese=total_cheese, total_exp=total_exp, - badge=badge_info, + badge_full_name=badge_full_name, level_up=level_up, ) + badge_code = badge_code if badge_code else None + # 3. 알림 생성 await cls.create_notice( - user_id=user_id, - title=title, - content=content, - reward_type=reward_type, + user_id=user_id, title=title, content=content, reward_type=reward_type, badge_code=badge_code ) @classmethod def create_title( cls, - badge: Optional[str] = None, + badge_full_name: Optional[str] = None, level_up: Optional[bool] = False, nickname: Optional[str] = None, new_level: Optional[int] = None, ) -> str: if level_up and nickname and new_level is not None: return f"{nickname} LV{new_level}로 레벨업!" - if badge: - return badge + if badge_full_name: + return badge_full_name return "보상을 받았어요!" @classmethod def create_reward_message( - cls, total_cheese: int, total_exp: int, badge: Optional[str] = None, level_up: Optional[bool] = False + cls, total_cheese: int, total_exp: int, badge_full_name: Optional[str] = None, level_up: Optional[bool] = False ) -> str: if level_up: return f"선물로 치즈 {total_cheese}개를 드릴게요!" if total_cheese > 0 else "레벨업을 축하드립니다!" - if badge: + if badge_full_name: return f"선물로 치즈 {total_cheese}개를 드릴게요!" if total_cheese > 0 else "뱃지 획득을 축하드립니다!" if total_cheese > 0 and total_exp > 0: From 00ca51349c584f83af554fbd8abf7619b76b497e Mon Sep 17 00:00:00 2001 From: taewoo-dev Date: Wed, 11 Dec 2024 22:17:34 +0900 Subject: [PATCH 60/60] =?UTF-8?q?=E2=9C=A8=20fix:=20=ED=85=94=EB=9F=AC?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20api=20=EB=B3=B4=EC=9C=A0=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/v2/colors/dtos/color_dto.py | 8 ++++---- src/app/v2/mobiles/dtos/teller_card_response.py | 10 +++++----- src/app/v2/mobiles/router.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/v2/colors/dtos/color_dto.py b/src/app/v2/colors/dtos/color_dto.py index 2306d63..df1a390 100644 --- a/src/app/v2/colors/dtos/color_dto.py +++ b/src/app/v2/colors/dtos/color_dto.py @@ -11,13 +11,13 @@ def builder(cls, color_raw: dict[str, str]) -> "ColorCodeDTO": class ColorDTO(BaseModel): colorCode: str - colorName: str - colorHexCode: str + # colorName: str + # colorHexCode: str @classmethod def builder(cls, color_raw: dict[str, str]) -> "ColorDTO": return cls( colorCode=color_raw.get("color_code", ""), - colorName=color_raw.get("color_name", ""), - colorHexCode=color_raw.get("color_hex_code", ""), + # colorName=color_raw.get("color_name", ""), + # colorHexCode=color_raw.get("color_hex_code", ""), ) diff --git a/src/app/v2/mobiles/dtos/teller_card_response.py b/src/app/v2/mobiles/dtos/teller_card_response.py index 22d1d2c..29f8349 100644 --- a/src/app/v2/mobiles/dtos/teller_card_response.py +++ b/src/app/v2/mobiles/dtos/teller_card_response.py @@ -3,15 +3,15 @@ from pydantic import BaseModel from app.v2.badges.dtos.badge_dto import BadgeDTO -from app.v2.colors.dtos.color_dto import ColorCodeDTO +from app.v2.colors.dtos.color_dto import ColorDTO from app.v2.levels.dtos.level_dto import LevelDTO, LevelInfoDTO from app.v2.users.dtos.user_info_dto import UserInfoDTO from common.base_models.base_dtos.base_response import BaseResponseDTO class DataDTO(BaseModel): - badges: List[BadgeDTO] - colors: List[ColorCodeDTO] + badges: list[BadgeDTO] + colors: list[ColorDTO] userInfo: UserInfoDTO levelInfo: LevelInfoDTO recordCount: int = 0 @@ -19,8 +19,8 @@ class DataDTO(BaseModel): @classmethod def builder( cls, - badges: List[BadgeDTO], - colors: List[ColorCodeDTO], + badges: list[BadgeDTO], + colors: list[ColorDTO], userInfo: UserInfoDTO, levelInfo: LevelInfoDTO, recordCount: Optional[int] = None, diff --git a/src/app/v2/mobiles/router.py b/src/app/v2/mobiles/router.py index 012f208..1e2c9fa 100644 --- a/src/app/v2/mobiles/router.py +++ b/src/app/v2/mobiles/router.py @@ -30,7 +30,7 @@ async def mobile_main_handler() -> None: async def mobile_teller_card_handler(user_id: str) -> TellerCardResponseDTO: badges_task = BadgeService.get_badges_with_details_by_user_id(user_id) - colors_task = ColorService.get_colors(user_id) + colors_task = ColorService.get_colors_with_details_by_user_id(user_id) level_info_task = LevelService.get_level_info_add_answer_days(user_id) teller_card_task = TellerCardService.get_teller_card(user_id) user_info_task = UserService.get_user_info(user_id)