feat(fe): add gamification UI - statistics, character, items, achieve…#229
feat(fe): add gamification UI - statistics, character, items, achieve…#229kdh-92 wants to merge 2 commits into
Conversation
…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>
WalkthroughTiggle 프론트엔드에 게임화(gamification) 기능을 대규모로 추가하고 CI/CD 관련 세션 문서를 추가했습니다. 새로운 페이지(통계, 캐릭터, 인벤토리, 달성도, 챌린지), 다수의 컴포넌트/스타일/타입/쿼리 키, 라우터 라우트 및 TypeScript 타입들이 포함되어 있습니다. Changes
Sequence Diagram(s)(생성 조건 미충족 — 생략) Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 분 Possibly related PRs
💫 개요이 PR은 Tiggle 프론트엔드에 게임화(gamification) 시스템을 도입합니다. 통계, 캐릭터, 인벤토리, 달성도, 챌린지 페이지와 관련 컴포넌트, 스타일, 쿼리 키, 타입 정의를 추가하며, 라우터 구성을 확장합니다. 변경 사항
예상 코드 리뷰 소요 시간🎯 4 (복잡함) | ⏱️ ~60 분 관련 PR
🐰 축하 시
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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:withSuspense의any를 제거하고 타입을 명확히 해주세요.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
⛔ Files ignored due to path filters (6)
frontend/tiggle/src/generated/index.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/AchievementApiControllerService.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/ChallengeApiControllerService.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/CharacterApiControllerService.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/ItemApiControllerService.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/StatisticsApiControllerService.tsis excluded by!**/generated/**
📒 Files selected for processing (46)
docs/sessions/2026-04-12_1_plan.mddocs/sessions/2026-04-12_1_result.mdfrontend/tiggle/src/components/atoms/TypeTag/TypeTag.tsxfrontend/tiggle/src/pages/AchievementPage/AchievementCard/AchievementCard.tsxfrontend/tiggle/src/pages/AchievementPage/AchievementCard/AchievementCardStyle.tsxfrontend/tiggle/src/pages/AchievementPage/AchievementPageStyle.tsxfrontend/tiggle/src/pages/AchievementPage/index.tsxfrontend/tiggle/src/pages/ChallengeCreatePage/ChallengeCreatePageStyle.tsxfrontend/tiggle/src/pages/ChallengeCreatePage/index.tsxfrontend/tiggle/src/pages/ChallengeDetailPage/ChallengeDetailPageStyle.tsxfrontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendar.tsxfrontend/tiggle/src/pages/ChallengeDetailPage/DailyLogCalendar/DailyLogCalendarStyle.tsxfrontend/tiggle/src/pages/ChallengeDetailPage/index.tsxfrontend/tiggle/src/pages/ChallengePage/ActiveChallenge/ActiveChallenge.tsxfrontend/tiggle/src/pages/ChallengePage/ActiveChallenge/ActiveChallengeStyle.tsxfrontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCard.tsxfrontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCardStyle.tsxfrontend/tiggle/src/pages/ChallengePage/ChallengePageStyle.tsxfrontend/tiggle/src/pages/ChallengePage/index.tsxfrontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsxfrontend/tiggle/src/pages/CharacterCatalogPage/index.tsxfrontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplay.tsxfrontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplayStyle.tsxfrontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsxfrontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBar.tsxfrontend/tiggle/src/pages/CharacterPage/ExpProgressBar/ExpProgressBarStyle.tsxfrontend/tiggle/src/pages/CharacterPage/index.tsxfrontend/tiggle/src/pages/CreatePage/TransactionPreviewCell/TransactionPreviewCell.tsxfrontend/tiggle/src/pages/DetailPage/ReplyCell/ReplyCell.tsxfrontend/tiggle/src/pages/DetailPage/index.tsxfrontend/tiggle/src/pages/InventoryPage/EquipmentSlots/EquipmentSlots.tsxfrontend/tiggle/src/pages/InventoryPage/EquipmentSlots/EquipmentSlotsStyle.tsxfrontend/tiggle/src/pages/InventoryPage/InventoryPageStyle.tsxfrontend/tiggle/src/pages/InventoryPage/ItemCard/ItemCard.tsxfrontend/tiggle/src/pages/InventoryPage/ItemCard/ItemCardStyle.tsxfrontend/tiggle/src/pages/InventoryPage/index.tsxfrontend/tiggle/src/pages/NotFoundPage.tsxfrontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdown.tsxfrontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdownStyle.tsxfrontend/tiggle/src/pages/StatisticsPage/StatisticsPageStyle.tsxfrontend/tiggle/src/pages/StatisticsPage/WeeklyComparison/WeeklyComparison.tsxfrontend/tiggle/src/pages/StatisticsPage/WeeklyComparison/WeeklyComparisonStyle.tsxfrontend/tiggle/src/pages/StatisticsPage/index.tsxfrontend/tiggle/src/query/queryKeys.tsfrontend/tiggle/src/router.tsxfrontend/tiggle/src/types/gamification.ts
| 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}; | ||
| } | ||
| `; |
There was a problem hiding this comment.
키보드 포커스 가시성이 없어 접근성이 떨어집니다.
현재 인터랙티브 버튼들에 :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.
| 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, | ||
| ), | ||
| ); | ||
|
|
There was a problem hiding this comment.
쿼리 실패 상태가 누락되어 실패 시 화면이 비어 보일 수 있습니다.
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>
There was a problem hiding this comment.
🧹 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
⛔ Files ignored due to path filters (4)
frontend/tiggle/src/generated/index.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/ChallengeApiControllerService.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/CharacterApiControllerService.tsis excluded by!**/generated/**frontend/tiggle/src/generated/services/ItemApiControllerService.tsis excluded by!**/generated/**
📒 Files selected for processing (12)
frontend/tiggle/src/pages/ChallengeCreatePage/index.tsxfrontend/tiggle/src/pages/ChallengePage/ChallengeHistoryCard/ChallengeHistoryCard.tsxfrontend/tiggle/src/pages/ChallengePage/ChallengePageStyle.tsxfrontend/tiggle/src/pages/ChallengePage/index.tsxfrontend/tiggle/src/pages/CharacterCatalogPage/CharacterCatalogPageStyle.tsxfrontend/tiggle/src/pages/CharacterCatalogPage/index.tsxfrontend/tiggle/src/pages/CharacterPage/CharacterDisplay/CharacterDisplayStyle.tsxfrontend/tiggle/src/pages/CharacterPage/CharacterPageStyle.tsxfrontend/tiggle/src/pages/CharacterPage/index.tsxfrontend/tiggle/src/pages/InventoryPage/InventoryPageStyle.tsxfrontend/tiggle/src/pages/StatisticsPage/CategoryBreakdown/CategoryBreakdownStyle.tsxfrontend/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
…ments, challenges
Phase 2-4 FE implementation for the 현명 소비 시스템:
📝 Pull Request
📌 변경 사항 요약
✅ 체크리스트
🔍 관련 이슈
close #000
Summary by CodeRabbit
릴리스 노트
New Features
Documentation
Style