Skip to content

feat : Claude Code Skills, Agent, md 구축#86

Open
Juhye0k wants to merge 3 commits intodevfrom
ranking
Open

feat : Claude Code Skills, Agent, md 구축#86
Juhye0k wants to merge 3 commits intodevfrom
ranking

Conversation

@Juhye0k
Copy link
Copy Markdown
Contributor

@Juhye0k Juhye0k commented Apr 3, 2026

🚀 1. 개요

  • Claude Code md, Skill, Agent 설정

📝 2. 주요 변경 사항

  • 전역적 CLAUDE.md를 1개로 관리하고, 각 디렉토리별로 별도의 CLAUDE.md를 설정했습니다.
  • Skill.md 정의 (spring core, 리뷰, 테스트 등등)
  • SubAgent 설정

Summary by CodeRabbit

릴리스 노트

  • 버그 수정

    • 최대 집중 스터디 세션의 종료 시간 계산 로직을 개선했습니다. (분 단위에서 시간 단위로 수정)
  • 개선 사항

    • 내부 아키텍처 및 테스트 문서를 재구성하고 통일된 개발 가이드라인을 적용했습니다.
    • FCM 푸시 알림 서비스의 예외 처리 로직을 강화했습니다.

@Juhye0k Juhye0k requested a review from kon28289 April 3, 2026 07:53
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

Walkthrough

PR은 레거시 AI 설명서를 제거하고, Claude 에이전트/스킬 정의 모음을 추가하여 코드 리뷰, 테스트, API 설명서, 백엔드 아키텍처 설계, 장애 대응 워크플로우를 자동화합니다. 도메인별 설명서 재구성, FCM 토큰 및 스터디 세션 시간 단위 조정(분→시간), MCP 서버 구성도 포함됩니다.

Changes

Cohort / File(s) Summary
문서 제거
.ai/ARCHITECTURE.md, .ai/TESTING.md, .ai/USE-CASES.md
기존 시스템 아키텍처, 테스트 전략, 유스케이스 문서 제거 (총 848줄 삭제)
Claude 에이전트 정의
.claude/agents/api-documenter.md, .claude/agents/backend-architect.md, .claude/agents/code-reviewer.md, .claude/agents/test-engineer.md
API 문서화, 백엔드 아키텍처 설계, 코드 리뷰, 테스트 자동화를 위한 에이전트 역할 및 체크리스트 정의
장애 대응 스킬 및 참조
.claude/skills/incident-response/SKILL.md, .claude/skills/incident-response/references/grafana-queries.md, .claude/skills/incident-response/references/phase-guide.md, .claude/skills/incident-response/references/templates.md
6 Phase 장애 대응 워크플로우, Grafana 모니터링 쿼리, 의사결정 가이드, Jira/PR/회고 템플릿 추가
스킬 생성 자동화
.claude/skills/skill-creator/SKILL.md, .claude/skills/skill-creator/agents/analyzer.md, .claude/skills/skill-creator/agents/comparator.md, .claude/skills/skill-creator/agents/grader.md, .claude/skills/skill-creator/assets/eval_review.html, .claude/skills/skill-creator/eval-viewer/generate_review.py, .claude/skills/skill-creator/eval-viewer/viewer.html
스킬 개발 반복 최적화 파이프라인: 평가(evals), 채점(grading), 비교(blind comparison), 분석, HTML 뷰어 구현
스킬 생성 스크립트
.claude/skills/skill-creator/references/schemas.md, .claude/skills/skill-creator/scripts/aggregate_benchmark.py, .claude/skills/skill-creator/scripts/generate_report.py, .claude/skills/skill-creator/scripts/improve_description.py, .claude/skills/skill-creator/scripts/package_skill.py, .claude/skills/skill-creator/scripts/quick_validate.py, .claude/skills/skill-creator/scripts/run_eval.py, .claude/skills/skill-creator/scripts/run_loop.py, .claude/skills/skill-creator/scripts/utils.py
평가 실행, 설명 개선, 패키징, 검증을 위한 Python 스크립트 및 JSON 스키마 정의
Spring 개발 스킬
.claude/skills/spring-core/SKILL.md, .claude/skills/spring-core/references/exceptions.md, .claude/skills/spring-core/references/templates.md, .claude/skills/spring-test/SKILL.md, .claude/skills/spring-test/evals/evals.json
Spring Boot 계층 아키텍처, 예외 처리, 코드 템플릿(Entity/DTO/Repository/Service/Controller), 테스트 작성 가이드 및 평가셋
스프링 리뷰 스킬
.claude/skills/spring-review/SKILL.md
Geumpumta 코드 리뷰 기준: 아키텍처, 인증/인가, 타임스탬프 신뢰성, 예외 처리, 응답 형식, 트랜잭션, JPA/쿼리, 입력 검증, 보안, 캐시/시즌 등 10개 카테고리
MCP 설정
.mcp.json
Context7 및 Sequential Thinking MCP 서버 프로세스 구성
온보딩 문서 축약
CLAUDE.md
상세 기술 설명 제거, 절대 규칙(서버 시간, 보안 설정 커밋 금지) 및 아키텍처 개요만 유지
도메인별 설명서 추가
src/main/java/com/gpt/geumpumtabackend/fcm/CLAUDE.md, src/main/java/com/gpt/geumpumtabackend/rank/CLAUDE.md
FCM 토큰 라이프사이클, 재시도 전략, 알림 트리거; 랭킹 병합 절차, 시즌 라이프사이클, 동점 처리 상세 명시
FCM 코드 변경
src/main/java/com/gpt/geumpumtabackend/fcm/exception/PermanentFcmException.java, src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmMessageSender.java, src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmService.java
PermanentFcmException 클래스 추가, 재시도 불가능 에러 처리 로직 개선, 트랜잭션 어노테이션 제거
스터디 세션 시간 단위 수정
src/main/java/com/gpt/geumpumtabackend/study/domain/StudySession.java, src/main/java/com/gpt/geumpumtabackend/study/service/StudySessionService.java, src/main/java/com/gpt/geumpumtabackend/study/scheduler/MaxFocusStudyScheduler.java
최대 집중 세션 종료 시 시간 기반 계산(plusHours 적용), 만료 세션 탐색 cutoff 시간 단위 일관성 유지, 스케줄러 예외 처리 단순화
임포트 정리
src/main/java/com/gpt/geumpumtabackend/board/dto/BoardListResponse.java, src/main/java/com/gpt/geumpumtabackend/rank/dto/response/SeasonDepartmentRankingResponse.java
미사용 임포트 제거

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement, documentation, refactor

Suggested reviewers

  • kon28289

Poem

🐰 문서 정리하고 에이전트 준비,
스킬 자동화로 리뷰는 빠르게,
시간을 시간으로 정확히 세우니,
장애 대응 5분, 복구는 순식간!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 Claude Code Skills, Agent, md 구축을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 요구되는 템플릿 구조(개요, 주요 변경 사항, 스크린샷)를 따르고 있으나, 스크린샷 섹션이 비어있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (19)
.claude/agents/backend-architect.md-4-4 (1)

4-4: ⚠️ Potential issue | 🟠 Major

설계 전용 에이전트에 과권한 도구가 포함되어 있습니다.

Line 4에서 Write, Edit, Bash까지 기본 허용하면, 아키텍처 제안 단계에서 불필요한 변경/실행 권한이 열립니다. 기본은 Read, Grep, Glob로 제한하고, 산출물 생성이 꼭 필요할 때만 단계적으로 권한을 열어주는 구성이 더 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/agents/backend-architect.md at line 4, The agent configuration line
containing the tools list ("tools: Read, Write, Edit, Bash, Grep, Glob") grants
excessive privileges for an architecture-only agent; change that declaration to
the minimal set ("tools: Read, Grep, Glob") and remove Write, Edit, Bash from
the default tools list in the same config entry for the backend-architect agent,
and update any related comments or documentation to state that Write/Edit/Bash
will be granted only in later steps when artifact creation or execution is
explicitly required.
.claude/agents/code-reviewer.md-4-4 (1)

4-4: ⚠️ Potential issue | 🟠 Major

코드 리뷰 전용 에이전트 권한이 과도합니다.

Line 4의 Write, Edit, Bash는 리뷰 범위를 넘어 의도치 않은 변경/실행을 유발할 수 있습니다. 기본은 읽기 전용으로 두고, 수정 에이전트로 위임하는 구성이 안전합니다.

권한 최소화 예시
-tools: Read, Write, Edit, Bash, Glob, Grep
+tools: Read, Glob, Grep
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/agents/code-reviewer.md at line 4, The agent's permission list
"tools: Read, Write, Edit, Bash, Glob, Grep" is overly broad; remove or revoke
the dangerous capabilities (Write, Edit, Bash) and leave only
read/listing/search privileges (e.g., "Read, Glob, Grep") so the reviewer
remains read-only; if editing/execution is needed, delegate those actions to a
separate modifier agent or CI job and document that delegation beside the
permission string.
.mcp.json-6-14 (1)

6-14: ⚠️ Potential issue | 🟠 Major

외부 패키지 버전을 명시적으로 고정해주세요.

npx -y @upstash/context7-mcp와 `npx -y `@modelcontextprotocol/server-sequential-thinking(7행, 14행)처럼 버전을 지정하지 않으면 실행 시마다 최신 버전을 가져오므로, 브레이킹 체인지로 인한 예기치 않은 오류 발생 가능성과 CI/로컬/운영 환경 간 동작 불일치 위험이 있습니다. 현재 사용 가능한 최신 버전(@upstash/context7-mcp@2.1.6, @modelcontextprotocol/server-sequential-thinking@2025.12.18)을 참고하여 버전을 명시적으로 고정하기를 권장합니다.

예시 변경
     "context7": {
       "command": "npx",
       "args": [
         "-y",
-        "@upstash/context7-mcp"
+        "@upstash/context7-mcp@2.1.6"
       ]
     },
     "sequential-thinking": {
       "command": "npx",
       "args": [
         "-y",
-        "@modelcontextprotocol/server-sequential-thinking"
+        "@modelcontextprotocol/server-sequential-thinking@2025.12.18"
       ]
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.mcp.json around lines 6 - 14, Pin the external packages in the npx args to
specific versions: replace the occurrences of "@upstash/context7-mcp" and
"@modelcontextprotocol/server-sequential-thinking" in the args arrays with their
fixed versions (use `@upstash/context7-mcp`@2.1.6 and
`@modelcontextprotocol/server-sequential-thinking`@2025.12.18) so the commands
that use these identifiers always fetch the specified, versioned packages.
.claude/agents/test-engineer.md-215-229 (1)

215-229: ⚠️ Potential issue | 🟠 Major

외부 바이너리 의존성: 호스트에 설치되지 않을 수 있습니다.

Lines 220-221에서 pg_isreadyredis-cli를 호스트에서 직접 실행하고 있습니다. 이 도구들이 설치되지 않은 환경에서는 실패합니다. Docker 컨테이너가 사용 중이므로 docker-compose exec 또는 HTTP health check를 통해 준비 상태를 확인하세요.

🐳 Docker 컨테이너 상태 확인으로 개선
  async waitForServices(timeout = 30000) {
    const startTime = Date.now();
    
    while (Date.now() - startTime < timeout) {
      try {
-        execSync('pg_isready -h localhost -p 5433', { stdio: 'pipe' });
-        execSync('redis-cli -p 6380 ping', { stdio: 'pipe' });
+        // Check Postgres via docker-compose
+        execSync('docker-compose -f docker-compose.test.yml exec -T postgres pg_isready', { stdio: 'pipe' });
+        // Check Redis via docker-compose
+        execSync('docker-compose -f docker-compose.test.yml exec -T redis redis-cli ping', { stdio: 'pipe' });
        return; // Services are ready
      } catch (error) {
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }
    
    throw new Error('Test services failed to start within timeout');
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/agents/test-engineer.md around lines 215 - 229, The waitForServices
function currently shells out with execSync to run external binaries (pg_isready
and redis-cli) which may not exist on the host; replace those execSync checks in
waitForServices with container-aware health checks: either run the commands via
docker-compose exec / docker exec (so pg_isready/redis-cli run inside the
DB/Redis containers) or, better, probe the services directly using native
clients (e.g., a small Postgres connection attempt with node-postgres and a
Redis PING via ioredis) or query Docker health status (docker inspect /
docker-compose ps health) to determine readiness; update the code paths that
call execSync to use the chosen container-aware/connection-based check and keep
the same timeout/retry loop logic.
.claude/agents/test-engineer.md-633-670 (1)

633-670: ⚠️ Potential issue | 🟠 Major

0으로 나누기 및 빈 배열 처리: 요청이 없는 경우를 처리하세요.

Lines 647, 649, 654에서 requests.length가 0일 때 0으로 나누기가 발생하고, lines 652-658에서 빈 responseTimes 배열에 대한 Math.min/max 호출이 Infinity를 반환합니다. 요청이 없는 경우를 명시적으로 처리해야 합니다.

🛡️ 빈 결과에 대한 방어 코드 추가
  analyzeResults(results) {
    const { requests, errors, startTime, endTime } = results;
    const totalDuration = endTime - startTime;
    
+    // Handle case when no requests were made
+    if (requests.length === 0) {
+      return {
+        summary: {
+          totalRequests: 0,
+          successfulRequests: 0,
+          failedRequests: errors.length,
+          errorRate: errors.length > 0 ? 1 : 0,
+          testDuration: totalDuration,
+          throughput: 0
+        },
+        responseTime: {
+          min: 0,
+          max: 0,
+          mean: 0,
+          p50: 0,
+          p90: 0,
+          p95: 0,
+          p99: 0
+        },
+        errors: {
+          total: errors.length,
+          byType: this.groupBy(errors, 'type'),
+          timeline: errors.map(e => ({ timestamp: e.timestamp, type: e.type }))
+        },
+        recommendations: []
+      };
+    }
+    
    // Calculate metrics
    const responseTimes = requests.map(r => r.duration);
    const successfulRequests = requests.filter(r => r.status < 400);
    const failedRequests = requests.filter(r => r.status >= 400);
    
    const analysis = {
      summary: {
        totalRequests: requests.length,
        successfulRequests: successfulRequests.length,
        failedRequests: failedRequests.length + errors.length,
        errorRate: (failedRequests.length + errors.length) / requests.length,
        testDuration: totalDuration,
        throughput: (requests.length / totalDuration) * 1000 // requests per second
      },
      responseTime: {
        min: Math.min(...responseTimes),
        max: Math.max(...responseTimes),
        mean: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length,
        p50: this.percentile(responseTimes, 50),
        p90: this.percentile(responseTimes, 90),
        p95: this.percentile(responseTimes, 95),
        p99: this.percentile(responseTimes, 99)
      },
      errors: {
        total: errors.length,
        byType: this.groupBy(errors, 'type'),
        timeline: errors.map(e => ({ timestamp: e.timestamp, type: e.type }))
      },
      recommendations: this.generatePerformanceRecommendations(
        analysis.summary,
        analysis.responseTime
      )
    };

    this.logResults(analysis);
    return analysis;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/agents/test-engineer.md around lines 633 - 670, The analyzeResults
method doesn't guard against no requests, causing divide-by-zero and
Math.min/Math.max on empty arrays; update analyzeResults to detect when
requests.length === 0 (or responseTimes.length === 0) and return a safe analysis
or populate fields with sensible defaults (e.g., totalRequests: 0,
successfulRequests: 0, failedRequests: errors.length, errorRate: errors.length ?
1 : 0, testDuration: totalDuration, throughput: 0) and ensure responseTime
values (min, max, mean, percentiles) are set to null or 0 when responseTimes is
empty; specifically change the calculations in analyzeResults that use
requests.length, responseTimes.reduce, Math.min/Math.max, and this.percentile to
use guarded logic or early return so no division by zero or Infinity/NaN values
are produced.
.claude/skills/incident-response/SKILL.md-105-111 (1)

105-111: 🛠️ Refactor suggestion | 🟠 Major

Jira 프로젝트 선택 로직의 모호성

"Atlassian MCP에서 사용 가능한 프로젝트 조회 후 적절한 프로젝트 선택"이라는 지시가 모호합니다. 여러 프로젝트가 존재할 때 자동 선택 기준이 명시되지 않아 에이전트가 임의로 선택할 수 있습니다.

다음 중 하나를 명시하세요:

  1. 프로젝트 키를 환경 변수나 설정으로 사전 정의 (예: JIRA_PROJECT_KEY=GEUMP)
  2. 프로젝트 이름 패턴 매칭 규칙 (예: "Geumpumta" 포함된 프로젝트 우선)
  3. 복수 프로젝트 발견 시 사용자에게 선택 요청
개선 예시
Atlassian MCP를 사용해 Jira에 티켓을 생성한다.
- 프로젝트: 환경 변수 `JIRA_PROJECT_KEY` 사용. 미설정 시 "Geumpumta" 이름 포함 프로젝트 선택. 
  복수 발견 시 사용자에게 확인 요청.
- 이슈 타입: Bug
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/SKILL.md around lines 105 - 111, Clarify
the Jira project selection logic in .claude/skills/incident-response/SKILL.md:
state that the agent should first use the environment variable JIRA_PROJECT_KEY
(if set), otherwise fall back to selecting the first project whose name contains
"Geumpumta" (or another documented pattern), and if multiple matching projects
remain, prompt the user to confirm the correct project; update the "프로젝트:"
bullet and include the exact env var name JIRA_PROJECT_KEY and the fallback
pattern so the agent logic (project selection) is unambiguous.
.claude/skills/incident-response/SKILL.md-203-204 (1)

203-204: 🛠️ Refactor suggestion | 🟠 Major

Notion 저장 실패 시 로컬 경로 생성 누락

"연결되지 않았으면 → 마크다운 파일로 로컬 저장"이라고 명시하지만, docs/incidents/ 디렉토리가 존재하지 않을 경우 파일 생성이 실패합니다. phase-guide.md Line 104-108에 mkdir -p 명령이 있지만 SKILL.md에는 명시되지 않았습니다.

Phase 6 실행 순서에 디렉토리 생성 단계를 명시하세요:

개선된 Phase 6 플로우
### Notion MCP 연결 상태 확인

1. Notion MCP 연결 시도
2. **연결 성공** → Notion 페이지에 자동 저장
3. **연결 실패** → 로컬 저장:
   ```bash
   mkdir -p docs/incidents
   # YYYY-MM-DD-{티켓번호}.md 파일 생성

저장 후 Notion 연결 방법 안내 (templates.md 참조)

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/SKILL.md around lines 203 - 204, Update the
Phase 6 flow in SKILL.md to explicitly create the local incidents directory
before writing the markdown file: when Notion MCP connection fails in Phase 6,
add a step that runs a directory-creation command to ensure docs/incidents
exists (e.g., mkdir -p docs/incidents) prior to generating
YYYY-MM-DD-{ticket}.md, and then point users to templates.md for Notion
connection instructions; modify the Phase 6 section header and steps so the
directory creation is clearly listed between "연결 실패 → 로컬 저장" and the file
creation step.


</details>

</blockquote></details>
<details>
<summary>.claude/skills/incident-response/SKILL.md-133-143 (1)</summary><blockquote>

`133-143`: _⚠️ Potential issue_ | _🟠 Major_

**Worktree 브랜치 베이스 선택의 위험성**

Line 142에서 `git worktree add -b "$BRANCH" "$WORKTREE_PATH" dev`로 항상 `dev` 브랜치를 베이스로 사용하는데, 장애가 이미 `main`이나 `prod`에 배포된 경우 `dev`가 최신이 아닐 수 있습니다.


프로덕션 장애의 경우 다음을 고려하세요:
1. 장애가 발생한 브랜치를 베이스로 선택 (prod 장애 시 `main` 또는 `prod` 브랜치)
2. Phase 1에서 영향 범위(prod/dev)를 식별했으므로, 해당 정보를 Phase 4에서 활용
3. `dev` 고정이 의도된 것이라면 "핫픽스는 항상 dev 기준, prod 머지는 별도 진행" 명시

<details>
<summary>조건부 베이스 브랜치 선택 로직 제안</summary>

```bash
# Phase 1의 영향 범위(prod/dev)에 따라 베이스 브랜치 결정
if [[ "$AFFECTED_ENV" == "prod" ]]; then
  BASE_BRANCH="main"  # 또는 "prod"
else
  BASE_BRANCH="dev"
fi

git worktree add -b "$BRANCH" "$WORKTREE_PATH" "$BASE_BRANCH"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/SKILL.md around lines 133 - 143, The script
always uses dev as the base for git worktree (git worktree add -b "$BRANCH"
"$WORKTREE_PATH" dev), which can be incorrect for prod-facing hotfixes; modify
the logic that sets the base branch (used by BRANCH and WORKTREE_PATH) to choose
BASE_BRANCH conditionally from the Phase-1 impact variable (e.g., AFFECTED_ENV)
so that when AFFECTED_ENV=="prod" you set BASE_BRANCH to main or prod, otherwise
dev, and then call git worktree add -b "$BRANCH" "$WORKTREE_PATH"
"$BASE_BRANCH"; alternatively, if using dev is intentional, add a clear comment
stating the policy ("hotfixes based on dev; prod merges handled separately").
.claude/skills/incident-response/references/phase-guide.md-65-70 (1)

65-70: ⚠️ Potential issue | 🟠 Major

Worktree 정리 명령의 안전성 문제

git worktree remove ../hotfix-{이전티켓} 명령을 제시하지만, 해당 worktree에 커밋되지 않은 변경사항이 있을 경우 데이터 손실 위험이 있습니다.

더 안전한 접근 방법:

안전한 worktree 정리 절차
 ### Worktree 경로 충돌 시
 `../hotfix-{티켓번호}` 경로가 이미 존재하면:
 ```bash
 git worktree list  # 기존 worktree 확인
+
+# 변경사항 확인
+cd ../hotfix-{이전티켓}
+git status
+# 커밋되지 않은 변경이 있으면 백업 후 진행
+
+cd -
-git worktree remove ../hotfix-{이전티켓}  # 불필요한 worktree 정리
+git worktree remove ../hotfix-{이전티켓} --force  # 강제 제거 (주의)
+# 또는
+git worktree remove ../hotfix-{이전티켓}  # 변경사항 없을 때만 성공

사용자에게 기존 worktree의 상태를 확인하도록 안내하고, 필요 시 백업이나 커밋을 먼저 수행하도록 경고하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/references/phase-guide.md around lines 65 -
70, Update the "Worktree 경로 충돌 시" section to warn about data loss and add a safe
procedure: instruct users to run git worktree list, cd into the existing
../hotfix-{이전티켓} and run git status to check for uncommitted changes, advise
committing or backing up any changes before removing the worktree, and show that
git worktree remove ../hotfix-{이전티켓} only succeeds when no local changes exist
while git worktree remove ../hotfix-{이전티켓} --force is available as a last-resort
with a clear caution about potential data loss.
src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmMessageSender.java-54-57 (1)

54-57: ⚠️ Potential issue | 🟠 Major

FCM 토큰 원문을 로그에 남기지 마세요.

디바이스 토큰은 민감한 식별자라 warn 로그에 전체 값을 남기면 운영 로그 유출 시 바로 노출됩니다. 토큰 대신 userId, 해시, 또는 suffix만 남기고, 같은 파일의 recovery 로그도 같은 원칙으로 맞추는 편이 안전합니다.

🔒 제안
-                log.warn("FCM permanent error [{}] for token {}: {}", code, messageDto.getToken(), e.getMessage());
+                log.warn("FCM permanent error [{}]: {}", code, e.getMessage());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmMessageSender.java`
around lines 54 - 57, The warn log in FcmMessageSender currently prints the full
device token (messageDto.getToken()); change logging to avoid sensitive token
leakage by logging a non-sensitive identifier instead (e.g.,
messageDto.getUserId() or a hashed/token-suffix), e.g., compute a short SHA256
or last 4-6 chars and log that with context in the warn message and in any
corresponding recovery/error logs; update the log statements around
MessagingErrorCode handling and the recovery path to use the same non-sensitive
representation (refer to MessagingErrorCode, messageDto.getToken(), and
PermanentFcmException to locate the spots).
.claude/skills/skill-creator/agents/comparator.md-13-18 (1)

13-18: ⚠️ Potential issue | 🟠 Major

결과 파일 경로 계약이 입력 정의에 없습니다.

Line 89는 “specified path”에 저장하라고 하지만, Lines 13-18에는 output_path가 없습니다. 이 상태면 배치/병렬 실행에서 모두 comparison.json 기본값으로 떨어져 서로 덮어쓸 수 있으니, 입력 섹션에 output_path를 명시하고 Step 7과 같은 이름으로 맞춰 주세요.

Also applies to: 87-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/agents/comparator.md around lines 13 - 18, The
inputs list is missing the "output_path" contract which Step 7 refers to as the
“specified path”; add "output_path" to the prompt parameters alongside
output_a_path, output_b_path, eval_prompt, and expectations, and ensure its name
exactly matches the identifier used in Step 7 so outputs are written to the
intended location instead of defaulting to comparison.json (which can cause
overwrite in parallel/batch runs).
.claude/skills/skill-creator/scripts/utils.py-24-47 (1)

24-47: ⚠️ Potential issue | 🟠 Major

필수 frontmatter 키 누락을 빈 문자열로 반환하지 마세요.

지금은 name이나 description이 없거나 오타가 나도 delimiter만 맞으면 파싱 성공으로 처리합니다. 이 헬퍼를 쓰는 downstream script는 잘못된 SKILL.md를 조용히 소비하게 되므로, 여기서 바로 ValueError를 던지는 편이 안전합니다.

수정 예시
     while i < len(frontmatter_lines):
         line = frontmatter_lines[i]
         if line.startswith("name:"):
             name = line[len("name:"):].strip().strip('"').strip("'")
@@
             else:
                 description = value.strip('"').strip("'")
         i += 1

+    if not name:
+        raise ValueError("SKILL.md frontmatter missing required field: name")
+    if not description:
+        raise ValueError("SKILL.md frontmatter missing required field: description")
+
     return name, description, content
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/utils.py around lines 24 - 47, The
parser currently returns empty strings when required frontmatter keys are
missing; after the frontmatter loop in the function that computes
frontmatter_lines, name, description and content, validate that name and
description are non-empty and if either is missing raise a ValueError with a
clear message (e.g. include which key is missing). Modify the section after the
while loop that produces name and description so it checks the variables name
and description and raises ValueError instead of returning empty strings;
reference the variables name, description, frontmatter_lines and the return at
the end to locate where to add the validation.
.claude/skills/skill-creator/eval-viewer/viewer.html-1063-1066 (1)

1063-1066: ⚠️ Potential issue | 🟠 Major

완료 상태를 바로 in_progress로 덮어쓰고 있습니다.

showDoneDialog()에서 status: "complete"를 전송한 뒤, 사용자가 OK를 누르면 Line 1065의 saveCurrentFeedback()가 다시 in_progress를 보냅니다. 최신 상태만 보는 소비자에서는 리뷰가 영원히 완료되지 않습니다.

수정 예시
 function closeDoneDialog() {
-      // Reset status back to in_progress
-      saveCurrentFeedback();
       document.getElementById("done-overlay").classList.remove("visible");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/eval-viewer/viewer.html around lines 1063 -
1066, closeDoneDialog currently calls saveCurrentFeedback() which
unconditionally resets the review status to "in_progress", overwriting the
"complete" sent by showDoneDialog; change this so closing the dialog does not
revert status: either update saveCurrentFeedback(status?) to accept an explicit
status or a preserveStatus flag and call saveCurrentFeedback("complete") or
saveCurrentFeedback({preserveStatus: true}) from closeDoneDialog, or remove the
call from closeDoneDialog and only persist feedback when an explicit status
change happens — reference functions closeDoneDialog, showDoneDialog, and
saveCurrentFeedback when making the change.
.claude/skills/skill-creator/eval-viewer/viewer.html-1012-1022 (1)

1012-1022: ⚠️ Potential issue | 🟠 Major

fetch 응답의 HTTP 상태 코드를 확인하지 않습니다.

fetch는 네트워크 오류가 아니면 reject되지 않습니다. 현재 코드는 response.ok를 확인하지 않아, 서버가 4xx/5xx를 반환해도 .then() 핸들러가 실행됩니다. 따라서 auto-save는 "Saved" 메시지를 표시하고 final submit은 완료 오버레이를 보여주지만, 실제로는 저장이 실패합니다.

응답 상태를 검증하도록 수정이 필요합니다:

수정 예시
-      }).then(() => {
+      }).then((resp) => {
+        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
         document.getElementById("feedback-status").textContent = "Saved";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/eval-viewer/viewer.html around lines 1012 -
1022, The fetch auto-save currently treats any HTTP response as success; update
the fetch("/api/feedback", { ... }) promise chain to inspect the Response object
(check response.ok) inside the then handler and throw or reject when
!response.ok so failures move to the catch path; on success set
document.getElementById("feedback-status").textContent = "Saved", and on error
(either network error or non-2xx) set the same element to "Will download on
submit" and optionally log the response.status/response.statusText for
debugging. Ensure the element id "feedback-status" and the POST body ({ reviews,
status: "in_progress" }) remain unchanged.
.claude/skills/skill-creator/eval-viewer/generate_review.py-288-307 (1)

288-307: ⚠️ Potential issue | 🟠 Major

포트 점유 프로세스를 자동 종료하는 로직은 너무 공격적입니다.

이 도구는 단순히 로컬 리뷰 UI를 띄우는 용도인데, 지금 방식은 같은 포트를 쓰는 다른 서비스를 조용히 SIGTERM 할 수 있습니다. 바로 아래에서 이미 bind 실패 시 임시 포트로 fallback 하므로, 강제 종료 대신 포트 충돌을 안내하거나 fallback만 사용하는 편이 안전합니다.

Also applies to: 438-446

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/eval-viewer/generate_review.py around lines 288
- 307, The _kill_port function is too aggressive—do not SIGTERM other processes;
instead change _kill_port to only detect/report that a port is in use (e.g.,
return a boolean or raise a non-fatal exception) and log a clear warning (use
sys.stderr or processLogger) without killing PIDs, and update its callers (the
code path that performs bind with fallback) to rely on the existing
temporary-port fallback or surface a user-facing message when the port is
occupied; remove any calls that send signal.SIGTERM and replace them with checks
that log the conflict so the bind/fallback behavior handles the resolution.
.claude/skills/skill-creator/scripts/aggregate_benchmark.py-149-154 (1)

149-154: ⚠️ Potential issue | 🟠 Major

output_charstokens 대체값으로 쓰면 벤치마크 수치가 왜곡됩니다.

여기서 execution_metrics.output_chars는 토큰 수가 아니라 출력 문자 수입니다. timing.json이 없을 때 이 값을 tokens로 넣으면 평균/델타가 서로 다른 단위로 집계되어 결과가 의미 없어집니다. 토큰이 없으면 비워 두거나, 실제 토큰 필드를 별도로 읽어와야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/aggregate_benchmark.py around lines 149
- 154, The code currently uses execution_metrics.output_chars as a fallback for
result["tokens"], which mixes characters with token counts and skews benchmarks;
change the logic in aggregate_benchmark.py where grading is read so that you do
NOT assign metrics.get("output_chars") to result["tokens"]; instead leave
result["tokens"] unset/None when a true token count is missing, or attempt to
read a real token field from metrics (e.g., "tokens", "output_tokens",
"token_count") and only assign if present; update the block handling
grading/get("execution_metrics") and result["tokens"] to reflect this so
character counts are not treated as tokens.
.claude/skills/skill-creator/scripts/run_loop.py-24-44 (1)

24-44: ⚠️ Potential issue | 🟠 Major

소규모 eval 세트에서는 train 셋이 비어 곧바로 성공으로 종료됩니다.

max(1, ...) 때문에 positive/negative가 각각 1개뿐이면 둘 다 test로 빠집니다. 그러면 아래 루프에서 train_total == 0인데도 train_failed == 0으로 간주되어 첫 iteration에 all_passed로 끝납니다. 최소 한 샘플은 train에 남기도록 분할 크기를 len(group) - 1로 상한 처리해야 합니다.

🔧 제안 수정
-    n_trigger_test = max(1, int(len(trigger) * holdout))
-    n_no_trigger_test = max(1, int(len(no_trigger) * holdout))
+    def test_size(group: list[dict]) -> int:
+        if len(group) <= 1:
+            return 0
+        return min(len(group) - 1, max(1, int(len(group) * holdout)))
+
+    n_trigger_test = test_size(trigger)
+    n_no_trigger_test = test_size(no_trigger)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/run_loop.py around lines 24 - 44, The
split_eval_set function currently forces at least one test sample per class via
max(1, ...), which can leave train empty for small groups; update the
calculation of n_trigger_test and n_no_trigger_test to cap the test count at
len(group)-1 so at least one sample remains in train (handle group sizes 0 or 1
by using max(0, len(group)-1) as the upper bound). Specifically, in
split_eval_set adjust the test-size computation for the "trigger" and
"no_trigger" lists so it becomes min(max(1, int(len(group) * holdout)), max(0,
len(group) - 1)) (or equivalent) to ensure train_set is never empty when
possible.
.claude/skills/skill-creator/eval-viewer/generate_review.py-279-281 (1)

279-281: ⚠️ Potential issue | 🟠 Major

임베드 JSON을 그대로 <script>에 넣으면 로컬 리뷰어가 스크립트 인젝션에 취약합니다.

runs/previous_feedback/출력 파일 내용은 임의 텍스트인데, json.dumps() 결과를 스크립트 본문에 바로 삽입하면 </script>가 포함된 데이터만으로 태그가 닫히고 뒤의 내용이 실행될 수 있습니다. 최소한 </>/&는 이스케이프해서 넣어야 합니다.

🔧 제안 수정
-    data_json = json.dumps(embedded)
+    data_json = (
+        json.dumps(embedded)
+        .replace("<", "\\u003c")
+        .replace(">", "\\u003e")
+        .replace("&", "\\u0026")
+    )
 
     return template.replace("/*__EMBEDDED_DATA__*/", f"const EMBEDDED_DATA = {data_json};")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/eval-viewer/generate_review.py around lines 279
- 281, The code currently inserts data_json directly into the script via
template.replace which allows script injection if fields contain "</script>" or
raw "<", ">" or "&". After creating data_json (the json.dumps(embedded) result),
produce a safe_json by escaping dangerous chars (e.g. data_json.replace('<',
'\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')) and then use that
safe_json in the template.replace that injects the EMBEDDED_DATA; update the
replacement call that writes EMBEDDED_DATA to use safe_json instead of
data_json.
.claude/skills/skill-creator/scripts/run_eval.py-133-168 (1)

133-168: ⚠️ Potential issue | 🟠 Major

첫 번째 비관련 tool 호출에서 바로 False를 반환하면 trigger 탐지가 누락됩니다.

스트리밍 경로와 assistant fallback 둘 다 Skill/Read가 아닌 첫 tool_use를 본 순간 바로 종료합니다. 그러면 같은 응답 안에서 뒤이어 오는 실제 Skill/Read 호출은 검사되지 않아 false negative가 생깁니다. 비관련 tool은 무시하고 message_stop/result까지 계속 스캔해야 합니다.

🔧 제안 수정
                         if se_type == "content_block_start":
                             cb = se.get("content_block", {})
                             if cb.get("type") == "tool_use":
                                 tool_name = cb.get("name", "")
                                 if tool_name in ("Skill", "Read"):
                                     pending_tool_name = tool_name
                                     accumulated_json = ""
-                                else:
-                                    return False
+                                else:
+                                    pending_tool_name = None
+                                    accumulated_json = ""
+                                    continue
 ...
                     elif event.get("type") == "assistant":
                         message = event.get("message", {})
                         for content_item in message.get("content", []):
                             if content_item.get("type") != "tool_use":
                                 continue
                             tool_name = content_item.get("name", "")
                             tool_input = content_item.get("input", {})
                             if tool_name == "Skill" and clean_name in tool_input.get("skill", ""):
-                                triggered = True
+                                return True
                             elif tool_name == "Read" and clean_name in tool_input.get("file_path", ""):
-                                triggered = True
-                            return triggered
+                                return True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/run_eval.py around lines 133 - 168, The
streaming and fallback logic prematurely returns False on seeing the first
non-Skill/Read tool_use, causing later Skill/Read calls in the same response to
be missed; update the content_block_start branch (variables: se_type, cb,
tool_name, pending_tool_name, accumulated_json) to not return False for
unrelated tools but instead skip/ignore them and continue scanning until
message_stop/result, and update the assistant fallback loop (variables: event,
message, content_item, tool_name, tool_input, triggered) to avoid returning
inside the for-loop—accumulate/set triggered when Skill/Read is found and only
return triggered after finishing scanning all content items (or after
message_end) so later tool_use entries are checked.
🟡 Minor comments (19)
.claude/agents/api-documenter.md-16-24 (1)

16-24: ⚠️ Potential issue | 🟡 Minor

체크리스트 항목의 문법 오류 수정 필요

Line 24의 "Versioning clear consistently"는 문법적으로 올바르지 않습니다. 다른 체크리스트 항목들과의 일관성을 위해 "Versioning documented clearly" 또는 유사한 형태로 수정해야 합니다.

📝 문법 수정 제안
 - Authentication documented clearly
 - Try-it-out functionality enabled
 - Multi-language examples provided
-- Versioning clear consistently
+- Versioning documented clearly
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/agents/api-documenter.md around lines 16 - 24, Replace the
grammatically incorrect checklist item "Versioning clear consistently" with a
consistent, parallel phrasing such as "Versioning documented clearly" (or
"Versioning documented consistently") in the API documentation checklist; ensure
the updated item matches the tone and structure of the other items like
"Authentication documented clearly" and "Error documentation comprehensive".
.claude/skills/incident-response/references/phase-guide.md-16-20 (1)

16-20: ⚠️ Potential issue | 🟡 Minor

Grafana Alert 상태 해석에 error 상태 누락

SKILL.md Line 39에서 states: ["firing", "error", "pending"]을 조회하지만, 이 가이드에는 error 상태의 해석이 없습니다. error 상태는 쿼리 실행 오류를 의미하므로 처리 방법이 필요합니다.

error 상태 해석 추가
 ### Grafana Alert 상태 해석
 - `Alerting (NoData)`: 쿼리 결과가 없음 → 쿼리 레이블 오류이거나 스크레이핑 중단
 - `Alerting`: 실제 조건 충족 → 진짜 장애
 - `Pending`: 조건 충족 중이나 `for` 기간 미달 → 주시 필요
+- `Error`: Alert 쿼리 실행 실패 → Grafana 설정 오류, datasource 연결 끊김, 잘못된 쿼리 구문

Error 상태 발견 시 장애 분석보다 Alert 설정 자체를 먼저 점검하도록 안내하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/references/phase-guide.md around lines 16 -
20, The guide is missing an interpretation for Grafana Alert "error" even though
the code checks states: ["firing", "error", "pending"]; update the Grafana Alert
상태 해석 section to add a short entry explaining that `Error` means the query
execution failed (e.g., syntax, datasource, or scraping error) and instruct
readers to first verify alert query/configuration and datasource/scraping health
before treating it as an incident; reference the existing bullets (Alerting
(NoData), Alerting, Pending) and add a fourth bullet for `Error` with the
recommended troubleshooting steps.
.claude/skills/incident-response/SKILL.md-64-74 (1)

64-74: ⚠️ Potential issue | 🟡 Minor

에러 코드 매핑이 CLAUDE.md와 일관성 유지 필요

CLAUDE.md(Context snippet 3)에 정의된 에러 코드 접두사는 C, S, T, U, M, ST, W, I, B, SE, F이지만, 이 테이블에는 M(메일)과 I(이미지) 매핑이 누락되었습니다.

📋 누락된 매핑 추가
 | 에러 접두사 | 도메인 경로 |
 |------------|------------|
 | ST (학습)  | `study/` |
 | SE (시즌)  | `rank/` |
 | U (사용자) | `user/` |
 | T (토큰)   | `token/` |
 | F (FCM)    | `fcm/` |
 | B (게시판) | `board/` |
 | W (WiFi)   | `wifi/` |
+| M (메일)   | `user/` (메일 인증) |
+| I (이미지) | `image/` 또는 `global/` |
 | C, S (공통)| `global/` |

CLAUDE.md와 동기화 유지를 위해 향후 에러 코드 추가 시 두 파일을 함께 업데이트하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/SKILL.md around lines 64 - 74, The error
code → domain mapping table in SKILL.md is missing the M (mail) and I (image)
prefixes present in CLAUDE.md; update the table used in the "에러 코드 → 도메인 매핑"
section to include rows for M → `mail/` and I → `image/` (keeping the same
format as other rows) and ensure the set of prefixes (C, S, T, U, M, ST, W, I,
B, SE, F) matches CLAUDE.md so both files stay synchronized when new prefixes
are added.
.claude/skills/incident-response/references/grafana-queries.md-35-44 (1)

35-44: ⚠️ Potential issue | 🟡 Minor

응답 시간 쿼리에 잠재적인 정확도 문제

Line 37의 by (le, uri) 그룹화는 URI별 p99를 계산하지만, Line 42의 p95 쿼리는 by (le)만 사용하여 전체 집계를 수행합니다. 의도적인 차이가 아니라면 불일치입니다.

🔍 일관성 확보를 위한 수정 제안

p95도 URI별로 분석하려면:

 # p95 응답시간
 histogram_quantile(0.95,
-  sum(rate(http_server_requests_seconds_bucket{job="geumpumta-backend-prod"}[5m])) by (le)
+  sum(rate(http_server_requests_seconds_bucket{job="geumpumta-backend-prod"}[5m])) by (le, uri)
 )

p99도 전체 집계로 통일하려면:

 # p99 응답시간 (초)
 histogram_quantile(0.99,
-  sum(rate(http_server_requests_seconds_bucket{job="geumpumta-backend-prod"}[5m])) by (le, uri)
+  sum(rate(http_server_requests_seconds_bucket{job="geumpumta-backend-prod"}[5m])) by (le)
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/references/grafana-queries.md around lines
35 - 44, The two Prometheus queries are inconsistent: the p99 query uses sum(...
) by (le, uri) to compute URI-level p99 while the p95 query uses sum(... ) by
(le) for overall aggregation; decide on the intended dimension and make them
consistent — either change the p95 expression to
sum(rate(http_server_requests_seconds_bucket{job="geumpumta-backend-prod"}[5m]))
by (le, uri) to compute p95 per URI (matching the p99’s URI granularity) or
change the p99 expression to sum(... ) by (le) to compute both p95 and p99 as
global aggregates; update the corresponding histogram_quantile calls
(histogram_quantile(0.95, ...) and histogram_quantile(0.99, ...)) accordingly so
both use the same grouping key.
.claude/skills/incident-response/references/phase-guide.md-10-14 (1)

10-14: ⚠️ Potential issue | 🟡 Minor

심각도 판단 기준의 모호한 경계

Critical과 High 사이의 5xx 에러율 경계(10%)가 명확하지만, High와 Medium의 경계(3%)는 Line 13에만 표현되고 Line 14에는 "< 3%"로 표현되어 정확히 3%일 때의 분류가 모호합니다.

경계값 명확화
 | 심각도 | 기준 |
 |--------|------|
-| Critical | prod 인스턴스 다운 / 5xx 에러율 > 10% / 전면 서비스 불가 |
-| High | 특정 기능 불가 / 5xx 에러율 3~10% / 응답시간 p99 > 5초 |
-| Medium | 일부 기능 저하 / 에러율 < 3% / 응답시간 p99 3~5초 |
+| Critical | prod 인스턴스 다운 / 5xx 에러율 ≥ 10% / 전면 서비스 불가 |
+| High | 특정 기능 불가 / 5xx 에러율 3~10% (미만) / 응답시간 p99 > 5초 |
+| Medium | 일부 기능 저하 / 5xx 에러율 < 3% / 응답시간 p99 3~5초 |

또는 오버랩을 제거하여 10% 이상, 3% 이상 10% 미만, 3% 미만으로 표현하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/references/phase-guide.md around lines 10 -
14, The severity table has an ambiguous boundary between High and Medium for 5xx
error rate (Line 13 shows "3~10%" while Line 14 shows "< 3%"), so update the
table to use non-overlapping, explicit ranges such as "10% 이상", "3% 이상 10% 미만",
and "3% 미만" (or ">=10%", ">=3% and <10%", "<3%") to remove ambiguity; modify the
relevant table rows (the "High" and "Medium" rows) so the 5xx error-rate
criteria are written consistently and unambiguously.
.claude/skills/incident-response/references/grafana-queries.md-68-87 (1)

68-87: ⚠️ Potential issue | 🟡 Minor

Redis 모니터링 쿼리의 job 레이블을 표준 명명 규칙에 맞게 수정 필요

Prometheus의 표준 job naming convention에 따르면 Redis exporter의 job 이름은 redis_exporter입니다. 현재 쿼리의 up{job="redis"} 레이블은 표준 규칙과 일치하지 않아 데이터를 반환하지 못할 가능성이 높습니다. MySQL의 up{job="mysql"}은 표준 규칙과 일치합니다.

실제 환경의 Prometheus 설정에서 job 이름을 확인한 후, 다음과 같이 수정하세요:

  • Redis: up{job="redis_exporter"} (또는 실제 환경의 job 이름으로 변경)
  • Redis 메모리 쿼리도 해당 job 이름에 맞게 필터링 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/references/grafana-queries.md around lines
68 - 87, The Redis queries use a non-standard job label (up{job="redis"}) and
must be updated to the exporter job name used in Prometheus (e.g.,
up{job="redis_exporter"}); update the up{job="redis"} expression and also add
the same job filter to redis_memory_used_bytes (or replace with
redis_memory_used_bytes{job="redis_exporter"}) so both queries use the actual
Redis exporter job name from your Prometheus config; verify the real job name in
your environment and use that string consistently.
.claude/skills/incident-response/references/templates.md-67-68 (1)

67-68: ⚠️ Potential issue | 🟡 Minor

Gradle 테스트 명령어의 중괄호 표기법 제거 필요

./gradlew test --tests "{테스트클래스명}"의 중괄호는 Gradle 공식 문법이 아닙니다. Gradle의 --tests 옵션은 패턴 문법을 사용하며, 따옴표는 공백이나 특수문자가 포함된 패턴에만 필요합니다.

수정 제안
-- [ ] 단위 테스트 통과 (`./gradlew test --tests "{테스트클래스명}"`)
+- [ ] 단위 테스트 통과 (`./gradlew test --tests MyServiceTest` 또는 `./gradlew test --tests "*ServiceTest"`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/incident-response/references/templates.md around lines 67 -
68, 교체할 텍스트 './gradlew test --tests "{테스트클래스명}"'에서 Gradle의 패턴 문법과 중괄호 사용이
잘못되었으므로 중괄호를 제거하고 필요 시에만 따옴표를 사용하도록 수정하세요; 예를 들어 './gradlew test --tests
테스트클래스명' 또는 공백/특수문자가 포함된 패턴일 때 './gradlew test --tests "com.example.MyTest"'처럼
바꾸고, 해당 변경을 체크리스트 항목(현재 문자열이 있는 줄)에서 반영하십시오.
src/main/java/com/gpt/geumpumtabackend/rank/CLAUDE.md-84-84 (1)

84-84: ⚠️ Potential issue | 🟡 Minor

테이블 전후에 빈 줄을 넣어 주세요.

Line 84와 Line 117 주변은 markdownlint MD058 경고가 납니다. 표 앞뒤 공백 줄만 추가해도 린트가 정리됩니다.

Also applies to: 117-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/gpt/geumpumtabackend/rank/CLAUDE.md` at line 84, Add a
blank line before and after the Markdown table(s) containing the row "| Cron |
작업 |" (and the similar table around the other occurrence at line ~117) to
satisfy markdownlint MD058; open the file CLAUDE.md, locate the table rows that
match "| Cron | 작업 |" and ensure there is an empty line both immediately above
and immediately below each table.
src/main/java/com/gpt/geumpumtabackend/fcm/CLAUDE.md-112-115 (1)

112-115: ⚠️ Potential issue | 🟡 Minor

문서가 제거된 구현명을 아직 가리키고 있습니다.

Line 114의 isPermanentError()는 현재 FcmMessageSender에 없는 메서드입니다. 실제 구현처럼 NON_RETRYABLE_ERROR_CODES와 해당 분기 로직을 가리키도록 수정해 두는 편이 안전합니다.

📝 제안
-3. 재시도 불가 에러 추가 시 `FcmMessageSender.send()`의 `isPermanentError()` 분기에 추가
+3. 재시도 불가 에러 추가 시 `FcmMessageSender`의 `NON_RETRYABLE_ERROR_CODES`와 해당 처리 분기를 함께 업데이트
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/gpt/geumpumtabackend/fcm/CLAUDE.md` around lines 112 - 115,
The doc references a non-existent method isPermanentError(); update the wording
to point to the actual retry logic: instruct callers to use the
NON_RETRYABLE_ERROR_CODES constant and the retry branching inside
FcmMessageSender.send() instead of isPermanentError(), and mention adding new
non-retryable codes to NON_RETRYABLE_ERROR_CODES when introducing permanent
errors; reference FcmService for adding new per-notification methods (e.g.,
sendMaxFocusNotification) and ResponseUtil.createSuccessResponse() for response
format.
CLAUDE.md-70-73 (1)

70-73: ⚠️ Potential issue | 🟡 Minor

최대 공부시간 스케줄 설명이 구현과 다릅니다.

여기는 10분 크론으로 적혀 있지만, src/main/java/com/gpt/geumpumtabackend/study/scheduler/MaxFocusStudyScheduler.java Line 22는 @Scheduled(fixedRate = 1000)입니다. 운영 기준 문서가 이렇게 어긋나면 점검 주기를 완전히 다르게 이해하게 됩니다.

📝 제안
- 최대 공부시간 `0 */10 * * * *` — 3시간 초과 세션 자동 종료 + FCM
+ 최대 공부시간 `매초 실행 (`@Scheduled`(fixedRate = 1000))` — 3시간 초과 세션 자동 종료 + FCM
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 70 - 73, The documentation for "최대 공부시간" states a
10-minute cron but the implementation in MaxFocusStudyScheduler (class
MaxFocusStudyScheduler, currently annotated with `@Scheduled`(fixedRate = 1000))
runs every second; update either the code or the doc to match: change the
`@Scheduled` annotation in MaxFocusStudyScheduler to the intended 10-minute
schedule (e.g., `@Scheduled`(cron = "0 */10 * * * *") or `@Scheduled`(fixedRate =
600_000)) or alternatively update the README entry to reflect the actual
fixedRate of 1000 if that behavior is intended.
src/main/java/com/gpt/geumpumtabackend/rank/CLAUDE.md-20-20 (1)

20-20: ⚠️ Potential issue | 🟡 Minor

코드 펜스 언어를 지정해 주세요.

Line 20과 Line 67의 fenced block에 언어가 없어 markdownlint MD040가 발생합니다. 디렉터리 트리와 예시는 text 지정이면 충분합니다.

Also applies to: 67-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/gpt/geumpumtabackend/rank/CLAUDE.md` at line 20, CLAUDE.md의
두 fenced code block(디렉터리 트리와 예시)에 언어가 빠져 markdownlint MD040이 발생하니, 각각의 ``` 를
```text 로 변경하여 코드 펜스 언어를 지정하세요; 파일 내 디렉터리 트리 블록과 예시 블록(현재 언어 미지정된 fenced
blocks)을 찾아 모두 `text` 언어 태그를 추가하면 됩니다.
.claude/skills/spring-review/SKILL.md-130-156 (1)

130-156: ⚠️ Potential issue | 🟡 Minor

리뷰 출력 예시 fence가 중간에 끊깁니다.

Lines 132-151에서 바깥 예시를 triple-backtick으로 열어 둔 상태에서 내부에 또 triple-backtick java fence를 넣고 있어, Line 141에서 바깥 fence가 조기 종료됩니다. 바깥 블록은 네 개 backtick + md/text로 감싸고, Line 154의 단독 예시도 언어 태그를 달아 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/spring-review/SKILL.md around lines 130 - 156, The outer
example block under "## 리뷰 출력 형식" uses triple-backticks and contains an inner
```java fence which prematurely closes the outer block; change the outer fence
to four backticks and add a language tag (e.g., "md" or "text") so the inner
```java remains valid, and also add an explicit language tag to the standalone
example at the end of that block (the single-line example starting with "**총평**:
아키텍처...") so all fences are properly nested and rendered.
.claude/skills/spring-core/references/exceptions.md-49-60 (1)

49-60: ⚠️ Potential issue | 🟡 Minor

텍스트 다이어그램 블록에 언어 태그를 붙여 주세요.

Line 51의 fenced block은 text 같은 info string이 없어 markdownlint MD040 경고가 납니다. 문서 lint를 깨끗하게 유지하려면 여기서 명시해 두는 편이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/spring-core/references/exceptions.md around lines 49 - 60,
The fenced code block showing the exception flow (the block containing
"BusinessException(ExceptionType)" and "GlobalExceptionHandler
(`@RestControllerAdvice`)") lacks an info string and triggers markdownlint MD040;
update the opening fence to include a language tag such as "text" (e.g., change
``` to ```text) so the block is explicitly tagged and the lint warning is
resolved.
.claude/skills/skill-creator/scripts/generate_report.py-205-209 (1)

205-209: ⚠️ Potential issue | 🟡 Minor

빈 history에서 max() 호출 시 ValueError 발생 가능

history가 빈 리스트인 경우 max(history, ...) 호출 시 ValueError: max() arg is an empty sequence 예외가 발생합니다.

🐛 수정 제안
     # Find best iteration for highlighting
+    best_iter = None
+    if history:
-    if test_queries:
-        best_iter = max(history, key=lambda h: h.get("test_passed") or 0).get("iteration")
-    else:
-        best_iter = max(history, key=lambda h: h.get("train_passed", h.get("passed", 0))).get("iteration")
+        if test_queries:
+            best_iter = max(history, key=lambda h: h.get("test_passed") or 0).get("iteration")
+        else:
+            best_iter = max(history, key=lambda h: h.get("train_passed", h.get("passed", 0))).get("iteration")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/generate_report.py around lines 205 -
209, Guard against an empty history before calling max(): compute the best entry
with entry = max(history, key=..., default=None) in both branches (when
test_queries is truthy and falsy) and then set best_iter =
entry.get("iteration") if entry is not None else None; update the code that uses
best_iter accordingly. Reference variables: history, test_queries, best_iter.
.claude/skills/spring-core/references/templates.md-213-214 (1)

213-214: ⚠️ Potential issue | 🟡 Minor

GlobalPageResponse.from()GlobalPageResponse.create() 메서드명 불일치

템플릿에서 GlobalPageResponse.from()을 사용하고 있지만, 실제 GlobalPageResponse 클래스는 create() 메서드를 제공합니다. 이대로 사용 시 컴파일 에러가 발생합니다.

🐛 수정 제안
 // Service
 Page<Example> examples = exampleRepository.findAll(PageRequest.of(page, size));
-return GlobalPageResponse.from(examples.map(ExampleResponse::from));
+return GlobalPageResponse.create(examples.map(ExampleResponse::from));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/spring-core/references/templates.md around lines 213 - 214,
The template calls GlobalPageResponse.from(...) but the class exposes
GlobalPageResponse.create(...), so update the call site (where
exampleRepository.findAll(PageRequest.of(page, size)) is assigned to examples
and then passed into GlobalPageResponse) to use GlobalPageResponse.create(...)
instead of from; ensure you still pass the examples.map(ExampleResponse::from)
result into create and leave ExampleResponse::from unchanged unless its API
differs.
.claude/skills/skill-creator/scripts/quick_validate.py-52-63 (1)

52-63: ⚠️ Potential issue | 🟡 Minor

name 값에 대한 검증 누락

name 필드가 존재하지만 빈 문자열("")이거나 공백만 있는 경우(" "), 현재 로직은 이를 유효한 것으로 처리합니다. Line 53에서 키 존재만 확인하고, line 63에서 if name:이 falsy일 때 검증을 건너뛰기 때문입니다.

🐛 수정 제안
     # Check required fields
     if 'name' not in frontmatter:
         return False, "Missing 'name' in frontmatter"
     if 'description' not in frontmatter:
         return False, "Missing 'description' in frontmatter"

     # Extract name for validation
     name = frontmatter.get('name', '')
     if not isinstance(name, str):
         return False, f"Name must be a string, got {type(name).__name__}"
     name = name.strip()
-    if name:
+    if not name:
+        return False, "Name cannot be empty"
+    else:
         # Check naming convention (kebab-case: lowercase with hyphens)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/quick_validate.py around lines 52 - 63,
The current validation only checks for the presence of 'name' in frontmatter but
allows empty or whitespace-only values; update the validation in
quick_validate.py around the name extraction (the variable name and surrounding
checks) to treat empty or whitespace-only strings as invalid by trimming the
string (name = name.strip()) and returning False with a clear message like
"Missing or empty 'name' in frontmatter" when name == "". Ensure this check
executes before any further name-specific validation logic.
.claude/skills/skill-creator/references/schemas.md-90-149 (1)

90-149: ⚠️ Potential issue | 🟡 Minor

예시 JSON의 수치가 서로 맞지 않습니다.

grading.json 예시는 expectations가 2개인데 summary.total이 3이고, timing.json 예시는 duration_ms/total_duration_seconds가 아래의 executor+grader duration과 맞지 않습니다. 이 파일이 아티팩트 계약 문서 역할을 하므로, 샘플 값이 어긋나면 구현이나 검증 코드가 잘못 따라가기 쉽습니다.

Also applies to: 203-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/references/schemas.md around lines 90 - 149,
The sample JSON has inconsistent aggregate fields: ensure "summary.total" equals
the actual count of items in "expectations" and that
"summary.passed"/"summary.failed" match the booleans (and recompute
"pass_rate"), ensure "timing.total_duration_seconds" equals the sum of
"timing.executor_duration_seconds" and "timing.grader_duration_seconds", and
ensure "execution_metrics.total_tool_calls" equals the sum of the entries under
"execution_metrics.tool_calls"; update the blocks containing "expectations",
"summary", "timing" and "execution_metrics" (look for those exact keys) to have
consistent derived values, and apply the same fixes to the other example block
referenced (lines ~203-214).
.claude/skills/skill-creator/scripts/aggregate_benchmark.py-263-272 (1)

263-272: ⚠️ Potential issue | 🟡 Minor

실제 반복 횟수 대신 runs_per_configuration을 3으로 고정하면 메타데이터가 틀어집니다.

이 값은 벤치마크 디렉터리에 실제로 존재하는 run 수를 반영해야 하는데, 지금은 항상 3으로 기록됩니다. 반복 횟수를 바꾸거나 일부 run이 빠진 경우 benchmark.jsonbenchmark.md가 잘못된 실험 규모를 보여주게 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/aggregate_benchmark.py around lines 263
- 272, The benchmark metadata currently hardcodes "runs_per_configuration": 3;
instead compute the actual number of runs and set that value dynamically before
constructing the benchmark dict. Locate where the benchmark dict is assembled
(the variable named benchmark and its "runs_per_configuration" key) and replace
the hardcoded 3 with a computed count derived from the real run artifacts (e.g.,
count of run subdirectories/files in the benchmark directory or the length of
the list of runs for each configuration, using eval_ids or the directory listing
used elsewhere in this script) so the JSON/MD reflect the true experiment size.
.claude/skills/skill-creator/scripts/improve_description.py-163-183 (1)

163-183: ⚠️ Potential issue | 🟡 Minor

1024자 제한이 실제로는 보장되지 않습니다.

재작성 결과 shortened도 1024자를 넘을 수 있는데, 여기서는 다시 검증하지 않고 그대로 반환합니다. 이 함수가 “하드 리밋”용 safety net이라면 마지막에 강제 클램프나 예외가 필요합니다.

🔧 제안 수정
         shorten_text = _call_claude(shorten_prompt, model)
         match = re.search(r"<new_description>(.*?)</new_description>", shorten_text, re.DOTALL)
         shortened = match.group(1).strip().strip('"') if match else shorten_text.strip().strip('"')
 
         transcript["rewrite_prompt"] = shorten_prompt
         transcript["rewrite_response"] = shorten_text
-        transcript["rewrite_description"] = shortened
-        transcript["rewrite_char_count"] = len(shortened)
-        description = shortened
+        if len(shortened) > 1024:
+            shortened = shortened[:1024].rstrip()
+        transcript["rewrite_description"] = shortened
+        transcript["rewrite_char_count"] = len(shortened)
+        description = shortened
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/skill-creator/scripts/improve_description.py around lines 163
- 183, The re-write step for long descriptions can still produce a string longer
than the 1024-char hard limit (variables: shortened, description, transcript,
_call_claude, shorten_prompt), so after extracting shortened you must verify and
enforce the 1024 limit: check len(shortened) and if it exceeds 1024 either (a)
truncate safely (e.g., cut to 1024 preserving word boundaries) and update
transcript["rewrite_description"] and transcript["rewrite_char_count"], or (b)
raise a clear exception so callers can retry; implement the chosen enforcement
immediately after the shortened assignment and before assigning description =
shortened so the function never returns a >1024-character description.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cbb9407e-a2be-4465-933e-21b231085c92

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3a0ac and 3d4610f.

📒 Files selected for processing (46)
  • .ai/ARCHITECTURE.md
  • .ai/TESTING.md
  • .ai/USE-CASES.md
  • .claude/agents/api-documenter.md
  • .claude/agents/backend-architect.md
  • .claude/agents/code-reviewer.md
  • .claude/agents/test-engineer.md
  • .claude/skills/incident-response/SKILL.md
  • .claude/skills/incident-response/references/grafana-queries.md
  • .claude/skills/incident-response/references/phase-guide.md
  • .claude/skills/incident-response/references/templates.md
  • .claude/skills/skill-creator/SKILL.md
  • .claude/skills/skill-creator/agents/analyzer.md
  • .claude/skills/skill-creator/agents/comparator.md
  • .claude/skills/skill-creator/agents/grader.md
  • .claude/skills/skill-creator/assets/eval_review.html
  • .claude/skills/skill-creator/eval-viewer/generate_review.py
  • .claude/skills/skill-creator/eval-viewer/viewer.html
  • .claude/skills/skill-creator/references/schemas.md
  • .claude/skills/skill-creator/scripts/__init__.py
  • .claude/skills/skill-creator/scripts/aggregate_benchmark.py
  • .claude/skills/skill-creator/scripts/generate_report.py
  • .claude/skills/skill-creator/scripts/improve_description.py
  • .claude/skills/skill-creator/scripts/package_skill.py
  • .claude/skills/skill-creator/scripts/quick_validate.py
  • .claude/skills/skill-creator/scripts/run_eval.py
  • .claude/skills/skill-creator/scripts/run_loop.py
  • .claude/skills/skill-creator/scripts/utils.py
  • .claude/skills/spring-core/SKILL.md
  • .claude/skills/spring-core/references/exceptions.md
  • .claude/skills/spring-core/references/templates.md
  • .claude/skills/spring-review/SKILL.md
  • .claude/skills/spring-test/SKILL.md
  • .claude/skills/spring-test/evals/evals.json
  • .mcp.json
  • CLAUDE.md
  • src/main/java/com/gpt/geumpumtabackend/board/dto/BoardListResponse.java
  • src/main/java/com/gpt/geumpumtabackend/fcm/CLAUDE.md
  • src/main/java/com/gpt/geumpumtabackend/fcm/exception/PermanentFcmException.java
  • src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmMessageSender.java
  • src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmService.java
  • src/main/java/com/gpt/geumpumtabackend/rank/CLAUDE.md
  • src/main/java/com/gpt/geumpumtabackend/rank/dto/response/SeasonDepartmentRankingResponse.java
  • src/main/java/com/gpt/geumpumtabackend/study/domain/StudySession.java
  • src/main/java/com/gpt/geumpumtabackend/study/scheduler/MaxFocusStudyScheduler.java
  • src/main/java/com/gpt/geumpumtabackend/study/service/StudySessionService.java
💤 Files with no reviewable changes (6)
  • src/main/java/com/gpt/geumpumtabackend/board/dto/BoardListResponse.java
  • .ai/ARCHITECTURE.md
  • .ai/USE-CASES.md
  • src/main/java/com/gpt/geumpumtabackend/rank/dto/response/SeasonDepartmentRankingResponse.java
  • src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmService.java
  • .ai/TESTING.md

Comment on lines 30 to +32
@Retryable(
retryFor = FirebaseMessagingException.class,
noRetryFor = PermanentFcmException.class,
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Spring Retry 에서 @retryable(retryFor = FirebaseMessagingException.class, noRetryFor = PermanentFcmException.class) 메서드가 RuntimeException 래퍼를 던지고 @recover(FirebaseMessagingException, ...)만 있을 때, @recover 실행되는지 확인해 주세요.

💡 Result:

아니요, @Recover(FirebaseMessagingException, ...) 메서드는 실행되지 않습니다. Spring Retry에서 @Retryable(retryFor = FirebaseMessagingException.class, noRetryFor = PermanentFcmException.class) 메서드가 RuntimeException 래퍼를 던질 때: 1. PermanentFcmException이 FirebaseMessagingException의 서브클래스이고 noRetryFor에 지정되어 있으면, 해당 예외는 재시도 대상에서 제외됩니다. BinaryExceptionClassifier (SubclassClassifier 기반)가 예외 타입과 서브클래스를 계층적으로 매칭하여 재시도 여부를 결정합니다. noRetryFor가 매칭되면 즉시 재시도하지 않고 ExhaustedRetryException을 발생시킵니다. (출처 18, 16, 21) 2. 재시도가 발생하지 않았으므로 모든 재시도 횟수가 소진된 후가 아니며, RecoverAnnotationRecoveryHandler의 recover가 호출되지 않습니다. recover는 재시도 횟수 소진 후 (ExhaustedRetryException 발생 시) 호출되며, cause.getClass (원래 예외 타입)을 SubclassClassifier로 @Recover 메서드와 매칭합니다. (출처 29, 30) 3. RuntimeException 래퍼라도 cause가 PermanentFcmException이면 noRetryFor 매칭으로 재시도 생략, @Recover 스킵. retryFor만 매칭되는 FirebaseMessagingException (PermanentFcmException 아님)이 재시도 후 소진되면 @Recover(FirebaseMessagingException)가 실행됩니다. noRetryFor 예외는 원래 예외 그대로 상위 호출자에게 전파됩니다.

Citations:


🏁 Script executed:

find . -name "FcmMessageSender.java" -type f

Repository: Geumpumta/backend

Length of output: 135


🏁 Script executed:

cat -n ./src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmMessageSender.java

Repository: Geumpumta/backend

Length of output: 3338


@Recover 메서드가 PermanentFcmException을 받지 않습니다.

현재 구조에서는 PermanentFcmExceptionnoRetryFor로 지정되어 있어, 이 예외가 발생하면 재시도 없이 즉시 상위 호출자로 전파됩니다. @Recover 메서드(Line 64)는 재시도 횟수 소진 후에만 호출되므로, PermanentFcmException에 대한 공통 예외 처리가 동작하지 않습니다. 호출자 측에서 PermanentFcmException을 별도로 처리해야 하거나, PermanentFcmException@Recover 메서드를 추가해야 합니다.

또한 Line 56의 로그에서 messageDto.getToken()을 그대로 노출하고 있는데, FCM 토큰은 민감한 정보이므로 마스킹하거나 로그에서 제외하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmMessageSender.java`
around lines 30 - 32, The `@Recover` method on FcmMessageSender doesn't handle
PermanentFcmException because that exception is listed in
`@Retryable`(noRetryFor=PermanentFcmException.class) and is propagated
immediately; add a new `@Recover` method that accepts PermanentFcmException (e.g.,
recover(PermanentFcmException ex, MessageDto messageDto) or similar signature
used today) to centralize handling and logging for permanent failures, or
alternatively remove PermanentFcmException from noRetryFor if you want it to
trigger retry+recover; also update logging in the send flow (references:
FcmMessageSender, `@Retryable`, `@Recover`, PermanentFcmException,
messageDto.getToken()) to avoid printing raw FCM tokens—mask or omit
messageDto.getToken() in logs (e.g., replace with truncated/masked value or
remove entirely).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant