# 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('취소')),
      ],
    )
    ```