Skip to content

feat(fe): add gamification UI - statistics, character, items, achieve…#229

Open
kdh-92 wants to merge 2 commits into
mainfrom
feat/fe-gamification
Open

feat(fe): add gamification UI - statistics, character, items, achieve…#229
kdh-92 wants to merge 2 commits into
mainfrom
feat/fe-gamification

Conversation

@kdh-92
Copy link
Copy Markdown
Owner

@kdh-92 kdh-92 commented Apr 12, 2026

…ments, challenges

Phase 2-4 FE implementation for the 현명 소비 시스템:

  • Statistics dashboard: weekly comparison, monthly summary, category breakdown
  • Character system: color rendering, egg/hatching, level progress, catalog
  • Item system: inventory, equipment slots, item catalog with tier colors
  • Achievement system: achievement list with progress tracking
  • Challenge system: create, track, daily log calendar, history
  • All pages lazy-loaded for code splitting
  • Includes TypeTag.tsx restore from fix/ci-stabilize

📝 Pull Request

📌 변경 사항 요약

  • 어떤 기능을 변경/추가/수정했는지 설명해주세요.

✅ 체크리스트

  • 코드가 정상적으로 동작함
  • 관련 테스트를 모두 통과함
  • 코드 스타일 가이드에 맞춤
  • 문서나 주석이 보완되었음 (필요 시)

🔍 관련 이슈

close #000

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 통계 페이지 추가: 주간 비교 및 월간 요약, 카테고리 분석
    • 캐릭터 기능 추가: 캐릭터 상세, 도감, 경험치 프로그레스, 시각화
    • 인벤토리/장비 UI 추가: 아이템 카드·장비 슬롯·장착 흐름
    • 업적 및 챌린지 추가: 업적 카드, 챌린지 생성/상세/일별 캘린더
  • Documentation

    • CI/CD 안정화 계획 및 실행 결과 문서 추가
  • Style

    • 코드 포맷 및 import 정리된 스타일 수정 사항 적용

…ments, challenges

Phase 2-4 FE implementation for the 현명 소비 시스템:
- Statistics dashboard: weekly comparison, monthly summary, category breakdown
- Character system: color rendering, egg/hatching, level progress, catalog
- Item system: inventory, equipment slots, item catalog with tier colors
- Achievement system: achievement list with progress tracking
- Challenge system: create, track, daily log calendar, history
- All pages lazy-loaded for code splitting
- Includes TypeTag.tsx restore from fix/ci-stabilize

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

Walkthrough

Tiggle 프론트엔드에 게임화(gamification) 기능을 대규모로 추가하고 CI/CD 관련 세션 문서를 추가했습니다. 새로운 페이지(통계, 캐릭터, 인벤토리, 달성도, 챌린지), 다수의 컴포넌트/스타일/타입/쿼리 키, 라우터 라우트 및 TypeScript 타입들이 포함되어 있습니다.

Changes

Cohort / File(s) Summary
문서
docs/sessions/2026-04-12_1_plan.md, docs/sessions/2026-04-12_1_result.md
CI/CD 안정화 계획 및 실행 결과 문서 추가. Tiggle/Calynda/AIVA-SaaS 관련 원인·조치·검증 기록.
라우팅·부트스트랩
frontend/tiggle/src/router.tsx
새 페이지를 lazy 로딩하고 withSuspense 래퍼로 등록해 라우트 확장.
타입·쿼리 키
frontend/tiggle/src/types/gamification.ts, frontend/tiggle/src/query/queryKeys.ts
게임화 도메인용 enum/DTO 타입 다수 추가 및 통일된 React-Query key 빌더 추가.
페이지: Statistics
frontend/tiggle/src/pages/StatisticsPage/index.tsx, .../WeeklyComparison/*, .../CategoryBreakdown/*, StatisticsPageStyle.tsx
주간/월간 통계 페이지와 서브컴포넌트(주간비교, 카테고리 분석) 및 스타일 추가.
페이지: Character / Catalog
frontend/tiggle/src/pages/CharacterPage/index.tsx, .../CharacterDisplay/*, .../ExpProgressBar/*, CharacterPageStyle.tsx, CharacterCatalogPage/*
캐릭터 조회·디스플레이·경험치바·카탈로그 페이지와 스타일/컴포넌트 추가.
페이지: Inventory
frontend/tiggle/src/pages/InventoryPage/index.tsx, .../EquipmentSlots/*, .../ItemCard/*, InventoryPageStyle.tsx
인벤토리 페이지, 장비 슬롯 UI, 아이템 카드, 장착/해제 모달 및 스타일 추가.
페이지: Achievements
frontend/tiggle/src/pages/AchievementPage/*
달성도 페이지, 카드 컴포넌트 및 스타일 추가. 최근·전체 달성도 조회 포함.
페이지: Challenges
frontend/tiggle/src/pages/ChallengePage/*, ChallengeCreatePage/*, ChallengeDetailPage/*, DailyLogCalendar/*, styles
챌린지 목록/생성/상세/일일 로그 달력 및 관련 컴포넌트·스타일·취소 흐름(변경·뮤테이션) 추가.
원자 컴포넌트
frontend/tiggle/src/components/atoms/TypeTag/TypeTag.tsx
거래 유형 표시용 TypeTag 컴포넌트 추가(크기·타입 props).
여러 스타일 모듈
frontend/tiggle/src/pages/.../*Style.tsx (다수)
새 페이지/컴포넌트에 필요한 styled-components 모듈 다수 추가 (카드, 리스트, 배지, 프로그레스 등).
마이너 포맷 변경
frontend/tiggle/src/pages/CreatePage/.../TransactionPreviewCell.tsx, .../DetailPage/index.tsx, .../NotFoundPage.tsx, ReplyCell.tsx
import 정렬, inline style 포맷 등 비기능적 코드 포매팅 변경.
다수의 신규 컴포넌트 구현
e.g. ActiveChallenge.tsx, ChallengeHistoryCard.tsx, AchievementCard.tsx, DailyLogCalendar.tsx, EquipmentSlots.tsx, ItemCard.tsx, CategoryBreakdown.tsx, WeeklyComparison.tsx, ExpProgressBar.tsx, CharacterDisplay.tsx
UI·로직을 포함한 컴포넌트 다수 추가 — props/상태/React Query 사용 패턴 확인 필요.

Sequence Diagram(s)

(생성 조건 미충족 — 생략)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 분

Possibly related PRs

💫 개요

이 PR은 Tiggle 프론트엔드에 게임화(gamification) 시스템을 도입합니다. 통계, 캐릭터, 인벤토리, 달성도, 챌린지 페이지와 관련 컴포넌트, 스타일, 쿼리 키, 타입 정의를 추가하며, 라우터 구성을 확장합니다.

변경 사항

Cohort / File(s) 요약
문서
docs/sessions/2026-04-12_1_plan.md, docs/sessions/2026-04-12_1_result.md
CI/CD 안정화 계획 및 실행 결과 문서 추가. Tiggle, Calynda, AIVA-SaaS의 배포 상태 및 변경 사항 기록.
원자 컴포넌트
frontend/tiggle/src/components/atoms/TypeTag/TypeTag.tsx
거래 유형 태그 컴포넌트 추가. 크기(md/lg)와 거래 유형(지출/환불/수익)을 지원.
달성도 기능
frontend/tiggle/src/pages/AchievementPage/index.tsx, AchievementPage/AchievementCard/*, AchievementPageStyle.tsx
달성도 페이지, 달성도 카드 컴포넌트, 스타일 모듈 추가. 최근 달성도 조회, 전체 달성도 목록, 통계 요약 렌더링.
챌린지 기능
frontend/tiggle/src/pages/ChallengePage/index.tsx, ChallengeCreatePage/index.tsx, ChallengeDetailPage/index.tsx, ChallengePage/*.tsx, ChallengeCreatePageStyle.tsx, ChallengeDetailPageStyle.tsx, DailyLogCalendar/*
활성 챌린지, 챌린지 생성, 챌린지 상세 페이지 및 관련 컴포넌트, 일일 로그 달력, 스타일 시트 추가.
캐릭터 기능
frontend/tiggle/src/pages/CharacterPage/index.tsx, CharacterPage/CharacterDisplay/*, CharacterPage/ExpProgressBar/*, CharacterCatalogPage/index.tsx, CharacterPageStyle.tsx, CharacterCatalogPageStyle.tsx
캐릭터 조회, 캐릭터 카탈로그, 경험치 진행 표시줄, 캐릭터 디스플레이 컴포넌트 및 스타일 추가.
인벤토리 기능
frontend/tiggle/src/pages/InventoryPage/index.tsx, InventoryPage/EquipmentSlots/*, InventoryPage/ItemCard/*, InventoryPageStyle.tsx
인벤토리 페이지, 장비 슬롯, 아이템 카드 컴포넌트 및 스타일 추가. 아이템 장착/장착 해제 기능 포함.
통계 기능
frontend/tiggle/src/pages/StatisticsPage/index.tsx, StatisticsPage/WeeklyComparison/*, StatisticsPage/CategoryBreakdown/*, StatisticsPageStyle.tsx
통계 페이지, 주간 비교, 카테고리 분석, 스타일 모듈 추가. 주간/월간 데이터 조회 및 렌더링.
인프라 및 타입
frontend/tiggle/src/query/queryKeys.ts, frontend/tiggle/src/types/gamification.ts, frontend/tiggle/src/router.tsx
게임화 도메인을 위한 쿼리 키 빌더 추가. 캐릭터, 아이템, 달성도, 챌린지, 통계 관련 DTO 타입 및 상수 정의. 새 페이지 경로 추가 및 Lazy 로딩 설정.
포매팅 개선
frontend/tiggle/src/pages/CreatePage/TransactionPreviewCell/ReplyCell.tsx, frontend/tiggle/src/pages/DetailPage/index.tsx, frontend/tiggle/src/pages/NotFoundPage.tsx
import 문 재정렬 및 스타일 객체 포매팅 개선.

예상 코드 리뷰 소요 시간

🎯 4 (복잡함) | ⏱️ ~60 분

관련 PR

🐰 축하 시

🐇 새 기능이 폴짝폴짝 와서
통계와 챌린지가 춤을 춰요
캐릭터와 인벤토리가 반짝이고
달성도가 빛나면 당근도 쏩니다 🥕
토끼가 웃는 오늘의 배포, 훌륭해요!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경 사항인 게임화 UI 구현(통계, 캐릭터, 아이템, 성취)을 명확하고 간결하게 요약하고 있습니다.

✏️ 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 feat/fe-gamification

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

@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: 10

Note

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

🟡 Minor comments (11)
frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsx-22-26 (1)

22-26: ⚠️ Potential issue | 🟡 Minor

진행률 계산에 하한(0%) 클램프를 추가해 주세요.
Line 25 계산식은 음수 experience가 들어오면 음수 퍼센트가 되어 BarFill에 비정상 width가 전달될 수 있습니다.

🐛 제안 diff
   const percent = isMaxLevel
     ? 100
     : nextLevelExp > 0
-      ? Math.min(Math.round((experience / nextLevelExp) * 100), 100)
+      ? Math.max(
+          0,
+          Math.min(Math.round((experience / nextLevelExp) * 100), 100),
+        )
       : 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsx`
around lines 22 - 26, The percent calculation in ExpProgressBar can produce
negative values when experience is negative, causing BarFill to receive invalid
widths; update the percent computation (variable percent in ExpProgressBar) to
clamp the result at a minimum of 0 (e.g., wrap the computed percentage with
Math.max(0, ...)) while preserving the existing max-level and nextLevelExp logic
so percent never goes below 0 or above 100 before passing to BarFill.
frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdown.tsx-35-38 (1)

35-38: ⚠️ Potential issue | 🟡 Minor

막대 너비 계산에 하한값(0%)도 포함해 주세요.

현재는 100% 상한만 적용되어 음수 비율 데이터가 들어오면 width 계산이 비정상일 수 있습니다. 0~100 범위로 clamp 하는 편이 안전합니다.

너비 clamp 보강안
-                <div
-                  className="bar-fill"
-                  style={{ width: `${Math.min(item.ratio, 100)}%` }}
-                />
+                <div
+                  className="bar-fill"
+                  style={{
+                    width: `${Math.max(0, Math.min(item.ratio, 100))}%`,
+                  }}
+                />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdown.tsx`
around lines 35 - 38, The bar width currently only applies an upper bound using
Math.min(item.ratio, 100) which lets negative ratios produce invalid widths;
update the width calculation for the element with className "bar-fill" in
CategoryBreakdown.tsx to clamp item.ratio into the 0–100 range (e.g., ensure the
value is at least 0 and at most 100) before appending "%" so negative values
become 0% and values above 100 become 100%.
frontend/tiggle/src/pages/InventoryPage/EquipmentSlots/EquipmentSlots.tsx-84-99 (1)

84-99: ⚠️ Potential issue | 🟡 Minor

장착 여부 판정이 표시 텍스트와 불일치할 수 있습니다.

현재는 ID 존재 여부로 $hasItem을 결정하지만, 이름 조회 실패 시 비장착 라벨이 렌더링되어 상태가 섞여 보일 수 있습니다. 동일 기준(equippedName)으로 판정하는 편이 안전합니다.

판정 기준 일치화 예시
           const equippedId = equipment[equipKey];
           const equippedName = findItemName(equippedId);
-          const hasItem = equippedId != null;
+          const hasItem = equippedName != null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/InventoryPage/EquipmentSlots/EquipmentSlots.tsx`
around lines 84 - 99, The component currently sets $hasItem based on equippedId
but renders label based on equippedName, which can mismatch; change the presence
check to use equippedName (result of findItemName) so $hasItem is derived from
equippedName truthiness instead of equippedId—update the binding that sets
hasItem (currently const hasItem = equippedId != null) to use const hasItem =
Boolean(equippedName) so SlotBox/$hasItem, SlotLabel and EquippedItemName all
rely on the same criterion; keep existing variables equippedId, equippedName,
findItemName, SlotBox, SlotIcon, SlotLabel, EquippedItemName and onSlotClick
references intact.
frontend/tiggle/src/pages/AchievementPage/AchievementCard/AchievementCard.tsx-15-25 (1)

15-25: ⚠️ Potential issue | 🟡 Minor

조건 라벨/단위 매핑을 타입 기반으로 고정해 주세요.

Record<string, string> + 인라인 분기 조합은 조건 타입 추가/변경 시 누락을 놓치기 쉽습니다. AchievementConditionTypeValue 기반 맵으로 묶어두면 컴파일 타임에 바로 잡을 수 있습니다.

타입 기반 매핑 예시
+type ConditionType = AchievementConditionTypeValue;
+
-const CONDITION_LABELS: Record<string, string> = {
+const CONDITION_LABELS: Record<ConditionType, string> = {
   RECORD_COUNT: "거래 기록",
   STREAK: "연속 기록",
   CHALLENGE_COMPLETE: "챌린지 완료",
   CATEGORY_COUNT: "카테고리 사용",
   SPENDING_DECREASE: "지출 감소",
   NO_ANOMALY_WEEKS: "정상 소비 주간",
   NO_SPEND_DAYS: "무지출일",
   COLOR_RARITY: "컬러 희귀도",
   CHARACTER_TIER: "캐릭터 등급",
 };
+
+const CONDITION_SUFFIX: Record<ConditionType, string> = {
+  RECORD_COUNT: "회",
+  STREAK: "일 연속",
+  CHALLENGE_COMPLETE: "회",
+  CATEGORY_COUNT: "개",
+  SPENDING_DECREASE: "%",
+  NO_ANOMALY_WEEKS: "주",
+  NO_SPEND_DAYS: "일",
+  COLOR_RARITY: "",
+  CHARACTER_TIER: "",
+};
@@
         <AchievementCondition>
           {conditionLabel} {conditionValue}
-          {conditionType === "RECORD_COUNT" && "회"}
-          {conditionType === "STREAK" && "일 연속"}
-          {conditionType === "CHALLENGE_COMPLETE" && "회"}
-          {conditionType === "CATEGORY_COUNT" && "개"}
-          {conditionType === "SPENDING_DECREASE" && "%"}
-          {conditionType === "NO_ANOMALY_WEEKS" && "주"}
-          {conditionType === "NO_SPEND_DAYS" && "일"}
+          {CONDITION_SUFFIX[conditionType]}
         </AchievementCondition>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/tiggle/src/pages/AchievementPage/AchievementCard/AchievementCard.tsx`
around lines 15 - 25, CONDITION_LABELS is currently typed as
Record<string,string>, which loses compile-time checks; change it to
Record<AchievementConditionTypeValue, string> (import/alias the
AchievementConditionTypeValue type used across the app) and update the object
literal for CONDITION_LABELS to use that type so the compiler will force updates
when condition variants change; also update any related maps (units or other
label maps) the same way and run type-check to fix any missing keys or incorrect
usages referencing CONDITION_LABELS or AchievementConditionTypeValue.
frontend/tiggle/src/pages/ChallengePage/ActiveChallenge/ActiveChallenge.tsx-29-32 (1)

29-32: ⚠️ Potential issue | 🟡 Minor

진행률은 0~100으로 clamp 해 두는 편이 안전합니다.

Line 29-32와 Line 56에서 achievedDays > targetDays인 경우 퍼센트가 100을 넘어가고 strokeDashoffset이 음수가 됩니다. 그 상태로는 링이 깨지거나 101% 같은 값이 그대로 노출됩니다.

수정 예시
-  const percentage = targetDays > 0 ? (achievedDays / targetDays) * 100 : 0;
+  const rawPercentage =
+    targetDays > 0 ? (achievedDays / targetDays) * 100 : 0;
+  const percentage = Math.min(100, Math.max(0, rawPercentage));

Also applies to: 56-56

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

In `@frontend/tiggle/src/pages/ChallengePage/ActiveChallenge/ActiveChallenge.tsx`
around lines 29 - 32, Clamp the computed percentage to the 0–100 range so the
SVG ring math and displayed value never go outside bounds: when computing
percentage (the variable named percentage used to derive strokeDashoffset)
replace the raw (achievedDays / targetDays) * 100 with a clamped value between 0
and 100 (also handle targetDays === 0 as 0), and use that clamped percentage
wherever strokeDashoffset and any displayed percent string are rendered
(references: percentage, strokeDashoffset, circumference, radius).
frontend/tiggle/src/pages/StatisticsPage/WeeklyComparison/WeeklyComparison.tsx-14-15 (1)

14-15: ⚠️ Potential issue | 🟡 Minor

기간 문자열을 slice로 자르면 연말 주차가 모호해집니다.

Line 14-15는 연도를 무조건 제거해서 12-30 ~ 01-05 같은 표기를 만듭니다. 연도 경계 주차에서는 기간이 역전된 것처럼 보이고, 응답 형식이 datetime으로 바뀌면 바로 깨집니다.

수정 예시
+import dayjs from "dayjs";
 import { ArrowDown, ArrowUp, AlertTriangle } from "react-feather";
@@
-const formatPeriod = (start: string, end: string): string =>
-  `${start.slice(5)} ~ ${end.slice(5)}`;
+const formatPeriod = (start: string, end: string): string => {
+  const startDate = dayjs(start);
+  const endDate = dayjs(end);
+
+  if (startDate.year() === endDate.year()) {
+    return `${startDate.format("MM.DD")} ~ ${endDate.format("MM.DD")}`;
+  }
+
+  return `${startDate.format("YYYY.MM.DD")} ~ ${endDate.format("YYYY.MM.DD")}`;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/tiggle/src/pages/StatisticsPage/WeeklyComparison/WeeklyComparison.tsx`
around lines 14 - 15, formatPeriod currently slices off the year
(start.slice(5)) which breaks year-boundary weeks and datetime inputs; update
the formatPeriod function to parse the incoming ISO date/datetime strings (e.g.,
trim to the date portion or use Date/dayjs) and format them robustly: keep full
YYYY-MM-DD or format as MM-DD but include the year when start and end fall in
different years (or when input contains time), e.g., derive dateStart and
dateEnd from start/end, compare years, and return either "MM-DD ~ MM-DD" when
same year or "YYYY-MM-DD ~ YYYY-MM-DD" when years differ, ensuring it handles
both date-only and datetime inputs.
docs/sessions/2026-04-12_1_plan.md-27-35 (1)

27-35: ⚠️ Potential issue | 🟡 Minor

테이블 전후 공백 라인 누락으로 markdownlint(MD058) 경고가 발생합니다.

헤더와 테이블 사이(및 필요 시 테이블 이후) 공백 라인을 명시해 lint 경고를 제거해 주세요.

수정 예시
 ## 작업 계획
+
 | # | 작업 | 프로젝트 | 파일 | 난이도 |
 |---|------|---------|------|--------|
 | 1 | TypeTag.tsx 복구 | Tiggle | atoms/TypeTag/TypeTag.tsx | S |
 | 2 | prettier/import 오류 수정 | Tiggle | DetailPage, NotFoundPage | S |
 | 3 | FE 빌드 + BE 테스트 로컬 검증 | Tiggle | - | S |
 | 4 | Flutter 테스트 CI 추가 | Calynda | ci.yml, deploy-nas.yml | M |
 | 5 | AIVA-SaaS 현황 분석 | AIVA-SaaS | - | S |
+
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/sessions/2026-04-12_1_plan.md` around lines 27 - 35, Add a blank line
between the section header "## 작업 계획" and the table header row ("| # | 작업 | 프로젝트
| 파일 | 난이도 |") and ensure there's at least one blank line after the table ends
as well to satisfy markdownlint rule MD058; update the markdown around those
unique lines so the header and table are separated by an empty line (and add a
trailing blank line after the final table row) to remove the lint warning.
frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendar.tsx-16-21 (1)

16-21: ⚠️ Potential issue | 🟡 Minor

축약 금액 포맷이 반올림되어 실제보다 크게 보일 수 있습니다.

Line 17/20의 반올림은 금액을 과대 표기할 수 있습니다(예: 15,000 → 2만). 절사 또는 소수 1자리 표기로 바꾸는 편이 안전합니다.

수정 예시(절사)
   if (amount >= 10000) {
-    return `${Math.round(amount / 10000)}만`;
+    return `${Math.floor(amount / 10000)}만`;
   }
   if (amount >= 1000) {
-    return `${(amount / 1000).toFixed(0)}천`;
+    return `${Math.floor(amount / 1000)}천`;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendar.tsx`
around lines 16 - 21, The current compact-format logic in DailyLogCalendar.tsx
uses Math.round for the branches checking "amount >= 10000" and "amount >=
1000", which can overstate values (e.g., 15000 → "2만"); change those returns to
either truncate (use Math.floor(amount / 10000) + '만' and Math.floor(amount /
1000) + '천') or format with one decimal place (use (amount / 10000).toFixed(1) +
'만' and (amount / 1000).toFixed(1) + '천') so displayed amounts do not round up;
update the two return expressions accordingly.
frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendar.tsx-58-65 (1)

58-65: ⚠️ Potential issue | 🟡 Minor

과거 미기록일을 future로 분류하면 상태 의미가 왜곡됩니다.

Line 64는 “오늘 이전인데 로그가 없는 날”도 future로 칠합니다. 과거/미기록을 구분하려면 empty(또는 별도 상태)로 분리하는 게 안전합니다.

수정 예시
-    let type: "no-spend" | "spent" | "future";
+    let type: "no-spend" | "spent" | "future" | "empty";
     if (current.isAfter(today, "day")) {
       type = "future";
     } else if (log) {
       type = log.isNoSpend ? "no-spend" : "spent";
     } else {
-      type = "future";
+      type = "empty";
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendar.tsx`
around lines 58 - 65, The current logic in DailyLogCalendar.tsx sets the local
variable type ("no-spend" | "spent" | "future") to "future" for any day after
today or any day with no log, which conflates past-unlogged days with true
future days; update the type union to include an "empty" (or "unrecorded")
state, change the else branch that currently assigns "future" when !log to
assign "empty" instead, and then update any downstream code/consumers that
inspect type (rendering, styles, tests) to handle the new "empty" case
accordingly (search for usages of the type variable and the DailyLogCalendar
rendering logic to adjust behavior).
frontend/tiggle/src/pages/ChallengePage/index.tsx-34-40 (1)

34-40: ⚠️ Potential issue | 🟡 Minor

로딩 조건이 불완전합니다.

&& 조건을 사용하면 하나의 쿼리만 완료되어도 로딩 상태가 해제됩니다. 이로 인해 activeChallenge 또는 historyPage가 아직 정의되지 않은 상태에서 UI가 렌더링될 수 있습니다.

🛠️ 수정 제안
-  if (isActiveLoading && isHistoryLoading) {
+  if (isActiveLoading || isHistoryLoading) {
     return (
       <ChallengePageStyle>
         <div className="loading">불러오는 중...</div>
       </ChallengePageStyle>
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/ChallengePage/index.tsx` around lines 34 - 40, The
loading guard uses && so it stops showing the spinner as soon as one query
finishes, allowing the UI to render before both results exist; change the
conditional in ChallengePage (where isActiveLoading and isHistoryLoading are
used) to require either loader to be true (use isActiveLoading ||
isHistoryLoading) or explicitly verify both data values (activeChallenge and
historyPage) are non-null before rendering, ensuring the component waits until
both queries have completed.
frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx-34-47 (1)

34-47: ⚠️ Potential issue | 🟡 Minor

인터랙티브 요소의 기본 버튼 스타일/포커스 가시성을 명시해 주세요.

Line 34-47(PathHeader)과 Line 146-153(BackLink) 모두 키보드 포커스 피드백이 약하고, 버튼 기본 스타일이 브라우저별로 달라질 수 있습니다.

스타일 보완 예시
 export const PathHeader = styled.button<{ $expanded: boolean }>`
+  border: none;
+  cursor: pointer;
   display: flex;
@@
   &:hover {
     background: ${({ theme }) => theme.color.bluishGray[50].value};
   }
+
+  &:focus-visible {
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+  }
 `;
@@
 export const BackLink = styled.a`
@@
   &:hover {
     text-decoration: underline;
   }
+
+  &:focus-visible {
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+  }
 `;

Also applies to: 146-153

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

In `@frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx`
around lines 34 - 47, PathHeader and BackLink lack normalized button defaults
and accessible keyboard focus styles; update both styled.button components to
reset browser defaults (e.g., -webkit-appearance: none; appearance: none;
border: none; background-clip: padding-box) and explicitly define focus and
focus-visible states that provide a clear visible ring (e.g., outline: none; and
a focus-visible box-shadow or border with the theme color and preserved
border-radius), keeping the existing hover behavior and transition. Ensure the
focus styles use theme colors and respect the $expanded border-radius logic in
PathHeader so keyboard users see a consistent focus ring, and apply the same
focus-visible rule to BackLink to cover both occurrences.
🧹 Nitpick comments (6)
frontend/tiggle/src/pages/ChallengeCreatePage/ChallengeCreatePageStyle.tsx (1)

33-65: 입력/셀렉트 기본 스타일이 중복되어 있어 공통 블록 추출을 권장합니다.
현재처럼 중복 유지 시 한쪽만 수정되는 드리프트가 생기기 쉽습니다.

♻️ 제안 diff
-import styled from "styled-components";
+import styled, { css } from "styled-components";
@@
 export const ChallengeCreatePageStyle = styled.div`
+  ${/* 공통 필드 컨트롤 스타일 */ ""}
+  .field-control {
+    width: 100%;
+    height: 48px;
+    padding: 0 16px;
+    border-radius: 12px;
+    border: 1px solid ${({ theme }) => theme.color.bluishGray[200].value};
+    background-color: ${({ theme }) => theme.color.white.value};
+    color: ${({ theme }) => theme.color.bluishGray[800].value};
+    ${({ theme }) => expandTypography(theme.typography.body.medium.regular)}
+
+    &:focus {
+      outline: none;
+      border-color: ${({ theme }) => theme.color.blue[600].value};
+    }
+  }
+
@@
-    .field-select {
-      width: 100%;
-      height: 48px;
-      padding: 0 16px;
-      border-radius: 12px;
-      border: 1px solid ${({ theme }) => theme.color.bluishGray[200].value};
-      background-color: ${({ theme }) => theme.color.white.value};
-      color: ${({ theme }) => theme.color.bluishGray[800].value};
-      ${({ theme }) => expandTypography(theme.typography.body.medium.regular)}
+    .field-select {
+      `@extend` .field-control;
       cursor: pointer;
       appearance: none;
-
-      &:focus {
-        outline: none;
-        border-color: ${({ theme }) => theme.color.blue[600].value};
-      }
     }
 
     .field-input {
-      width: 100%;
-      height: 48px;
-      padding: 0 16px;
-      border-radius: 12px;
-      border: 1px solid ${({ theme }) => theme.color.bluishGray[200].value};
-      background-color: ${({ theme }) => theme.color.white.value};
-      color: ${({ theme }) => theme.color.bluishGray[800].value};
-      ${({ theme }) => expandTypography(theme.typography.body.medium.regular)}
-
-      &:focus {
-        outline: none;
-        border-color: ${({ theme }) => theme.color.blue[600].value};
-      }
+      `@extend` .field-control;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/ChallengeCreatePage/ChallengeCreatePageStyle.tsx`
around lines 33 - 65, The .field-select and .field-input blocks duplicate
layout/visual rules; extract their shared styles into a single reusable block
(e.g., a CSS class or styled helper/mixin like .field-base or a `fieldStyles`
constant using expandTypography) and then have .field-select and .field-input
extend/compose that shared block while keeping only their unique rules
(appearance and cursor for .field-select). Update references to
expandTypography(theme.typography.body.medium.regular) to live in the shared
block so typography remains consistent.
frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsx (1)

38-40: 진행 바에 ARIA 메타데이터를 추가해 접근성을 보강해 주세요.
현재는 시각적으로만 진행률이 표현되어 보조기기 사용자가 상태를 파악하기 어렵습니다.

♿ 제안 diff
-      <BarTrack>
+      <BarTrack
+        role="progressbar"
+        aria-label="경험치 진행률"
+        aria-valuemin={0}
+        aria-valuemax={100}
+        aria-valuenow={percent}
+      >
         <BarFill $percent={percent} />
       </BarTrack>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsx`
around lines 38 - 40, Add proper ARIA metadata to the visual progress bar so
assistive tech can read its value: inside the ExpProgressBar component wrap or
update the element rendering BarTrack/BarFill (references: BarTrack, BarFill,
and the percent prop) to expose role="progressbar" plus aria-valuenow (numeric
percent clamped 0–100), aria-valuemin="0", aria-valuemax="100", and either
aria-label or aria-labelledby that describes the progress (e.g., "experience
progress"); optionally include aria-valuetext when you need a localized
human-friendly string. Ensure the percent value passed to aria-valuenow is
coerced to a number and clamped so it never reports NaN or values outside 0–100.
frontend/tiggle/src/router.tsx (1)

1-2: withSuspenseany를 제거하고 타입을 명확히 해주세요.

eslint disable 없이 ComponentType로 충분히 표현 가능합니다. fallback도 최소한의 로딩 상태를 드러내는 편이 좋습니다.

타입 안전 리팩터 예시
-import { lazy, Suspense } from "react";
+import { lazy, Suspense, type ComponentType } from "react";
@@
-// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-const withSuspense = (Component: React.ComponentType<any>) => (
-  <Suspense fallback={<div />}>
+const withSuspense = (Component: ComponentType) => (
+  <Suspense fallback={<div aria-busy="true" />}>
     <Component />
   </Suspense>
 );

Also applies to: 26-31

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

In `@frontend/tiggle/src/router.tsx` around lines 1 - 2, Remove the use of any in
the withSuspense helper and replace it with a proper React type (use
React.ComponentType or React.ComponentType<Record<string, unknown>> as the
generic for the wrapped component) so props are type-checked; update
withSuspense's signature and return type accordingly, and swap the vague
fallback for a minimal loading UI (e.g., a small <div> or simple spinner
element) so Suspense displays a concrete loading state; apply the same
typing/fallback change to the other occurrences referenced around the 26-31 area
to keep consistency.
frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendarStyle.tsx (1)

41-54: 하드코딩된 색상을 테마 색상으로 대체하는 것을 권장합니다.

#e8f5e9, #ffebee, #28c79c, #f25f5f 등의 하드코딩된 색상이 사용되고 있습니다. 테마 색상 시스템과의 일관성을 위해 시맨틱 색상을 테마에 정의하고 사용하는 것이 좋습니다. 다크 모드 지원 시에도 유리합니다.

Also applies to: 55-68, 73-84, 87-96

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

In
`@frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendarStyle.tsx`
around lines 41 - 54, Replace hardcoded hex colors in the styled component
switch on prop `$type` (the background-color block) and the other similar blocks
(lines referenced after 55-68, 73-84, 87-96) with semantic tokens from the
theme: add descriptive keys like `theme.color.challenge.noSpend`,
`theme.color.challenge.spent`, `theme.color.challenge.future`,
`theme.color.challenge.empty` (or similar names you prefer) to your theme
object, then update the switch cases in the component to return those theme
tokens instead of literal values; ensure you import/use the same `theme` object
already referenced in the styled component so dark mode and global theming apply
consistently.
frontend/tiggle/src/pages/AchievementPage/index.tsx (1)

29-43: 로딩 상태 처리가 없습니다.

쿼리가 로딩 중일 때 빈 배열이 기본값으로 사용되어 "0 달성 완료", "0% 달성률" 등이 잠시 표시될 수 있습니다. 다른 페이지들과 일관성을 위해 로딩 상태를 추가하는 것을 권장합니다.

💡 로딩 상태 추가 제안
+  const isLoading = !allRes || !recentRes;
+
+  if (isLoading) {
+    return (
+      <AchievementPageContainer>
+        <EmptyMessage>불러오는 중...</EmptyMessage>
+      </AchievementPageContainer>
+    );
+  }
+
   const allAchievements: AchievementRespDto[] = allRes?.data ?? [];
   const recentAchievements: AchievementRespDto[] = recentRes?.data ?? [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/AchievementPage/index.tsx` around lines 29 - 43,
Queries for achievements are using useQuery without handling loading, causing
temporary "0" counts; update both useQuery calls (the ones with queryKey
achievementKeys.lists() and achievementKeys.recent(3)) to extract
isLoading/isFetching flags, and gate usage of
allAchievements/recentAchievements, achievedCount and totalCount until data is
loaded (e.g., show a loader/skeleton or return null while either isLoading is
true) so the UI does not render 0%/0 counts during the fetch.
frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx (1)

67-90: 키보드 포커스 상태도 :hover와 동등하게 제공해 주세요.

Line 82-84는 마우스 호버만 처리되어 있어 키보드 탐색 시 시각적 피드백이 약합니다.

접근성 보완 예시
 export const CatalogLink = styled.a`
@@
   &:hover {
     background: ${({ theme }) => theme.color.blue[50].value};
   }
+
+  &:focus-visible {
+    background: ${({ theme }) => theme.color.blue[50].value};
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+  }
@@
 `;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx` around lines
67 - 90, The CatalogLink styled component only styles &:hover for visual
feedback; add equivalent keyboard focus styles by including &:focus and
&:focus-visible (or a combined selector like &:hover, &:focus, &:focus-visible)
to apply the same background color (theme.color.blue[50].value) and any custom
focus ring you prefer so keyboard users receive the same visual cue; update the
CatalogLink component to include these selectors and ensure they use the same
theme values as the existing &:hover rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/tiggle/src/pages/ChallengeCreatePage/index.tsx`:
- Around line 27-28: The preview computes endDate incorrectly causing a 1-day
overcount: change the endDate calculation in ChallengeCreatePage so it adds
targetDays - 1 days to startDate (ensure you guard for targetDays <= 0, e.g.,
Math.max(0, targetDays - 1)) when using dayjs; update the symbols startDate and
endDate where endDate is currently computed from startDate.add(targetDays,
"day") to use targetDays - 1 so a 1-day challenge shows as a single day.

In
`@frontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCard.tsx`:
- Around line 32-35: The card currently uses ChallengeHistoryCardStyle with an
onClick handler (onClick={() => navigate(`/challenges/${id}`)}), which breaks
keyboard accessibility; replace the non-semantic clickable div with a semantic
focusable element — e.g., render ChallengeHistoryCardStyle as a react-router
<Link> (to={`/challenges/${id}`}) or as a <button> and move the navigate call
into its onClick, ensuring it supports keyboard activation (Enter/Space) and has
appropriate accessible attributes (role/aria-label if needed); update any styles
to apply to the new element and remove the plain onClick from a non-semantic
div.

In `@frontend/tiggle/src/pages/ChallengePage/ChallengePageStyle.tsx`:
- Around line 6-9: The wrapper in ChallengePageStyle.tsx using width: 100% with
padding: 24px causes overflow on small viewports because the element's
content-box adds padding to the specified width; update the style for that
wrapper (the CSS rule containing width: 100%, max-width: 480px, margin: 0 auto,
padding: 24px) to use box-sizing: border-box so padding is included in the width
(or alternatively change width to calc(100% - 48px)), ensuring it no longer
exceeds its parent and prevents horizontal scrolling on mobile.

In `@frontend/tiggle/src/pages/ChallengePage/index.tsx`:
- Around line 52-58: The Link currently wraps the button so clicking navigates
even when hasActiveChallenge is true; update the rendering in the ChallengePage
component to conditionally render: when hasActiveChallenge is true, render a
disabled button without the Link wrapper; when false, render the Link
(to="/challenges/create") containing the active button. Locate the JSX using
Link, the button with className "create-button", and the hasActiveChallenge
variable and change the conditional rendering accordingly so navigation is only
possible when hasActiveChallenge is false.

In `@frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx`:
- Line 120: BackLink is currently a styled.a and using href causes full page
navigation; in CharacterCatalogPage replace the href-based BackLink navigation
with React Router navigation (use Link or pass as={Link} / component={Link}) and
set to="/mypage/character" so the SPA routing preserves cache/scroll/state;
update the BackLink usage in CharacterCatalogPage (and adjust BackLink styled
component if needed to accept a "to" prop or polymorphic "as"/"component") to
perform client-side navigation.

In
`@frontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplayStyle.tsx`:
- Around line 81-97: The LEGENDARY branch for CharacterDisplayStyle currently
applies continuous animations (holoGradient and pulseGlow); wrap those
animations in a prefers-reduced-motion media query so users who request reduced
motion get no or minimal animation. Update the styled-component condition where
$rarity === "LEGENDARY" (and similarly any holoGradient usage) to include `@media`
(prefers-reduced-motion: reduce) { animation: none; background-size: initial; }
or an equivalent minimal-change fallback to disable/stop the holoGradient and
pulseGlow animations when the user preference is set.

In `@frontend/tiggle/src/pages/CharacterPage/index.tsx`:
- Around line 38-42: The call to useQuery is using the removed positional args
style and must be converted to TanStack Query v5's object syntax: replace the
positional call useQuery(characterKeys.me(), () =>
CharacterApiControllerService.getMyCharacter(), { staleTime: 1000 * 60 * 5 })
with the object form useQuery({ queryKey: characterKeys.me(), queryFn: () =>
CharacterApiControllerService.getMyCharacter(), staleTime: 1000 * 60 * 5 }) so
that queryKey uses characterKeys.me and queryFn calls
CharacterApiControllerService.getMyCharacter.

In `@frontend/tiggle/src/pages/InventoryPage/InventoryPageStyle.tsx`:
- Around line 34-52: TabButton currently only defines :hover styles so keyboard
users can't see focus; add a :focus-visible rule to TabButton that gives a clear
visible ring (e.g., outline or box-shadow and outline-offset) and preserve
existing hover/active color logic so keyboard focus matches active state; ensure
you do not remove default focus behavior for mouse users (use :focus-visible not
:focus). Apply the same change to the other interactive styled button components
in this file (the other tab/button styled components around the other ranges) so
all keyboard-focusable controls use a consistent focus-visible style.

In
`@frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdownStyle.tsx`:
- Around line 6-15: The fixed widths in CategoryBreakdownStyle.tsx (currently
width: 327px / 480px with separate padding) cause overflow on narrow screens;
change the layout to use fluid sizing and include padding in the box model:
replace the hard width with width: 100% plus max-width: 327px (and max-width:
480px inside ${({ theme }) => theme.mq.desktop}), and add box-sizing: border-box
so the padding is included in the element's width; this keeps the card
responsive in small viewports while preserving the intended max widths on larger
screens.

In `@frontend/tiggle/src/pages/StatisticsPage/index.tsx`:
- Around line 19-33: The weekly and monthly queries only destructure data and
isLoading; also destructure isError and error from both useQuery calls (for
weeklyResp/isWeeklyLoading and monthlyResp/isMonthlyLoading) and surface a
combined error state (e.g., isWeeklyError || isMonthlyError) before rendering
the main content; when an error exists, render a clear error/fallback UI (show
error.message or a friendly message) instead of the empty page. Locate the
useQuery calls for StatisticsApiControllerService.getWeeklyComparison and
getMonthlySummary and update them to return/handle isError and error, and add a
conditional early return that prevents entering the body when either query
failed.

---

Minor comments:
In `@docs/sessions/2026-04-12_1_plan.md`:
- Around line 27-35: Add a blank line between the section header "## 작업 계획" and
the table header row ("| # | 작업 | 프로젝트 | 파일 | 난이도 |") and ensure there's at
least one blank line after the table ends as well to satisfy markdownlint rule
MD058; update the markdown around those unique lines so the header and table are
separated by an empty line (and add a trailing blank line after the final table
row) to remove the lint warning.

In
`@frontend/tiggle/src/pages/AchievementPage/AchievementCard/AchievementCard.tsx`:
- Around line 15-25: CONDITION_LABELS is currently typed as
Record<string,string>, which loses compile-time checks; change it to
Record<AchievementConditionTypeValue, string> (import/alias the
AchievementConditionTypeValue type used across the app) and update the object
literal for CONDITION_LABELS to use that type so the compiler will force updates
when condition variants change; also update any related maps (units or other
label maps) the same way and run type-check to fix any missing keys or incorrect
usages referencing CONDITION_LABELS or AchievementConditionTypeValue.

In
`@frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendar.tsx`:
- Around line 16-21: The current compact-format logic in DailyLogCalendar.tsx
uses Math.round for the branches checking "amount >= 10000" and "amount >=
1000", which can overstate values (e.g., 15000 → "2만"); change those returns to
either truncate (use Math.floor(amount / 10000) + '만' and Math.floor(amount /
1000) + '천') or format with one decimal place (use (amount / 10000).toFixed(1) +
'만' and (amount / 1000).toFixed(1) + '천') so displayed amounts do not round up;
update the two return expressions accordingly.
- Around line 58-65: The current logic in DailyLogCalendar.tsx sets the local
variable type ("no-spend" | "spent" | "future") to "future" for any day after
today or any day with no log, which conflates past-unlogged days with true
future days; update the type union to include an "empty" (or "unrecorded")
state, change the else branch that currently assigns "future" when !log to
assign "empty" instead, and then update any downstream code/consumers that
inspect type (rendering, styles, tests) to handle the new "empty" case
accordingly (search for usages of the type variable and the DailyLogCalendar
rendering logic to adjust behavior).

In `@frontend/tiggle/src/pages/ChallengePage/ActiveChallenge/ActiveChallenge.tsx`:
- Around line 29-32: Clamp the computed percentage to the 0–100 range so the SVG
ring math and displayed value never go outside bounds: when computing percentage
(the variable named percentage used to derive strokeDashoffset) replace the raw
(achievedDays / targetDays) * 100 with a clamped value between 0 and 100 (also
handle targetDays === 0 as 0), and use that clamped percentage wherever
strokeDashoffset and any displayed percent string are rendered (references:
percentage, strokeDashoffset, circumference, radius).

In `@frontend/tiggle/src/pages/ChallengePage/index.tsx`:
- Around line 34-40: The loading guard uses && so it stops showing the spinner
as soon as one query finishes, allowing the UI to render before both results
exist; change the conditional in ChallengePage (where isActiveLoading and
isHistoryLoading are used) to require either loader to be true (use
isActiveLoading || isHistoryLoading) or explicitly verify both data values
(activeChallenge and historyPage) are non-null before rendering, ensuring the
component waits until both queries have completed.

In
`@frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx`:
- Around line 34-47: PathHeader and BackLink lack normalized button defaults and
accessible keyboard focus styles; update both styled.button components to reset
browser defaults (e.g., -webkit-appearance: none; appearance: none; border:
none; background-clip: padding-box) and explicitly define focus and
focus-visible states that provide a clear visible ring (e.g., outline: none; and
a focus-visible box-shadow or border with the theme color and preserved
border-radius), keeping the existing hover behavior and transition. Ensure the
focus styles use theme colors and respect the $expanded border-radius logic in
PathHeader so keyboard users see a consistent focus ring, and apply the same
focus-visible rule to BackLink to cover both occurrences.

In `@frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsx`:
- Around line 22-26: The percent calculation in ExpProgressBar can produce
negative values when experience is negative, causing BarFill to receive invalid
widths; update the percent computation (variable percent in ExpProgressBar) to
clamp the result at a minimum of 0 (e.g., wrap the computed percentage with
Math.max(0, ...)) while preserving the existing max-level and nextLevelExp logic
so percent never goes below 0 or above 100 before passing to BarFill.

In `@frontend/tiggle/src/pages/InventoryPage/EquipmentSlots/EquipmentSlots.tsx`:
- Around line 84-99: The component currently sets $hasItem based on equippedId
but renders label based on equippedName, which can mismatch; change the presence
check to use equippedName (result of findItemName) so $hasItem is derived from
equippedName truthiness instead of equippedId—update the binding that sets
hasItem (currently const hasItem = equippedId != null) to use const hasItem =
Boolean(equippedName) so SlotBox/$hasItem, SlotLabel and EquippedItemName all
rely on the same criterion; keep existing variables equippedId, equippedName,
findItemName, SlotBox, SlotIcon, SlotLabel, EquippedItemName and onSlotClick
references intact.

In
`@frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdown.tsx`:
- Around line 35-38: The bar width currently only applies an upper bound using
Math.min(item.ratio, 100) which lets negative ratios produce invalid widths;
update the width calculation for the element with className "bar-fill" in
CategoryBreakdown.tsx to clamp item.ratio into the 0–100 range (e.g., ensure the
value is at least 0 and at most 100) before appending "%" so negative values
become 0% and values above 100 become 100%.

In
`@frontend/tiggle/src/pages/StatisticsPage/WeeklyComparison/WeeklyComparison.tsx`:
- Around line 14-15: formatPeriod currently slices off the year (start.slice(5))
which breaks year-boundary weeks and datetime inputs; update the formatPeriod
function to parse the incoming ISO date/datetime strings (e.g., trim to the date
portion or use Date/dayjs) and format them robustly: keep full YYYY-MM-DD or
format as MM-DD but include the year when start and end fall in different years
(or when input contains time), e.g., derive dateStart and dateEnd from
start/end, compare years, and return either "MM-DD ~ MM-DD" when same year or
"YYYY-MM-DD ~ YYYY-MM-DD" when years differ, ensuring it handles both date-only
and datetime inputs.

---

Nitpick comments:
In `@frontend/tiggle/src/pages/AchievementPage/index.tsx`:
- Around line 29-43: Queries for achievements are using useQuery without
handling loading, causing temporary "0" counts; update both useQuery calls (the
ones with queryKey achievementKeys.lists() and achievementKeys.recent(3)) to
extract isLoading/isFetching flags, and gate usage of
allAchievements/recentAchievements, achievedCount and totalCount until data is
loaded (e.g., show a loader/skeleton or return null while either isLoading is
true) so the UI does not render 0%/0 counts during the fetch.

In `@frontend/tiggle/src/pages/ChallengeCreatePage/ChallengeCreatePageStyle.tsx`:
- Around line 33-65: The .field-select and .field-input blocks duplicate
layout/visual rules; extract their shared styles into a single reusable block
(e.g., a CSS class or styled helper/mixin like .field-base or a `fieldStyles`
constant using expandTypography) and then have .field-select and .field-input
extend/compose that shared block while keeping only their unique rules
(appearance and cursor for .field-select). Update references to
expandTypography(theme.typography.body.medium.regular) to live in the shared
block so typography remains consistent.

In
`@frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendarStyle.tsx`:
- Around line 41-54: Replace hardcoded hex colors in the styled component switch
on prop `$type` (the background-color block) and the other similar blocks (lines
referenced after 55-68, 73-84, 87-96) with semantic tokens from the theme: add
descriptive keys like `theme.color.challenge.noSpend`,
`theme.color.challenge.spent`, `theme.color.challenge.future`,
`theme.color.challenge.empty` (or similar names you prefer) to your theme
object, then update the switch cases in the component to return those theme
tokens instead of literal values; ensure you import/use the same `theme` object
already referenced in the styled component so dark mode and global theming apply
consistently.

In `@frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx`:
- Around line 67-90: The CatalogLink styled component only styles &:hover for
visual feedback; add equivalent keyboard focus styles by including &:focus and
&:focus-visible (or a combined selector like &:hover, &:focus, &:focus-visible)
to apply the same background color (theme.color.blue[50].value) and any custom
focus ring you prefer so keyboard users receive the same visual cue; update the
CatalogLink component to include these selectors and ensure they use the same
theme values as the existing &:hover rule.

In `@frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsx`:
- Around line 38-40: Add proper ARIA metadata to the visual progress bar so
assistive tech can read its value: inside the ExpProgressBar component wrap or
update the element rendering BarTrack/BarFill (references: BarTrack, BarFill,
and the percent prop) to expose role="progressbar" plus aria-valuenow (numeric
percent clamped 0–100), aria-valuemin="0", aria-valuemax="100", and either
aria-label or aria-labelledby that describes the progress (e.g., "experience
progress"); optionally include aria-valuetext when you need a localized
human-friendly string. Ensure the percent value passed to aria-valuenow is
coerced to a number and clamped so it never reports NaN or values outside 0–100.

In `@frontend/tiggle/src/router.tsx`:
- Around line 1-2: Remove the use of any in the withSuspense helper and replace
it with a proper React type (use React.ComponentType or
React.ComponentType<Record<string, unknown>> as the generic for the wrapped
component) so props are type-checked; update withSuspense's signature and return
type accordingly, and swap the vague fallback for a minimal loading UI (e.g., a
small <div> or simple spinner element) so Suspense displays a concrete loading
state; apply the same typing/fallback change to the other occurrences referenced
around the 26-31 area to keep consistency.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d99104cc-e825-40e1-9724-ffbbfdc21bcd

📥 Commits

Reviewing files that changed from the base of the PR and between 707cf8a and 1dd56d9.

⛔ Files ignored due to path filters (6)
  • frontend/tiggle/src/generated/index.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/AchievementApiControllerService.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/ChallengeApiControllerService.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/CharacterApiControllerService.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/ItemApiControllerService.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/StatisticsApiControllerService.ts is excluded by !**/generated/**
📒 Files selected for processing (46)
  • docs/sessions/2026-04-12_1_plan.md
  • docs/sessions/2026-04-12_1_result.md
  • frontend/tiggle/src/components/atoms/TypeTag/TypeTag.tsx
  • frontend/tiggle/src/pages/AchievementPage/AchievementCard/AchievementCard.tsx
  • frontend/tiggle/src/pages/AchievementPage/AchievementCard/AchievementCardStyle.tsx
  • frontend/tiggle/src/pages/AchievementPage/AchievementPageStyle.tsx
  • frontend/tiggle/src/pages/AchievementPage/index.tsx
  • frontend/tiggle/src/pages/ChallengeCreatePage/ChallengeCreatePageStyle.tsx
  • frontend/tiggle/src/pages/ChallengeCreatePage/index.tsx
  • frontend/tiggle/src/pages/ChallengeDetailPage/ChallengeDetailPageStyle.tsx
  • frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendar.tsx
  • frontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendarStyle.tsx
  • frontend/tiggle/src/pages/ChallengeDetailPage/index.tsx
  • frontend/tiggle/src/pages/ChallengePage/ActiveChallenge/ActiveChallenge.tsx
  • frontend/tiggle/src/pages/ChallengePage/ActiveChallenge/ActiveChallengeStyle.tsx
  • frontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCard.tsx
  • frontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCardStyle.tsx
  • frontend/tiggle/src/pages/ChallengePage/ChallengePageStyle.tsx
  • frontend/tiggle/src/pages/ChallengePage/index.tsx
  • frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx
  • frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx
  • frontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplay.tsx
  • frontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplayStyle.tsx
  • frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx
  • frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsx
  • frontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBarStyle.tsx
  • frontend/tiggle/src/pages/CharacterPage/index.tsx
  • frontend/tiggle/src/pages/CreatePage/TransactionPreviewCell/TransactionPreviewCell.tsx
  • frontend/tiggle/src/pages/DetailPage/ReplyCell/ReplyCell.tsx
  • frontend/tiggle/src/pages/DetailPage/index.tsx
  • frontend/tiggle/src/pages/InventoryPage/EquipmentSlots/EquipmentSlots.tsx
  • frontend/tiggle/src/pages/InventoryPage/EquipmentSlots/EquipmentSlotsStyle.tsx
  • frontend/tiggle/src/pages/InventoryPage/InventoryPageStyle.tsx
  • frontend/tiggle/src/pages/InventoryPage/ItemCard/ItemCard.tsx
  • frontend/tiggle/src/pages/InventoryPage/ItemCard/ItemCardStyle.tsx
  • frontend/tiggle/src/pages/InventoryPage/index.tsx
  • frontend/tiggle/src/pages/NotFoundPage.tsx
  • frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdown.tsx
  • frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdownStyle.tsx
  • frontend/tiggle/src/pages/StatisticsPage/StatisticsPageStyle.tsx
  • frontend/tiggle/src/pages/StatisticsPage/WeeklyComparison/WeeklyComparison.tsx
  • frontend/tiggle/src/pages/StatisticsPage/WeeklyComparison/WeeklyComparisonStyle.tsx
  • frontend/tiggle/src/pages/StatisticsPage/index.tsx
  • frontend/tiggle/src/query/queryKeys.ts
  • frontend/tiggle/src/router.tsx
  • frontend/tiggle/src/types/gamification.ts

Comment thread frontend/tiggle/src/pages/ChallengeCreatePage/index.tsx Outdated
Comment thread frontend/tiggle/src/pages/ChallengePage/ChallengePageStyle.tsx
Comment thread frontend/tiggle/src/pages/ChallengePage/index.tsx Outdated
Comment thread frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx Outdated
Comment thread frontend/tiggle/src/pages/CharacterPage/index.tsx Outdated
Comment on lines +34 to +52
export const TabButton = styled.button<{ $active: boolean }>`
flex: 1;
padding: 12px 0;
${({ theme }) => expandTypography(theme.typography.body.medium.bold)}
color: ${({ theme, $active }) =>
$active ? theme.color.blue[600].value : theme.color.bluishGray[400].value};
border-bottom: 2px solid
${({ theme, $active }) =>
$active ? theme.color.blue[600].value : "transparent"};
background: none;
cursor: pointer;
transition:
color 0.15s ease,
border-color 0.15s ease;

&:hover {
color: ${({ theme }) => theme.color.blue[600].value};
}
`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

키보드 포커스 가시성이 없어 접근성이 떨어집니다.

현재 인터랙티브 버튼들에 :hover만 있고 :focus-visible이 없어 키보드 사용자에게 현재 포커스 위치가 보이지 않습니다. 포커스 스타일을 명시적으로 추가해 주세요.

접근성 포커스 스타일 제안
 export const TabButton = styled.button<{ $active: boolean }>`
+  border: none;
   flex: 1;
   padding: 12px 0;
@@
   &:hover {
     color: ${({ theme }) => theme.color.blue[600].value};
   }
+
+  &:focus-visible {
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+  }
 `;
@@
 export const EquipModalItem = styled.button<{ $selected: boolean }>`
+  border: 2px solid
+    ${({ theme, $selected }) =>
+      $selected
+        ? theme.color.blue[600].value
+        : theme.color.bluishGray[200].value};
@@
   &:hover {
     border-color: ${({ theme }) => theme.color.blue[600].value};
   }
+
+  &:focus-visible {
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+  }
 `;
@@
 export const EquipModalButton = styled.button<{ $primary?: boolean }>`
+  border: none;
   flex: 1;
@@
   &:hover {
     opacity: 0.85;
   }
+
+  &:focus-visible {
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+  }
 `;

Also applies to: 112-132, 145-160

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

In `@frontend/tiggle/src/pages/InventoryPage/InventoryPageStyle.tsx` around lines
34 - 52, TabButton currently only defines :hover styles so keyboard users can't
see focus; add a :focus-visible rule to TabButton that gives a clear visible
ring (e.g., outline or box-shadow and outline-offset) and preserve existing
hover/active color logic so keyboard focus matches active state; ensure you do
not remove default focus behavior for mouse users (use :focus-visible not
:focus). Apply the same change to the other interactive styled button components
in this file (the other tab/button styled components around the other ranges) so
all keyboard-focusable controls use a consistent focus-visible style.

Comment on lines +19 to +33
const { data: weeklyResp, isLoading: isWeeklyLoading } = useQuery(
statisticsKeys.weekly(0),
() => StatisticsApiControllerService.getWeeklyComparison(0),
);

const now = new Date();
const { data: monthlyResp, isLoading: isMonthlyLoading } = useQuery(
statisticsKeys.monthly(now.getFullYear(), now.getMonth() + 1),
() =>
StatisticsApiControllerService.getMonthlySummary(
now.getFullYear(),
now.getMonth() + 1,
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

쿼리 실패 상태가 누락되어 실패 시 화면이 비어 보일 수 있습니다.

Line 19-33에서 isError/error를 읽지 않고 Line 44 이후 로딩만 해제하면 본문으로 진입합니다. API 실패 시 제목만 보이는 상태가 되어 사용자 입장에서 원인 파악이 어렵습니다.

수정 예시
-  const { data: weeklyResp, isLoading: isWeeklyLoading } = useQuery(
+  const {
+    data: weeklyResp,
+    isLoading: isWeeklyLoading,
+    isError: isWeeklyError,
+  } = useQuery(
     statisticsKeys.weekly(0),
     () => StatisticsApiControllerService.getWeeklyComparison(0),
   );

-  const { data: monthlyResp, isLoading: isMonthlyLoading } = useQuery(
+  const {
+    data: monthlyResp,
+    isLoading: isMonthlyLoading,
+    isError: isMonthlyError,
+  } = useQuery(
     statisticsKeys.monthly(now.getFullYear(), now.getMonth() + 1),
     () =>
       StatisticsApiControllerService.getMonthlySummary(
         now.getFullYear(),
         now.getMonth() + 1,
       ),
   );

   const isLoading = isWeeklyLoading || isMonthlyLoading;
+  const isError = isWeeklyError || isMonthlyError;

   if (isLoading) {
     return (
       <StatisticsPageWrapper>
         <p className="page-title">소비 통계</p>
         <div className="loading-container">
           <p>불러오는 중...</p>
         </div>
       </StatisticsPageWrapper>
     );
   }
+
+  if (isError) {
+    return (
+      <StatisticsPageWrapper>
+        <p className="page-title">소비 통계</p>
+        <div className="loading-container">
+          <p>통계를 불러오지 못했어요. 잠시 후 다시 시도해 주세요.</p>
+        </div>
+      </StatisticsPageWrapper>
+    );
+  }

Also applies to: 44-53

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

In `@frontend/tiggle/src/pages/StatisticsPage/index.tsx` around lines 19 - 33, The
weekly and monthly queries only destructure data and isLoading; also destructure
isError and error from both useQuery calls (for weeklyResp/isWeeklyLoading and
monthlyResp/isMonthlyLoading) and surface a combined error state (e.g.,
isWeeklyError || isMonthlyError) before rendering the main content; when an
error exists, render a clear error/fallback UI (show error.message or a friendly
message) instead of the empty page. Locate the useQuery calls for
StatisticsApiControllerService.getWeeklyComparison and getMonthlySummary and
update them to return/handle isError and error, and add a conditional early
return that prevents entering the body when either query failed.

- Fix challenge end date off-by-one (targetDays → targetDays-1)
- Add keyboard accessibility to ChallengeHistoryCard (as="button")
- Add box-sizing: border-box to prevent mobile overflow
- Fix disabled Link still navigating (conditional render)
- Replace href with React Router Link for SPA navigation
- Add prefers-reduced-motion for character animations
- Standardize useQuery to object syntax (v4 compat, v5 ready)
- Add focus-visible styles for keyboard accessibility
- Fix fixed-width overflow on small screens (width → max-width)
- Add error state handling to StatisticsPage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (4)
frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx (1)

36-49: PathHeader:focus-visible 스타일을 추가하는 것을 권장합니다.

키보드 탐색 시 현재 포커스 위치를 명확히 보여주면 접근성이 더 좋아집니다.

포커스 가시성 강화 예시
 export const PathHeader = styled.button<{ $expanded: boolean }>`
   display: flex;
   align-items: center;
   justify-content: space-between;
+  cursor: pointer;
   width: 100%;
   padding: 16px 20px;
   background: ${({ theme }) => theme.color.white.value};
   border-radius: ${({ $expanded }) => ($expanded ? "16px 16px 0 0" : "16px")};
   transition: border-radius 0.2s ease;

   &:hover {
     background: ${({ theme }) => theme.color.bluishGray[50].value};
   }
+
+  &:focus-visible {
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+  }
 `;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx`
around lines 36 - 49, Add a :focus-visible state to the PathHeader styled.button
so keyboard users get clear focus indication; update the PathHeader
(styled.button with prop $expanded) to include a visible focus style (e.g.,
outline or box-shadow and optionally adjust background) that complements the
existing hover state while preserving border-radius behavior when $expanded is
true.
frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx (2)

66-84: 기본 [] 생성으로 인한 useMemo 재계산을 줄일 수 있습니다.

data가 없을 때 매 렌더마다 새 배열이 만들어져 grouped가 불필요하게 다시 계산됩니다. 공유 상수 배열을 기본값으로 쓰면 안정적입니다.

미세 성능 개선 예시
+const EMPTY_CATALOG: CharacterCatalogDto[] = [];
...
-  const catalog = (data?.data as CharacterCatalogDto[] | undefined) ?? [];
+  const catalog =
+    (data?.data as CharacterCatalogDto[] | undefined) ?? EMPTY_CATALOG;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx` around lines 66 -
84, The issue: using (data?.data as CharacterCatalogDto[] | undefined) ?? []
creates a new empty array each render causing useMemo(() => { ... }, [catalog])
to recompute unnecessarily; fix by introducing a shared stable empty array
constant (e.g., const EMPTY_CATALOG: CharacterCatalogDto[] = []) declared
outside the component and replace the default fallback with ?? EMPTY_CATALOG so
catalog is stable when data is absent; update references in this file (the
catalog variable and the grouped useMemo that iterates PATH_ORDER and sorts
items) to use the stable EMPTY_CATALOG.

129-137: 접힘/펼침 헤더에 aria-expanded/aria-controls를 추가해 주세요.

현재는 시각적으로만 상태가 보이고, 보조기기에는 섹션 확장 상태가 충분히 전달되지 않습니다. 버튼-패널 관계를 명시하면 접근성이 개선됩니다.

접근성 속성 추가 예시
-            <PathHeader $expanded={isExpanded} onClick={() => togglePath(path)}>
+            <PathHeader
+              type="button"
+              $expanded={isExpanded}
+              aria-expanded={isExpanded}
+              aria-controls={`catalog-section-${path}`}
+              onClick={() => togglePath(path)}
+            >
...
-              <FormGrid>
+              <FormGrid id={`catalog-section-${path}`}>
...
-              <FormGrid>
+              <FormGrid id={`catalog-section-${path}`}>

Also applies to: 139-167

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

In `@frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx` around lines 129 -
137, The header currently only reflects expanded state visually; update the
interactive header (PathHeader) to expose accessibility attributes by adding
aria-expanded={isExpanded} and aria-controls pointing to the ID of the content
panel; ensure the panel element rendered for this path (the collapsible section
rendered after PathHeader in the same file, e.g., the component rendered in the
139-167 block) has a matching id (for example `${path}-panel`). Keep the
existing onClick={togglePath(path)} and also ensure the header is a proper
button (or has role="button" and is keyboard-focusable) so screen readers can
interact with it.
frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx (1)

84-86: Link 컴포넌트에 키보드 포커스 상태를 명시적으로 추가하세요.

이 Link 컴포넌트는 :hover만 정의되어 있고 포커스 상태가 없습니다. 전역 reset에서는 outline을 제거하지 않지만, 명시적인 :focus-visible 추가는 접근성을 강화하고 다른 인터랙티브 요소와의 일관성을 보장합니다. 같은 패턴이 InventoryPageStyle에 이미 구현되어 있습니다.

제안 패치
  &:hover {
    background: ${({ theme }) => theme.color.blue[50].value};
  }
+
+  &:focus-visible {
+    outline: 2px solid ${({ theme }) => theme.color.blue[600].value};
+    outline-offset: 2px;
+    background: ${({ theme }) => theme.color.blue[50].value};
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx` around lines
84 - 86, The Link styled rule in CharacterPageStyle.tsx only defines &:hover and
lacks a keyboard focus style; add a matching :focus-visible pseudo-class to the
same selector (the Link component) that applies the same background
(theme.color.blue[50].value) and any consistent focus outline/visual (matching
InventoryPageStyle) so keyboard users get an explicit visible focus state;
update the Link selector (the styled Link component) to include &:focus-visible
with the same styling as &:hover.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx`:
- Around line 36-49: Add a :focus-visible state to the PathHeader styled.button
so keyboard users get clear focus indication; update the PathHeader
(styled.button with prop $expanded) to include a visible focus style (e.g.,
outline or box-shadow and optionally adjust background) that complements the
existing hover state while preserving border-radius behavior when $expanded is
true.

In `@frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx`:
- Around line 66-84: The issue: using (data?.data as CharacterCatalogDto[] |
undefined) ?? [] creates a new empty array each render causing useMemo(() => {
... }, [catalog]) to recompute unnecessarily; fix by introducing a shared stable
empty array constant (e.g., const EMPTY_CATALOG: CharacterCatalogDto[] = [])
declared outside the component and replace the default fallback with ??
EMPTY_CATALOG so catalog is stable when data is absent; update references in
this file (the catalog variable and the grouped useMemo that iterates PATH_ORDER
and sorts items) to use the stable EMPTY_CATALOG.
- Around line 129-137: The header currently only reflects expanded state
visually; update the interactive header (PathHeader) to expose accessibility
attributes by adding aria-expanded={isExpanded} and aria-controls pointing to
the ID of the content panel; ensure the panel element rendered for this path
(the collapsible section rendered after PathHeader in the same file, e.g., the
component rendered in the 139-167 block) has a matching id (for example
`${path}-panel`). Keep the existing onClick={togglePath(path)} and also ensure
the header is a proper button (or has role="button" and is keyboard-focusable)
so screen readers can interact with it.

In `@frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx`:
- Around line 84-86: The Link styled rule in CharacterPageStyle.tsx only defines
&:hover and lacks a keyboard focus style; add a matching :focus-visible
pseudo-class to the same selector (the Link component) that applies the same
background (theme.color.blue[50].value) and any consistent focus outline/visual
(matching InventoryPageStyle) so keyboard users get an explicit visible focus
state; update the Link selector (the styled Link component) to include
&:focus-visible with the same styling as &:hover.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c1805c1-db5c-4ed3-a9df-a0b94b882dc3

📥 Commits

Reviewing files that changed from the base of the PR and between 1dd56d9 and 0670a7c.

⛔ Files ignored due to path filters (4)
  • frontend/tiggle/src/generated/index.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/ChallengeApiControllerService.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/CharacterApiControllerService.ts is excluded by !**/generated/**
  • frontend/tiggle/src/generated/services/ItemApiControllerService.ts is excluded by !**/generated/**
📒 Files selected for processing (12)
  • frontend/tiggle/src/pages/ChallengeCreatePage/index.tsx
  • frontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCard.tsx
  • frontend/tiggle/src/pages/ChallengePage/ChallengePageStyle.tsx
  • frontend/tiggle/src/pages/ChallengePage/index.tsx
  • frontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsx
  • frontend/tiggle/src/pages/CharacterCatalogPage/index.tsx
  • frontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplayStyle.tsx
  • frontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsx
  • frontend/tiggle/src/pages/CharacterPage/index.tsx
  • frontend/tiggle/src/pages/InventoryPage/InventoryPageStyle.tsx
  • frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdownStyle.tsx
  • frontend/tiggle/src/pages/StatisticsPage/index.tsx
✅ Files skipped from review due to trivial changes (3)
  • frontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdownStyle.tsx
  • frontend/tiggle/src/pages/ChallengePage/ChallengePageStyle.tsx
  • frontend/tiggle/src/pages/InventoryPage/InventoryPageStyle.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • frontend/tiggle/src/pages/StatisticsPage/index.tsx
  • frontend/tiggle/src/pages/ChallengeCreatePage/index.tsx
  • frontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCard.tsx
  • frontend/tiggle/src/pages/ChallengePage/index.tsx
  • frontend/tiggle/src/pages/CharacterPage/index.tsx
  • frontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplayStyle.tsx

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.

2 participants