프로젝트 최초 커밋
This commit is contained in:
301
lib/screens/login/widgets/login_view.dart
Normal file
301
lib/screens/login/widgets/login_view.dart
Normal file
@@ -0,0 +1,301 @@
|
||||
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('로그인'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user