Feature/policy api#50
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthrough정책, 블록, 제한, 알림 및 공유풀 관련 API/타입/컴포넌트를 서버 기반으로 대규모 전환 및 확장했습니다. limitService 추가, React Query로 상태/페칭 전환, 여러 블록/제한 편집 UI 및 유틸/상수·타입 확장이 포함됩니다. Changes
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: 읽음 상태 반영
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: 무효화/갱신
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: 목록 갱신
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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: 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.tsx→Permission.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:type이undefined일 때 맵 접근 동작 확인
notification.value?.type이undefined일 경우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:onScroll과onScrollCapture중복 사용두 이벤트 핸들러가 모두
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
📒 Files selected for processing (39)
src/api/index.tssrc/api/services/blockService.tssrc/api/services/familyService.tssrc/api/services/limitService.tssrc/api/services/notificationService.tssrc/api/services/sharedPoolService.tssrc/components/Header.tsxsrc/components/common/RangeSlider.tsxsrc/constants/alarmMessages.tssrc/data/alarmDummyData.tssrc/page/Alarm.tsxsrc/page/Main/MainPage.tsxsrc/page/Main/components/FamilyMemberList.tsxsrc/page/Main/components/MemberCard.tsxsrc/page/Main/components/PieChart.tsxsrc/page/Main/components/SharedPoolUsage.tsxsrc/page/Policy/Policy.tsxsrc/page/Policy/components/DataRemainingCard.tsxsrc/page/Policy/components/DataThresholdSlider.tsxsrc/page/Policy/components/Permisssion.tsxsrc/page/PolicyDetail/PolicyDetail.tsxsrc/page/PolicyDetail/components/Block/BlockItem.tsxsrc/page/PolicyDetail/components/Block/EditPanel.tsxsrc/page/PolicyDetail/components/Block/ScrollPicker.tsxsrc/page/PolicyDetail/components/Block/TimePicker.tsxsrc/page/PolicyDetail/components/BlockTab.tsxsrc/page/PolicyDetail/components/Blockpolicymanager.tsxsrc/page/PolicyDetail/components/Immediateblockpolicy.tsxsrc/page/PolicyDetail/components/LimitTab.tsxsrc/page/PolicyDetail/components/SliderCard.tsxsrc/store/alarmStore.tssrc/types/FamilyMember.tssrc/types/SharedData.tssrc/types/alarm.tssrc/types/block.tssrc/types/limit.tssrc/types/permission.tssrc/utils/alarmUtils.tssrc/utils/dataFormat.ts
💤 Files with no reviewable changes (2)
- src/store/alarmStore.ts
- src/data/alarmDummyData.ts
| // 즉시 차단 토글/수정 | ||
| updateImmediateBlock: (lineId: number, blockEndAt: string) => | ||
| apiClient.patch("/policies/lines/immediate-block", { blockEndAt }, { | ||
| params: { lineId }, | ||
| }), | ||
| apiClient.patch( | ||
| "/policies/lines/immediate-block", | ||
| { blockEndAt }, | ||
| { | ||
| params: { lineId }, | ||
| }, | ||
| ), |
There was a problem hiding this comment.
🛠️ 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 -C2Repository: pooli-dev/pooli-fe
Length of output: 5784
🏁 Script executed:
sed -n '154,184p' src/api/services/blockService.ts | cat -nRepository: pooli-dev/pooli-fe
Length of output: 1097
updateImmediateBlock와 patchImmediateBlock 통합 필요
두 메서드가 동일한 엔드포인트(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.
|
|
||
| // 즉시 차단 조회 | ||
| 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 } }, | ||
| ), |
There was a problem hiding this comment.
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.
| // 즉시 차단 조회 | |
| 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.
| markAsRead: (alarmHistoryId: number) => | ||
| apiClient.patch<Notification>("/notifications", { alarmHistoryId }), |
There was a problem hiding this comment.
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.
| getUsageData: () => | ||
| apiClient.get<UsageData>("/shared-pools/usage/monthly-total"), |
There was a problem hiding this comment.
🧩 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 2Repository: 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 1Repository: 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로 변환하지 않는 일관성 문제
getMainRemainingAmount와 getMySharedPool은 API 응답 데이터를 bytesToGb()로 변환하지만, getUsageData는 변환 없이 원시 응답을 반환합니다. UsageData의 sharedPoolTotalData 필드와 MemberUsage의 monthlySharedPoolUsage 필드도 바이트 단위이므로 다른 메서드와 동일하게 변환이 필요합니다.
권장 수정 예시
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.
| <p className="text-xs text-gray-400 mt-3"> | ||
| 종료 시간: {calcEndTimeDisplay(previewEndTime)} | ||
| </p> |
There was a problem hiding this comment.
차단이 활성화된 상태에서도 미리보기 종료 시간이 표시됩니다.
현재 차단이 활성화된 경우 실제 종료 시간(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.
| <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.
| const { data } = useQuery({ | ||
| queryKey: ["limits", lineId], | ||
| queryFn: () => limitService.getLimits(lineId!).then((res) => res.data), | ||
| enabled: !!lineId, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/page/PolicyDetail/components/LimitTab.tsx | head -50Repository: pooli-dev/pooli-fe
Length of output: 1382
🏁 Script executed:
cat -n src/page/PolicyDetail/components/SliderCard.tsx | head -80Repository: pooli-dev/pooli-fe
Length of output: 3105
로딩/에러 가드 없이 렌더되어 잘못된 초기 상태와 mutation 실패 위험이 있습니다.
현재는 데이터 로딩 상태를 확인하지 않고 SliderCard를 항상 렌더하고 있습니다. limitPolicyId는 data?.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).
| const gb = formatData(bytes); | ||
| const maxGb = formatData(max); |
There was a problem hiding this comment.
formatData 반환값 단위 불일치로 인한 데이터 오류
formatData는 1GB 이상이면 GB 값을, 1GB 미만이면 MB 값을 반환합니다. 하지만 코드에서는 항상 GB로 가정하고 * 1e9로 바이트 변환합니다.
예: 500MB (5e8 bytes) → formatData 반환 500 → handleChange(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.
| 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("차단이 해제되었습니다."); // 토글 업데이트 | ||
| }; |
There was a problem hiding this comment.
handleBlockRelease 성공/실패 처리 누락
blockService.patchImmediateBlock 호출 시 에러 핸들링이 없습니다. API 호출 실패 시 사용자에게 피드백이 제공되지 않고, activeBlockEndTime이 null로 설정되어 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.
| 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.
| 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, |
There was a problem hiding this comment.
toBlockPolicy의 입력 검증 누락으로 런타임 예외/잘못된 데이터가 발생할 수 있습니다.
Line 72는 res.days가 비어 있으면 바로 크래시합니다. 또한 Line 83은 매핑 실패 시 undefined가 days에 들어갈 수 있습니다. 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.
이슈
✔️ 체크리스트
🔍 작업 내용
Summary by CodeRabbit
New Features
Improvements