diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 9678ef3..37f17f4 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -1,41 +1,46 @@ -name: web kakao api added - +name: Deploy Flutter Web to GitHub Pages on: push: - branches: [ "kpaasTest" ] # 1. 'develop'에서 'kpaasTest'로 변경 + branches: [ "kpaasTest" ] jobs: build_and_deploy: runs-on: ubuntu-latest - # gh-pages 브랜치에 쓰기 권한 부여 permissions: - contents: write + contents: write steps: - # 2. 레포지토리 코드 가져오기 - name: 📦 Checkout Repository uses: actions/checkout@v4 - # 3. Flutter 환경 설정 - name: ☕ Set up Flutter SDK uses: subosito/flutter-action@v2 with: channel: 'stable' - - # 4. Flutter 웹 설정 및 파일 생성 - - name: 🌐 Configure Flutter for Web - run: flutter create . --platforms web - - # 5. 프로젝트 의존성 설치 + + - name: 🌐 Create Web Platform + run: | + if [ ! -d "web" ]; then + flutter create . --platforms web + fi + + # ⭐ 빈 .env 파일 생성 (pubspec.yaml asset 오류 방지) + - name: 📄 Create Empty .env File + run: touch .env + - name: 🧩 Get Dependencies run: flutter pub get - - # 6. Build Web App (환경 변수 주입 및 HTML 렌더러 사용) + + - name: 📝 Add Base Href Placeholder + run: | + if ! grep -q 'FLUTTER_BASE_HREF' web/index.html; then + sed -i 's||\n |' web/index.html + fi + - name: 🏗️ Build Web App run: | flutter build web --release \ - --web-renderer=html \ --base-href "/FE/" \ --dart-define=API_BASE_URL="${{ secrets.API_BASE_URL }}" \ --dart-define=KAKAO_NATIVE_APP_KEY="${{ secrets.KAKAO_NATIVE_APP_KEY }}" \ @@ -43,8 +48,7 @@ jobs: --dart-define=NAVER_CLIENT_ID="${{ secrets.NAVER_CLIENT_ID }}" \ --dart-define=NAVER_CLIENT_SECRET="${{ secrets.NAVER_CLIENT_SECRET }}" \ --dart-define=OPENWEATHERMAP_API_KEY="${{ secrets.OPENWEATHERMAP_API_KEY }}" - - # 7. 빌드 결과물을 GitHub Pages에 배포 + - name: 🚀 Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: diff --git a/.github/workflows/mai123123n.yml b/.github/workflows/mai123123n.yml deleted file mode 100644 index e947245..0000000 --- a/.github/workflows/mai123123n.yml +++ /dev/null @@ -1,61 +0,0 @@ -# .github/workflows/deploy-flutter-web.yml - -name: Deploy Flutter Web to GitHub Pages - -on: - push: - branches: [ "feature/test-1" ] # develop 브랜치에 푸시될 때 실행 - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - - # gh-pages 브랜치에 쓰기 권한 부여 - permissions: - contents: write - - steps: - # 1. 레포지토리 코드 가져오기 - - name: 📦 Checkout Repository - uses: actions/checkout@v4 - - # 2. Flutter 환경 설정 (⭐ 수정됨) - # Flutter 버전을 고정하여 '--web-renderer' 옵션을 인식하도록 합니다. - - name: ☕ Set up Flutter SDK - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - flutter-version: '3.22.3' # 3.22.3 또는 3.24.x 등 최신 안정 버전 명시 - - # 3. .env 파일 생성 (원본 유지) - # 프로젝트에 필요한 Secret들을 .env 파일로 생성합니다. - - name: ✍️ Create .env file - run: | - echo "API_BASE_URL=${{ secrets.API_BASE_URL }}" >> .env - echo "KAKAO_NATIVE_APP_KEY=${{ secrets.KAKAO_NATIVE_APP_KEY }}" >> .env - echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env - echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env - echo "OPENWEATHERMAP_API_KEY=${{ secrets.OPENWEATHERMAP_API_KEY }}" >> .env - echo ".env 파일 생성 완료:" - cat .env # 생성된 파일 내용 확인 (키 값 자체는 마스킹 처리됨) - - # 4. Flutter 웹 활성화 (⭐ 수정됨) - # 'flutter create' 대신 'flutter config'를 사용하여 웹 지원을 활성화합니다. - - name: 🌐 Enable Web - run: flutter config --enable-web - - # 5. 프로젝트 의존성 설치 - - name: 🧩 Get Dependencies - run: flutter pub get - - # 6. Flutter 웹 앱 빌드 (⭐ 수정됨) - # '--web-renderer=html' 대신 '--web-renderer html' (공백)을 사용합니다. - - name: 🏗️ Build Web App - run: flutter build web --release --web-renderer html --base-href "/FE/" - - # 7. 빌드 결과물을 GitHub Pages에 배포 (원본 유지) - - name: 🚀 Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build/web diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index f5fed91..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Deploy Flutter Web to GitHub Pages -on: - push: - branches: [ "develop" ] - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - name: 📦 Checkout Repository - uses: actions/checkout@v4 - - - name: ☕ Set up Flutter SDK - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - - - name: ✍️ Create .env file - run: | - echo "API_BASE_URL=${{ secrets.API_BASE_URL }}" >> .env - echo "KAKAO_NATIVE_APP_KEY=${{ secrets.KAKAO_NATIVE_APP_KEY }}" >> .env - echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env - echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env - echo "OPENWEATHERMAP_API_KEY=${{ secrets.OPENWEATHERMAP_API_KEY }}" >> .env - echo ".env 파일 생성 완료:" - cat .env - - - name: 🌐 Configure Flutter for Web - run: flutter create . --platforms web - - - name: 🧩 Get Dependencies - run: flutter pub get - - # ⭐ --web-renderer 옵션 제거 - - name: 🏗️ Build Web App - run: flutter build web --release --base-href "/FE/" - - - name: 🚀 Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build/web diff --git a/.github/workflows/main123.yml b/.github/workflows/main123.yml deleted file mode 100644 index e947245..0000000 --- a/.github/workflows/main123.yml +++ /dev/null @@ -1,61 +0,0 @@ -# .github/workflows/deploy-flutter-web.yml - -name: Deploy Flutter Web to GitHub Pages - -on: - push: - branches: [ "feature/test-1" ] # develop 브랜치에 푸시될 때 실행 - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - - # gh-pages 브랜치에 쓰기 권한 부여 - permissions: - contents: write - - steps: - # 1. 레포지토리 코드 가져오기 - - name: 📦 Checkout Repository - uses: actions/checkout@v4 - - # 2. Flutter 환경 설정 (⭐ 수정됨) - # Flutter 버전을 고정하여 '--web-renderer' 옵션을 인식하도록 합니다. - - name: ☕ Set up Flutter SDK - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - flutter-version: '3.22.3' # 3.22.3 또는 3.24.x 등 최신 안정 버전 명시 - - # 3. .env 파일 생성 (원본 유지) - # 프로젝트에 필요한 Secret들을 .env 파일로 생성합니다. - - name: ✍️ Create .env file - run: | - echo "API_BASE_URL=${{ secrets.API_BASE_URL }}" >> .env - echo "KAKAO_NATIVE_APP_KEY=${{ secrets.KAKAO_NATIVE_APP_KEY }}" >> .env - echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env - echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env - echo "OPENWEATHERMAP_API_KEY=${{ secrets.OPENWEATHERMAP_API_KEY }}" >> .env - echo ".env 파일 생성 완료:" - cat .env # 생성된 파일 내용 확인 (키 값 자체는 마스킹 처리됨) - - # 4. Flutter 웹 활성화 (⭐ 수정됨) - # 'flutter create' 대신 'flutter config'를 사용하여 웹 지원을 활성화합니다. - - name: 🌐 Enable Web - run: flutter config --enable-web - - # 5. 프로젝트 의존성 설치 - - name: 🧩 Get Dependencies - run: flutter pub get - - # 6. Flutter 웹 앱 빌드 (⭐ 수정됨) - # '--web-renderer=html' 대신 '--web-renderer html' (공백)을 사용합니다. - - name: 🏗️ Build Web App - run: flutter build web --release --web-renderer html --base-href "/FE/" - - # 7. 빌드 결과물을 GitHub Pages에 배포 (원본 유지) - - name: 🚀 Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build/web diff --git a/.github/workflows/main123123.yml b/.github/workflows/main123123.yml deleted file mode 100644 index 012cb8f..0000000 --- a/.github/workflows/main123123.yml +++ /dev/null @@ -1,85 +0,0 @@ -네, 클로드(Claude)가 제안한 내용이 **아주 정확하고 좋은 해결책**입니다\! 👏 - -하얀 화면이 뜨는 문제는 클로드의 분석대로, GitHub Actions가 `.env` 파일을 웹 빌드에 포함시키지 못해서 발생하는 **환경 변수 문제**가 맞습니다. - -클로드가 제안한 `--dart-define` 방식은 CI/CD에서 환경 변수를 주입하는 **가장 표준적이고 안정적인 방법**입니다. - -### **그런데... (중요\! ⚠️)** - -클로드가 제안한 `.yaml` 파일은 이전에 발생했던 **Wasm 호환성 오류**(`flutter_secure_storage_web` 관련)를 해결하기 위한 `--web-renderer=html` 옵션을 빠뜨렸습니다. - -가장 완벽한 방법은 **두 가지 해결책을 모두 합치는 것**입니다. - -1. **환경 변수 문제 해결**: `echo` 대신 `--dart-define` 사용 (클로드 제안) -2. **Wasm 호환성 문제 해결**: `--web-renderer=html` 사용 (우리 해결책) - -제가 두 가지를 모두 반영하여 Canvas의 `deploy-web.yml` 파일을 수정해 드릴게요. - -### 1단계: `.yaml` 파일 수정 - -http://googleusercontent.com/immersive_entry_chip/0 - -### 2단계: Dart 코드 수정 (필수\!) - -`--dart-define`을 사용하려면, 클로드의 설명대로 **Dart 코드에서 환경 변수를 불러오는 방식**을 `dotenv.env`에서 `String.fromEnvironment`로 **반드시** 변경해야 합니다. - -`ApiConfig.dart`, `main.dart`, `NaverMapApiService.dart`, `WeatherApiService.dart` 등 `.env`를 사용하는 모든 파일을 수정해야 합니다. - -**예시 1: `ApiConfig.dart`** - -````dart -// Before: -// import 'package:flutter_dotenv/flutter_dotenv.dart'; -// static String get baseUrl { -// final value = dotenv.env['API_BASE_URL']; -// ... -// } - -// After: -class ApiConfig { - static String get baseUrl { - // --dart-define으로 주입된 값을 컴파일 타임에 읽어옵니다. - const value = String.fromEnvironment('API_BASE_URL'); - if (value.isEmpty) { - throw StateError('Missing API_BASE_URL (was not passed with --dart-define)'); - } - return value; - } - // ... -} - -**예시 2: `main.dart` (카카오 키)** -```dart -// Before: -// await dotenv.load(fileName: '.env'); -// final kakaoNativeAppKey = dotenv.env['KAKAO_NATIVE_APP_KEY']; - -// After: -Future main() async { - // .env 파일 로드 관련 코드가 더 이상 필요 없습니다. - - // --dart-define으로 주입된 값을 읽어옵니다. - const kakaoNativeAppKey = String.fromEnvironment('KAKAO_NATIVE_APP_KEY'); - if (kakaoNativeAppKey.isEmpty) { - throw Exception('KAKAO_NATIVE_APP_KEY is not set (was not passed with --dart-define)'); - } - - KakaoSdk.init(nativeAppKey: kakaoNativeAppKey); - runApp(const MyApp()); -} - -**예시 3: `NaverMapApiService.dart` (네이버 키)** -```dart -// Before: -// final String? _clientId = dotenv.env['NAVER_CLIENT_ID']; - -// After: -class NaverMapApiService { - // const String.fromEnvironment로 읽어옵니다. - final String? _clientId = const String.fromEnvironment('NAVER_CLIENT_ID'); - final String? _clientSecret = const String.fromEnvironment('NAVER_CLIENT_SECRET'); - // ... -} - -이렇게 **1) `.yaml` 파일 수정**과 **2) Dart 코드 수정**을 모두 완료하고 푸시하면, GitHub Secrets가 올바르게 주입되어 하얀 화면 문제가 해결될 것입니다! -```` diff --git a/.github/workflows/main1313131313.yml b/.github/workflows/main1313131313.yml deleted file mode 100644 index d5200a8..0000000 --- a/.github/workflows/main1313131313.yml +++ /dev/null @@ -1,54 +0,0 @@ -# .github/workflows/deploy-flutter-web.yml -name: Deploy Flutter Web to GitHub Pages - -on: - push: - branches: [ "develop" ] - workflow_dispatch: - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: 📦 Checkout Repository - uses: actions/checkout@v4 - - - name: ☕ Set up Flutter SDK - uses: subosito/flutter-action@v2 - with: - channel: stable - flutter-version: '3.22.3' - - - name: 🌐 Enable Web - run: flutter config --enable-web - - - name: 🧩 Get Dependencies - run: flutter pub get - - - name: 🏗️ Build Web App - run: | - flutter build web \ - --release \ - --web-renderer html \ - --base-href "/FE/" \ - --dart-define API_BASE_URL=${{ secrets.API_BASE_URL }} \ - --dart-define KAKAO_NATIVE_APP_KEY=${{ secrets.KAKAO_NATIVE_APP_KEY }} \ - --dart-define NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }} \ - --dart-define NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }} \ - --dart-define OPENWEATHERMAP_API_KEY=${{ secrets.OPENWEATHERMAP_API_KEY }} - - - name: 🔐 Inject Kakao JS Key into index.html - run: | - if [ -z "${{ secrets.KAKAO_JAVASCRIPT_KEY }}" ]; then - echo "KAKAO_JAVASCRIPT_KEY is not set"; exit 1; - fi - sed -i "s/__KAKAO_JS_KEY__/${{ secrets.KAKAO_JAVASCRIPT_KEY }}/g" build/web/index.html - - - name: 🚀 Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build/web diff --git a/.github/workflows/main133133.yml b/.github/workflows/main133133.yml deleted file mode 100644 index f34df76..0000000 --- a/.github/workflows/main133133.yml +++ /dev/null @@ -1,61 +0,0 @@ -# .github/workflows/deploy-flutter-web.yml - -name: Deploy Flutter Web to GitHub Pages - -on: - push: - branches: [ "feature/test-1" ] # feature/test-1 브랜치에 푸시될 때만 실행 - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - # 1. 레포지토리 코드 가져오기 - - name: 📦 Checkout Repository - uses: actions/checkout@v4 - - # 2. Flutter 환경 설정 (버전 고정) - - name: ☕ Set up Flutter SDK - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - flutter-version: '3.22.3' # 또는 최신 안정 버전 - - # 3. .env 파일 생성 (로컬 개발 대응, 실제 빌드엔 중요도 낮음) - - name: ✍️ Create .env file (Optional) - run: | - echo "API_BASE_URL=${{ secrets.API_BASE_URL }}" >> .env - echo "KAKAO_NATIVE_APP_KEY=${{ secrets.KAKAO_NATIVE_APP_KEY }}" >> .env - echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env - echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env - echo "OPENWEATHERMAP_API_KEY=${{ secrets.OPENWEATHERMAP_API_KEY }}" >> .env - cat .env - - # 4. Flutter 웹 활성화 - - name: 🌐 Enable Web - run: flutter config --enable-web - - # 5. 프로젝트 의존성 설치 - - name: 🧩 Get Dependencies - run: flutter pub get - - # 6. Flutter 웹 앱 빌드 (⭐ dart-define 옵션 필수!) - - name: 🏗️ Build Web App - run: | - flutter build web --release --web-renderer html --base-href "/FE/" \ - --dart-define API_BASE_URL=${{ secrets.API_BASE_URL }} \ - --dart-define KAKAO_NATIVE_APP_KEY=${{ secrets.KAKAO_NATIVE_APP_KEY }} \ - --dart-define NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }} \ - --dart-define NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }} \ - --dart-define OPENWEATHERMAP_API_KEY=${{ secrets.OPENWEATHERMAP_API_KEY }} - - # 7. 빌드 결과물을 GitHub Pages에 배포 - - name: 🚀 Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build/web diff --git a/.metadata b/.metadata index 3cf9e8c..5722f85 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8" + revision: "9f455d2486bcb28cad87b062475f42edc959f636" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 - base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 - - platform: windows - create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 - base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: web + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 # User provided section diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 90cdb35..8e4abd5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ defaultHeaders = { - 'Content-Type': 'application/json', - }; - - // -------- Auth/User -------- + // Auth static const String kakaoLogin = '/auth/kakao'; + static const String userInfo = '/auth/user-info'; + + // Tips + static const String tips = '/tip'; + static String tipDetail(int id) => '/tip/$id'; + + // User static const String user = '/user'; - static const String userInfo = '/user'; // alias (기존 사용처 호환) static const String userProfile = '/user/profile'; static const String userBookmarks = '/user/bookmarks'; static const String userChallenges = '/user/challenges'; - // -------- Tips -------- - static const String tips = '/tip'; - static String tipDetail(int tipId) => '/tip/$tipId'; - - // -------- Terms -------- + // Terms static const String terms = '/term'; - // -------- Bookmark -------- + // Bookmark static const String bookmark = '/bookmark'; - // -------- Bills -------- + // Bills + static const String billsOcr = '/api/bills/ocr'; static const String billsSummary = '/api/bills/summary'; static const String billsReportDetail = '/api/bills/report/detail'; -} + // Headers + static const Map defaultHeaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; +} diff --git a/lib/features/home/data/naver_map_api_service.dart b/lib/features/home/data/naver_map_api_service.dart index 1ad579f..f1c43ae 100644 --- a/lib/features/home/data/naver_map_api_service.dart +++ b/lib/features/home/data/naver_map_api_service.dart @@ -3,20 +3,23 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; -// 주소 검색 결과를 담을 간단한 모델 +// ... (Address 모델은 동일) class Address { final String roadAddress; final String jibunAddress; final double lat; final double lon; - Address({required this.roadAddress, required this.jibunAddress, required this.lat, required this.lon}); + Address( + {required this.roadAddress, + required this.jibunAddress, + required this.lat, + required this.lon}); factory Address.fromJson(Map json) { return Address( roadAddress: json['roadAddress'] ?? '', jibunAddress: json['jibunAddress'] ?? '', - // 네이버 API는 x가 경도(lon), y가 위도(lat)입니다. lat: double.tryParse(json['y'] ?? '0.0') ?? 0.0, lon: double.tryParse(json['x'] ?? '0.0') ?? 0.0, ); @@ -24,37 +27,44 @@ class Address { } class NaverMapApiService { - String? _id() { - String v = const String.fromEnvironment('NAVER_CLIENT_ID'); - if (v.isEmpty) { try { v = dotenv.env['NAVER_CLIENT_ID'] ?? ''; } catch (_) {} } - return v.isEmpty ? null : v; - } - String? _secret() { - String v = const String.fromEnvironment('NAVER_CLIENT_SECRET'); - if (v.isEmpty) { try { v = dotenv.env['NAVER_CLIENT_SECRET'] ?? ''; } catch (_) {} } - return v.isEmpty ? null : v; + final String _clientId; + final String _clientSecret; + + NaverMapApiService() + : _clientId = _getEnv('NAVER_CLIENT_ID'), + _clientSecret = _getEnv('NAVER_CLIENT_SECRET'); + + static String _getEnv(String key) { + String value = String.fromEnvironment(key); + if (value.isEmpty) { + value = dotenv.env[key] ?? ''; + } + return value; } // Geocoding: 주소 검색 Future> searchAddress(String query) async { - final id = _id(); - final secret = _secret(); - if (id == null || secret == null) { - throw Exception("API keys are not configured in .env file"); + if (_clientId.isEmpty || _clientSecret.isEmpty) { + throw Exception( + "API keys are not configured in .env or via --dart-define"); } final response = await http.get( - Uri.parse('https://maps.apigw.ntruss.com/map-geocode/v2/geocode?query=$query'), + Uri.parse( + 'https://maps.apigw.ntruss.com/map-geocode/v2/geocode?query=$query'), headers: { - 'X-NCP-APIGW-API-KEY-ID': id, - 'X-NCP-APIGW-API-KEY': secret, + // ⭐ 오류 수정: id, secret -> _clientId, _clientSecret + 'X-NCP-APIGW-API-KEY-ID': _clientId, + 'X-NCP-APIGW-API-KEY': _clientSecret, }, ); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK' && data['addresses'] != null) { - return (data['addresses'] as List).map((addr) => Address.fromJson(addr)).toList(); + return (data['addresses'] as List) + .map((addr) => Address.fromJson(addr)) + .toList(); } return []; } else { @@ -64,43 +74,55 @@ class NaverMapApiService { // Reverse Geocoding: 좌표 -> 주소 변환 Future coordToAddress(double lat, double lon) async { - final id = _id(); - final secret = _secret(); - if (id == null || secret == null) { - throw Exception("API keys are not configured in .env file"); + if (_clientId.isEmpty || _clientSecret.isEmpty) { + throw Exception( + "API keys are not configured in .env or via --dart-define"); } final response = await http.get( - Uri.parse('https://maps.apigw.ntruss.com/map-reversegeocode/v2/gc?coords=$lon,$lat&output=json'), + Uri.parse( + 'https://maps.apigw.ntruss.com/map-reversegeocode/v2/gc?coords=$lon,$lat&output=json'), headers: { - 'X-NCP-APIGW-API-KEY-ID': id, - 'X-NCP-APIGW-API-KEY': secret, + // ⭐ 오류 수정: id, secret -> _clientId, _clientSecret + 'X-NCP-APIGW-API-KEY-ID': _clientId, + 'X-NCP-APIGW-API-KEY': _clientSecret, }, ); + // 디버깅 로그 (기존과 동일) + if (kDebugMode) { + print('--- 네이버 Reverse Geocoding 응답 ---'); + print('Status Code: ${response.statusCode}'); + print('Response Body: ${utf8.decode(response.bodyBytes)}'); + print('----------------------------------'); + } + if (response.statusCode == 200) { - final data = json.decode(response.body); + final data = json.decode(utf8.decode(response.bodyBytes)); if (data['status']['code'] == 0 && data['results'].isNotEmpty) { final region = data['results'][0]['region']; - return '${region['area1']['name']} ${region['area2']['name']} ${region['area3']['name']}'; + final area1 = region['area1']?['name'] ?? ''; + final area2 = region['area2']?['name'] ?? ''; + final area3 = region['area3']?['name'] ?? ''; + + if (area1.isNotEmpty || area2.isNotEmpty || area3.isNotEmpty) { + return '$area1 $area2 $area3'.trim(); + } else { + return '상세 주소 정보를 찾을 수 없습니다.'; + } } - return '주소를 찾을 수 없습니다.'; + return '주소를 찾을 수 없습니다. (응답 코드: ${data['status']?['code']})'; } else { - // API 호출 실패 시, 서버로부터 받은 실제 응답 내용을 출력합니다. - // 이렇게 하면 인증 실패인지, 쿼리 문제인지 정확히 알 수 있습니다. final errorBody = json.decode(response.body); final errorMessage = errorBody['error']?['message'] ?? response.body; - - // 디버그 콘솔에 상세 오류 출력 if (kDebugMode) { print('--- 네이버 API 오류 ---'); print('Status Code: ${response.statusCode}'); print('Error Body: ${response.body}'); print('---------------------'); } - - // 사용자에게 보여줄 예외 메시지 throw Exception('주소 변환 실패: $errorMessage'); } } -} \ No newline at end of file +} + diff --git a/lib/features/home/data/weather_api_service.dart b/lib/features/home/data/weather_api_service.dart index 6a04813..1f5b0af 100644 --- a/lib/features/home/data/weather_api_service.dart +++ b/lib/features/home/data/weather_api_service.dart @@ -1,15 +1,16 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; // kDebugMode를 위해 import +import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; -// API 응답 데이터를 담을 모델 클래스 +// ... (WeatherData 모델은 동일) class WeatherData { final double temp; final String description; final String icon; - WeatherData({required this.temp, required this.description, required this.icon}); + WeatherData( + {required this.temp, required this.description, required this.icon}); factory WeatherData.fromJson(Map json) { return WeatherData( @@ -21,24 +22,45 @@ class WeatherData { } class WeatherApiService { - String? _resolveKey() { - String v = const String.fromEnvironment('OPENWEATHERMAP_API_KEY'); - if (v.isEmpty) { - try { v = dotenv.env['OPENWEATHERMAP_API_KEY'] ?? ''; } catch (_) {} + // ⭐ 변경점: final 변수로 선언 + final String _apiKey; + static const String _baseUrl = + 'https://api.openweathermap.org/data/2.5/weather'; + + // ⭐ 변경점: 생성자에서 하이브리드 로직 구현 + WeatherApiService() + : _apiKey = + const String.fromEnvironment('OPENWEATHERMAP_API_KEY').isEmpty + ? dotenv.env['OPENWEATHERMAP_API_KEY'] ?? '' + : const String.fromEnvironment('OPENWEATHERMAP_API_KEY'); + + Future getWeather(double lat, double lon) async { + // ⭐ 변경점: 키 유효성 검사 + if (_apiKey.isEmpty) { + throw Exception( + "OpenWeatherMap API key is not configured in .env or via --dart-define"); + } + + final url = + '$_baseUrl?lat=$lat&lon=$lon&appid=$_apiKey&units=metric&lang=kr'; + + final response = await http.get(Uri.parse(url)); + + if (kDebugMode) { + print('--- OpenWeatherMap API 응답 ---'); + print('Request URL: $url'); + print('Status Code: ${response.statusCode}'); + print('Response Body: ${utf8.decode(response.bodyBytes)}'); + print('-----------------------------'); } - return v.isEmpty ? null : v; - } - Future getWeather(double lat, double lon) async { - final apiKey = _resolveKey(); - if (apiKey == null) return null; - final url = Uri.parse('https://api.openweathermap.org/data/2.5/weather?lat=$lat&lon=$lon&appid=$apiKey&units=metric&lang=kr'); - final res = await http.get(url); - if (res.statusCode == 200) { - final map = jsonDecode(res.body) as Map; - return WeatherData.fromJson(map); + if (response.statusCode == 200) { + return WeatherData.fromJson( + json.decode(utf8.decode(response.bodyBytes))); + } else { + throw Exception( + 'Failed to load weather data. Status: ${response.statusCode}, Body: ${utf8.decode(response.bodyBytes)}'); } - return null; } } diff --git a/lib/features/landing/presentation/login_screen.dart b/lib/features/landing/presentation/login_screen.dart index 14cffff..5cf1fc5 100644 --- a/lib/features/landing/presentation/login_screen.dart +++ b/lib/features/landing/presentation/login_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:billow/main.dart'; +import 'package:billow/main.dart' hide MainNavigationPage; import '../../../services/auth_service.dart'; import 'package:kakao_flutter_sdk_user/kakao_flutter_sdk_user.dart'; diff --git a/lib/main.dart b/lib/main.dart index 01e2e1c..f4b199d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,65 +1,157 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:kakao_flutter_sdk_common/kakao_flutter_sdk_common.dart'; import 'package:billow/features/landing/presentation/splash_screen.dart'; -import 'package:billow/features/landing/presentation/kakao_web_callback_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; // 1. dotenv import +import 'package:flutter/foundation.dart'; // 2. kIsWeb import +import 'features/home/presentation/home_screen.dart'; +import 'features/challenge/presentation/challenge_screen.dart'; +import 'features/tips/presentation/tips_screen.dart'; +import 'features/profile/presentation/profile_screen.dart'; +import 'features/auth/presentation/auth_test_screen.dart'; +import 'package:kakao_flutter_sdk_user/kakao_flutter_sdk_user.dart'; +import 'services/token_storage.dart'; Future main() async { + // 3. runApp 전에 Flutter 엔진 초기화 보장 WidgetsFlutterBinding.ensureInitialized(); - String kakaoKey = const String.fromEnvironment('KAKAO_NATIVE_APP_KEY'); - if (kakaoKey.isEmpty) { + // 4. 로컬 디버깅을 위해 .env 파일 로드 + if (!kIsWeb) { try { - await dotenv.load(fileName: '.env'); - kakaoKey = dotenv.env['KAKAO_NATIVE_APP_KEY'] ?? ''; - } catch (_) {} + await dotenv.load(fileName: ".env"); + } catch (e) { + print('⚠️ .env 파일을 로드할 수 없습니다: $e'); + } } - if (kakaoKey.isEmpty) { - throw Exception('KAKAO_NATIVE_APP_KEY is not set (use --dart-define or .env)'); + + // 5. 플랫폼(웹/모바일)에 따라 다른 카카오 키로 SDK 초기화 + if (kIsWeb) { + // --- 웹 환경일 경우 --- + const jsAppKey = String.fromEnvironment('KAKAO_JAVASCRIPT_APP_KEY'); + if (jsAppKey.isEmpty) { + throw Exception('KAKAO_JAVASCRIPT_APP_KEY is not set via --dart-define'); + } + KakaoSdk.init(javaScriptAppKey: jsAppKey); + } else { + // --- 모바일 (Android/iOS) 환경일 경우 --- + String nativeAppKey = + const String.fromEnvironment('KAKAO_NATIVE_APP_KEY'); + if (nativeAppKey.isEmpty) { + nativeAppKey = dotenv.env['KAKAO_NATIVE_APP_KEY'] ?? ''; + } + + if (nativeAppKey.isEmpty) { + throw Exception( + 'KAKAO_NATIVE_APP_KEY is not set in .env or via --dart-define'); + } + KakaoSdk.init(nativeAppKey: nativeAppKey); } - KakaoSdk.init(nativeAppKey: kakaoKey); - runApp(const BillowApp()); + // 6. MyApp 실행 + runApp(const MyApp()); } -class BillowApp extends StatelessWidget { - const BillowApp({super.key}); +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override Widget build(BuildContext context) { - final theme = ThemeData( - useMaterial3: true, - colorScheme: const ColorScheme.light( - primary: Colors.teal, - secondary: Colors.teal, - surface: Colors.white, - background: Colors.white, - ), - scaffoldBackgroundColor: Colors.white, - appBarTheme: const AppBarTheme( - backgroundColor: Colors.white, - surfaceTintColor: Colors.transparent, - elevation: 0.5, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - backgroundColor: Colors.white, - selectedItemColor: Colors.teal, - unselectedItemColor: Colors.grey, - type: BottomNavigationBarType.fixed, - ), - ); - return MaterialApp( + title: 'Billow', debugShowCheckedModeBanner: false, - theme: theme, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), + scaffoldBackgroundColor: Colors.grey[50], + appBarTheme: const AppBarTheme( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0.5, + ), + useMaterial3: true, + ), home: const SplashScreen(), - onGenerateRoute: (settings) { - if (settings.name == '/auth/callback') { - return MaterialPageRoute(builder: (_) => const KakaoWebCallbackScreen()); - } - return null; - }, ); } } + +// MainNavigationPage는 SplashScreen이나 로그인 로직에서 호출되므로 +// main.dart에 같이 두어도 괜찮습니다. +class MainNavigationPage extends StatefulWidget { + const MainNavigationPage({super.key}); + + @override + State createState() => _MainNavigationPageState(); +} + +class _MainNavigationPageState extends State { + int _selectedIndex = 0; + + final List _pages = [ + const HomeScreen(), + const ChallengeScreen(), + const TipsScreen(), + const ProfileScreen(), + ]; + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 64, + titleSpacing: 40, + title: Text( + 'Billow', + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + fontSize: 22, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.vpn_key, color: Colors.black54), + tooltip: 'Auth Test', + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const AuthTestScreen()), + ); + }, + ), + // Swagger 토큰 출력용 버튼 (이전 코드에서 복원) + IconButton( + icon: const Icon(Icons.key, size: 24, color: Colors.orange), + tooltip: 'Show Bearer Token (Swagger용)', + onPressed: () async { + await TokenStorage.printAccessTokenForSwagger(); + }, + ), + IconButton( + icon: const Icon(Icons.notifications_none, color: Colors.black54), + tooltip: 'Notifications', + onPressed: () {}, + ), + const SizedBox(width: 24), + ], + ), + body: _pages[_selectedIndex], + bottomNavigationBar: NavigationBar( + selectedIndex: _selectedIndex, + onDestinationSelected: _onItemTapped, + destinations: const [ + NavigationDestination(icon: Icon(Icons.home_outlined), label: '홈'), + NavigationDestination(icon: Icon(Icons.emoji_events_outlined), label: '챌린지'), + NavigationDestination(icon: Icon(Icons.lightbulb_outline), label: '꿀팁'), + NavigationDestination(icon: Icon(Icons.person_outline), label: '마이'), + ], + ), + ); + } +} + +// 7. 머지 충돌로 잘못 들어온 하단 코드는 모두 삭제합니다. + diff --git a/pubspec.lock b/pubspec.lock index f564eea..1164ff0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.4.0" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -350,6 +366,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.5" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -536,6 +560,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" image_picker: dependency: "direct main" description: @@ -864,6 +896,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d9e6e4a..182f6cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,13 @@ dev_dependencies: flutter_lints: ^3.0.0 build_runner: ^2.4.6 json_serializable: ^6.7.1 + flutter_launcher_icons: "^0.14.4" + +flutter_launcher_icons: + android: "launcher_icon" # Android 아이콘 활성화 및 이름 설정 (res/mipmap-xxxhdi/launcher_icon.png) + ios: true # iOS 아이콘 활성화 + image_path: "assets/images/billow_logo.png" # 준비한 아이콘 이미지 경로 (예시) + min_sdk_android: 21 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..0c36f6e --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "billow", + "short_name": "billow", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}