Skip to content

Discord /stats 슬래시커맨드로 대시보드 지표 대화형 조회 - #671

Merged
sevineleven merged 3 commits into
devfrom
feat/664-discord-stats-slash
Jul 4, 2026
Merged

Discord /stats 슬래시커맨드로 대시보드 지표 대화형 조회#671
sevineleven merged 3 commits into
devfrom
feat/664-discord-stats-slash

Conversation

@sevineleven

@sevineleven sevineleven commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Situation

Task

  • Discord 슬래시커맨드로 대시보드 지표를 대화형 조회한다. 대시보드 대체가 아니라 병존 (시각 탐색은 대시보드, 빠른 질의는 Discord).
  • 집계 로직은 재사용하고 Discord 는 얇은 표현 계층으로만 둔다. LLM 없이 드롭다운 선택으로 조회한다.

Action

설계 결정

  • LLM 없이 구조화 선택: 자유 자연어 파싱(Gemini)을 넣지 않고, period(오늘/어제/최근 7일/최근 30일)·metric(요약/가입/위시/토너먼트/푸시) 드롭다운 choices 로 조회한다. period 는 MetricsService 의 preset(today/yesterday/7d/30d)에 1:1 로 맞춰 집계 코드를 그대로 재사용한다.
  • 인터랙션 라우팅: Discord 앱당 인터랙션 URL 이 1개라, 기존 /admin-access/discord 컨트롤러를 공통 게이트(서명 검증, PING, 채널, allowlist) + data.name 라우팅으로 분해했다. 기존 piki-admin 로직은 별도 핸들러로 옮겨 회귀 없이 보존한다.
커맨드 처리
piki-admin 선택 env 의 원타임 grant 링크 발급 (기존 동작)
stats period·metric 옵션으로 MetricsService.snapshot() 조회 후 ephemeral embed

구현

  • 공통 유틸/인터페이스: DiscordInteractions(옵션·유저·응답 조립), DiscordCommandHandler·DiscordInteraction(라우팅 계약).
  • 핸들러 분리: AdminGrantCommandHandler(piki-admin), StatsCommandHandler(stats, excludeInternal=true 개발진 제외 기본).
  • 표현 계층: StatsEmbed(metric 섹션별 embed), StatsMetric·StatsPeriod(옵션 파싱, 누락·미지원은 안전 기본값).

등록 방식

  • discord-register-commands.yml 워크플로 추가 — workflow_dispatchsecrets.DISCORD_BOT_TOKEN 을 러너 안에서만 써서 커맨드를 등록한다. 로컬에서 봇 토큰을 다룰 필요가 없다.

테스트

  • 순수 단위 TDD: embed 매핑(StatsEmbedTest)·옵션 파싱(StatsOptionTest)·인터랙션 파싱(DiscordInteractionsTest). 집계 자체는 기존 MetricsDashboardIntegrationTest 가 통합으로 커버해 중복하지 않는다.

Result

  • /stats period:.. metric:.. 로 admin 채널에서 지표를 ephemeral 카드로 즉시 조회할 수 있다.
  • /stats 는 인터랙션을 받는 prod 서버의 DB 만 조회한다(Interactions URL 이 prod 하나). dev/staging 통계까지 필요하면 env 옵션과 env별 조회 경로 설계가 필요해 후속으로 남긴다.
  • admin.enabled=false 인 test 컨텍스트라 /admin-access/discord 엔드포인트 통합테스트는 이 범위 밖이다(전역 컨텍스트 영향). admin 통합테스트 인프라는 별도 이슈 후보.
  • 커맨드 등록·Interactions URL 등록은 운영 셋업(admin Discord 슬래시커맨드·Interactions URL 운영 셋업 (#654 후속) #670)에서 이어진다.

연관 이슈

Summary by CodeRabbit

  • New Features
    • Discord에서 수동으로 명령을 등록할 수 있는 워크플로가 추가되었습니다.
    • /piki-admin 명령으로 환경별 관리자 인증 링크를 발급할 수 있게 되었습니다.
    • /stats 명령으로 기간과 지표를 선택해 통계 요약을 바로 확인할 수 있습니다.
  • Bug Fixes
    • Discord 인터랙션 처리 흐름이 정리되어, 잘못된 명령·권한·채널 상황에서 더 명확한 응답을 제공합니다.
    • 만료되었거나 이미 사용한 인증 토큰은 즉시 안내되도록 개선되었습니다.

- DiscordAccessController를 공통 게이트(서명·PING·채널·allowlist) + data.name 라우팅으로 분해
- piki-admin 로직을 AdminGrantCommandHandler로 이동, StatsCommandHandler 신규 추가
- /stats period·metric 옵션 → MetricsService.snapshot 재사용 → ephemeral embed (LLM 없음, 개발진 제외 기본)
- 순수 단위 TDD: StatsEmbed·StatsMetric·StatsPeriod·DiscordInteractions
- workflow_dispatch 로 Actions 버튼 실행, secrets.DISCORD_BOT_TOKEN 사용(로컬 curl 불필요)
- 길드 스코프 POST upsert, command 입력으로 all/piki-admin/stats 선택
- HTTP 200/201 확인 + 실패 시 응답 본문과 함께 에러
@sevineleven sevineleven added the feat 외부 가시적 새 기능 label Jul 4, 2026
@sevineleven sevineleven self-assigned this Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sevineleven, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 0cb08172-d19c-41da-a79e-4f751feec3ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4c47d0f and fbe17e4.

📒 Files selected for processing (2)
  • src/main/kotlin/com/depromeet/piki/admin/access/DiscordAccessController.kt
  • src/test/kotlin/com/depromeet/piki/admin/access/DiscordInteractionsTest.kt

Walkthrough

Discord 슬래시커맨드 처리를 위한 공통 인터랙션 헬퍼(DiscordInteractions)와 DiscordCommandHandler 인터페이스가 추가되고, DiscordAccessController가 핸들러 라우팅 구조로 재구성됐다. /piki-admin(AdminGrantCommandHandler)과 /stats(StatsCommandHandler, StatsPeriod, StatsMetric, StatsEmbed) 커맨드가 신설되고, 커맨드 등록용 GitHub Actions 워크플로와 단위 테스트가 함께 추가됐다.

Changes

Discord 슬래시커맨드 인프라 및 핸들러

Layer / File(s) Summary
공통 인터랙션 파싱/응답 헬퍼 및 핸들러 계약
src/main/kotlin/.../DiscordInteractions.kt, src/test/kotlin/.../DiscordInteractionsTest.kt
DiscordInteractions 오브젝트가 커맨드명/옵션값/사용자ID·표시명 파싱과 pong/ephemeral embed 응답 조립을 제공하고, DiscordInteraction 데이터 클래스와 DiscordCommandHandler 인터페이스를 정의하며 단위 테스트로 파싱 로직을 검증한다.
컨트롤러 핸들러 라우팅 재구성
src/main/kotlin/.../DiscordAccessController.kt
생성자에 handlers: List<DiscordCommandHandler>가 추가되고 handlersByName 맵으로 커맨드를 라우팅하며, PING 즉시 응답, admin 채널 fail-closed 검사, allowlist 게이트, 미등록 커맨드 처리 흐름이 재구성됐다. grant 토큰 소비 실패 시 404 평문 응답 후 조기 반환하도록 변경되고 기존 인라인 링크 발급/임베드 상수 로직이 제거됐다.
/piki-admin 핸들러
src/main/kotlin/.../AdminGrantCommandHandler.kt
env 옵션으로 grant 호스트를 조회하고 존재하지 않으면 에러 embed, 존재하면 grant token 발급 후 /admin-access/grant?token=... 링크가 포함된 성공 embed를 반환한다.
/stats 핸들러 및 기간/지표 파싱, embed 조립
src/main/kotlin/.../StatsPeriod.kt, StatsEmbed.kt, StatsCommandHandler.kt, src/test/kotlin/.../StatsEmbedTest.kt, StatsOptionTest.kt
StatsPeriod/StatsMetric enum과 from() 팩토리(기본값 폴백 포함)가 추가되고, StatsCommandHandler가 옵션 파싱 후 MetricsService로 스냅샷을 조회하며, StatsEmbed.build가 metric별 필드를 담은 embed를 생성한다. 단위 테스트가 파싱과 embed 필드 매핑을 검증한다.
커맨드 등록 워크플로
.github/workflows/discord-register-commands.yml
수동 실행(workflow_dispatch)으로 piki-admin/stats 커맨드를 Discord 길드 API에 등록하는 워크플로가 추가됐다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Discord
  participant DiscordAccessController
  participant DiscordInteractions
  participant DiscordCommandHandler
  participant MetricsService

  Discord->>DiscordAccessController: POST /admin-access/discord (인터랙션)
  DiscordAccessController->>DiscordAccessController: Ed25519 서명 검증
  DiscordAccessController->>DiscordAccessController: admin 채널 검증
  DiscordAccessController->>DiscordInteractions: userId/userName 추출
  DiscordAccessController->>DiscordAccessController: allowlist 검사
  DiscordAccessController->>DiscordCommandHandler: handle(DiscordInteraction)
  alt /stats 커맨드
    DiscordCommandHandler->>MetricsService: resolveRange(), snapshot()
    MetricsService-->>DiscordCommandHandler: MetricsSnapshot
  end
  DiscordCommandHandler-->>DiscordAccessController: embed 응답 Map
  DiscordAccessController-->>Discord: embed 응답
Loading

Assessment against linked issues

Objective Addressed Explanation
슬래시커맨드로 기간별 통계 조회 (#664)
기존 지표 로직(MetricsService) 재사용, 집계 중복 금지 (#664)
봇 인터랙션 엔드포인트(Ed25519 서명 검증) + 슬래시커맨드 등록 (#664)
허용된 Discord userId만 인증(admin allowlist) (#664)
Gemini 자연어 질의 파싱 (2단계, 선택 사항) (#664) 구조화 파라미터 기반 옵션 파싱만 구현되었고 Gemini 연동은 포함되지 않음(선택 항목)

Estimated code review effort: 3 (Moderate) | ~25 minutes


한 마디 짧고 위트있게: PING엔 즉답, allowlist는 철벽, 예외처리로 404까지 깔끔! 다만 Gemini는 다음 라운드를 위해 남겨두신 센스, 좋습니다 🎯

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/664-discord-stats-slash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sevineleven

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
src/test/kotlin/com/depromeet/piki/admin/access/DiscordInteractionsTest.kt (1)

8-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

allowlist 게이트 핵심 입력값인 userId/userName의 "없음" 폴백 케이스 테스트가 빠져있습니다.

DiscordAccessControllerDiscordInteractions.userId(root) 결과로 discordAdminUserIds 포함 여부를 체크합니다(allowlist 게이트). member.user.id가 없는 페이로드(예: DM 인터랙션 등 예상 밖 payload)가 들어오면 userId()가 빈 문자열을 반환하는데, 이 fail-safe 동작이 테스트로 고정되어 있지 않습니다. 또한 userName()의 최종 폴백값 "unknown", pong()/embed()의 응답 구조(type, flags, embeds)도 검증 대상에서 빠져 있습니다.

보안 게이트에 직결되는 순수 함수인 만큼 아래와 같은 케이스를 추가하면 회귀를 더 확실히 잡을 수 있습니다.

`@Test`
fun `member user 정보가 없으면 userId 는 빈 문자열이다`() {
    val root = mapper.readTree("""{"data":{"name":"stats"}}""")
    assertEquals("", DiscordInteractions.userId(root))
    assertEquals("unknown", DiscordInteractions.userName(root))
}

`@Test`
fun `embed 응답은 ephemeral flag 를 포함한다`() {
    val res = DiscordInteractions.embed(DiscordInteractions.COLOR_RED, "t", "d")
    assertEquals(DiscordInteractions.FLAG_EPHEMERAL, (res["data"] as Map<*, *>)["flags"])
}

참고로 관련 경로 지침에 따르면 "핵심 비즈니스 규칙, 예외 케이스, 경계값 검증이 충분한지" 확인이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/kotlin/com/depromeet/piki/admin/access/DiscordInteractionsTest.kt`
around lines 8 - 37, `DiscordInteractionsTest` is missing coverage for fail-safe
fallbacks that the allowlist gate depends on. Add tests around
`DiscordInteractions.userId()` and `DiscordInteractions.userName()` to verify
missing `member.user.id` returns an empty string and missing name fields fall
back to `"unknown"`, and also add response-shape assertions for `pong()` and
`embed()` (including `type`, `flags`, and `embeds`) so the behavior stays fixed.

Source: Path instructions

src/main/kotlin/com/depromeet/piki/admin/access/DiscordInteractions.kt (1)

18-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

옵션 값이 문자열이 아니면 조용히 빈 문자열로 처리됩니다.

optionValuevalue가 문자열(isString)일 때만 값을 반환하고, 그 외 타입(숫자/불리언 등)이면 빈 문자열을 돌려줍니다. 지금 스택의 /stats period/metric은 드롭다운 STRING 옵션이라 문제없지만, 이후 다른 커맨드에서 INTEGER/BOOLEAN 옵션을 추가하면 이 헬퍼를 그대로 재사용하다가 값이 계속 빈 문자열로 나와서 원인 파악에 시간이 걸릴 수 있어요. 주석(17번 줄)에 "문자열 전용"이라는 점을 명시해두면 다음에 재사용할 팀원이 헷갈리지 않을 것 같습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/com/depromeet/piki/admin/access/DiscordInteractions.kt`
around lines 18 - 31, `DiscordInteractions.optionValue` currently only returns a
value when the option `value` is a string, which can silently hide non-string
option types. Update the helper or its documentation to explicitly indicate it
is string-only, and add a clear note near `optionValue` (and its `root`/`name`
parameters) that INTEGER/BOOLEAN options will be returned as empty string unless
a typed extractor is introduced. This will make reuse of the helper in future
commands less error-prone.
src/main/kotlin/com/depromeet/piki/admin/access/AdminGrantCommandHandler.kt (2)

28-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

만료 안내 문구가 grantTokenTtl 설정값과 분리되어 있어요.

메시지에 "3분 내"가 하드코딩돼 있는데, 실제 만료 시간은 AdminProperties.grantTokenTtl(기본 3분이지만 설정으로 조정 가능)에서 옵니다. 운영 중 이 값을 5분으로 늘리면 안내 문구는 여전히 "3분"을 보여줘서 사용자가 실제보다 일찍 링크가 만료됐다고 오해하거나, 반대로 만료된 링크를 계속 유효하다고 믿고 재시도하는 혼선이 생길 수 있습니다.

adminProperties.grantTokenTtl을 분 단위로 변환해서 메시지에 넣어주면 설정 변경에도 항상 일치하는 안내를 줄 수 있어요.

🔧 제안 diff
+        val ttlMinutes = adminProperties.grantTokenTtl.toMinutes()
         return DiscordInteractions.embed(
             DiscordInteractions.COLOR_GREEN,
             "✅ 관리자 인증됨 — ${interaction.userName}",
-            "**$env** 접속: 이 기기에서 3분 내 아래 링크를 여세요 (그 기기 IP 가 등록됩니다).\n$link",
+            "**$env** 접속: 이 기기에서 ${ttlMinutes}분 내 아래 링크를 여세요 (그 기기 IP 가 등록됩니다).\n$link",
         )

As per path instructions, "운영 리스크 — 민감 로그, 외부 API 타임아웃/재시도, 설정 영향, null"을 우선 검토해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/com/depromeet/piki/admin/access/AdminGrantCommandHandler.kt`
at line 28, The expiration 안내 in AdminGrantCommandHandler is hardcoded to “3분”
and should be tied to the actual grant token TTL. Update the message building
logic in AdminGrantCommandHandler to use adminProperties.grantTokenTtl converted
to minutes, so the displayed text always matches the configured expiration time.
Keep the existing link/message structure intact and make the TTL value the
single source of truth for the 안내 문구.

Source: Path instructions


18-24: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

토큰 "발급" 시점의 감사 로그가 빠져 있어요.

DiscordAccessController.grant()(같은 코호트, 이미 병합된 코드)는 토큰 소비 성공/실패를 AdminAuditAction.ACCESS_GRANTED / ACCESS_DENIED로 남기는데, 정작 이 핸들러에서 토큰을 발급하는 순간에는 아무 기록이 없습니다. 누가, 언제, 어떤 env로 grant 링크를 요청했는지는 추적할 수 없고, 실제로 클릭해서 소비할 때만 흔적이 남는 구조예요.

허용된 관리자만 도달 가능한 경로라 당장 보안 사고로 이어지진 않지만, "발급됐지만 아직 안 쓴 링크가 몇 개인지", "특정 계정이 비정상적으로 자주 링크를 요청하는지" 같은 사후 추적이 불가능해집니다. AdminAuditService를 주입받아 발급 이벤트도 남겨두면 좋겠습니다.

class AdminGrantCommandHandler(
    private val allowlistService: AdminAllowlistService,
    private val adminProperties: AdminProperties,
    private val auditService: AdminAuditService,
) : DiscordCommandHandler {
    ...
    val token = allowlistService.issueGrantToken(interaction.userId, interaction.userName, env)
    auditService.record(interaction.userName, AdminAuditAction.GRANT_TOKEN_ISSUED, "env=$env grant 토큰 발급", interaction.clientIp)

(AdminAuditActionGRANT_TOKEN_ISSUED 같은 값 추가가 필요할 수 있습니다.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/com/depromeet/piki/admin/access/AdminGrantCommandHandler.kt`
around lines 18 - 24, The token issuance path in
AdminGrantCommandHandler.handle() is missing an audit record, so add logging for
the moment a grant link is created. Inject AdminAuditService into
AdminGrantCommandHandler, and after allowlistService.issueGrantToken(...) record
an audit event with the requester identity, env, and client IP using a new
AdminAuditAction such as GRANT_TOKEN_ISSUED. Keep the existing grant link
generation flow unchanged and ensure the audit message clearly indicates token
issuance rather than token consumption.
.github/workflows/discord-register-commands.yml (2)

36-50: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

curl 타임아웃 미설정 — 네트워크 이슈 시 워크플로가 무기한 대기

curl--max-time/--connect-timeout이 없어 Discord API가 응답을 지연하거나 네트워크 파티션이 발생하면 job이 기본 타임아웃(6시간)까지 붙잡혀 있게 됩니다. 수동 실행 워크플로라 자주 발생하진 않겠지만, 외부 API 호출에는 타임아웃을 거는 게 안전합니다.

🔧 제안 수정
             code=$(curl -s -o /tmp/resp.json -w '%{http_code}' \
+              --connect-timeout 5 --max-time 15 \
               -X POST "https://discord.com/api/v10/applications/$APP_ID/guilds/$GUILD_ID/commands" \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/discord-register-commands.yml around lines 36 - 50, The
curl call inside register() has no timeout, so the workflow can hang on Discord
API/network issues; update the curl invocation to include both a connection
timeout and a total max time. Keep the existing success/error handling in
register(), but make sure the request fails fast instead of waiting
indefinitely.

Source: Path instructions


24-27: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

permissions 블록 없음 — 최소 권한 원칙 미준수

이 워크플로는 GITHUB_TOKEN을 전혀 사용하지 않는데도(Discord API 호출만 함) 명시적 permissions가 없어 리포지토리 기본 설정(경우에 따라 read-write)이 그대로 적용됩니다. 불필요한 권한을 명시적으로 차단해주세요.

🔧 제안 수정
+permissions:
+  contents: read
+
 jobs:
   register:
     runs-on: ubuntu-latest

As per path instructions, .github/workflows/** 파일은 "permissions 가 최소 권한 원칙을 지키는지" 검토가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/discord-register-commands.yml around lines 24 - 27, 워크플로의
register job에는 GITHUB_TOKEN을 쓰지 않으므로 기본 권한을 그대로 두지 말고 최소 권한을 명시적으로 차단하세요.
.github/workflows/discord-register-commands.yml의 register job 정의에 permissions
블록을 추가하고, 해당 job이 필요한 권한만 남기거나 모두 비활성화되도록 설정하세요. jobs.register 아래에 두면 됩니다.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/kotlin/com/depromeet/piki/admin/access/DiscordAccessController.kt`:
- Around line 86-91: The Discord route in DiscordAccessController.discord()
calls handler.handle(...) without any exception handling, so failures from
handlers like StatsCommandHandler can escape and break the interaction response.
Wrap the handler invocation in a try-catch around the existing handler lookup
and execution, log the exception with the command name using
DiscordInteractions.commandName(root), and return a red ephemeral error embed
from DiscordInteractions.embed so users get a failure response instead of a
silent 500.
- Around line 38-40: `DiscordAccessController`에서 `handlers.associateBy {
it.commandName }`가 중복 `commandName`을 조용히 덮어쓰는 문제가 있으니, 생성자 초기화 시점에 `handlers`를
검사해 중복을 발견하면 즉시 실패하도록 수정하세요. `DiscordAccessController`의 `init` 블록(또는
`handlersByName` 생성 전)에서 `commandName`을 기준으로 중복을 찾아 `require`로 예외를 던지면, 배포 후 잘못된
매핑을 방지할 수 있습니다.

In `@src/main/kotlin/com/depromeet/piki/admin/access/StatsCommandHandler.kt`:
- Around line 17-25: The StatsCommandHandler.handle flow currently calls
metricsService.snapshot without any exception handling, so failures can escape
to DiscordAccessController.discord and bypass the normal ephemeral error
response path. Wrap the snapshot/relevant range resolution logic in try-catch
inside StatsCommandHandler.handle, and on failure return the same style of
ephemeral error embed used for other access/command failures so the user sees a
consistent “❌” response instead of a propagated exception.

---

Nitpick comments:
In @.github/workflows/discord-register-commands.yml:
- Around line 36-50: The curl call inside register() has no timeout, so the
workflow can hang on Discord API/network issues; update the curl invocation to
include both a connection timeout and a total max time. Keep the existing
success/error handling in register(), but make sure the request fails fast
instead of waiting indefinitely.
- Around line 24-27: 워크플로의 register job에는 GITHUB_TOKEN을 쓰지 않으므로 기본 권한을 그대로 두지 말고
최소 권한을 명시적으로 차단하세요. .github/workflows/discord-register-commands.yml의 register
job 정의에 permissions 블록을 추가하고, 해당 job이 필요한 권한만 남기거나 모두 비활성화되도록 설정하세요.
jobs.register 아래에 두면 됩니다.

In `@src/main/kotlin/com/depromeet/piki/admin/access/AdminGrantCommandHandler.kt`:
- Line 28: The expiration 안내 in AdminGrantCommandHandler is hardcoded to “3분”
and should be tied to the actual grant token TTL. Update the message building
logic in AdminGrantCommandHandler to use adminProperties.grantTokenTtl converted
to minutes, so the displayed text always matches the configured expiration time.
Keep the existing link/message structure intact and make the TTL value the
single source of truth for the 안내 문구.
- Around line 18-24: The token issuance path in
AdminGrantCommandHandler.handle() is missing an audit record, so add logging for
the moment a grant link is created. Inject AdminAuditService into
AdminGrantCommandHandler, and after allowlistService.issueGrantToken(...) record
an audit event with the requester identity, env, and client IP using a new
AdminAuditAction such as GRANT_TOKEN_ISSUED. Keep the existing grant link
generation flow unchanged and ensure the audit message clearly indicates token
issuance rather than token consumption.

In `@src/main/kotlin/com/depromeet/piki/admin/access/DiscordInteractions.kt`:
- Around line 18-31: `DiscordInteractions.optionValue` currently only returns a
value when the option `value` is a string, which can silently hide non-string
option types. Update the helper or its documentation to explicitly indicate it
is string-only, and add a clear note near `optionValue` (and its `root`/`name`
parameters) that INTEGER/BOOLEAN options will be returned as empty string unless
a typed extractor is introduced. This will make reuse of the helper in future
commands less error-prone.

In `@src/test/kotlin/com/depromeet/piki/admin/access/DiscordInteractionsTest.kt`:
- Around line 8-37: `DiscordInteractionsTest` is missing coverage for fail-safe
fallbacks that the allowlist gate depends on. Add tests around
`DiscordInteractions.userId()` and `DiscordInteractions.userName()` to verify
missing `member.user.id` returns an empty string and missing name fields fall
back to `"unknown"`, and also add response-shape assertions for `pong()` and
`embed()` (including `type`, `flags`, and `embeds`) so the behavior stays fixed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 8076a20a-2db6-4481-a0c5-0cce2c2be17b

📥 Commits

Reviewing files that changed from the base of the PR and between bdbc6ff and 4c47d0f.

📒 Files selected for processing (10)
  • .github/workflows/discord-register-commands.yml
  • src/main/kotlin/com/depromeet/piki/admin/access/AdminGrantCommandHandler.kt
  • src/main/kotlin/com/depromeet/piki/admin/access/DiscordAccessController.kt
  • src/main/kotlin/com/depromeet/piki/admin/access/DiscordInteractions.kt
  • src/main/kotlin/com/depromeet/piki/admin/access/StatsCommandHandler.kt
  • src/main/kotlin/com/depromeet/piki/admin/access/StatsEmbed.kt
  • src/main/kotlin/com/depromeet/piki/admin/access/StatsPeriod.kt
  • src/test/kotlin/com/depromeet/piki/admin/access/DiscordInteractionsTest.kt
  • src/test/kotlin/com/depromeet/piki/admin/access/StatsEmbedTest.kt
  • src/test/kotlin/com/depromeet/piki/admin/access/StatsOptionTest.kt

@github-actions
github-actions Bot requested a review from m-a-king July 4, 2026 10:02
- handler.handle 예외를 runCatching 으로 감싸 에러 embed 로 응답 (인터랙션 3초 타임아웃·상호작용 실패 방지)
- 커맨드명 중복 시 init require 로 부팅 실패 (associateBy 의 조용한 덮어쓰기 차단)
- DiscordInteractions userId/userName 폴백·embed·pong 응답 구조 단위테스트 추가
@sevineleven
sevineleven merged commit 3406de1 into dev Jul 4, 2026
12 checks passed
@sevineleven
sevineleven deleted the feat/664-discord-stats-slash branch July 4, 2026 10:15
@sevineleven

Copy link
Copy Markdown
Collaborator Author

CodeRabbit 리뷰 대응 정리

Actionable (3, 전부 accept + resolve)

  • 커맨드명 중복 → init require 부팅 가드 (fbe17e4)
  • handler.handle() 예외 격리 → runCatching 에러 embed (fbe17e4)
  • snapshot() 예외 → 라우팅 경계 한 곳에서 함께 격리 (fbe17e4)

Nitpick (6)

  • DiscordInteractions userId/userName 폴백·embed/pong 응답 구조 테스트 (fbe17e4)
  • ✅ grant 안내 TTL 을 grantTokenTtl 설정값 연동 (c16e239)
  • ✅ 등록 워크플로 permissions: {} + curl --max-time 30 (c16e239)
  • ⏭️ 옵션값 비문자열 → 빈 문자열: Discord 옵션은 항상 문자열(type 3)이라 의도된 방어, 유지
  • ⏭️ grant 발급 시점 audit(GRANT_TOKEN_ISSUED): 감사 범위 확장이라 후속으로 분리

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.

Discord 대화형 통계 조회 (슬래시커맨드 + 지표 재사용)

1 participant