diff --git a/frontend/ongi/assets/images/tutorial/tutorial1.png b/frontend/ongi/assets/images/tutorial/tutorial1.png new file mode 100644 index 0000000..01aee42 Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial1.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial10.png b/frontend/ongi/assets/images/tutorial/tutorial10.png new file mode 100644 index 0000000..5252af8 Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial10.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial2.png b/frontend/ongi/assets/images/tutorial/tutorial2.png new file mode 100644 index 0000000..b088b0a Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial2.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial3.png b/frontend/ongi/assets/images/tutorial/tutorial3.png new file mode 100644 index 0000000..0cff337 Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial3.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial4.png b/frontend/ongi/assets/images/tutorial/tutorial4.png new file mode 100644 index 0000000..da63246 Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial4.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial5.png b/frontend/ongi/assets/images/tutorial/tutorial5.png new file mode 100644 index 0000000..7c35ae6 Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial5.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial6.png b/frontend/ongi/assets/images/tutorial/tutorial6.png new file mode 100644 index 0000000..22829a4 Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial6.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial7.png b/frontend/ongi/assets/images/tutorial/tutorial7.png new file mode 100644 index 0000000..153f3ba Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial7.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial8.png b/frontend/ongi/assets/images/tutorial/tutorial8.png new file mode 100644 index 0000000..b08f71b Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial8.png differ diff --git a/frontend/ongi/assets/images/tutorial/tutorial9.png b/frontend/ongi/assets/images/tutorial/tutorial9.png new file mode 100644 index 0000000..4710292 Binary files /dev/null and b/frontend/ongi/assets/images/tutorial/tutorial9.png differ diff --git a/frontend/ongi/lib/screens/login/login_ready_screen.dart b/frontend/ongi/lib/screens/login/login_ready_screen.dart index 18c755a..e179133 100644 --- a/frontend/ongi/lib/screens/login/login_ready_screen.dart +++ b/frontend/ongi/lib/screens/login/login_ready_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:ongi/core/app_background.dart'; import 'package:ongi/screens/bottom_nav.dart'; +import 'package:ongi/screens/tutorial_screen.dart'; class LoginReadyScreen extends StatefulWidget { final String username; @@ -19,7 +20,19 @@ class _LoginReadyScreenState extends State { void initState() { super.initState(); - _timer = Timer(const Duration(seconds: 2), () { + _timer = Timer(const Duration(seconds: 2), () async { + if (!mounted) return; + + // 튜토리얼 화면 표시 (매번 표시) + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => TutorialScreen( + imageAssets: List.generate(10, (i) => 'assets/images/tutorial/tutorial${i + 1}.png'), + ), + fullscreenDialog: true, + ), + ); + if (!mounted) return; Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const BottomNavScreen()), diff --git a/frontend/ongi/lib/screens/signup/ready_screen.dart b/frontend/ongi/lib/screens/signup/ready_screen.dart index 2bfcd5f..eb997b0 100644 --- a/frontend/ongi/lib/screens/signup/ready_screen.dart +++ b/frontend/ongi/lib/screens/signup/ready_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:ongi/core/app_background.dart'; import 'package:ongi/core/app_colors.dart'; import 'package:ongi/screens/bottom_nav.dart'; +import 'package:ongi/screens/tutorial_screen.dart'; class ReadyScreen extends StatelessWidget { const ReadyScreen({super.key}); @@ -46,11 +47,24 @@ class ReadyScreen extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), ), - onPressed: () => Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (_) => const BottomNavScreen()), - (Route route) => false, - ), + onPressed: () async { + // 튜토리얼 화면 표시 (매번 표시) + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => TutorialScreen( + imageAssets: List.generate(10, (i) => 'assets/images/tutorial/tutorial${i + 1}.png'), + ), + fullscreenDialog: true, + ), + ); + + if (!context.mounted) return; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const BottomNavScreen()), + (Route route) => false, + ); + }, child: const Text( '준비완료!', style: TextStyle( diff --git a/frontend/ongi/lib/screens/tutorial_screen.dart b/frontend/ongi/lib/screens/tutorial_screen.dart new file mode 100644 index 0000000..9c13854 --- /dev/null +++ b/frontend/ongi/lib/screens/tutorial_screen.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:ongi/core/app_colors.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ongi/core/app_email_background.dart'; + +class TutorialScreen extends StatefulWidget { + const TutorialScreen({super.key, required this.imageAssets}); + + final List imageAssets; + + // 앱 내부에서만 쓰는 고정 키 (버전 바꾸고 싶으면 문자열만 변경) + static const String _TutorialFlagKey = 'v1'; + + static Future showIfNeeded( + BuildContext context, { + required List imageAssets, + }) async { + final prefs = await SharedPreferences.getInstance(); + final seen = prefs.getBool(_TutorialFlagKey) ?? false; + if (seen) return false; + + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => TutorialScreen(imageAssets: imageAssets), + fullscreenDialog: true, + ), + ); + return true; + } + + @override + State createState() => _TutorialScreenState(); +} + +class _TutorialScreenState extends State { + PageController? _pageController; + int _index = 0; + + @override + void initState() { + super.initState(); + _pageController = PageController(); + + // 이미지 미리 로드(깜빡임 방지) + WidgetsBinding.instance.addPostFrameCallback((_) async { + for (final path in widget.imageAssets) { + await precacheImage(AssetImage(path), context); + } + }); + } + + @override + void dispose() { + _pageController?.dispose(); + _pageController = null; + super.dispose(); + } + + Future _finish() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(TutorialScreen._TutorialFlagKey, true); + if (mounted) Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final isLast = _index == widget.imageAssets.length - 1; + + return Scaffold( + // backgroundColor: AppColors.ongiOrange, + backgroundColor: Colors.transparent, + body: AppEmailBackground( + child: SafeArea( + child: Column( + children: [ + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only(right: 14), + child: GestureDetector( + onTap: _finish, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isLast ? null : Icons.skip_next_rounded, + color: AppColors.pillsItemBackground, + size: 38, + ), + if (!isLast) ...[ + Text( + '건너뛰기', + style: TextStyle( + color: AppColors.pillsItemBackground, + fontSize: 11, + height: 0.6, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + ), + ), + const SizedBox(height: 5), + Expanded( + child: _pageController != null + ? PageView.builder( + controller: _pageController, + itemCount: widget.imageAssets.length, + onPageChanged: (i) => setState(() => _index = i), + itemBuilder: (_, i) { + return Center( + child: InteractiveViewer( + child: Image.asset( + widget.imageAssets[i], + fit: BoxFit.contain, + width: double.infinity, + height: double.infinity, + ), + ), + ); + }, + ) + : const Center(child: CircularProgressIndicator()), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(widget.imageAssets.length, (i) { + final active = i == _index; + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.symmetric(horizontal: 4), + width: active ? 18 : 8, + height: 8, + decoration: BoxDecoration( + color: active ? Colors.white : Colors.white38, + borderRadius: BorderRadius.circular(999), + ), + ); + }), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), + child: SizedBox( + width: double.infinity, + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + minimumSize: const Size(double.infinity, 35), + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + onPressed: () async { + if (isLast) { + await _finish(); + } else { + final controller = _pageController; + if (controller != null && controller.hasClients) { + await controller.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + } + } + }, + child: Text( + isLast ? '시작하기' : '다음', + style: TextStyle( + fontSize: 27, + fontWeight: FontWeight.w400, + color: AppColors.ongiOrange, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/ongi/pubspec.yaml b/frontend/ongi/pubspec.yaml index 6a68c67..a118107 100644 --- a/frontend/ongi/pubspec.yaml +++ b/frontend/ongi/pubspec.yaml @@ -73,6 +73,7 @@ flutter: - assets/images/photobook_icon.png - assets/images/reward_products/ - assets/images/users/ + - assets/images/tutorial/ - assets/images/close_icon_black.svg - assets/images/close_icon_white.svg - assets/images/back_icon_black.svg