Skip to content

Release v1.1.0 - #105

Merged
jh-lee-cryptolab merged 27 commits into
mainfrom
release/v1.1.0
Aug 13, 2026
Merged

Release v1.1.0#105
jh-lee-cryptolab merged 27 commits into
mainfrom
release/v1.1.0

Conversation

@jh-lee-cryptolab

@jh-lee-cryptolab jh-lee-cryptolab commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

포함 내용

기능

  • 이중 언어 UI: 정적 en/ko 로케일 + 언어 토글, i18next 기반 라이브 전환(리로드 플래시 없음)
  • 컴포넌트 단위 인라인 문자열, 중앙화된 copy 상수, 드롭다운 옵션까지 전면 로컬라이즈
  • 브랜드 표기를 대문자 RUNE로 통일

리팩터

  • MemberDetailDrawer / UsersPage / TreeDetailView를 hooks + views로 분해
  • 포크된 모달 통합, 모달 스타일, 테이블 헤더, 폼 라벨, aria-label, 페이지 타이틀 중앙화
  • 선택/페이지네이션/배치 실패 훅 공용화, 파일 구조 컨벤션 정렬, dead code 제거

수정

  • 비소유자 로그인 soft-block, 탈퇴 세션 만료 처리
  • drawer가 refetch된 멤버십을 렌더, 팀 트리 빌드 메모이제이션
  • Node >= 22.22.0 요구(react-router@8 engine 제약)

성능과 스타일

  • KST Intl formatter 호이스팅, 페이지네이션 5페이지 윈도우, 모달 푸터 간격 gap-4 통일

What's included

Features

  • Bilingual UI: static en/ko locales plus a language toggle, with live i18next switching (no reload flash)
  • Localization coverage extended to component-level inline strings, centralized copy constants, and dropdown options
  • Brand spelling standardized to all-caps RUNE

Refactors

  • MemberDetailDrawer, UsersPage, and TreeDetailView decomposed into hooks + views
  • Forked modals unified; modal styles, table headers, form labels, aria-labels, and page titles centralized
  • Shared selection/pagination/batch-failure hooks extracted, file structure aligned with conventions, dead code removed

Fixes

  • Soft-block non-owner login and expire withdrawn sessions
  • Drawer renders refetched memberships; team tree build memoized
  • Require Node >= 22.22.0 (react-router@8 engine constraint)

Performance and style

  • KST Intl formatter hoisted, pagination windowed to 5 pages, modal footer spacing unified on gap-4

jh-lee-cryptolab and others added 26 commits July 22, 2026 14:05
The console single-admin lock hard-refused a non-owner at the OAuth
callback (admin_locked), so once the owner withdrew their cloud account
the console was effectively bricked: a new account could not even learn
who owned it or switch. Separately the local session was validated only
by its 12h TTL, so a withdrawn owner kept reporting logged-in from the
cached snapshot.

- login.go: a non-owner login now succeeds (soft block) and receives a
  console session, but the org-admin registrar runs only for the owner,
  so a non-owner never takes over admin authority. Remove failOwnerLocked
  and the admin_locked bounce.
- service.go: GET /console/session revalidates a live session against the
  cloud (GET /me). A 401 (nil principal, nil error) is the signal a
  withdrawn account produces and expires the local login on the spot. The
  response reports is_owner (+ owner_email) so the app can gate.
- frontend: RequireAuth shows a new OwnerLockedNotice for is_owner:false
  instead of the admin shell; TSession carries is_owner/owner_email.

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

react-router@8.2.0 declares engines.node >=22.22.0, so a fresh install on
Node 20 fails with EBADENGINE/notsup despite our engines field promising
>=20 works. Align the declared engine with what the dependency tree
actually requires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(frontend): require Node >=22.22.0 to match react-router@8 engine …
Move every user-visible Korean string into src/locales (ko.ts verbatim,
en.ts translated). The locale is chosen once at page load (localStorage
rc_lang override, else browser language) and exported as L; components
read plain constants — no i18n library, no runtime lookup. ko.ts is
typed against en.ts's shape so the two languages cannot drift apart
silently. Parameterized strings are per-language functions so Korean
counters and English word order each read naturally.

A globe LanguageToggle (labeled with the language it switches to) sits
in both navbars — signed-in and signed-out — and persists + reloads.

Tests are pinned to Korean in setup.ts since the suite asserts the
original copy; locales.test.ts covers the English side and structural
parity. Dev-only fixtures (UITestPage, teamsDummyData) and code
comments intentionally keep their Korean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Normalize brand 'Rune'/'rune' → 'RUNE' in the en/ko locale tables
(account-required, invite status, update notice). Technical identifiers
are unaffected (none present in the locale files).
Drive language state with plain i18next; L becomes a Proxy over the active
locale table so all L.x.y call sites and the compile-time-typed tables are
unchanged. A LocaleProvider subscribes to i18next's languageChanged and
remounts its subtree, so switching re-renders in place instead of
window.location.reload() — removing the switch flash. setLanguage calls
changeLanguage (keeps localStorage persistence); LanguageToggle reads the
live language via getLanguage(). Locale test compares L by structure since
it is now a Proxy.
CI installs with --frozen-lockfile; add the i18next entry so the lockfile
matches package.json.
- 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>
Module-scope constants that captured L.* froze at the load-time language,
because LocaleProvider's key={lang} remount re-runs render but not module
top-level code. Make the shared containers resolve per-access and inline
the rest at their render sites:

- styleConstants: MEMBER/INVITATION/WORKSPACE_STATUS_VAR labels via a
  statusVar() getter helper (status badges)
- commonConstants: BTN_TEXT/MODAL_TITLES as forwarding proxies over
  L.btn/L.modal; NAV_LIST titles as getters (buttons, modal titles, nav)
- inline L.* at render sites for team-name hint, invite/delete/role/
  membership messages, username error; batchReason() helper in the drawer
Phase 1 of the i18n re-integration. release/v1.0.6 refactored the same
components this branch localized (decomposed drawers/pages into hooks+
views, unified forked modals, centralized copy into constants), so the
28 conflicts were resolved by taking release's version wholesale — the
newer architecture is the baseline. The i18n-only files (locales/en.ts,
ko.ts, index.ts, LocaleProvider, LanguageToggle) are non-conflicting and
survive. Two forked modals release deleted (teams/RemoveMembershipModal,
teams/RoleChangeConfirmModal) are dropped to match.

Phase 2 (follow-up commit) re-wires the i18n layer onto release's
centralized copy and localizes the new hook/view files. The tree is
intentionally un-localized (release Korean) at this commit.
Re-integrate the i18n layer onto release/v1.0.6's architecture, starting
with the highest-leverage surface — the centralized copy constants, which
cascade to every screen:

- mount LanguageToggle in the release Navbar; localize the workspace badge
- commonConstants: BTN_TEXT, PAGE_TITLES, MODAL_TITLES, TABLE_HEADERS,
  INPUT_LABELS, PLACEHOLDERS, ARIA_LABELS, FEEDBACK_TEXT, NAV_LIST → live
  getters/functions over L.*
- noticeConstants, errorConstants (computed-key getters), styleConstants
  (status badges via statusVar getters) → live over L.*
- teamConstants/email/username bare exports inlined at call sites (live)

All copy re-resolves on a live switch (getters/proxies), so no reload flash.
Component-level inline strings (pages, modal bodies) are the remaining pass.
tsc clean, 333/333 tests pass, build succeeds.
Complete the re-integration: every user-visible Korean string in the
components/pages now reads from the locale tables, so a live language
switch turns the entire UI English (not just the shared chrome).

- 25 files auto-localized (JSX text, attributes, string props) via the
  Korean→key reverse index from ko.ts
- interpolated strings wired to the locale's function keys (footers,
  aria-labels, confirm-dialog bodies, member-count headings, team meta,
  invite notices with the embedded status chip)
- inlined the remaining frozen module-const message captures so they
  re-resolve live (send/remove/role/delete failure copy)

Only code comments and regex character classes retain Korean. tsc clean,
333/333 tests pass, build succeeds.
The Users and Sessions filter/sort option arrays were module-scope
constants whose labels read L.* / getters — so they froze at the
load-time language and the dropdowns stayed in that language after a
live switch (the rest of the page switched, leaving the page looking
half-translated). Convert them to builder functions called in the
component body so they re-resolve on every render.

tsc clean, 333/333 tests pass, build succeeds.
The mock server seeds English sample team names (Platform, Data, …).
Add a demo-only L.demoTeams map + localizeTeamName() so those sample
names follow the language toggle on the Team Management surfaces
(tree, org chart, team card, pickers). Real, customer-created names
are not in the map and render verbatim — team names are user data and
must never be auto-translated in production; drop this map when the app
talks to a real backend.

Also aligns nav.sessions to the wireframe (세션 관리 / Session Management).
The delete-confirm tests type the displayed (localized) name. tsc clean,
333/333 tests pass, build succeeds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jh-lee-cryptolab
jh-lee-cryptolab merged commit 208b130 into main Aug 13, 2026
1 check passed
@jh-lee-cryptolab
jh-lee-cryptolab deleted the release/v1.1.0 branch August 13, 2026 23:11
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.

4 participants