1
0
forked from baron/baron-sso
Files
baron-sso/docs/UI_DESIGN_POLICY.md

118 lines
7.5 KiB
Markdown

# UI 버튼 위치 및 정렬 정책 (UI Button Placement Policy)
본 문서는 Baron SSO 프로젝트 내 모든 프론트엔드 애플리케이션(`userfront`, `devfront`, `adminfront`)에서 일관된 사용자 경험(UX)을 제공하기 위한 UI 버튼 배치 및 정렬 가이드라인을 정의합니다. (관련 이슈: [#308](https://gitea.hmac.kr/baron/baron-sso/issues/308))
## 1. 버튼 종류별 위치 (Button Placement by Type)
버튼의 성격에 따라 다음과 같이 배치합니다.
* **Primary Action (주요 동작)**
* **예시**: 저장, 확인, 제출, 생성 등
* **위치**: 우측 하단 (Bottom Right) 또는 모달/다이얼로그의 우측 끝에 배치합니다. 사용자의 시선 흐름(좌에서 우, 위에서 아래)에 따라 최종 액션을 우측 하단에서 마무리하도록 유도합니다.
* **Secondary Action (보조 동작)**
* **예시**: 취소, 닫기, 이전으로 등
* **위치**: Primary 버튼의 바로 **좌측**에 배치합니다.
* **Destructive Action (파괴적 동작)**
* **예시**: 삭제, 초기화, 권한 해제 등
* **위치 및 스타일**: 붉은색(Red/Destructive) 스타일을 적용하여 시각적으로 명확히 구분합니다. Primary/Secondary 그룹과 물리적으로 분리하거나 (예: 좌측 끝 배치), Secondary 액션 위치에 두되 색상으로 강력한 경고를 줍니다.
## 2. 정렬 기준 (Alignment Rules)
* **폼(Form) 하단 버튼 그룹**
* **기본 정렬**: 우측 정렬 (Right-aligned). "취소"는 왼쪽, "저장"은 오른쪽에 위치합니다. `[ 취소 ] [ 저장 ]`
* **리스트 아이템 내부 액션 버튼**
* **기본 정렬**: 리스트/테이블의 각 행(Row) 우측 끝에 배치합니다.
* 버튼 개수가 많을 경우 (3개 이상), 툴팁이나 Dropdown 메뉴(예: 햄버거 버튼 또는 "더보기" 아이콘)로 숨겨 UI 복잡도를 낮춥니다.
## 3. 반응형 고려 (Responsive Design)
* **모바일 환경 (Mobile / Small Screens)**
* 화면 너비가 좁은 모바일 기기(예: `userfront` 앱 환경, `devfront`/`adminfront`의 모바일 뷰)에서는 버튼 그룹을 **Full Width (화면 가득 채움)**로 변경하여 터치 영역을 확보합니다.
* 여러 개의 버튼이 있는 경우 세로로 스택(Stack)하며, **Primary Action을 맨 위**에, Secondary Action을 그 아래에 배치합니다.
* *데스크탑*: `[ 취소 ] [ 확인 ]`
* *모바일*:
```
[ 확인 ]
[ 취소 ]
```
## 4. 로딩 및 피드백 (Loading & Feedback)
* **중복 제출 방지**: 폼 전송이나 API 호출을 발생시키는 버튼을 클릭하면 즉각적으로 버튼을 비활성화(Disabled) 상태로 변경하여 다중 클릭을 방지합니다.
* **로딩 스피너**: 버튼 내부에 로딩 스피너(Spinner)를 표시하여 사용자에게 진행 상황을 시각적으로 알립니다.
* **스켈레톤 로딩(Skeleton Loading)**: 화면 진입 시 전체 데이터를 로딩해야 하는 경우, 무의미한 빈 화면(빈 공간) 대신 스켈레톤 UI를 사용하여 로딩 중임을 직관적으로 알리고 체감 대기 시간을 줄입니다.
* **작업 결과 안내**: 성공, 실패 등의 결과는 Toast 메시지 (혹은 스낵바)를 통해 화면 하단/상단에 일시적으로 노출하여 사용자가 흐름을 끊지 않고도 인지할 수 있게 돕습니다.
## 5. 빈 상태 처리 (Empty State)
* **빈 목록 안내**: 테이블이나 리스트에 표시할 항목이 없는 경우 단순히 빈 화면으로 두지 않고 중앙 정렬된 아이콘이나 일러스트와 함께 "데이터가 없습니다." 등의 명확한 문구를 표시합니다.
* **콜 투 액션(Call to Action)**: 데이터가 비어 있는 경우 생성 버튼(Primary Action)을 빈 상태 안내 영역 아래에 배치하여 사용자가 즉시 데이터를 추가할 수 있도록 유도합니다.
## 6. 오류 표시 (Error Handling)
* **인라인(Inline) 오류**: 폼(Form)의 유효성 검사에서 실패한 경우, 각 입력 필드 바로 아래에 붉은색 텍스트로 실패 원인을 명확하게 표시합니다.
* **포커스 이동**: 제출 버튼 클릭 시 오류가 있는 첫 번째 입력 필드로 자동 스크롤 하거나 포커스(Focus)를 이동시켜 수정이 용이하게 합니다.
## 7. 접근성 (Accessibility - a11y)
* **포커스 링(Focus Ring)**: 키보드를 통해 탐색(Tab)하는 사용자를 위해 버튼, 텍스트 입력창 등에 포커스가 갈 경우 외곽선을 명확히 렌더링(예: 파란색 테두리 등)해야 합니다. `outline: none`을 무분별하게 사용하지 않습니다.
* **대체 텍스트**: 텍스트 없이 아이콘만 존재하는 버튼(예: X 형태의 닫기 버튼)의 경우 반드시 `aria-label` 속성(또는 Flutter의 `Semantics`)을 사용하여 스크린 리더 사용자가 해당 버튼의 역할을 알 수 있게 해야 합니다.
## 8. 프론트엔드 환경별 구현 가이드 (Implementation Guide)
현재 운영 중인 프론트엔드 환경에 맞춘 구현 가이드라인입니다.
### 8.1. React 환경 (`devfront`, `adminfront`)
Tailwind CSS 기반의 컴포넌트를 사용하여 아래와 같이 구현합니다.
* **버튼 그룹 우측 정렬 (데스크탑)**: `flex justify-end gap-2`
* **반응형 (모바일 세로 배치, 데스크탑 가로 배치)**: `flex flex-col-reverse sm:flex-row sm:justify-end gap-2`
*(참고: `flex-col-reverse`를 사용하면 코드 상 먼저 작성된 취소 버튼이 모바일에서는 아래로, 나중에 작성된 확인 버튼이 위로 올라가게 배치할 수 있습니다.)*
* **코드 예시**:
```tsx
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 mt-4">
<Button variant="outline" onClick={onCancel} disabled={isLoading}>취소</Button>
<Button variant="default" onClick={onSave} disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
저장
</Button>
</div>
```
### 8.2. Flutter 환경 (`userfront`)
Flutter 프레임워크를 사용하는 환경에서는 화면 너비에 따라 위젯 구성을 동적으로 처리해야 합니다.
* **폼 하단 정렬**: `Row` 위젯과 `MainAxisAlignment.end` 사용.
* **반응형 대응**: 화면 너비(MediaQuery)에 따라 `Row`를 전체 너비를 채우는 `Column`으로 스위칭하거나, `OverflowBar` 위젯 등을 활용할 수 있습니다.
* **코드 예시**:
```dart
// 데스크탑/태블릿용 (우측 정렬)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(onPressed: isLoading ? null : onCancel, child: const Text('취소')),
const SizedBox(width: 8),
ElevatedButton(
onPressed: isLoading ? null : onSave,
child: isLoading
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('확인')
),
],
)
// 모바일용 (전체 너비 세로 배치)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: isLoading ? null : onSave,
child: isLoading
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('확인')
),
const SizedBox(height: 8),
TextButton(onPressed: isLoading ? null : onCancel, child: const Text('취소')),
],
)
```