Skip to content

Feature/policy api#50

Merged
haeni82 merged 12 commits into
developfrom
feature/policy-api
Mar 15, 2026
Merged

Feature/policy api#50
haeni82 merged 12 commits into
developfrom
feature/policy-api

Conversation

@haeni82
Copy link
Copy Markdown
Contributor

@haeni82 haeni82 commented Mar 15, 2026

이슈

✔️ 체크리스트

  • : Merge할 브랜치를 확인해 주세요.

🔍 작업 내용

  • 정책 페이지, 구성원별 정책 제어 페이지, 메인 페이지, 알림 api 연결

⚠️ 주의 사항 / 기타

  • 알림 ID가 null 값으로 들어오고 있어 전체 읽음이나 안읽음 개수 카운팅이 정확하지 않음. 읽기 처리도 안됨
  • 즉시 차단에 대해서 현재시간보다 지나면 차단이 없는걸로 쳐져야하는데 그렇지 않음
  • 현재 적용중인 정책에서도 이미 지나간 정책을 띄움
  • 사용로그 페이지만 하면 api 연동끝
  • 페이지 마다 로딩 처리 하고 에러 없는지 꼼꼼히 테스트 해볼

Summary by CodeRabbit

  • New Features

    • 알림센터 개선: 무한 스크롤, 읽음 처리(개별/전체), 실시간 미확인 알림 카운트(30초 폴링).
    • 한도 관리 추가: 회선별 공유/일별 데이터 한도 조회 및 즉시 수정 기능.
    • 차단 정책 강화: 반복 및 즉시 차단 생성·편집·삭제 UI 및 시간 선택기 추가.
    • 소유권 이전: 가족 계정 대표자 권한 양도 기능.
    • 사용량 보기: 가족 구성원별 월간 사용량 차트 지원.
  • Improvements

    • 제한값 표시 강화("무제한" 표기) 및 차트/사용량 표시 개선.
    • 구성원 목록과 권한 UI에 소유자 인식 반영.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 15, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ffaab6af-4324-4570-ae7c-a583f40786fe

📥 Commits

Reviewing files that changed from the base of the PR and between a362b8b and c25ef49.

📒 Files selected for processing (4)
  • src/api/services/blockService.ts
  • src/api/services/familyService.ts
  • src/page/Admin/components/policy/BlockPolicyManager.tsx
  • src/page/Admin/components/policy/BlockPolicyTab.tsx

📝 Walkthrough

Walkthrough

정책, 블록, 제한, 알림 및 공유풀 관련 API/타입/컴포넌트를 서버 기반으로 대규모 전환 및 확장했습니다. limitService 추가, React Query로 상태/페칭 전환, 여러 블록/제한 편집 UI 및 유틸/상수·타입 확장이 포함됩니다.

Changes

Cohort / File(s) Summary
API 바렐·새 서비스
src/api/index.ts, src/api/services/limitService.ts
limitService 추가 및 index에 export. 라인 한도 조회/토글/패치 API 5개 엔드포인트 구현.
기존 API 타입·시그니처 변경
src/api/services/blockService.ts, src/api/services/familyService.ts, src/api/services/notificationService.ts, src/api/services/sharedPoolService.ts
blockService가 RepeatBlockResponse로 마이그레이션 및 즉시블록 엔드포인트 추가; familyService에 getMembersSimple/transferOwner 추가; notificationService에 조회/읽음/카운트 API 추가; sharedPoolService에 getUsageData 추가.
프론트엔드: 리액트쿼리·알림 전환
src/components/Header.tsx, src/page/Alarm.tsx, src/store/alarmStore.ts, src/data/alarmDummyData.ts
헤더와 Alarm 페이지를 React Query 기반으로 전환(무한스크롤, 폴링). 로컬 alarmStore 및 dummy 데이터 제거.
정책/정책상세 전반 리팩토링
src/page/Policy/**, src/page/PolicyDetail/**, src/page/Admin/**
Policy 페이지에 소유권 양도/패치, DataThresholdSlider 바이트 기반 로직·디바운스, PermissionManager memo화 및 토스트, PolicyDetail에서 블록·제한을 React Query 기반으로 전환.
블록 편집 UI 추가
src/page/PolicyDetail/components/Block/*
BlockItem, EditPanel, ScrollPicker, TimePicker 컴포넌트 추가로 블록 편집·스냅핑 UI 도입.
Limit UI 및 SliderCard 추가
src/page/PolicyDetail/components/LimitTab.tsx, src/page/PolicyDetail/components/SliderCard.tsx
LimitTab이 lineId 기반 데이터 페칭으로 변경, SliderCard 컴포넌트 추가하여 GB 입력·슬라이더·토글·서버 동기화 구현.
메인 화면 및 컴포넌트 수정
src/page/Main/**, src/page/Main/components/*
사용량 UsageData 조회 및 SharedPoolUsage 사용자 바인딩, FamilyMemberList/Card에 isUserOwner 전달, PieChart 크기 변경 등 UI·데이터 흐름 조정.
타입·유틸·상수 추가/변경
src/types/*, src/utils/*, src/constants/alarmMessages.ts
알림(Notification) 타입 및 NotificationResponse 추가, block/limit/shared usage 타입 추가, alarm 메시지·카테고리 맵 및 알림 유틸, 데이터 포맷·변환 유틸 추가.
UI/클래스 소소 변경
src/components/common/RangeSlider.tsx, src/page/Main/components/SharedPoolUsage.tsx, 기타 컴포넌트
RangeSlider 값 클램프, SharedPoolUsage overflow 방지 및 스타일 변경, 여러 컴포넌트의 prop·기본값 변경 소소 반영.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AlarmPage as Alarm Page
    participant ReactQuery as React Query
    participant NotificationAPI as Notification Service
    participant Server

    User->>AlarmPage: 페이지 진입
    AlarmPage->>ReactQuery: useInfiniteQuery 초기화
    ReactQuery->>NotificationAPI: getNotifications(page=0)
    NotificationAPI->>Server: GET /notifications
    Server-->>NotificationAPI: NotificationResponse
    NotificationAPI-->>ReactQuery: 데이터 반환
    ReactQuery-->>AlarmPage: 알림 표시

    User->>AlarmPage: 아래로 스크롤
    AlarmPage->>AlarmPage: IntersectionObserver 감지
    AlarmPage->>ReactQuery: fetchNextPage()
    ReactQuery->>NotificationAPI: getNotifications(page=1)
    NotificationAPI->>Server: GET /notifications?page=1
    Server-->>NotificationAPI: 다음 페이지
    NotificationAPI-->>ReactQuery: 병합
    ReactQuery-->>AlarmPage: 추가 항목 표시

    User->>AlarmPage: 항목 클릭
    AlarmPage->>ReactQuery: markAsRead(id)
    ReactQuery->>NotificationAPI: PATCH /notifications
    NotificationAPI->>Server: PATCH
    Server-->>NotificationAPI: 성공
    NotificationAPI-->>ReactQuery: 무효화/갱신
    ReactQuery-->>AlarmPage: 읽음 상태 반영
Loading
sequenceDiagram
    participant User
    participant PolicyPage as PolicyDetail Page
    participant ReactQuery as React Query
    participant BlockAPI as Block Service
    participant LimitAPI as Limit Service
    participant Server

    User->>PolicyPage: 구성원 선택 (lineId)
    PolicyPage->>ReactQuery: getImmediateBlock(lineId)
    ReactQuery->>BlockAPI: GET /immediate-blocks?lineId
    BlockAPI->>Server: GET
    Server-->>BlockAPI: blockEndAt
    BlockAPI-->>ReactQuery: 반환
    ReactQuery-->>PolicyPage: activeBlockEndTime 갱신

    PolicyPage->>ReactQuery: getLimits(lineId)
    ReactQuery->>LimitAPI: GET /policies/lines/limits?lineId
    LimitAPI->>Server: GET
    Server-->>LimitAPI: LineLimitResponse
    LimitAPI-->>ReactQuery: 반환
    ReactQuery-->>PolicyPage: SliderCard 렌더링

    User->>PolicyPage: 즉시 블록 적용
    PolicyPage->>ReactQuery: patchImmediateBlock(lineId, blockEndAt)
    ReactQuery->>BlockAPI: PATCH /immediate-blocks
    BlockAPI->>Server: PATCH
    Server-->>BlockAPI: 성공
    BlockAPI-->>ReactQuery: 무효화
    ReactQuery-->>PolicyPage: 배너·시간 업데이트

    User->>PolicyPage: 슬라이더 조정
    PolicyPage->>PolicyPage: 디바운스 지연
    PolicyPage->>ReactQuery: patchDailyLimit(limitPolicyId, value)
    ReactQuery->>LimitAPI: PATCH /policies/lines/days/limits
    LimitAPI->>Server: PATCH
    Server-->>LimitAPI: 성공
    LimitAPI-->>ReactQuery: 무효화/갱신
Loading
sequenceDiagram
    participant User
    participant BlockManager as BlockPolicyManager
    participant ReactQuery as React Query
    participant BlockAPI as Block Service
    participant Server

    User->>BlockManager: 마운트 (lineId)
    BlockManager->>ReactQuery: getRepeatBlockPolicies(lineId)
    ReactQuery->>BlockAPI: GET /repeat-blocks?lineId
    BlockAPI->>Server: GET
    Server-->>BlockAPI: RepeatBlockResponse[]
    BlockAPI-->>ReactQuery: 반환
    ReactQuery-->>BlockManager: 목록 렌더링

    User->>BlockManager: 정책 추가 -> 저장
    BlockManager->>ReactQuery: createRepeatBlockPolicy(data)
    ReactQuery->>BlockAPI: POST /repeat-blocks
    BlockAPI->>Server: POST
    Server-->>BlockAPI: 생성된 정책
    BlockAPI-->>ReactQuery: 무효화/갱신
    ReactQuery-->>BlockManager: 목록 갱신
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive PR 제목이 너무 일반적이고 모호하여 변경 사항의 핵심을 명확히 전달하지 못함. 정책 API 연동, 알림 기능 추가 등 주요 변경 사항을 구체적으로 명시하는 더 자세한 제목으로 수정하세요.
Linked Issues check ❓ Inconclusive 이슈 #45의 요구 사항 대부분(사용자 정보, 임계치 설정, 권한 설정 컴포넌트)이 구현되었으나, 권한 양도 컴포넌트가 미완료 상태임. 권한 양도 컴포넌트(transferOwner 기능)의 구현 완성도를 확인하고, 필요시 추가 작업을 완료하세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고 있으며, 작업 내용과 주의 사항이 포함되어 있음.
Out of Scope Changes check ✅ Passed 대부분의 변경 사항이 정책 페이지 API 연동 범위 내이지만, 사용로그 페이지 관련 작업은 아직 미완료 상태임.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/policy-api
📝 Coding Plan
  • Generate coding plan for human review comments

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: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/services/familyService.ts (1)

3-3: ⚠️ Potential issue | 🔴 Critical

SimpleMember 타입 import 누락으로 빌드가 실패합니다.

Line 52에서 SimpleMember를 사용하지만 Line 3 import에 포함되지 않아 타입체크가 깨집니다.

🔧 제안 수정안
-import type { FamilyApiResponse } from "@/types/FamilyMember";
+import type { FamilyApiResponse, SimpleMember } from "@/types/FamilyMember";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/services/familyService.ts` at line 3, The build fails because the
SimpleMember type is used in familyService (e.g., at the usage around line 52)
but not imported; update the import statement that currently reads import type {
FamilyApiResponse } from "@/types/FamilyMember"; to also import SimpleMember
(e.g., import type { FamilyApiResponse, SimpleMember } from
"@/types/FamilyMember";) so the SimpleMember type is available for the
functions/types in this module.
🧹 Nitpick comments (15)
src/page/Policy/components/Permisssion.tsx (3)

1-1: 파일명 오타: Permisssion.tsxPermission.tsx

파일명에 's'가 3개 있습니다. Permission.tsx로 수정하는 것을 권장합니다.

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

In `@src/page/Policy/components/Permisssion.tsx` at line 1, Rename the file named
Permisssion.tsx to Permission.tsx (fix the triple-'s' typo) and update any
imports referencing Permisssion.tsx to import from Permission.tsx instead
(search for references to "Permisssion" or the component name exported from that
file and update them accordingly) so the component and its module resolution
remain consistent.

32-33: 프로덕션 코드에서 디버그용 console.log 제거 필요

디버그용 console.log 문이 남아있습니다. 머지 전에 제거해주세요.

🧹 제안된 수정
-  console.log("permissionsData:", permissionsData);
-  console.log("memberPermissions:", permissionsData?.memberPermissions);
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/Policy/components/Permisssion.tsx` around lines 32 - 33, Remove the
debug console.log statements in the Permisssion.tsx component (the lines logging
"permissionsData:" and "memberPermissions:"); either delete those console.log
calls or replace them with the app's structured logger if runtime debug info is
required, ensuring no console.log remains in production code and keeping any
needed diagnostics behind a development-only flag.

72-82: 뮤테이션 에러 처리 개선 - 쿼리 무효화 및 사용자 피드백 강화 고려

성공 시 쿼리를 무효화하여 최신 데이터를 반영하고, 에러 시 더 구체적인 정보를 제공하는 것이 좋습니다.

♻️ 제안된 개선
+import { useQueryClient } from "@tanstack/react-query";
+
+// 컴포넌트 내부에서:
+const queryClient = useQueryClient();

 const { mutate: patchPermissions } = useMutation({
   mutationFn: (permissions: PatchPermissionRequest[]) =>
     permissionService.patchMemberPermissions(permissions),
   onSuccess: () => {
     setShowModal(false);
     show("권한이 적용되었습니다.");
+    queryClient.invalidateQueries({ queryKey: ["memberPermissions"] });
   },
-  onError: () => {
+  onError: (error) => {
+    console.error("Permission patch failed:", error);
     show("권한 적용에 실패했습니다. 다시 시도해주세요.", "error");
   },
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/Policy/components/Permisssion.tsx` around lines 72 - 82, The
mutation currently only hides the modal and shows a success/failure toast;
improve it by obtaining a QueryClient via useQueryClient() and, in the onSuccess
handler of patchPermissions, call queryClient.invalidateQueries(...) for the
permission-related query keys (e.g., the permissions/member or policy list
queries) so UI reflects updated data, keep setShowModal(false) and show("권한이
적용되었습니다."); and in onError accept the error param (e.g., (error) => ...) and
show a more specific message by passing error.message or the response error text
to show(..., "error") to surface the actual failure details; update imports and
references to useQueryClient, patchPermissions,
permissionService.patchMemberPermissions, setShowModal, and show accordingly.
src/page/Policy/Policy.tsx (1)

225-230: 취소 버튼 로직이 handleCloseModal과 중복됨

취소 버튼의 onClick 핸들러가 handleCloseModal 함수와 동일한 로직을 인라인으로 구현하고 있습니다.

♻️ 제안된 수정
 <button
-  onClick={() => {
-    setIsTransferModalOpen(false);
-    setSelectedTarget(null);
-    setConfirmText("");
-  }}
+  onClick={handleCloseModal}
   className="flex-1 py-3 bg-gray-100 text-gray-600 rounded-xl text-sm font-medium"
 >
   취소
 </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/Policy/Policy.tsx` around lines 225 - 230, The cancel button's
inline onClick duplicates the existing handleCloseModal logic; replace the
inline handler with a direct call to handleCloseModal (i.e.,
onClick={handleCloseModal}) and remove the repeated setIsTransferModalOpen,
setSelectedTarget, and setConfirmText calls to avoid duplication and keep single
source of truth for closing behavior.
src/utils/alarmUtils.ts (1)

8-11: typeundefined일 때 맵 접근 동작 확인

notification.value?.typeundefined일 경우 ALARM_MESSAGE_MAP[undefined]로 접근하게 됩니다. JavaScript에서는 이것이 undefined를 반환하고 nullish coalescing으로 기본값이 적용되므로 동작은 하지만, 타입 안전성을 높이기 위해 명시적 체크를 추가하는 것이 좋습니다.

♻️ 제안된 개선
 export const getAlarmMessage = (notification: Notification): string => {
   const type = notification.value?.type;
+  if (!type) return "새로운 알림이 있습니다.";
-  return ALARM_MESSAGE_MAP[type] ?? "새로운 알림이 있습니다.";
+  return ALARM_MESSAGE_MAP[type] ?? "새로운 알림이 있습니다.";
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/alarmUtils.ts` around lines 8 - 11, The getAlarmMessage function
currently reads notification.value?.type which can be undefined and then indexes
ALARM_MESSAGE_MAP with undefined; update getAlarmMessage to explicitly guard the
type (e.g., check that notification?.value?.type is a valid string and that
ALARM_MESSAGE_MAP has that key) before indexing ALARM_MESSAGE_MAP, and otherwise
return the default "새로운 알림이 있습니다."; reference getAlarmMessage,
notification.value?.type, and ALARM_MESSAGE_MAP when making the change.
src/utils/dataFormat.ts (1)

50-63: clampEndTime의 24시간 초과 분기는 현재 계산식에서 거의 도달하지 않습니다.

Line 56-61에서 diff가 계산되는 방식상(시간/분만 사용) Line 62 조건은 일반 입력 범위에서 실질적으로 발동되지 않습니다. 함수 의도와 실제 동작이 어긋나 있어 로직 단순화 또는 입력 모델 재정의가 필요합니다.

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

In `@src/utils/dataFormat.ts` around lines 50 - 63, The branch checking diff >
24*60 in clampEndTime is unreachable because diff is computed between 0 and
1439; remove that unreachable branch and replace with a clear cap: compute diff
as now, treat diff === 0 as a full 24h duration if that was intended (or simply
cap with Math.min(diff, 24*60)), then compute the clamped end total as
(startTotal + cappedDiff) % (24*60) and return the resulting hours/minutes;
update references to startTotal, diff and maxTotal accordingly to use cappedDiff
so behavior is correct and the unreachable >24*60 branch is eliminated.
src/page/PolicyDetail/components/SliderCard.tsx (1)

77-80: lineId가 undefined일 때 toggleMutate 호출 시 오류 발생

handleToggle에서 toggleMutate()를 호출하지만, lineId가 undefined면 서비스 호출이 실패합니다. Toggle 컴포넌트의 disabled={!lineId} 조건이 있지만, 방어적 코딩을 위해 핸들러에서도 체크하는 것이 좋습니다.

🛡️ 제안
   const handleToggle = (newEnabled: boolean) => {
+    if (!lineId) return;
     setOverride((prev) => ({ ...prev, enabled: newEnabled }));
     toggleMutate();
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/components/SliderCard.tsx` around lines 77 - 80,
handleToggle should defensively avoid calling toggleMutate when lineId is
undefined; update handleToggle (which currently calls setOverride and
toggleMutate) to check the lineId value first (or early return) before invoking
toggleMutate so the service call never runs with an undefined lineId; keep the
setOverride((prev) => ({ ...prev, enabled: newEnabled })) behavior but only call
toggleMutate() when lineId is truthy (or pass a validated payload that includes
lineId) to mirror the Toggle component's disabled={!lineId} guard.
src/page/PolicyDetail/components/Block/EditPanel.tsx (2)

138-144: 수정 시 정책이 항상 활성화됨

edit 모드에서 "정책 수정하기" 버튼을 누르면 enabled: true가 강제됩니다. 사용자가 정책을 비활성화 상태로 유지하면서 시간/요일만 수정하고 싶을 수 있습니다.

♻️ 제안: 기존 enabled 상태 유지
             <button
-              onClick={() => onConfirm({ ...draft, enabled: true })}
+              onClick={() => onConfirm(draft)}
               className="flex-1 py-2.5 rounded-full text-sm font-semibold text-white"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/components/Block/EditPanel.tsx` around lines 138 - 144,
현재 EditPanel의 "정책 수정하기" 버튼이 클릭되면 onConfirm에 항상 enabled: true를 강제해서 사용자가 비활성화 상태를
유지할 수 없습니다; 버튼의 onClick 핸들러를 수정해 강제로 enabled를 덮어쓰지 않도록 하고 기존 draft.enabled 값을
유지해서 onConfirm({ ...draft }) 또는 onConfirm({ ...draft, enabled: draft.enabled })를
호출하도록 변경하세요 (참조: onConfirm 함수, draft 객체, enabled 필드).

58-65: days 배열이 비어있을 때 유효성 검증 누락

toggleDay 로직은 정상이지만, 사용자가 모든 요일을 해제한 상태로 정책을 저장할 수 있습니다. 요일이 선택되지 않은 정책은 의미가 없으므로 최소 1개 이상의 요일이 선택되었는지 검증이 필요합니다.

♻️ 제안: 저장 전 유효성 검증 추가
         <button
-              onClick={() => onConfirm(draft)}
+              onClick={() => {
+                if (draft.days.length === 0) {
+                  // Toast 또는 경고 표시
+                  return;
+                }
+                onConfirm(draft);
+              }}
               className="flex-1 py-2.5 rounded-full text-sm font-semibold text-white"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/components/Block/EditPanel.tsx` around lines 58 - 65,
toggleDay correctly toggles days but there's no validation to prevent saving a
policy with an empty draft.days array; add a validation step in the save handler
(e.g., the function that commits the draft or the form submit handler) that
checks draft.days.length > 0 and blocks the save when it's zero, returning a
user-facing error message or disabling the Save action; ensure this ties into
the same state used by toggleDay (setDraft / draft) so the UI prevents or warns
about saving until at least one DayKey is selected.
src/page/PolicyDetail/components/Block/ScrollPicker.tsx (2)

35-42: 스크롤 중 과도한 onChange 호출

handleScroll이 매 스크롤 이벤트마다 호출되어 onChange가 빈번하게 트리거됩니다. 부모 컴포넌트에서 상태 업데이트가 잦아지면 성능 저하가 발생할 수 있습니다. 스크롤 완료 시에만 onChange를 호출하도록 handleScrollEnd에서만 처리하는 것을 고려해 보세요.

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

In `@src/page/PolicyDetail/components/Block/ScrollPicker.tsx` around lines 35 -
42, handleScroll currently computes the snapped value and calls onChange on
every scroll tick (using containerRef, ITEM_HEIGHT, values, selected), causing
excessive updates; remove the onChange call from handleScroll and instead
set/refresh the isScrolling flag and the intermediate index there, then invoke
onChange only from handleScrollEnd after snapping/scroll settling completes (use
the same snapped calculation logic in handleScrollEnd to determine the final
value and compare with selected before calling onChange) so state updates happen
once per finished scroll.

89-92: onScrollonScrollCapture 중복 사용

두 이벤트 핸들러가 모두 handleScroll을 호출합니다. capture phase와 bubble phase에서 각각 호출되므로 한 스크롤 이벤트당 두 번 처리될 수 있습니다. 둘 중 하나만 사용해도 충분합니다.

♻️ 제안
         onScroll={handleScroll}
-        onScrollCapture={handleScroll}
         onMouseUp={handleScrollEnd}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/components/Block/ScrollPicker.tsx` around lines 89 -
92, The component is calling handleScroll twice per user scroll because both
onScroll and onScrollCapture are attached; remove one of them (prefer keeping
onScroll unless you specifically need capture-phase handling) from the
ScrollPicker component props so each scroll triggers handleScroll once, leaving
onMouseUp/onTouchEnd with handleScrollEnd unchanged; verify no logic relies on
capture-phase behavior and run related UI interactions/tests after the change.
src/page/Alarm.tsx (1)

166-172: 클라이언트 측 카테고리 필터링은 페이지네이션과 함께 사용 시 UX 문제 발생

서버에서 모든 알림을 가져온 후 클라이언트에서 필터링하므로, 특정 카테고리의 알림이 적은 경우 빈 목록이 표시되지만 더 많은 페이지가 있을 수 있습니다. 사용자가 카테고리를 선택했을 때 해당 카테고리 알림만 불러오도록 서버 측 필터링을 고려하세요.

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

In `@src/page/Alarm.tsx` around lines 166 - 172, filteredNotifications is computed
client-side from allNotifications using selectedCategory and getAlarmCategory,
which causes empty pages when paginating; instead, update the notification
fetching logic (the function that loads notifications / useEffect or
data-fetcher that currently supplies allNotifications) to accept a category
parameter, call the server API with selectedCategory when it changes, and
replace allNotifications with server-filtered results; also ensure pagination
state (page/offset) is reset when selectedCategory changes so the first page of
category results is shown.
src/page/Policy/components/DataThresholdSlider.tsx (1)

41-49: 렌더링 중 상태 업데이트 패턴

PolicyDetail.tsx와 동일한 패턴입니다. props 변경 시 override를 리셋하는 로직은 동작하지만, useEffect가 더 명확합니다.

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

In `@src/page/Policy/components/DataThresholdSlider.tsx` around lines 41 - 49, The
current render-time state updates in DataThresholdSlider (comparing
lineThreshold vs prevLineThreshold and sharedPoolThreshold vs
prevSharedPoolThreshold and calling setPrevLineThreshold, setIndividualOverride,
setPrevSharedPoolThreshold, setFamilyOverride) should be moved into a useEffect
so updates run after render; implement a useEffect that depends on
[lineThreshold, sharedPoolThreshold] and inside it compare to
prevLineThreshold/prevSharedPoolThreshold and call setPrevLineThreshold,
setIndividualOverride (clearing overrides) and setPrevSharedPoolThreshold,
setFamilyOverride as needed to reset overrides when props change.
src/page/PolicyDetail/PolicyDetail.tsx (2)

131-137: 렌더링 중 setSelectedMember 호출은 불필요한 리렌더링을 유발할 수 있습니다.

Lines 88-100과 동일한 패턴입니다. useEffect를 사용하면 렌더링 로직과 부수 효과를 명확히 분리할 수 있습니다.

♻️ useEffect 사용 제안
-  const [prevFamilyMembers, setPrevFamilyMembers] = useState(familyMembers);
-  if (familyMembers !== prevFamilyMembers && familyMembers.length > 0) {
-    setPrevFamilyMembers(familyMembers);
-    const currentUser = familyMembers.find((m) => m.lineId === lineId);
-    setSelectedMember(currentUser || familyMembers[0]);
-  }
+  useEffect(() => {
+    if (familyMembers.length > 0 && !selectedMember) {
+      const currentUser = familyMembers.find((m) => m.lineId === lineId);
+      setSelectedMember(currentUser || familyMembers[0]);
+    }
+  }, [familyMembers, lineId, selectedMember]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/PolicyDetail.tsx` around lines 131 - 137, The block
that calls setPrevFamilyMembers and setSelectedMember during render should be
moved into a useEffect to avoid unnecessary re-renders; wrap the conditional
logic that checks familyMembers !== prevFamilyMembers and familyMembers.length >
0 into a useEffect with dependencies [familyMembers, lineId] (or [familyMembers]
and include lineId usage inside) and perform setPrevFamilyMembers(familyMembers)
and setSelectedMember(currentUser || familyMembers[0]) there, referencing the
same symbols prevFamilyMembers, setPrevFamilyMembers, setSelectedMember,
familyMembers, and lineId so the effect runs only when inputs change rather than
during render.

88-100: 렌더링 중 상태 비교 패턴은 동작하지만, useEffect가 더 명확합니다.

렌더링 중 prevImmediateBlockData를 비교하여 activeBlockEndTime을 동기화하는 패턴은 React 18+에서 지원되지만, 일반적인 React 패턴이 아닙니다. useEffect를 사용하면 의도가 더 명확해집니다.

♻️ useEffect 사용 제안
-  const [prevImmediateBlockData, setPrevImmediateBlockData] =
-    useState(immediateBlockData);
-  if (immediateBlockData !== prevImmediateBlockData) {
-    setPrevImmediateBlockData(immediateBlockData);
-    if (
-      immediateBlockData?.blockEndAt &&
-      new Date(immediateBlockData.blockEndAt) > new Date()
-    ) {
-      setActiveBlockEndTime(new Date(immediateBlockData.blockEndAt));
-    } else {
-      setActiveBlockEndTime(null);
-    }
-  }
+  useEffect(() => {
+    if (
+      immediateBlockData?.blockEndAt &&
+      new Date(immediateBlockData.blockEndAt) > new Date()
+    ) {
+      setActiveBlockEndTime(new Date(immediateBlockData.blockEndAt));
+    } else {
+      setActiveBlockEndTime(null);
+    }
+  }, [immediateBlockData]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/PolicyDetail.tsx` around lines 88 - 100, Replace the
render-time state comparison with a useEffect that watches immediateBlockData:
remove the current logic that compares immediateBlockData to
prevImmediateBlockData and updates prevImmediateBlockData and activeBlockEndTime
during render, and instead implement a useEffect that depends on
[immediateBlockData] which calls setPrevImmediateBlockData(immediateBlockData)
and sets setActiveBlockEndTime(new Date(immediateBlockData.blockEndAt)) if
immediateBlockData?.blockEndAt is in the future, otherwise sets null; keep using
the same state variables prevImmediateBlockData, setPrevImmediateBlockData,
immediateBlockData, and setActiveBlockEndTime to locate where to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/services/blockService.ts`:
- Around line 170-184: There are two identical keys named getImmediateBlock in
the exported service object causing a TS1117 duplicate-definition error; locate
the duplicate getImmediateBlock definition (the one that conflicts with the
other definition near the other blockService methods) and remove it so only a
single getImmediateBlock remains, leaving patchImmediateBlock intact; ensure the
remaining getImmediateBlock uses apiClient.get<{ lineId: number; blockEndAt:
string }>(...) and that no other methods accidentally share the same name.
- Around line 155-163: The two methods updateImmediateBlock and
patchImmediateBlock both call PATCH /policies/lines/immediate-block and are
redundant; remove updateImmediateBlock and replace all call sites (e.g.,
Admin-related components) to use patchImmediateBlock (which already has the
correct return type). Ensure imports/exports are updated so only
patchImmediateBlock is exported from blockService and update any references to
pass the same parameters (lineId and blockEndAt) and handle the returned type
the same way.
- Line 1: BlockPolicyManager.tsx imports RepeatBlockDay and RepeatBlockPolicy
from blockService.ts but those exports are incorrect: RepeatBlockDay is defined
in "@/types/block" and RepeatBlockPolicy does not exist (should use
RepeatBlockResponse). Update blockService.ts to re-export or correct the
imports: export or forward-export RepeatBlockDay from "@/types/block" and
replace any RepeatBlockPolicy exports/usages with RepeatBlockResponse (and
update BlockPolicyManager.tsx imports to import RepeatBlockDay from
"@/types/block" and RepeatBlockResponse from the corrected blockService.ts
export). Ensure identifiers RepeatBlockDay, RepeatBlockPolicy, and
RepeatBlockResponse are consistently referenced so TypeScript compiles.

In `@src/api/services/notificationService.ts`:
- Around line 27-28: The markAsRead method calls apiClient.patch<Notification>
but the Notification type is not imported, causing a TypeScript error; add an
import for the Notification type at the top of notificationService.ts (the same
module where Notification is declared) so apiClient.patch<Notification>
resolves, and ensure the import name matches the Notification type used in
markAsRead.

In `@src/api/services/sharedPoolService.ts`:
- Around line 64-65: getUsageData currently returns raw byte values while
getMainRemainingAmount and getMySharedPool convert bytes to GB; update
getUsageData (the apiClient.get<UsageData>("/shared-pools/usage/monthly-total")
call) to await the response, convert response.data.sharedPoolTotalData with
bytesToGb, and map response.data.membersUsageList turning each
member.monthlySharedPoolUsage into bytesToGb(...) (preserving other member
fields) so the returned shape matches UsageData but with GB values for
sharedPoolTotalData and monthlySharedPoolUsage.

In `@src/page/Alarm.tsx`:
- Around line 136-149: The onSuccess handlers for the useMutation hooks
markAsRead and markAllAsRead currently only invalidate the ["notifications"]
query, so the header's unread badge (queried under ["unreadCount"]) isn't
refreshed immediately; update both onSuccess callbacks in the useMutation
definitions (for mutate: markAsRead and mutate: markAllAsRead) to call
queryClient.invalidateQueries for both ["notifications"] and ["unreadCount"]
(ensure you reference the existing queryClient.invalidateQueries call pattern
used now).

In `@src/page/Main/components/SharedPoolUsage.tsx`:
- Around line 39-50: The width calculation can receive NaN/Infinity/negative
values when user.percentage is invalid; inside the SharedPoolUsage component fix
the rendering by normalizing/clamping user.percentage to a finite 0–100 range
before using it in leftOffset and width calculations (use the same normalized
value for both), and fall back to 0 when the value is non-finite; update
references to leftOffset, users.slice(...).reduce(...), and the style.width
expression so they use the clamped percentage to guarantee the computed left and
width are always valid.

In `@src/page/Main/MainPage.tsx`:
- Around line 48-54: The calculation in usageUsers (inside the
usageData?.membersUsageList.map) can divide by zero or undefined via
usageData.sharedPoolTotalData; update the mapping to guard the denominator: read
const denom = usageData.sharedPoolTotalData and set percentage = denom > 0 ?
Math.round((member.monthlySharedPoolUsage / denom) * 100) : 0 (or another safe
fallback), keep COLOR selection as COLORS[index % COLORS.length], and ensure you
reference usageUsers, usageData, membersUsageList, sharedPoolTotalData, and
COLORS when applying the change.

In `@src/page/Policy/components/DataThresholdSlider.tsx`:
- Around line 90-99: handleFamilyToggle updates local state but does not send
the enabled flag to the server; modify the call to
thresholdService.patchSharedPoolThreshold so it sends both the threshold value
and the toggle state (the isThresholdActive field) like patchLineThreshold does
for individuals. In other words, when invoking patchSharedPoolThreshold from
handleFamilyToggle, include the enabled value (e.g., send an object containing
newFamilyThreshold: familyBytes and isThresholdActive: enabled or the equivalent
shape expected by SharedPoolThreshold) so the server persists the toggle.
- Around line 51-52: The conversion helpers in DataThresholdSlider are
inconsistent: formatData returns mixed units (GB or MB) but toBytes(gb) assumes
the input is always GB, causing large errors for <1GB values; update the logic
so toGB uses the correct unit converter (replace toGB =
(bytes)=>formatData(bytes) with a bytesToGb/bytesToMb-aware approach) or
implement a reciprocal of formatData that returns both numeric value and unit,
then change toBytes to convert based on that unit (use existing bytesToGb and
bytesToMb utilities and adjust the functions named toGB and toBytes accordingly,
and ensure any usage of formatData in DataThresholdSlider maps to the correct
reverse conversion).

In `@src/page/Policy/Policy.tsx`:
- Around line 58-70: Add error handling to the transferOwner mutation by
supplying an onError handler to the useMutation call that shows user feedback
(e.g., error toast or set an error state) when familyService.transferOwner
fails; also wrap the onSuccess logic that calls userService.getMyInfo() in a
try/catch so failures there are caught and surfaced (log or notify) instead of
propagating — update the useMutation block around transferOwner and modify the
onSuccess handler to try { const { data } = await userService.getMyInfo();
setUserInfo(data); } catch (err) { /* notify/log */ } and add onError: (err) =>
{ /* notify/log and keep modal state consistent */ }.

In `@src/page/PolicyDetail/components/Block/BlockItem.tsx`:
- Around line 22-25: The clickable div in BlockItem (the element using
onClick={() => setOpen(v => !v)}) lacks keyboard accessibility; update that
element to behave like a button by adding role="button", tabIndex={0}, and an
onKeyDown handler that listens for Enter and Space keys and calls setOpen(v =>
!v) (and prevents default/propagation for Space to avoid scrolling); ensure the
handler references the same setOpen used by the onClick so keyboard and mouse
toggles remain consistent.

In `@src/page/PolicyDetail/components/Blockpolicymanager.tsx`:
- Around line 64-73: The newDraft function uses lineId! even though lineId is
optional, so calling newDraft() when lineId is undefined produces invalid
policies; fix by guarding UI and creation: update the Add button and any
showAddPanel toggle to be disabled/blocked when lineId is falsy (e.g., change
the add button's enabled condition to require lineId), and/or wrap the panel
rendering so showAddPanel can only be true if lineId is present; alternatively
make newDraft() defensive (return null or throw if lineId is undefined) and
ensure the caller checks before creating a BlockPolicy. Ensure you update
references to newDraft, showAddPanel, and the Add button click handler to
enforce the guard.
- Around line 54-62: The onSuccess in the toggleBlock useMutation always
invalidates ["repeatBlocks"] but other mutations use ["repeatBlocks", lineId],
causing stale data; update the onSuccess signature to accept the mutation
variables (e.g., onSuccess: (_data, variables) => ...) and call
queryClient.invalidateQueries({ queryKey: ["repeatBlocks", variables.id] }) so
the same per-policy cache key used elsewhere (and by
blockService.updateRepeatBlockPolicy / mutate(toggleBlock)) is invalidated.

In `@src/page/PolicyDetail/components/Immediateblockpolicy.tsx`:
- Around line 283-285: The end-time display always uses previewEndTime but
should show the actual block end when the block is active; update the rendering
in Immediateblockpolicy (the paragraph using calcEndTimeDisplay(previewEndTime))
to choose the value conditionally: if the block is active (use data?.blockEndAt
or a boolean like data?.isBlocked), call calcEndTimeDisplay(data?.blockEndAt)
else call calcEndTimeDisplay(previewEndTime); handle null/undefined safely so
the component doesn't break when data?.blockEndAt is absent.
- Around line 90-98: handleToggle uses calcBlockEndAt(selectedMinutes) (which
returns a local "YYYY-MM-DDTHH:mm:ss" string) when enabling but uses
toISOString().slice(0,19) (UTC) when disabling; change the disable branch to
produce the same local-format timestamp as calcBlockEndAt (e.g., call
calcBlockEndAt(0) or use the same local formatter used by calcBlockEndAt) and
then pass that result to patchBlock so patchBlock, handleToggle and
calcBlockEndAt all use a consistent local "YYYY-MM-DDTHH:mm:ss" format.

In `@src/page/PolicyDetail/components/LimitTab.tsx`:
- Around line 6-10: The component renders SliderCard without guarding for
loading/error from useQuery, causing undefined limitPolicyId (data?.lineLimitId)
and unsafe non-null assertions in SliderCard mutations; update the useQuery call
to destructure isLoading/isError (from the query that uses
limitService.getLimits) and only render SliderCard when data is present (e.g.,
if (isLoading) show a loader; if (isError || !data) show an error/placeholder),
pass the real data values (avoid masking with fallbacks like data?.maxSharedData
?? 0) and ensure SliderCard’s interactions handle undefined limitPolicyId by
validating before calling its mutation (remove or avoid using non-null assertion
on limitPolicyId in SliderCard and check limitPolicyId exists before invoking
mutations).

In `@src/page/PolicyDetail/components/SliderCard.tsx`:
- Around line 51-52: formatData currently returns MB for values <1GB which
breaks byte reconversion (gb = formatData(bytes) is treated as GB); update
formatData (or add a new helper like formatDataInGB) so it always returns a
numeric value expressed in GB (e.g., 0.5 for 500MB) and then use that consistent
GB value for conversions (e.g., handleChange(gb * 1e9)); update both uses (gb
and maxGb in SliderCard and the other occurrence around lines 121-131) to rely
on the GB-normalized return so byte math is correct.

In `@src/page/PolicyDetail/PolicyDetail.tsx`:
- Around line 108-120: The handleBlockRelease function lacks error handling
around the async API call; wrap the call to
blockService.patchImmediateBlock(selectedMember.lineId, nowStr) in a try/catch,
only call setActiveBlockEndTime(null), queryClient.invalidateQueries({ queryKey:
["immediateBlock", selectedMember.lineId] }) and show("차단이 해제되었습니다.") after the
patch succeeds, and in the catch branch show an error message (e.g., show("차단
해제에 실패했습니다.") or the error message) and avoid mutating activeBlockEndTime so the
UI remains consistent; keep the existing selectedMember?.lineId guard and
preserve nowStr generation and use of patchImmediateBlock for locating the
change.

In `@src/utils/dataFormat.ts`:
- Around line 70-84: The toBlockPolicy function lacks input validation for
res.days and DAY_MAP lookups; add checks in toBlockPolicy to (1) ensure res.days
exists and is non-empty before accessing res.days[0] and throw or return a clear
error if not, (2) validate startAt/endAt parse results (startHour/startMin and
endHour/endMin) are numbers and throw/handle parse failures, and (3) when
mapping days via DAY_MAP inside res.days.map(...), filter out or handle entries
where DAY_MAP[d.dayOfWeek] is undefined (or throw a descriptive error) so days
never contain undefined; use the function name toBlockPolicy and types
RepeatBlockResponse and DAY_MAP to locate and update the logic accordingly.

---

Outside diff comments:
In `@src/api/services/familyService.ts`:
- Line 3: The build fails because the SimpleMember type is used in familyService
(e.g., at the usage around line 52) but not imported; update the import
statement that currently reads import type { FamilyApiResponse } from
"@/types/FamilyMember"; to also import SimpleMember (e.g., import type {
FamilyApiResponse, SimpleMember } from "@/types/FamilyMember";) so the
SimpleMember type is available for the functions/types in this module.

---

Nitpick comments:
In `@src/page/Alarm.tsx`:
- Around line 166-172: filteredNotifications is computed client-side from
allNotifications using selectedCategory and getAlarmCategory, which causes empty
pages when paginating; instead, update the notification fetching logic (the
function that loads notifications / useEffect or data-fetcher that currently
supplies allNotifications) to accept a category parameter, call the server API
with selectedCategory when it changes, and replace allNotifications with
server-filtered results; also ensure pagination state (page/offset) is reset
when selectedCategory changes so the first page of category results is shown.

In `@src/page/Policy/components/DataThresholdSlider.tsx`:
- Around line 41-49: The current render-time state updates in
DataThresholdSlider (comparing lineThreshold vs prevLineThreshold and
sharedPoolThreshold vs prevSharedPoolThreshold and calling setPrevLineThreshold,
setIndividualOverride, setPrevSharedPoolThreshold, setFamilyOverride) should be
moved into a useEffect so updates run after render; implement a useEffect that
depends on [lineThreshold, sharedPoolThreshold] and inside it compare to
prevLineThreshold/prevSharedPoolThreshold and call setPrevLineThreshold,
setIndividualOverride (clearing overrides) and setPrevSharedPoolThreshold,
setFamilyOverride as needed to reset overrides when props change.

In `@src/page/Policy/components/Permisssion.tsx`:
- Line 1: Rename the file named Permisssion.tsx to Permission.tsx (fix the
triple-'s' typo) and update any imports referencing Permisssion.tsx to import
from Permission.tsx instead (search for references to "Permisssion" or the
component name exported from that file and update them accordingly) so the
component and its module resolution remain consistent.
- Around line 32-33: Remove the debug console.log statements in the
Permisssion.tsx component (the lines logging "permissionsData:" and
"memberPermissions:"); either delete those console.log calls or replace them
with the app's structured logger if runtime debug info is required, ensuring no
console.log remains in production code and keeping any needed diagnostics behind
a development-only flag.
- Around line 72-82: The mutation currently only hides the modal and shows a
success/failure toast; improve it by obtaining a QueryClient via
useQueryClient() and, in the onSuccess handler of patchPermissions, call
queryClient.invalidateQueries(...) for the permission-related query keys (e.g.,
the permissions/member or policy list queries) so UI reflects updated data, keep
setShowModal(false) and show("권한이 적용되었습니다."); and in onError accept the error
param (e.g., (error) => ...) and show a more specific message by passing
error.message or the response error text to show(..., "error") to surface the
actual failure details; update imports and references to useQueryClient,
patchPermissions, permissionService.patchMemberPermissions, setShowModal, and
show accordingly.

In `@src/page/Policy/Policy.tsx`:
- Around line 225-230: The cancel button's inline onClick duplicates the
existing handleCloseModal logic; replace the inline handler with a direct call
to handleCloseModal (i.e., onClick={handleCloseModal}) and remove the repeated
setIsTransferModalOpen, setSelectedTarget, and setConfirmText calls to avoid
duplication and keep single source of truth for closing behavior.

In `@src/page/PolicyDetail/components/Block/EditPanel.tsx`:
- Around line 138-144: 현재 EditPanel의 "정책 수정하기" 버튼이 클릭되면 onConfirm에 항상 enabled:
true를 강제해서 사용자가 비활성화 상태를 유지할 수 없습니다; 버튼의 onClick 핸들러를 수정해 강제로 enabled를 덮어쓰지 않도록
하고 기존 draft.enabled 값을 유지해서 onConfirm({ ...draft }) 또는 onConfirm({ ...draft,
enabled: draft.enabled })를 호출하도록 변경하세요 (참조: onConfirm 함수, draft 객체, enabled 필드).
- Around line 58-65: toggleDay correctly toggles days but there's no validation
to prevent saving a policy with an empty draft.days array; add a validation step
in the save handler (e.g., the function that commits the draft or the form
submit handler) that checks draft.days.length > 0 and blocks the save when it's
zero, returning a user-facing error message or disabling the Save action; ensure
this ties into the same state used by toggleDay (setDraft / draft) so the UI
prevents or warns about saving until at least one DayKey is selected.

In `@src/page/PolicyDetail/components/Block/ScrollPicker.tsx`:
- Around line 35-42: handleScroll currently computes the snapped value and calls
onChange on every scroll tick (using containerRef, ITEM_HEIGHT, values,
selected), causing excessive updates; remove the onChange call from handleScroll
and instead set/refresh the isScrolling flag and the intermediate index there,
then invoke onChange only from handleScrollEnd after snapping/scroll settling
completes (use the same snapped calculation logic in handleScrollEnd to
determine the final value and compare with selected before calling onChange) so
state updates happen once per finished scroll.
- Around line 89-92: The component is calling handleScroll twice per user scroll
because both onScroll and onScrollCapture are attached; remove one of them
(prefer keeping onScroll unless you specifically need capture-phase handling)
from the ScrollPicker component props so each scroll triggers handleScroll once,
leaving onMouseUp/onTouchEnd with handleScrollEnd unchanged; verify no logic
relies on capture-phase behavior and run related UI interactions/tests after the
change.

In `@src/page/PolicyDetail/components/SliderCard.tsx`:
- Around line 77-80: handleToggle should defensively avoid calling toggleMutate
when lineId is undefined; update handleToggle (which currently calls setOverride
and toggleMutate) to check the lineId value first (or early return) before
invoking toggleMutate so the service call never runs with an undefined lineId;
keep the setOverride((prev) => ({ ...prev, enabled: newEnabled })) behavior but
only call toggleMutate() when lineId is truthy (or pass a validated payload that
includes lineId) to mirror the Toggle component's disabled={!lineId} guard.

In `@src/page/PolicyDetail/PolicyDetail.tsx`:
- Around line 131-137: The block that calls setPrevFamilyMembers and
setSelectedMember during render should be moved into a useEffect to avoid
unnecessary re-renders; wrap the conditional logic that checks familyMembers !==
prevFamilyMembers and familyMembers.length > 0 into a useEffect with
dependencies [familyMembers, lineId] (or [familyMembers] and include lineId
usage inside) and perform setPrevFamilyMembers(familyMembers) and
setSelectedMember(currentUser || familyMembers[0]) there, referencing the same
symbols prevFamilyMembers, setPrevFamilyMembers, setSelectedMember,
familyMembers, and lineId so the effect runs only when inputs change rather than
during render.
- Around line 88-100: Replace the render-time state comparison with a useEffect
that watches immediateBlockData: remove the current logic that compares
immediateBlockData to prevImmediateBlockData and updates prevImmediateBlockData
and activeBlockEndTime during render, and instead implement a useEffect that
depends on [immediateBlockData] which calls
setPrevImmediateBlockData(immediateBlockData) and sets setActiveBlockEndTime(new
Date(immediateBlockData.blockEndAt)) if immediateBlockData?.blockEndAt is in the
future, otherwise sets null; keep using the same state variables
prevImmediateBlockData, setPrevImmediateBlockData, immediateBlockData, and
setActiveBlockEndTime to locate where to change.

In `@src/utils/alarmUtils.ts`:
- Around line 8-11: The getAlarmMessage function currently reads
notification.value?.type which can be undefined and then indexes
ALARM_MESSAGE_MAP with undefined; update getAlarmMessage to explicitly guard the
type (e.g., check that notification?.value?.type is a valid string and that
ALARM_MESSAGE_MAP has that key) before indexing ALARM_MESSAGE_MAP, and otherwise
return the default "새로운 알림이 있습니다."; reference getAlarmMessage,
notification.value?.type, and ALARM_MESSAGE_MAP when making the change.

In `@src/utils/dataFormat.ts`:
- Around line 50-63: The branch checking diff > 24*60 in clampEndTime is
unreachable because diff is computed between 0 and 1439; remove that unreachable
branch and replace with a clear cap: compute diff as now, treat diff === 0 as a
full 24h duration if that was intended (or simply cap with Math.min(diff,
24*60)), then compute the clamped end total as (startTotal + cappedDiff) %
(24*60) and return the resulting hours/minutes; update references to startTotal,
diff and maxTotal accordingly to use cappedDiff so behavior is correct and the
unreachable >24*60 branch is eliminated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a8de89b-81af-4b61-8980-89ed450b9662

📥 Commits

Reviewing files that changed from the base of the PR and between ff754c0 and a362b8b.

📒 Files selected for processing (39)
  • src/api/index.ts
  • src/api/services/blockService.ts
  • src/api/services/familyService.ts
  • src/api/services/limitService.ts
  • src/api/services/notificationService.ts
  • src/api/services/sharedPoolService.ts
  • src/components/Header.tsx
  • src/components/common/RangeSlider.tsx
  • src/constants/alarmMessages.ts
  • src/data/alarmDummyData.ts
  • src/page/Alarm.tsx
  • src/page/Main/MainPage.tsx
  • src/page/Main/components/FamilyMemberList.tsx
  • src/page/Main/components/MemberCard.tsx
  • src/page/Main/components/PieChart.tsx
  • src/page/Main/components/SharedPoolUsage.tsx
  • src/page/Policy/Policy.tsx
  • src/page/Policy/components/DataRemainingCard.tsx
  • src/page/Policy/components/DataThresholdSlider.tsx
  • src/page/Policy/components/Permisssion.tsx
  • src/page/PolicyDetail/PolicyDetail.tsx
  • src/page/PolicyDetail/components/Block/BlockItem.tsx
  • src/page/PolicyDetail/components/Block/EditPanel.tsx
  • src/page/PolicyDetail/components/Block/ScrollPicker.tsx
  • src/page/PolicyDetail/components/Block/TimePicker.tsx
  • src/page/PolicyDetail/components/BlockTab.tsx
  • src/page/PolicyDetail/components/Blockpolicymanager.tsx
  • src/page/PolicyDetail/components/Immediateblockpolicy.tsx
  • src/page/PolicyDetail/components/LimitTab.tsx
  • src/page/PolicyDetail/components/SliderCard.tsx
  • src/store/alarmStore.ts
  • src/types/FamilyMember.ts
  • src/types/SharedData.ts
  • src/types/alarm.ts
  • src/types/block.ts
  • src/types/limit.ts
  • src/types/permission.ts
  • src/utils/alarmUtils.ts
  • src/utils/dataFormat.ts
💤 Files with no reviewable changes (2)
  • src/store/alarmStore.ts
  • src/data/alarmDummyData.ts

Comment thread src/api/services/blockService.ts
Comment on lines 155 to +163
// 즉시 차단 토글/수정
updateImmediateBlock: (lineId: number, blockEndAt: string) =>
apiClient.patch("/policies/lines/immediate-block", { blockEndAt }, {
params: { lineId },
}),
apiClient.patch(
"/policies/lines/immediate-block",
{ blockEndAt },
{
params: { lineId },
},
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find usages of both methods to understand if both are needed
rg -n "updateImmediateBlock|patchImmediateBlock" --type=ts -C2

Repository: pooli-dev/pooli-fe

Length of output: 5784


🏁 Script executed:

sed -n '154,184p' src/api/services/blockService.ts | cat -n

Repository: pooli-dev/pooli-fe

Length of output: 1097


updateImmediateBlockpatchImmediateBlock 통합 필요

두 메서드가 동일한 엔드포인트(PATCH /policies/lines/immediate-block)를 호출하며 기능상 차이가 없습니다. patchImmediateBlock에만 반환 타입 지정이 있으므로, 이 메서드를 유지하고 updateImmediateBlock 사용 부분(Admin 관련 컴포넌트)을 통합해야 합니다.

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

In `@src/api/services/blockService.ts` around lines 155 - 163, The two methods
updateImmediateBlock and patchImmediateBlock both call PATCH
/policies/lines/immediate-block and are redundant; remove updateImmediateBlock
and replace all call sites (e.g., Admin-related components) to use
patchImmediateBlock (which already has the correct return type). Ensure
imports/exports are updated so only patchImmediateBlock is exported from
blockService and update any references to pass the same parameters (lineId and
blockEndAt) and handle the returned type the same way.

Comment on lines +170 to +184

// 즉시 차단 조회
getImmediateBlock: (lineId: number) =>
apiClient.get<{ lineId: number; blockEndAt: string }>(
"/policies/lines/immediate-block",
{ params: { lineId } },
),

// 즉시 차단 적용
patchImmediateBlock: (lineId: number, blockEndAt: string) =>
apiClient.patch<{ lineId: number; blockEndAt: string }>(
"/policies/lines/immediate-block",
{ blockEndAt },
{ params: { lineId } },
),
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 | 🔴 Critical

getImmediateBlock가 중복 정의되어 빌드 실패가 발생합니다.

getImmediateBlock이 Lines 147-153과 Lines 171-176에서 두 번 정의되어 있습니다. 이로 인해 TypeScript 컴파일 에러(TS1117)가 발생합니다.

🐛 중복 정의 제거
-  // 즉시 차단 조회
-  getImmediateBlock: (lineId: number) =>
-    apiClient.get<{ lineId: number; blockEndAt: string }>(
-      "/policies/lines/immediate-block",
-      { params: { lineId } },
-    ),
-
   // 즉시 차단 적용
   patchImmediateBlock: (lineId: number, blockEndAt: string) =>
     apiClient.patch<{ lineId: number; blockEndAt: string }>(
       "/policies/lines/immediate-block",
       { blockEndAt },
       { params: { lineId } },
     ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 즉시 차단 조회
getImmediateBlock: (lineId: number) =>
apiClient.get<{ lineId: number; blockEndAt: string }>(
"/policies/lines/immediate-block",
{ params: { lineId } },
),
// 즉시 차단 적용
patchImmediateBlock: (lineId: number, blockEndAt: string) =>
apiClient.patch<{ lineId: number; blockEndAt: string }>(
"/policies/lines/immediate-block",
{ blockEndAt },
{ params: { lineId } },
),
// 즉시 차단 적용
patchImmediateBlock: (lineId: number, blockEndAt: string) =>
apiClient.patch<{ lineId: number; blockEndAt: string }>(
"/policies/lines/immediate-block",
{ blockEndAt },
{ params: { lineId } },
),
🧰 Tools
🪛 GitHub Actions: CI

[error] 172-172: TS1117: An object literal cannot have multiple properties with the same name.

🪛 GitHub Check: build

[failure] 172-172:
An object literal cannot have multiple properties with the same name.

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

In `@src/api/services/blockService.ts` around lines 170 - 184, There are two
identical keys named getImmediateBlock in the exported service object causing a
TS1117 duplicate-definition error; locate the duplicate getImmediateBlock
definition (the one that conflicts with the other definition near the other
blockService methods) and remove it so only a single getImmediateBlock remains,
leaving patchImmediateBlock intact; ensure the remaining getImmediateBlock uses
apiClient.get<{ lineId: number; blockEndAt: string }>(...) and that no other
methods accidentally share the same name.

Comment on lines +27 to +28
markAsRead: (alarmHistoryId: number) =>
apiClient.patch<Notification>("/notifications", { alarmHistoryId }),
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 | 🔴 Critical

Notification 타입이 import되지 않음

markAsRead 메서드에서 apiClient.patch<Notification>을 사용하지만, Notification 타입이 import 목록에 없습니다. 이는 TypeScript 컴파일 오류를 발생시킵니다.

🐛 수정 제안
-import type { AlarmCode, NotificationResponse } from "@/types/alarm";
+import type { AlarmCode, Notification, NotificationResponse } from "@/types/alarm";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/services/notificationService.ts` around lines 27 - 28, The markAsRead
method calls apiClient.patch<Notification> but the Notification type is not
imported, causing a TypeScript error; add an import for the Notification type at
the top of notificationService.ts (the same module where Notification is
declared) so apiClient.patch<Notification> resolves, and ensure the import name
matches the Notification type used in markAsRead.

Comment on lines +64 to +65
getUsageData: () =>
apiClient.get<UsageData>("/shared-pools/usage/monthly-total"),
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# UsageData 사용처에서 단위 변환이 필요한지 확인

# UsageData 타입 정의 확인
echo "=== UsageData type definition ==="
rg -n "type UsageData" --type=ts -A 10

# getUsageData 사용처에서 변환 로직이 있는지 확인
echo -e "\n=== getUsageData usage ==="
rg -n "getUsageData" --type=ts -A 5 -B 2

Repository: pooli-dev/pooli-fe

Length of output: 1086


🏁 Script executed:

#!/bin/bash

# Check MemberUsage type definition
echo "=== MemberUsage type definition ==="
rg -n "type MemberUsage" --type=ts -A 10

# Check other methods in sharedPoolService
echo -e "\n=== getMainRemainingAmount method ==="
rg -n "getMainRemainingAmount" --type=ts -A 5 src/api/services/

# Check getMySharedPool method
echo -e "\n=== getMySharedPool method ==="
rg -n "getMySharedPool" --type=ts -A 5 src/api/services/

# Check bytesToGb usage
echo -e "\n=== bytesToGb usage ==="
rg -n "bytesToGb" --type=ts -B 2 -A 2 src/api/services/

# Check how usageData is rendered in MainPage
echo -e "\n=== usageData usage in MainPage ==="
rg -n "usageData" --type=tsx src/page/Main/MainPage.tsx -A 3 -B 1

Repository: pooli-dev/pooli-fe

Length of output: 3110


🏁 Script executed:

#!/bin/bash

# Check usageData usage in MainPage with correct syntax
echo "=== usageData usage in MainPage ==="
rg -n "usageData" --type=ts src/page/Main/MainPage.tsx -A 3 -B 1

# Get full sharedPoolService.ts to understand the pattern
echo -e "\n=== Full sharedPoolService.ts ==="
cat -n src/api/services/sharedPoolService.ts | head -70

# Check if there's any conversion happening at display level
echo -e "\n=== Check for bytesToGb in MainPage ==="
rg -n "bytesToGb" src/page/Main/

Repository: pooli-dev/pooli-fe

Length of output: 2933


getUsageData에서 바이트를 GB로 변환하지 않는 일관성 문제

getMainRemainingAmountgetMySharedPool은 API 응답 데이터를 bytesToGb()로 변환하지만, getUsageData는 변환 없이 원시 응답을 반환합니다. UsageDatasharedPoolTotalData 필드와 MemberUsagemonthlySharedPoolUsage 필드도 바이트 단위이므로 다른 메서드와 동일하게 변환이 필요합니다.

권장 수정 예시
getUsageData: async () => {
  const response = await apiClient.get<UsageData>("/shared-pools/usage/monthly-total");
  const data = response.data;
  
  return {
    sharedPoolTotalData: bytesToGb(data.sharedPoolTotalData),
    membersUsageList: data.membersUsageList.map(member => ({
      ...member,
      monthlySharedPoolUsage: bytesToGb(member.monthlySharedPoolUsage),
    })),
  };
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/services/sharedPoolService.ts` around lines 64 - 65, getUsageData
currently returns raw byte values while getMainRemainingAmount and
getMySharedPool convert bytes to GB; update getUsageData (the
apiClient.get<UsageData>("/shared-pools/usage/monthly-total") call) to await the
response, convert response.data.sharedPoolTotalData with bytesToGb, and map
response.data.membersUsageList turning each member.monthlySharedPoolUsage into
bytesToGb(...) (preserving other member fields) so the returned shape matches
UsageData but with GB values for sharedPoolTotalData and monthlySharedPoolUsage.

Comment on lines +283 to +285
<p className="text-xs text-gray-400 mt-3">
종료 시간: {calcEndTimeDisplay(previewEndTime)}
</p>
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 | 🟡 Minor

차단이 활성화된 상태에서도 미리보기 종료 시간이 표시됩니다.

현재 차단이 활성화된 경우 실제 종료 시간(data?.blockEndAt)을 표시해야 하지만, 항상 previewEndTime(선택된 분 기준)을 표시합니다. 이로 인해 사용자에게 혼란을 줄 수 있습니다.

🔧 조건부 종료 시간 표시 제안
      <p className="text-xs text-gray-400 mt-3">
-        종료 시간: {calcEndTimeDisplay(previewEndTime)}
+        종료 시간: {calcEndTimeDisplay(enabled ? data?.blockEndAt : previewEndTime)}
      </p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="text-xs text-gray-400 mt-3">
종료 시간: {calcEndTimeDisplay(previewEndTime)}
</p>
<p className="text-xs text-gray-400 mt-3">
종료 시간: {calcEndTimeDisplay(enabled ? data?.blockEndAt : previewEndTime)}
</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/components/Immediateblockpolicy.tsx` around lines 283 -
285, The end-time display always uses previewEndTime but should show the actual
block end when the block is active; update the rendering in Immediateblockpolicy
(the paragraph using calcEndTimeDisplay(previewEndTime)) to choose the value
conditionally: if the block is active (use data?.blockEndAt or a boolean like
data?.isBlocked), call calcEndTimeDisplay(data?.blockEndAt) else call
calcEndTimeDisplay(previewEndTime); handle null/undefined safely so the
component doesn't break when data?.blockEndAt is absent.

Comment on lines +6 to +10
const { data } = useQuery({
queryKey: ["limits", lineId],
queryFn: () => limitService.getLimits(lineId!).then((res) => res.data),
enabled: !!lineId,
});
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

🧩 Analysis chain

🏁 Script executed:

cat -n src/page/PolicyDetail/components/LimitTab.tsx | head -50

Repository: pooli-dev/pooli-fe

Length of output: 1382


🏁 Script executed:

cat -n src/page/PolicyDetail/components/SliderCard.tsx | head -80

Repository: pooli-dev/pooli-fe

Length of output: 3105


로딩/에러 가드 없이 렌더되어 잘못된 초기 상태와 mutation 실패 위험이 있습니다.

현재는 데이터 로딩 상태를 확인하지 않고 SliderCard를 항상 렌더하고 있습니다. limitPolicyIddata?.lineLimitId로 전달되므로 undefined가 될 수 있는데, 사용자가 상호작용할 때 SliderCard.tsx Line 71-72의 limitPolicyId!를 사용한 mutation에서 실패합니다.
또한 초기 렌더 시 data?.maxSharedData ?? 0 등의 폴백 값으로 실제 로드되지 않은 상태가 숨겨집니다.

🛠️ 제안 수정
-  const { data } = useQuery({
+  const { data, isLoading, isError } = useQuery({
     queryKey: ["limits", lineId],
-    queryFn: () => limitService.getLimits(lineId!).then((res) => res.data),
-    enabled: !!lineId,
+    queryFn: async () => {
+      if (lineId == null) throw new Error("lineId is required");
+      return limitService.getLimits(lineId).then((res) => res.data);
+    },
+    enabled: lineId != null,
   });
+
+  if (lineId == null) return null;
+  if (isLoading) {
+    return <div className="py-4 text-sm text-gray-400">제한 정보를 불러오는 중입니다.</div>;
+  }
+  if (isError || !data) {
+    return <div className="py-4 text-sm text-red-400">제한 정보를 불러오지 못했습니다.</div>;
+  }
@@
-        limitPolicyId={data?.lineLimitId}
-        initialValue={data?.sharedDataLimit}
-        initialEnabled={data?.isSharedDataLimitActive ?? false}
-        max={data?.maxSharedData ?? 0}
+        limitPolicyId={data.lineLimitId}
+        initialValue={data.sharedDataLimit}
+        initialEnabled={data.isSharedDataLimitActive}
+        max={data.maxSharedData}
@@
-        limitPolicyId={data?.lineLimitId}
-        initialValue={data?.dailyDataLimit}
-        initialEnabled={data?.isDailyDataLimitActive ?? false}
-        max={data?.maxDailyData ?? 0}
+        limitPolicyId={data.lineLimitId}
+        initialValue={data.dailyDataLimit}
+        initialEnabled={data.isDailyDataLimitActive}
+        max={data.maxDailyData}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/components/LimitTab.tsx` around lines 6 - 10, The
component renders SliderCard without guarding for loading/error from useQuery,
causing undefined limitPolicyId (data?.lineLimitId) and unsafe non-null
assertions in SliderCard mutations; update the useQuery call to destructure
isLoading/isError (from the query that uses limitService.getLimits) and only
render SliderCard when data is present (e.g., if (isLoading) show a loader; if
(isError || !data) show an error/placeholder), pass the real data values (avoid
masking with fallbacks like data?.maxSharedData ?? 0) and ensure SliderCard’s
interactions handle undefined limitPolicyId by validating before calling its
mutation (remove or avoid using non-null assertion on limitPolicyId in
SliderCard and check limitPolicyId exists before invoking mutations).

Comment on lines +51 to +52
const gb = formatData(bytes);
const maxGb = formatData(max);
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 | 🔴 Critical

formatData 반환값 단위 불일치로 인한 데이터 오류

formatData는 1GB 이상이면 GB 값을, 1GB 미만이면 MB 값을 반환합니다. 하지만 코드에서는 항상 GB로 가정하고 * 1e9로 바이트 변환합니다.

예: 500MB (5e8 bytes)formatData 반환 500handleChange(500 * 1e9) = 500GB로 잘못 저장됨

🐛 수정 제안: GB 단위로 통일
+  // bytes를 GB로 변환 (항상 GB 단위 사용)
+  const toGB = (bytes: number) => Math.round((bytes / 1e9) * 10) / 10;
+
-  const gb = formatData(bytes);
-  const maxGb = formatData(max);
+  const gb = toGB(bytes);
+  const maxGb = toGB(max);

Also applies to: 121-131

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

In `@src/page/PolicyDetail/components/SliderCard.tsx` around lines 51 - 52,
formatData currently returns MB for values <1GB which breaks byte reconversion
(gb = formatData(bytes) is treated as GB); update formatData (or add a new
helper like formatDataInGB) so it always returns a numeric value expressed in GB
(e.g., 0.5 for 500MB) and then use that consistent GB value for conversions
(e.g., handleChange(gb * 1e9)); update both uses (gb and maxGb in SliderCard and
the other occurrence around lines 121-131) to rely on the GB-normalized return
so byte math is correct.

Comment on lines +108 to 120
const handleBlockRelease = async () => {
if (!selectedMember?.lineId) return;
const now = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
const nowStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;

await blockService.patchImmediateBlock(selectedMember.lineId, nowStr);
setActiveBlockEndTime(null); // 배너 숨김
queryClient.invalidateQueries({
queryKey: ["immediateBlock", selectedMember.lineId],
});
show("차단이 해제되었습니다."); // 토글 업데이트
};
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 | 🟡 Minor

handleBlockRelease 성공/실패 처리 누락

blockService.patchImmediateBlock 호출 시 에러 핸들링이 없습니다. API 호출 실패 시 사용자에게 피드백이 제공되지 않고, activeBlockEndTimenull로 설정되어 UI가 실제 상태와 불일치할 수 있습니다.

🛡️ 에러 핸들링 추가 제안
  const handleBlockRelease = async () => {
    if (!selectedMember?.lineId) return;
    const now = new Date();
    const pad = (n: number) => String(n).padStart(2, "0");
    const nowStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;

-    await blockService.patchImmediateBlock(selectedMember.lineId, nowStr);
-    setActiveBlockEndTime(null); // 배너 숨김
-    queryClient.invalidateQueries({
-      queryKey: ["immediateBlock", selectedMember.lineId],
-    });
-    show("차단이 해제되었습니다."); // 토글 업데이트
+    try {
+      await blockService.patchImmediateBlock(selectedMember.lineId, nowStr);
+      setActiveBlockEndTime(null);
+      queryClient.invalidateQueries({
+        queryKey: ["immediateBlock", selectedMember.lineId],
+      });
+      show("차단이 해제되었습니다.");
+    } catch {
+      show("차단 해제에 실패했습니다.", "error");
+    }
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleBlockRelease = async () => {
if (!selectedMember?.lineId) return;
const now = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
const nowStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
await blockService.patchImmediateBlock(selectedMember.lineId, nowStr);
setActiveBlockEndTime(null); // 배너 숨김
queryClient.invalidateQueries({
queryKey: ["immediateBlock", selectedMember.lineId],
});
show("차단이 해제되었습니다."); // 토글 업데이트
};
const handleBlockRelease = async () => {
if (!selectedMember?.lineId) return;
const now = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
const nowStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
try {
await blockService.patchImmediateBlock(selectedMember.lineId, nowStr);
setActiveBlockEndTime(null);
queryClient.invalidateQueries({
queryKey: ["immediateBlock", selectedMember.lineId],
});
show("차단이 해제되었습니다.");
} catch {
show("차단 해제에 실패했습니다.", "error");
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/PolicyDetail/PolicyDetail.tsx` around lines 108 - 120, The
handleBlockRelease function lacks error handling around the async API call; wrap
the call to blockService.patchImmediateBlock(selectedMember.lineId, nowStr) in a
try/catch, only call setActiveBlockEndTime(null),
queryClient.invalidateQueries({ queryKey: ["immediateBlock",
selectedMember.lineId] }) and show("차단이 해제되었습니다.") after the patch succeeds, and
in the catch branch show an error message (e.g., show("차단 해제에 실패했습니다.") or the
error message) and avoid mutating activeBlockEndTime so the UI remains
consistent; keep the existing selectedMember?.lineId guard and preserve nowStr
generation and use of patchImmediateBlock for locating the change.

Comment thread src/utils/dataFormat.ts
Comment on lines +70 to +84
export function toBlockPolicy(res: RepeatBlockResponse): BlockPolicy {
// days 배열에서 첫 번째 기준으로 시간 파싱 (모든 요일이 같은 시간 가정)
const firstDay = res.days[0];
const [startHour, startMin] = firstDay.startAt.split(":").map(Number);
const [endHour, endMin] = firstDay.endAt.split(":").map(Number);

return {
id: res.repeatBlockId,
lineId: res.lineId,
startHour,
startMin,
endHour,
endMin,
days: res.days.map((d) => DAY_MAP[d.dayOfWeek]),
enabled: res.isActive,
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 | 🔴 Critical

toBlockPolicy의 입력 검증 누락으로 런타임 예외/잘못된 데이터가 발생할 수 있습니다.

Line 72는 res.days가 비어 있으면 바로 크래시합니다. 또한 Line 83은 매핑 실패 시 undefineddays에 들어갈 수 있습니다. API 응답 검증을 먼저 해야 안전합니다.

🛠️ 제안 수정
 export function toBlockPolicy(res: RepeatBlockResponse): BlockPolicy {
-  // days 배열에서 첫 번째 기준으로 시간 파싱 (모든 요일이 같은 시간 가정)
-  const firstDay = res.days[0];
+  if (!res.days?.length) {
+    throw new Error("repeat block days is empty");
+  }
+  // days 배열에서 첫 번째 기준으로 시간 파싱 (모든 요일이 같은 시간 가정)
+  const firstDay = res.days[0];
   const [startHour, startMin] = firstDay.startAt.split(":").map(Number);
   const [endHour, endMin] = firstDay.endAt.split(":").map(Number);
+  const mappedDays = res.days.map((d) => {
+    const mapped = DAY_MAP[d.dayOfWeek];
+    if (!mapped) {
+      throw new Error(`unsupported dayOfWeek: ${d.dayOfWeek}`);
+    }
+    return mapped;
+  });
 
   return {
@@
-    days: res.days.map((d) => DAY_MAP[d.dayOfWeek]),
+    days: mappedDays,
     enabled: res.isActive,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/dataFormat.ts` around lines 70 - 84, The toBlockPolicy function
lacks input validation for res.days and DAY_MAP lookups; add checks in
toBlockPolicy to (1) ensure res.days exists and is non-empty before accessing
res.days[0] and throw or return a clear error if not, (2) validate startAt/endAt
parse results (startHour/startMin and endHour/endMin) are numbers and
throw/handle parse failures, and (3) when mapping days via DAY_MAP inside
res.days.map(...), filter out or handle entries where DAY_MAP[d.dayOfWeek] is
undefined (or throw a descriptive error) so days never contain undefined; use
the function name toBlockPolicy and types RepeatBlockResponse and DAY_MAP to
locate and update the logic accordingly.

@haeni82 haeni82 merged commit cab8fee into develop Mar 15, 2026
1 of 2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 16, 2026
1 task
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.

[feat] 정책 페이지 api 연동

1 participant