2021.09 - 2022.02

Flutter Dart

<aside> <img src="/icons/arrow-southeast_gray.svg" alt="/icons/arrow-southeast_gray.svg" width="40px" /> 목록으로 돌아가기

Hyeonsoo Kang

</aside>


<aside> <img src="/icons/phone_gray.svg" alt="/icons/phone_gray.svg" width="40px" /> 01. ShopMain

확실한 사업자만 검토하기 위해 전화번호로 로그인만 제공하였습니다.

APM HTTP 서버에서 배너사진, 특정 공지등을 get해서 화면에 출력했습니다. 또한 firebase messaging을 사용하여 운영팀에서 프로모션 알림을 보낼수있게 만들었습니다.

아래 사진에는 없는 내 가게 출장지역 설정을 통해 내 가게의 위치를 설정하고 출장갈수있는 반경 거리를 설정할수있었습니다. 이를 토대로 유저에게 가게가 노출되었습니다.

사용 Package

Firebase core

Firebase messaging

Flutter local notifications

Firebase Authentication

Google Maps Flutter

Geolocator

내 가게 출장지역 설정 부분 코드

Google Pixel 4 XL Screenshot 0.png

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:spoon_partner/utility.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_webservice/places.dart';
import 'package:google_api_headers/google_api_headers.dart';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_google_places_hoc081098/flutter_google_places_hoc081098.dart';

const kGoogleApiKey = 'api key';

class ShopWorkingSetting extends StatefulWidget {
  final String session;

  ShopWorkingSetting({required this.session});

  @override
  _ShopWorkingSettingState createState() => _ShopWorkingSettingState();
}

class _ShopWorkingSettingState extends State<ShopWorkingSetting> {
  double lat = 36.119485;
  double lon = 128.3445734;
  double rad = 0;
  Mode _mode = Mode.overlay;
  double _currentSliderValue = 0;
  late GoogleMapController mapController;
  final Completer<GoogleMapController> _controller = Completer();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        elevation: 0,
        title: Text(
          "내 가게 출장지역 설정",
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        backgroundColor: Colors.teal[300],
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          children: [
            Expanded(
              child: Stack(
                children: [
                  GoogleMap(
                    initialCameraPosition: CameraPosition(
                      target: LatLng(lat, lon),
                      zoom: 12,
                    ),
                    onCameraMove: _onCameraMove,
                    myLocationButtonEnabled: false,
                    onMapCreated: (GoogleMapController controller) {
                      _controller.complete(controller);
                    },
                    circles: Set.from([
                      Circle(
                        circleId: CircleId('currentCircle'),
                        center: LatLng(lat, lon),
                        radius: rad,
                        fillColor: Colors.blue.shade200.withOpacity(0.5),
                        strokeColor: Colors.blue.shade200.withOpacity(0.1),
                      ),
                    ]),
                  ),
                  Positioned(
                    top: (MediaQuery.of(context).size.height -
                            AppBar().preferredSize.height -
                            MediaQuery.of(context).padding.top -
                            80) /
                        2,
                    right: (MediaQuery.of(context).size.width - 40) / 2,
                    child: Icon(
                      Icons.location_on,
                      size: 40,
                      color: Colors.red,
                    ),
                  ),
                  Positioned(
                    bottom: 20,
                    right: 20,
                    child: Column(
                      children: [
                        FloatingActionButton(
                          child: Icon(
                            Icons.search,
                            size: 30,
                          ),
                          backgroundColor: Colors.teal[400],
                          onPressed: _handlePressButton,
                        ),
                        SizedBox(height: 10),
                        FloatingActionButton(
                          child: Icon(
                            Icons.my_location,
                            size: 30,
                          ),
                          backgroundColor: Colors.teal[400],
                          onPressed: citycheck,
                        ),
                      ],
                    ),
                  ),
                  Positioned(
                    bottom: 40,
                    right: (MediaQuery.of(context).size.width - 180) / 2,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        Container(
                          width: 200,
                          height: 40,
                          color: Colors.white,
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Text("반경"),
                              Container(
                                width: 160,
                                child: Slider(
                                  value: _currentSliderValue,
                                  min: 0,
                                  max: 30,
                                  divisions: 60,
                                  label: "반경 ${_currentSliderValue.toStringAsFixed(1)}km",
                                  onChanged: (double value) {
                                    setState(() {
                                      _currentSliderValue = value;
                                      rad = 1000 * _currentSliderValue;
                                    });
                                  },
                                ),
                              ),
                            ],
                          ),
                        ),
                        SizedBox(height: 10),
                        Container(
                          height: 60,
                          width: 200,
                          child: RaisedButton(
                            color: Colors.white,
                            child: Text(
                              "이 위치로 설정하기",
                              style: TextStyle(
                                color: Colors.grey[900],
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            onPressed: shopworkingpost,
                          ),
                        )
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> citycheck() async {
    try {
      Position position = await Geolocator.getCurrentPosition(
          desiredAccuracy: LocationAccuracy.high);
      lat = position.latitude;
      lon = position.longitude;

      final GoogleMapController controller = await _controller.future;
      controller.animateCamera(CameraUpdate.newLatLngZoom(LatLng(lat, lon), 14));
    } catch (e) {
      showSnackbar(context, "인터넷 연결을 확인해 주십시오.\\n위치 정보 권한을 허용해 주십시오.");
    }
  }

  Future<void> _handlePressButton() async {
    try {
      final p = await PlacesAutocomplete.show(
        context: context,
        apiKey: kGoogleApiKey,
        onError: (response) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(response.errorMessage ?? 'Unknown error'),
            ),
          );
        },
        mode: _mode,
        language: 'kr',
        components: [Component(Component.country, 'kr')],
        logo: SizedBox(height: 0.1),
      );

      await displayPrediction(p, ScaffoldMessenger.of(context));
    } catch (e) {
      showSnackbar(context, "오류가 발생했습니다. 인터넷 연결을 확인해주세요.");
    }
  }

  Future<void> displayPrediction(
      Prediction? p, ScaffoldMessengerState messengerState) async {
    if (p == null) return;

    try {
      final _places = GoogleMapsPlaces(
        apiKey: kGoogleApiKey,
        apiHeaders: await GoogleApiHeaders().getHeaders(),
      );

      final detail = await _places.getDetailsByPlaceId(p.placeId!);
      final geometry = detail.result.geometry!;
      final lat = geometry.location.lat;
      final lon = geometry.location.lng;

      setState(() {
        this.lat = lat;
        this.lon = lon;
      });

      final GoogleMapController controller = await _controller.future;
      controller.animateCamera(CameraUpdate.newLatLngZoom(LatLng(lat, lon), 14));
    } catch (e) {
      showSnackbar(context, "오류가 발생했습니다. 인터넷 연결을 확인해주세요.");
    }
  }

  void _onCameraMove(CameraPosition position) {
    setState(() {
      lat = position.target.latitude;
      lon = position.target.longitude;
    });
  }

  Future<void> shopworkingpost() async {
    if (_currentSliderValue < 0 || _currentSliderValue > 30) {
      showSnackbar(context, "오류가 발생했습니다. 다시 시도해주세요.");
      return;
    }

    try {
      final response = await http.post(
        Uri.parse("<https://geunsujeo.com/partner/shopworkingpost.php>"),
        body: {
          "session": widget.session,
          "lat": '$lat',
          "lon": '$lon',
          "distance": '$_currentSliderValue',
        },
      );

      final jsondata = jsonDecode(response.body);

      if (jsondata['status'] == "success") {
        showSnackbar(context, "완료 되었습니다.");
        Navigator.pop(context);
      } else {
        handleErrorResponse(jsondata['status']);
      }
    } catch (e) {
      showSnackbar(context, "오류가 발생했습니다. 인터넷 연결을 확인해주세요.");
    }
  }

  void handleErrorResponse(String status) {
    if (status == "text_error") {
      showSnackbar(context, "오류가 발생했습니다. 정확하게 입력했는지 확인해주세요.");
    } else if (status == "login_error") {
      showSnackbar(context, "로그인 에러가 발생하였습니다. 로그아웃 후 로그인하여 다시 시도해주세요.");
    } else if (status == "enter_error") {
      showSnackbar(context, "가게가 입점되어있지 않습니다. 입점 후 다시 시도해주세요.");
    } else {
      showSnackbar(context, "알 수 없는 오류가 발생했습니다. 관리자에게 문의해주세요.");
    }
  }
}

</aside>

<aside> <img src="/icons/phone_gray.svg" alt="/icons/phone_gray.svg" width="40px" /> 02. ShopSetting

내 가게 기본정보를 수정할수있습니다.

처음에 patner측에서 버튼을 누르면 http 서버에서 세션을 확인 후 가게정보를 반환하여 각 칸에 채워넣습니다. 이후 정보를 수정하면 세션과 데이터를 서버로 POST 하여 서버측에서 값을 확인 후 DB 정보가 수정됩니다.

사용 Package

image picker

Google Pixel 4 XL Screenshot 1.png

</aside>

<aside> <img src="/icons/phone_gray.svg" alt="/icons/phone_gray.svg" width="40px" /> 03. ShopItem view, insert, edit+remove

가게 상품에 대한 정보를 입력, 수정, 제거하는 기능이 있습니다. 새로운 상품을 가게에 추가하려면 오른쪽 하단 '+' 버튼을 사용하면 됩니다. 이미 등록된 상품의 정보를 변경하려면 상품을 눌러 '수정' 및 ‘제거 ’기능을 사용할수있습니다.

가게 상품을 입력,수정,제거할때는 token을 서버측에서 재확인하였습니다.

사용 Package

photo view

image picker

pull to refresh

Google Pixel 4 XL Screenshot 2.png

Google Pixel 4 XL Screenshot 3.png

</aside>