데스크톱 화면 props 계약 (src/react/ui/desktop/**)
작성: 2026-06-10 · C안(멀티패널 워크스페이스) 1차. 모든 컴포넌트는 순수 프레젠테이션 (props-in / 콜백-out), Supabase·services·영속 저장 import 없음. 콜백은 Promise 반환 가능. 공통 데이터 형태(sessionLite/session(full)/exerciseLite/exerciseDetail/profile)는 모바일 계약(session-screen-props.md, records-props.md, profile-screen-props.md)과 동일. (profile 계약은 2026-08-19 실코드 기준 재작성본 — 공통 profile 형태 정의 포함.)
로드 순서: DesktopWidgets.jsx → 각 화면 (화면들이 window.UiDesktopWidgets를 참조). 스타일: desktop/styles/desktop.css 단독 로드(모바일 styles.css 불필요). 루트 엘리먼트에 .dk-app 필요. 변형: data-accent="ocean|teal"(생략=sky), data-dk-density="comfortable"(생략=compact).
DesktopSidebar — 로그아웃 (#1530, v0.17.10)
- 왼쪽 고정 메뉴 하단에
로그아웃버튼을 둔다. 내 정보 화면의 기존 로그아웃도 유지한다. onLogout: () => unknown을 필수로 전달한다.GymDesktopApp은 내 정보와 같은 공통signOut을 연결하며, 버튼은 인증 서비스·저장소에 직접 접근하지 않는다.- 버튼은
type="button", 기존 actionprofile.logout을 사용한다. 마우스·Enter·Space로 실행할 수 있고 탭 이동과 별개의 실행 버튼이다. - 현재 기기 로그아웃·로그인 화면 복귀·계정별 정리는 기존 컨트롤러가 맡는다. 모든 기기 로그아웃이나 계정 삭제로 연결하지 않는다.
UiDesktopHomeScreen (홈 v3 · 세션 피드 + 통계 컬럼 — 2026-07-08)
- 레이아웃: 카드 한 장, 인카드 헤더 없음 — │ 좌 프로필/통계 컬럼(프로필 · 벤치마크 기록 · 연간 잔디 · 들어올린 무게 · 주간 계획) / 우 세션 피드 컬럼(쓰레드 포스트), 넓은 본문에서는 비율 1:1, 좁으면 한 열 · document 스크롤 · 사이 hairline. (내비 라벨 "프로필")
- 프로필(통계 컬럼 최상단, hook
homeProfile):profileNameprofilePhoto?profileShort?+monthStats.volume("이번 달 N kg 들어올림") + 벤치마크 기록bestRecords[{ id, name, nameEn?, glyph?, value, unit, grade?, topPct? }] (최대 8개, 셀 클릭 =pr.open/onOpenExercise).bestRecords는 유일한 기록 입력이며 즐겨찾기 배열로 대체하지 않는다. feed[{ kind:'session', session(lite), title, when(YYYY-MM-DD · 시각 · 소요), duration?, review?, kpi:{ volume, sets, exercises, reps }, mains?:[{ name, nameEn?, sets:[{ load, reps }] }], author?:{ id, name, handle?, photo?, short? } }] —author(2026-08-23 팔로잉 포스트)가 있으면 팔로우한 친구의 카드: 포스트 헤더 아바타/이름을 author로 그린다(없으면 내 프로필) — 세션 포스트만 표시(session 외 kind 무시). 헤더 = 닉네임(profileName) + 완료필 + when, 리뷰가 본문, 기록은 첨부 카드(헤더=세션명(title), 일지 '이 날의 메인 세트' 문법에서 톱세트 accent 제외, max-width 480px) · mains 없으면 푸터에 세션명+KPI.feed가 없거나 비어 있으면 빈 피드를 표시하며activity등 다른 배열로 합성하지 않는다.- 좌 통계:
yearRecap(1월 1일부터 최대 371칸 + KPI: 세션/운동한 날/무게/mainReps?/PR —yearRecapsByYear?{"2025": recap 동형} 공급 시 헤더 ◀▶ 연도 스테퍼) ·youLifted(yearGoal포함) ·weekPlan(항상 7일, 날짜별sessions[], 완료volume/계획estVolume/완료top). 잔디 날짜 상세에는sessionsByDate와fullById를 사용한다. - 현재 계약에서 제거됨:
activitytopExercisestrainingLoadtodaySessionbigThreevolumeTrendpartDistributionfavoritePRsrecentPRsprCardsprCardExercisestodayPlanrecentSessionstodayLabelhomeMonthhomePeriodsplanProgress. UI와 mapper는 이 값을 수용하거나 폴백으로 읽지 않는다. - 콜백:
onOpenSession(lite)onOpenDay(dateIso)onRequestDay(dateIso)onDayModalChange(dateIso|null)onOpenExercise(id)onCreatePlan(dateIso?)onAddPastRecord()onGoVolume(period?). - 마커: view
dashboard.home, hookdashboardView/homeActivity(피드)/homeProfile/homeBestRecords/homeYearRecap/homeYouLifted/homeWeekPlan· actionsession.openpr.open. - 빈 피드(세션 포스트 0 && 초기 로딩 아님, 2026-07-18): 피드 컬럼 정중앙에 웰컴 히어로 카드 1장(마스코트 일러스트 + "○○님, 환영해요!" + CTA
onAddPastRecord/onCreatePlan). hookhomeEmptyFeed. 데이터 0 참조 fixture:desktop/fixtures/desktopFreshFixture.ts. - 데모 데이터:
desktop/fixtures/desktopHomeFixture.ts(dkHomeFixture—feed포함) — 컨테이너 집계 형태 참조용
UiDesktopSearchScreen (세션 검색 — 2026-07-08)
- 레이아웃: 카드 한 장(max-width 1120 중앙) — 헤더(검색 인풋 + "PR 세션만" 토글 + 결과 카운트, hairline) 아래 홈 피드와 동일한 포스트 문법(.dk-h3-post 재사용)의 결과 피드.
sessions[{ session(lite), title, when(YYYY-MM-DD · 시각 · 소요), review?, kpi:{ volume, sets, exercises }, mains?:[{ name, exerciseId?, pr?, sets:[{ load, reps }] }] }] 최신순 —get_session_search가 반환한 최근 완료 세션 최대 120건. 달력 월 캐시를 재사용하지 않는다.- 로딩 호환 props:
loading?initialLoading?loadingLabel?— 컨테이너가 검색 인덱스 적재 상태를 전달한다. 현재 화면에서 카드 로딩 표현을 연결하는 작업은 Claude Design 소유다. initialQuery?initialPrOnly?— 딥링크 초기 필터(PR 상세 "자세히 보기" → 종목명/PR만). 컨테이너는 값 변경 시 key로 재마운트.- 필터링(클라이언트): 쿼리 = 세션 제목 or 종목명 부분일치(히트 종목 행 accent 강조), "PR 세션만" = mains.pr 포함 세션만. PR 종목 행엔 PR 필 표시(홈 피드와 달리 검색 맥락에서는 표시).
- 콜백:
onOpenSession(lite)· 헤더 닉네임/아바타:profileNameprofilePhoto?profileShort? - 마커: hook
sessionSearchView(식별용 신규) · actionsession.open재사용 · view는 미부착 —dashboard.search등록 후 부착(Codex 후속) - 데모 데이터:
desktop/fixtures/desktopSearchFixture.ts(dkSearchSessions)
UiDesktopSessionScreen (운동일지 v2 · 저널 워크벤치 — 2026-07-29)
- 레이아웃: 2컬럼 — 좌 단일 월 스텝퍼 달력(7일+주 합계 열, 날짜 셀 안 세션 엔트리, 이전/다음 월 버튼, 오늘 이동, UUID 종목 필터) / 우 하루 패널(날짜+하루 통계 → 시간순 세션 레일 → 그날의 메인 세트). 주/월 선택 시 우측은
DkRangeView요약으로 바뀐다. 세션 상세와 계획/백필 작성·수정은 각각 중앙 모달로 열린다. todayISO ·selectedDateISO ·sessionsByDate{ iso: [sessionLite] } ·selectedSessionsession(full)|nullfullById{ id: session(full) } — 달력 셀 볼륨·주/월 합계·하루 통계·레일 시간(startTime) 계산에 사용. 상세 미로딩 완료 세션도volume/setCount/exCount요약값을 포함하므로, 달력은exercises가 비어 있어도 명시적volume을 먼저 표시해야 한다 (미주입 시 메타 폴백=lite.summary).visibleMonth{ year, month } — 표시 월 초기값·외부 날짜 이동 동기화. 평범한 월 스텝은 선택 날짜를 바꾸지 않고onEnsureMonth({year,month})로 표시 월과 인접 월 데이터를 요청한다.highlightExerciseIdUUID|null — PR 로그 점프 시 조회 모달의 해당 종목을 강조한다. 이름·slug·뷰 행 ID는 identity 폴백으로 쓰지 않는다.- 달력 필터: 검색 결과에서 UUID 종목 하나를 선택하면 그 종목을 포함한 세션만 남긴다. 세션 ID는 비 UUID를 포함할 수 있는 opaque 값으로 취급한다.
exerciseCatalogfavoritesoneRmsbodyWeightdefaultPlanNamedefaultSessionName— 중앙 계획/기록 컴포저 입력.bodyWeight는 새 draft용 최신 체중이며, 기존 기록 통계는 session에 저장된 체중 스냅샷을 사용한다.exerciseSearchSignals(선택, 이슈 #1101) — 종목 피커 결과 순서 신호(ExerciseSearchSignals | null: 내 종목별 마지막 수행일·종목별 사용자 수). 컴포저의 DkpPicker까지 그대로 전달되며, null이면 인기 등급만으로 정렬한다.onUpdateSession(sessionId, patch)/onDeleteSession(session)— 기록 수정 모달 저장과 원본 세션 삭제. 삭제는 session ID만이 아니라 원본 세션 객체를 전달한다.- 콜백:
onSelectDate(iso)onEnsureMonth(month)onGoToday()onOpenSession(lite)(행 클릭, 같은 행 재클릭=닫기)onCloseSession()onSavePlan(date,draft)onSaveBackfill(date,session)onAddCustomExercise. - 기록 수정은 구 상세 내부 인라인 편집기가 아니라 중앙 컴포저를 사용한다. 읽기 전용 상세는 조회·수정 진입·삭제 확인만 소유한다.
- 마커: view
dashboard.calendar/dashboard.session.detail, hookcalendarScroll/journalDayPanel/journalTimeRail(신규 2개는 식별용)/sessionDetailView/sessionDeleteDialog, action 재사용calendar.today/calendar.month.select/calendar.week.select/calendar.plan.add|backfill.add|plan.editToggle/session.open/session.detail.edit|delete/session.delete.confirm|cancel, fieldworkout.session.note(중앙 에디터 textarea). condition/postCondition 칩 UI는 기능 폐기로 제거(2026-08-24) — draft 필드는 기존 세션 값 보존용으로만 통과 — 신규 action 없음 - 삭제된 구성(구 C안): 미니 달력 · 세션 리스트 pane · 인스펙터 · 상세 내부 인라인 편집기 · 종목 요약 패널 — 활성 기능은 하루 패널·중앙 상세/컴포저 모달로 통합
- 맨몸운동 체중 반영 (2026-07-19): 새 prop
bodyWeight?(kg) — 내장 계획/기록 에디터(UiDesktopPlanEditor)로 전달. 세션(full)에bodyWeight?(저장 당시 체중 스냅샷) + 종목에bwFactor?가 있으면 모든 볼륨/강도/존/탑세트 집계가 유효 무게(load + bodyWeight × bwFactor) 기준으로 계산되고, 세트 표기는 "체중+10kg × 8"이 된다(툴팁 = 환산 kg). 맨몸 세트의 "1RM 대비"(등록 1RM)는 기준 상이로 미표시. 규칙 구현:ui/shared/bodyweightSemantics.ts.- 일 상세 탑세트 TOP 3 (2026-08-23, 오너 D1): 서버
get_calendar_day_summary.top_sets가 유효 무게(stats_effective_load_kg)로 순위를 매기고bodyweight_factor를 실어 준다(유효 무게 0 제외, NULL 뒤로). 행 표기 = 유효 kg × 회, 맨몸 세트는.ld .bw태그 "체중"/"체중+Nkg"(topSetKg/topSetBwTag,ui/shared/bodyweightSemantics.ts), 캡션 "유효 무게 기준", KPI "최고 중량 세트"도 유효 kg. 모바일DayDetailBody동일 규칙. 구 서버 응답(키 없음)은 외부 kg로 폴백 — 배포 순서 무관.
- 일 상세 탑세트 TOP 3 (2026-08-23, 오너 D1): 서버
- 수행 디테일 표기 (2026-07-18 · 07-19 재적용): 종목에
details?: string[]가 있으면 조회 모달·일 메인세트 요약 종목명에 "디테일 종목명" 평문 접두(dksDtlTxt).
UiDesktopPrScreen
- 주요 props:
exercisesfavoritesrecentPRsselectedExercisecardscardExercisesboard와 필수 controlledsearch. 데스크톱 닫기 콜백 이름은 모바일의onCloseExercise와 달리onCloseDetail이다. - 히트맵 날짜 상세는
dayDetailsByDatedayDetailLoadingDatesdayDetailErrorsByDate와onRequestDayDetail(dateIso, exerciseId)의 controlled 계약만 사용한다.selectedExercise.dayDetails같은 선적재 사본으로 폴백하지 않는다. search= 컨테이너가 소유하는 검색 계약:{ input, query, items, results, hasQuery, isPending, debounceMs, onInput(value,isComposing?), onCompositionStart(), onCompositionEnd(value), onCommit(value?), onClear() }.input은 인풋에 즉시 표시하는 값,query는 IME 조합 종료 후 debounce가 끝나 실제 결과에 적용된 값이다.results는 공백 차이와 검색 토큰 순서에 무관하게 매칭된 전체 종목 목록이다.- 콜백:
onOpenExercise(id)onCloseDetail()onToggleFavorite(id)onReorderFavorites(orderedIds)onAddManualPr(input)onOpenSessionFromHistory(log)onOpenReport(id)onOpenDate(iso)onEnsureYear(year)onEnsureHistoryYear(year)onRequestDayDetail(iso,id). - 레이아웃:
selectedExercise가 없으면 종합 기록·즐겨찾기 표와 controlled 검색 드로어를 포함한 보드, 있으면 해당 종목의 전폭 상세를 렌더하는 2-state 구조다. 데스크톱 콤보 상세와 인라인 퀵룩 경로는 사용하지 않는다. - 검색 상태·정규화·필터링은 컨트롤러 소유다. UI는
value={search.input}을 사용하고onChange에서search.onInput(value, nativeEvent.isComposing), composition start/end에서 대응 콜백, Enter에서search.onCommit(), 지우기에서search.onClear()를 호출한다. 결과 목록은 로컬.includes()대신search.results를 렌더하며search.isPending동안 직전 결과를 유지한다. Claude Design은 이 어댑터만 화면에 연결하고 검색 로직을 UI에 복제하지 않는다. (fieldpr.search, actionpr.search.clear) - 마커: view
dashboard.pr/dashboard.pr.detail, hookmobilePrView/prDetailView/prChart/prVolChart, actionpr.open/pr.detail.fav/pr.log.open - PR·탑세트 블록 행은
onOpenSessionFromHistory(log)로 세션 화면을 열고, 훈련 목록 행은 먼저 세트 상세 모달을 연다. 세션 로딩 key는log.sessionId ?? log.id를 정확히 사용한다.
UiDesktopVolumeScreen (훈련 통계 · 종목별 볼륨)
periods{ week|month|quarter|year: periodData } — periodData = { total(표시문자열), unit, rangeLabel, sets, sessions(카운트), days, trend:[{label,rangeLabel,iso,volume,durationSessionCounts,timeSlotCounts}], byExercise:[{id,name,nameEn,volume,sets}](volume 내림차순), sessionList:[{id,date,title,summary,sets,volume,condition,exerciseIds:[id]}] }.durationSessionCounts는[<30,30..<60,60..<90,90..<120,>=120분]5칸,timeSlotCounts는 06:00~24:00의 30분 단위 36칸인 서버 집계입니다.sessionList는 현재 offset 0에서 선택 종목의 수행 태그 분포를 집계할 때 사용하고,sessionsByDate/fullById는 일·주·월 drilldown의 세션·세트 요약을 구성한다. 데스크톱 경계에서trend[].label은 프리뷰 fixture와 동일한 표시값(주YYYY년 M월 N주차, 월YYYY년 M월, 분기YYYY년 N분기, 연YYYY년)이며iso는 정렬·조회용 canonical key로 유지한다.favorites[exerciseId] — 종목 선택 드롭다운의 즐겨찾기 정렬- legacy preview의
startHoursByDate는 더 이상 운영 집계 원천으로 사용하지 않습니다. ④ 꾸준함 카드의 세션 시간 분포와 시간대 리듬은 선택한trend[]행의 위 두 exact vector만 렌더링합니다. initialPeriod"week"|"month"|"quarter"|"year" (생략 또는 미지원 값=month)- 콜백:
onOpenExercise(id)— 종목별 구성의 '기록 보기' → 나의 기록 상세 점프 ·onOpenDate(iso)— 날짜 상세 모달에서 운동일지로 점프.goals/onOpenSession/onOpenRecords/sessionDetailLoadingIds/sessionDetailLoadingLabel은 컨테이너 호환용 예약 prop이며 현재 v4에서는 소비하지 않는다. - 레이아웃(v4): 상단 컨트롤 바(기간 세그 주간/월간/분기/연간
volume.period+ 종목 선택 드롭다운volExerciseSelect: 검색volume.search+즐겨찾기 그룹+전체 종목, 맨 위 "전체 종목"=집계). 선택·드롭다운 open·검색어는 UI-local. 본문 카드는 헤드라인·요약·최고 강도 세트·막대 차트와 하단 3열(종목별 구성/강도 분포/훈련 밀도)로 구성한다. 막대 클릭은 주·월이면 날짜 상세, 분기·연이면 주간·월간 요약 모달을 연다. 이전 기간 값·종목별 추세는 trend 버킷 비율로 화면 합성한다. - 마커: view
dashboard.volume; hookdesktopVolumeView/volPeriodSeg/volExerciseSelect/volExselTabs/volGroupOption/volSaveImage/volHero/volBlock-*/volChartMode/volAllocation/volAllocLens/volIntensity/volRhythm/volRangeModal; actionvolume.period/volume.period.prev/volume.period.next/volume.exercise.select/volume.metric/pr.open.
UiDesktopProfileScreen
profile{name, short, meta, photo} ·bodyMetrics{weight, muscleMass, bodyFat, updatedAt};bodyFat은 체지방률이 아닌 체지방량 kg이다.wodupImportMeta?inbodyImportMeta?최근 인입 요약.- 계정 연결:
socialIdentities?[{provider,label,connected,enabled}] ·socialIdentitiesLoading?·socialIdentityError?·pendingSocialIdentityProvider?·onLinkSocialIdentity(provider). 이미 연결된 identity는 상태만 표시하며 해제 action은 제공하지 않는다. - 콜백:
onUploadPhoto(file)onSaveBodyMetrics({weight,muscleMass,bodyFat})onWodupImport(file)onInbodyImport(file)onExport()onImport(file)onLogout()onDeleteAccount?(). - 성별(2026-09-02, 이슈 #1142):
profile.sex("m" | "f" | "") +onSaveSex(sex). 아이디 카드 아래 "성별" 카드(hookprofileSex,role=radiogroup): 남성/여성dk-btn두 개(현재 값 =primary), 현재 값과 다른 쪽을 클릭하면 즉시 저장. 미설정이면 힌트 "아직 없어요 — 등급이 남성 기준으로 매겨지고 있어요". 저장·토스트·등급 기준표 재적재는 컨테이너(profileController.saveProfileSex)가 맡는다. - 계정 삭제(2026-08-25, #663):
onDeleteAccount전달 시 "계정" 카드에 "계정 삭제" 행 추가. 클릭 →DkScrim2단 확인(안내 목록·내보내기 힌트 → 확인 문구계정 삭제입력, 공유 카피src/react/ui/shared/accountDeletionCopy.ts). 문구 일치 시에만 실행,ok:true면 앱이 로그아웃 상태로 전환, 실패면 인라인 안내 후 재시도. - 저장 계약: 사진/InBody 원본은 사용자 경로 private Storage, 수동 지표와 InBody 지표는
body_metrics에 source/source_ref 기준 upsert. 기존body_fat_percent와 신규body_fat_kg를 혼용하지 않는다. - 마커: view
dashboard.profile, hookmobileProfileView/mobileSocialIdentityConnections/profileSex/profileBodyMetrics/profileWodupImport/profileInbodyImport/desktopSocialIdentityConnections/desktopProfileConsentSettings/importFile/accountDeleteButton/accountDeleteDialog, actionprofile.linkIdentity(data-lg-option-value=<provider>)/profile.export|import|logout/profile.deleteAccount(+.cancel/.continue/.confirm)/profile.consentToggle(data-lg-option-value=<consentType>)/profile.consentDocument, fieldprofile.importFile/profile.deleteAccount.phrase - 개인정보·공개 설정(#1625):
consentSettings?{ items: ConsentPlanItem[](서버 요구 중 선택 동의만,accepted포함), pendingType } ·onSetConsent?(item, accepted)→apply_user_consents_v1. 카드는 선택 동의가 하나 이상일 때만 보인다(서버 요구 전환 전에는 숨김). 끄면 서버가 다른 회원의 열람을 즉시 막고 개인 기록은 그대로 남는다. 필수 동의는 여기서 끌 수 없다(철회 = 계정 삭제와 같은 절차).
UiDesktopOnboarding (첫 사용자 온보딩 — 2026-07-17)
- 흐름:
view="login"(비로그인, Kakao·Google·Apple 버튼) → 컨테이너 OAuth → 신규 가입(프로필 미완성)이면view="setup": 약관(필수 2건) → 신체 정보 → 프로필 확인 → 1RM → 웰컴 → 기존 홈. 기존 회원은 setup 없이 바로 홈으로 간다. - 프로필 스텝에서 provider가 전달한 닉네임·사진을 프리필한다. 사진이 없거나 "기본 이미지로 시작"을 선택하면 성별별 기본 아바타
desktop/assets/avatar_default_m.png/avatar_default_f.png를 사용한다. - props:
view"login"|"setup" ·logoSrc?·providerProfile?{ provider, label, nickname?, photo? } ·loginProviders?[{ provider:"kakao"|"google"|"apple", label }] ·oneRmExercises?[{ id, name, nameEn }] ·welcomeAutoMs?(기본 0) - callbacks:
onSocialLogin(provider)·onOpenTerms(kind)kind="terms"|"privacy" ·onComplete(draft)— draft = { agreements:{terms,privacy}, consentDecisions?:[{ consentType, documentVersion, required, accepted }], nickname, useProviderPhoto, body:{ sex:"m"|"f", height, weight, muscleMass?, bodyFatPct? }, oneRms:[{ exerciseId, name, value, unit:"kg" }] }. - 가입 동의 구조(#1625, 2026-09-15):
consentPlan?·onOpenDocument?(path)·mode?("onboarding"|"consentRefresh")·onSubmitConsents?(decisions)·allowDeferConsents?·onDeferConsents?— 의미는 모바일 온보딩 가입 동의 구조와 같다. 만 14세 확인은 동의 단계 첫 행이며 확인 전 "다음"이 잠긴다(#1637, 2026-09-15 — 로그인 화면의 버튼 잠금은 폐기). 로그인 버튼 아이콘은 각 서비스 정식 아이콘(shared/icons/socialProviderIcons). 동의문 전문은 새 탭(window.open)이며noopener의 null 반환을 팝업 차단으로 보지 않는다. **bodyFatPct는 체지방률 %**이며 내 정보의 bodyMetrics.bodyFat(kg)과 혼용하지 않는다. ·onEnterHome() onOpenServiceInformation()(v0.17.1): 로그인·약관 단계에서 현재 운영자·문의처·공개 범위 안내(/legal/service-info.html)를 브라우저 새 탭으로 연다. 원래 가입 화면을 유지하며noopener의 null 반환을 이유로 현재 탭까지 이동하지 않는다. 동의 상태나 제출 조건을 변경하지 않는다. 문구와 공개 안내의 의미는 모바일 온보딩 운영·공개 안내와 같다.- 마커: 식별용 hook
desktopOnboarding/onboardingLogin/onboardingTerms/onboardingProfile/onboardingBody/onboardingOneRm/onboardingWelcome/onboardingAgeConfirm/onboardingConsentAll/onboardingConsentRow(+data-consent-type) · action(재동의 모드)onboarding.submitConsents/onboarding.deferConsents - 데모:
_preview-local.jsx사이드바 "온보딩 (데모)" 탭 — 좌하단 피커(로그인/가입 스텝/셸로 나가기), 웰컴 종료 시 홈 탭 진입
UiDesktopPlanEditor
dateISO ·initialDraft{ title, exercises:[{ id, name, details?:string[], sets:[{ id, type, load, loadPct?, loadLb?, reps }] }] } | null (수정 진입 시 전달 — 전 종목 확정 상태로 시작) ·exerciseCatalog[{ id, name, nameEn, equip, part, details?:string[] }]. 카탈로그의details는 해당 종목에서 선택 가능한 수행 디테일 후보이고, draft의exercise.details는 사용자가 실제 선택한 값이다.- 복합 종목 (2026-07-17, B빌더+V1스킴칩): exercises[] 항목에
composite?: [{ exerciseId, name, details?:string[] }](순서=수행 순서, 2~5개)가 있으면 복합 종목. 이름은 "1 하이클린 + 1 저크" 자동 파생(구성 변경 시 재파생), 항목exerciseId는""(카탈로그 미등록 — 귀속은 composite[].exerciseId). sets는reps(횟수 합) 옆에parts?: [{ reps?, load?, … }](동작별 값 묶음 — 이슈 #1188 단일 형식). 1세트 = 시퀀스 1회 수행 · 세트당 무게 1개(kg/% 토글 미노출). 생성 = 피커 하단 "여러 종목 묶어서 하기"(보라) → 빌더 모달(hookcompositeBuilderOpen/compositeBuilder), 구성 수정 = 카드 헤더 "종목 수정"(hookcompositeEditOpen) → 동일 빌더 모달(초기값=현 구성, 확정 시 세트 parts를 슬롯 원본 인덱스(src)로 재매핑). 세트행 동작별 횟수 칩 팝오버(hookcompositeMovementReps). 확정 뷰 = "60kg × (3+1)" + 동작별 1RM 대비 열(e1RM/강도 생략) + 통계 귀속 박스. 컨테이너(Codex): 저장 draft의 세트 × composite → 각 종목 (load, reps=parts[i].reps)로 fan-out해 통계·PR 후보 반영하고, 일반 종목과 복합 동작의 선택된details를 세부 종목 행(session_exercise_part)의details에 보존한다(이슈 #1215, 2026-09-04 — 그 전에는composite_meta.exercise_details; 복합 소속은 이제session_exercise층이 관계형으로 든다). 신규 data-lg-action/field 없음(식별용 hook만). (workoutFixture.exerciseCatalog 형태) ·defaultPlanNamestring favorites[exerciseId|name] (선택, 2026-07-14) — picker 검색어 없을 때 프리셋 목록(없으면 전체 폴백)backfill(선택 bool, 2026-07-15) — 완료한 운동 기록 모드: 세트 행에 휴식(초) 셀 추가(빈 값=자유 휴식 → set{ rest:null, freeRest:true }, 입력 시{ rest:초, freeRest:false }), 확정 카드에 휴식 컬럼, 저장 버튼 "기록 저장", 기본 제목 폴백 "운동 기록"oneRms{ [exerciseId]: kg } (선택, 2026-07-15) — canonical exercise UUID로만 조회하며 확정 카드 우측 1RM 대비/e1RM/세트 강도 + kg↔% 입력 토글에 사용- 세트 필드:
loadPct(% 모드 입력 원본) ·loadLb(lb 모드 입력 원본),load는 통계용 kg 파생값. 단위 변경 또는 빈 입력 시 비활성 원본 필드를null로 지워 stale 표시/저장을 막는다. - 콜백:
onSave(draft)onCancel()onAddCustomExercise({name,nameEn,equip,part}) - draft·확정상태(doneIds)·커스텀 종목은 UI-local. 저장 버튼은 종목 ≥1이면 활성(2026-07-15) — 미확정 종목 있으면 저장 대신 해당 종목으로 이동+경고(탭 빨강 필/선택 탭 빨간 테두리)
- 마커: view
dashboard.plan.editor/workout.picker, hookplanEditorView/exercisePickerView/draftList/exerciseSearch/exerciseSearchResults/exerciseDetails/exerciseDetailAdd(equipTabs/partTabs는 07-14 필터 UI 제거로 미렌더), actionworkout.plan.cancel|rename|rename.save|saveBar/workout.plan.exercise.remove(.confirm)/workout.set.add|remove|edit|editDone/workout.picker.select|addCustom/workout.custom.save|cancel/workout.exercise.add, fieldworkout.set.type|load|reps/workout.picker.search, slotworkout.draft.list/workout.set.entryLabel - 드래그(세트 번호·종목 탭)는 포인터/DnD 핸들 — 액션 마커 아님 (모바일과 동일 규칙)
- 컨테이너 라우팅:
calendar.plan.add→ 편집기 열기(initialDraft=null), planned 세션 상세의calendar.plan.editToggle→ 해당 draft로 편집기.onSave→session(status planned) 행 저장(save_session_v5) 후 캘린더 복귀 - SessionScreen 경유 시(compose 모달): 저장 payload에
time("HH:MM", 예정 시작 시간 — 미지정 시 없음)·editSessionId(계획 수정 시 원본 id — update로 처리, 없으면 insert) 추가 (2026-07-15, Codex 후속) - 완료 기록(backfill)도 동일 모달 —
onSaveBackfill(date, { title, date, startTime, endTime, exercises })(2026-07-15): 시작/종료 시간 필수(미입력 시 시간 박스 빨간 하이라이트+저장 중단), 세트에rest/freeRest포함. 구 UiDesktopWorkoutFlow 임베드 흐름 대체. - 자유 기록 엔트리 (2026-07-20):
exercises[]에{ kind:"note", name(제목 ≤40자), note(메모 ≤500자) }— sets 없음, 볼륨/세트/종목 수 집계 미기여. backfill 모드에선 일반 종목과 동일하게review?(종목 리뷰, fieldworkout.exercise.review)를 편집·왕복. 조회(세션 상세 모달)는 메모장 블록(.dk-memo) + 리뷰 말풍선(.dk-ms-review).
v49 계획 편집 배선
- 계획 컴포저 입력은
favorites: string[],oneRms: Record<exerciseId, kg>를 포함한다. 1RM 조회도 canonical exercise UUID만 사용한다. - 계획 수정 저장 draft는
editSessionId를 포함하고, 예정 시각을 입력했다면time: "HH:mm"을 포함한다. 컨테이너는 같은session(status planned) 행을 교체하며 사용자가 시각을 변경하지 않으면 기존 값을 보존한다. - kg/lb/% 모드는 UI-local이지만
%원본은loadPct, lb 원본은loadLb, 계산된 절대 중량은load로 함께 저장하고 다시 편집할 때 round-trip 한다.
맨몸운동 체중 반영 (2026-07-19)
- 새 prop
bodyWeight?(kg, 최신 신체 정보 몸무게 — 컨테이너 공급). 종목 draft에bwFactor?(체중 계수): 저장된 세션 스냅샷을 우선하고, 새 기록은 canonical 카탈로그의 명시 계수를 사용한다. 정규 맨몸 종목은 0 / 0.25 / 0.5 / 0.75 / 1.0의 검토된 프로필을 사용하며 이름·slug·장비 문자열로 추론하지 않는다 (ui/shared/bodyweightSemantics.ts). - 유효 세트 무게 = load(추가 중량) + bodyWeight × bwFactor. load 입력값은 추가 중량 그대로 영속.
onSave(draft)종목에bwFactor포함 — 세션 영속 시session.bodyWeight(저장 당시 체중 스냅샷)와 함께 저장할 것(과거 기록 고정). - UI: 카드 헤더 "체중 100%" 칩(
.dk-bwtag) · 중량 placeholder "추가 중량" · 확정 카드 무게 "체중+10kg × 8"(툴팁 = 유효 kg) · e1RM은 유효 무게 기준, 1RM 대비/세트 강도·% 입력 모드는 미노출(등록 1RM은 추가 중량 기준이라 상이). UiDesktopWorkoutFlow(라이브 기록)도 동일 계약:bodyWeightprop, 볼륨 유효 무게 합산,buildSession결과에bodyWeight/종목별bwFactor포함.
수행 디테일(변형 태그) (2026-07-18 · main 미머지분 2026-07-19 재적용)
- 종목 draft
exercises[].details?: string[](최대 3) — 같은 종목에 붙는 수식 태그(통계 귀속 exerciseId 불변, 표시만 구분). 에딧 중 종목명 앞 칩 + "+ 디테일" 팝오버(프리셋+직접 입력), 확정/조회 표기는 "디테일 종목명" 평문. - 복합 종목은 슬롯별
composite[].details?: string[]— 자동 종목명에 포함("1 무릎높이 정지 3초 데드리프트 + …"), fan-out 시 해당 exerciseId로 귀속. - 저장/조회 왕복 보존(영속은 Codex). 프리셋 v1 하드코드 — 관리자 디테일 카탈로그 공급으로 교체 예정. hook
exerciseDetailAdd.
UiDesktopAdmin (운동 관리자)
view+onChangeView(view)controlled 계약. 독립 관리자 셸(src/react/vite/admin/AdminStandaloneRoot.tsx, barbelic-docs/admin)의 사이드바가ADMIN_VIEW_TABS를 재사용해 전체 종목/그룹/디테일/계층/매핑/인입/운영 콘솔/신고 큐를 전환한다(미주입 시 UI-local 폴백). 데스크톱 앱 사이드바의 관리자 하위 메뉴는 2026-08-21 이관으로 제거.- 하나의 관리자 페이지에서 다음 뷰를 전환:
- 운동 분류 (
UiDesktopAdminExercisesBody, 3-pane: 아키타입 → 하위 운동 → 디테일 패널) - 디테일 (
UiDesktopAdminDetailsBody, 수행 디테일 카탈로그, 2-pane) - 신고 큐 (
UiDesktopAdminReportsBody, 2026-08-25 이슈 #670 — UGC 신고 대기/조치/기각 필터 + 기각·내리기(원본 불변)·사용자 정지/해제, 서버get_admin_content_reports_v1/admin_resolve_content_report_v1/admin_set_user_suspension_v1, 상태는 DesktopAdminFeature 소유, hookadminReportsView)
- 운동 분류 (
- 부모
UiDesktopAdmin(DesktopAdmin.jsx)이 공유 셸 소유 = 토픽바(브랜드 "운동 관리" + 세그먼트 탭 + 검색 + 뷰별 카운트). body는.adm-body만 렌더. - 전체 유저 공유 글로벌 데이터 — 개인 데이터(1RM/PR/즐겨찾기 등) 없음.
- 루트
.adm(자체 베이스 토큰 →.dk-app불필요). 실 앱은 사이드바 없이 full-bleed 렌더 권장. 프리뷰 하니스는 dev 사이드바 옆에.adm(flex:1)로 배치. - props(부모가 받아 body로 그대로 전달):
archetypes[{ id, ko, en, part, pattern, children:[exercise] }]unfiled[exercise] — 아키타입 미배정(매핑 대상)reviewQueue[{ id, ko, en, submittedBy, at, count, kind, suggestion, note }] kind:duplicate(병합 후보) |map(아키타입 매핑) |new(신규 승인) — 큐 상세/모달은 별도(미구현)- exercise = { id, ko, en, equip(분류 8종: 바벨/덤벨/케틀벨/머신/밴드/맨몸/유산소/기타 —
exercise_category라벨), part, unit(kg/회/초/거리), side(양측/편측), kind(컴파운드/고립), aliases:[string](교정 별칭 = default synonym의 aliases), synonyms:[{ id, name, nameEn, isDefault, aliases:[string] }](표기 트리 전체 — 대표 포함, 표기별 소속 별칭까지. 이슈 #963에서 { name, nameEn } 부분 투영을 확장), recordingFields:[load|reps|distance|duration](기록 필드 프로필, 읽기 전용), origin(system|user— #1175: 소유자 파생값), details:[string], mains:[string], subs:[string], status(normal|inactive), tags?:[string] } ← 개인 측정값 없음 · 2026-08-22 정리: 장비 → 분류(8종), 별칭 → 교정 별칭 + 다른 표기(synonym 트리 2.0.0), 기록 필드·출처 행 추가. Wodup 매핑 뷰는 Wodup → Barbelic 단방향 고정(방향 토글·adminMappingDirToggle제거). · details = 정지·템포·구간 변형 등 수행 디테일(예: "3초 정지", "네거티브", "무릎 정지"). 종목 디테일 패널에서 별칭과 태그 사이 섹션. 정규 원본은details카탈로그. details[{ id, ko, en?, type, desc, status(normal|inactive), usedBy:[{ id, ko, en, equip, archetypeKo }] }] — 디테일 탭의 카탈로그. type ∈ "템포"|"이센트릭"|"구간 정지"|"구간 변형"(TYPE_ORDER 순 그룹핑). usedBy = 이 디테일을 부착한 운동(컨테이너가 exercise.details 역참조로 구성).- 상단 카운트(운동 분류: 아키타입/정규/검수/미분류 · 디테일: 디테일/유형/미사용)는 props에서 파생.
- 콜백(전부 옵셔널, no-op safe):
- 운동 분류:
onAddArchetype()onAddExercise(archId)onOpenReviewQueue()onEditName(id)onMove(id)onMerge(id)onToggleActive(id)onAddAlias(id)onRemoveAlias(id, alias)onAddExerciseDetail(id)onRemoveExerciseDetail(id, detail)onAddTag(id) - 표기 편집(이슈 #963, 상세 "표기와 별칭" 카드):
onAddSynonym(exerciseId, {ko, en})onRenameSynonym(synonymId, {ko, en})onReplaceSynonymAliases(synonymId, aliases)onPromoteSynonymAlias(exerciseId, alias)onDeleteSynonym(synonymId)onSetDefaultSynonym(synonymId)— 콜백 부재 시 카드가 읽기 전용으로 렌더. - 디테일 카탈로그:
onAddDetail()onEditDetail(id)onMergeDetail(id)onToggleDetail(id)onOpenExercise(exerciseId)(→ 운동 분류 뷰의 해당 종목으로 라우팅, 컨테이너/Codex 소유)
- 운동 분류:
- 선택(뷰/아키타입/종목/디테일)·검색어는 UI-local. 데이터 형태 =
fixtures/adminExercisesFixture.js+fixtures/adminDetailsFixture.js - 마커: 신규 action/view 마커 0건(등록 전 — 아래 요청). 식별 hook만:
adminView(root)adminViewTabs· 운동분류adminExerciseDetail/adminArchetypeList/adminChildList/adminReviewQueue/adminAliasEditor/adminExerciseDetailEditor(디테일 부착) · 디테일카탈로그adminDetailList/adminDetailInspector/adminDetailUsage. 드래그 재분류는 포인터 핸들(액션 마커 아님).
UiDesktopAdminImportBody (Wodup 세션 인입 · 업로드/진행/결과)
관리자 페이지 "Wodup 인입" 탭 body. 12단계 파이프라인 전체의 관리자 UI를 하나의 phase 기계로 표현. 순수 프레젠테이션 — 파일 읽기·집계·Storage 업로드·Edge Function·batch polling은 전부 컨테이너(Codex Frontend) 소유.
phase"idle"(드롭존·1단계) |"checking"(사전검사·2단계) |"ready"(통과) |"invalid"(실패) |"processing"(서버 처리·4~11단계) |"done"(결과·12단계)file{ name, size(bytes), lines? } | nullchecks[{ k:label, status:"pass"|"fail"|"pending", detail? }] — 사전검사 5종: 파일 확장자(.jsonl)/첫 줄 JSON 파싱/source=wodup/schema_version/wodup_user_id 일관성summary{ wodupUserId, sessionCount, exerciseCount, setCount, dateRange } | null — phase==="ready"에서만. 파일에서 감지 가능한 값만progress{ pct?(0~100, 생략 시 indeterminate 바), message?, stages:[{ k, label, status:"done"|"active"|"pending", detail? }] } — phase==="processing". 스텝 권장: 업로드→정규화→매핑/placeholder→일지반영→통계result{ status:"completed"|"completed_with_placeholders"|"failed", sessionCount, exerciseCount, setCount, mappedCount, placeholderCount, errorCount, placeholders:[{ id(=provider_exercise_key), name, category?, sessions, sets }], errors?:[{ line?, message }] } — phase==="done"providerLabelstring (생략="Wodup")- 콜백:
onPickFile(File)·onStartImport()·onReset()(다른 파일) ·onNewImport()(결과→새 인입, 생략 시 onReset) ·onGoMapping()(placeholder→Wodup 매핑 탭, DesktopAdmin이 setView("mapping")로 기본 배선) ·onRetry()(failed 재시도) - 드롭존 dragover 하이라이트는 UI-local. 데이터 형태 =
fixtures/adminImportFixture.js(idle/checking/ready/invalid/processing/done/doneClean/failed) - DesktopAdmin 통합:
TABS에{k:"import", l:"Wodup 인입", ic:"upload"}. props는importPhase/importFile/importChecks/importSummary/importProgress/importResult+onImportPickFile/onImportStart/onImportReset/onImportNew/onImportRetry. import 뷰 카운트 = result(세션/정식매핑/placeholder) > summary(세션/운동/세트) > 상태 칩. - 마커: 식별 hook:
adminImportView(root) ·adminImportDrop·adminImportFile·adminImportStart·adminImportPolicy·adminImportProgress·adminImportResult·adminImportPlaceholders. - 인입 대상 계정 (2026-08-21 · 관리자 대리 인입): 업로드 단계(idle/checking/ready/invalid) 상단에 대상 계정 카드. 기본은 본인, 검색해 다른 유저를 고르면 그 계정 명의로 배치가 만들어진다(서버 권한 =
is_lift_guild_admin(), 배치initiated_by_user_id에 관리자 기록). processing/done에서는 드래프트에 고정된 대상(importTargetUser)을 한 줄로 표시.- props:
importTarget{ id, email, displayName } | null(=본인) ·importTargetUser(배치에 고정된 대상, 처리 중/결과 표시용) ·importTargetQuery·importTargetResults[{ id, email, displayName }] ·importTargetStatus"idle"|"searching"|"error" ·importTargetError - 콜백:
onImportTargetSearch(query)·onImportTargetSelect(user)·onImportTargetClear() - 마커: hook
adminImportTarget·adminImportTargetResults, actionadmin.import.target.select(option=user id) ·admin.import.target.clear, fieldadmin.import.target.query. 인입 시작 버튼 라벨은 대상이 있으면 "〈대상〉 계정에 인입 시작".
- props:
- Codex 후속(마커 등록 요청): action
admin.import.pick·admin.import.start·admin.import.reset·admin.import.new·admin.import.retry·admin.import.goMapping. — 2026-08-24 현황:admin.import.goMapping을 제외한 5종은 designContract에 등록 완료(goMapping은 여전히 DesktopAdmin 기본 배선·마커 없음).
UiDesktopAdminMotraImportBody (모트라 세션 인입 · 업로드/진행/결과 — 2026-08-24)
관리자 페이지 "모트라 인입" 탭 body. 입력은 모트라 export XLSX(브라우저에서 블록 스캔 후 변환기 코어 convert-core.mjs 를 그대로 실행) 또는 변환기 산출물 motra-normalized.jsonl — 정규화 정본은 scripts/motra-import 변환기이고 화면은 검사·대상 지정·저장만 한다. 서버 배치·폴링·Edge Function 없음: 브라우저 사전검사(motraNormalizedUpload.ts) 뒤 import_motra_workouts_v1 RPC 1회로 일지 반영+통계까지 끝난다. 순수 프레젠테이션 — 검사·RPC 호출은 컨테이너(motraImportController.ts) 소유. CSS는 .adm-imp-* 전량 재사용(신규 클래스 0).
phase"idle"|"checking"(jsonl 사전검사) |"ready"|"invalid"|"processing"(RPC 1회) |"done"file{ name, size(bytes), lines?(=세션 수) } | nullchecks[{ k:label, status:"pass"|"fail"|"pending", detail? }] — 사전검사 5종: 파일 형식(.xlsx/.jsonl)/구조·변환(변환기 코어)/세션 identity/판정 매니페스트(map·new slug, custom motra_name)/규모 제한summary{ sessionCount, exerciseCount(엔트리), setCount, weightsCount, cardioCount, noteCount, catalogCount, customCount, dateRange } | null — phase==="ready"progress{ message?, stages:[{ k, label, status, detail? }] } — phase==="processing", indeterminate 바 고정(서버 신호 없음)result{ status:"completed"|"failed", sessionCount, exerciseCount, setCount, weightsCount, cardioCount, noteCount, catalogCount, customCount, customExercises:[{ id(=motra:* key), name }], errorCount, errors?:[{ message }] } — phase==="done". custom 실체화 종목 카드가 와드업의 placeholder 카드를 대체(오너 D1).- 콜백:
onPickFile(File)·onStartImport()·onReset()·onNewImport()(생략 시 onReset) ·onRetry(). goMapping 없음 — 판정은 매니페스트가 정본이라 후속 매핑 검수 개념이 없다. - 인입 대상 계정: 와드업의
ImportTargetCard를 그대로 재사용(→DesktopAdminImport.jsx에서 export). props/콜백/마커 규약 동일 — props:importTarget/importTargetUser/importTargetQuery/importTargetResults/importTargetStatus/importTargetError, 콜백onImportTargetSearch/Select/Clear. DesktopAdmin 배선에서는motraImportTarget*/onMotraImportTarget*이름으로 전달된다(와드업 상태와 독립). - DesktopAdmin 통합:
TABS에{k:"motraImport", l:"모트라 인입", ic:"table"}. props는motraImportPhase/motraImportFile/motraImportChecks/motraImportSummary/motraImportProgress/motraImportResult+onMotraImportPickFile/onMotraImportStart/onMotraImportReset/onMotraImportNew/onMotraImportRetry. - 마커: 식별 hook
adminMotraImportView(root) ·adminMotraImportDrop·adminMotraImportFile·adminMotraImportStart·adminMotraImportPolicy·adminMotraImportProgress·adminMotraImportResult·adminMotraImportPlaceholders(custom 실체화 카드), actionadmin.motraImport.pick|start|reset|new|retry(designContract 등록 완료). 대상 계정 카드 마커는 와드업 것 공유(adminImportTarget등).
공통 로딩 오버레이 (DesktopLoadingOverlay — 2026-07-11)
- 공용 컴포넌트
desktop/screens/DesktopLoadingOverlay.tsx:UiDesktopLoadingOverlay({ loading, label?, variant?: "card"|"section"|"item"|"inline", className? })+UiDesktopLoadingPill+dkLovIdSet. - 방식: 호스트 부모에
.dk-lov-host(position:relative·min-w/h:0) + 로딩 중data-lov-busy="1"→ 오버레이는absolute inset:0불투명 var(--surface) 커버(부모 경계·카드 크기 무변, 레이아웃 이동 0). busy 시 호스트의 다른 자식은 CSSvisibility:hidden— 뒤 콘텐츠 마우스·키보드(포커스) 차단.role="status" aria-live="polite" aria-busy="true". label은 전달값 그대로(화면별 하드코딩 없음, 미전달 시 스피너만). 스피너/필 CSS는 desktop.css 말미.dk-lov*한 번만 정의..lg-data-loading-overlay(전체 화면 fixed) 미사용. 신규 data-lg-* 마커 0. - 운동일지
UiDesktopSessionScreen:selectedDateDetailLoading+selectedDateDetailLoadingDate(=== selectedDate일 때만)+selectedDateDetailLoadingLabel→ 우측 하루 패널(.dk-j2-bar 아래)만 가림(달력·필터 바 무변).monthLoading(현재 표시 월)+monthLoadingKeys(["YYYY-MM"])+monthLoadingLabel→ 단일 월 셀 자리(주 수×97px 밴드.dk-j2-mload)만 가리고, 전경 카드 로딩과 인접 월 background prefetch를 구분한다.sessionDetailLoadingIds는 레일/달력 엔트리의 중복 요청만 막으며 행 오버레이는 그리지 않는다.sessionDetailPendingId(2026-07-18 지시서): 상세 응답 대기 중인 세션 id 1건 — 현재 날짜에 속한 정확한 ID일 때만 세션 상세 모달(journalSessionModal)이 클릭 즉시 열리고, selectedSession 도착 전까지 본문에만 section 로더(.dk-j2-sess-pendingmin-height 460px)를 표시한다. active selectedSession이 pending보다 우선하며, 닫기 X·스크림·같은 행 재클릭 판정은 loading guard보다 먼저 실행한다. 컨테이너 책임: 닫힘/다른 세션 선택 후 도착한 스테일 응답은 selectedSession으로 올리지 않기, 캐시 적중 시 pending 생략. - 훈련 리포트
UiDesktopVolumeScreen:loading+loadingLabel→.dk-vol-bodymain카드만 가림(사이드바·배경 유지, 0수치/빈차트 미노출).sessionDetailLoadingIds/Label은 컨테이너 호환을 위한 예약 prop이며, 현재 v4 레이아웃에는 세션 오픈 행이 없어 소비하지 않는다. - 메인세트 모아보기
UiDesktopLogTableScreen:loading+loadingLabel→.dk-logt-scrollwrap(표 콘텐츠)만 가림 — 월 네비게이터·볼륨/탑세트 토글 사용 가능, 열 폭·행 높이 유지(visibility 기반). 종목 드로어의 슬림DkPrDetail은 기록 행을 렌더하지 않으므로 세션 상세 로더 props를 받지 않는다. - 나의 기록
UiDesktopPrScreen:loading+loadingLabel→.dk-prb-card내부만 가림(빈 즐겨찾기/0 PR 보드 선노출 방지).detailLoading은 상세 root의aria-busy만 갱신하고 시각 오버레이를 추가하지 않는다.sessionDetailLoadingIds/Label→ E2 PR 행.pb, E3 탑세트 행.tb, F 훈련 목록 행.fh의 item 로더와 클릭 가드(r.sessionId ?? r.id정확 매칭). E2/E3은 세션으로 이동하고 F는 세트 상세 모달을 먼저 연다. - 프로필 피드
UiDesktopHomeScreen:profileFeedInitialLoading+profileFeedLoadingLabel→.dk-h3-feed컬럼만 오버레이(최초 로딩 중 "아직 기록이 없어요" 미표시). 추가 페이지profileFeedLoading은 기존 하단 인라인 로더(#177) 유지 — 기존 피드 미가림.sessionDetailLoadingIds/Label→ 클릭한 포스트만 item 로더. - 세션 검색
UiDesktopSearchScreen:sessionDetailLoadingIds/Label→ 클릭한 결과 포스트만 item 로더. - 공통 규칙: 로더는 props만 신뢰(자체 타이머 없음 — 캐시 적중/오류 종료 시 컨테이너가 상태를 내리면 즉시 소멸). 서로 다른 두 세션 동시 로딩 가능(Set 포함 판정). 상위 카드/날짜 상세 오버레이 표시 중엔 세션별 로더 숨김.
신규 마커 요청
- Admin (Codex 등록 요청) — 현재 식별 hook만 부착, 실제 action/view/field 마커 미등록:
- view:
dashboard.admin(단일 페이지, 뷰 전환은 UI-local 탭) - action(운동 분류):
admin.view.tab·admin.archetype.add|select·admin.exercise.add|select|editName|move|merge|toggleActive·admin.alias.add|remove·admin.exercise.detail.add|remove·admin.tag.add·admin.review.open - action(디테일 카탈로그):
admin.detail.add|select|editName|merge|toggleActive·admin.detail.openExercise - field:
admin.search - URL 전용 라우트 배선(메뉴 비노출)은 app-shell 소유 = Codex. 디자인 측은 화면/props 계약만 제공.
- view:
- (이전) 데스크톱 1차는 전부 기존 등록명으로 커버. 미커버 인터랙션은 기능 확정 시 PR 설명으로 등록 요청.
U03 공통 편집기·창 크기 대응 (2026-09-10, #1421)
적용 대상은 release/v0.18.0, Production 미반영이다. 작업·검증 기록이 기존 고정 크기·내부 스크롤 설명보다 우선한다.
- 계획은 A13의 PlanEditorState, 완료 기록 작성·수정은 A12의 WorkoutEditorState를 직접 소유한다. UiDesktopPlanEditor/UiDesktopWorkoutEditor는 같은 카드 표현을 쓰되 typed command adapter와 저장 의도를 분리한다. 완료 기록을 계획 모델로 열거나 backfill extra를 다시 합치는 활성 경로는 제거했다.
- 화면은 raw 분·초/무게/복합 입력과 IME 조합 여부를 명령으로 보낸다. 입력 해석·단위 환산·세트 판정·실패·자식 ID·저장 투영은 공통 모델이 담당한다. 표시 단위·focus·drag·picker 열림은 화면 상태다. 기존 서버 점수·통계는 보존하되 의미가 바뀐 입력의 statsLoad/statsEffectiveLoad는 공통 reducer가 무효화한다.
- publishDesktopWorkoutSnapshot(requestId, snapshot)은 완료 기록 편집의 최신 원문을 S08에 전달한다. 저장 시작 뒤 바뀐 입력은 별도 초안 operation으로 남긴다. durable queue/receipt가 이전 저장을 정리해도 최신 원문을 지우지 않고, receipt 자식 ID·revision을 채택한다. owner가 바뀌면 화면을 닫고 이전 owner 늦은 응답이 새 화면을 바꾸지 못하게 한다. 계획의 지연 저장도 최신 입력을 유지하고 다음 저장은 생성된 계획을 수정한다.
- recoveryPanel은 S09 typed 상태와 명령만 받는다. 현재값 미조회·권한 없음·오프라인·미리보기 stale·기기 보관·서버 확정을 구분한다. 명시 확인 전 적용하지 않으며 서버 미조회 값을 계산으로 채우지 않는다.
- 홈·일지·리포트는 document 스크롤과 가용 본문 폭으로 재배치한다. 홈 피드 추가 요청은 document viewport 기준이다. PR 보드의 열 구조만 dk-prb-table-scroll(키보드 focus 가능한 region)에서 가로 스크롤한다. 통계 값·컬럼 의미는 변경하지 않는다.
- 편집 compose/폐기 확인은 U01 UiDialog다. dialog 자체가 긴 내용을 스크롤하고 focus를 가둔다. 디테일 picker는 열린 compose 안에 portal을 두며 topmost Escape를 소비한다. 저장 중 폐기 방지와 owner 변경 시 닫힘은 화면/컨트롤러가 담당한다.