Skip to content

[REFACTOR/FIX] 회원 그룹 관리 페이지 구조 개선 및 QA 반영#560

Merged
jinj00oo merged 12 commits intodevelopfrom
refactor/group-management-page
Mar 9, 2026
Merged

[REFACTOR/FIX] 회원 그룹 관리 페이지 구조 개선 및 QA 반영#560
jinj00oo merged 12 commits intodevelopfrom
refactor/group-management-page

Conversation

@jinj00oo
Copy link
Collaborator

@jinj00oo jinj00oo commented Feb 28, 2026

✅ 체크리스트

  • 코드에 any가 사용되지 않았습니다
  • 스토리북에 추가했습니다 (해당 시)
  • 이슈와 커밋이 명확히 연결되어 있습니다

🔗 관련 이슈

📌 작업 목적

  • GroupManagementDetailPage에 집중되어 있던 오케스트레이션 로직(데이터 조회, 폼 상태 관리, 모드별 UI 정책 등)을 분리하여 책임을 명확히 하고 가독성을 개선합니다.
  • 렌더링과 정책 로직을 분리해 페이지를 View 레이어 중심 구조로 정리합니다.

🔨 주요 작업 내용

  • Header 정책 분리
    • 모드(create / edit / view)에 따른 header 설정을 getHeaderConfig로 분리
  • StickyButton 정책 분리
    • 모드 및 상태(canSubmit, pending 등)에 따른 버튼 정책을 getStickyButtonConfig로 분리
  • Alert 묶음 분리
    • 페이지에 흩어져 있던 Alert 로직을 useGroupManagementAlerts로 통합
    • 페이지에서는 단순히 openXxxAlert만 호출하도록 정리
  • BottomSheet 묶음 분리
    • 바텀 시트 open 로직을 useGroupManagementBottomSheets로 분리
    • 페이지에서 sheet payload 조립 책임 제거
  • Controller 도입
    • useGroupManagementDetailController를 도입하여
      • 데이터 조회
      • 폼 hydrate
      • 모드별 정책 결정
      • 주요 핸들러 관리
    • 페이지는 JSX 조립(View 역할)에 집중하도록 구조 개선

⚠️ 기존 문제 (선택)

  • 그룹 생성 후 상세 페이지로 이동 시 잠시 empty space가 보이는 문제가 있었습니다.
  • 기존 로직상, moveForm으로 폼 이관을 했으므로 바로 데이터가 보이는 흐름이어야 했습니다.
  • 로그를 통해 확인해본 결과, moveForm에서 create 폼을 지운 후부터 상세 페이지로 라우팅 되기 전 순간까지 create 모드로 리렌더링이 일어나면서 empty space가 보인 것으로 확인되었습니다.

☑️ 해결 방법 (선택)

  • moveFormcopyForm으로 변경하여 새로운 폼을 생성하기만 하고, 기존 폼의 삭제는 수행하지 않도록 수정했습니다.
  • create 폼을 클린업 하기 위해서 상세 페이지가 마운트되면 create 폼을 지우는 로직을 추가하였습니다.

🧪 테스트 방법

  • 회원 그룹 생성 / 수정 / 삭제 로직이 잘 작동하는지 확인해주세요.

💬 논의할 점

🙋🏻 참고 자료

Summary by CodeRabbit

Summary by CodeRabbit

  • Refactor
    • 그룹 관리 상세 페이지를 컨트롤러 기반으로 재구성해 UI 흐름을 통합했습니다.
  • New Features
    • 편집·삭제·이탈·리더선택용 확인 알림(모달)을 추가했습니다.
    • 세대·그룹유형·리더 선택용 하단 시트를 도입했습니다.
  • UI
    • 세대 아코디언에 기본 열림 옵션을 추가하고 기본 상태를 조정했습니다.
    • 멤버 검색 리스트에서 아코디언을 기본 열림으로 변경했습니다.
  • Behavior Changes
    • 그룹 폼 복사 시 원본을 보존하도록 동작을 변경했습니다.
  • Other
    • 고정 액션 버튼의 대기(pending) 판정에 네비게이션 상태를 반영하도록 개선했습니다.

@jinj00oo jinj00oo self-assigned this Feb 28, 2026
@vercel
Copy link

vercel bot commented Feb 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
surf-admin Ready Ready Preview, Comment Mar 6, 2026 11:41am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
surf-web Skipped Skipped Mar 6, 2026 11:41am

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

상세 페이지의 오케스트레이션 로직을 controller로 이동해 리팩터링했습니다. new: useController(데이터 페칭, 뮤테이션, 폼 상태, 내비게이션, 알림·바텀시트 연동), useAlerts(저장/삭제/되돌아가기/리더 미지정 알림), useBottomSheets(세대/그룹타입/리더 선택). 컴포넌트는 controller 반환값(c.*)으로 렌더링하도록 변경되었습니다. 유틸명 변경: mapModeToHeaderPropsgetHeaderConfig. 폼 전송 로직은 moveForm에서 copyForm으로 변경되어 원본을 보존합니다. 아코디언에 defaultOpen prop이 추가되고 sticky 버튼에 네비게이션 대기 상태(isCreateNavigating)가 반영되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

코드 리뷰 피드백

1) 책임 분리(아키텍처)

  • 문제: useController에 데이터 페칭, 뮤테이션, 폼 로직, 알림·바텀시트, 라우팅 등 다수 책임이 집중되어 단위 테스트·유지보수 비용이 높습니다.
  • 권장 개선: 책임별로 작은 훅으로 분리하세요.
    • 예: useGroupForm(폼 상태·유효성), useGroupActions(create/update/delete + toast), useGroupUI(header/sticky 상태).
    • benefit: 각 훅을 독립적으로 테스트하고, 수정을 국소화할 수 있습니다.

2) 네이밍 & 타입 안전성

  • 문제: 여러 파일에서 범용 Params 타입을 사용하면 충돌·혼동 소지 있습니다.
  • 권장 개선: 파일 맥락을 드러내는 이름 사용(예: UseControllerParams, UseAlertsParams, UseBottomSheetsParams). 또한 공개 반환 타입에 인터페이스 명시로 소비자 의도를 문서화하세요.

3) 에러 처리(안정성)

  • 문제: 뮤테이션(create/update/delete)과 외부 요청의 에러 경로가 일관되게 처리되지 않았습니다.
  • 권장 개선:
    • 모든 mutation에 명시적 onError 핸들러 추가 또는 try/catch로 감싸 사용자 메시지와 복구 흐름 제공(예: 재시도 버튼).
    • 실패 시 로컬 상태 롤백이나 로딩 상태 정리 보장.
    • 네트워크 실패·타임아웃을 고려한 공통 에러 유틸을 만드는 것을 권장합니다.

4) 제출 가능성(canSubmit) 검증 강화

  • 문제: canSubmit 산출에 필수 필드(leader, groupType 등)가 명시적으로 포함되어 있지 않을 수 있습니다.
  • 권장 개선: 검증 규칙을 명시화하여 경계 조건을 방지하세요(예: 빈 문자열, null, 미선택 항목). 유닛 테스트로 경계값 커버리지를 추가하세요.

5) 의존성 주입으로 테스트 용이성 향상

  • 문제: 훅들이 전역 스토어나 전역 유틸에 강하게 결합되어 단위테스트 작성이 어렵습니다.
  • 권장 개선: 기본 인자로 스토어/유틸을 주입하도록 시그니처 변경(예: useAlerts(params, alertStore = useAlertStore()))하면 목 주입이 쉬워집니다.

6) 퍼블릭 API 축소·문서화

  • 문제: useController가 매우 많은 필드를 반환해 소비자가 불필요한 내부 상태에 의존할 위험이 있습니다.
  • 권장 개선: 외부 컴포넌트에 꼭 필요한 핸들러/데이터만 노출하고 나머지는 내부로 캡슐화하세요. 공개 인터페이스에 JSDoc으로 부작용(라우팅, 전역스토어 변경)을 문서화하세요.

7) 변경된 퍼블릭 동작: moveFormcopyForm

  • 문제: 동작이 변경되어(원본 보존) 호출부 동작에 영향이 있을 수 있습니다.
  • 권장 개선: 이 변화를 명시적으로 문서화하고, create 흐름에서 중복/정리 로직(예: 빈 폼 제거)이 필요한지 검토하세요. 필요 시 마이그레이션 주석 추가.

8) 접근성(A11y) 및 UX

  • 권장 점검:
    • Alert에 role="alertdialog", aria-labelledby/aria-describedby 추가.
    • 확인/취소 버튼에 명확한 aria-label, 키보드 포커스 관리(포커스 트랩) 적용.
    • 바텀시트는 포커스 트랩과 ESC 닫기, 스크린리더 announced 상태 검증.
    • toast/알림 시 비시각적 피드백(aria-live) 고려.

9) 브라우저 호환성 및 성능

  • 권장 점검:
    • 큰 훅(많은 의존성)의 메모이제이션(useMemo/useCallback) 적절성 확인해 불필요한 리렌더 방지.
    • 대량 멤버 렌더링 시 가상화나 key 안정성 확인.

10) 테스트 커버리지

  • 권장 작성 테스트:
    • useController: create/update/delete 성공·실패 경로, canSubmit 로직.
    • useAlerts/useBottomSheets: 알림 열기/확인/취소 흐름과 콜백 호출.
    • copyForm 동작: 원본 보존 여부와 overwrite 케이스.
  • 단위 테스트와 함께 적어도 1~2개의 통합(패턴) 테스트로 새 컨트롤러 흐름을 검증하세요.

요약: 리팩터링 방향은 적절합니다. 다음 단계로 훅의 책임 분산, 명확한 타입네이밍, 일관된 에러 처리·테스트 전략, 접근성 점검 및 copyForm 동작 문서화를 우선 적용하시길 권장합니다.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확하게 요약합니다. 회원 그룹 관리 페이지의 구조 개선과 QA 반영이라는 핵심 목표를 정확하게 나타냅니다.
Linked Issues check ✅ Passed 코드 변경사항이 #559의 목표를 충족합니다. 컨트롤러(useController), 정책 분리(getHeaderConfig, getStickyButtonConfig), Alert/BottomSheet 훅 분리가 구현되어 오케스트레이션 로직의 책임 분리가 명확히 이루어졌습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #559의 목표 범위 내에 있습니다. MemberGenerationAccordion의 defaultOpen 추가, moveForm에서 copyForm 변경은 모두 구조 개선과 버그 수정에 관련된 필수 변경입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 refactor/group-management-page

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
apps/admin/src/app-pages/group-management/detail/model/useAlerts.tsx (1)

13-31: useCallback 메모이제이션 고려

useBottomSheets 훅에서는 useCallback으로 핸들러를 메모이제이션했는데, 이 훅에서는 일반 함수로 선언되어 있습니다. 이 핸들러들이 자식 컴포넌트에 props로 전달될 경우 불필요한 리렌더링이 발생할 수 있습니다.

일관성과 성능을 위해 useCallback 적용을 권장합니다.

♻️ useCallback 적용 예시
+ import { useCallback } from 'react';

- const openSaveEditAlert = () => {
+ const openSaveEditAlert = useCallback(() => {
    openAlert({
      state: 'default',
      title: '수정하시겠습니까?',
      // ...
    });
- };
+ }, [openAlert, closeAlert, onSubmitEdit]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/src/app-pages/group-management/detail/model/useAlerts.tsx` around
lines 13 - 31, Wrap the openSaveEditAlert handler in React.useCallback to
memoize it (similar to how handlers in useBottomSheets are memoized) so it
doesn't change across renders when passed as a prop; reference the
openSaveEditAlert function and ensure its dependency array includes openAlert,
closeAlert, and onSubmitEdit so the callback updates only when those change.
apps/admin/src/app-pages/group-management/ui/GroupManagementDetailPage.tsx (1)

24-31: 로딩 상태 접근성 개선 권장

스크린 리더 사용자에게 로딩 상태를 알리기 위해 aria-live 또는 role="status" 속성 추가를 고려해 주세요.

♿ 접근성 개선 예시
  if (!c.draft) {
    return (
      <div className="flex h-full flex-col">
        <AppHeader overrideHeader={c.header} />
-       <div className="flex flex-1 items-center justify-center">Loading...</div>
+       <div className="flex flex-1 items-center justify-center" role="status" aria-live="polite">
+         로딩 중...
+       </div>
      </div>
    );
  }

As per coding guidelines: "접근성: aria-label, role, 키보드 내비게이션, 포커스 트랩, 대비/색상 의존 금지."

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

In `@apps/admin/src/app-pages/group-management/ui/GroupManagementDetailPage.tsx`
around lines 24 - 31, The loading fallback for the GroupManagementDetailPage
(the block checking c.draft) lacks screen-reader announcements; update the
loading container (the div currently rendering "Loading..." inside the return
when !c.draft) to include an accessible live region—e.g., add aria-live="polite"
or role="status" (and ensure the text is visible or provide a visually-hidden
element with the message) so assistive technologies are notified when the page
is loading; target the div that currently has className="flex flex-1
items-center justify-center" to apply this change.
apps/admin/src/app-pages/group-management/detail/model/useController.tsx (2)

83-90: safeDraft 객체 메모이제이션 고려

safeDraft가 매 렌더링마다 새 객체를 생성합니다. 현재 사용 패턴에서는 큰 문제가 없지만, 향후 의존성 배열에 사용될 경우 불필요한 리렌더링을 유발할 수 있습니다.

♻️ useMemo 적용 예시
- const safeDraft: GroupFormDraft = draft ?? {
-   generation: maxGeneration,
-   groupType: 'study' as ContentsType,
-   groupName: '',
-   groupIntroduction: '',
-   leader: undefined,
-   members: [],
- };
+ const safeDraft: GroupFormDraft = useMemo(() => draft ?? {
+   generation: maxGeneration,
+   groupType: 'study' as ContentsType,
+   groupName: '',
+   groupIntroduction: '',
+   leader: undefined,
+   members: [],
+ }, [draft, maxGeneration]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`
around lines 83 - 90, safeDraft is recreated on every render; wrap its creation
in useMemo so it only changes when inputs change: compute safeDraft with useMemo
based on draft and maxGeneration (keep the fallback shape with generation:
maxGeneration, groupType: 'study' as ContentsType, groupName:'',
groupIntroduction:'', leader: undefined, members:[]) and ensure useMemo is
imported; update references to safeDraft in useController.tsx so dependent
hooks/arrays use the memoized value.

117-135: 비동기 핸들러 에러 처리 강화 권장

handleDeleteGrouphandleSubmit에서 mutation 호출 시 에러가 mutation 훅 내부에서 처리되지만, 예기치 않은 에러(네트워크 중단 등)가 발생할 경우 unhandled rejection이 될 수 있습니다.

방어적으로 try-catch를 추가하거나, 에러 바운더리와 함께 사용하는 것을 권장합니다.

🛡️ 에러 처리 강화 예시
  const handleDeleteGroup = async () => {
    if (mode !== 'edit') return;
+   try {
      await deleteGroup();
      showToast('그룹이 삭제되었습니다.');
      router.back();
+   } catch {
+     // mutation onError에서 이미 처리됨, 추가 처리 필요시 여기에 작성
+   }
  };

As per coding guidelines: "에러/예외 처리 기준: 사용자-facing 에러 메시지, 로깅, 재시도 전략, timeout, abort 등을 일관되게."

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

In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`
around lines 117 - 135, handleDeleteGroup and handleSubmit should guard against
unexpected rejections by wrapping async mutation calls in try-catch blocks: in
handleDeleteGroup wrap await deleteGroup() in try, on success call
showToast('그룹이 삭제되었습니다.') and router.back(), on catch call showToast with a
user-facing error message and log the error (e.g., console.error or existing
logger) without navigating; in handleSubmit keep the isSubmitPending
short-circuit, then for the create branch wrap const created = await
createGroup(safeDraft) in try and on success
router.replace(PAGE_ROUTES.GROUP_MNG.VIEW(created.teamId)) and on error
showToast/log the error, and for the edit branch wrap await
updateGroup(safeDraft) similarly and only route on success; ensure errors are
not swallowed so callers don’t get unhandled rejections.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/admin/src/app-pages/group-management/detail/model/useAlerts.tsx`:
- Around line 13-31: Wrap the openSaveEditAlert handler in React.useCallback to
memoize it (similar to how handlers in useBottomSheets are memoized) so it
doesn't change across renders when passed as a prop; reference the
openSaveEditAlert function and ensure its dependency array includes openAlert,
closeAlert, and onSubmitEdit so the callback updates only when those change.

In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`:
- Around line 83-90: safeDraft is recreated on every render; wrap its creation
in useMemo so it only changes when inputs change: compute safeDraft with useMemo
based on draft and maxGeneration (keep the fallback shape with generation:
maxGeneration, groupType: 'study' as ContentsType, groupName:'',
groupIntroduction:'', leader: undefined, members:[]) and ensure useMemo is
imported; update references to safeDraft in useController.tsx so dependent
hooks/arrays use the memoized value.
- Around line 117-135: handleDeleteGroup and handleSubmit should guard against
unexpected rejections by wrapping async mutation calls in try-catch blocks: in
handleDeleteGroup wrap await deleteGroup() in try, on success call
showToast('그룹이 삭제되었습니다.') and router.back(), on catch call showToast with a
user-facing error message and log the error (e.g., console.error or existing
logger) without navigating; in handleSubmit keep the isSubmitPending
short-circuit, then for the create branch wrap const created = await
createGroup(safeDraft) in try and on success
router.replace(PAGE_ROUTES.GROUP_MNG.VIEW(created.teamId)) and on error
showToast/log the error, and for the edit branch wrap await
updateGroup(safeDraft) similarly and only route on success; ensure errors are
not swallowed so callers don’t get unhandled rejections.

In `@apps/admin/src/app-pages/group-management/ui/GroupManagementDetailPage.tsx`:
- Around line 24-31: The loading fallback for the GroupManagementDetailPage (the
block checking c.draft) lacks screen-reader announcements; update the loading
container (the div currently rendering "Loading..." inside the return when
!c.draft) to include an accessible live region—e.g., add aria-live="polite" or
role="status" (and ensure the text is visible or provide a visually-hidden
element with the message) so assistive technologies are notified when the page
is loading; target the div that currently has className="flex flex-1
items-center justify-center" to apply this change.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce5fb22 and ce9d9b6.

📒 Files selected for processing (6)
  • apps/admin/src/app-pages/group-management/detail/model/getHeaderConfig.ts
  • apps/admin/src/app-pages/group-management/detail/model/getStickyButtonConfig.ts
  • apps/admin/src/app-pages/group-management/detail/model/useAlerts.tsx
  • apps/admin/src/app-pages/group-management/detail/model/useBottomSheets.tsx
  • apps/admin/src/app-pages/group-management/detail/model/useController.tsx
  • apps/admin/src/app-pages/group-management/ui/GroupManagementDetailPage.tsx

jinj00oo added 2 commits March 6, 2026 11:27
- create 폼 삭제 후에서 상세 페이지로 이동하는 시간 사이 리렌더링이 발생하면 empty space 노출되었음
- 상세 페이지 마운트시 create 폼 삭제
@jinj00oo jinj00oo changed the title [REFACTOR] 회원 그룹 관리 페이지 구조 개선 [REFACTOR/FIX] 회원 그룹 관리 페이지 구조 개선 및 QA 반영 Mar 6, 2026
@vercel vercel bot temporarily deployed to Preview – surf-web March 6, 2026 04:31 Inactive
Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
apps/admin/src/widgets/group-management/ui/MemberSearchAccordionList.tsx (1)

27-32: ⚠️ Potential issue | 🟠 Major

모든 아코디언을 기본 오픈하면 초기 로딩 비용이 급격히 커집니다.

지금 구성은 generation 개수만큼 MemberGenerationAccordion가 마운트 직후 열린 상태가 되어, 세대별 조회와 후속 멤버 조회가 한 번에 발사됩니다. 검색 결과가 많은 운영 환경에서는 첫 렌더 지연과 백엔드 burst가 커지고, 키보드 사용자도 모든 멤버 카드가 즉시 포커스 트리에 들어와 탐색 비용이 커집니다. 기본 오픈이 꼭 필요하다면 첫 섹션만 열거나, 요구사항상 전부 열려야 한다면 세대별 개별 fetch 대신 리스트 레벨 배치 조회로 바꾸는 편이 안전합니다. As per coding guidelines, apps/admin/**: "테이블/필터/페이지네이션은 성능(대량 데이터)과 접근성(키보드/스크린리더) 모두 점검."

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

In `@apps/admin/src/widgets/group-management/ui/MemberSearchAccordionList.tsx`
around lines 27 - 32, All accordions are being opened by default which causes
heavy initial fetches and accessibility/performance issues; locate the
MemberGenerationAccordion usage in MemberSearchAccordionList and remove or
conditionally set the defaultOpen prop so not every generation mounts open
(e.g., defaultOpen only for the first generation: defaultOpen={generation ===
firstGeneration} or omit defaultOpen entirely), or if business requires
all-open, refactor the data flow so MemberSearchAccordionList performs a single
batched members fetch and passes results down to MemberGenerationAccordion to
avoid per-section fetch storms (adjust fetch logic invoked by
MemberGenerationAccordion accordingly).
🧹 Nitpick comments (1)
apps/admin/src/features/group-management/model/useGroupFormStore.ts (1)

28-29: copyForm의 새 계약이 구현 내부에만 숨어 있습니다.

지금 구현은 source를 유지하고, fromKey 누락이나 toKey 충돌 시 조용히 no-op 합니다. 그런데 반환 타입이 void라서 호출부는 복사 성공 여부를 알 수 없고, commit/cleanup 같은 후속 로직을 안전하게 분기하기 어렵습니다. boolean/result 반환이나 JSDoc으로 no-op/cleanup 계약을 밖으로 드러내 두는 편이 안전합니다.

As per coding guidelines, "변경이 “규칙/패턴”이라면 docs로 남긴다(컨벤션/ADR/가이드). 단발성 예외는 금지."

Also applies to: 75-97

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

In `@apps/admin/src/features/group-management/model/useGroupFormStore.ts` around
lines 28 - 29, The copyForm API currently hides its failure semantics by
returning void and silently no-oping on missing fromKey or toKey conflicts;
update the copyForm signature to return a boolean or a Result-like type so
callers can know success/failure (e.g., change copyForm: (fromKey: string,
toKey: string, opts?: { overwrite?: boolean }) => boolean) and implement it to
return false on missing source or conflict (true on success); additionally,
document the contract with JSDoc on copyForm (and mirror behavior doc for
removeForm/FormKey if needed) so callers know when cleanup/commit should run and
what conditions cause no-op versus overwritten behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`:
- Around line 125-143: The handlers (handleDeleteGroup, handleSubmit) — and
other callbacks mentioned (onSubmitEdit, onDeleteGroup, sticky onCreate/onEdit)
— call TanStack Query mutateAsync (from useCreateGroupMutation,
useDeleteGroupMutation, useUpdateGroupMutation) without catching rejections;
wrap each await mutateAsync(...) call in a try/catch that catches the error (no
extra handling required since onError already logs/shows toast) to prevent
unhandled promise rejections, then proceed with existing post-success logic
(router.replace/router.back/showToast) only after the awaited call resolves
successfully.
- Around line 4-5: Remove the internal Next import (AppRouterInstance) and
replace it with a local RouterLike type that exposes push/replace/back, update
Params to use RouterLike and keep ReadonlyURLSearchParams; then add proper async
error handling around deleteGroup, createGroup and updateGroup in
handleDeleteGroup and handleSubmit (wrap calls in try/catch, showToast or log on
failure, and only call router.back()/router.replace(...) on success) and expose
the handlers directly (use onSubmitEdit: handleSubmit and onDeleteGroup:
handleDeleteGroup instead of wrapping with void) so errors are handled inside
the handlers.

---

Outside diff comments:
In `@apps/admin/src/widgets/group-management/ui/MemberSearchAccordionList.tsx`:
- Around line 27-32: All accordions are being opened by default which causes
heavy initial fetches and accessibility/performance issues; locate the
MemberGenerationAccordion usage in MemberSearchAccordionList and remove or
conditionally set the defaultOpen prop so not every generation mounts open
(e.g., defaultOpen only for the first generation: defaultOpen={generation ===
firstGeneration} or omit defaultOpen entirely), or if business requires
all-open, refactor the data flow so MemberSearchAccordionList performs a single
batched members fetch and passes results down to MemberGenerationAccordion to
avoid per-section fetch storms (adjust fetch logic invoked by
MemberGenerationAccordion accordingly).

---

Nitpick comments:
In `@apps/admin/src/features/group-management/model/useGroupFormStore.ts`:
- Around line 28-29: The copyForm API currently hides its failure semantics by
returning void and silently no-oping on missing fromKey or toKey conflicts;
update the copyForm signature to return a boolean or a Result-like type so
callers can know success/failure (e.g., change copyForm: (fromKey: string,
toKey: string, opts?: { overwrite?: boolean }) => boolean) and implement it to
return false on missing source or conflict (true on success); additionally,
document the contract with JSDoc on copyForm (and mirror behavior doc for
removeForm/FormKey if needed) so callers know when cleanup/commit should run and
what conditions cause no-op versus overwritten behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 54a456b4-19af-45b0-bd9d-81e2c6ee8afa

📥 Commits

Reviewing files that changed from the base of the PR and between ce9d9b6 and 26e1435.

📒 Files selected for processing (5)
  • apps/admin/src/app-pages/group-management/detail/model/useController.tsx
  • apps/admin/src/features/group-management/model/queries/useCreateGroupMutation.ts
  • apps/admin/src/features/group-management/model/useGroupFormStore.ts
  • apps/admin/src/features/member-by-generation/ui/MemberGenerationAccordion.tsx
  • apps/admin/src/widgets/group-management/ui/MemberSearchAccordionList.tsx

- 원인: 수정과 달리 생성 후 canSubmit이 true가 되었음
- 해결: 라우팅 중임을 나타나는 state 추가
Copy link

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

♻️ Duplicate comments (2)
apps/admin/src/app-pages/group-management/detail/model/useController.tsx (2)

4-5: 🛠️ Refactor suggestion | 🟠 Major

next/dist/* 내부 타입 의존은 공개 표면으로 바꾸는 편이 안전합니다.

App Router의 공개 문서는 라우터를 next/navigation에서 사용하도록 안내하고, 이 훅이 실제로 쓰는 메서드도 push/replace/back뿐입니다. 그래서 AppRouterInstance를 내부 빌드 경로에서 직접 가져오기보다, 이 파일에서 필요한 표면만 RouterLike로 선언해 받는 쪽이 업그레이드 내성이 더 좋습니다. (nextjs.org)

예시
-import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
 import type { ReadonlyURLSearchParams } from 'next/navigation';
+
+type RouterLike = {
+  push: (href: string, options?: { scroll?: boolean }) => void;
+  replace: (href: string, options?: { scroll?: boolean }) => void;
+  back: () => void;
+};

 type Params = {
   mode: GroupManagementMode;
   id?: string;
-  router: AppRouterInstance;
+  router: RouterLike;
   searchParams: ReadonlyURLSearchParams;
 };

Also applies to: 25-30

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

In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`
around lines 4 - 5, The file imports the internal type AppRouterInstance from
"next/dist/…" which exposes an unstable public surface; replace that dependency
by declaring and using a minimal RouterLike type in useController.tsx that only
includes the methods you actually use (push, replace, back) and update function
signatures/params that currently reference AppRouterInstance to use RouterLike
instead; keep the existing ReadonlyURLSearchParams import from
"next/navigation", and apply the same replacement wherever AppRouterInstance is
referenced (e.g., the handler/props around the useController function and any
variables assigned that type).

136-140: ⚠️ Potential issue | 🟠 Major

삭제/수정 경로의 mutateAsync 거부는 여기서도 잡아주세요.

mutateAsync는 실패 시 reject되는 Promise라서, onError가 있어도 호출부에서 catch하지 않으면 unhandled rejection이 남을 수 있습니다. 지금은 삭제 경로와 수정 경로가 await를 try/catch 없이 통과하고, alert 콜백에서는 void로 Promise를 버리고 있어서 실패 시 후속 라우팅까지 섞일 위험이 있습니다. 핸들러 내부에서 catch한 뒤 성공했을 때만 토스트와 라우팅을 이어가세요. (tanstack.com)

예시
 const handleDeleteGroup = async () => {
   if (mode !== 'edit') return;
-  await deleteGroup();
-  showToast('그룹이 삭제되었습니다.');
-  router.back();
+  try {
+    await deleteGroup();
+    showToast('그룹이 삭제되었습니다.');
+    router.back();
+  } catch {
+    // mutation onError에서 사용자 피드백 처리
+  }
 };
 } else if (mode === 'edit' && groupId) {
-  await updateGroup(safeDraft);
-  router.replace(PAGE_ROUTES.GROUP_MNG.VIEW(groupId));
+  try {
+    await updateGroup(safeDraft);
+    router.replace(PAGE_ROUTES.GROUP_MNG.VIEW(groupId));
+  } catch {
+    // mutation onError에서 사용자 피드백 처리
+  }
 }
 const { openSaveEditAlert, openDeleteAlert, openGoBackAlert, openPickLeaderAlert } = useAlerts({
-  onSubmitEdit: () => void handleSubmit(),
-  onDeleteGroup: () => void handleDeleteGroup(),
+  onSubmitEdit: handleSubmit,
+  onDeleteGroup: handleDeleteGroup,
   onLeavePage: handleLeavePage,
 });

As per coding guidelines, "[apps/admin] 어드민 권한/감사 로그/파괴적 액션(삭제 등)은 안전장치(확인 모달, 롤백/재시도, 권한 체크)를 필수로."

Also applies to: 143-158, 175-177

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

In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`
around lines 136 - 140, The delete handler handleDeleteGroup currently awaits
deleteGroup() (a mutateAsync) without try/catch, causing potential unhandled
rejections and incorrect post-success actions; wrap the call to deleteGroup() in
a try/catch (or use .catch) and only call showToast('그룹이 삭제되었습니다.') and
router.back() when the mutation resolves successfully, and in the catch branch
log/show an error (e.g., showToast with failure message) to prevent routing on
failure; apply the same fix pattern to the edit/update handlers that call
mutateAsync (referenced mutate functions around lines after handleDeleteGroup)
so all destructive/mutation paths handle rejection explicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`:
- Around line 36-37: The current effect hydrates the edit form too early using
only !isGroupDetailLoading which lets the default initialDraft overwrite even on
failed/invalid fetches; update the hydrate guard so that when mode === 'edit'
you only call hydrate (from useGroupFormStore) after confirming groupId is a
valid Number, groupDetail is present (successful fetch), and
isGroupDetailLoading is false (and optionally no fetch error), while keeping the
existing create path (formKey for 'create') unchanged; reference the symbols
groupId, formKey, mode, isGroupDetailLoading, groupDetail, hydrate, and the
hasForm guard to ensure the hydrate call is only reached for true successful
detail loads so server values rehydrate correctly.

---

Duplicate comments:
In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`:
- Around line 4-5: The file imports the internal type AppRouterInstance from
"next/dist/…" which exposes an unstable public surface; replace that dependency
by declaring and using a minimal RouterLike type in useController.tsx that only
includes the methods you actually use (push, replace, back) and update function
signatures/params that currently reference AppRouterInstance to use RouterLike
instead; keep the existing ReadonlyURLSearchParams import from
"next/navigation", and apply the same replacement wherever AppRouterInstance is
referenced (e.g., the handler/props around the useController function and any
variables assigned that type).
- Around line 136-140: The delete handler handleDeleteGroup currently awaits
deleteGroup() (a mutateAsync) without try/catch, causing potential unhandled
rejections and incorrect post-success actions; wrap the call to deleteGroup() in
a try/catch (or use .catch) and only call showToast('그룹이 삭제되었습니다.') and
router.back() when the mutation resolves successfully, and in the catch branch
log/show an error (e.g., showToast with failure message) to prevent routing on
failure; apply the same fix pattern to the edit/update handlers that call
mutateAsync (referenced mutate functions around lines after handleDeleteGroup)
so all destructive/mutation paths handle rejection explicitly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6bf44a8e-97d0-4572-a7ef-677163f2a6ee

📥 Commits

Reviewing files that changed from the base of the PR and between 26e1435 and 74a3f3f.

📒 Files selected for processing (2)
  • apps/admin/src/app-pages/group-management/detail/model/getStickyButtonConfig.ts
  • apps/admin/src/app-pages/group-management/detail/model/useController.tsx

Comment on lines +57 to +73
const hasForm = useGroupFormStore((s) => s.forms[formKey] != null);
const draft = useGroupFormStore((s) => s.forms[formKey]?.draft);
const dirty = useGroupFormStore((s) => s.forms[formKey]?.dirty ?? false);
const isValid = useGroupFormStore((s) => s.isValid(formKey));
const canSubmit = dirty && isValid;

const hydrate = useGroupFormStore((s) => s.hydrate);
const removeForm = useGroupFormStore((s) => s.removeForm);
const resetDraft = useGroupFormStore((s) => s.resetDraft);

const setGeneration = useGroupFormStore((s) => s.setGeneration);
const setGroupType = useGroupFormStore((s) => s.setGroupType);
const setGroupName = useGroupFormStore((s) => s.setGroupName);
const setGroupIntroduction = useGroupFormStore((s) => s.setGroupIntroduction);

const pickLeader = useGroupFormStore((s) => s.pickLeader);
const removeMember = useGroupFormStore((s) => s.removeMember);
Copy link
Contributor

Choose a reason for hiding this comment

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

필요한 값만 구독해 불필요한 리렌더를 줄이려는 의도가 잘 드러나는 코드인거 같네요!!
다만 구독하는 값이 많다 보니 코드가 다소 길어 보이는 것 같습니다.

hydrate, removeForm과 같은 액션 함수들은 일반적으로 참조가 변경되지 않는 값들이라, shallow 비교를 활용해 묶어서 구조 분해하면 가독성을 조금 더 개선할 수 있을 것 같습니다.

예를 들어 아래와 같이 액션들만 별도로 묶어 가져오는 방식도 고려해볼 수 있을 것 같습니다.

const {
  hydrate,
  removeForm,
  resetDraft,
  setGeneration,
  setGroupType,
  setGroupName,
  setGroupIntroduction,
  pickLeader,
  removeMember,
} = useGroupFormStore(
  (s) => ({
    hydrate: s.hydrate,
    removeForm: s.removeForm,
    resetDraft: s.resetDraft,
    setGeneration: s.setGeneration,
    setGroupType: s.setGroupType,
    setGroupName: s.setGroupName,
    setGroupIntroduction: s.setGroupIntroduction,
    pickLeader: s.pickLeader,
    removeMember: s.removeMember,
  }),
  shallow
);

Copy link
Collaborator Author

@jinj00oo jinj00oo Mar 6, 2026

Choose a reason for hiding this comment

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

좋은 의견 감사드립니다!
shallow는 타입스크립트에서 두 번째 인자를 넘기지 못하도록 추론하는 문제가 있어, useShallow를 사용하는 방식으로 수정하였습니다!

jinj00oo added 3 commits March 6, 2026 19:48
- mutateAsync는 Promise 반환하므로 try-catch
- Next에서 공식 지원하지 않는 의존성 제거
- 잘못된 id로 접근하는 경우 hydrate 방지
@vercel vercel bot temporarily deployed to Preview – surf-web March 6, 2026 11:40 Inactive
Copy link

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

🧹 Nitpick comments (1)
apps/admin/src/app-pages/group-management/detail/model/useController.tsx (1)

214-219: void 연산자 패턴에 대한 코멘트 권장

() => void handleSubmit() 패턴은 Promise 반환값을 명시적으로 무시하는 유효한 방식입니다. 에러 처리가 핸들러 내부에서 완료되므로 안전합니다. 다만 이 패턴이 흔하지 않아 다른 개발자가 혼란스러워할 수 있으니, 간단한 주석을 추가하거나 더 명시적인 래퍼 함수를 고려해볼 수 있습니다.

💡 대안 예시
// 옵션 1: 주석 추가
const { openSaveEditAlert, openDeleteAlert, openGoBackAlert, openPickLeaderAlert } = useAlerts({
  // void를 사용하여 Promise를 명시적으로 무시 (에러는 핸들러 내부에서 처리)
  onSubmitEdit: () => void handleSubmit(),
  onDeleteGroup: () => void handleDeleteGroup(),
  onLeavePage: handleLeavePage,
});

// 옵션 2: 헬퍼 함수 (프로젝트 전반에서 사용할 경우)
const fireAndForget = <T extends (...args: never[]) => Promise<unknown>>(fn: T) => 
  (...args: Parameters<T>) => void fn(...args);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`
around lines 214 - 219, The arrow wrappers `() => void handleSubmit()` and `()
=> void handleDeleteGroup()` silently discard returned Promises which is legal
but uncommon; update the `useAlerts` props to make this intent explicit by
either adding a brief inline comment near the `onSubmitEdit` and `onDeleteGroup`
assignments stating that the Promise is intentionally ignored and errors are
handled internally, or replace the pattern with a small, shared `fireAndForget`
helper wrapper and use it when passing `handleSubmit` and `handleDeleteGroup`
into `useAlerts` so the intent is clear (refer to `useAlerts`, `handleSubmit`,
`handleDeleteGroup`, and `fireAndForget`).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`:
- Around line 42-43: The current conversion Number(id) can yield NaN which will
make formKey become "NaN" and break the form store; replace the naive cast with
a validation: parse and check the result (e.g., use parseInt/Number and
Number.isFinite or !isNaN) when computing groupId, set groupId to undefined if
invalid, and compute formKey as mode === 'create' ? 'create' : (groupId !==
undefined ? String(groupId) : handle-invalid-id), where "handle-invalid-id" is
either a stable fallback key or trigger an early error/redirect; update the
logic around groupId and formKey in useController.tsx to use these validated
values.

---

Nitpick comments:
In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`:
- Around line 214-219: The arrow wrappers `() => void handleSubmit()` and `() =>
void handleDeleteGroup()` silently discard returned Promises which is legal but
uncommon; update the `useAlerts` props to make this intent explicit by either
adding a brief inline comment near the `onSubmitEdit` and `onDeleteGroup`
assignments stating that the Promise is intentionally ignored and errors are
handled internally, or replace the pattern with a small, shared `fireAndForget`
helper wrapper and use it when passing `handleSubmit` and `handleDeleteGroup`
into `useAlerts` so the intent is clear (refer to `useAlerts`, `handleSubmit`,
`handleDeleteGroup`, and `fireAndForget`).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eabae0d7-0916-4e9b-a3d9-07d1ab8d4b95

📥 Commits

Reviewing files that changed from the base of the PR and between 74a3f3f and 719551f.

📒 Files selected for processing (1)
  • apps/admin/src/app-pages/group-management/detail/model/useController.tsx

Comment on lines +42 to +43
const groupId = id ? Number(id) : undefined;
const formKey = mode === 'create' ? 'create' : String(id);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

id 파라미터 유효성 검증 필요

Number(id)가 유효하지 않은 문자열에 대해 NaN을 반환할 수 있습니다. NaNundefined와 다르게 truthy 체크를 통과하지만 숫자 연산에서는 실패합니다. 이로 인해 formKey"NaN"이 되어 예기치 않은 폼 스토어 동작이 발생할 수 있습니다.

🛡️ 유효성 검증 추가 예시
- const groupId = id ? Number(id) : undefined;
- const formKey = mode === 'create' ? 'create' : String(id);
+ const parsedId = id ? Number(id) : undefined;
+ const groupId = parsedId && !Number.isNaN(parsedId) ? parsedId : undefined;
+ const formKey = mode === 'create' ? 'create' : groupId ? String(groupId) : 'invalid';

또는 잘못된 id가 전달되었을 때 조기에 에러를 처리하는 방식도 고려해볼 수 있습니다.

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

In `@apps/admin/src/app-pages/group-management/detail/model/useController.tsx`
around lines 42 - 43, The current conversion Number(id) can yield NaN which will
make formKey become "NaN" and break the form store; replace the naive cast with
a validation: parse and check the result (e.g., use parseInt/Number and
Number.isFinite or !isNaN) when computing groupId, set groupId to undefined if
invalid, and compute formKey as mode === 'create' ? 'create' : (groupId !==
undefined ? String(groupId) : handle-invalid-id), where "handle-invalid-id" is
either a stable fallback key or trigger an early error/redirect; update the
logic around groupId and formKey in useController.tsx to use these validated
values.

@jinj00oo jinj00oo merged commit 9e72aa2 into develop Mar 9, 2026
8 checks passed
@jinj00oo jinj00oo deleted the refactor/group-management-page branch March 9, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] 어드민 회원 그룹 관리 페이지 구조 개선

2 participants