Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature/develop] #2

Merged
merged 60 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
09d4999
🚚 build: 기능별 폴더 구조 생성
taewoo-dev Oct 3, 2024
3b1dc8b
🚚 build: sql-alchemy, pymysql 삭제, tortoise-orm 추가 및 database setting
taewoo-dev Oct 10, 2024
5a6fc97
📝 docs: ci file 수정
taewoo-dev Oct 10, 2024
3b349f1
✨ feat: raw query executor 추가 및 mypage api 추가
taewoo-dev Oct 13, 2024
2ee2d08
✨ feat: tellercard ui page 추가
taewoo-dev Oct 13, 2024
c800910
✨ feat: Query fomatter 추가 및 raw query 분리
taewoo-dev Oct 14, 2024
dce8889
✨ feat: teller card & mypage Ui api 구현
taewoo-dev Oct 19, 2024
3208163
feat: teller_card patch api added
taewoo-dev Oct 19, 2024
f2fc7b3
✨ feat: 연속 작성일 기능 구현
taewoo-dev Oct 19, 2024
cfc29de
feat: level up 로직 구현
taewoo-dev Oct 20, 2024
38d4bb2
✨ feat: color & emotion 결제 로직 추가
taewoo-dev Oct 27, 2024
678962b
✨ feat: mission api 기본 구조 설계
taewoo-dev Oct 28, 2024
10a6659
✨ feat: mission service 로직 구현
taewoo-dev Nov 2, 2024
1bb4899
✨ feat: Mission reward 로직 + 경험치 + 레벨업 마무리
taewoo-dev Nov 3, 2024
668e4d9
✨ feat: purchase 주문처리 시스템 연동
taewoo-dev Nov 9, 2024
b8e3451
🐛 fix: java server에 맞춰 Request, Response format 변경
taewoo-dev Nov 10, 2024
a701bdf
🐛 fix: mypy
taewoo-dev Nov 11, 2024
9e68f1a
✨ feat: level info dto added
taewoo-dev Nov 16, 2024
92114de
build: tortoise orm db driver 변경:
taewoo-dev Nov 16, 2024
50ef848
chore: mypy setting
taewoo-dev Nov 20, 2024
492d6b7
fix : cheese manager get_cheese_balance 수정
taewoo-dev Nov 21, 2024
318cd5c
fix: 미션 로직 변경
taewoo-dev Nov 23, 2024
3941958
✨ feat: 보유 색상 정보 조회 api 추가
taewoo-dev Nov 24, 2024
81a4c68
💡 chore: 오타 수정
taewoo-dev Nov 28, 2024
f5b7556
✨ feat: 애플 인앱 영수증 테스트 api 추가
taewoo-dev Nov 28, 2024
849078f
fix : purchase_api response
taewoo-dev Nov 28, 2024
7a9288e
💡 chore: 오타 수정
taewoo-dev Nov 28, 2024
37fc9a3
💡 chore: 오타
taewoo-dev Nov 28, 2024
0501aad
✨ feat: 텔러카드 업데이트 검증 로직 추가
taewoo-dev Nov 29, 2024
4189d3a
✨ feat: 보유 감정 조회 api 추가
taewoo-dev Nov 30, 2024
095105e
🐛 fix: level dto 스네이크 -> 카멜 수정
taewoo-dev Nov 30, 2024
a1a4d79
🐛 fix: teller_card 응답 연속 작성일 추가
taewoo-dev Nov 30, 2024
7613f93
💡 chore: type hinting 추가
taewoo-dev Dec 1, 2024
d381bfe
✨ feat: apple 구독 기능 추가
taewoo-dev Dec 1, 2024
0f24216
🐛 fix: 오타 수정
taewoo-dev Dec 1, 2024
f00d834
🐛 fix: 결제 히스토리 영수증 필드 추가
taewoo-dev Dec 1, 2024
633993a
✨ feat: celery 초기 세팅
taewoo-dev Dec 2, 2024
75bf863
🐛 fix: mypage is premium 버그 수정
taewoo-dev Dec 3, 2024
83e7390
🐛 fix: 프리미엄 유저 감정 12종 추가
taewoo-dev Dec 3, 2024
6c5e569
🐛fix: 텔러카트 수정 api 개별 수정 추가
taewoo-dev Dec 3, 2024
1db630d
✨ feat: 샐러리 with 스케줄러를 이용한 구독 갱신 기능 및 만료 및 미션 리셋 추가
taewoo-dev Dec 4, 2024
8b35ab5
✨ feat: 프리미엄 유저 만료 처리
taewoo-dev Dec 5, 2024
b62ad19
✨ feat: 환불 영수증 처리 로직 추가
taewoo-dev Dec 7, 2024
7829a7b
🐛 fix: 미션 api backgroud task -> celery로 변경
taewoo-dev Dec 7, 2024
2880f4a
🐛 fix: 샐러리 워커를 이용한 미션 처리
taewoo-dev Dec 7, 2024
dbf81cb
✨ feat: 미션 달성 시 보상 알림 기능 추가
taewoo-dev Dec 7, 2024
ab4ec1c
🐛 fix: 애플 구독 api 변경
taewoo-dev Dec 8, 2024
6fa6fb0
🐛 fix: 응답 변환
taewoo-dev Dec 8, 2024
4568c18
🐛 fix: mypage api allow notification 추가
taewoo-dev Dec 8, 2024
16a6f34
🐛 fix: 결제 api 검증 로직 수정
taewoo-dev Dec 8, 2024
12fdb21
🐛 fix: 애플 영수증 구독 만료 처리 수정
taewoo-dev Dec 8, 2024
d85aea6
🐛fix: 미션 백그라운드 로직 수정
taewoo-dev Dec 8, 2024
a7c0320
✨ feat: 영수증 정보 확인용 api 추가
taewoo-dev Dec 8, 2024
36a97e8
✨ fix: 구독 만료 처리 스케줄러 제거
taewoo-dev Dec 8, 2024
fe06d4f
✨ fix: 경험치 지급 및 보상 알림 로직 변경
taewoo-dev Dec 10, 2024
3436281
✨ feat: 레벨업 메세지 추가
taewoo-dev Dec 11, 2024
4bf94d0
💡 chore: 오타수정
taewoo-dev Dec 11, 2024
2bde07e
✨ feat: user_color api 프리미엄 유저 색상 추가
taewoo-dev Dec 11, 2024
1d0a397
✨ feat: notice 뱃지 코드 추가
taewoo-dev Dec 11, 2024
00ca513
✨ fix: 텔러카드 api 보유 색상 조회 수정
taewoo-dev Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 0 additions & 44 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
name: Code Quality Checks

# 트리거 이벤트: push 또는 pull_request 시 CI가 실행됩니다.
on:
push:
branches:
Expand All @@ -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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ celerybeat.pid
*.sage.py

# Environments
.env
src/.env.local
.venv
env/
venv/
Expand Down
757 changes: 678 additions & 79 deletions poetry.lock

Large diffs are not rendered by default.

32 changes: 28 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,46 @@ 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"
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]
isort = "^5.13.2"
mypy = "^1.11.2"
black = "^24.8.0"
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 무시

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.black]
line-length = 120


[tool.isort]
profile = "black"
line_length = 120

Empty file added src/app/__init__.py
Empty file.
Empty file added src/app/v2/__init__.py
Empty file.
Empty file added src/app/v2/answers/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
58 changes: 58 additions & 0 deletions src/app/v2/answers/models/answer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 (
SELECT_ANSWER_BY_USER_UUID_QUERY,
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


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)
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)

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) -> 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) -> 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) -> Any:
query = SELECT_MOST_RECENT_ANSWER_BY_USER_UUID_QUERY
value = user_id
return await QueryExecutor.execute_query(query, values=value, fetch_type="single")
Empty file.
18 changes: 18 additions & 0 deletions src/app/v2/answers/querys/answer_query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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_BY_USER_UUID_QUERY = f"""
SELECT * FROM answer
WHERE {USER_ID_QUERY}
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
"""
21 changes: 21 additions & 0 deletions src/app/v2/answers/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from fastapi import APIRouter

from app.v2.levels.services.level_service import LevelService

router = APIRouter(prefix="/answer", tags=["Test용"])


# FastAPI 비동기 뷰


@router.get("/level-up")
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)
Empty file.
46 changes: 46 additions & 0 deletions src/app/v2/answers/services/answer_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from datetime import datetime, timedelta
from typing import Any

from app.v2.answers.models.answer import Answer


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)
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:
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.date(): # 날짜만 비교
record += 1
target_date = target_date - timedelta(days=1)
else:
break

return record

@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) -> Any:
answer = await Answer.get_most_recent_answer_by_user_id(user_id=user_id)
if answer == 0:
return {}
return answer
Empty file added src/app/v2/badges/__init__.py
Empty file.
Empty file.
25 changes: 25 additions & 0 deletions src/app/v2/badges/dtos/badge_dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pydantic import BaseModel


class BadgeCodeDTO(BaseModel):
badgeCode: str

@classmethod
def builder(cls, badge_raw: dict[str, str]) -> "BadgeCodeDTO":
return cls(badgeCode=badge_raw.get("badge_code", ""))


class BadgeDTO(BaseModel):
badgeCode: str
badgeName: str
badgeMiddleName: str
badgeCondition: str

@classmethod
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", ""),
)
6 changes: 6 additions & 0 deletions src/app/v2/badges/dtos/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from app.v2.badges.dtos.badge_dto import BadgeDTO
from common.base_models.base_dtos.base_response import BaseResponseDTO


class BadgeListResponseDTO(BaseResponseDTO):
data: list[BadgeDTO]
Empty file.
61 changes: 61 additions & 0 deletions src/app/v2/badges/models/badge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 (
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,
)
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)
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) -> 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) -> 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) -> Any:
query = SELECT_BADGE_CODE_BY_USER_UUID_QUERY
value = user_id
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)


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"

@property
def badge_full_name(self) -> str:
return f"{self.badge_middle_name} {self.badge_name}"
Empty file.
Loading
Loading