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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
BASE_URL=
KAKAO_API_KEY=
2 changes: 2 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
# 팀 공용 디버그 keystore는 공유해야 하므로 예외 (비밀번호가 공개된 표준값이라 안전)
!app/debug.keystore
Comment thread
leejia324 marked this conversation as resolved.
11 changes: 10 additions & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,21 @@ android {
applicationId = "com.fablo.moly_mobile"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}

signingConfigs {
getByName("debug") {
storeFile = file("debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
Expand Down
Binary file added android/app/debug.keystore
Binary file not shown.
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="moly_mobile"
android:name="${applicationName}"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string>6.0</string>
<key>CFBundleName</key>
<string>moly_mobile</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>현재 위치를 지도에 표시하기 위해 위치 권한이 필요합니다.</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
Expand Down
1 change: 1 addition & 0 deletions lib/core/env/app_env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';

abstract final class AppEnv {
static String get baseUrl => dotenv.env['BASE_URL'] ?? '';
static String get kakaoApiKey => dotenv.env['KAKAO_API_KEY'] ?? '';
Comment thread
leejia324 marked this conversation as resolved.
}
75 changes: 70 additions & 5 deletions lib/features/map/presentation/pages/map_page.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,79 @@
import 'package:flutter/material.dart';
import 'package:moly_mobile/common/widgets/moly_scaffold.dart';
import 'package:moly_mobile/core/constants/text_style.dart';
import 'package:geolocator/geolocator.dart';
import 'package:kakao_map_sdk/kakao_map_sdk.dart';
import 'package:moly_mobile/features/map/presentation/widgets/location_dot.dart';
import 'package:moly_mobile/features/map/presentation/widgets/map_fab.dart';

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

@override
State<MapPage> createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
KakaoMapController? _controller;

Future<void> _moveToCurrentLocation() async {
try {
final position = await _currentPosition();
final controller = _controller;
if (position == null || controller == null || !mounted) return;

final latLng = LatLng(position.latitude, position.longitude);
await controller.moveCamera(
CameraUpdate.newCenterPosition(latLng, zoomLevel: 18),
);

if (!mounted) return;
final icon = await KImage.fromWidget(
const LocationDot(),
const Size(60, 60),
context: context,
);
await controller.labelLayer.addPoi(
latLng,
style: PoiStyle(icon: icon, anchor: const KPoint(0.5, 0.3)),
);
} catch (e) {
debugPrint('현재 위치 이동 실패: $e');
}
}

Future<Position?> _currentPosition() async {
if (!await Geolocator.isLocationServiceEnabled()) return null;

var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
return null;
}

return Geolocator.getCurrentPosition();
Comment thread
leejia324 marked this conversation as resolved.
}

@override
Widget build(BuildContext context) {
return const MolyScaffold(
body: Center(child: Text('지도', style: MolyTextStyle.headline2)),
return Scaffold(
body: Stack(
children: [
KakaoMap(
option: const KakaoMapOption(
position: LatLng(37.5665, 126.9780),
zoomLevel: 16,
mapType: MapType.normal,
),
onMapReady: (controller) {
_controller = controller;
_moveToCurrentLocation();
},
),
const MapFab(),
Comment thread
leejia324 marked this conversation as resolved.
],
),
);
}
}
28 changes: 28 additions & 0 deletions lib/features/map/presentation/widgets/location_dot.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:moly_mobile/core/constants/color.dart';

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

@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: MolyColor.brown100,
border: Border.all(color: MolyColor.white, width: 2.5),
boxShadow: [
BoxShadow(
color: MolyColor.brown100.withValues(alpha: 0.45),
blurRadius: 2,
spreadRadius: 3,
),
],
),
),
);
}
}
122 changes: 122 additions & 0 deletions lib/features/map/presentation/widgets/map_fab.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:moly_mobile/core/constants/color.dart';
import 'package:moly_mobile/core/constants/text_style.dart';

class MapFab extends StatefulWidget {
const MapFab({super.key, this.onTimeCapsule, this.onMemo});

final VoidCallback? onTimeCapsule;
final VoidCallback? onMemo;

@override
State<MapFab> createState() => _MapFabState();
}

class _MapFabState extends State<MapFab> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 130),
);
bool _open = false;

void _toggle() {
setState(() => _open = !_open);
_open ? _controller.forward() : _controller.reverse();
}

void _run(VoidCallback? action) {
_toggle();
action?.call();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
if (_open)
Positioned.fill(
child: GestureDetector(
onTap: _toggle,
child: ColoredBox(
color: MolyColor.black.withValues(alpha: 0.3),
),
),
),
Positioned(
right: 20,
bottom: 40,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [_menu(), const SizedBox(height: 16), _fab()],
),
),
],
);
}

Widget _menu() {
return SizeTransition(
sizeFactor: CurvedAnimation(parent: _controller, curve: Curves.easeOut),
alignment: Alignment.bottomCenter,
child: FadeTransition(
opacity: _controller,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_action('타임캡슐 만들기', () => _run(widget.onTimeCapsule)),
const SizedBox(height: 12),
_action('메모 만들기', () => _run(widget.onMemo)),
],
),
),
);
}

Widget _action(String label, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: MolyColor.white,
borderRadius: BorderRadius.circular(40),
boxShadow: [
BoxShadow(
color: MolyColor.black.withValues(alpha: 0.1),
blurRadius: 8,
),
],
),
child: Text(
label,
style: MolyTextStyle.captionMedium.copyWith(color: MolyColor.gray300),
),
),
);
}

Widget _fab() {
return GestureDetector(
onTap: _toggle,
child: Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
color: MolyColor.brown100,
shape: BoxShape.circle,
),
child: RotationTransition(
turns: Tween<double>(begin: 0, end: 0.125).animate(_controller),
child: const Icon(Icons.add, color: MolyColor.white, size: 32),
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ class OnboardingPage extends StatelessWidget {
const SizedBox(height: 12),
MolyButton(
label: '구글로 시작하기',
onPressed: () {},
// TODO: oauth 연동
onPressed: () {
// TODO: oauth 연동
context.go('/map');
},
backgroundColor: MolyColor.white,
hasBorder: true,
textStyle: MolyTextStyle.buttonBold.copyWith(
Expand Down
11 changes: 10 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:kakao_map_sdk/kakao_map_sdk.dart';
import 'package:moly_mobile/core/constants/color.dart';
import 'package:moly_mobile/core/env/app_env.dart';
import 'package:moly_mobile/core/router/app_router.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');
await dotenv.load(fileName: '.env', isOptional: true);

final kakaoApiKey = AppEnv.kakaoApiKey;
if (kakaoApiKey.isEmpty) {
throw StateError('KAKAO_API_KEY가 비어 있습니다. .env 파일을 확인하세요.');
}
await KakaoMapSdk.instance.initialize(kakaoApiKey);

runApp(const MyApp());
}

Expand Down
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation

import geolocator_apple

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
}
Loading
Loading