전역 구조 리팩터링 및 테스트 확장

This commit is contained in:
JiWoong Sul
2025-09-29 01:51:47 +09:00
parent c00c0c9ab2
commit fef7108479
70 changed files with 7709 additions and 3185 deletions

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
const double _kFieldSpacing = 8;
const double _kFieldCaptionSpacing = 6;
/// 폼 필드 라벨과 본문을 일관되게 배치하기 위한 위젯.
class SuperportFormField extends StatelessWidget {
const SuperportFormField({
super.key,
required this.label,
required this.child,
this.required = false,
this.caption,
this.errorText,
this.trailing,
this.spacing = _kFieldSpacing,
});
final String label;
final Widget child;
final bool required;
final String? caption;
final String? errorText;
final Widget? trailing;
final double spacing;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final captionStyle = theme.textTheme.muted.copyWith(fontSize: 12);
final errorStyle = theme.textTheme.small.copyWith(
fontSize: 12,
color: theme.colorScheme.destructive,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: _FieldLabel(label: label, required: required),
),
if (trailing != null) trailing!,
],
),
SizedBox(height: spacing),
child,
if (errorText != null && errorText!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: _kFieldCaptionSpacing),
child: Text(errorText!, style: errorStyle),
)
else if (caption != null && caption!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: _kFieldCaptionSpacing),
child: Text(caption!, style: captionStyle),
),
],
);
}
}
/// `ShadInput`을 Superport 스타일에 맞게 설정한 텍스트 필드.
class SuperportTextInput extends StatelessWidget {
const SuperportTextInput({
super.key,
this.controller,
this.placeholder,
this.onChanged,
this.onSubmitted,
this.keyboardType,
this.enabled = true,
this.readOnly = false,
this.maxLines = 1,
this.leading,
this.trailing,
});
final TextEditingController? controller;
final Widget? placeholder;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final TextInputType? keyboardType;
final bool enabled;
final bool readOnly;
final int maxLines;
final Widget? leading;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return ShadInput(
controller: controller,
placeholder: placeholder,
enabled: enabled,
readOnly: readOnly,
keyboardType: keyboardType,
maxLines: maxLines,
leading: leading,
trailing: trailing,
onChanged: onChanged,
onSubmitted: onSubmitted,
);
}
}
/// `ShadSwitch`를 라벨과 함께 사용하기 위한 헬퍼.
class SuperportSwitchField extends StatelessWidget {
const SuperportSwitchField({
super.key,
required this.value,
required this.onChanged,
this.label,
this.caption,
});
final bool value;
final ValueChanged<bool> onChanged;
final String? label;
final String? caption;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (label != null) Text(label!, style: theme.textTheme.small),
const SizedBox(height: 8),
ShadSwitch(value: value, onChanged: onChanged),
if (caption != null && caption!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: _kFieldCaptionSpacing),
child: Text(caption!, style: theme.textTheme.muted),
),
],
);
}
}
class _FieldLabel extends StatelessWidget {
const _FieldLabel({required this.label, required this.required});
final String label;
final bool required;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final textStyle = theme.textTheme.small.copyWith(
fontWeight: FontWeight.w600,
);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: textStyle),
if (required)
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
'*',
style: theme.textTheme.small.copyWith(
color: theme.colorScheme.destructive,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
}