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

7.5 KiB

UI 버튼 위치 및 정렬 정책 (UI Button Placement Policy)

본 문서는 Baron SSO 프로젝트 내 모든 프론트엔드 애플리케이션(userfront, devfront, adminfront)에서 일관된 사용자 경험(UX)을 제공하기 위한 UI 버튼 배치 및 정렬 가이드라인을 정의합니다. (관련 이슈: #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를 사용하면 코드 상 먼저 작성된 취소 버튼이 모바일에서는 아래로, 나중에 작성된 확인 버튼이 위로 올라가게 배치할 수 있습니다.)
  • 코드 예시:
    <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 위젯 등을 활용할 수 있습니다.
  • 코드 예시:
    // 데스크탑/태블릿용 (우측 정렬)
    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('취소')),
      ],
    )