Skip to content

[Bug] [Android] FeedTemplate의 androidExecutionParams 파라미터가 전달되지 않는 문제 #209

@nannom02

Description

@nannom02

이슈 설명 (Issue description)

요약 (Summary)

kakao_flutter_sdk 최신 버전을 사용하여 FeedTemplate으로 메시지를 공유할 때, ContentLink 객체에 androidExecutionParams를 설정하여 딥링크 파라미터를 전달하고 있습니다.

Flutter 코드에서 생성된 KakaoTalk share URI 로그를 확인하면, template_args 안에 ANDROID_EXECUTION_URL 값으로 파라미터가 포함된 정상적인 딥링크가 생성된 것을 확인할 수 있습니다.

하지만, 실제로 앱에서 딥링크를 수신했을 때의 로그를 보면 PathQuery가 모두 비어있는 상태로, 파라미터가 완전히 유실되는 문제가 발생합니다.

개발 환경 (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)

  1. 아래와 같이 FeedTemplate을 구성하여 ShareClient.instance.shareDefault를 호출합니다.
    (자세한 코드는 아래 첨부파일 recommended_diet_screen.dart 참고)

  2. 이때, 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...

  3. 카카오톡에서 공유된 메시지를 클릭하여 앱을 실행합니다.

  4. 앱의 main.dart에 있는 딥링크 리스너에서 Query: {} 로그를 확인합니다.

지금까지 시도해 본 것 (Things I've Already Tried)

  • 카카오 개발자 콘솔의 키 해시가 실제 앱의 서명과 일치하는 것을 여러 번 확인했습니다.
  • 패키지 명 또한 일치합니다.
  • AndroidManifest.xmlschemehost="kakaolink"를 포함한 인텐트 필터를 설정했고, pathPrefix="/"를 추가한 필터도 설정했습니다.
  • MainActivityandroid:launchMode="singleTop" 속성을 제거하여 테스트했지만 동일한 문제가 발생했습니다.
  • FeedTemplateContent 링크에서 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!

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions