- 환경/라우터 모듈에 approval_flow_v2 토글을 추가하고 FeatureFlags 초기화를 연결 (.env*, lib/core/**) - ApiClient 빌더·ApiRoutes 확장과 ApprovalRepositoryRemote 리팩터링으로 include·액션 시그니처를 정합화 - ApprovalFlow·ApprovalDraft 엔티티/레포/유즈케이스를 도입해 서버 초안과 단계 액션(승인·회수·재상신)을 지원 - Approval 컨트롤러·히스토리·템플릿 페이지와 공유 위젯을 재작성해 감사 로그·회수 UX·템플릿 CRUD를 반영 - Inbound/Outbound/Rental 컨트롤러·페이지에 결재 섹션을 삽입하고 대시보드 pending 카드 요약을 갱신 - SuperportDialog·FormField 등 공통 위젯을 보강하고 승인 위젯 가이드를 추가해 UI 가이드를 정리 - 결재/재고 테스트 픽스처와 단위·위젯·통합 테스트를 확장하고 flutter_test_config로 스테이징 호스트를 허용 - Approval Flow 레포트/플랜 문서를 업데이트하고 ApprovalFlow_System_Integration_and_ChangePlan.md를 추가 - 실행: flutter analyze, flutter test
147 lines
3.8 KiB
Dart
147 lines
3.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
|
|
/// Superport 전역에서 사용하는 토스트/스낵바 헬퍼.
|
|
class SuperportToast {
|
|
SuperportToast._();
|
|
|
|
/// 성공 처리 완료를 사용자에게 안내한다.
|
|
static void success(BuildContext context, String message) {
|
|
_show(context, message, _ToastVariant.success);
|
|
}
|
|
|
|
/// 정보성 피드백을 노출한다.
|
|
static void info(BuildContext context, String message) {
|
|
_show(context, message, _ToastVariant.info);
|
|
}
|
|
|
|
/// 주의가 필요한 상황을 경고한다.
|
|
static void warning(BuildContext context, String message) {
|
|
_show(context, message, _ToastVariant.warning);
|
|
}
|
|
|
|
/// 오류 발생 시 스낵바를 표시한다.
|
|
static void error(BuildContext context, String message) {
|
|
_show(context, message, _ToastVariant.error);
|
|
}
|
|
|
|
/// 공통 스낵바 렌더링 로직.
|
|
static void _show(
|
|
BuildContext context,
|
|
String message,
|
|
_ToastVariant variant,
|
|
) {
|
|
final theme = ShadTheme.of(context);
|
|
final (Color background, Color foreground) = switch (variant) {
|
|
_ToastVariant.success => (
|
|
theme.colorScheme.primary,
|
|
theme.colorScheme.primaryForeground,
|
|
),
|
|
_ToastVariant.info => (
|
|
theme.colorScheme.accent,
|
|
theme.colorScheme.accentForeground,
|
|
),
|
|
_ToastVariant.warning => (
|
|
theme.colorScheme.secondary,
|
|
theme.colorScheme.secondaryForeground,
|
|
),
|
|
_ToastVariant.error => (
|
|
theme.colorScheme.destructive,
|
|
theme.colorScheme.destructiveForeground,
|
|
),
|
|
};
|
|
|
|
final messenger = ScaffoldMessenger.maybeOf(context);
|
|
if (messenger == null) {
|
|
return;
|
|
}
|
|
messenger
|
|
..hideCurrentSnackBar()
|
|
..showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
message,
|
|
style: theme.textTheme.small.copyWith(
|
|
color: foreground,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
backgroundColor: background,
|
|
behavior: SnackBarBehavior.floating,
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
enum _ToastVariant { success, info, warning, error }
|
|
|
|
/// 기본 골격을 표현하는 스켈레톤 블록.
|
|
class SuperportSkeleton extends StatelessWidget {
|
|
const SuperportSkeleton({
|
|
super.key,
|
|
this.width,
|
|
this.height = 16,
|
|
this.borderRadius = const BorderRadius.all(Radius.circular(8)),
|
|
});
|
|
|
|
final double? width;
|
|
final double height;
|
|
final BorderRadius borderRadius;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = ShadTheme.of(context);
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 600),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.muted,
|
|
borderRadius: borderRadius,
|
|
),
|
|
width: width,
|
|
height: height,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 리스트 데이터를 대체하는 반복 스켈레톤 레이아웃.
|
|
class SuperportSkeletonList extends StatelessWidget {
|
|
const SuperportSkeletonList({
|
|
super.key,
|
|
this.itemCount = 6,
|
|
this.height = 56,
|
|
this.gap = 12,
|
|
this.padding = const EdgeInsets.all(16),
|
|
});
|
|
|
|
/// 렌더링할 스켈레톤 행 개수.
|
|
final int itemCount;
|
|
|
|
/// 각 항목 높이.
|
|
final double height;
|
|
|
|
/// 행 사이 간격.
|
|
final double gap;
|
|
|
|
/// 전체 패딩.
|
|
final EdgeInsetsGeometry padding;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: padding,
|
|
child: Column(
|
|
children: [
|
|
for (var i = 0; i < itemCount; i++) ...[
|
|
SuperportSkeleton(
|
|
height: height,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
if (i != itemCount - 1) SizedBox(height: gap),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|