관리자 페이지 barbelic-docs 이관 — 독립 셸·정리·데스크톱 제거 (2026-08-22)
- 기간: 2026-08-21 ~ 08-22 (세션 1개, 오너 지시 5회로 단계 전환)
- 랜딩: PR #551(Phase 1·2 독립 셸 + docs 동봉) · #560(관리자 대리 인입, 마이그레이션
20260821330000+ 엣지 2종) · #569(카탈로그 정규화·Wodup 매핑 단방향·상세 라벨 정리) · #571(로드 실패 원인 표시) · #575(카탈로그 어댑터 owner 규칙 수리) · #577(Phase 3 데스크톱 제거, 구 #554 대체). Production 적용은 #560 1건(마이그레이션·엣지), 나머지는 프론트·문서 - 정본:
docs/process/deployment-pipeline.md"관리자 셸" 절(빌드·배포·오너 콘솔),docs/contracts/desktop-screens-props.mdUiDesktopAdmin 절(화면 계약),docs/product/prd.md5.11 - 셸:
admin/index.html+vite.admin.config.mjs→src/react/vite/admin/{main,AdminStandaloneRoot, adminCatalogStore,adminShellView}; docs 빌드vitepress build && node scripts/build-admin.mjs; 주소https://barbelic-docs.vercel.app/admin/
1. 배경
관리자 페이지(종목 카탈로그·계열·디테일·Wodup 매핑·Wodup 인입·운영 콘솔)는 데스크톱 앱의 URL 전용 라우트(/admin)와 사이드바 드로어로 살아 있었고, 앱 컨트롤러가 만드는 adminRuntime (gymData·reloadRemoteData·ensureExerciseCatalog·toast…)에 기생했다. 오너는 이 페이지를 문서 사이트(barbelic-docs, VitePress) 쪽으로 "완전히" 옮기고 싶어 했다 — 운영 도구를 서비스 앱에서 떼어내고, 데스크톱 앱 번들에서 관리자 코드를 빼기 위해서다.
오너 지시는 다섯 번에 걸쳐 단계가 바뀌었다: "그 구조로 진행"(08-21) → "phase마다 묻지 말고 완료조건 = docs에 완전 이식 + 데스크톱 제거" → "일단 데스크톱은 유지, docs 관리자 신설만 적용"(Phase 3 보류) → "종목 정비 반영 + Wodup 매핑 단방향으로 정리" → "이제 데스크톱에서 관리자 패널 지워줘"(08-22, Phase 3 재개).
2. 문제 제기
docs는 Vue, 관리자는 React다
VitePress 안에 관리자를 "그대로" 옮길 길은 없다. 세 가지 구조를 놓고 봤다: (a) Vue로 재작성(화면 7종·컨트롤러·매핑 페이지 재구현, 비현실적) (b) React 미니 앱(독립 셸)을 docs Vercel 프로젝트 산출물에 동봉 (c) 앱의 /admin 라우트를 iframe으로 끼움(이관이 아니라 창). (b)만이 "소스 이동 0·개명 0"으로 이관이 된다.
관리자 화면은 앱 런타임 7필드를 먹는다
DesktopAdminFeature는 remoteUser·ownerId·isCurrentOwner·gymData·reloadRemoteData· ensureExerciseCatalog·showToast를 앱 부팅에서 받는다. 독립 셸은 앱 부팅이 없으므로 이 일곱을 카탈로그 RPC 하나로 재구성해야 했고, 여기서 이관 뒤 첫 사고(아래 "작업 중 드러난 것")가 났다.
데스크톱 제거는 스택 PR이라 꼬인다
Phase 3(데스크톱 제거)은 Phase 1·2 브랜치 위 stacked PR(#554)로 준비했는데, 오너가 보류하는 사이 #551이 스쿼시 머지되고 main이 통계·인입 트랙으로 여러 번 움직여 base가 어긋났다 (CONFLICTING). 재개 시 "통째 복사"가 아니라 main 위 재이식이 필요했다.
3. 해결 방안
원칙
- 소스는 제자리: 관리자 UI·컨트롤러·CSS(
ui/desktop/screens/DesktopAdmin*.tsx,features/admin/,controllers/admin*.ts,desktop-admin.css)는 이동·개명하지 않고 독립 셸이 제자리에서 import 한다. 데스크톱 앱과 docs 셸이 같은 화면 코드를 쓰므로 정리 PR이 둘에 동시에 반영된다. - 서버 가드는 origin 무관: 권한은
is_lift_guild_admin()하나(JWT app_metadata role 또는lift_guild_admins행). DB 무변경으로 이관한다. - 앱 부팅과 같은 경로: 셸이 카탈로그를 만들 때 앱과 같은 정규화(
setRemoteExercises→exerciseCatalog)를 쓴다 — 셸만의 변종을 만들지 않는다. - 정책 반영은 화면 정리와 함께: 종목 정비(exercise_category 8종·synonym 3층 트리·기록 필드·BRID)와 "Wodup → Barbelic 단방향" 정책을 관리자 화면 라벨·행·토글에 반영한다.
- 재이식은 3-way: 보류됐던 Phase 3는 cherry-pick으로 main 위에 다시 얹고 충돌 파일만 손으로 합친다(겹치는 파일 통째 복사 금지).
접근
Phase 1·2(셸 + docs 동봉)를 한 PR로 먼저 올리고, 오너 지시에 따라 Phase 3은 draft로 보류. 그 사이 오너가 관리자 화면에서 본 증상(UUID 종목명 → "카탈로그를 불러오지 못했습니다")을 실측으로 좁혀 가며 정리·수리 PR을 쌓고, 마지막에 Phase 3을 재이식해 닫았다.
4. 적용한 내용
Phase 1·2 — 독립 셸 + docs 동봉 (#551, de1e3986)
admin/index.html 엔트리와 vite.admin.config.mjs(root = 레포 루트, input admin/index.html, assetsDir admin/assets, outDir dist-admin 또는 BARBELIC_ADMIN_OUT_DIR, cacheDir node_modules/.vite-admin으로 앱 dev 서버와 격리). 셸 = 공용 LoginGate(소셜 로그인) → isCurrentUserAdmin() 게이트 → adminCatalogStore(카탈로그 RPC로 런타임 7필드 대체) → DesktopAdminFeature 제자리 import, 사이드바는 ADMIN_VIEW_TABS 재사용·#view 해시 라우팅. docs 쪽은 docs/scripts/build-admin.mjs(레포 루트 없으면 셸만 스킵), ignoreCommand가 ../src 등을 감시, nav "관리자" 링크. 오너 콘솔 선행 3항(Vercel "Include source files outside of the Root Directory" · Supabase Redirect URLs /admin/** · Node ≥ 22.13)을 배포 문서에 기록. 머지 직전 migration-smoke 레드는 CASE-009 온보딩 e2e의 rest:profiles network_failure 플레이크 — main 리베이스 후 3/3 그린.
관리자 대리 인입 (#560, abd2bf39 · 마이그레이션 20260821330000)
오너 요청("어떤 유저한테 인입할지 선택")으로 인입 탭에 대상 계정 선택기를 붙였다. 배치에 initiated_by_user_id, RLS(wodup_import_batches_insert_admin_on_behalf 등)·storage 정책· admin_search_users_v1 RPC, 엣지 2종(wodup-start-import·wodup-process-import-jobs)은 호출자≠소유자면 is_lift_guild_admin 확인 후 소유자 명의로 처리. Production 적용·프로브 완료.
정리 — 카탈로그 정규화·매핑 단방향·라벨 (#569, 444f42d5)
오너 스크린샷(전체 종목에 UUID·영문명 없음·장비 공란·계열 미분류)의 근본 원인: 셸 스토어가 RPC 원시 행(name_ko/exercise_category…)을 gymData.EXERCISES에 실었는데 lgDesktopAdminProps 는 앱 정규화 형태(name/equip/part…)를 읽어 ko: exercise.name || exercise.id가 UUID로 떨어졌다. 앱 부팅과 같은 setRemoteExercises(items) → exerciseCatalog()로 수리(공용 인덱스도 채워짐). 같은 PR에서 Wodup 매핑을 Wodup → Barbelic 단방향 고정(방향 토글·adminMappingDirToggle 마커·.adm-map-band-dir 제거, 컨트롤러 기본 w2l), 종목 상세/편집기를 현 모델로(장비 → 분류 8종, 별칭 → 교정 별칭 + 읽기 전용 "다른 표기", 기록 필드·출처 행, 검색에 다른 표기 포함). 로컬 스모크: #all 상세 "4-7-8 호흡 / 4 7 8 Breathing / 맨몸 / 호흡", #mapping 밴드 Wodup·Wodup·Barbelic·Barbelic, 토글 0개.
로드 실패 원인 표시 (#571, 3496e634)
오너가 "운동 종목 카탈로그를 불러오지 못했습니다"를 보고했을 때 셸은 원인을 콘솔에만 남겼다. 스토어가 errorDetail(실제 예외 메시지)을 보관하고 에러 게이트가 그대로 노출하도록 바꿔 다음 보고에서 원인 문구를 바로 받았다.
카탈로그 어댑터 owner 규칙 수리 (#575, 876c5224)
표시된 원인: items.708.owner_user_id must be present exactly for user-origin exercises. 오너의 관리자 계정(Google 계정, lift_guild_admins 행)은 그날 13:05 WodUp 재인입을 완주했고, 그 결과 카탈로그 863행 중 external placeholder 145행이 전부 owner_user_id = 인입 계정으로 들어왔다. 클라이언트 어댑터 adaptExerciseCatalogItemRows의 구 규칙("owner는 user 출처에만")이 이 행을 거부해 카탈로그 전체가 실패했고, 같은 어댑터를 앱도 쓴다. DB 제약 exercises_owner_origin_check 는 system ⇒ null, user ⇒ not null, external ⇒ either(08-20 인입 파이프라인부터 owner-scoped placeholder). 어댑터를 제약과 일치시키고(homeFragmentBoundaries 재앵커·pending-changes 사유· exercise-identity-hard-cutover.md 문장 정정), 관리자 계정 Production 페이로드 재생으로 증명 (구 어댑터 708 거부 / 수정 어댑터 863/863 통과). docs 셸·앱 배포 번들 모두 반영 확인.
Phase 3 — 데스크톱 제거 (#577, 4669d94f · 구 #554 대체)
main c6a19afa 위에 Phase 3 커밋 2건을 cherry-pick(3-way). 충돌은 prd.md 5.11(단방향 매핑 문구와 합침)·pending-changes.json(통계 Phase 2/4-1 사유 뒤에 덧붙임) 2파일, 나머지 12파일 자동 병합. 제거 범위: desktopApp.tsx의 관리자 feature lazy 경계·desktopAdminView/ adminFeatureActivated·tab==="admin" 분기·adminRuntime 소비, DesktopSidebar의 '관리자' 항목·ADMIN_VIEW_TABS 서브메뉴, appController의 adminRuntime 블록·내비 options 통로, lgInitialDashboardTabFromLocation의 /admin·?view=dashboard.admin·#dashboard.admin 판정, desktop.css 드로어 규칙. 감사 테스트 6건 재앵커. 앱 빌드에서 관리자 UI 청크 부재 확인, 오너가 데스크톱 서비스에서 관리자 부재를 확인.
주요 결정과 그 근거
- 구조 (b) 독립 셸 — 앱 런타임에 기생하던 관리자를 Vue로 옮길 수 없고, iframe은 이관이 아니다. 같은 소스를 docs 산출물에 두 번째 Vite 엔트리로 싣는 것이 유일한 현실적 경로.
- Phase 3 보류 → 재개를 오너 결정으로 — "일단 데스크톱 유지" 지시로 #554를 draft로 잠그고, 오너가 docs 쪽을 직접 써 본 뒤 "지워줘"로 재개.
- Wodup → Barbelic 단방향 — 인입 재설계(08-19)의 정책(WodUp → 바벨릭 단방향 번역)을 관리자 화면에도 적용. 역방향 토글은 혼동만 낳는다.
- 어댑터를 DB 제약에 맞춤(반대가 아니라) — external placeholder가 owner-scoped인 것은 인입 파이프라인 설계(
docs/data/import-pipeline.md)이고, 클라이언트 규칙이 낡은 쪽이었다. - 셸 에러 게이트에 원인 노출 — 오너가 devtools 없이도 실패 원인을 보낼 수 있어야 진단이 한 바퀴로 끝난다.
작업 중 드러난 것
- Vite root를 하위 폴더(
admin/)로 두면 dev에서../src스크립트 경로가 깨진다 — root = 레포 루트 + rollup input 지정이 정답. - 카카오 로그인 후
www.barbelic.com으로 튕기는 현상은 코드 리다이렉트가 아니라 Supabase Redirect URL allowlist 폴백(Site URL) — docs 도메인/admin/**등록으로 해소(오너 콘솔). - 셸 스토어가 원시 행을 넘기던 결함은 단위 테스트 픽스처(
{id:"bench"}류)가 실제 와이어 형태가 아니어서 통과했다 — 픽스처를 실제 RPC 행 형태로 바꾸고 "정규화된 행을 넘긴다" 계약을 추가. - 주석 속
adm-mapdir리터럴이 dead-css 게이트를 통과시키고 있었다 — 주석을 고치자 비로소 미사용으로 잡혀 함께 삭제. - "오너 계정"을 이메일로 가정하면 틀린다: 관리자는 Google 계정(
lift_guild_admins행·최근 인입 배치user_id)이었고, 다른 계정으로 한 첫 실측은 external 0행이라 멀쩡해 보였다. - Production 읽기 전용 실측은 management API
database/query(SELECT·request.jwt.claims+set_config('role','authenticated')로 특정 계정·역할 시뮬레이션)로 충분했다; 게이트웨이 로그 엔드포인트는 이 프로젝트에서 빈 결과. - 재배포 직후 열려 있던 탭은 이전 해시 자산이 404가 된다(Vercel은 구 배포 자산을 프로덕션 도메인에 남기지 않음) — 증상 보고 시 강력 새로고침을 먼저 권한다.
- 스택 PR은 base 스쿼시 한 번에 CONFLICTING이 된다 — 보류가 예상되면 처음부터 main 기준으로 잘라 두는 편이 낫다.
5. 적용 결과
- Production/배포: docs
https://barbelic-docs.vercel.app/admin/200·에셋 200·nav 링크, 오너 관리자 계정으로 종목 863행 정상 렌더(외부 placeholder 145 포함), Wodup 매핑 단방향, 앱 데스크톱에서 관리자 부재(오너 확인). 마이그레이션20260821330000·엣지 2종 Production 적용. - 검증: PR별 CI scope·verify(·migration-smoke) 그린(#551·#560·#569·#571·#575·#577), 로컬
npm run check전 게이트 통과, 로컬 Supabase 스택 셸 스모크, 관리자 계정 Production 페이로드 재생(863/863), 앱 빌드 관리자 청크 부재. - DB 접촉은 #560 1건뿐. 나머지는 프론트·테스트·문서.
6. 이번 개선으로 향상된 것
관리자 도구가 서비스 앱 밖에 있다
운영 도구는 문서 사이트 프로젝트에 살고, 데스크톱 앱 번들은 관리자 코드를 싣지 않는다. 권한은 여전히 서버 한 곳(is_lift_guild_admin)이 판정한다.
관리자 화면이 현 카탈로그 모델을 말한다
분류 8종·교정 별칭/다른 표기·기록 필드·출처가 상세에 보이고, 매핑은 정책대로 한 방향이다.
인입 계정의 카탈로그가 다시 열린다
external placeholder를 거부하던 클라이언트 규칙이 DB 제약과 같아졌다 — 관리자 셸만이 아니라 앱의 카탈로그 갱신도 같은 수리로 풀렸다.
실패가 화면에 이유를 말한다
카탈로그 로드 실패 게이트가 실제 예외 메시지를 노출해, 다음 사고는 보고 한 번으로 원인이 잡힌다.
남은 것
- docs 커스텀 도메인(docs.barbelic.com 등) 연결 시 Supabase Redirect URLs에 새
/admin/**재등록(오너 콘솔). - synonym 트리 편집기: 관리자 상세의 "다른 표기"는 읽기 전용 — 동등 표기 추가/승격 UI는 카탈로그 트랙 잔여 항목.
- 세션 만료 UX: 셸은 만료 시 로그인 게이트로 돌아간다. 장시간 열어 두는 운영 화면이라면 자동 갱신 실패 시 안내 문구를 검토.