프로젝트 최초 커밋

This commit is contained in:
JiWoong Sul
2025-07-02 17:45:44 +09:00
commit e346f83c97
235 changed files with 23139 additions and 0 deletions

View 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('로그인'),
),
);
}
}