302 lines
9.4 KiB
Dart
302 lines
9.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:superport/utils/constants.dart';
|
|
import 'package:superport/screens/common/theme_tailwind.dart';
|
|
import 'dart:math' as math;
|
|
import 'package:wave/wave.dart';
|
|
import 'package:wave/config.dart';
|
|
import 'package:superport/screens/login/controllers/login_controller.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
/// 로그인 화면 진입점 위젯 (controller를 ChangeNotifierProvider로 주입)
|
|
class LoginView extends StatelessWidget {
|
|
final LoginController controller;
|
|
final VoidCallback onLoginSuccess;
|
|
const LoginView({
|
|
Key? key,
|
|
required this.controller,
|
|
required this.onLoginSuccess,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ChangeNotifierProvider<LoginController>.value(
|
|
value: controller,
|
|
child: const _LoginViewBody(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 로그인 화면 전체 레이아웃 및 애니메이션 배경
|
|
class _LoginViewBody extends StatelessWidget {
|
|
const _LoginViewBody({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: Stack(
|
|
children: [
|
|
// wave 패키지로 wavy liquid 애니메이션 배경 적용
|
|
Positioned.fill(
|
|
child: WaveWidget(
|
|
config: CustomConfig(
|
|
gradients: [
|
|
[Color(0xFFF7FAFC), Color(0xFFB6E0FE)],
|
|
[Color(0xFFB6E0FE), Color(0xFF3182CE)],
|
|
[Color(0xFF3182CE), Color(0xFF243B53)],
|
|
],
|
|
durations: [4200, 5000, 7000],
|
|
heightPercentages: [0.18, 0.25, 0.38],
|
|
blur: const MaskFilter.blur(BlurStyle.solid, 8),
|
|
gradientBegin: Alignment.topLeft,
|
|
gradientEnd: Alignment.bottomRight,
|
|
),
|
|
waveAmplitude: 18,
|
|
size: Size.infinite,
|
|
),
|
|
),
|
|
Center(
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 40,
|
|
horizontal: 32,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.95),
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 32,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: const [
|
|
AnimatedBoatIcon(),
|
|
SizedBox(height: 32),
|
|
Text('supERPort', style: AppThemeTailwind.headingStyle),
|
|
SizedBox(height: 24),
|
|
LoginForm(),
|
|
SizedBox(height: 16),
|
|
SaveIdCheckbox(),
|
|
SizedBox(height: 32),
|
|
LoginButton(),
|
|
SizedBox(height: 48),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// 카피라이트를 화면 중앙 하단에 고정
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 32,
|
|
child: Center(
|
|
child: Opacity(
|
|
opacity: 0.7,
|
|
child: Text(
|
|
'Copyright 2025 CClabs. All rights reserved.',
|
|
style: AppThemeTailwind.smallText.copyWith(fontSize: 13),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 요트 아이콘 애니메이션 위젯
|
|
class AnimatedBoatIcon extends StatefulWidget {
|
|
final Color color;
|
|
final double size;
|
|
const AnimatedBoatIcon({
|
|
Key? key,
|
|
this.color = const Color(0xFF3182CE),
|
|
this.size = 80,
|
|
}) : super(key: key);
|
|
@override
|
|
State<AnimatedBoatIcon> createState() => _AnimatedBoatIconState();
|
|
}
|
|
|
|
class _AnimatedBoatIconState extends State<AnimatedBoatIcon>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _boatGrowController;
|
|
late Animation<double> _boatScaleAnim;
|
|
late AnimationController _boatFloatController;
|
|
late Animation<double> _boatFloatAnim;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_boatGrowController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1100),
|
|
);
|
|
_boatScaleAnim = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _boatGrowController, curve: Curves.elasticOut),
|
|
);
|
|
_boatFloatController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1800),
|
|
);
|
|
_boatFloatAnim = Tween<double>(begin: -0.08, end: 0.08).animate(
|
|
CurvedAnimation(parent: _boatFloatController, curve: Curves.easeInOut),
|
|
);
|
|
_boatGrowController.forward().then((_) {
|
|
_boatFloatController.repeat(reverse: true);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_boatGrowController.dispose();
|
|
_boatFloatController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: Listenable.merge([_boatGrowController, _boatFloatController]),
|
|
builder: (context, child) {
|
|
final double scale = _boatScaleAnim.value;
|
|
final double angle =
|
|
(_boatGrowController.isCompleted) ? _boatFloatAnim.value : 0.0;
|
|
return Transform.translate(
|
|
offset: Offset(
|
|
(_boatGrowController.isCompleted) ? math.sin(angle) * 8 : 0,
|
|
0,
|
|
),
|
|
child: Transform.rotate(
|
|
angle: angle,
|
|
child: Transform.scale(scale: scale, child: child),
|
|
),
|
|
);
|
|
},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: widget.color.withOpacity(0.18),
|
|
blurRadius: widget.size * 0.3,
|
|
offset: Offset(0, widget.size * 0.1),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
Icons.directions_boat,
|
|
size: widget.size,
|
|
color: widget.color,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 로그인 입력 폼 위젯 (ID, PW)
|
|
class LoginForm extends StatelessWidget {
|
|
const LoginForm({Key? key}) : super(key: key);
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final controller = Provider.of<LoginController>(context);
|
|
return Column(
|
|
children: [
|
|
TextField(
|
|
controller: controller.idController,
|
|
focusNode: controller.idFocus,
|
|
decoration: const InputDecoration(
|
|
labelText: 'ID',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
style: AppThemeTailwind.bodyStyle,
|
|
textInputAction: TextInputAction.next,
|
|
onSubmitted: (_) {
|
|
FocusScope.of(context).requestFocus(controller.pwFocus);
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: controller.pwController,
|
|
focusNode: controller.pwFocus,
|
|
decoration: const InputDecoration(
|
|
labelText: 'PW',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
style: AppThemeTailwind.bodyStyle,
|
|
obscureText: true,
|
|
textInputAction: TextInputAction.done,
|
|
onSubmitted: (_) {
|
|
// 엔터 시 로그인 버튼에 포커스 이동 또는 로그인 시도 가능
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 아이디 저장 체크박스 위젯
|
|
class SaveIdCheckbox extends StatelessWidget {
|
|
const SaveIdCheckbox({Key? key}) : super(key: key);
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final controller = Provider.of<LoginController>(context);
|
|
return Row(
|
|
children: [
|
|
Checkbox(
|
|
value: controller.saveId,
|
|
onChanged: (bool? value) {
|
|
controller.setSaveId(value ?? false);
|
|
},
|
|
),
|
|
Text('아이디 저장', style: AppThemeTailwind.bodyStyle),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 로그인 버튼 위젯
|
|
class LoginButton extends StatelessWidget {
|
|
const LoginButton({Key? key}) : super(key: key);
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final controller = Provider.of<LoginController>(context, listen: false);
|
|
final onLoginSuccess =
|
|
(context.findAncestorWidgetOfExactType<LoginView>() as LoginView)
|
|
.onLoginSuccess;
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: AppThemeTailwind.primaryButtonStyle.copyWith(
|
|
elevation: MaterialStateProperty.all(4),
|
|
shadowColor: MaterialStateProperty.all(
|
|
const Color(0xFF3182CE).withOpacity(0.18),
|
|
),
|
|
),
|
|
onPressed: () async {
|
|
final bool result = controller.login();
|
|
if (!result) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('로그인에 실패했습니다.')));
|
|
return;
|
|
}
|
|
// 로그인 성공 시 애니메이션 등은 필요시 별도 처리
|
|
onLoginSuccess();
|
|
},
|
|
child: const Text('로그인'),
|
|
),
|
|
);
|
|
}
|
|
}
|