Skip to content

UI 컴포넌트 계약

작성: 0chil

이 문서는 docs/ui/ui.md를 실제 화면에 적용할 때 사용하는 기준이다. shadcn/ui의 이름만 빌려 새 모양을 만들지 않는다. 아래의 공식 구성과 예제 → 이 저장소의 HTML/CSS → 브라우저 결과 순서로 확인한다.

  • 참고 구현: shadcn/ui v4 base (Base UI), b0fcb58a6e7c4df1a88f7f6327b083592c2dfd9c. 2026-10-01 확인.
  • 이 저장소는 React, Base UI, Tailwind를 설치하지 않는다. 공식 컴포넌트의 용도·구성·상태·접근성을 Thymeleaf, native HTML, ui-* CSS, ES module로 옮긴다. 문서 또는 CSS가 공식 구현과 다른 경우 아래의 로컬 선택을 우선한다.
  • 실제 모양은 컴포넌트 미리보기를 브라우저로 열어 확인한다. 이 파일은 애플리케이션 경로가 아니라 저장소 안의 검토용 HTML이다. 컴포넌트 CSS의 현재 위치는 api/src/main/resources/static/assets/ui/control-panel.css다.
  • Dialog 동작은 ES module을 사용하므로 저장소 루트에서 python3 -m http.server 8766 --bind 127.0.0.1을 실행한 뒤 http://127.0.0.1:8766/docs/ui/ui-component-preview.html을 연다.
  • 공식 사이트의 최신 문서는 새 컴포넌트를 탐색할 때만 사용한다. 이미 도입한 컴포넌트의 기준을 갱신하려면 이 문서, CSS, 미리보기를 한 변경에서 함께 갱신한다.
작업공식 컴포넌트로컬 구현선택 기준
작업 실행Buttonui-button + variantvariant가 글자·배경·테두리·상태를 결정한다.
서로 관련된 작업을 연결Button Groupui-button-group기본형이 붙어 있는 버튼 그룹이다. 이 저장소에서는 현재 버튼 자식만 지원한다.
입력 필드 안에 검색·보조 버튼Input Groupui-input-group하나의 입력 표면과 focus 상태를 공유한다.
페이지 이동Paginationnav.ui-pagination__controls, ui-pagination__link페이지 이동은 Button Group으로 표현하지 않는다.
한 값을 고르는 필터Toggle Groupfieldset.ui-toggle-group + native radio선택값은 checked가 소유하고 방향키로 이동한다.
선택값과 직접 입력을 함께 제공하는 필터Toggle Group + Fieldfieldset.ui-toggle-group, ui-form-field, 별도 조회 버튼직접 입력을 선택하면 숫자 필드를 표시하고, form 제출로 적용한다.
레이블·도움말·오류가 있는 입력Fieldui-form-field, ui-form-field__message입력 오류는 해당 필드에 표시한다.
여러 항목 선택Checkboxnative input.ui-checkbox + label각 항목은 독립적으로 선택·해제한다.
여러 줄의 자유 텍스트Textarea + Fieldtextarea.ui-textarea + ui-form-field레이블, 도움말, 오류 메시지를 연결하고 줄바꿈을 보존한다.
정보 영역Cardui-card 및 __header, __toolbar, __content, __footer카드 내부의 의미와 작업 위치를 명확히 한다.
정보 영역 구분Separatorhr.ui-separator서로 다른 정보 묶음을 가로 구분선으로 나눈다.
현재 화면의 보조 작업Dialognative dialog.ui-dialog, initializeDialog()버튼으로 열고 명시적으로 결과를 반영한다.
편집하면서 참고·삽입하는 보조 목록Sheet + non-modal Dialognative dialog.ui-drawer, initializeNonModalDrawer()배경 편집·스크롤·Tab 이동을 유지한다.
아이콘 버튼의 보조 설명Tooltipdata-ui-tooltip, initializeTooltips(), popover="manual"hover·키보드 포커스에서 설명을 보여 준다.
비동기 상태와 결과 제안Alertui-alert, __title, __description, __actions상태·결과·오류와 관련 작업을 화면 안에 함께 표시한다.
상세 정보 접기·펼치기Collapsibledetails.ui-collapsible + summary제목은 유지하고 해당 정보 묶음만 접거나 펼친다.
행과 열로 구성된 조회 결과Tableui-table-scroll + table.ui-table반환 순서와 열의 의미를 유지하고 좁은 화면에서는 표 영역만 스크롤한다.

공식 Button Group은 입력과 Input Group의 조합도 지원한다. 이 저장소의 ui-button-group CSS는 현재 버튼 자식만 다루므로 입력을 넣으려면 먼저 해당 조합을 공통 컴포넌트로 구현하고 미리보기에 추가한다. 입력 옆에 버튼이 있다는 이유만으로 어느 그룹을 사용할지 추측하지 않는다.

공식 소스: Button, Button Group.

<div class="ui-button-group" role="group" aria-label="선택한 항목 작업">
<button class="ui-button ui-button--outline" type="button">링크 복사</button>
<button class="ui-button ui-button--outline" type="button">CSV 다운로드</button>
</div>
  • ui-button-group은 자식의 경계와 모서리를 연결한다. 떨어진 버튼은 ui-action-row로 배치한다.
  • 검색·초기화처럼 역할이 다른 작업을 모양만 붙이기 위해 사용하지 않는다. 입력과 검색 버튼은 ui-input-group, 초기화는 독립 버튼으로 둔다.
  • ui-button에는 정확히 한 가지 색상 variant를 지정한다: --primary, --outline, --ghost, --destructive, --destructive-outline. --icon은 크기 modifier다.
  • hover, :focus-visible, disabled, pending 상태와 텍스트 대비를 미리보기에서 확인한다. 비동기 실행은 setButtonPending()을 사용한다.
  • 그룹에는 용도를 설명하는 aria-label 또는 aria-labelledby를 제공한다. 자식 버튼은 각각 Tab으로 이동한다.

공식 소스: Input Group.

<div class="ui-form-field">
<label class="ui-form-field__label" for="productSearch">상품 검색</label>
<span class="ui-input-group">
<input class="ui-input" id="productSearch" type="search">
<button class="ui-button ui-button--outline" type="submit">검색</button>
</span>
</div>

로컬 구현은 현재 입력과 버튼의 단순한 한 줄 조합만 지원한다. 공식 InputGroupAddon의 블록 배치나 복수 보조 요소가 필요하면 새 공통 variant를 먼저 설계한다. 오류 시 입력에 aria-invalid="true", 부모에 is-invalid, 메시지에 설명 텍스트를 함께 적용한다.

공식 소스: Input. 공식 Input의 type 전달, 파일 선택 버튼의 투명 배경·border 제거, disabled 계약을 확인했다.

  • input[type="file"].ui-input.ui-input--file에 native 파일 선택·파일명 표시·키보드 동작을 맡긴다. .ui-input의 border·focus ring·오류·disabled를 재사용하고, 파일 버튼의 높이와 여백만 modifier에서 맞춘다. 파일 버튼의 로컬 hover는 공통 secondary 색상을 사용한다.
  • 레이블과 오류 메시지를 Field에 연결하고 이미지 선택은 accept="image/*"를 지정한다. API에서도 자료형과 빈 파일을 검증한다. 선택 해제는 독립된 ui-button--outline 버튼으로 제공한다. 화면별 배치와 업로드 처리는 화면 stylesheet와 module이 소유한다.
  • 미리보기에서 기본·hover·focus·disabled·오류, 긴 파일명과 선택 해제를 데스크톱·모바일로 확인한다.

공식 소스: Pagination.

<nav aria-label="코디 목록 페이지" class="ui-pagination__controls">
<button class="ui-button ui-button--ghost ui-pagination__link" type="button">이전</button>
<span class="ui-pagination__pages">
<button class="ui-button ui-button--outline ui-pagination__link" aria-current="page" type="button">1</button>
<button class="ui-button ui-button--ghost ui-pagination__link" type="button">2</button>
</span>
<button class="ui-button ui-button--ghost ui-pagination__link" type="button">다음</button>
</nav>

공식 구현은 링크 기반이다. 이 저장소의 API 목록은 화면 내 데이터만 다시 가져오므로 버튼을 사용한다. 실제 경로가 있는 페이지 이동에는 링크를 사용한다. 현재 페이지에 aria-current="page", 이동할 수 없는 버튼에 disabled를 준다. 결과 건수나 페이지 번호 요약은 별도 ui-pagination__summary에 둔다.

공식 소스: Toggle Group.

<fieldset class="ui-toggle-group">
<legend class="sr-only">노출 상태</legend>
<label><input checked name="visibility" type="radio" value=""><span>전체</span></label>
<label><input name="visibility" type="radio" value="true"><span>노출</span></label>
<label><input name="visibility" type="radio" value="false"><span>숨김</span></label>
</fieldset>

로컬 구현은 단일 선택 필터에 한정하고 native radio를 사용한다. 그룹의 바깥 테두리와 화면에 보이는 legend는 두지 않는다. legend.sr-only는 스크린 리더에 그룹 이름을 제공한다. 공식 Toggle Group의 다중 선택이나 선택을 해제해 빈 값으로 만드는 동작은 이 구현의 계약에 포함되지 않는다. 선택값을 읽을 때는 :checked를 사용한다.

공식 소스: Checkbox. 공식 Usage·Field/FieldLabel·Group·Disabled·Invalid 구성을 따른다.

<label class="ui-action-row">
<input class="ui-checkbox" type="checkbox">
<span>연결할 상품 선택</span>
</label>
  • 로컬 선택으로 Base UI Root/Indicator 대신 native checkbox의 체크 표시와 checked, disabled, Space 동작을 사용한다. CSS는 크기·accent-color·focus·오류 상태만 지정한다. checkbox를 switch나 단일 선택 Toggle Group으로 표현하지 않는다.
  • label이 클릭 영역과 접근성 이름을 제공한다. 오류에는 aria-invalid="true"와 설명 메시지를 함께 표시한다. 기본·체크·hover·focus·disabled·오류 상태는 공통 미리보기에서 확인한다.
  • 코디 상품 추가 Dialog는 검색 결과의 label 안에 checkbox·상품 사진·이름·ID를 배치한다. 선택 상태는 검색 결과가 바뀌어도 유지하고, 선택 목록은 선택 순서를 표시한다. 이미 연결된 상품은 disabled와 ‘이미 연결됨’ 설명으로 구별한다. ‘선택한 N개 추가’ 전에는 편집기에 반영하지 않고 창을 닫으면 선택을 폐기한다.

공식 소스: Toggle Group, Field. 공식 Usage·Composition·상태 예제와 Field 접근성·오류 예제를 확인한다.

<form novalidate>
<div class="ui-form-field">
<span aria-hidden="true" class="ui-form-field__label">미해결 발생 수</span>
<fieldset class="ui-toggle-group" id="minimumCountPreset">
<legend class="sr-only">미해결 발생 수</legend>
<label><input name="minimumCount" type="radio" value="1"><span>≥ 1건</span></label>
<label><input checked name="minimumCount" type="radio" value="2"><span>≥ 2건</span></label>
<label><input name="minimumCount" type="radio" value="5"><span>≥ 5건</span></label>
<label><input name="minimumCount" type="radio" value="10"><span>≥ 10건</span></label>
<label><input name="minimumCount" type="radio" value="custom"><span>직접 입력</span></label>
</fieldset>
</div>
<div class="ui-form-field" hidden>
<label class="ui-form-field__label" for="minimumCountInput">최소 발생 수 (건)</label>
<input aria-describedby="minimumCountError" class="ui-input" disabled id="minimumCountInput" inputmode="numeric" min="1" required step="1" type="number">
<span class="ui-form-field__message" id="minimumCountError" role="alert"></span>
</div>
<button class="ui-button ui-button--primary" type="submit">조회</button>
</form>
  • 기존 단일 선택 Toggle Group 계약을 사용한다. :checked로 선택값을 읽고, 방향키로 이동한다. 그룹에 legend.sr-only를 제공하며 화면에 보이는 이름은 그룹 밖에 둔다. 좁은 화면의 줄바꿈은 화면 레이아웃이 소유한다.
  • 직접 입력 필드는 선택에 따라 hidden과 disabled를 함께 바꾼다. 직접 입력을 선택하면 입력란으로 focus를 옮긴다. 숨긴 입력은 필수값 검증과 Tab 순서에서 제외한다.
  • 조회 버튼과 Enter는 같은 form 제출을 사용한다. 제출 전 선택·입력값과 이미 적용한 조회값을 구분해 페이지 이동과 재조회에 적용값을 유지한다. 오류는 setFieldError()로 입력 아래에 표시한다.
  • 조회 중 선택·입력을 비활성화하고 버튼에 setButtonPending()을 적용한다. 레이아웃은 화면이 소유하며, 선택·입력·버튼을 ui-button-group으로 묶지 않는다. 미리보기에서 기본·직접 입력·오류·비활성 상태를 확인한다.

공식 소스: Field, Card.

<section class="ui-card" aria-labelledby="settingsTitle">
<div class="ui-card__header"><h2 id="settingsTitle">설정</h2></div>
<div class="ui-card__content">
<div class="ui-form-field is-invalid">
<label class="ui-form-field__label" for="sellerName">셀러명</label>
<input aria-describedby="sellerNameError" aria-invalid="true" class="ui-input" id="sellerName">
<span class="ui-form-field__message" id="sellerNameError">셀러명을 입력하세요.</span>
</div>
</div>
</section>

ui-form-field__message는 부모가 is-invalid일 때 표시된다. JS에서는 setFieldError(input, messageElement, message)를 사용한다. 카드 본문의 화면별 여백은 화면 전용 레이아웃이 소유한다.

코디의 ‘코디 정보’ Card는 본문에 ui-form-grid를 사용해 두 열을 같은 너비로 나눈다. 왼쪽에는 이미지 파일 선택기, 플라스크 SVG가 있는 ui-button--outline 생성 버튼과 AI 제안을, 오른쪽에는 제목·해시태그·노출을 둔다. 공식 Card의 Header·Content·Footer와 Field의 레이블·설명·오류 구성을 재사용하며, 공식 Button의 With Icon처럼 텍스트 앞에 장식용 aria-hidden SVG를 둔다. 아이콘 간격은 로컬 ui-button의 gap이 소유한다. 900px 이하에서는 공통 form grid 기준에 따라 이미지 영역 → 제목·태그·노출 순서로 쌓는다. 열 안의 배치만 화면 전용 ui-coordination-information-*가 소유한다.

코디의 ‘상품 연결’ Card는 코디 정보 아래에서 화면 전체 너비를 사용한다. 데스크톱에서는 왼쪽에 이미지, 오른쪽에 상품 추가·순차 위치 지정 버튼, 현재 상품·진행 안내와 연결 상품 목록을 배치한다. 오른쪽 조작 영역은 sticky로 유지하고 긴 목록만 스크롤한다. 1100px 이하에서는 버튼·진행 안내 → 이미지 → 상품 목록 순서로 쌓아 이미지 클릭 중에도 현재 상품을 확인할 수 있게 한다. 등록·변경사항 저장 버튼과 저장 결과는 이 카드의 Footer에 두며 같은 편집 form의 submit을 사용한다. 이 화면 배치는 vingle-admin.css의 ui-coordination-linking-*가 소유하며 다른 편집 화면의 레이아웃을 변경하지 않는다.

상품 추가·생성 이미지 채택은 위치 지정을 자동 시작하지 않는다. ‘순서대로 위치 지정’은 미지정 상품을 목록 순서대로 안내하며 클릭 또는 좌표 입력 후 Enter로 다음 상품에 진행한다. 중단·Escape는 완료한 좌표를 유지한다. 개별 수정·상품 추가창 열기·이미지 교체는 진행을 종료한다. 진행 안내는 role="status"로 제공하고, 현재 상품 행을 강조한다. 좌표 입력 중에는 행을 다시 렌더링하지 않아 포커스와 타이핑을 유지한다.

코디 생성은 Dialog 본문 안에 Field, native textarea.ui-textarea, select.ui-select와 독립된 작업 버튼을 조합한다. 공식 Field의 레이블·설명·오류 구성을 따른다. 생성 프롬프트는 기본 내용이 채워진 여러 줄 textarea에서 전체를 수정하며, 입력한 내용 그대로 전달한다. 품질 선택은 두 값만 필요한 native select를 사용하며 커스텀 팝업을 만들지 않는다. 후보 이미지와 채택·폐기 작업은 같은 다이얼로그의 별도 영역에 두고, 채택 전에는 편집기에 반영하거나 제목·해시태그 분석을 호출하지 않는다. ‘이 이미지 사용’은 이미지와 상품을 편집기에 반영한 직후 기존 업로드 이미지와 같은 제목·해시태그 분석을 시작한다. 분석 진행·결과·재시도는 편집기의 제안 영역에서 제공하며 분석이 이미지 사용이나 위치 지정을 막지 않는다. 채택한 상품의 위치는 미지정 상태로 시작하며, 클릭과 레이블이 있는 숫자 입력으로 사용자가 직접 지정한다. 이 화면의 참조 이미지·좌표 입력 배치는 vingle-admin.css의 ui-coordination-*가 소유한다.

코디 프롬프트 프리셋은 ui-action-row 안에 독립된 button.ui-button--outline을 두고 role="group"과 레이블·교체 안내를 제공한다. 공식 Button의 outline Usage와 native 버튼의 Tab·Enter·Space 동작을 따른다. 버튼은 선택 상태를 유지하지 않고 전체 textarea 값을 교체하는 작업이며, 적용 후 textarea에 포커스를 옮겨 바로 수정할 수 있게 한다. 프리셋 정의는 vingle-coordination-prompt-presets.js 한 곳에 두고, 기본값과 버튼 입력에 함께 사용한다.

상품 콜라주는 첫 번째 프리셋이며 화면 진입 시 기본 프롬프트로 채워진다. 모델 없이 상품 사진을 밝은 민트빛 흰색 배경에 배치하는 코디 보드다. 실제 참조 상품의 종류와 개수에 따라 의류·신발·가방·액세서리를 여백이 있는 구도로 배치하며, 참조에 없는 상품을 추가하지 않도록 전체 프롬프트에 명시한다.

배경색은 레이블·설명이 있는 native select.ui-select로 고르며, 직접 선택 시 native input[type="color"].ui-input--color와 HEX 입력을 독립된 Field로 표시한다. 공식 Input의 input type 전달과 Field의 레이블·설명·오류 구성을 따른다. 로컬 선택으로 색상 팝업은 브라우저에 맡기고 색상 입력의 폭·여백만 ui-input--color에서 지정한다. HEX 입력은 키보드로 임의의 색을 지정할 수 있게 하며, 잘못된 값은 해당 Field에 오류를 표시하고 생성을 막는다.

색상 선택은 전체 프롬프트의 배경색: 줄을 교체하거나 없으면 추가한다. 선택한 색은 프리셋 적용 시에도 반영되며, 서버에는 화면에 표시된 전체 프롬프트만 전달한다. 프롬프트대로는 현재 프롬프트 내용을 유지한다. 사용자가 배경색 줄을 직접 수정하면 선택도 프롬프트대로로 바꿔 수동 입력을 보존한다. 다른 입력과 마찬가지로 색상 변경 시 기존 후보와 진행 중인 생성 요청을 무효화한다.

공식 소스: Dialog. 공식 Usage·Composition·Scrollable Content의 Trigger, Content, Header, Title, Description, Footer, Close 구성을 따른다.

<button aria-controls="exampleDialog" aria-haspopup="dialog" class="ui-button ui-button--primary" id="exampleDialogTrigger" type="button">작업 열기</button>
<dialog aria-describedby="exampleDialogDescription" aria-labelledby="exampleDialogTitle" class="ui-dialog" id="exampleDialog">
<div class="ui-dialog__header">
<div><h2 id="exampleDialogTitle">작업 제목</h2><p id="exampleDialogDescription">작업 설명</p></div>
<button aria-label="창 닫기" class="ui-button ui-button--ghost ui-button--icon" data-ui-dialog-close data-ui-tooltip="닫기" type="button"><svg aria-hidden="true" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</div>
<div class="ui-dialog__content">작업 내용</div>
<div class="ui-dialog__footer"><button class="ui-button ui-button--outline" data-ui-dialog-close type="button">닫기</button></div>
</dialog>
  • 화면 module에서 initializeDialog(dialog, trigger)로 연결한다. 공통 helper는 열기, data-ui-dialog-close, backdrop 클릭, 닫은 뒤 열기 버튼으로 포커스 복원을 소유한다. 열 때 이전 returnValue를 초기화한다. 비동기 검증·조회 후 열어야 하는 화면은 { openOnClick: false }를 세 번째 인자로 전달하고 성공 시 화면 module에서 showModal()을 호출한다. 닫기와 포커스 복원은 같은 helper를 사용한다.
  • 로컬 선택으로 Base UI portal 대신 native showModal()을 사용한다. modal의 배경 inert 처리, Tab 포커스 제한과 Escape 닫기는 브라우저에 맡긴다. autofocus로 첫 입력을 지정할 수 있으며, 제목·설명은 aria-labelledby·aria-describedby로 연결한다.
  • ui-dialog__content만 스크롤하고 header와 선택적인 footer는 보이는 상태로 유지한다. 기본 폭은 40rem, 입력과 후보를 나란히 비교하는 ui-dialog--wide는 96rem이다. 모바일에서는 viewport 여백과 최대 높이를 유지한다. 화면별 본문 열 구성은 화면 stylesheet가 소유한다.
  • 공통 실행 확인은 confirmDangerousAction()과 vingle-confirm-dialog를 사용한다. 바깥 native dialog가 최대 30rem의 폭과 모바일 여백을 정하고 기본 padding을 제거한다. 내부 form.ui-confirm-dialog는 폭 100%와 본문 padding만 맡아 좁은 화면에서도 가로 스크롤 없이 표시한다.
  • 코디 편집 화면은 코디 정보 Card와 상품 연결 Card를 위에서부터 순서대로 배치한다. 코디 이미지 파일 선택기 아래의 플라스크 아이콘 생성 버튼이 coordination-image-generation-dialog.html fragment를 연다. 이 다이얼로그는 하단 닫기 버튼 없이 header의 X, Escape, backdrop으로 닫는다. 닫아도 입력·후보·진행 중인 요청은 유지하고, 새로고침·화면 이탈 시 사라짐을 안내한다. 이 이미지 사용은 이미지와 상품을 채택한 뒤 창을 닫고 편집기의 ‘순서대로 위치 지정’ 버튼으로 포커스를 옮긴다. 제목·해시태그는 편집기의 적용하기로 별도 반영한다.

공식 소스: 고정 버전 Sheet. 공식 Usage·Composition·Side와 Base UI Dialog의 modal=false, disablePointerDismissal 접근성 계약을 확인했다.

<button aria-controls="exampleDrawer" aria-expanded="false" aria-haspopup="dialog" class="ui-button ui-button--outline" id="exampleDrawerTrigger" type="button">목록 열기</button>
<dialog aria-describedby="exampleDrawerDescription" aria-labelledby="exampleDrawerTitle" aria-modal="false" class="ui-drawer" id="exampleDrawer">
<div class="ui-drawer__header">
<div><h2 autofocus id="exampleDrawerTitle" tabindex="-1">보조 목록</h2><p id="exampleDrawerDescription">편집하며 참고할 내용입니다.</p></div>
<button aria-label="목록 닫기" class="ui-button ui-button--ghost" data-ui-drawer-close type="button">닫기</button>
</div>
<div class="ui-drawer__content">목록과 작업 버튼</div>
</dialog>
  • 로컬 선택으로 Sheet의 Trigger·Header·Title·Description·Close 구성을 native dialog.show()에 적용한다. showModal()·backdrop·배경 inert·포커스 trap은 사용하지 않는다. 바깥 클릭·포커스 이동에도 목록을 유지한다. 기존 modal navigation Sheet는 변경하지 않는다.
  • 공통 helper initializeNonModalDrawer(drawer, trigger)가 열기 버튼의 토글·aria-expanded, data-ui-drawer-close, Escape를 처리한다. 초기 포커스는 제목의 autofocus, tabindex="-1"로 지정하여 닫기 버튼의 툴팁이 자동 표시되지 않게 한다. Tab으로 닫기 버튼에 이동하거나 hover하면 기존 툴팁을 표시한다. 다른 modal이 열려 있으면 Escape는 그 modal이 처리한다. 닫기 시 포커스 복원은 native dialog에 맡겨 배경 입력에 있는 포커스를 빼앗지 않는다.
  • 기본 폭은 22rem이며 오른쪽에 표시한다. 1024px 이하에서는 아래쪽에 최대 45dvh로 표시한다. Header는 유지하고 Content만 스크롤한다. 본문을 가리지 않기 위한 여백은 화면 레이아웃이 소유한다. 드래그·스와이프 동작은 제공하지 않는다.
  • 캠페인 알림 발송은 버튼 안에 회원 이름 or 고객과 ${memberName}을 표시한다. 삽입 동작은 장식용 플러스 SVG 하나로 표시하고 버튼의 aria-label에 삽입 의미를 제공한다. Header·항목 아래의 설명과 커서 안내는 생략하고 삽입 결과는 스크린 리더에만 알린다. 마지막으로 선택한 제목·본문의 커서 또는 선택 영역에 삽입하고 해당 입력으로 포커스를 돌린다. 삽입 위치는 기존 Toggle Group으로도 선택한다. 다른 선택 필드에는 삽입하지 않으며, 삽입 시 기존 input 이벤트로 미리보기를 무효화한다. 요청 처리 중에는 drawer의 삽입 Fieldset도 비활성화한다.
  • 공통 미리보기에서 기본·hover·focus·disabled·오류와 배경 입력·Tab·Escape·modal 중첩을 확인한다. 실제 알림 화면에서는 선택 영역 교체·줄바꿈 보존·처리 중 비활성과 모바일 배치도 확인한다.

공식 소스: Tooltip. 공식 Usage·Composition의 Trigger와 Content 구성, hover·키보드 focus 표시를 따른다.

  • 레이블은 아이콘 버튼의 aria-label, 보조 설명은 data-ui-tooltip에 둔다. initializeTooltips()가 공통 #controlPanelTooltip[role="tooltip"]를 aria-describedby로 연결한다. 필수 정보와 실행 버튼을 tooltip에 넣지 않는다.
  • 로컬 선택으로 React portal 대신 native Popover API의 popover="manual"과 showPopover()·hidePopover()를 사용한다. Tooltip은 표시할 때 브라우저 top layer에 올라가므로 native modal 앞에 보이며, Escape 닫기나 modal 포커스를 방해하지 않는다. 관련 dialog가 닫히면 tooltip도 숨긴다.
  • 공통 tooltip은 ui-tooltip의 모양을 사용하며 native popover 기본 margin·inset·border를 초기화한다. 위쪽 중앙을 우선하고 viewport 상단 공간이 부족하면 아래에 표시하며, 좌우는 viewport 안에 둔다. 공식 화살표는 로컬 구현에서 생략한다.
  • 컴포넌트 미리보기는 실제 공통 tooltip helper를 연결한다. 넓은 dialog의 X 버튼에서 hover·focus, dialog 닫기 후 숨김, 데스크톱·모바일 경계를 확인한다.

캠페인 알림 발송 화면은 기존 Card·Field·Textarea·Alert 구성을 재사용한다. 발송 카드 위의 기본 안내는 표시하지 않고 API 오류·등록 결과가 있을 때만 Alert를 표시한다. 미리보기 후 입력이 바뀌면 카드 안에서 재확인을 안내한다. 입력은 대상 회원 ID → 제목 → 본문 → 이미지 선택 → 이동 경로 → 추가 데이터 순으로 배치한다. 대체어 목록 버튼은 제목 입력 오른쪽에 기존 ui-input-group으로 연결한다. 본문 아래의 예시 안내는 표시하지 않는다. 이미지 선택은 기존 코디 편집기처럼 레이블이 있는 native input[type="file"][accept="image/*"].ui-input.ui-input--file을 사용하고, 선택 해제 버튼으로 이미지를 제거한다. 선택 파일은 미리보기 전에 업로드하며 반환 URL을 미리보기·발송 등록에 재사용한다. 같은 파일로 재확인할 때는 다시 업로드하지 않고 파일을 바꾸거나 해제하면 기존 URL과 미리보기를 무효화한다. 업로드 중에는 입력·중복 요청을 막고 실패는 이미지 Field에 표시해 재시도할 수 있게 한다. 공통 미리보기에서 파일 선택의 기본·hover·focus·disabled·오류와 선택 해제를 확인한다. extras는 문자열 값만 허용하는 JSON textarea로 입력하며, 오류는 Field의 aria-invalid와 연결된 메시지에 표시한다. 미리보기는 조회 성공 후 native dialog.ui-dialog.ui-campaign-preview로 열고 대상 요약·제외 ID·최대 20개의 Card와 공통 선택 항목을 표시한다. 폭은 기본 Dialog의 40rem, 최대 높이는 44rem으로 제한한다. Header·Content·Footer의 padding은 1rem, 문구 Card의 padding은 .75rem으로 맞춘다. Dialog 제목은 1rem, 본문·회원 정보·문구 제목·JSON·버튼은 .875rem을 사용하고 제목은 굵기로 구분한다. 실제 화면과 미리보기 모두 vingle-admin.css의 화면 전용 조합을 재사용한다. 문구 Card 뒤에 이동 경로·추가 데이터를 표시하며 일반 안내문은 생략한다. 유효 대상이 없는 경우에만 빈 상태를 안내한다. 본문만 스크롤하며 Header와 발송 등록 Footer는 유지하고 하단 닫기 버튼은 두지 않는다. 초기 포커스는 제목으로 두고, Escape·배경 클릭·상단 X 버튼으로 닫은 뒤 우측 정렬된 미리보기 버튼으로 돌아간다. 본문은 white-space: pre-wrap, JSON과 긴 경로는 줄바꿈해 표시한다. 대규모 제외 ID는 native Collapsible 안의 읽기 전용 textarea로 제공해 ID마다 DOM을 만들지 않는다. 이미지 미리보기는 native img와 실패 안내를 사용하며, 등록 확인에는 기존 confirmDangerousAction()을 사용한다. 확인 취소 시 미리보기를 유지하고, 등록 실패는 미리보기 안에 표시해 재시도할 수 있다. 등록 성공 시 미리보기를 닫고 페이지에 결과를 표시한다. 이미 미리보기를 닫은 뒤 요청이 실패하면 페이지에 오류를 표시한다. 원격 이미지에 referrer를 보내지 않고 이미지 로딩 실패는 발송 내용 입력을 유지한 채 안내한다. 공식 Field·Card·Textarea·Alert의 레이블·설명·오류·Header/Content/Footer 구성을 따르고 별도의 공통 variant는 추가하지 않는다.

공식 소스: Textarea, Field. 공식 Textarea의 Field 조합, disabled, invalid 예제와 비교했다.

<div class="ui-form-field">
<label class="ui-form-field__label" for="sellerMemo">셀러 특이사항</label>
<textarea aria-describedby="sellerMemoDescription sellerMemoError" class="ui-textarea" id="sellerMemo" maxlength="10000" rows="6"></textarea>
<span class="ui-form-field__description" id="sellerMemoDescription">최대 10,000자까지 입력할 수 있습니다.</span>
<span class="ui-form-field__message" id="sellerMemoError" role="alert"></span>
</div>
  • native label의 for와 textarea의 id를 연결한다. 도움말과 오류에는 aria-describedby를 제공한다. 오류 상태는 setFieldError()로 표시한다.
  • 값은 .value로 읽고 쓴다. 텍스트를 HTML로 렌더링하지 않으며 공백과 줄바꿈을 임의로 제거하지 않는다.
  • 조회·저장 중에는 textarea를 disabled로 두고 form에 aria-busy를 제공한다. 저장 버튼은 setButtonPending()을 사용한다.
  • 공식 구현의 field-sizing: content 자동 높이 대신 로컬 CSS의 최소 높이 6rem, rows, 세로 resize를 사용한다. focus ring, disabled 배경·opacity, 오류 border는 기존 공통 CSS를 따른다.
  • 미리보기의 기본·disabled·오류 조합과 Tab 이동을 데스크톱·모바일에서 확인한다.

공식 소스: Separator. 공식 Usage의 단일 요소 구성, 가로 구분 예제와 Base UI의 접근성·orientation 계약을 확인했다.

<section aria-labelledby="sellerOverviewTitle">…</section>
<hr class="ui-separator">
<section aria-labelledby="sellerDetailOverviewTitle">…</section>
  • 로컬 구현은 가로 구분선만 지원한다. 공식 구현의 border 색상, 1px 높이, 전체 너비를 공통 CSS로 옮긴다. 구분선 전후 여백은 화면 레이아웃이 소유한다.
  • React 컴포넌트 대신 native hr의 separator 의미를 사용한다. 단순 장식용 구분선에는 aria-hidden="true"를 지정한다.
  • 비대화형 요소이므로 Tab 순서에 포함하지 않고 hover·focus·disabled·오류 상태를 만들지 않는다. 미리보기와 실제 화면에서 데스크톱·모바일 배치 및 주변 요소의 Tab 이동을 확인한다.

공식 소스: Collapsible. 공식 Usage와 Base UI의 Root·Trigger·Panel 구성, native details의 키보드·열림 상태 계약을 확인했다.

<details class="ui-collapsible">
<summary>셀러 상세 정보</summary>
<dl>…</dl>
</details>
  • 공식 Root·Trigger·Content 구성을 native details·첫 번째 자식 summary·나머지 본문으로 옮긴다. React 상태와 별도 버튼 대신 브라우저가 열림 상태와 키보드 동작을 소유한다.
  • open 속성으로 초기 펼침 상태를 정한다. 셀러 상세 화면은 기본적으로 접으며, 데이터 갱신은 내부 목록만 교체해 사용자가 선택한 접힘 상태를 유지한다.
  • native marker를 유지하고 summary 안에 별도 버튼·링크를 넣지 않는다. Tab으로 제목에 이동하고 Enter·Space로 조작한다. aria-expanded를 수동으로 중복 관리하지 않는다.
  • 공통 hover와 focus ring을 사용한다. 셀러 상세 정보 제목은 화면 CSS에서 hover 밑줄을 제거하고 focus ring은 유지한다. 본문 여백과 제목 크기·전체 너비 클릭 영역은 화면 레이아웃이 소유한다. 로컬 구현에는 disabled·오류 variant나 펼침 애니메이션이 없다.
  • 미리보기의 접힘·펼침 상태와 hover·focus를 데스크톱·모바일에서 확인한다. 실제 화면에서는 접은 상태의 데이터 갱신과 펼친 뒤의 값도 확인한다.

공식 소스: Table.

캠페인 알림 발송 내역은 기존 Table·Collapsible·Pagination 조합을 사용한다. 상단 안내와 목록 설명·조회 완료 시각은 표시하지 않으며 조회 상태 Alert는 로딩·오류에만 사용한다. 캠페인 ID·등록일·제목과 4개 지표는 표에 유지하고, 제목 셀 안의 native details에 원본 문구·선택 항목·이미지를 둔다. 등록 시 확정한 수신자 수와 그중 푸시 권한이 있는 기기를 가진 회원 수를 구분하고 두 경로 중 하나라도 읽은 회원 수와 최초 읽음이 푸시 클릭인 회원 수를 표시한다. 내역을 먼저 읽은 뒤 푸시를 클릭하면 추가 집계하지 않는다. 공식 Table의 행·열 구조와 Collapsible의 Trigger·Content 구성을 따르며 새 공통 variant는 추가하지 않는다. 좁은 화면에서는 표 영역만 가로 스크롤하고, 새로고침·이전·다음 버튼은 표 밖에 둔다. 펼친 문구는 줄바꿈을 보존하고 문자열을 HTML로 해석하지 않는다. 로딩·빈 목록·오류·disabled 상태는 기존 공통 UI를 사용한다.

<div aria-labelledby="resultsTitle" class="ui-table-scroll" role="region" tabindex="0">
<table class="ui-table ui-table--compact">
<caption class="sr-only">브랜드 검색 결과</caption>
<thead><tr><th scope="col">ID</th><th scope="col">표시명</th></tr></thead>
<tbody><tr><td>1</td><td>Nike</td></tr></tbody>
</table>
</div>
  • 진행은 ui-alert--info, 제안 도착은 ui-alert--success, 실패는 ui-alert--error를 사용한다. 실행 버튼의 진행 상태에는 setButtonPending()을 사용한다.
  • 공식 구현은 컨테이너에 role="alert"를 지정한다. 로컬 비동기 제안은 입력을 방해하지 않도록 상태 문구에 role="status"와 aria-atomic="true"를 지정해 정중하게 알린다. 작업 버튼은 live region 밖에 둔다.
  • 공식 AlertAction의 우측 상단 배치 대신 본문 아래의 ui-alert__actions를 사용한다. 모바일에서는 버튼이 줄바꿈되며 Tab·Enter·Space로 실행할 수 있다.
  • 코디 편집 화면은 이미지 업로드 또는 생성 이미지 채택 시 분석을 시작하되 입력값은 적용 버튼을 눌렀을 때만 변경한다. 생성 후보는 분석하지 않는다. 이미지 교체·제안 닫기·화면 이탈 시 이전 요청을 취소해 오래된 제안을 적용하지 않는다. 이미지 생성 다이얼로그의 입력 변경·닫기·후보 폐기는 현재 편집기 이미지의 분석에 영향을 주지 않는다.
  • 공식 구성처럼 native table, caption, thead, tbody, tr, th, td와 가로 스크롤 wrapper를 사용한다. 열 제목에는 scope="col"을 지정한다.
  • 로컬 표는 열의 가독성을 위해 기본 최소 너비 65rem을 사용한다. 열이 적은 조회 결과에는 ui-table--compact(40rem)를 사용한다. 공식 구현에는 이 최소 너비 variant가 없다.
  • 스크롤 영역에 tabindex="0"과 이름이 있는 region을 제공해 키보드로 가로 스크롤할 수 있게 한다. 빈 목록은 하나의 ui-table-empty 셀과 열 수에 맞는 colspan으로 표시한다.
  • 행 hover와 스크롤 영역 focus를 미리보기에서 확인한다. 조회 상태는 표 밖의 메시지와 aria-busy로 표시하며 조회 결과의 셀을 버튼처럼 취급하지 않는다.
  1. 이 문서의 선택표와 현재 템플릿을 검색한다. 기존 로컬 구현이 있으면 그것을 재사용한다.
  2. 없다면 공식 문서의 Usage, Composition, Accessibility, 상태별 예제와 고정된 소스를 확인한다. 이름이나 스크린샷만 보고 구현하지 않는다.
  3. 참고 컴포넌트·로컬 HTML 구성·공식과 다른 점을 이 문서에 기록한다. 공통 스타일은 ui-* CSS에, 화면 고유 배치는 해당 화면 CSS에 둔다.
  4. 미리보기에 기본·hover/focus·disabled·오류 등 해당 상태를 추가하고 데스크톱·모바일 폭 및 키보드로 확인한다. 변경한 화면도 확인한다.
  5. 화면의 API와 업무 규칙은 컴포넌트 교체만으로 바꾸지 않는다. ./gradlew :core:check를 실행한다.