-
Notifications
You must be signed in to change notification settings - Fork 74
Description
이슈 설명 (Issue description)
요약 (Summary)
kakao_flutter_sdk
최신 버전을 사용하여 FeedTemplate
으로 메시지를 공유할 때, Content
의 Link
객체에 androidExecutionParams
를 설정하여 딥링크 파라미터를 전달하고 있습니다.
Flutter 코드에서 생성된 KakaoTalk share URI
로그를 확인하면, template_args
안에 ANDROID_EXECUTION_URL
값으로 파라미터가 포함된 정상적인 딥링크가 생성된 것을 확인할 수 있습니다.
하지만, 실제로 앱에서 딥링크를 수신했을 때의 로그를 보면 Path
와 Query
가 모두 비어있는 상태로, 파라미터가 완전히 유실되는 문제가 발생합니다.
개발 환경 (Environment)
- OS: Windows 11
- IDE: IntelliJ IDEA 2025.1.3 (Ultimate Edition)
- 테스트 기기: Samsung Galaxy Note 10 Plus
- Android 버전: 12
- Flutter 버전: Flutter 3.32.6 (Stable), Dart 3.8.1
- kakao_flutter_sdk 버전: ^1.9.7+3
문제 현상 (Observed Behavior)
카카오톡 메시지의 콘텐츠 영역을 클릭하면 앱은 정상적으로 실행되지만, 파라미터를 수신하지 못합니다. main.dart
에서 수신한 링크를 출력한 로그는 아래와 같습니다.
I/flutter (30057): Kakao scheme stream: kakao2c0d9b4500ff55cb940d693d7edb800c://kakaolink
I/flutter (30057): Handling kakao deep link: kakao2c0d9b4500ff55cb940d693d7edb800c://kakaolink
I/flutter (30057): Scheme: kakao2c0d9b4500ff55cb940d693d7edb800c, Host: kakaolink, Path:
I/flutter (30057): Query: {}
I/flutter (30057): Path Segments: []
I/flutter (30057): No planId found in query parameters
예상 결과 (Expected Behavior)
앱에서 아래와 같이 Query
에 파라미터가 포함된 링크를 수신해야 합니다.
I/flutter (30057): Handling kakao deep link: kakao2c0d9b4500ff55cb940d693d7edb800c://kakaolink?id=58&planId=58
I/flutter (30057): Query: {id: 58, planId: 58}
I/flutter (30057): Found planId: 58
재현 과정 및 로그 (Steps to Reproduce & Logs)
-
아래와 같이
FeedTemplate
을 구성하여ShareClient.instance.shareDefault
를 호출합니다.
(자세한 코드는 아래 첨부파일recommended_diet_screen.dart
참고) -
이때,
shareDefault
가 반환하는uri
값을 로그로 출력하면,template_args
안에 정상적으로ANDROID_EXECUTION_URL
이 생성된 것을 확인할 수 있습니다.보내는 쪽 로그:
I/flutter (23704): KakaoTalk share URI: kakaolink://send?...&template_args=...%22%24%7BANDROID_EXECUTION_URL%7D%22%3A%22kakao2c0d9b4500ff55cb940d693d7edb800c%3A%2F%2Fkakaolink%3Fid%3D58%26planId%3D58%22...
-
카카오톡에서 공유된 메시지를 클릭하여 앱을 실행합니다.
-
앱의
main.dart
에 있는 딥링크 리스너에서Query: {}
로그를 확인합니다.
지금까지 시도해 본 것 (Things I've Already Tried)
- 카카오 개발자 콘솔의 키 해시가 실제 앱의 서명과 일치하는 것을 여러 번 확인했습니다.
- 패키지 명 또한 일치합니다.
AndroidManifest.xml
에scheme
과host="kakaolink"
를 포함한 인텐트 필터를 설정했고,pathPrefix="/"
를 추가한 필터도 설정했습니다.MainActivity
의android:launchMode="singleTop"
속성을 제거하여 테스트했지만 동일한 문제가 발생했습니다.FeedTemplate
의Content
링크에서webUrl
을 제거하고ExecutionParams
만 남겨 딥링크 목적을 명확히 했지만, 문제가 해결되지 않았습니다.
앱 ID (App ID)
1277569
플랫폼 (Platform)
Android 12
디바이스 (Device)
Samsung Galaxy Note 10 Plus
Version
Kakao flutter sdk 1.9.7+3
Flutter SDK
Flutter 3.32.6 (Channel Stable), Dart 3.8.1
재현 방법 (Steps to reproduce) - 최종 수정본
아래 내용으로 교체해서 붙여넣으세요.
1. FeedTemplate의 content.link 객체에 webUrl을 제외하고 androidExecutionParams만 설정합니다.
2. ShareClient.instance.shareDefault(template: template)를 호출하여 카카오톡으로 메시지를 공유합니다.
3. 공유된 메시지의 콘텐츠 영역을 클릭하여 앱을 딥링크로 실행합니다.
4. 앱의 딥링크 수신 리스너에서 전달받은 URI의 queryParameters와 pathSegments를 로그로 확인합니다.
5. 파라미터가 모두 비어있는 것(`Query: {}`, `Path Segments: []`)을 확인합니다.
6. **결과:** 앱이 실행되지만, 상세 식단 정보를 서버에서 불러오는 데 필요한 `planId`를 전달받지 못합니다. 이 때문에 데이터를 요청할 수 없어, 결국 상세 화면으로 이동하지 못하고 초기 화면에 머무릅니다.
코드 샘플 (Code Sample)
================================================================
recommended_diet_screen.dart
================================================================
// lib/screens/recommended_diet_screen.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Clipboard를 위해 추가
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:mobile/providers/font_size_provider.dart';
import 'package:provider/provider.dart';
import 'package:kakao_flutter_sdk/kakao_flutter_sdk.dart';
import 'package:url_launcher/url_launcher.dart';
class RecommendedDietScreen extends StatefulWidget {
final Map<String, dynamic> dietPlan;
const RecommendedDietScreen({super.key, required this.dietPlan});
@override
State<RecommendedDietScreen> createState() => _RecommendedDietScreenState();
}
class _RecommendedDietScreenState extends State<RecommendedDietScreen> {
bool _isLoading = false;
String? _latestPlanId; // 가장 최근에 저장된 planId를 저장할 변수
// [수정] URL을 생성하고 클립보드에 복사하는 디버그용 함수 (최근 planId 사용)
Future<void> _copyUrlToClipboard() async {
if (_latestPlanId != null) {
const String authority = '38edc8163b62.ngrok-free.app'; // 현재 사용중인 ngrok 주소
const String contextPath = '/krcore-fhir-app';
final Uri webUrl = Uri.https(authority, '$contextPath/app/diet-details/$_latestPlanId');
// 클립보드에 URL 복사
await Clipboard.setData(ClipboardData(text: webUrl.toString()));
// 사용자에게 복사 완료 알림
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('디버그 URL이 복사되었습니다: ${webUrl.toString()}')),
);
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('먼저 "카카오톡으로 나에게 보내기"를 눌러 식단을 저장해야 URL을 복사할 수 있습니다.')),
);
}
}
}
Future<void> _sendToMeKakaoTalk() async {
setState(() { _isLoading = true; });
try {
final client = HttpClient()..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
final httpClient = IOClient(client);
const String authority = '38edc8163b62.ngrok-free.app';
const String contextPath = '/krcore-fhir-app';
final Uri saveUrl = Uri.https(authority, '$contextPath/app/diet-plans');
final dietPlanForServer = {
'title': widget.dietPlan['date'],
'summary': "나의 건강 코치가 제안하는 맞춤 식단이에요.",
'disclaimer': widget.dietPlan['disclaimer'],
'meals': (widget.dietPlan['meals'] as List).map((meal) {
return {
'name': meal['name'],
'menu': meal['menu'],
'benefit': meal['benefit']
};
}).toList()
};
final response = await httpClient.post(
saveUrl,
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(dietPlanForServer),
);
if (response.statusCode != 200) {
throw Exception('서버에 식단을 저장하는 데 실패했습니다: ${response.body}');
}
final String planId = response.body;
_latestPlanId = planId;
print('🔥 Generated planId: $planId'); // 디버깅 로그 추가
final String title = widget.dietPlan['date'] ?? 'AI 맞춤 식단';
final String summary = "나의 건강 코치가 제안하는 맞춤 식단이 도착했어요.";
final List<dynamic> meals = widget.dietPlan['meals'] as List? ?? [];
final String firstMealMenu = (meals.isNotEmpty && meals.first is Map) ? "${meals.first['name']}: ${meals.first['menu']}" : "상세 식단 정보";
final Uri webUrl = Uri.https(authority, '$contextPath/app/diet-details/$planId');
print('🔥 Generated webUrl: $webUrl'); // 디버깅 로그 추가
// 딥링크 URI 생성 - kakaolink 패스 없이 직접 생성
// final directDeepLink = 'kakao2c0d9b4500ff55cb940d693d7edb800c://diet?id=$planId';
final directDeepLink = 'kakao2c0d9b4500ff55cb940d693d7edb800c://kakaolink?id=$planId';
print('🔥 Direct deepLink: $directDeepLink');
final template = FeedTemplate(
content: Content(
title: title,
description: summary,
imageUrl: Uri.parse(
'https://cdn.pixabay.com/photo/2017/04/04/18/03/healthy-food-2202338_1280.jpg'),
link: Link(
// webUrl: webUrl,
// mobileWebUrl: webUrl,
// 직접 딥링크 URL 지정
androidExecutionParams: {
'id': planId,
'planId': planId, // 백업용
},
iosExecutionParams: {
'id': planId,
'planId': planId, // 백업용
},
),
),
itemContent: ItemContent(
profileText: '나의 건강 코치',
items: [
ItemInfo(item: '오늘의 식단', itemOp: firstMealMenu),
],
),
buttons: [
Button(
title: '자세히 보기',
link: Link(
webUrl: webUrl,
mobileWebUrl: webUrl,
),
),
],
);
print('🔥 FeedTemplate created with androidExecutionParams: id=$planId'); // 디버깅 로그 추가
final bool isKakaoTalkSharingAvailable = await ShareClient.instance.isKakaoTalkSharingAvailable();
if (isKakaoTalkSharingAvailable) {
Uri uri = await ShareClient.instance.shareDefault(template: template);
print('🔥 KakaoTalk share URI: $uri'); // 디버깅 로그 추가
await ShareClient.instance.launchKakaoTalk(uri);
} else {
print('🔥 KakaoTalk not available, launching webUrl: $webUrl');
if (await canLaunchUrl(webUrl)) {
await launchUrl(webUrl, mode: LaunchMode.externalApplication);
}
}
} catch (e) {
print('🔥 카카오톡 공유 프로세스 실패: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('공유에 실패했습니다: ${e.toString()}')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
final meals = widget.dietPlan['meals'] as List? ?? [];
final disclaimer = widget.dietPlan['disclaimer'] as String?;
final fontSizeProvider = Provider.of<FontSizeProvider>(context);
return Scaffold(
backgroundColor: const Color(0xFFF2F2F7),
appBar: AppBar(
title: Text(widget.dietPlan['date'] ?? 'AI 추천 식단'),
backgroundColor: const Color(0xFFF2F2F7),
elevation: 0,
actions: [
IconButton(
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.text_decrease, size: 20,),
onPressed: () => fontSizeProvider.decrease(),
tooltip: '글자 작게',
),
IconButton(
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.replay, size: 20,),
onPressed: () => fontSizeProvider.reset(),
tooltip: '기본 크기로',
),
IconButton(
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.text_increase, size: 20,),
onPressed: () => fontSizeProvider.increase(),
tooltip: '글자 크게',
),
],
),
body: Stack(
children: [
ListView(
padding: const EdgeInsets.all(16.0),
children: [
...meals.map((meal) {
if (meal is Map<String, dynamic>) {
return MealCard(
mealName: meal['name'] ?? '',
menu: meal['menu'] ?? '',
benefit: meal['benefit'] ?? '',
fontSize: fontSizeProvider.fontSize,
);
}
return const SizedBox.shrink();
}).toList(),
const SizedBox(height: 16),
if (disclaimer != null && disclaimer.isNotEmpty)
_buildDisclaimerCard(context, disclaimer),
const SizedBox(height: 120), // 아래 버튼 공간 확보
],
),
if (_isLoading)
Container(
color: Colors.black.withOpacity(0.4),
child: const Center(
child: CircularProgressIndicator(),
),
),
],
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: _isLoading ? null : _sendToMeKakaoTalk,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFEE500),
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.chat_bubble),
SizedBox(width: 8),
Text('카카오톡으로 나에게 보내기',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold))
]),
),
const SizedBox(height: 8),
TextButton(
onPressed: _copyUrlToClipboard,
child: const Text('[디버그: URL 복사]'),
),
],
),
),
);
}
Widget _buildDisclaimerCard(BuildContext context, String text) {
return Card(
elevation: 0,
color: Theme.of(context).primaryColor.withOpacity(0.05),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
color: Colors.grey.shade600,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade800,
height: 1.5,
),
),
),
],
),
),
);
}
}
class MealCard extends StatelessWidget {
final String mealName;
final String menu;
final String benefit;
final double fontSize;
const MealCard({
super.key,
required this.mealName,
required this.menu,
required this.benefit,
required this.fontSize,
});
@override
Widget build(BuildContext context) {
final Map<String, IconData> mealIcons = {
'아침': Icons.light_mode_outlined,
'점심': Icons.wb_sunny_outlined,
'저녁': Icons.nights_stay_outlined,
};
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(mealIcons[mealName] ?? Icons.restaurant,
color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
mealName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Icon(Icons.lightbulb_outline,
color: Colors.amber.shade800, size: 20),
),
const SizedBox(width: 8),
Expanded(
child: Text(
menu,
style: TextStyle(
fontSize: fontSize + 3, fontWeight: FontWeight.w600),
),
),
],
),
const SizedBox(height: 16),
MarkdownBody(
data: benefit.replaceAll('\\n', '\n'),
extensionSet: md.ExtensionSet.gitHubWeb,
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context))
.copyWith(
p: TextStyle(
fontSize: fontSize,
height: 1.6,
color: Colors.grey.shade800),
strong: TextStyle(
fontWeight: FontWeight.bold,
fontSize: fontSize + 1,
color: Colors.black87),
),
),
],
),
),
);
}
}
================================================================
main.dart
================================================================
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:kakao_flutter_sdk/kakao_flutter_sdk.dart';
import 'package:mobile/providers/font_size_provider.dart';
import 'package:mobile/screens/onboarding_screen.dart';
import 'package:mobile/screens/recommended_diet_screen.dart';
import 'package:provider/provider.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
KakaoSdk.init(nativeAppKey: '2c0d9b4500ff55cb940d693d7edb800c');
runApp(
ChangeNotifierProvider(
create: (context) => FontSizeProvider(),
child: const HealthCareApp(),
),
);
}
class HealthCareApp extends StatefulWidget {
const HealthCareApp({super.key});
@override
State<HealthCareApp> createState() => _HealthCareAppState();
}
class _HealthCareAppState extends State<HealthCareApp> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
_initKakaoDeepLinkListener();
}
Future<void> _initKakaoDeepLinkListener() async {
// 1. 앱이 시작될 때 대기 중인 딥링크 확인 (앱이 종료된 상태에서 딥링크로 실행된 경우)
try {
final String? pendingUrl = await receiveKakaoScheme();
if (pendingUrl != null) {
print('🔥 Pending kakao scheme: $pendingUrl');
_handleKakaoDeepLink(pendingUrl);
}
} catch (e) {
print('🔥 Error receiving kakao scheme: $e');
}
// 2. 실행 중인 앱의 딥링크 수신 (앱이 실행 중일 때 딥링크가 호출된 경우)
kakaoSchemeStream.listen((url) {
if (url != null) {
print('🔥 Kakao scheme stream: $url');
_handleKakaoDeepLink(url);
}
}, onError: (e) {
print('🔥 Error in kakao scheme stream: $e');
});
}
void _handleKakaoDeepLink(String deepLink) {
final uri = Uri.parse(deepLink);
// 앱이 실제로 받은 전체 주소(uri)와 그 구성요소들을 로그로 출력한다.
print('🔥 Handling kakao deep link: $uri');
print('🔥 Scheme: ${uri.scheme}, Host: ${uri.host}, Path: ${uri.path}');
print('🔥 Query: ${uri.queryParameters}');
print('🔥 Path Segments: ${uri.pathSegments}');
// 카카오톡 스킴 확인
if (uri.scheme == 'kakao2c0d9b4500ff55cb940d693d7edb800c') {
final planId = uri.queryParameters['id'] ?? uri.queryParameters['planId'];
if (planId != null && planId.isNotEmpty) {
print('🔥 Found planId: $planId');
// 화면 전환
navigatorKey.currentState?.pushReplacement(
MaterialPageRoute(
builder: (context) => FutureBuilder<Map<String, dynamic>>(
future: _fetchDietPlan(planId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
backgroundColor: Color(0xFFF2F2F7),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('식단 정보를 불러오는 중...'),
],
),
),
);
} else if (snapshot.hasError) {
return RecommendedDietScreen(
dietPlan: {
'date': 'Error Fallback: $planId',
'meals': [],
'disclaimer': '서버에서 데이터를 불러오지 못했습니다.',
},
);
} else if (snapshot.hasData) {
return RecommendedDietScreen(dietPlan: snapshot.data!);
} else {
return const OnboardingScreen();
}
},
),
),
);
} else {
print('🔥 No planId found in query parameters');
}
} else {
print('🔥 Invalid kakao scheme: ${uri.scheme}');
}
}
Future<Map<String, dynamic>> _fetchDietPlan(String planId) async {
const String authority = 'e49074d0f639.ngrok-free.app';
const String contextPath = '/krcore-fhir-app';
final Uri url = Uri.https(authority, '$contextPath/app/diet-details/$planId');
print('Requesting URL: $url');
try {
final response = await http.get(url, headers: {'Accept': 'application/json'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to fetch diet plan: ${response.statusCode}');
}
} catch (e) {
print('Error fetching diet plan: $e');
return {
'date': 'Error Fallback: $planId',
'meals': [],
'disclaimer': '서버에서 데이터를 불러오지 못했습니다.',
};
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Personalized Health Care App',
navigatorKey: navigatorKey,
home: const OnboardingScreen(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('ko', 'KR'),
Locale('en', 'US'),
],
theme: ThemeData(
useMaterial3: true,
fontFamily: 'SystemDefault',
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF007AFF),
background: const Color(0xFFF2F2F7),
),
scaffoldBackgroundColor: const Color(0xFFF2F2F7),
),
debugShowCheckedModeBanner: false,
);
}
}
================================================================
AndroidManifest.xml
================================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.my_health_coach_app">
<queries>
<package android:name="com.kakao.talk" />
</queries>
<application
android:label="my_health_coach_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!--
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
-->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- 기존 카카오톡 딥링크 -->
<!--
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kakao2c0d9b4500ff55cb940d693d7edb800c" />
</intent-filter>
-->
<!-- 카카오링크 전용 딥링크 (kakaolink 패스 포함) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="kakaolink" android:scheme="kakao2c0d9b4500ff55cb940d693d7edb800c" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kakao2c0d9b4500ff55cb940d693d7edb800c" android:host="kakaolink" android:pathPrefix="/"/>
</intent-filter>
</activity>
<activity
android:name="com.tekartik.sqflite.SqflitePluginActivity"
android:theme="@style/LaunchTheme" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
Logs
1. 메시지 전송 시점의 로그 (Sending Log)
shareDefault() 호출 시, template_args 안에 ANDROID_EXECUTION_URL 값으로 파라미터가 포함된 정상적인 딥링크가 생성된 것을 확인했습니다.
I/flutter (27783): Generated planId: 62
I/flutter (27783): Generated webUrl: https://38edc8163b62.ngrok-free.app/krcore-fhir-app/app/diet-details/62
I/flutter (27783): Direct deepLink: kakao2c0d9b4500ff55cb940d693d7edb800c://kakaolink?id=62
I/flutter (27783): FeedTemplate created with androidExecutionParams: id=62
I/flutter (27783): KakaoTalk share URI: kakaolink://send?linkver=4.0&appkey=2c0d9b4500ff55cb940d693d7edb800c&appver=1.0.0&template_id=3139&template_args=%7B%22%24%7BIMAGE_WIDTH%7D%22%3A%22400%22%2C%22%24%7BITEM3_OP%7D%22%3A%22%22%2C%22%24%7BPROFILE_TEXT2%7D%22%3A%22%EB%82%98%EC%9D%98%20%EA%B1%B4%EA%B0%95%20%EC%BD%94%EC%B9%98%22%2C%22%24%7BFIRST_BUTTON_TITLE%7D%22%3A%22%EC%9E%90%EC%84%B8%ED%9E%88%20%EB%B3%B4%EA%B8%B0%22%2C%22%24%7BDESCRIPTION%7D%22%3A%22%EB%82%98%EC%9D%98%20%EA%B1%B4%EA%B0%95%20%EC%BD%94%EC%B9%98%EA%B0%80%20%EC%A0%9C%EC%95%88%ED%95%98%EB%8A%94%20%EB%A7%9E%EC%B6%A4%20%EC%8B%9D%EB%8B%A8%EC%9D%B4%20%EB%8F%84%EC%B0%A9%ED%96%88%EC%96%B4%EC%9A%94.%22%2C%22%24%7BITEM5%7D%22%3A%22%22%2C%22%24%7BANDROID_EXECUTION_URL%7D%22%3A%22kakao2c0d9b4500ff55cb940d693d7edb800c%3A%2F%2Fkakaolink%3Fid%3D62%26planId%3D62%22%2C%22%24%7BITEM1%7D%22%3A%22%EC%98%A4%EB%8A%98%EC%9D%98%20%EC%8B%9D%EB%8B%A8%22%2C%22%24%7BITEM3%7D%22%3A%22%22%2C%22%24%7BFIRST_BUTTON_IOS_EXECUTION_URL%7D%22%3A%22%22%2C%22%24%7BITEM4_OP%7D%22%3A%22%22%2C%22
2. 메시지 클릭 후 앱에서 수신한 로그 (Receiving Log)
실제로 앱이 수신한 URI에는 Query와 Path가 모두 비어있습니다.
I/flutter (27783): Kakao scheme stream: kakao2c0d9b4500ff55cb940d693d7edb800c://kakaolink
I/flutter (27783): Handling kakao deep link: kakao2c0d9b4500ff55cb940d693d7edb800c://kakaolink
I/flutter (27783): Scheme: kakao2c0d9b4500ff55cb940d693d7edb800c, Host: kakaolink, Path:
I/flutter (27783): Query: {}
I/flutter (27783): Path Segments: []
I/flutter (27783): No planId found in query parameters
Flutter Doctor
PS C:\FHIR\ngrok-v3-stable-windows-amd64> flutter doctor -v
[√] Flutter (Channel stable, 3.32.6, on Microsoft Windows [Version 10.0.26100.4652], locale ko-KR) [654ms]
• Flutter version 3.32.6 on channel stable at C:\FHIR\flutter
• Upstream repository git@github.com:flutter/flutter.git
• Framework revision 077b4a4ce1 (9 days ago), 2025-07-08 13:31:08 -0700
• Engine revision 72f2b18bb0
• Dart version 3.8.1
• DevTools version 2.45.1
[√] Windows Version (11 Pro 64-bit, 24H2, 2009) [3.5s]
[√] Android toolchain - develop for Android devices (Android SDK version 35.0.1) [3.7s]
• Android SDK at C:\Users\lim_s\AppData\Local\Android\Sdk
• Platform android-35, build-tools 35.0.1
• Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)
• All Android licenses accepted.
[√] Chrome - develop for the web [325ms]
• Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.14.7) [323ms]
• Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
• Visual Studio Community 2022 version 17.14.36221.1
• Windows 10 SDK version 10.0.26100.0
[√] Android Studio (version 2024.3.2) [145ms]
• Android Studio at C:\Program Files\Android\Android Studio
• Flutter plugin can be installed from:
https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)
[√] IntelliJ IDEA Community Edition (version 2024.3) [143ms]
• IntelliJ at C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2024.3.4.1
• Flutter plugin can be installed from:
https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
https://plugins.jetbrains.com/plugin/6351-dart
[√] IntelliJ IDEA Ultimate Edition (version 2025.1) [142ms]
• IntelliJ at C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2024.3.4.1\IntelliJ IDEA 2024.3.4.1
• Flutter plugin version 86.0.2
• Dart plugin version 251.26927.39
[√] Connected device (3 available) [257ms]
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.26100.4652]
• Chrome (web) • chrome • web-javascript • Google Chrome 138.0.7204.101
• Edge (web) • edge • web-javascript • Microsoft Edge 138.0.3351.83
[√] Network resources [369ms]
• All expected network resources are available.
• No issues found!