Flutter ์ฑ์์ ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ๋๋ฌ ์ง์ ์ ํ๋ฅผ ๊ฑธ ์ ์๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋จ๊ณ๋ณ๋ก ์์๋ณด๊ฒ ์ต๋๋ค.
1. ํ์ํ ํจํค์ง ์ค์น
๋จผ์ url_launcher ํจํค์ง๋ฅผ ์ค์นํด์ผ ํฉ๋๋ค. ์ด ํจํค์ง๋ ์ธ๋ถ ์ฑ(์ ํ, ๋ฌธ์, ๋ธ๋ผ์ฐ์ ๋ฑ)์ ์คํํ ์ ์๊ฒ ํด์ค๋๋ค.
pubspec.yaml์ ์์กด์ฑ ์ถ๊ฐ
dependencies:
flutter:
sdk: flutter
url_launcher: ^6.2.1 # ์ต์ ๋ฒ์ ์ฌ์ฉ
ํจํค์ง ์ค์น
flutter pub get
2. ํ๋ซํผ๋ณ ๊ถํ ์ค์
Android ์ค์
android/app/src/main/AndroidManifest.xml ํ์ผ์ ์ ํ ๊ถํ์ ์ถ๊ฐํฉ๋๋ค:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ์ ํ ๊ฑธ๊ธฐ ๊ถํ ์ถ๊ฐ -->
<uses-permission android:name="android.permission.CALL_PHONE" />
<application>
<!-- ๊ธฐ์กด ์ค์ ๋ค... -->
</application>
</manifest>
iOS ์ค์
ios/Runner/Info.plist ํ์ผ์ URL ์คํด์ ์ถ๊ฐํฉ๋๋ค:
<dict>
<!-- ๊ธฐ์กด ์ค์ ๋ค... -->
<!-- URL ์คํด ํ์ฉ ๋ชฉ๋ก -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>tel</string>
</array>
</dict>
3. ์ฝ๋ ๊ตฌํ
๊ธฐ๋ณธ ๊ตฌํ
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class PhoneCallExample extends StatelessWidget {
const PhoneCallExample({Key? key}) : super(key: key);
// ์ ํ๊ฑธ๊ธฐ ๋ฉ์๋
Future<void> _makePhoneCall(String phoneNumber) async {
print('์ ํ๊ฑธ๊ธฐ ์๋: $phoneNumber');
// 1. ์ ํ๋ฒํธ ๊ฒ์ฆ
if (phoneNumber.isEmpty) {
print('์ ํ๋ฒํธ๊ฐ ๋น์ด์์ต๋๋ค');
return;
}
try {
// 2. ์ ํ๋ฒํธ ์ ๋ฆฌ (ํ์ดํ, ๊ณต๋ฐฑ ๋ฑ ์ ๊ฑฐ)
String cleanNumber = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
// 3. ์ ํ URI ์์ฑ
final Uri phoneUri = Uri(scheme: 'tel', path: cleanNumber);
print('์ ๋ฆฌ๋ ์ ํ๋ฒํธ: $cleanNumber');
print('์ ํ URI: $phoneUri');
// 4. ์ ํ ์ฑ ์คํ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
if (await canLaunchUrl(phoneUri)) {
// 5. ์ ํ ์ฑ ์คํ
await launchUrl(phoneUri);
print('์ ํ ์ฑ ์คํ ์ฑ๊ณต');
} else {
print('์ ํ ์ฑ์ ์คํํ ์ ์์ต๋๋ค');
}
} catch (e) {
print('์ ํ๊ฑธ๊ธฐ ์๋ฌ: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('์ ํ๊ฑธ๊ธฐ ์์ ')),
body: Center(
child: ElevatedButton(
onPressed: () => _makePhoneCall('02-1234-5678'),
child: Text('์ ํ๊ฑธ๊ธฐ'),
),
),
);
}
}
๊ณ ๊ธ ๊ตฌํ (์๋ฌ ์ฒ๋ฆฌ ํฌํจ)
class AdvancedPhoneCall extends StatelessWidget {
Future<void> _makePhoneCallWithErrorHandling(
BuildContext context,
String phoneNumber
) async {
// 1. ์ ํ๋ฒํธ ์ ํจ์ฑ ๊ฒ์ฌ
if (phoneNumber.isEmpty || phoneNumber == '๋ฒํธ์์') {
_showSnackBar(context, '์ ํ๋ฒํธ๊ฐ ๋ฑ๋ก๋์ด ์์ง ์์ต๋๋ค', Colors.red);
return;
}
try {
// 2. ์ ํ๋ฒํธ ์ ๋ฆฌ
String cleanNumber = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
// 3. ํ๊ตญ ์ ํ๋ฒํธ ํ์ ๊ฒ์ฆ
if (!_isValidKoreanPhoneNumber(cleanNumber)) {
_showSnackBar(context, '์ฌ๋ฐ๋ฅด์ง ์์ ์ ํ๋ฒํธ ํ์์
๋๋ค', Colors.red);
return;
}
final Uri phoneUri = Uri(scheme: 'tel', path: cleanNumber);
// 4. ์ ํ ์ฑ ์คํ
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
_showSnackBar(context, '์ ํ๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค', Colors.green);
} else {
_showSnackBar(context, '์ ํ ์ฑ์ ์คํํ ์ ์์ต๋๋ค', Colors.red);
}
} catch (e) {
_showSnackBar(context, '์ ํ๊ฑธ๊ธฐ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: $e', Colors.red);
}
}
// ํ๊ตญ ์ ํ๋ฒํธ ํ์ ๊ฒ์ฆ
bool _isValidKoreanPhoneNumber(String phoneNumber) {
// ํด๋ํฐ: 010์ผ๋ก ์์ํ๋ 11์๋ฆฌ
// ์ผ๋ฐ์ ํ: 02, 031-063์ผ๋ก ์์
final RegExp mobilePattern = RegExp(r'^010\d{8}$');
final RegExp landlinePattern = RegExp(r'^0(2|[3-6][1-4])\d{7,8}$');
return mobilePattern.hasMatch(phoneNumber) ||
landlinePattern.hasMatch(phoneNumber);
}
// ์ค๋ต๋ฐ ํ์
void _showSnackBar(BuildContext context, String message, Color color) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: color,
duration: Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () => _makePhoneCallWithErrorHandling(context, '010-1234-5678'),
child: Text('ํด๋ํฐ์ผ๋ก ์ ํ'),
),
ElevatedButton(
onPressed: () => _makePhoneCallWithErrorHandling(context, '02-1234-5678'),
child: Text('์ผ๋ฐ์ ํ๋ก ์ ํ'),
),
],
);
}
}
4. ์๋ฌ ์ฒ๋ฆฌ ๋ฐ ์์ธ์ํฉ
์ฃผ์ ์์ธ์ํฉ๋ค
์ ํ๋ฒํธ๊ฐ ๋น์ด์๋ ๊ฒฝ์ฐ
if (phoneNumber.isEmpty || phoneNumber == '๋ฒํธ์์') {
// ์๋ฌ ๋ฉ์์ง ํ์
return;
}
์ ํ ์ฑ์ด ์ค์น๋์ง ์์ ๊ฒฝ์ฐ
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
} else {
// ์ ํ ์ฑ์ ์คํํ ์ ์์
}
์๋ชป๋ ์ ํ๋ฒํธ ํ์
String cleanNumber = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
if (cleanNumber.length < 10) {
// ๋๋ฌด ์งง์ ์ ํ๋ฒํธ
}
๋ก๊น ๊ณผ ๋๋ฒ๊น
Future<void> _makePhoneCallWithLogging(String phoneNumber) async {
print('๐ [์ ํ๊ฑธ๊ธฐ] ์์: $phoneNumber');
try {
String cleanNumber = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
print('๐ [์ ํ๊ฑธ๊ธฐ] ์ ๋ฆฌ๋ ๋ฒํธ: $cleanNumber');
final Uri phoneUri = Uri(scheme: 'tel', path: cleanNumber);
print('๐ [์ ํ๊ฑธ๊ธฐ] URI: $phoneUri');
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
print('๐ [์ ํ๊ฑธ๊ธฐ] ์ฑ๊ณต');
} else {
print('๐ [์ ํ๊ฑธ๊ธฐ] ์คํจ: ์ฑ ์คํ ๋ถ๊ฐ');
}
} catch (e, stackTrace) {
print('๐ [์ ํ๊ฑธ๊ธฐ] ์์ธ ๋ฐ์: $e');
print('๐ [์ ํ๊ฑธ๊ธฐ] ์คํํธ๋ ์ด์ค: $stackTrace');
}
}
5. ์ค์ ์ฌ์ฉ ์์
์์์ ์ฑ์์์ ๊ตฌํ
class RestaurantContactCard extends StatelessWidget {
final String restaurantName;
final String phoneNumber;
const RestaurantContactCard({
Key? key,
required this.restaurantName,
required this.phoneNumber,
}) : super(key: key);
Future<void> _callRestaurant(BuildContext context) async {
print('[$restaurantName] ์ ํ๊ฑธ๊ธฐ ์๋: $phoneNumber');
if (phoneNumber.isEmpty || phoneNumber == '๋ฒํธ์์') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$restaurantName์ ์ ํ๋ฒํธ๊ฐ ๋ฑ๋ก๋์ด ์์ง ์์ต๋๋ค'),
backgroundColor: Colors.red,
),
);
return;
}
try {
String cleanNumber = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
final Uri phoneUri = Uri(scheme: 'tel', path: cleanNumber);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$restaurantName์ ์ ํ๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('์ ํ ์ฑ์ ์คํํ ์ ์์ต๋๋ค'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('์ ํ๊ฑธ๊ธฐ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Text(restaurantName, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('์ ํ: $phoneNumber'),
SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _callRestaurant(context),
icon: Icon(Icons.phone),
label: Text('์ ํ๊ฑธ๊ธฐ'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
}
6. ์ฃผ์์ฌํญ ๋ฐ ํ
1. ํ ์คํธ ํ๊ฒฝ
- ์๋ฎฌ๋ ์ดํฐ์์๋ ์ ํ ๊ธฐ๋ฅ์ด ์๋ํ์ง ์์ต๋๋ค
- ๋ฐ๋์ ์ค์ ๊ธฐ๊ธฐ์์ ํ ์คํธํด์ผ ํฉ๋๋ค
2. ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
// ์ ํ๊ฑธ๊ธฐ ์ ํ์ธ ๋ค์ด์ผ๋ก๊ทธ
Future<void> _showCallConfirmDialog(BuildContext context, String phoneNumber) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('์ ํ๊ฑธ๊ธฐ'),
content: Text('$phoneNumber๋ก ์ ํ๋ฅผ ๊ฑฐ์๊ฒ ์ต๋๊น?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('์ทจ์'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_makePhoneCall(phoneNumber);
},
child: Text('์ ํ๊ฑธ๊ธฐ'),
),
],
);
},
);
}
3. ์ฑ๋ฅ ์ต์ ํ
// ์ ํ๋ฒํธ ์ ๋ฆฌ ํจ์๋ฅผ ๋ณ๋๋ก ๋ถ๋ฆฌ
String _cleanPhoneNumber(String phoneNumber) {
return phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
}
// ์บ์ฑ์ ํตํ ์ฑ๋ฅ ํฅ์
final Map<String, String> _cleanedNumbers = {};
String _getCleanedNumber(String original) {
return _cleanedNumbers.putIfAbsent(original, () => _cleanPhoneNumber(original));
}
๊ฒฐ๋ก
Flutter์์ ์ ํ๊ฑธ๊ธฐ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๊ฒ์ url_launcher ํจํค์ง์ ์ ์ ํ ๊ถํ ์ค์ ๋ง ์์ผ๋ฉด ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค. ํ์ง๋ง ์ค์ ์ฑ์์๋ ๋ค์ํ ์์ธ์ํฉ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ณ ๋ คํ ๊ตฌํ์ด ํ์ํฉ๋๋ค.
ํต์ฌ ํฌ์ธํธ
- url_launcher ํจํค์ง ์ฌ์ฉ
- ํ๋ซํผ๋ณ ๊ถํ ์ค์ ํ์
- ์ ํ๋ฒํธ ์ ํจ์ฑ ๊ฒ์ฆ
- ์ ์ ํ ์๋ฌ ์ฒ๋ฆฌ
- ์ค์ ๊ธฐ๊ธฐ์์ ํ ์คํธ
์ด ๊ฐ์ด๋๋ฅผ ๋ฐ๋ผ ๊ตฌํํ๋ฉด ์์ ์ ์ด๊ณ ์ฌ์ฉ์ ์นํ์ ์ธ ์ ํ๊ฑธ๊ธฐ ๊ธฐ๋ฅ์ ๋ง๋ค ์ ์์ต๋๋ค!