Skip to content

refactor(frontend): constants, shared hooks, component decomposition, and structure alignment - #103

Open
geuna0204 wants to merge 11 commits into
release/v1.0.6from
refactor/frontend-cleanup
Open

refactor(frontend): constants, shared hooks, component decomposition, and structure alignment#103
geuna0204 wants to merge 11 commits into
release/v1.0.6from
refactor/frontend-cleanup

Conversation

@geuna0204

Copy link
Copy Markdown
Contributor

개요

frontend 전면 코드 리뷰 후 진행한 리팩토링입니다. 커밋 11개, 전 구간 tsc/eslint/vitest 그린 유지 (테스트 313 → 329개).

주요 변경

죽은 코드 제거

  • teamsDummyData.ts(468줄, import 0) · alertStore(소비처 0) 삭제, TreeDetailView의 더미 팀 ID 제거
  • /ui-test 라우트를 DEV 전용 lazy import로 게이트 — 1,084줄 showcase가 프로덕션 번들에서 제외

상수화 (문구/값의 단일 소스)

  • apiConstants — wire 값 6종(상태/역할/에러 코드), 타입은 상수에서 파생 + styleConstants 상태 맵에 satisfies 고정
  • errorConstants / noticeConstants — 파일마다 재선언되던 에러 문구 맵·showNotice 문구 통합
  • TABLE_HEADERS·INPUT_LABELS·PLACEHOLDERS·ARIA_LABELS·PAGE_TITLES·FEEDBACK_TEXT·DEFAULT_PAGE_SIZE — 이미 발생해 있던 문구 드리프트 3건("전체 선택"/"전체선택", 이메일 오류 문구 마침표, 모달 body gap) 해소

버그 수정

  • MemberDetailDrawer가 상세 쿼리/뮤테이션 재조회 결과를 무시하던 문제 — 서버 진실은 props에서 파생하고 편집분만 diff 스테이징하도록 재설계 (회귀 테스트 포함)
  • UsersPage가 클램프 전 페이지로 범위 밖 요청을 보내던 문제 — useServerPagination으로 통일

공유 훅 레이어 (참고 프로젝트 es2-cloud-fe 패턴)

  • usePageScopedSelection · useServerPagination · useBatchFailureModal — 3개 파일에 갈라져 있던 중복 구현 통일 (단위 테스트 14건)
  • useStagedRoleEdits · useTeamCrud · useMembershipDrafts · useUserBatchActions — 대형 컴포넌트의 상태 머신 추출

컴포넌트 분해

  • TreeDetailView 720 → 428줄, MemberDetailDrawer 660 → 362줄, UsersPage 580 → 370줄
  • teams/users에 포크돼 있던 확인 모달 2쌍을 users 버전으로 통합 — SC-06 역할 변경/멤버십 제거 결과가 모달 내부(E-1/E-2)에 표시되도록 wireframe v0.12 계약으로 정렬

구조 정합

  • store 단일 홈(state/store/), TTeamNode 이름 충돌 해소(TTeamViewNode), workspace 타입 분리, components/ 내 순수 .ts를 utils/·constants/로 재배치, 단일 파일 폴더(drawer/·toast/) 정리

성능

  • TreeDetailView 트리 빌드 memoize + O(n²)→O(n), Intl 포매터 모듈 상수화, Pagination 슬라이딩 5페이지 윈도우

검증

  • tsc -b / eslint . / vitest run 329 테스트 전부 통과, 프로덕션 빌드로 번들 제외 확인

geuna0204 and others added 11 commits July 28, 2026 10:42
- Delete teamsDummyData.ts (468 lines, zero imports) and its orphaned
  wire-type copies
- Delete unused alertStore + test; its role is served by noticeStore
  (drop now-orphaned TAlert type)
- Drop dummy team ids (t_a/t_e) from TreeDetailView fallback selection
  and default expansion — they never match real API data
- Route /ui-test only in dev builds via a DEV-gated lazy import so the
  1,084-line showcase page and its chunk stay out of production

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…py in constants

Add three constants modules as single sources for strings that were
hardcoded per file, so a wording or wire-value change is a one-line
edit and typos become compile errors:

- constants/apiConstants.ts — wire-contract vocabulary
  (INVITATION_STATUS, SESSION_STATUS, WORKSPACE_STATUS,
  SYSTEM_UPDATE_STATE, TEAM_MEMBER_ROLE, ERROR_CODES); the matching
  union types are now derived from these objects, and the status→label
  maps in styleConstants are pinned with satisfies so a vocabulary
  change fails compilation until the labels follow
- constants/errorConstants.ts — error-code→copy maps (TEAM_REASON,
  BATCH_REASON, ADD_MEMBER_REASON, BATCH_REASON_FALLBACK), replacing
  five per-file re-declarations; the duplicate-team-name copy is shared
  with the create/rename modals' client-side check. Unmapped-code
  fallback behavior is unchanged and now documented per call site
- constants/noticeConstants.ts — showNotice copy per flow
  (NOTICE_TEXT, 11 flows, 20 call sites), collapsing cross-file
  duplicates (resend invitation, create team, remove membership);
  titles reference MODAL_TITLES where the notice reports that modal's
  action

Also add DEFAULT_PAGE_SIZE to commonConstants (was declared as
PAGE_SIZE=10 in three files) and drop UITestPage's duplicate
ROLE_OPTIONS in favor of the teamOptions export. MemberStatus chip
props stay literal on purpose — they are chip vocabulary behind the
CHIP_STATUS seam, not wire values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e build

MemberDetailDrawer copied user.memberships into useState once on mount,
so the fresher GET /users/{id} payload and every post-mutation refetch
were silently ignored in favor of locally patched rows. Server truth
now flows straight from props and the drawer stages only the user's own
edits (pendingRoles map + checkedIds set), re-applied as a diff on top
of whatever the server currently says — the manual state mirroring
after add/remove/role mutations is gone, since those mutations already
invalidate the user detail/list queries. Batch partial-failure behavior
is unchanged: failed targets keep their staged edits for a retry.
Adds a regression test: refetched memberships render without a remount
and staged picks survive.

TreeDetailView rebuilt the whole team tree on every render (each
keystroke/checkbox/staged edit) with an O(n^2) filter-per-parent scan.
flatById/teamNodes are now memoized on the teams array and the build
runs as a single pass over a children index; ancestorIds reuses the
memoized map instead of building its own per render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, and page titles

Extend commonConstants with the remaining repeated UI copy so a wording
change lands in one place:

- PAGE_TITLES — page/section vocabulary; the nav, every page's section
  aria-label, and MODAL_TITLES.workspaceManage now derive from it
- TABLE_HEADERS — column headers shared by list tables, modal tables
  (ModalTable head arrays + hand-rolled th), and sort-option labels.
  TreeDetailView's 역할 vs 권한 drift is kept verbatim as roleAlt with
  a comment pending a copy decision
- INPUT_LABELS / PLACEHOLDERS — invite & add-member form copy
  (팀 선택 4x, 권한 선택 4x, 추가할 팀 없음, ...)
- ARIA_LABELS — fixes the 전체선택/전체 선택 drift to one spelling
- FEEDBACK_TEXT — the refresh-retry line repeated on three pages

Add utils/email.ts (EMAIL_PATTERN + EMAIL_FORMAT_ERROR) following the
username.ts pattern-with-copy convention — the regex was declared twice
and the error copy had already drifted on punctuation; both modals now
share one with-period version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… hooks

Add the UI-state hook layer the pages were missing (the reference
project's useXxxForm/behavior-hook pattern) and point all consumers at
it:

- usePageScopedSelection — the Set-of-ids checkbox selection duplicated
  across UsersPage, TreeDetailView, and MemberDetailDrawer (toggleOne/
  toggleAll/clearSelection + setSelectedIds for batch reconciliation)
- useServerPagination — one pagination state machine replacing three
  divergent implementations, standardized on the strictest variant:
  totalPages tracks the last response as state and the returned page is
  clamped BEFORE the query call, so an out-of-range request never fires.
  Fixes UsersPage passing the raw (unclamped) page to the query for one
  cycle, and gives TreeDetailView clamping it never had. Callers report
  totals back via useSyncPaginationTotal
- useBatchFailureModal + toBatchFailureRows — the partial-failure modal
  state and failed→{label, reason} mapping repeated five times across
  three files; the per-flow fallback policy (generic retry copy vs raw
  code) stays a caller choice

Each hook ships with unit tests (14 new).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re table state rows

- Delete teams/RoleChangeConfirmModal and teams/RemoveMembershipModal —
  TreeDetailView now uses the users versions, which were designed for
  reuse (subjectLabel/targets props, ModalTable). Kills the hand-rolled
  duplicate modal-table styles and the th padding drift (py-2 vs
  py-1.5). Intended UX change on SC-06: role-change/removal results now
  render inside the confirm modal (wireframe v0.12 states E-1/E-2),
  matching the SC-13 drawer; partial-failure handling and staged-edit
  retention are unchanged. Tests updated to the new contract
- Add MODAL_STYLE_VAR (message/body/footer/footerCompact) to
  styleConstants and replace 27 class literals across 12 modal files;
  resolves the body gap-4/gap-5 drift. The two footer spacings in
  active use are kept as named variants pending a design pass
- Add TableLoadingRow/TableEmptyRow beside TableErrorRow and replace
  the inline state rows in SessionsPage/UsersPage/TreeDetailView,
  resolving the py-6/py-8 and text-faint/text-muted-foreground drift
- Drop the now-orphaned NOTICE_TEXT.roleChange entry and
  removeMembership.failure (both render in-modal now)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure moves/renames — no runtime behavior change:

- Single store home: src/stores/* → src/state/store/ (reference
  convention), stores/ folder removed, 20 import sites updated
- Resolve the TTeamNode name collision: the recursive UI tree node is
  now TTeamViewNode in teamTypes; TTeamNode remains only the flat wire
  row, so the same name can no longer resolve to two shapes
- Split workspace types out of commonTypes into types/workspaceTypes;
  commonTypes keeps only genuinely cross-cutting shapes
- Relocate non-component .ts out of components/: teamHierarchy and
  invitePreview → utils/; teamOptions split into
  constants/teamConstants (pattern/rule text/ROLE_OPTIONS) +
  utils/buildTeamOptions; memberStatusMap → constants/userConstants
- Fold single-file folders: drawer/MembershipRow → components/users
  (its only consumer domain), toast/ToastContainer →
  components/elements (reference keeps Toast in elements)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onents

720-line component → 428-line composition plus five focused modules,
behavior preserved (all existing tests pass unchanged):

- hooks/useStagedRoleEdits — the staged role-edit machine (pendingRoles/
  savedRoles, un-stage on picking the base value back, partial-failure
  reconciliation committing only what succeeded)
- hooks/useTeamCrud — create/rename/delete orchestration with the
  TEAM_REASON error mapping and success notices; TeamsPage's duplicated
  empty-state create flow now uses it too, removing the last
  near-verbatim logic duplication between the two files
- components/teams/TeamCard, TeamMembersToolbar, TeamMembersTable —
  pure views; staging/selection state stays in the parent's hooks
- buildTeamNodes/findTeamNode/ancestorIds move to utils/teamHierarchy
  beside the existing tree lookups

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ooks and views

Behavior preserved (all existing tests pass unchanged):

- hooks/useMembershipDrafts — the SC-13 membership machine extracted
  from the drawer: props-derived rows with diff staging (pendingRoles/
  checkedIds), the add-team flow, and the role-change/removal confirm
  reconciliation incl. the batch-failure surface. The two 25-line async
  confirm closures that lived inside modal JSX become one-line handler
  references
- components/users/MembershipSection — the 소속 팀 block (table +
  action bar + add-picker row) as a pure view
- hooks/useUserBatchActions — the SC-11 bulk flows (invite/resend/
  batch delete) with their notice/batch-failure wiring; selection
  reconciliation and drawer close-out stay caller-injected
- components/users/UsersToolbar, UserRow — pure views;
  membershipSummary moves next to its only consumer

Drawer: 660 → 362 lines, UsersPage: 580 → 370. Every page component
is now composition over the shared hook layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pages

- formatDate constructed a new Intl.DateTimeFormat on every call — one
  of the costlier Intl operations, running in every table timestamp
  cell on every render. The formatter options never change, so it is
  now a module-level constant
- Pagination rendered one button per page unbounded; the session
  history table grows without bound, so hundreds of pages meant
  hundreds of buttons. It now shows a sliding 5-page window centered
  on the current page, clamped at both ends so the button count never
  jumps while paging (1→[1..5], 4→[2..6], 9 of 10→[6..10]); page
  counts of 5 or fewer render unchanged. Window/clamp regression
  tests added

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The team/workspace confirm modals spaced their button rows with gap-2
while the users flows used gap-4 — since the modal-fork unification both
spacings could appear on the same screen (SC-06). Drop the
footerCompact variant and point its seven call sites at
MODAL_STYLE_VAR.footer (gap-4, items-center).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jh-lee-cryptolab

Copy link
Copy Markdown
Contributor

Need to change base branch to release/v1.0.6

@esifea
esifea changed the base branch from main to release/v1.0.6 August 5, 2026 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants