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.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 createState() => _AnimatedBoatIconState(); } class _AnimatedBoatIconState extends State with TickerProviderStateMixin { late AnimationController _boatGrowController; late Animation _boatScaleAnim; late AnimationController _boatFloatController; late Animation _boatFloatAnim; @override void initState() { super.initState(); _boatGrowController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1100), ); _boatScaleAnim = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _boatGrowController, curve: Curves.elasticOut), ); _boatFloatController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1800), ); _boatFloatAnim = Tween(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(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(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(context, listen: false); final onLoginSuccess = (context.findAncestorWidgetOfExactType() 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('로그인'), ), ); } }