Skip to content

공통 UI primitive props 계약 — U01 v1

  • 계획: U01 / #1289, v0.18.0 총괄.
  • 버전: UI_PRIMITIVES_CONTRACT_VERSION = 1 (src/react/contracts/ui-primitives-props.ts). 소비하는 G03 core 버전은 1이다.
  • 규범: 디자인 전달 계약. 표현/배선의 전달 입력은 이 문서이며 테스트 하니스는 규범이 아니다.
  • 표현 담당 U01은 아래 props와 시각 상태를 소유한다. 기능 담당 A12/A13은 입력 판정, A08/A10·S 계열은 저장 단계 판정을 소유한다. 그 결과만 ViewModel로 주입한다. 기존 core/DTO·화면 props는 변경하지 않는다.
  • 스타일·소유권·예외: 공통 시각 기반.

경계와 도입 범위

contracts/ui-primitives-props.ts는 React를 모르는 표시 모델만 선언한다. DOM 이벤트·ReactNode·ref를 포함한 실제 컴포넌트 props는 ui/shared/primitives/*.tsx에 선언한다. ViewModel 브랜드는 feature mapper에서 부여하고 UI 안에서 DTO를 캐스팅하거나 브랜드를 새로 붙이지 않는다. 버튼의 children·다이얼로그 제목 같은 UI 구조 props에는 도메인 DTO를 넣지 않는다.

입력기능 담당이 내려주는 결론UI가 하지 않는 일
UiInputModellabel, raw, hint?, G03 InputCell과 같은 state; invalid만 error 필수parse/정규화/단위 계산/빈 값→0 변환/저장 가능 판정
UiPersistenceModel저장 단계와 전송 보류/차단, 통계 대기 여부영수증·세대 비교/online 감지/낙관적 성공 승격
UiStateModelloading/empty/error와 제목·설명빈 결과 추측/인증 오류와 데이터 오류 합치기/재조회 실행
intent callbackonValueChange, onCommit, onDismiss, onClick, onPressRPC·controller·storage import/Promise 완료를 저장 성공으로 해석

이번 대표 연결은 DataErrorGate 재시도 버튼과 AppErrorScreen 두 버튼이다. 기존 클래스·배치·문구·콜백을 보존한다. 입력·다이얼로그·저장 상태의 전체 화면 채택은 U02/U03 단계다. 기존 모바일 UiModal과 데스크톱 DkScrim은 아직 유지한다.

버튼

UiButton은 native button 속성·ARIA 이름·children·className·onClick을 전달한다. 기본 type="button", 폼 제출은 명시적인 type="submit"이다. 아이콘만 있는 버튼은 기능을 설명하는 aria-label을 호출자가 준다. 링크 이동에는 button 대신 native anchor를 쓴다.

트리거상태 변화/intent복귀 경로
click / Enter / Space, 사용 가능native click으로 onClick 1회호출자가 새 상태를 주입
disabled=truenative disabled, Tab 순서에서 제외호출자가 disabled 해제
pending=truearia-disabled, aria-busy; click·폼 제출·상위 click 전파 차단, 현재 포커스 유지호출자가 pending 해제

pending은 실행 중인 행위의 상태다. 기기 저장 후 서버 동기화가 대기 중이라는 이유로 모든 버튼을 pending으로 잠그지 않는다. 중복 명령 방지는 controller 책임이며 이 버튼은 서버 멱등성을 대신하지 않는다. pending 중 버튼을 제거하거나 이름을 빈 문자열로 바꾸지 않는다. 결과 안내는 별도 상태 영역으로 전달한다.

입력·검증·IME

UiInput은 native text/search input과 label/hint/error를 묶는다. 숫자도 inputMode="decimal" 또는 numeric인 text로 받고 원문을 보존한다. model.raw가 유일한 value다. empty/typing/valid/invalid 판정은 G03 입력 셀 §6과 A12/A13 binding이 소유한다.

트리거상태 변화/intent복귀 경로
input changeonValueChange(raw); "", "0", "12.", 한글을 변환 없이 전달binding이 raw/state/model을 다시 주입
composition start/end조합 이벤트 그대로 전달; UI의 조합 중 flag는 키보드 보호용end의 raw를 binding이 판단; UI가 검색/저장을 실행하지 않음
조합 중 Enter / isComposing / keyCode 229onCommit·폼 제출·상위 Enter 동작 차단조합 종료 후 별도 Enter 가능
일반 Enter + onCommit외부 onKeyDown이 취소하지 않았다면 commit intent 1회, implicit submit 차단binding이 검증·다음 포커스/저장 여부 결정
readOnly/disabledcommit 발화 없음; readOnly는 선택·복사 허용호출자가 해제
invalid modelaria-invalid, aria-errormessage, label/hint/error ID 연결수정된 model이 오면 error 관계도 제거

id 생략 시 React useId로 연결한다. 여러 인스턴스에서 중복 id를 지정하지 않는다. 외부 aria-describedby도 hint/error와 함께 보존한다. 오류 문구는 사용자용 문장으로 mapper가 준다. raw 오류 객체·서버 메시지·request payload를 그대로 노출하지 않는다. typing 상태를 빨간 오류로 그리지 않는다.

입력마다 assertive live region을 만들지 않는다. 제출 실패 때 binding이 첫 invalid 입력에 초점을 옮기고 요약 한 곳을 알린다. 숫자 확정·blur rollback·자동 선택·검색 debounce는 이 primitive에 없다. 기존 자동 숫자 선택 정책을 이식할 때는 A12/A13이 충돌 없이 배선한다.

다이얼로그·포커스

UiDialog는 native <dialog>.showModal()을 쓰는 신규 modal 전용이다. 제목으로 accessible name, 선택적 짧은 설명으로 description을 제공한다. 긴 구조화된 내용 전체를 description에 중복 연결하지 않는다. showModal을 지원하는 브라우저가 전제이며 지원하지 않는 WebView에는 임의 nonmodal fallback을 만들지 않는다(U06/N01 검증).

트리거상태 변화/intent복귀 경로
open: false → true브라우저 top layer, 배경 inert; 내부 initialFocusRef 또는 제목에 초점표준 Tab 순환
Tab / Shift+Tab보이는 enabled 요소의 끝/처음에서 내부 순환; custom widget이 preventDefault한 키는 존중다이얼로그 안 유지
EscapeonDismiss("escape"), native 자동 close는 방지소유자가 open=false로 닫거나 확인 절차 유지
IME 조합 중 Escapedismiss 없음, 조합 취소만조합 종료 후 Escape
닫기 버튼onDismiss("close_button")위와 같음
배경 클릭자동 dismiss 없음닫기 버튼/Escape 사용
open=false 또는 unmountclose; 지정 returnFocusRef 또는 열기 직전 요소로 복귀사라질 트리면 호출자가 살아남는 복귀 ref를 제공

초기 ref와 복귀 ref 객체는 안정적으로 유지한다. 열 때 복귀 대상을 캡처한다. 사라졌거나 inert인 요소에 focus하지 않는다. 파괴적 확인은 initialFocusRef를 안전한 취소 동작에 두고 confirm에는 autofocus하지 않는다. positive tabindex는 사용하지 않는다. virtualized list에서 opener가 사라지는 경우 U04가 안정된 복귀 대상으로 연결한다.

이 API의 open은 UI 표현 상태다. 저장·삭제·뒤로가기 정책은 callback 소유자가 결정한다. 자식에서 dialog.close(), form method="dialog", 외부 open 속성 제어를 하지 않는다. 한 기능은 하나의 overlay 계열을 사용하며 기존 document Escape 리스너/앱 루트 포털과 신규 top-layer modal을 중첩하지 않는다. overlay 스택·native back gesture·owner 전환 닫힘은 앱 shell 소유이고 U02/U03에서 이식할 때 함께 배선한다.

loading·empty·error

UiState({ model, action? })의 kind 선택은 기능 담당이 한다. loading은 role=status, empty는 조용한 일반 콘텐츠, action 후 error는 role=alert다. 제목·설명만 알리고 재시도 버튼을 alert 영역 안에 반복 포함하지 않는다. action은 명시된 callback만 호출한다. loading의 live text에 aria-busy=true를 붙여 알림을 보류하지 않는다. 갱신 중인 콘텐츠 영역의 busy 표시는 그 영역 소유자가 관리한다.

데이터 조회 실패를 로그인 화면으로 바꾸지 않는다. 기존 degraded 화면을 무조건 error로 승격하지 않는다. 네트워크 재시도 중 기존 데이터 유지/empty 선택은 resource snapshot과 화면 계약이 결정한다.

저장 단계·동기화 안내

UiPersistenceStatus는 아래 표의 고정 의미만 그린다. G03 §15의 stage 판정은 controller에서 끝낸다.

주입 값제목설명의 의미
device_persisted + pending기기에 보관됨서버 대기, 통계는 동기화 후 반영
device_persisted + held기기에 보관됨로그인 확인 뒤 동기화, 통계 미반영
device_persisted + blocked동기화 실패기기에 보관된 기록 확인·명시 복구
server_committed + stats pending서버 저장 완료통계 반영 대기
server_committed + stats not_applicable서버 저장 완료통계 대상이 아닌 저장(계획 등)
stats_published통계 반영 완료읽기 모델이 저장 기록을 반영함
unknown저장 상태 확인 필요결과를 아직 확인하지 못함; 성공 아님

상태 영역은 role=status, polite, atomic이다. 색/아이콘만으로 구별하지 않는다. 텍스트는 플랫폼 공통이며 모양은 플랫폼이 소유한다. unknown을 오류로 단정하거나 server_committed로 올리지 않는다. not_applicable은 receipt 정책의 결과를 binding이 주입하며 UI가 requested version=0을 추측하지 않는다.

이것은 표시 내용 계약이며 새 toast 발화 정책이 아니다. 완료 기록 쓰기 파이프라인 §4·§7의 기기 적재 즉시 진행·서버 성공 무음·영구 거부 때 기록 배지/명시 복구를 유지한다. 저장 도중 일반 네트워크 실패를 갑자기 modal error로 띄우지 않는다. 아직 기기에 적재되지 않았다면 device_persisted를 주지 않는다.

검증과 인계

  • 자동 Node 검사: tests/react/uiPrimitives.test.mjs — 버튼 행위·7가지 저장 표시·상태 알림. 기존 frontend/contracts import boundary가 새로운 shared 파일도 검사한다.
  • 실제 브라우저: tests/browser/u01/primitives.spec.mjs — 두 viewport에서 raw·IME·Enter·pending·Tab·Escape·unmount·초기/복귀 focus·오류 연결·대표 재시도·CSS scope·긴 dialog. 실행법은 테스트 안내.
  • U02/U03: mapper에서 ViewModel을 만들고 의미론 앵커(#input/#persistence 등)를 배선 PR에 적는다. fixture가 아닌 실제 storage/receipt/resource와 연결된 매퍼 행동 검사를 추가한다.
  • U07: 시각 계약의 selector 목록, 레이어·예외 소유자를 소비한다. 실제 화면 이식 뒤 기존 primitive 삭제를 판단한다.
  • U06: 실기기 VoiceOver/TalkBack·IME·native back·200% 글자 확대·overlay/포털 상호작용·긴 목록 복귀를 최종 검증한다. 이 문서가 모든 기존 화면의 검증 완료를 뜻하지 않는다.

CHANGE-NOTES: 신규 intent onValueChange/onCommit — 계약 #input, 신규 intent onDismiss — 계약 #dialog, 상태·저장 표시 — 계약 #states/#persistence.