Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:presentation/designsystem/colors/app_colors.dart';
import 'package:presentation/designsystem/dimensions/app_dimensions.dart';
import 'package:presentation/designsystem/spacing/app_spacing.dart';
import 'package:presentation/designsystem/typography/app_typography.dart';

class AdBannerContent {
final String appName;
final String ctaLabel;
final ImageProvider? image;

const AdBannerContent({
required this.appName,
this.ctaLabel = 'install',
this.image,
});
}

/// 알약 모양의 인앱 광고 배너 (시안: onboarding-8.png 참고).
///
/// 실제 AdMob SDK 연동은 별도 작업으로 분리. 현 단계에서는 동일한 형태의
/// 정적 UI만 제공하므로, 추후 SDK 응답을 [AdBannerContent]에 매핑해 같은
/// 위젯을 재사용할 수 있다.
class AppAdBanner extends StatelessWidget {
final AdBannerContent content;
final VoidCallback? onTap;
final VoidCallback? onDismiss;

const AppAdBanner({
super.key,
required this.content,
this.onTap,
this.onDismiss,
});

@override
Widget build(BuildContext context) {
return Container(
height: 56.h,
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.h12,
vertical: 6.h,
),
decoration: BoxDecoration(
color: AppColors.backgroundNormal,
borderRadius: BorderRadius.circular(AppDimensions.radiusPill),
border: Border.all(color: AppColors.borderUnselected),
),
child: Row(
children: [
_buildImage(),
SizedBox(width: AppSpacing.h12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
content.appName,
style: AppTypography.body2Bold.copyWith(
color: AppColors.status100,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Row(
children: [
_chip('Ad'),
SizedBox(width: AppSpacing.h4),
_chip(content.ctaLabel),
],
),
],
),
),
SizedBox(width: AppSpacing.h8),
_circleIconButton(
icon: Icons.play_arrow_rounded,
onTap: onTap,
semanticLabel: '광고 보기',
),
SizedBox(width: AppSpacing.h4),
_circleIconButton(
icon: Icons.close_rounded,
onTap: onDismiss,
semanticLabel: '광고 닫기',
iconColor: AppColors.status100_50,
),
],
),
);
}

Widget _buildImage() {
final image = content.image;
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: AppColors.borderUnselected,
borderRadius: BorderRadius.circular(10),
),
clipBehavior: Clip.antiAlias,
child: image == null
? Center(
child: Text(
'image',
style: AppTypography.caption3Regular.copyWith(
color: AppColors.status100_50,
),
),
)
: Image(image: image, fit: BoxFit.cover),
);
}

Widget _chip(String label) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.h,
),
decoration: BoxDecoration(
color: AppColors.green100,
borderRadius: BorderRadius.circular(AppDimensions.radiusPill),
),
child: Text(
label,
style: AppTypography.caption3Regular.copyWith(
color: AppColors.green500,
height: 1.2,
),
),
);
}

Widget _circleIconButton({
required IconData icon,
required VoidCallback? onTap,
required String semanticLabel,
Color iconColor = AppColors.green500,
}) {
return Semantics(
button: true,
label: semanticLabel,
child: InkResponse(
onTap: onTap,
radius: 20.r,
child: Icon(icon, size: 22, color: iconColor),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:presentation/designsystem/colors/app_colors.dart';
import 'package:presentation/designsystem/components/ads/app_ad_banner.dart';
import 'package:presentation/designsystem/components/menu/app_selectable_menu_bar.dart';
import 'package:presentation/designsystem/components/toggles/app_goal_category_toggle.dart';
import 'package:presentation/designsystem/spacing/app_spacing.dart';
Expand All @@ -8,9 +9,18 @@ import 'package:presentation/viewmodels/goal/goal_management_viewmodel.dart';
import 'package:presentation/views/goal/widget/goal_manage_list_section.dart';
import 'package:provider/provider.dart';

class GoalManageBody extends StatelessWidget {
class GoalManageBody extends StatefulWidget {
const GoalManageBody({super.key});

@override
State<GoalManageBody> createState() => _GoalManageBodyState();
}

class _GoalManageBodyState extends State<GoalManageBody> {
bool _adDismissed = false;

static const _placeholderAd = AdBannerContent(appName: 'App Name');

@override
Widget build(BuildContext context) {
final viewModel = context.watch<GoalManagementViewModel>();
Expand All @@ -20,6 +30,13 @@ class GoalManageBody extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: AppSpacing.v20),
if (!_adDismissed)
AppAdBanner(
content: _placeholderAd,
onTap: () {},
onDismiss: () => setState(() => _adDismissed = true),
),
if (!_adDismissed) SizedBox(height: AppSpacing.v20),
_HeaderSection(viewModel: viewModel),
SizedBox(height: AppSpacing.v24),
viewModel.filteredGoals.isEmpty
Expand Down
53 changes: 43 additions & 10 deletions toondo/packages/presentation/lib/views/welcome/welcome_body.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
Expand All @@ -6,9 +7,9 @@ import 'package:presentation/viewmodels/welcome/welcome_viewmodel.dart';
import 'package:presentation/designsystem/colors/app_colors.dart';
import 'package:presentation/designsystem/spacing/app_spacing.dart';
import 'package:presentation/designsystem/typography/app_typography.dart';
import 'package:presentation/designsystem/components/buttons/app_google_login_button.dart';
import 'package:presentation/designsystem/components/buttons/app_kakao_login_button.dart';
import 'package:presentation/designsystem/components/buttons/app_phone_login_button.dart';
import 'package:presentation/views/mypage/help_guide/legal/legal_texts.dart';
import 'package:presentation/views/welcome/widget/legal_bottom_sheet.dart';

class WelcomeBody extends StatelessWidget {
const WelcomeBody({super.key});
Expand All @@ -28,7 +29,7 @@ class WelcomeBody extends StatelessWidget {
// SizedBox(height: AppSpacing.v52), // 소셜 로그인 사용 시 주석 해제
_buildButtons(context),
Spacer(),
_buildTermsText(),
_buildTermsText(context),
],
),
);
Expand Down Expand Up @@ -105,17 +106,49 @@ class WelcomeBody extends StatelessWidget {
);
}

Widget _buildTermsText() {
Widget _buildTermsText(BuildContext context) {
final base = AppTypography.caption3Regular.copyWith(
color: AppColors.green600,
);
final linkStyle = base.copyWith(
decoration: TextDecoration.underline,
decorationColor: AppColors.green600,
fontWeight: FontWeight.w600,
);
return SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
'버튼을 눌러 다음 화면으로 이동 시,\n서비스 이용 약관 및 개인정보 처리 방안에 동의한 것으로 간주합니다.',
textAlign: TextAlign.center,
style: AppTypography.caption3Regular.copyWith(
color: AppColors.green600
),
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: base,
children: [
const TextSpan(text: '버튼을 눌러 다음 화면으로 이동 시,\n'),
TextSpan(
text: '서비스 이용 약관',
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () => LegalBottomSheet.show(
context,
title: '서비스 이용 약관',
content: LegalTexts.termsOfService,
),
),
const TextSpan(text: ' 및 '),
TextSpan(
text: '개인정보 수집 및 이용',
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () => LegalBottomSheet.show(
context,
title: '개인정보 수집 및 이용',
content: LegalTexts.privacyPolicy,
),
),
const TextSpan(text: '에 동의한 것으로 간주합니다.'),
],
),
),
),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:presentation/designsystem/colors/app_colors.dart';
import 'package:presentation/designsystem/components/bottom_sheets/app_bottom_sheet.dart';
import 'package:presentation/designsystem/spacing/app_spacing.dart';
import 'package:presentation/designsystem/typography/app_typography.dart';

class LegalBottomSheet extends StatelessWidget {
final String title;
final String content;

const LegalBottomSheet({
super.key,
required this.title,
required this.content,
});

static Future<void> show(
BuildContext context, {
required String title,
required String content,
}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => LegalBottomSheet(title: title, content: content),
);
}

@override
Widget build(BuildContext context) {
return AppBottomSheet(
initialSize: 0.8,
maxSize: 0.95,
body: Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.h20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: AppTypography.h2Bold),
SizedBox(height: AppSpacing.v20),
Text(
content,
style: AppTypography.caption1Regular.copyWith(
color: AppColors.status100_75,
height: 1.55,
),
),
SizedBox(height: AppSpacing.v32),
],
),
),
);
}
}