Skip to content

Feat/30 사용횟수 제한 기능 구현#116

Merged
selnem merged 4 commits into
developfrom
feat/30-사용횟수-제한-기능-구현
Jun 4, 2026

Hidden character warning

The head ref may contain hidden characters: "feat/30-\uc0ac\uc6a9\ud69f\uc218-\uc81c\ud55c-\uae30\ub2a5-\uad6c\ud604"
Merged

Feat/30 사용횟수 제한 기능 구현#116
selnem merged 4 commits into
developfrom
feat/30-사용횟수-제한-기능-구현

Conversation

@selnem

@selnem selnem commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

🔗 Related Issue

✨ 작업 개요

Gemini API·DB 부하 방지를 위해 이슈 핀 AI 글 생성 / 게시 / 수정에 Redis 기반 일일 성공 횟수 제한을 추가했습니다.

  • 리셋: KST 자정 기준
  • 카운트 시점: 실패(LLM·검증·롤백)는 미집계, 성공 시에만 Redis INCR
  • 제한 단위
    • AI 생성 (POST /issues/pin/ai): uid / 일
    • 게시 (POST /issues/pin): uid / 일
    • 수정 (PATCH /issues/pin/{pin_id}): pin_id(글) / 일
  • 에러코드 - 429: 한도 초과 시에만 (AI_4291, ISSUE_4291, ISSUE_4292 — edit는 pinId 포함)
  • 기존 요청/응답 스키마 유지: multipart request + photos 형식 변경 없음
  • 성공 응답 additive: 기존 result 필드 유지 + rateLimitQuota 추가 (프론트 선택 사용)
  • quota 조회 API (신규)
    • GET /issues/pin/ai/quota
    • GET /issues/pin/create/quota
    • GET /issues/pin/edit/quota?pin_id= (본인 pin 소유 검증)

주요 변경 파일

구분 파일
서비스 IssuePinDailyRateLimitService
연동 IssueService, IssueRoute, deps, config, codes, IssueDTO
테스트 tests/test_issue_pin_daily_rate_limit.py (13 cases)

*_RATE_LIMIT_ENABLED=false면 해당 kind만 비활성화 (assert/record/429 미적용).
Redis 미연결 시 fail-open(제한 스킵) + warning 로그.

체크리스트

  • Reviewers, Assignees, Labels를 모두 등록했나요?
  • .gitignore 설정을 하였나요?
  • PR 머지 전 반드시 CI가 정상적으로 작동하는지 확인해주세요!

📷 이미지 첨부 (선택)

image image

selnem added 3 commits June 3, 2026 22:14
AiPinGenerationRateLimitService를 IssuePinDailyRateLimitService로 통합하고,
uid/pin_id 기준 Redis 일일 제한(assert/record)과 GET quota 엔드포인트를 추가한다.
@selnem selnem requested a review from qkwltkwkd1 June 3, 2026 17:01
@selnem selnem self-assigned this Jun 3, 2026
@selnem selnem added the ⭐ feat 새로운 기능 label Jun 3, 2026
@selnem selnem linked an issue Jun 3, 2026 that may be closed by this pull request
1 task

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a daily rate limiting feature for issue pin operations (AI preview generation, pin creation, and pin editing) using Redis with KST midnight resets. It introduces the IssuePinDailyRateLimitService, configures rate limit thresholds, adds quota status retrieval endpoints, integrates rate limit checks into the issue service, and includes comprehensive unit tests and documentation. The review feedback highlights a robustness issue in IssuePinDailyRateLimitService.py where unpacking the Redis script result outside of the try...except block could cause a crash if Redis returns an unexpected response, potentially violating the fail-open policy.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread app/services/internal/IssuePinDailyRateLimitService.py
@selnem

selnem commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a daily rate limiting mechanism for issue pin operations (AI generation, creation, and editing) using Redis. It introduces the IssuePinDailyRateLimitService to manage quotas, adds endpoints to retrieve quota status, and updates existing endpoints to enforce and return quota details. The review feedback suggests returning the IssuePin object from ensure_issue_pin_edit_access to avoid redundant database lookups, refactoring seconds_until_kst_midnight to reuse next_kst_midnight and eliminate code duplication, and adding defensive type checking on the Redis Lua script output to prevent potential TypeErrors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +141 to +154
async def ensure_issue_pin_edit_access(self, *, uid: str, pin_id: int) -> None:
issue_pin = await self._issue_pin_repo.get_by_pin_id(pin_id)
if issue_pin is None or issue_pin.pin is None:
raise_business_exception(ErrorCode.ISSUE_NOT_FOUND)

pin = issue_pin.pin
if pin.pin_type != PinType.ISSUE:
raise_business_exception(ErrorCode.ISSUE_NOT_FOUND)
if pin.uid != uid:
raise_business_exception(ErrorCode.FORBIDDEN)
if issue_pin.issue_pin_state != IssuePinState.BEFORE_PROGRESS:
raise_business_exception(ErrorCode.ISSUE_PIN_EDIT_VALIDATION)
if pin.pin_location is None:
raise_business_exception(ErrorCode.ISSUE_PIN_EDIT_FAILED)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 ensure_issue_pin_edit_access 메서드는 검증만 수행하고 None을 반환합니다. 하지만 이 메서드를 호출하는 곳(예: update_issue_pin 등)에서는 검증된 IssuePin 객체의 정보가 필요한 경우가 많습니다. 이 메서드가 검증된 IssuePin 객체를 반환하도록 수정하면, 호출부에서 중복으로 데이터베이스를 조회하거나 동일한 검증 로직을 중복 작성할 필요 없이 반환된 객체를 재사용할 수 있어 유지보수성과 효율성이 향상됩니다.

Suggested change
async def ensure_issue_pin_edit_access(self, *, uid: str, pin_id: int) -> None:
issue_pin = await self._issue_pin_repo.get_by_pin_id(pin_id)
if issue_pin is None or issue_pin.pin is None:
raise_business_exception(ErrorCode.ISSUE_NOT_FOUND)
pin = issue_pin.pin
if pin.pin_type != PinType.ISSUE:
raise_business_exception(ErrorCode.ISSUE_NOT_FOUND)
if pin.uid != uid:
raise_business_exception(ErrorCode.FORBIDDEN)
if issue_pin.issue_pin_state != IssuePinState.BEFORE_PROGRESS:
raise_business_exception(ErrorCode.ISSUE_PIN_EDIT_VALIDATION)
if pin.pin_location is None:
raise_business_exception(ErrorCode.ISSUE_PIN_EDIT_FAILED)
async def ensure_issue_pin_edit_access(self, *, uid: str, pin_id: int) -> IssuePin:
issue_pin = await self._issue_pin_repo.get_by_pin_id(pin_id)
if issue_pin is None or issue_pin.pin is None:
raise_business_exception(ErrorCode.ISSUE_NOT_FOUND)
pin = issue_pin.pin
if pin.pin_type != PinType.ISSUE:
raise_business_exception(ErrorCode.ISSUE_NOT_FOUND)
if pin.uid != uid:
raise_business_exception(ErrorCode.FORBIDDEN)
if issue_pin.issue_pin_state != IssuePinState.BEFORE_PROGRESS:
raise_business_exception(ErrorCode.ISSUE_PIN_EDIT_VALIDATION)
if pin.pin_location is None:
raise_business_exception(ErrorCode.ISSUE_PIN_EDIT_FAILED)
return issue_pin

Comment on lines +114 to +126
def seconds_until_kst_midnight(*, now: datetime | None = None) -> int:
current = now or datetime.now(_KST)
if current.tzinfo is None:
current = current.replace(tzinfo=_KST)
else:
current = current.astimezone(_KST)
next_midnight = (current + timedelta(days=1)).replace(
hour=0,
minute=0,
second=0,
microsecond=0,
)
return int((next_midnight - current).total_seconds()) + _EXPIRE_BUFFER_SECONDS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

seconds_until_kst_midnight 메서드와 next_kst_midnight 메서드는 현재 시각(current)을 구하고 다음 자정(next_midnight)을 계산하는 로직이 완전히 중복됩니다. seconds_until_kst_midnight 내부에서 next_kst_midnight을 호출하도록 수정하면 코드 중복을 제거하고 유지보수성을 높일 수 있습니다.

    @staticmethod
    def seconds_until_kst_midnight(*, now: datetime | None = None) -> int:
        current = now or datetime.now(_KST)
        if current.tzinfo is None:
            current = current.replace(tzinfo=_KST)
        else:
            current = current.astimezone(_KST)
        next_midnight = IssuePinDailyRateLimitService.next_kst_midnight(now=current)

Comment on lines +282 to +283
if raw is None or len(raw) < 2:
raise ValueError(f"unexpected redis script result: {raw!r}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Redis Lua 스크립트 실행 결과인 raw가 예상치 못한 타입(예: 에러 메시지 문자열 또는 정수 등)일 경우, len(raw) 호출 시 TypeError가 발생할 수 있습니다. try-except 블록으로 예외가 처리되기는 하지만, isinstance(raw, (list, tuple))를 통해 타입을 명확히 검증한 후 길이를 체크하는 것이 방어적 프로그래밍 관점에서 더 안전합니다.

Suggested change
if raw is None or len(raw) < 2:
raise ValueError(f"unexpected redis script result: {raw!r}")
if not isinstance(raw, (list, tuple)) or len(raw) < 2:
raise ValueError(f"unexpected redis script result: {raw!r}")

@qkwltkwkd1 qkwltkwkd1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다 고생하셨습니다

@selnem selnem merged commit 52ee2a6 into develop Jun 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⭐ feat 새로운 기능

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT]ai 사용횟수 제한 기능 구현

2 participants