├── .gitignore ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mobitant │ │ └── bestfood │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mobitant │ │ │ └── bestfood │ │ │ ├── BestFoodInfoActivity.java │ │ │ ├── BestFoodKeepFragment.java │ │ │ ├── BestFoodListFragment.java │ │ │ ├── BestFoodMapFragment.java │ │ │ ├── BestFoodRegisterActivity.java │ │ │ ├── BestFoodRegisterImageFragment.java │ │ │ ├── BestFoodRegisterInputFragment.java │ │ │ ├── BestFoodRegisterLocationFragment.java │ │ │ ├── Constant.java │ │ │ ├── IndexActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── MyApp.java │ │ │ ├── PermissionActivity.java │ │ │ ├── ProfileActivity.java │ │ │ ├── ProfileIconActivity.java │ │ │ ├── adapter │ │ │ ├── InfoListAdapter.java │ │ │ ├── KeepListAdapter.java │ │ │ └── MapListAdapter.java │ │ │ ├── custom │ │ │ ├── EndlessRecyclerViewScrollListener.java │ │ │ └── WorkaroundMapFragment.java │ │ │ ├── item │ │ │ ├── FoodInfoItem.java │ │ │ ├── GeoItem.java │ │ │ ├── ImageItem.java │ │ │ ├── KeepItem.java │ │ │ └── MemberInfoItem.java │ │ │ ├── lib │ │ │ ├── BitmapLib.java │ │ │ ├── DialogLib.java │ │ │ ├── EtcLib.java │ │ │ ├── FileLib.java │ │ │ ├── GeoLib.java │ │ │ ├── GoLib.java │ │ │ ├── KeepLib.java │ │ │ ├── MyLog.java │ │ │ ├── MyToast.java │ │ │ ├── RemoteLib.java │ │ │ └── StringLib.java │ │ │ └── remote │ │ │ ├── RemoteService.java │ │ │ └── ServiceGenerator.java │ └── res │ │ ├── drawable │ │ ├── bg_basic_black_transparent.xml │ │ ├── bg_basic_gray.xml │ │ ├── bg_bestfood_drawer.png │ │ ├── bg_index.png │ │ ├── bg_round.xml │ │ ├── bg_round_gray.xml │ │ ├── button_circle.xml │ │ ├── button_round.xml │ │ ├── button_round_green.xml │ │ ├── button_round_red.xml │ │ ├── ic_camera.png │ │ ├── ic_keep_off.png │ │ ├── ic_keep_on.png │ │ ├── ic_list.png │ │ ├── ic_list2.png │ │ ├── ic_map.png │ │ ├── ic_person.png │ │ ├── ic_profile.png │ │ ├── ic_register.png │ │ └── ic_tell.png │ │ ├── layout │ │ ├── activity_bestfood_info.xml │ │ ├── activity_bestfood_register.xml │ │ ├── activity_index.xml │ │ ├── activity_main.xml │ │ ├── activity_permission.xml │ │ ├── activity_profile.xml │ │ ├── activity_profile_icon.xml │ │ ├── app_bar_main.xml │ │ ├── content_main.xml │ │ ├── fragment_bestfood_keep.xml │ │ ├── fragment_bestfood_list.xml │ │ ├── fragment_bestfood_map.xml │ │ ├── fragment_bestfood_register_image.xml │ │ ├── fragment_bestfood_register_input.xml │ │ ├── fragment_bestfood_register_location.xml │ │ ├── loading_layout.xml │ │ ├── nav_header_main.xml │ │ ├── row_bestfood_keep.xml │ │ ├── row_bestfood_list.xml │ │ ├── row_bestfood_map.xml │ │ └── toolbar.xml │ │ ├── menu │ │ ├── activity_main_drawer.xml │ │ ├── menu_close.xml │ │ └── menu_submit.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ └── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── mobitant │ └── bestfood │ └── ExampleUnitTest.java └── web ├── .gitignore ├── app.js ├── bin └── www ├── db.js ├── package.json ├── public ├── css │ └── style.css └── img │ ├── 296.png │ ├── 439.png │ ├── 444.png │ └── 7_1457590813472.png ├── routes ├── food.js ├── keep.js └── member.js └── views ├── error.ejs └── index.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.bak 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # and_node 2 | 오픈소스를 활용한 안드로이드 서비스 개발(with Node.js) 3 | 4 | ## MariaDB 테이블 생성문 5 | 6 | 7 | **bestfood_info 테이블 생성문** 8 | CREATE TABLE IF NOT EXISTS `bestfood_info` ( 9 | `seq` int(11) NOT NULL AUTO_INCREMENT, 10 | `member_seq` int(11) NOT NULL, 11 | `name` varchar(20) NOT NULL, 12 | `tel` varchar(20) NOT NULL, 13 | `address` varchar(50) NOT NULL, 14 | `latitude` double NOT NULL, 15 | `longitude` double NOT NULL, 16 | `description` varchar(500) NOT NULL, 17 | `keep_cnt` int(11) NOT NULL DEFAULT '0', 18 | `reg_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | `PRIMARY KEY (`seq`) 20 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 21 | 22 | 23 | **bestfood_info_image 테이블 생성문** 24 | CREATE TABLE IF NOT EXISTS `bestfood_info_image` ( 25 | `seq` int(11) NOT NULL AUTO_INCREMENT, 26 | `info_seq` int(11) NOT NULL, 27 | `filename` varchar(30) NOT NULL, 28 | `image_memo` varchar(100) NOT NULL, 29 | `reg_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | PRIMARY KEY (`seq`) 31 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 32 | 33 | 34 | **bestfood_keep 테이블 생성문** 35 | CREATE TABLE IF NOT EXISTS `bestfood_keep` ( 36 | `seq` int(11) NOT NULL AUTO_INCREMENT, 37 | `member_seq` int(11) NOT NULL, 38 | `info_seq` int(11) NOT NULL, 39 | `reg_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 40 | PRIMARY KEY (`seq`) 41 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 42 | 43 | 44 | **bestfood_member 테이블 생성문** 45 | CREATE TABLE IF NOT EXISTS `bestfood_member` ( 46 | `seq` int(11) NOT NULL AUTO_INCREMENT, 47 | `phone` varchar(30) NOT NULL, 48 | `name` varchar(30) DEFAULT NULL, 49 | `sextype` varchar(10) DEFAULT NULL, 50 | `birthday` varchar(30) DEFAULT NULL, 51 | `member_icon_filename` varchar(50) DEFAULT NULL, 52 | `reg_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 53 | PRIMARY KEY (`seq`) 54 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion '25.0.3' 6 | 7 | defaultConfig { 8 | applicationId "com.mobitant.bestfood" 9 | minSdkVersion 23 10 | targetSdkVersion 25 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | testCompile 'junit:junit:4.12' 28 | 29 | compile 'com.android.support:appcompat-v7:25.3.1' 30 | compile 'com.android.support:design:25.3.1' 31 | compile 'com.android.support:support-v4:25.3.1' 32 | compile 'com.android.support:recyclerview-v7:25.3.1' 33 | compile 'com.android.support:cardview-v7:25.3.1' 34 | 35 | compile 'com.squareup.retrofit2:retrofit:2.1.0' 36 | compile 'com.squareup.retrofit2:converter-gson:2.1.0' 37 | compile 'com.squareup.okhttp3:logging-interceptor:3.3.1' 38 | compile 'com.squareup.okhttp3:okhttp:3.3.1' 39 | compile 'org.parceler:parceler:1.0.0' 40 | compile 'com.google.code.gson:gson:2.7' 41 | compile 'de.hdodenhof:circleimageview:2.1.0' 42 | compile 'com.squareup.picasso:picasso:2.5.2' 43 | 44 | compile 'com.google.android.gms:play-services-base:10.2.6' 45 | compile 'com.google.android.gms:play-services-maps:10.2.6' 46 | } 47 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Dropbox\MOBITANT_PRJ\Android\sdk1/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mobitant/bestfood/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 20 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/BestFoodKeepFragment.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.StaggeredGridLayoutManager; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.TextView; 13 | 14 | import com.google.android.gms.maps.model.LatLng; 15 | import com.mobitant.bestfood.adapter.KeepListAdapter; 16 | import com.mobitant.bestfood.item.FoodInfoItem; 17 | import com.mobitant.bestfood.item.GeoItem; 18 | import com.mobitant.bestfood.item.KeepItem; 19 | import com.mobitant.bestfood.lib.MyLog; 20 | import com.mobitant.bestfood.remote.RemoteService; 21 | import com.mobitant.bestfood.remote.ServiceGenerator; 22 | 23 | import java.util.ArrayList; 24 | 25 | import retrofit2.Call; 26 | import retrofit2.Callback; 27 | import retrofit2.Response; 28 | 29 | /** 30 | * 맛집 즐겨찾기 리스트를 보여주는 프래그먼트 31 | */ 32 | public class BestFoodKeepFragment extends Fragment { 33 | private final String TAG = this.getClass().getSimpleName(); 34 | 35 | Context context; 36 | int memberSeq; 37 | 38 | RecyclerView keepRecyclerView; 39 | TextView noDataText; 40 | 41 | KeepListAdapter keepListAdapter; 42 | 43 | ArrayList keepList = new ArrayList<>(); 44 | 45 | /** 46 | * BestFoodKeepFragment 인스턴스를 생성한다. 47 | * @return BestFoodListFragment 인스턴스 48 | */ 49 | public static BestFoodKeepFragment newInstance() { 50 | BestFoodKeepFragment f = new BestFoodKeepFragment(); 51 | return f; 52 | } 53 | 54 | /** 55 | * fragment_bestfood_keep.xml 기반으로 뷰를 생성한다. 56 | * @param inflater XML를 객체로 변환하는 LayoutInflater 객체 57 | * @param container null이 아니라면 부모 뷰 58 | * @param savedInstanceState null이 아니라면 이전에 저장된 상태를 가진 객체 59 | * @return 생성한 뷰 객체 60 | */ 61 | @Override 62 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 63 | context = this.getActivity(); 64 | 65 | memberSeq = ((MyApp)getActivity().getApplication()).getMemberSeq(); 66 | 67 | View layout = inflater.inflate(R.layout.fragment_bestfood_keep, container, false); 68 | 69 | return layout; 70 | } 71 | 72 | /** 73 | * 프래그먼트가 일시 중지 상태가 되었다가 다시 보여질 때 호출된다. 74 | * BestFoodInfoActivity가 실행된 후, 75 | * 즐겨찾기 상태가 변경되었을 경우 이를 반영하는 용도로 사용한다. 76 | */ 77 | @Override 78 | public void onResume() { 79 | super.onResume(); 80 | 81 | MyApp myApp = ((MyApp) getActivity().getApplication()); 82 | FoodInfoItem currentInfoItem = myApp.getFoodInfoItem(); 83 | 84 | if (keepListAdapter != null && currentInfoItem != null) { 85 | keepListAdapter.setItem(currentInfoItem); 86 | myApp.setFoodInfoItem(null); 87 | 88 | if (keepListAdapter.getItemCount() == 0) { 89 | noDataText.setVisibility(View.VISIBLE); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * onCreateView() 메소드 뒤에 호출되며 화면 뷰들을 설정한다. 96 | * @param view onCreateView() 메소드에 의해 반환된 뷰 97 | * @param savedInstanceState null이 아니라면 이전에 저장된 상태를 가진 객체 98 | */ 99 | @Override 100 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 101 | super.onViewCreated(view, savedInstanceState); 102 | ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.nav_keep); 103 | 104 | keepRecyclerView = (RecyclerView) view.findViewById(R.id.keep_list); 105 | noDataText = (TextView) view.findViewById(R.id.no_keep); 106 | 107 | keepListAdapter = new KeepListAdapter(context, 108 | R.layout.row_bestfood_keep, keepList, memberSeq); 109 | StaggeredGridLayoutManager layoutManager 110 | = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); 111 | layoutManager.setGapStrategy( 112 | StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 113 | keepRecyclerView.setLayoutManager(layoutManager); 114 | keepRecyclerView.setAdapter(keepListAdapter); 115 | 116 | listKeep(memberSeq, GeoItem.getKnownLocation()); 117 | } 118 | 119 | /** 120 | * 서버에서 즐겨찾기한 맛집 정보를 조회한다. 121 | * @param memberSeq 사용자 시퀀스 122 | * @param userLatLng 사용자 위도 경도 객체 123 | */ 124 | private void listKeep(int memberSeq, LatLng userLatLng) { 125 | RemoteService remoteService = ServiceGenerator.createService(RemoteService.class); 126 | 127 | Call> call 128 | = remoteService.listKeep(memberSeq, userLatLng.latitude, userLatLng.longitude); 129 | call.enqueue(new Callback>() { 130 | @Override 131 | public void onResponse(Call> call, 132 | Response> response) { 133 | ArrayList list = response.body(); 134 | 135 | if (list == null) { 136 | list = new ArrayList<>(); 137 | } 138 | 139 | noDataText.setVisibility(View.GONE); 140 | 141 | if (response.isSuccessful()) { 142 | MyLog.d(TAG, "list size " + list.size()); 143 | if (list.size() == 0) { 144 | noDataText.setVisibility(View.VISIBLE); 145 | } else { 146 | keepListAdapter.setItemList(list); 147 | } 148 | } else { 149 | MyLog.d(TAG, "not success"); 150 | } 151 | } 152 | 153 | @Override 154 | public void onFailure(Call> call, Throwable t) { 155 | MyLog.d(TAG, "no internet connectivity"); 156 | MyLog.d(TAG, t.toString()); 157 | } 158 | }); 159 | } 160 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/BestFoodRegisterActivity.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v7.app.ActionBar; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.Toolbar; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | 13 | import com.mobitant.bestfood.item.FoodInfoItem; 14 | import com.mobitant.bestfood.item.GeoItem; 15 | import com.mobitant.bestfood.lib.GoLib; 16 | import com.mobitant.bestfood.lib.MyLog; 17 | 18 | /** 19 | * 맛집 등록 액티비티이다. 20 | * 액티비티의 기본적인 화면 구성을 하며 실제 사용자 화면은 프래그먼트로 구성한다. 21 | */ 22 | public class BestFoodRegisterActivity extends AppCompatActivity { 23 | private final String TAG = this.getClass().getSimpleName(); 24 | public static FoodInfoItem currentItem = null; 25 | 26 | Context context; 27 | 28 | /** 29 | * BestFoodRegisterLocationFragment를 실행하기 위한 기본적인 정보를 설정하고 30 | * 프래그먼트를 실행한다. 31 | */ 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_bestfood_register); 36 | 37 | context = this; 38 | 39 | int memberSeq = ((MyApp)getApplication()).getMemberSeq(); 40 | 41 | //BestFoodRegisterLocationFragment로 넘길 기본적인 정보를 저장한다. 42 | FoodInfoItem infoItem = new FoodInfoItem(); 43 | infoItem.memberSeq = memberSeq; 44 | infoItem.latitude = GeoItem.getKnownLocation().latitude; 45 | infoItem.longitude = GeoItem.getKnownLocation().longitude; 46 | 47 | MyLog.d(TAG, "infoItem " + infoItem.toString()); 48 | 49 | setToolbar(); 50 | 51 | //BestFoodRegisterLocationFragment를 화면에 보여준다. 52 | GoLib.getInstance().goFragment(getSupportFragmentManager(), 53 | R.id.content_main, BestFoodRegisterLocationFragment.newInstance(infoItem)); 54 | } 55 | 56 | /** 57 | * 툴바를 설정한다. 58 | */ 59 | private void setToolbar() { 60 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 61 | setSupportActionBar(toolbar); 62 | 63 | ActionBar actionBar = getSupportActionBar(); 64 | 65 | if (actionBar != null) { 66 | actionBar.setDisplayHomeAsUpEnabled(true); 67 | actionBar.setTitle(R.string.bestfood_register); 68 | } 69 | } 70 | 71 | /** 72 | * 오른쪽 상단 메뉴를 구성한다. 73 | * 닫기 메뉴만이 설정되어 있는 menu_close.xml를 지정한다. 74 | * @param menu 메뉴 객체 75 | * @return 메뉴를 보여준다면 true, 보여주지 않는다면 false 76 | */ 77 | @Override 78 | public boolean onCreateOptionsMenu(Menu menu) { 79 | getMenuInflater().inflate(R.menu.menu_close, menu); 80 | return true; 81 | } 82 | 83 | /** 84 | * 왼쪽 화살표 메뉴(android.R.id.home)를 클릭했을 때와 85 | * 오른쪽 상단 닫기 메뉴를 클릭했을 때의 동작을 지정한다. 86 | * 여기서는 모든 버튼이 액티비티를 종료한다. 87 | * @param item 메뉴 아이템 객체 88 | * @return 메뉴를 처리했다면 true, 그렇지 않다면 false 89 | */ 90 | @Override 91 | public boolean onOptionsItemSelected(MenuItem item) { 92 | switch (item.getItemId()) { 93 | case android.R.id.home: 94 | finish(); 95 | break; 96 | 97 | case R.id.action_close: 98 | finish(); 99 | break; 100 | } 101 | 102 | return true; 103 | } 104 | 105 | /** 106 | * 다른 액티비티를 실행한 결과를 처리하는 메소드 107 | * (실제로는 프래그먼트로 onActivityResult 호출을 전달하기 위한 목적으로 작성) 108 | * @param requestCode 액티비티를 실행하면서 전달한 요청 코드 109 | * @param resultCode 실행한 액티비티가 설정한 결과 코드 110 | * @param data 결과 데이터 111 | */ 112 | @Override 113 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 114 | for (Fragment fragment : getSupportFragmentManager().getFragments()) { 115 | fragment.onActivityResult(requestCode, resultCode, data); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/BestFoodRegisterInputFragment.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.content.Context; 4 | import android.location.Address; 5 | import android.os.Bundle; 6 | import android.support.v4.app.Fragment; 7 | import android.text.Editable; 8 | import android.text.TextWatcher; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.Button; 13 | import android.widget.EditText; 14 | import android.widget.TextView; 15 | 16 | import com.google.android.gms.maps.model.LatLng; 17 | import com.mobitant.bestfood.item.FoodInfoItem; 18 | import com.mobitant.bestfood.lib.EtcLib; 19 | import com.mobitant.bestfood.lib.GeoLib; 20 | import com.mobitant.bestfood.lib.GoLib; 21 | import com.mobitant.bestfood.lib.MyLog; 22 | import com.mobitant.bestfood.lib.MyToast; 23 | import com.mobitant.bestfood.lib.StringLib; 24 | import com.mobitant.bestfood.remote.RemoteService; 25 | import com.mobitant.bestfood.remote.ServiceGenerator; 26 | 27 | import org.parceler.Parcels; 28 | 29 | import okhttp3.ResponseBody; 30 | import retrofit2.Call; 31 | import retrofit2.Callback; 32 | import retrofit2.Response; 33 | 34 | /** 35 | * 맛집 정보를 입력하는 액티비티 36 | */ 37 | public class BestFoodRegisterInputFragment extends Fragment implements View.OnClickListener { 38 | public static final String INFO_ITEM = "INFO_ITEM"; 39 | private final String TAG = this.getClass().getSimpleName(); 40 | 41 | Context context; 42 | FoodInfoItem infoItem; 43 | Address address; 44 | 45 | EditText nameEdit; 46 | EditText telEdit; 47 | EditText descriptionEdit; 48 | TextView currentLength; 49 | 50 | /** 51 | * FoodInfoItem 객체를 인자로 저장하는 52 | * BestFoodRegisterInputFragment 인스턴스를 생성해서 반환한다. 53 | * @param infoItem 맛집 정보를 저장하는 객체 54 | * @return BestFoodRegisterInputFragment 인스턴스 55 | */ 56 | public static BestFoodRegisterInputFragment newInstance(FoodInfoItem infoItem) { 57 | Bundle bundle = new Bundle(); 58 | bundle.putParcelable(INFO_ITEM, Parcels.wrap(infoItem)); 59 | 60 | BestFoodRegisterInputFragment fragment = new BestFoodRegisterInputFragment(); 61 | fragment.setArguments(bundle); 62 | 63 | return fragment; 64 | } 65 | 66 | /** 67 | * 프래그먼트가 생성될 때 호출되며 인자에 저장된 FoodInfoItem를 68 | * BestFoodRegisterActivity에 currentItem를 저장한다. 69 | * @param savedInstanceState 프래그먼트가 새로 생성되었을 경우, 이전 상태 값을 가지는 객체 70 | */ 71 | @Override 72 | public void onCreate(Bundle savedInstanceState) { 73 | super.onCreate(savedInstanceState); 74 | 75 | if (getArguments() != null) { 76 | infoItem = Parcels.unwrap(getArguments().getParcelable(INFO_ITEM)); 77 | if (infoItem.seq != 0) { 78 | BestFoodRegisterActivity.currentItem = infoItem; 79 | } 80 | MyLog.d(TAG, "infoItem " + infoItem); 81 | } 82 | } 83 | 84 | /** 85 | * fragment_bestfood_register_input.xml 기반으로 뷰를 생성한다. 86 | * @param inflater XML를 객체로 변환하는 LayoutInflater 객체 87 | * @param container null이 아니라면 부모 뷰 88 | * @param savedInstanceState null이 아니라면 이전에 저장된 상태를 가진 객체 89 | * @return 생성한 뷰 객체 90 | */ 91 | @Override 92 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 93 | context = this.getActivity(); 94 | address = GeoLib.getInstance().getAddressString(context, 95 | new LatLng(infoItem.latitude, infoItem.longitude)); 96 | MyLog.d(TAG, "address" + address); 97 | 98 | return inflater.inflate(R.layout.fragment_bestfood_register_input, container, false); 99 | } 100 | 101 | /** 102 | * onCreateView() 메소드 뒤에 호출되며 맛집 정보를 입력할 뷰들을 생성한다. 103 | * @param view onCreateView() 메소드에 의해 반환된 뷰 104 | * @param savedInstanceState null이 아니라면 이전에 저장된 상태를 가진 객체 105 | */ 106 | @Override 107 | public void onViewCreated(View view, Bundle savedInstanceState) { 108 | super.onViewCreated(view, savedInstanceState); 109 | 110 | currentLength = (TextView) view.findViewById(R.id.current_length); 111 | nameEdit = (EditText) view.findViewById(R.id.bestfood_name); 112 | telEdit = (EditText) view.findViewById(R.id.bestfood_tel); 113 | descriptionEdit = (EditText) view.findViewById(R.id.bestfood_description); 114 | descriptionEdit.addTextChangedListener(new TextWatcher() { 115 | @Override 116 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 117 | } 118 | 119 | @Override 120 | public void onTextChanged(CharSequence s, int start, int before, int count) { 121 | currentLength.setText(String.valueOf(s.length())); 122 | } 123 | 124 | @Override 125 | public void afterTextChanged(Editable s) { 126 | } 127 | }); 128 | 129 | EditText addressEdit = (EditText) view.findViewById(R.id.bestfood_address); 130 | 131 | infoItem.address = GeoLib.getInstance().getAddressString(address); 132 | if (!StringLib.getInstance().isBlank(infoItem.address)) { 133 | addressEdit.setText(infoItem.address); 134 | } 135 | 136 | Button prevButton = (Button) view.findViewById(R.id.prev); 137 | prevButton.setOnClickListener(this); 138 | 139 | Button nextButton = (Button) view.findViewById(R.id.next); 140 | nextButton.setOnClickListener(this); 141 | } 142 | 143 | /** 144 | * 클릭이벤트를 처리한다. 145 | * @param v 클릭한 뷰에 대한 정보 146 | */ 147 | @Override 148 | public void onClick(View v) { 149 | infoItem.name = nameEdit.getText().toString(); 150 | infoItem.tel = telEdit.getText().toString(); 151 | infoItem.description = descriptionEdit.getText().toString(); 152 | MyLog.d(TAG, "onClick imageItem " + infoItem); 153 | 154 | if (v.getId() == R.id.prev) { 155 | GoLib.getInstance().goFragment(getFragmentManager(), 156 | R.id.content_main, BestFoodRegisterLocationFragment.newInstance(infoItem)); 157 | } else if (v.getId() == R.id.next) { 158 | save(); 159 | } 160 | } 161 | 162 | /** 163 | * 사용자가 입력한 정보를 확인하고 저장한다. 164 | */ 165 | private void save() { 166 | if (StringLib.getInstance().isBlank(infoItem.name)) { 167 | MyToast.s(context, context.getResources().getString(R.string.input_bestfood_name)); 168 | return; 169 | } 170 | 171 | if (StringLib.getInstance().isBlank(infoItem.tel) 172 | || !EtcLib.getInstance().isValidPhoneNumber(infoItem.tel)) { 173 | MyToast.s(context, context.getResources().getString(R.string.not_valid_tel_number)); 174 | return; 175 | } 176 | 177 | insertFoodInfo(); 178 | } 179 | 180 | /** 181 | * 사용자가 입력한 정보를 서버에 저장한다. 182 | */ 183 | private void insertFoodInfo() { 184 | MyLog.d(TAG, infoItem.toString()); 185 | 186 | RemoteService remoteService = ServiceGenerator.createService(RemoteService.class); 187 | 188 | Call call = remoteService.insertFoodInfo(infoItem); 189 | call.enqueue(new Callback() { 190 | @Override 191 | public void onResponse(Call call, Response response) { 192 | if (response.isSuccessful()) { 193 | int seq = 0; 194 | String seqString = response.body(); 195 | 196 | try { 197 | seq = Integer.parseInt(seqString); 198 | } catch (Exception e) { 199 | seq = 0; 200 | } 201 | 202 | if (seq == 0) { 203 | //등록 실패 204 | } else { 205 | infoItem.seq = seq; 206 | goNextPage(); 207 | } 208 | } else { // 등록 실패 209 | int statusCode = response.code(); 210 | ResponseBody errorBody = response.errorBody(); 211 | MyLog.d(TAG, "fail " + statusCode + errorBody.toString()); 212 | } 213 | } 214 | 215 | @Override 216 | public void onFailure(Call call, Throwable t) { 217 | MyLog.d(TAG, "no internet connectivity"); 218 | } 219 | }); 220 | } 221 | 222 | /** 223 | * 맛집 이미지를 등록할 수 있는 프래그먼트로 이동한다. 224 | */ 225 | private void goNextPage() { 226 | GoLib.getInstance().goFragmentBack(getFragmentManager(), 227 | R.id.content_main, BestFoodRegisterImageFragment.newInstance(infoItem.seq)); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/Constant.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | public interface Constant { 4 | int MAX_LENGTH_DESCRIPTION = 50; 5 | 6 | int MAP_MAX_ZOOM_LEVEL = 10; 7 | int MAP_ZOOM_LEVEL_DETAIL = 13; 8 | 9 | String ORDER_TYPE_METER = ""; 10 | String ORDER_TYPE_FAVORITE = "keep_cnt"; 11 | String ORDER_TYPE_RECENT = "reg_date"; 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/IndexActivity.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.view.View; 9 | import android.widget.Button; 10 | import android.widget.TextView; 11 | 12 | import com.mobitant.bestfood.item.MemberInfoItem; 13 | import com.mobitant.bestfood.lib.EtcLib; 14 | import com.mobitant.bestfood.lib.GeoLib; 15 | import com.mobitant.bestfood.lib.MyLog; 16 | import com.mobitant.bestfood.lib.RemoteLib; 17 | import com.mobitant.bestfood.lib.StringLib; 18 | import com.mobitant.bestfood.remote.RemoteService; 19 | import com.mobitant.bestfood.remote.ServiceGenerator; 20 | 21 | import okhttp3.ResponseBody; 22 | import retrofit2.Call; 23 | import retrofit2.Callback; 24 | import retrofit2.Response; 25 | 26 | /** 27 | * 시작 액티비티이며 이 액티비티에서 사용자 정보를 조회해서 28 | * 메인 액티비티를 실행할 지, 프로필 액티비티를 실행할 지를 결정한다. 29 | */ 30 | public class IndexActivity extends AppCompatActivity { 31 | private final String TAG = this.getClass().getSimpleName(); 32 | 33 | Context context; 34 | 35 | /** 36 | * 레이아웃을 설정하고 인터넷에 연결되어 있는지를 확인한다. 37 | * 만약 인터넷에 연결되어 있지 않다면 showNoService() 메소드를 호출한다. 38 | * @param savedInstanceState 액티비티가 새로 생성되었을 경우, 이전 상태 값을 가지는 객체 39 | */ 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | setContentView(R.layout.activity_index); 44 | 45 | context = this; 46 | 47 | if (!RemoteLib.getInstance().isConnected(context)) { 48 | showNoService(); 49 | return; 50 | } 51 | } 52 | 53 | /** 54 | * 일정 시간(1.2초) 이후에 startTask() 메소드를 호출해서 55 | * 서버에서 사용자 정보를 조회한다. 56 | */ 57 | @Override 58 | protected void onStart() { 59 | super.onStart(); 60 | 61 | Handler mHandler = new Handler(); 62 | mHandler.postDelayed(new Runnable() { 63 | @Override 64 | public void run() { 65 | startTask(); 66 | } 67 | }, 1200); 68 | } 69 | 70 | /** 71 | * 현재 인터넷에 접속할 수 없기 때문에 서비스를 사용할 수 없다는 메시지와 72 | * 화면 종료 버튼을 보여준다. 73 | */ 74 | private void showNoService() { 75 | TextView messageText = (TextView) findViewById(R.id.message); 76 | messageText.setVisibility(View.VISIBLE); 77 | 78 | Button closeButton = (Button) findViewById(R.id.close); 79 | closeButton.setOnClickListener(new View.OnClickListener() { 80 | @Override 81 | public void onClick(View v) { 82 | finish(); 83 | } 84 | }); 85 | closeButton.setVisibility(View.VISIBLE); 86 | } 87 | 88 | /** 89 | * 현재 폰의 전화번호와 동일한 사용자 정보를 조회할 수 있도록 90 | * selectMemberInfo() 메소드를 호출한다. 91 | * 그리고 setLastKnownLocation() 메소드를 호출해서 현재 위치 정보를 설정한다. 92 | */ 93 | public void startTask() { 94 | String phone = EtcLib.getInstance().getPhoneNumber(this); 95 | 96 | selectMemberInfo(phone); 97 | GeoLib.getInstance().setLastKnownLocation(this); 98 | } 99 | 100 | /** 101 | * 리트로핏을 활용해서 서버로부터 사용자 정보를 조회한다. 102 | * 사용자 정보를 조회했다면 setMemberInfoItem() 메소드를 호출하고 103 | * 그렇지 않다면 goProfileActivity() 메소드를 호출한다. 104 | * 105 | * @param phone 폰의 전화번호 106 | */ 107 | public void selectMemberInfo(String phone) { 108 | RemoteService remoteService = ServiceGenerator.createService(RemoteService.class); 109 | 110 | Call call = remoteService.selectMemberInfo(phone); 111 | call.enqueue(new Callback() { 112 | @Override 113 | public void onResponse(Call call, Response response) { 114 | MemberInfoItem item = response.body(); 115 | 116 | if (response.isSuccessful() && !StringLib.getInstance().isBlank(item.name)) { 117 | MyLog.d(TAG, "success " + response.body().toString()); 118 | setMemberInfoItem(item); 119 | } else { 120 | MyLog.d(TAG, "not success"); 121 | goProfileActivity(item); 122 | } 123 | } 124 | 125 | @Override 126 | public void onFailure(Call call, Throwable t) { 127 | MyLog.d(TAG, "no internet connectivity"); 128 | MyLog.d(TAG, t.toString()); 129 | } 130 | }); 131 | } 132 | 133 | /** 134 | * 전달받은 MemberInfoItem을 Application 객체에 저장한다. 135 | * 그리고 startMain() 메소드를 호출한다. 136 | * 137 | * @param item 사용자 정보 138 | */ 139 | private void setMemberInfoItem(MemberInfoItem item) { 140 | ((MyApp) getApplicationContext()).setMemberInfoItem(item); 141 | 142 | startMain(); 143 | } 144 | 145 | /** 146 | * MainActivity를 실행하고 현재 액티비티를 종료한다. 147 | */ 148 | public void startMain() { 149 | Intent intent = new Intent(IndexActivity.this, MainActivity.class); 150 | startActivity(intent); 151 | 152 | finish(); 153 | } 154 | 155 | /** 156 | * 사용자 정보를 조회하지 못했다면 insertMemberPhone() 메소드를 통해 157 | * 전화번호를 서버에 저장하고 MainActivity를 실행한 후 ProfileActivity를 실행한다. 158 | * 그리고 현재 액티비티를 종료한다. 159 | * 160 | * @param item 사용자 정보 161 | */ 162 | private void goProfileActivity(MemberInfoItem item) { 163 | if (item == null || item.seq <= 0) { 164 | insertMemberPhone(); 165 | } 166 | 167 | Intent intent = new Intent(IndexActivity.this, MainActivity.class); 168 | startActivity(intent); 169 | 170 | Intent intent2 = new Intent(this, ProfileActivity.class); 171 | startActivity(intent2); 172 | 173 | finish(); 174 | } 175 | 176 | /** 177 | * 폰의 전화번호를 서버에 저장한다. 178 | */ 179 | private void insertMemberPhone() { 180 | String phone = EtcLib.getInstance().getPhoneNumber(context); 181 | RemoteService remoteService = 182 | ServiceGenerator.createService(RemoteService.class); 183 | 184 | Call call = remoteService.insertMemberPhone(phone); 185 | call.enqueue(new Callback() { 186 | @Override 187 | public void onResponse(Call call, Response response) { 188 | if (response.isSuccessful()) { 189 | MyLog.d(TAG, "success insert id " + response.body().toString()); 190 | } else { 191 | int statusCode = response.code(); 192 | 193 | ResponseBody errorBody = response.errorBody(); 194 | 195 | MyLog.d(TAG, "fail " + statusCode + errorBody.toString()); 196 | } 197 | } 198 | 199 | @Override 200 | public void onFailure(Call call, Throwable t) { 201 | MyLog.d(TAG, "no internet connectivity"); 202 | } 203 | }); 204 | } 205 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.NavigationView; 5 | import android.support.v4.view.GravityCompat; 6 | import android.support.v4.widget.DrawerLayout; 7 | import android.support.v7.app.ActionBarDrawerToggle; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.Toolbar; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.widget.TextView; 13 | 14 | import com.mobitant.bestfood.item.MemberInfoItem; 15 | import com.mobitant.bestfood.lib.GoLib; 16 | import com.mobitant.bestfood.lib.StringLib; 17 | import com.mobitant.bestfood.remote.RemoteService; 18 | import com.squareup.picasso.Picasso; 19 | 20 | import de.hdodenhof.circleimageview.CircleImageView; 21 | 22 | /** 23 | * 맛집 정보앱의 핵심 액티비티이며 왼쪽에 네비게이션 뷰를 가지며 24 | * 다양한 프래그먼트를 보여주는 컨테이너 역할을 한다. 25 | */ 26 | public class MainActivity extends AppCompatActivity 27 | implements NavigationView.OnNavigationItemSelectedListener { 28 | private final String TAG = getClass().getSimpleName(); 29 | 30 | MemberInfoItem memberInfoItem; 31 | DrawerLayout drawer; 32 | View headerLayout; 33 | 34 | CircleImageView profileIconImage; 35 | 36 | /** 37 | * 액티비티와 네비게이션 뷰를 설정하고 BestFoodListFragment를 화면에 보여준다. 38 | * @param savedInstanceState 액티비티가 새로 생성되었을 경우, 이전 상태 값을 가지는 객체 39 | */ 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | setContentView(R.layout.activity_main); 44 | 45 | memberInfoItem = ((MyApp)getApplication()).getMemberInfoItem(); 46 | 47 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 48 | setSupportActionBar(toolbar); 49 | 50 | drawer = (DrawerLayout) findViewById(R.id.drawer_layout); 51 | 52 | ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( 53 | this, drawer, toolbar, R.string.navigation_drawer_open, 54 | R.string.navigation_drawer_close); 55 | drawer.addDrawerListener(toggle); 56 | toggle.syncState(); 57 | 58 | NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); 59 | navigationView.setNavigationItemSelectedListener(this); 60 | 61 | headerLayout = navigationView.getHeaderView(0); 62 | 63 | GoLib.getInstance() 64 | .goFragment(getSupportFragmentManager(), R.id.content_main, 65 | BestFoodListFragment.newInstance()); 66 | 67 | 68 | } 69 | 70 | /** 71 | * 프로필 정보는 별도 액티비티에서 변경될 수 있으므로 72 | * 변경을 바로 감지하기 위해 화면이 새로 보여질 대마다 setProfileView() 를 호출한다. 73 | */ 74 | @Override 75 | protected void onResume() { 76 | super.onResume(); 77 | 78 | setProfileView(); 79 | } 80 | 81 | /** 82 | * 프로필 이미지와 프로필 이름을 설정한다. 83 | */ 84 | private void setProfileView() { 85 | profileIconImage = (CircleImageView) headerLayout.findViewById(R.id.profile_icon); 86 | profileIconImage.setOnClickListener(new View.OnClickListener() { 87 | @Override 88 | public void onClick(View v) { 89 | drawer.closeDrawer(GravityCompat.START); 90 | GoLib.getInstance().goProfileActivity(MainActivity.this); 91 | } 92 | }); 93 | 94 | if (StringLib.getInstance().isBlank(memberInfoItem.memberIconFilename)) { 95 | Picasso.with(this).load(R.drawable.ic_person).into(profileIconImage); 96 | } else { 97 | Picasso.with(this) 98 | .load(RemoteService.MEMBER_ICON_URL + memberInfoItem.memberIconFilename) 99 | .into(profileIconImage); 100 | } 101 | 102 | TextView nameText = (TextView) headerLayout.findViewById(R.id.name); 103 | 104 | if (memberInfoItem.name == null || memberInfoItem.name.equals("")) { 105 | nameText.setText(R.string.name_need); 106 | } else { 107 | nameText.setText(memberInfoItem.name); 108 | } 109 | } 110 | 111 | /** 112 | * 폰에서 뒤로가기 버튼을 클릭했을 때 호출하는 메소드이며 113 | * 네비게이션 메뉴가 보여진 상태일 경우, 네비게이션 메뉴를 닫는다. 114 | */ 115 | @Override 116 | public void onBackPressed() { 117 | if (drawer.isDrawerOpen(GravityCompat.START)) { 118 | drawer.closeDrawer(GravityCompat.START); 119 | } else { 120 | super.onBackPressed(); 121 | } 122 | } 123 | 124 | /** 125 | * 네비게이션 메뉴를 클릭했을 때 호출되는 메소드 126 | * @param item 메뉴 아이템 객체 127 | * @return 메뉴 클릭 이벤트의 처리 여부 128 | */ 129 | @SuppressWarnings("StatementWithEmptyBody") 130 | @Override 131 | public boolean onNavigationItemSelected(MenuItem item) { 132 | int id = item.getItemId(); 133 | 134 | if (id == R.id.nav_list) { 135 | GoLib.getInstance().goFragment(getSupportFragmentManager(), 136 | R.id.content_main, BestFoodListFragment.newInstance()); 137 | 138 | } else if (id == R.id.nav_map) { 139 | GoLib.getInstance().goFragment(getSupportFragmentManager(), 140 | R.id.content_main, BestFoodMapFragment.newInstance()); 141 | 142 | } else if (id == R.id.nav_keep) { 143 | GoLib.getInstance().goFragment(getSupportFragmentManager(), 144 | R.id.content_main, BestFoodKeepFragment.newInstance()); 145 | 146 | } else if (id == R.id.nav_register) { 147 | GoLib.getInstance().goBestFoodRegisterActivity(this); 148 | 149 | } else if (id == R.id.nav_profile) { 150 | GoLib.getInstance().goProfileActivity(this); 151 | } 152 | 153 | drawer.closeDrawer(GravityCompat.START); 154 | return true; 155 | } 156 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/MyApp.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.app.Application; 4 | import android.os.StrictMode; 5 | 6 | import com.mobitant.bestfood.item.FoodInfoItem; 7 | import com.mobitant.bestfood.item.MemberInfoItem; 8 | 9 | /** 10 | * 앱 전역에서 사용할 수 있는 클래스 11 | */ 12 | public class MyApp extends Application { 13 | private MemberInfoItem memberInfoItem; 14 | private FoodInfoItem foodInfoItem; 15 | 16 | @Override 17 | public void onCreate() { 18 | super.onCreate(); 19 | 20 | // FileUriExposedException 문제를 해결하기 위한 코드 21 | // 관련 설명은 책의 [참고] 페이지 참고 22 | StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); 23 | StrictMode.setVmPolicy(builder.build()); 24 | } 25 | 26 | public MemberInfoItem getMemberInfoItem() { 27 | if (memberInfoItem == null) memberInfoItem = new MemberInfoItem(); 28 | 29 | return memberInfoItem; 30 | } 31 | 32 | public void setMemberInfoItem(MemberInfoItem item) { 33 | this.memberInfoItem = item; 34 | } 35 | 36 | public int getMemberSeq() { 37 | return memberInfoItem.seq; 38 | } 39 | 40 | public void setFoodInfoItem(FoodInfoItem foodInfoItem) { 41 | this.foodInfoItem = foodInfoItem; 42 | } 43 | 44 | public FoodInfoItem getFoodInfoItem() { 45 | return foodInfoItem; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/PermissionActivity.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.Manifest; 4 | import android.content.DialogInterface; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.os.Bundle; 10 | import android.provider.Settings; 11 | import android.support.v4.app.ActivityCompat; 12 | import android.support.v4.content.ContextCompat; 13 | import android.support.v7.app.AlertDialog; 14 | import android.support.v7.app.AppCompatActivity; 15 | 16 | import com.mobitant.bestfood.lib.MyToast; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * 앱을 실행할 때 필요한 권한을 처리하기 위한 액티비티 23 | */ 24 | public class PermissionActivity extends AppCompatActivity { 25 | private static final int PERMISSION_MULTI_CODE = 100; 26 | 27 | /** 28 | * 화면을 구성하고 SDK 버전과 권한에 따른 처리를 한다. 29 | * @param savedInstanceState 액티비티가 새로 생성되었을 경우, 이전 상태 값을 가지는 객체 30 | */ 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_permission); 35 | 36 | if (Build.VERSION.SDK_INT < 23) { 37 | goIndexActivity(); 38 | } else { 39 | if (checkAndRequestPermissions()) { 40 | goIndexActivity(); 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * 권한을 확인하고 권한이 부여되어 있지 않다면 권한을 요청한다. 47 | * @return 필요한 권한이 모두 부여되었다면 true, 그렇지 않다면 false 48 | */ 49 | private boolean checkAndRequestPermissions() { 50 | String [] permissions = new String[]{ 51 | Manifest.permission.CAMERA, 52 | Manifest.permission.READ_PHONE_STATE, 53 | Manifest.permission.ACCESS_FINE_LOCATION 54 | }; 55 | 56 | List listPermissionsNeeded = new ArrayList<>(); 57 | 58 | for (String permission:permissions) { 59 | if (ContextCompat.checkSelfPermission(this,permission ) 60 | != PackageManager.PERMISSION_GRANTED){ 61 | listPermissionsNeeded.add(permission); 62 | } 63 | } 64 | 65 | if (!listPermissionsNeeded.isEmpty()) { 66 | ActivityCompat.requestPermissions(this, 67 | listPermissionsNeeded.toArray(new String[listPermissionsNeeded.size()]), 68 | PERMISSION_MULTI_CODE); 69 | return false; 70 | } 71 | 72 | return true; 73 | } 74 | 75 | /** 76 | * 권한 요청 결과를 받는 메소드 77 | * @param requestCode 요청 코드 78 | * @param permissions 권한 종류 79 | * @param grantResults 권한 결과 80 | */ 81 | @Override 82 | public void onRequestPermissionsResult(int requestCode, String[] permissions, 83 | int[] grantResults) { 84 | if (grantResults.length == 0) return; 85 | 86 | switch (requestCode) { 87 | case PERMISSION_MULTI_CODE: 88 | checkPermissionResult(permissions, grantResults); 89 | 90 | break; 91 | } 92 | } 93 | 94 | /** 95 | * 권한 처리 결과를 보고 인덱스 액티비티를 실행할 지, 96 | * 권한 설정 요청 다이얼로그를 보여줄 지를 결정한다. 97 | * 모든 권한이 승인되었을 경우에는 goIndexActivity() 메소드를 호출한다. 98 | * @param permissions 권한 종류 99 | * @param grantResults 권한 부여 결과 100 | */ 101 | private void checkPermissionResult(String[] permissions, int[] grantResults) { 102 | boolean isAllGranted = true; 103 | 104 | for (int i = 0; i < permissions.length; i++) { 105 | if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { 106 | isAllGranted = false; 107 | } 108 | } 109 | 110 | //권한이 부여되었다면 111 | if (isAllGranted) { 112 | goIndexActivity(); 113 | 114 | //권한이 부여되어 있지 않다면 115 | } else { 116 | showPermissionDialog(); 117 | } 118 | } 119 | 120 | /** 121 | * 인덱스 액티비티를 실행하고 현재 액티비티를 종료한다. 122 | */ 123 | private void goIndexActivity() { 124 | Intent intent = new Intent(this, IndexActivity.class); 125 | startActivity(intent); 126 | 127 | finish(); 128 | } 129 | 130 | /** 131 | * 권한 설정 화면으로 이동할 지를 선택하는 다이얼로그를 보여준다. 132 | */ 133 | private void showPermissionDialog() { 134 | AlertDialog.Builder dialog = new AlertDialog.Builder(this); 135 | dialog.setTitle(R.string.permission_setting_title); 136 | dialog.setMessage(R.string.permission_setting_message); 137 | dialog.setPositiveButton(R.string.setting, new DialogInterface.OnClickListener() { 138 | public void onClick(DialogInterface dialog, int which) { 139 | dialog.cancel(); 140 | MyToast.s(PermissionActivity.this, R.string.permission_setting_restart); 141 | PermissionActivity.this.finish(); 142 | 143 | goAppSettingActivity(); 144 | } 145 | }); 146 | dialog.setNegativeButton(R.string.cancel,new DialogInterface.OnClickListener() { 147 | public void onClick(DialogInterface dialog, int which) { 148 | dialog.cancel(); 149 | PermissionActivity.this.finish(); 150 | } 151 | }); 152 | dialog.show(); 153 | } 154 | 155 | /** 156 | * 권한을 설정할 수 있는 설정 액티비티를 실행한다. 157 | */ 158 | private void goAppSettingActivity() { 159 | Intent intent = new Intent(); 160 | intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 161 | Uri uri = Uri.fromParts("package", getPackageName(), null); 162 | intent.setData(uri); 163 | startActivity(intent); 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/ProfileIconActivity.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.graphics.Bitmap; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.provider.MediaStore; 9 | import android.support.v7.app.ActionBar; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | import android.view.View; 15 | import android.widget.Button; 16 | import android.widget.ImageView; 17 | 18 | import com.mobitant.bestfood.item.MemberInfoItem; 19 | import com.mobitant.bestfood.lib.FileLib; 20 | import com.mobitant.bestfood.lib.MyLog; 21 | import com.mobitant.bestfood.lib.RemoteLib; 22 | import com.mobitant.bestfood.lib.StringLib; 23 | import com.mobitant.bestfood.remote.RemoteService; 24 | import com.squareup.picasso.Picasso; 25 | 26 | import java.io.File; 27 | 28 | /** 29 | * 프로필 아이콘을 등록하는 액티비티 30 | */ 31 | public class ProfileIconActivity extends AppCompatActivity implements View.OnClickListener { 32 | private final String TAG = getClass().getSimpleName(); 33 | 34 | private static final int PICK_FROM_CAMERA = 0; 35 | private static final int PICK_FROM_ALBUM = 1; 36 | private static final int CROP_FROM_CAMERA = 2; 37 | private static final int CROP_FROM_ALBUM = 3; 38 | 39 | Context context; 40 | 41 | ImageView profileIconImage; 42 | 43 | MemberInfoItem memberInfoItem; 44 | 45 | File profileIconFile; 46 | String profileIconFilename; 47 | 48 | /** 49 | * 액티비티를 생성하고 화면을 구성한다. 50 | * @param savedInstanceState 액티비티가 새로 생성되었을 경우, 이전 상태 값을 가지는 객체 51 | */ 52 | @Override 53 | protected void onCreate(Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | setContentView(R.layout.activity_profile_icon); 56 | 57 | context = this; 58 | 59 | memberInfoItem = ((MyApp) getApplication()).getMemberInfoItem(); 60 | 61 | setToolbar(); 62 | setView(); 63 | setProfileIcon(); 64 | } 65 | 66 | /** 67 | * 액티비티 툴바를 설정한다. 68 | */ 69 | private void setToolbar() { 70 | final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 71 | setSupportActionBar(toolbar); 72 | final ActionBar actionBar = getSupportActionBar(); 73 | 74 | if (actionBar != null) { 75 | actionBar.setDisplayHomeAsUpEnabled(true); 76 | actionBar.setTitle(R.string.profile_setting); 77 | } 78 | } 79 | 80 | /** 81 | * 액티비티 화면을 설정한다. 82 | */ 83 | public void setView() { 84 | profileIconImage = (ImageView) findViewById(R.id.profile_icon); 85 | 86 | Button albumButton = (Button) findViewById(R.id.album); 87 | albumButton.setOnClickListener(this); 88 | 89 | Button cameraButton = (Button) findViewById(R.id.camera); 90 | cameraButton.setOnClickListener(this); 91 | } 92 | 93 | /** 94 | * 프로필 아이콘을 설정한다. 95 | */ 96 | private void setProfileIcon() { 97 | MyLog.d(TAG, "onResume " + 98 | RemoteService.MEMBER_ICON_URL + memberInfoItem.memberIconFilename); 99 | 100 | if (StringLib.getInstance().isBlank(memberInfoItem.memberIconFilename)) { 101 | Picasso.with(this).load(R.drawable.ic_person).into(profileIconImage); 102 | } else { 103 | Picasso.with(this) 104 | .load(RemoteService.MEMBER_ICON_URL + memberInfoItem.memberIconFilename) 105 | .into(profileIconImage); 106 | } 107 | } 108 | 109 | /** 110 | * 사용자가 선택한 프로필 아이콘을 저장할 파일 이름을 설정한다. 111 | */ 112 | private void setProfileIconFile() { 113 | profileIconFilename = memberInfoItem.seq + "_" + String.valueOf(System.currentTimeMillis()); 114 | 115 | profileIconFile = FileLib.getInstance().getProfileIconFile(context, profileIconFilename); 116 | } 117 | 118 | /** 119 | * 프로필 아이콘을 설정하기 위해 선택할 수 있는 앨범이나 카메라 버튼의 클릭 이벤트를 처리한다. 120 | * @param v 클릭한 뷰 객체 121 | */ 122 | @Override 123 | public void onClick(View v) { 124 | setProfileIconFile(); 125 | 126 | if (v.getId() == R.id.album) { 127 | getImageFromAlbum(); 128 | 129 | } else if (v.getId() == R.id.camera) { 130 | getImageFromCamera(); 131 | } 132 | } 133 | 134 | /** 135 | * 오른쪽 상단 메뉴를 구성한다. 136 | * 닫기 메뉴만이 설정되어 있는 menu_close.xml를 지정한다. 137 | * @param menu 메뉴 객체 138 | * @return 메뉴를 보여준다면 true, 보여주지 않는다면 false 139 | */ 140 | @Override 141 | public boolean onCreateOptionsMenu(Menu menu) { 142 | getMenuInflater().inflate(R.menu.menu_close, menu); 143 | return true; 144 | } 145 | 146 | /** 147 | * 왼쪽 화살표 메뉴(android.R.id.home)를 클릭했을 때와 148 | * 오른쪽 상단 닫기 메뉴를 클릭했을 때의 동작을 지정한다. 149 | * 여기서는 모든 버튼이 액티비티를 종료한다. 150 | * @param item 메뉴 아이템 객체 151 | * @return 메뉴를 처리했다면 true, 그렇지 않다면 false 152 | */ 153 | @Override 154 | public boolean onOptionsItemSelected(MenuItem item) { 155 | switch (item.getItemId()) { 156 | case android.R.id.home: 157 | finish(); 158 | break; 159 | 160 | case R.id.action_close: 161 | finish(); 162 | break; 163 | } 164 | 165 | return true; 166 | } 167 | 168 | /** 169 | * 카메라 앱을 실행해서 이미지를 촬영한다. 170 | */ 171 | private void getImageFromCamera() { 172 | Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 173 | intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(profileIconFile)); 174 | startActivityForResult(intent, PICK_FROM_CAMERA); 175 | } 176 | 177 | /** 178 | * 카메라 앨범앱을 실행해서 이미지를 선택한다. 179 | */ 180 | private void getImageFromAlbum() { 181 | Intent intent = new Intent(Intent.ACTION_PICK); 182 | intent.setType(MediaStore.Images.Media.CONTENT_TYPE); 183 | startActivityForResult(intent, PICK_FROM_ALBUM); 184 | } 185 | 186 | /** 187 | * 이미지를 자르기 위한 Intent를 생성해서 반환한다. 188 | * @param inputUri 이미지를 자르기전 Uri 189 | * @param outputUri 이미지를 자른 결과 파일 Uri 190 | * @return 이미지를 자르기 위한 인텐트 191 | */ 192 | private Intent getCropIntent(Uri inputUri, Uri outputUri) { 193 | Intent intent = new Intent("com.android.camera.action.CROP"); 194 | intent.setDataAndType(inputUri, "image/*"); 195 | intent.putExtra("aspectX", 1); 196 | intent.putExtra("aspectY", 1); 197 | intent.putExtra("outputX", 200); 198 | intent.putExtra("outputY", 200); 199 | intent.putExtra("scale", true); 200 | intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); 201 | intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.toString()); 202 | 203 | return intent; 204 | } 205 | 206 | /** 207 | * 카메라에서 촬영한 이미지를 프로필 아이콘에 사용할 크기로 자른다. 208 | */ 209 | private void cropImageFromCamera() { 210 | Uri uri = Uri.fromFile(profileIconFile); 211 | Intent intent = getCropIntent(uri, uri); 212 | startActivityForResult(intent, CROP_FROM_CAMERA); 213 | } 214 | 215 | /** 216 | * 카메라 앨범에서 선택한 이미지를 프로필 아이콘에 사용할 크기로 자른다. 217 | */ 218 | private void cropImageFromAlbum(Uri inputUri) { 219 | Uri outputUri = Uri.fromFile(profileIconFile); 220 | 221 | MyLog.d(TAG, "startPickFromAlbum uri " + inputUri.toString()); 222 | Intent intent = getCropIntent(inputUri, outputUri); 223 | startActivityForResult(intent, CROP_FROM_ALBUM); 224 | } 225 | 226 | /** 227 | * startActivityForResult() 메소드로 호출한 액티비티의 결과를 처리한다. 228 | * @param requestCode 액티비티를 실행하면서 전달한 요청 코드 229 | * @param resultCode 실행한 액티비티가 설정한 결과 코드 230 | * @param intent 결과 데이터 231 | */ 232 | @Override 233 | protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 234 | MyLog.d(TAG, "onActivityResult " + intent); 235 | 236 | if (resultCode != RESULT_OK) return; 237 | 238 | if (requestCode == PICK_FROM_CAMERA) { 239 | cropImageFromCamera(); 240 | 241 | } else if (requestCode == CROP_FROM_CAMERA) { 242 | Picasso.with(this).load(profileIconFile).into(profileIconImage); 243 | uploadProfileIcon(); 244 | 245 | } else if (requestCode == PICK_FROM_ALBUM && intent != null) { 246 | Uri dataUri = intent.getData(); 247 | if (dataUri != null) { 248 | cropImageFromAlbum(dataUri); 249 | } 250 | } else if (requestCode == CROP_FROM_ALBUM && intent != null) { 251 | Picasso.with(this).load(profileIconFile).into(profileIconImage); 252 | uploadProfileIcon(); 253 | } 254 | } 255 | 256 | /** 257 | * 프로필 아이콘을 서버에 업로드한다. 258 | */ 259 | private void uploadProfileIcon() { 260 | RemoteLib.getInstance().uploadMemberIcon(memberInfoItem.seq, profileIconFile); 261 | 262 | memberInfoItem.memberIconFilename = profileIconFilename + ".png"; 263 | } 264 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/adapter/InfoListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.adapter; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Message; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import com.mobitant.bestfood.Constant; 14 | import com.mobitant.bestfood.MyApp; 15 | import com.mobitant.bestfood.R; 16 | import com.mobitant.bestfood.item.FoodInfoItem; 17 | import com.mobitant.bestfood.item.MemberInfoItem; 18 | import com.mobitant.bestfood.lib.DialogLib; 19 | import com.mobitant.bestfood.lib.GoLib; 20 | import com.mobitant.bestfood.lib.MyLog; 21 | import com.mobitant.bestfood.lib.StringLib; 22 | import com.mobitant.bestfood.remote.RemoteService; 23 | import com.squareup.picasso.Picasso; 24 | 25 | import java.util.ArrayList; 26 | 27 | /** 28 | * 맛집 정보 리스트의 아이템을 처리하는 어댑터 29 | */ 30 | public class InfoListAdapter extends RecyclerView.Adapter { 31 | private final String TAG = this.getClass().getSimpleName(); 32 | 33 | private Context context; 34 | private int resource; 35 | private ArrayList itemList; 36 | private MemberInfoItem memberInfoItem; 37 | 38 | /** 39 | * 어댑터 생성자 40 | * @param context 컨텍스트 객체 41 | * @param resource 아이템을 보여주기 위해 사용할 리소스 아이디 42 | * @param itemList 아이템 리스트 43 | */ 44 | public InfoListAdapter(Context context, int resource, ArrayList itemList) { 45 | this.context = context; 46 | this.resource = resource; 47 | this.itemList = itemList; 48 | 49 | memberInfoItem = ((MyApp) context.getApplicationContext()).getMemberInfoItem(); 50 | } 51 | 52 | /** 53 | * 특정 아이템의 변경사항을 적용하기 위해 기본 아이템을 새로운 아이템으로 변경한다. 54 | * @param newItem 새로운 아이템 55 | */ 56 | public void setItem(FoodInfoItem newItem) { 57 | for (int i=0; i < itemList.size(); i++) { 58 | FoodInfoItem item = itemList.get(i); 59 | 60 | if (item.seq == newItem.seq) { 61 | itemList.set(i, newItem); 62 | notifyItemChanged(i); 63 | break; 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * 현재 아이템 리스트에 새로운 아이템 리스트를 추가한다. 70 | * @param itemList 새로운 아이템 리스트 71 | */ 72 | public void addItemList(ArrayList itemList) { 73 | this.itemList.addAll(itemList); 74 | notifyDataSetChanged(); 75 | } 76 | 77 | /** 78 | * 즐겨찾기 상태를 변경한다. 79 | * @param seq 맛집 정보 시퀀스 80 | * @param keep 즐겨찾기 추가 유무 81 | */ 82 | private void changeItemKeep(int seq, boolean keep) { 83 | for (int i=0; i < itemList.size(); i++) { 84 | if (itemList.get(i).seq == seq) { 85 | itemList.get(i).isKeep = keep; 86 | notifyItemChanged(i); 87 | break; 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * 아이템 크기를 반환한다. 94 | * @return 아이템 크기 95 | */ 96 | @Override 97 | public int getItemCount() { 98 | return this.itemList.size(); 99 | } 100 | 101 | /** 102 | * 뷰홀더(ViewHolder)를 생성하기 위해 자동으로 호출된다. 103 | * @param parent 부모 뷰그룹 104 | * @param viewType 새로운 뷰의 뷰타입 105 | * @return 뷰홀더 객체 106 | */ 107 | @Override 108 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 109 | View v = LayoutInflater.from(parent.getContext()).inflate(resource, parent, false); 110 | 111 | return new ViewHolder(v); 112 | } 113 | 114 | /** 115 | * 뷰홀더(ViewHolder)와 아이템을 리스트 위치에 따라 연동한다. 116 | * @param holder 뷰홀더 객체 117 | * @param position 리스트 위치 118 | */ 119 | @Override 120 | public void onBindViewHolder(ViewHolder holder, int position) { 121 | final FoodInfoItem item = itemList.get(position); 122 | MyLog.d(TAG, "getView " + item); 123 | 124 | if (item.isKeep) { 125 | holder.keep.setImageResource(R.drawable.ic_keep_on); 126 | } else { 127 | holder.keep.setImageResource(R.drawable.ic_keep_off); 128 | } 129 | 130 | holder.name.setText(item.name); 131 | holder.description.setText(StringLib.getInstance().getSubString(context, 132 | item.description, Constant.MAX_LENGTH_DESCRIPTION)); 133 | 134 | setImage(holder.image, item.imageFilename); 135 | 136 | holder.itemView.setOnClickListener(new View.OnClickListener() { 137 | @Override 138 | public void onClick(View view) { 139 | GoLib.getInstance().goBestFoodInfoActivity(context, item.seq); 140 | } 141 | }); 142 | 143 | holder.keep.setOnClickListener(new View.OnClickListener() { 144 | @Override 145 | public void onClick(View view) { 146 | if (item.isKeep) { 147 | DialogLib.getInstance().showKeepDeleteDialog(context, 148 | keepDeleteHandler, memberInfoItem.seq, item.seq); 149 | } else { 150 | DialogLib.getInstance().showKeepInsertDialog(context, 151 | keepInsertHandler, memberInfoItem.seq, item.seq); 152 | } 153 | } 154 | }); 155 | } 156 | 157 | /** 158 | * 이미지를 설정한다. 159 | * @param imageView 이미지를 설정할 뷰 160 | * @param fileName 이미지 파일이름 161 | */ 162 | private void setImage(ImageView imageView, String fileName) { 163 | if (StringLib.getInstance().isBlank(fileName)) { 164 | Picasso.with(context).load(R.drawable.bg_bestfood_drawer).into(imageView); 165 | } else { 166 | Picasso.with(context).load(RemoteService.IMAGE_URL + fileName).into(imageView); 167 | } 168 | } 169 | 170 | /** 171 | * 즐겨찾기 추가가 성공한 경우를 처리하는 핸들러 172 | */ 173 | Handler keepInsertHandler = new Handler() { 174 | @Override 175 | public void handleMessage(Message msg) { 176 | super.handleMessage(msg); 177 | 178 | changeItemKeep(msg.what, true); 179 | } 180 | }; 181 | 182 | /** 183 | * 즐겨찾기 삭제가 성공한 경우를 처리하는 핸들러 184 | */ 185 | Handler keepDeleteHandler = new Handler() { 186 | @Override 187 | public void handleMessage(Message msg) { 188 | super.handleMessage(msg); 189 | 190 | changeItemKeep(msg.what, false); 191 | } 192 | }; 193 | 194 | /** 195 | * 아이템을 보여주기 위한 뷰홀더 클래스 196 | */ 197 | public class ViewHolder extends RecyclerView.ViewHolder { 198 | ImageView image; 199 | ImageView keep; 200 | TextView name; 201 | TextView description; 202 | 203 | public ViewHolder(View itemView) { 204 | super(itemView); 205 | 206 | image = (ImageView) itemView.findViewById(R.id.image); 207 | keep = (ImageView) itemView.findViewById(R.id.keep); 208 | name = (TextView) itemView.findViewById(R.id.name); 209 | description = (TextView) itemView.findViewById(R.id.description); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/adapter/KeepListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.adapter; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Message; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import com.mobitant.bestfood.Constant; 14 | import com.mobitant.bestfood.R; 15 | import com.mobitant.bestfood.item.FoodInfoItem; 16 | import com.mobitant.bestfood.item.KeepItem; 17 | import com.mobitant.bestfood.lib.DialogLib; 18 | import com.mobitant.bestfood.lib.GoLib; 19 | import com.mobitant.bestfood.lib.MyLog; 20 | import com.mobitant.bestfood.lib.StringLib; 21 | import com.mobitant.bestfood.remote.RemoteService; 22 | import com.squareup.picasso.Picasso; 23 | 24 | import java.util.ArrayList; 25 | 26 | /** 27 | * 맛집 정보 즐겨찾기 리스트의 아이템을 처리하는 어댑터 28 | */ 29 | public class KeepListAdapter extends RecyclerView.Adapter { 30 | private final String TAG = this.getClass().getSimpleName(); 31 | 32 | private Context context; 33 | private int resource; 34 | private ArrayList itemList; 35 | private int memberSeq; 36 | 37 | /** 38 | * 어댑터 생성자 39 | * @param context 컨텍스트 객체 40 | * @param resource 아이템을 보여주기 위해 사용할 리소스 아이디 41 | * @param itemList 아이템 리스트 42 | */ 43 | public KeepListAdapter(Context context, int resource, ArrayList itemList, int memberSeq) { 44 | this.context = context; 45 | this.resource = resource; 46 | this.itemList = itemList; 47 | this.memberSeq = memberSeq; 48 | } 49 | 50 | /** 51 | * 새로운 아이템 리스트를 설정한다. 52 | * @param itemList 새로운 아이템 리스트 53 | */ 54 | public void setItemList(ArrayList itemList) { 55 | this.itemList = itemList; 56 | notifyDataSetChanged(); 57 | } 58 | 59 | /** 60 | * 특정 아이템의 변경사항을 적용하기 위해 기본 아이템을 새로운 아이템으로 변경한다. 61 | * @param newItem 새로운 아이템 62 | */ 63 | public void setItem(FoodInfoItem newItem) { 64 | for (int i=0; i < itemList.size(); i++) { 65 | KeepItem item = itemList.get(i); 66 | 67 | if (item.seq == newItem.seq && !newItem.isKeep) { 68 | itemList.remove(i); 69 | notifyItemChanged(i); 70 | break; 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * 맛집 정보 시퀀스와 일치하는 아이템을 즐겨찾기 리스트에서 삭제한다. 77 | * @param seq 78 | */ 79 | private void removeItem(int seq) { 80 | for (int i=0; i < itemList.size(); i++) { 81 | if (itemList.get(i).seq == seq) { 82 | itemList.remove(i); 83 | notifyItemChanged(i); 84 | break; 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * 아이템 크기를 반환한다. 91 | * @return 아이템 크기 92 | */ 93 | @Override 94 | public int getItemCount() { 95 | return this.itemList.size(); 96 | } 97 | 98 | /** 99 | * 뷰홀더(ViewHolder)를 생성하기 위해 자동으로 호출된다. 100 | * @param parent 부모 뷰그룹 101 | * @param viewType 새로운 뷰의 뷰타입 102 | * @return 뷰홀더 객체 103 | */ 104 | @Override 105 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 106 | View v = LayoutInflater.from(parent.getContext()).inflate(resource, parent, false); 107 | 108 | return new ViewHolder(v); 109 | } 110 | 111 | /** 112 | * 뷰홀더(ViewHolder)와 아이템을 리스트 위치에 따라 연동한다. 113 | * @param holder 뷰홀더 객체 114 | * @param position 리스트 위치 115 | */ 116 | @Override 117 | public void onBindViewHolder(ViewHolder holder, int position) { 118 | final KeepItem item = itemList.get(position); 119 | MyLog.d(TAG, "getView " + item); 120 | 121 | if (item.isKeep) { 122 | holder.keep.setImageResource(R.drawable.ic_keep_on); 123 | } else { 124 | holder.keep.setImageResource(R.drawable.ic_keep_off); 125 | } 126 | 127 | holder.name.setText(item.name); 128 | holder.description.setText( 129 | StringLib.getInstance().getSubString(context, 130 | item.description, Constant.MAX_LENGTH_DESCRIPTION)); 131 | 132 | setImage(holder.image, item.imageFilename); 133 | 134 | holder.itemView.setOnClickListener(new View.OnClickListener() { 135 | @Override 136 | public void onClick(View view) { 137 | GoLib.getInstance().goBestFoodInfoActivity(context, item.seq); 138 | } 139 | }); 140 | 141 | holder.keep.setOnClickListener(new View.OnClickListener() { 142 | @Override 143 | public void onClick(View view) { 144 | DialogLib.getInstance().showKeepDeleteDialog(context, keepHandler, memberSeq, item.seq); 145 | } 146 | }); 147 | } 148 | /** 149 | * 이미지를 설정한다. 150 | * @param imageView 이미지를 설정할 뷰 151 | * @param fileName 이미지 파일이름 152 | */ 153 | private void setImage(ImageView imageView, String fileName) { 154 | MyLog.d(TAG, "setImage fileName " + fileName); 155 | 156 | if (StringLib.getInstance().isBlank(fileName)) { 157 | Picasso.with(context).load(R.drawable.bg_bestfood_drawer).into(imageView); 158 | } else { 159 | Picasso.with(context).load(RemoteService.IMAGE_URL + fileName).into(imageView); 160 | } 161 | } 162 | 163 | /** 164 | * 즐겨찾기 리스트에서 해당 아이템을 삭제하기 위한 핸들러 165 | */ 166 | Handler keepHandler = new Handler() { 167 | @Override 168 | public void handleMessage(Message msg) { 169 | super.handleMessage(msg); 170 | 171 | removeItem(msg.what); 172 | } 173 | }; 174 | 175 | /** 176 | * 아이템을 보여주기 위한 뷰홀더 클래스 177 | */ 178 | public class ViewHolder extends RecyclerView.ViewHolder { 179 | ImageView image; 180 | ImageView keep; 181 | TextView name; 182 | TextView description; 183 | 184 | public ViewHolder(View itemView) { 185 | super(itemView); 186 | 187 | image = (ImageView) itemView.findViewById(R.id.image); 188 | keep = (ImageView) itemView.findViewById(R.id.keep); 189 | name = (TextView) itemView.findViewById(R.id.name); 190 | description = (TextView) itemView.findViewById(R.id.description); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/adapter/MapListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import com.mobitant.bestfood.Constant; 12 | import com.mobitant.bestfood.R; 13 | import com.mobitant.bestfood.item.FoodInfoItem; 14 | import com.mobitant.bestfood.lib.GoLib; 15 | import com.mobitant.bestfood.lib.MyLog; 16 | import com.mobitant.bestfood.lib.StringLib; 17 | import com.mobitant.bestfood.remote.RemoteService; 18 | import com.squareup.picasso.Picasso; 19 | 20 | import java.util.ArrayList; 21 | 22 | /** 23 | * 구글 지도 맵에서 맛집 정보 리스트의 아이템을 처리하는 어댑터 24 | */ 25 | public class MapListAdapter extends RecyclerView.Adapter { 26 | private final String TAG = this.getClass().getSimpleName(); 27 | 28 | private Context context; 29 | private int resource; 30 | private ArrayList itemList; 31 | 32 | /** 33 | * 어댑터 생성자 34 | * @param context 컨텍스트 객체 35 | * @param resource 아이템을 보여주기 위해 사용할 리소스 아이디 36 | * @param itemList 아이템 리스트 37 | */ 38 | public MapListAdapter(Context context, int resource, ArrayList itemList) { 39 | this.context = context; 40 | this.resource = resource; 41 | this.itemList = itemList; 42 | } 43 | 44 | /** 45 | * 새로운 아이템 리스트를 설정한다. 46 | * @param itemList 새로운 아이템 리스트 47 | */ 48 | public void setItemList(ArrayList itemList) { 49 | this.itemList = itemList; 50 | notifyDataSetChanged(); 51 | } 52 | 53 | /** 54 | * 아이템 크기를 반환한다. 55 | * @return 아이템 크기 56 | */ 57 | @Override 58 | public int getItemCount() { 59 | return this.itemList.size(); 60 | } 61 | 62 | /** 63 | * 뷰홀더(ViewHolder)를 생성하기 위해 자동으로 호출된다. 64 | * @param parent 부모 뷰그룹 65 | * @param viewType 새로운 뷰의 뷰타입 66 | * @return 뷰홀더 객체 67 | */ 68 | @Override 69 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 70 | View v = LayoutInflater.from(parent.getContext()).inflate(resource, parent, false); 71 | 72 | return new ViewHolder(v); 73 | } 74 | 75 | /** 76 | * 뷰홀더(ViewHolder)와 아이템을 리스트 위치에 따라 연동한다. 77 | * @param holder 뷰홀더 객체 78 | * @param position 리스트 위치 79 | */ 80 | @Override 81 | public void onBindViewHolder(ViewHolder holder, int position) { 82 | final FoodInfoItem item = itemList.get(position); 83 | 84 | int meter = (int) item.userDistanceMeter; 85 | 86 | if (meter == 0) { 87 | holder.distanceMeter.setText(""); 88 | } else if (meter < 1000) { 89 | holder.distanceMeter.setText(meter + context.getResources().getString(R.string.unit_m)); 90 | } else { 91 | holder.distanceMeter.setText( (meter / 1000) 92 | + context.getResources().getString(R.string.unit_km)); 93 | } 94 | 95 | holder.name.setText(item.name); 96 | holder.description.setText(StringLib.getInstance().getSubString(context, 97 | item.description, Constant.MAX_LENGTH_DESCRIPTION)); 98 | 99 | setImage(holder.imageView, item.imageFilename); 100 | 101 | holder.itemView.setOnClickListener(new View.OnClickListener() { 102 | @Override 103 | public void onClick(View view) { 104 | GoLib.getInstance().goBestFoodInfoActivity(context, item.seq); 105 | } 106 | }); 107 | } 108 | 109 | /** 110 | * 이미지를 설정한다. 111 | * @param imageView 이미지를 설정할 뷰 112 | * @param fileName 이미지 파일이름 113 | */ 114 | private void setImage(ImageView imageView, String fileName) { 115 | MyLog.d(TAG, "setImage fileName " + fileName); 116 | 117 | if (StringLib.getInstance().isBlank(fileName)) { 118 | Picasso.with(context).load(R.drawable.bg_bestfood_drawer).into(imageView); 119 | } else { 120 | Picasso.with(context).load(RemoteService.IMAGE_URL + fileName).into(imageView); 121 | } 122 | } 123 | 124 | /** 125 | * 아이템을 보여주기 위한 뷰홀더 클래스 126 | */ 127 | public class ViewHolder extends RecyclerView.ViewHolder { 128 | ImageView imageView; 129 | TextView distanceMeter; 130 | TextView name; 131 | TextView description; 132 | 133 | public ViewHolder(View itemView) { 134 | super(itemView); 135 | 136 | imageView = (ImageView) itemView.findViewById(R.id.image); 137 | distanceMeter = (TextView) itemView.findViewById(R.id.distance_meter); 138 | name = (TextView) itemView.findViewById(R.id.name); 139 | description = (TextView) itemView.findViewById(R.id.description); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/custom/EndlessRecyclerViewScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.custom; 2 | 3 | import android.support.v7.widget.GridLayoutManager; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.StaggeredGridLayoutManager; 7 | 8 | /** 9 | * 리사이클러뷰에서 스크롤 할 경우, 데이터를 추가적으로 가지고 올 수 있도록 해주는 추상 클래스 10 | */ 11 | public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener { 12 | //새로운 아이템을 로드하기 위한 현재 스크롤 위치 하단의 아이템 개수 13 | private int visibleThreshold = 5; 14 | // 로딩할 페이지 인덱스 15 | private int currentPage = 0; 16 | // 최근 로딩 후의 전체 아이템 개수 17 | private int previousTotalItemCount = 0; 18 | // 로딩하고 있는 중인지에 대한 상태 19 | private boolean loading = true; 20 | // 시작 페이지 인덱스 21 | private int startingPageIndex = 0; 22 | 23 | RecyclerView.LayoutManager layoutManager; 24 | 25 | /** 26 | * LinearLayoutManager를 위한 생성자 27 | * @param layoutManager LinearLayoutManager 28 | */ 29 | public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) { 30 | this.layoutManager = layoutManager; 31 | } 32 | 33 | /** 34 | * GridLayoutManager를 위한 생성자 35 | * @param layoutManager GridLayoutManager 36 | */ 37 | public EndlessRecyclerViewScrollListener(GridLayoutManager layoutManager) { 38 | this.layoutManager = layoutManager; 39 | visibleThreshold = visibleThreshold * layoutManager.getSpanCount(); 40 | } 41 | 42 | /** 43 | * StaggeredGridLayoutManager를 위한 생성자 44 | * @param layoutManager StaggeredGridLayoutManager 45 | */ 46 | public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager) { 47 | this.layoutManager = layoutManager; 48 | visibleThreshold = visibleThreshold * layoutManager.getSpanCount(); 49 | } 50 | 51 | /** 52 | * 현재 화면의 여러 스팬 중에서 가장 마지막에 보이는 아이템의 위치를 반환한다. 53 | * @param lastVisibleItemPositions 마지막에 보이는 아이템의 포지션 값들 54 | * @return 가장 마지막 아이템 위치 55 | */ 56 | public int getLastVisibleItemPosition(int[] lastVisibleItemPositions) { 57 | int maxSize = 0; 58 | for (int i = 0; i < lastVisibleItemPositions.length; i++) { 59 | if (i == 0) { 60 | maxSize = lastVisibleItemPositions[i]; 61 | } 62 | else if (lastVisibleItemPositions[i] > maxSize) { 63 | maxSize = lastVisibleItemPositions[i]; 64 | } 65 | } 66 | return maxSize; 67 | } 68 | 69 | /** 70 | * 스크롤된 후에 호출되는 컬백 메소드이며 아이템을 추가로 로딩해야 하는지를 체크한다. 71 | * @param view 리사이클러뷰 72 | * @param dx 수평으로 스크롤된 양 73 | * @param dy 수직으로 스크롤된 양 74 | */ 75 | @Override 76 | public void onScrolled(RecyclerView view, int dx, int dy) { 77 | int lastVisibleItemPosition = 0; 78 | int totalItemCount = layoutManager.getItemCount(); 79 | 80 | if (layoutManager instanceof StaggeredGridLayoutManager) { 81 | int[] lastVisibleItemPositions = 82 | ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(null); 83 | lastVisibleItemPosition = getLastVisibleItemPosition(lastVisibleItemPositions); 84 | } else if (layoutManager instanceof GridLayoutManager) { 85 | lastVisibleItemPosition = 86 | ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); 87 | } else if (layoutManager instanceof LinearLayoutManager) { 88 | lastVisibleItemPosition = 89 | ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); 90 | } 91 | 92 | // 새로 로딩한 전체 아이템 개수가 이전에 설정된 전체 아이템 개수보다 작을 경우 93 | // 상태를 초기화한다. 이런 경우는 리사이클러뷰의 데이터가 초기화된 경우에 발생한다. 94 | if (totalItemCount < previousTotalItemCount) { 95 | this.currentPage = this.startingPageIndex; 96 | this.previousTotalItemCount = totalItemCount; 97 | if (totalItemCount == 0) { 98 | this.loading = true; 99 | } 100 | } 101 | 102 | // 현재 로딩중인 상태이고 새로 로딩한 전체 아이템 개수가 이전에 저장한 전체 아이템 개수보다 103 | // 크다면 로딩이 완료한 것으로 간주한다. 104 | if (loading && (totalItemCount > previousTotalItemCount)) { 105 | loading = false; 106 | previousTotalItemCount = totalItemCount; 107 | } 108 | 109 | // 로딩중이 아니고 화면에 보이는 마지막 아이템 위치에 visibleThreshold을 더한 값이 110 | // totalItemCount 보다 큰 경우 새로 로딩할 아이템이 있는 것으로 간주한다. 111 | // 이때 onLoadMore() 메소드가 호출된다. 112 | if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) { 113 | currentPage++; 114 | onLoadMore(currentPage, totalItemCount, view); 115 | loading = true; 116 | } 117 | } 118 | 119 | /** 120 | * 아이템을 더 로딩하기 위한 메소드로서 해당 메소드를 직접 작성해야 한다. 121 | * @param page 로딩할 페이지 122 | * @param totalItemsCount 전체 아이템 개수 123 | * @param view 리사이클러뷰 124 | */ 125 | public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view); 126 | 127 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/custom/WorkaroundMapFragment.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.custom; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.v4.content.ContextCompat; 6 | import android.view.LayoutInflater; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.FrameLayout; 11 | 12 | import com.google.android.gms.maps.SupportMapFragment; 13 | 14 | /** 15 | * 스크롤 내부의 구글맵의 스크롤을 정상적으로 동작시키기 위한 커스텀 클래스 16 | */ 17 | public class WorkaroundMapFragment extends SupportMapFragment { 18 | private OnTouchListener mListener; 19 | 20 | /** 21 | * 뷰를 생성해서 반환한다. 22 | * @param layoutInflater 레이아웃 인플레이터 객체 23 | * @param viewGroup 뷰그룹 24 | * @param savedInstance 번들 객체 25 | * @return 생성한 뷰 객체 26 | */ 27 | @Override 28 | public View onCreateView(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle savedInstance) { 29 | View layout = super.onCreateView(layoutInflater, viewGroup, savedInstance); 30 | 31 | TouchableWrapper frameLayout = new TouchableWrapper(getActivity()); 32 | 33 | int bgColor = ContextCompat.getColor(getActivity(), android.R.color.transparent); 34 | 35 | frameLayout.setBackgroundColor(bgColor); 36 | 37 | ((ViewGroup) layout).addView(frameLayout, 38 | new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 39 | ViewGroup.LayoutParams.MATCH_PARENT)); 40 | 41 | return layout; 42 | } 43 | 44 | /** 45 | * 터치 리스너를 설정한다. 46 | * @param listener 47 | */ 48 | public void setListener(OnTouchListener listener) { 49 | mListener = listener; 50 | } 51 | 52 | /** 53 | * 터치 리스너 인터페이스 54 | */ 55 | public interface OnTouchListener { 56 | void onTouch(); 57 | } 58 | 59 | /** 60 | * 터치 가능한 영역을 처리하기 위한 래퍼 클래스 61 | */ 62 | public class TouchableWrapper extends FrameLayout { 63 | 64 | public TouchableWrapper(Context context) { 65 | super(context); 66 | } 67 | 68 | @Override 69 | public boolean dispatchTouchEvent(MotionEvent event) { 70 | switch (event.getAction()) { 71 | case MotionEvent.ACTION_DOWN: 72 | mListener.onTouch(); 73 | break; 74 | case MotionEvent.ACTION_UP: 75 | mListener.onTouch(); 76 | break; 77 | } 78 | return super.dispatchTouchEvent(event); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/item/FoodInfoItem.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.item; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * 맛집 정보를 저장하는 객체 7 | */ 8 | @org.parceler.Parcel 9 | public class FoodInfoItem { 10 | public int seq; 11 | @SerializedName("member_seq") public int memberSeq; 12 | public String name; 13 | public String tel; 14 | public String address; 15 | public double latitude; 16 | public double longitude; 17 | public String description; 18 | @SerializedName("reg_date") public String regDate; 19 | @SerializedName("mod_date") public String modDate; 20 | @SerializedName("user_distance_meter") public double userDistanceMeter; 21 | @SerializedName("is_keep") public boolean isKeep; 22 | @SerializedName("image_filename") public String imageFilename; 23 | 24 | @Override 25 | public String toString() { 26 | return "FoodInfoItem{" + 27 | "seq=" + seq + 28 | ", memberSeq=" + memberSeq + 29 | ", name='" + name + '\'' + 30 | ", tel='" + tel + '\'' + 31 | ", address='" + address + '\'' + 32 | ", latitude=" + latitude + 33 | ", longitude=" + longitude + 34 | ", description='" + description + '\'' + 35 | ", regDate='" + regDate + '\'' + 36 | ", modDate='" + modDate + '\'' + 37 | ", userDistanceMeter=" + userDistanceMeter + 38 | ", isKeep=" + isKeep + 39 | ", imageFilename='" + imageFilename + '\'' + 40 | '}'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/item/GeoItem.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.item; 2 | 3 | import com.google.android.gms.maps.model.LatLng; 4 | 5 | /** 6 | * 위치 정보를 저장하는 객체 7 | */ 8 | public class GeoItem { 9 | public static double knownLatitude; 10 | public static double knownLongitude; 11 | 12 | /** 13 | * 사용자의 위도, 경도 객체를 반환한다. 만약 사용자의 위치를 알 수 없다면 서울 위치를 반환한다. 14 | * @return LatLng 위도,경도 객체 15 | */ 16 | public static LatLng getKnownLocation() { 17 | if (knownLatitude == 0 || knownLongitude == 0) { 18 | return new LatLng(37.566229, 126.977689); 19 | } else { 20 | return new LatLng(knownLatitude, knownLongitude); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/item/ImageItem.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.item; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * 맛집 이미지 정보를 저장하는 객체 7 | */ 8 | public class ImageItem { 9 | @SerializedName("seq") public int seq; 10 | @SerializedName("info_seq") public int infoSeq; 11 | @SerializedName("file_name") public String fileName; 12 | @SerializedName("image_memo") public String imageMemo; 13 | @SerializedName("reg_date") public String regDate; 14 | 15 | @Override 16 | public String toString() { 17 | return "ImageItem{" + 18 | "seq=" + seq + 19 | ", infoSeq=" + infoSeq + 20 | ", fileName='" + fileName + '\'' + 21 | ", imageMemo='" + imageMemo + '\'' + 22 | ", regDate='" + regDate + '\'' + 23 | '}'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/item/KeepItem.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.item; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * 즐겨찾기 정보를 저장하는 객체 7 | */ 8 | public class KeepItem extends FoodInfoItem{ 9 | @SerializedName("keep_seq") public String keepSeq; 10 | @SerializedName("keep_member_seq") public String keepMemberSeq; 11 | @SerializedName("keep_date") public String keepDate; 12 | 13 | @Override 14 | public String toString() { 15 | return "KeepItem{" + 16 | "keepSeq='" + keepSeq + '\'' + 17 | ", keepMemberSeq='" + keepMemberSeq + '\'' + 18 | ", keepDate='" + keepDate + '\'' + 19 | '}'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/item/MemberInfoItem.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.item; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * 사용자 정보를 저장하는 객체 7 | */ 8 | public class MemberInfoItem { 9 | public int seq; 10 | public String phone; 11 | public String name; 12 | public String sextype; 13 | public String birthday; 14 | @SerializedName("member_icon_filename") public String memberIconFilename; 15 | @SerializedName("reg_date") public String regDate; 16 | 17 | @Override 18 | public String toString() { 19 | return "MemberInfoItem{" + 20 | "seq=" + seq + 21 | ", phone='" + phone + '\'' + 22 | ", name='" + name + '\'' + 23 | ", sextype='" + sextype + '\'' + 24 | ", birthday='" + birthday + '\'' + 25 | ", memberIconFilename='" + memberIconFilename + '\'' + 26 | ", regDate='" + regDate + '\'' + 27 | '}'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/BitmapLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.graphics.Bitmap; 4 | import android.os.Handler; 5 | 6 | import java.io.File; 7 | import java.io.FileOutputStream; 8 | 9 | /** 10 | * 비트맵 관련 라이브러리 11 | */ 12 | public class BitmapLib { 13 | public static final String TAG = BitmapLib.class.getSimpleName(); 14 | private volatile static BitmapLib instance; 15 | 16 | public static BitmapLib getInstance() { 17 | if (instance == null) { 18 | synchronized (BitmapLib.class) { 19 | if (instance == null) { 20 | instance = new BitmapLib(); 21 | } 22 | } 23 | } 24 | return instance; 25 | } 26 | 27 | /** 28 | * 비트맵을 별도 스레드에서 파일로 저장한다. 29 | * @param handler 결과를 알려줄 핸들러 30 | * @param file 파일 객체 31 | * @param bitmap 비트맵 객체 32 | */ 33 | public void saveBitmapToFileThread(final Handler handler, final File file, 34 | final Bitmap bitmap) { 35 | new Thread() { 36 | @Override 37 | public void run() { 38 | saveBitmapToFile(file, bitmap); 39 | handler.sendEmptyMessage(0); 40 | } 41 | }.start(); 42 | } 43 | 44 | /** 45 | * 비트맵을 파일에 저장한다. 46 | * @param file 파일 객체 47 | * @param bitmap 비트맵 객체 48 | * @return 파일 저장 성공 여부 49 | */ 50 | private boolean saveBitmapToFile(File file, Bitmap bitmap) { 51 | if (bitmap == null) return false; 52 | 53 | boolean save = false; 54 | 55 | FileOutputStream out = null; 56 | try { 57 | out = new FileOutputStream(file); 58 | 59 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, out); 60 | save = true; 61 | } catch (Exception e) { 62 | save = false; 63 | } finally { 64 | try { 65 | out.close(); 66 | } catch (Exception e) { 67 | } 68 | } 69 | return save; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/DialogLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.os.Handler; 7 | 8 | import com.mobitant.bestfood.R; 9 | 10 | /** 11 | * 다이얼로그와 관련된 메소드로 구성된 라이브러리 12 | */ 13 | public class DialogLib { 14 | public final String TAG = DialogLib.class.getSimpleName(); 15 | private volatile static DialogLib instance; 16 | 17 | public static DialogLib getInstance() { 18 | if (instance == null) { 19 | synchronized (DialogLib.class) { 20 | if (instance == null) { 21 | instance = new DialogLib(); 22 | } 23 | } 24 | } 25 | return instance; 26 | } 27 | 28 | /** 29 | * 즐겨찾기 추가 다이얼로그 화면을 보여준다. 30 | * @param context 컨텍스트 객체 31 | * @param handler 핸들러 객체 32 | * @param memberSeq 사용자 일련번호 33 | * @param infoSeq 맛집 정보 일련번호 34 | */ 35 | public void showKeepInsertDialog(Context context, final Handler handler, 36 | final int memberSeq, final int infoSeq) { 37 | new AlertDialog.Builder(context) 38 | .setTitle(R.string.keep_insert) 39 | .setMessage(R.string.keep_insert_message) 40 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 41 | @Override 42 | public void onClick(DialogInterface dialog, int which) { 43 | 44 | } 45 | }) 46 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 47 | @Override 48 | public void onClick(DialogInterface dialog, int which) { 49 | KeepLib.getInstance().insertKeep(handler, memberSeq, infoSeq); 50 | } 51 | }) 52 | .show(); 53 | } 54 | 55 | /** 56 | * 즐겨찾기 삭제 다이얼로그 화면을 보여준다. 57 | * @param context 컨텍스트 객체 58 | * @param handler 핸들러 객체 59 | * @param memberSeq 사용자 일련번호 60 | * @param infoSeq 맛집 정보 일련번호 61 | */ 62 | public void showKeepDeleteDialog(Context context, final Handler handler, 63 | final int memberSeq, final int infoSeq) { 64 | new AlertDialog.Builder(context) 65 | .setTitle(R.string.keep_delete) 66 | .setMessage(R.string.keep_delete_message) 67 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 68 | @Override 69 | public void onClick(DialogInterface dialog, int which) { 70 | 71 | } 72 | }) 73 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 74 | @Override 75 | public void onClick(DialogInterface dialog, int which) { 76 | KeepLib.getInstance().deleteKeep(handler, memberSeq, infoSeq); 77 | } 78 | }) 79 | .show(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/EtcLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.provider.Settings; 6 | import android.telephony.TelephonyManager; 7 | 8 | import java.util.Locale; 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * 기타 라이브러리 13 | */ 14 | public class EtcLib { 15 | public final String TAG = EtcLib.class.getSimpleName(); 16 | private volatile static EtcLib instance; 17 | 18 | public static EtcLib getInstance() { 19 | if (instance == null) { 20 | synchronized (EtcLib.class) { 21 | if (instance == null) { 22 | instance = new EtcLib(); 23 | } 24 | } 25 | } 26 | return instance; 27 | } 28 | 29 | /** 30 | * 현재 기기의 전화 번호를 반환한다(+82가 붙은 전화번호로 반환). 31 | * @param context 컨텍스트 객체 32 | * @return 전화번호 문자열 33 | */ 34 | public String getPhoneNumber(Context context) { 35 | 36 | TelephonyManager tm = 37 | (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 38 | String number = tm.getLine1Number(); 39 | 40 | if (number != null && !number.equals("") && number.length() >= 8) { 41 | if (Locale.getDefault().getCountry().equals("KR")) { 42 | if (number.startsWith("82")) { 43 | number = "+" + number; 44 | } 45 | 46 | if (number.startsWith("0")) { 47 | number = "+82" + number.substring(1, number.length()); 48 | } 49 | } 50 | 51 | MyLog.d(TAG, "number " + number); 52 | 53 | } else { 54 | number = getDeviceId(context); 55 | } 56 | 57 | return number; 58 | } 59 | 60 | /** 61 | * 전화번호가 없을 경우 기기 아이디를 반환한다. 62 | * @param context 컨텍스트 객체 63 | * @return 기기 아이디 문자열 64 | */ 65 | private String getDeviceId(Context context) { 66 | TelephonyManager tm = 67 | (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 68 | 69 | String tmDevice = tm.getDeviceId(); 70 | String androidId = 71 | Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 72 | String serial = null; 73 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO) serial = Build.SERIAL; 74 | 75 | if (tmDevice != null) return "01" + tmDevice; 76 | if (androidId != null) return "02" + androidId; 77 | if (serial != null) return "03" + serial; 78 | 79 | return null; 80 | } 81 | 82 | 83 | /** 84 | * 전화번호가 유효한 자리수를 가지고 있는지를 체크한다. 85 | * @param number 전화번호 문자열 86 | * @return 유효한 전화번호일 경우 true, 그렇지 않으면 false 87 | */ 88 | public boolean isValidPhoneNumber(String number) { 89 | if (number == null) { 90 | return false; 91 | } else { 92 | if (Pattern.matches("\\d{2}-\\d{3}-\\d{4}", number) 93 | || Pattern.matches("\\d{3}-\\d{3}-\\d{4}", number) 94 | || Pattern.matches("\\d{3}-\\d{4}-\\d{4}", number) 95 | || Pattern.matches("\\d{10}", number) 96 | || Pattern.matches("\\d{11}", number) ) { 97 | return true; 98 | } else { 99 | return false; 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * 전화번호에 '-'를 붙여서 반환한다. 106 | * @param number 전화번호 문자열 107 | * @return 변경된 전화번호 문자열 108 | */ 109 | public String getPhoneNumberText(String number) { 110 | String phoneText = ""; 111 | 112 | if (StringLib.getInstance().isBlank(number)) { 113 | return phoneText; 114 | } 115 | 116 | number = number.replace("-", ""); 117 | 118 | int length = number.length(); 119 | 120 | if (number.length() >= 10) { 121 | phoneText = number.substring(0, 3) + "-" 122 | + number.substring(3, length-4) + "-" 123 | + number.substring(length-4, length); 124 | } 125 | 126 | return phoneText; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/FileLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | 6 | import java.io.File; 7 | 8 | /** 9 | * 파일과 관련된 라이브러리 10 | */ 11 | public class FileLib { 12 | public static final String TAG = FileLib.class.getSimpleName(); 13 | private volatile static FileLib instance; 14 | 15 | public static FileLib getInstance() { 16 | if (instance == null) { 17 | synchronized (FileLib.class) { 18 | if (instance == null) { 19 | instance = new FileLib(); 20 | } 21 | } 22 | } 23 | return instance; 24 | } 25 | 26 | /** 27 | * 파일을 저장할 수 있는 디렉토리 객체를 반환한다. 28 | * @param context 컨텍스트 객체 29 | * @return 파일 객체 30 | */ 31 | private File getFileDir(Context context) { 32 | String state = Environment.getExternalStorageState(); 33 | File filesDir; 34 | 35 | if (Environment.MEDIA_MOUNTED.equals(state)) { 36 | filesDir = context.getExternalFilesDir(null); 37 | } else { 38 | filesDir = context.getFilesDir(); 39 | } 40 | 41 | return filesDir; 42 | } 43 | 44 | /** 45 | * 프로필 아이콘 파일을 저장할 파일 객체를 반환한다. 46 | * @param context 컨텍스트 객체 47 | * @param name 파일 이름 48 | * @return 파일 객체 49 | */ 50 | public File getProfileIconFile(Context context, String name) { 51 | return new File(FileLib.getInstance().getFileDir(context), name + ".png"); 52 | } 53 | 54 | /** 55 | * 이미지 파일 객체를 반환한다. 56 | * @param context 컨텍스트 객체 57 | * @param name 파일 이름 58 | * @return 파일 객체 59 | */ 60 | public File getImageFile(Context context, String name) { 61 | return new File(FileLib.getInstance().getFileDir(context), name + ".png"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/GeoLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.Manifest; 4 | import android.content.Context; 5 | import android.content.pm.PackageManager; 6 | import android.location.Address; 7 | import android.location.Geocoder; 8 | import android.location.Location; 9 | import android.location.LocationManager; 10 | import android.support.v4.content.ContextCompat; 11 | 12 | import com.google.android.gms.maps.GoogleMap; 13 | import com.google.android.gms.maps.model.LatLng; 14 | import com.google.android.gms.maps.model.VisibleRegion; 15 | import com.mobitant.bestfood.item.GeoItem; 16 | 17 | import java.util.List; 18 | import java.util.Locale; 19 | 20 | /** 21 | * 위치 정보와 관련된 라이브러리 22 | */ 23 | public class GeoLib { 24 | public final String TAG = GeoLib.class.getSimpleName(); 25 | private volatile static GeoLib instance; 26 | 27 | public static GeoLib getInstance() { 28 | if (instance == null) { 29 | synchronized (GeoLib.class) { 30 | if (instance == null) { 31 | instance = new GeoLib(); 32 | } 33 | } 34 | } 35 | return instance; 36 | } 37 | 38 | /** 39 | * 사용자의 현재 위도, 경도를 반환한다. 40 | * 실제로는 최근 측정된 위치 정보이다. 41 | * @param context 컨텍스트 객체 42 | */ 43 | public void setLastKnownLocation(Context context) { 44 | LocationManager locationManager 45 | = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); 46 | Location location = null; 47 | 48 | int result = ContextCompat.checkSelfPermission(context, 49 | Manifest.permission.ACCESS_FINE_LOCATION); 50 | if (result == PackageManager.PERMISSION_GRANTED) { 51 | location = locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER); 52 | } 53 | 54 | if (location != null) { 55 | GeoItem.knownLatitude = location.getLatitude(); 56 | GeoItem.knownLongitude = location.getLongitude(); 57 | } else { 58 | //서울 설정 59 | GeoItem.knownLatitude = 37.566229; 60 | GeoItem.knownLongitude = 126.977689; 61 | } 62 | } 63 | 64 | /** 65 | * 지정된 위도경도 객체에 해당하는 주소 문자열을 반환한다. 66 | * @param context 컨텍스트 객체 67 | * @param latLng 위도, 경도 객체 68 | * @return Address 주소 객체 69 | */ 70 | public Address getAddressString(Context context, LatLng latLng) { 71 | Geocoder geocoder = new Geocoder(context, Locale.getDefault()); 72 | 73 | List
list = null; 74 | 75 | try { 76 | list = geocoder.getFromLocation(latLng.latitude, latLng.longitude, 1); 77 | } catch (Exception e) { 78 | e.printStackTrace(); 79 | } 80 | 81 | if (list != null && list.size() > 0) { 82 | return list.get(0); 83 | } else { 84 | return null; 85 | } 86 | } 87 | 88 | /** 89 | * Address 객체로부터 주소 문자열을 추출하여 반환한다. 90 | * @param address 주소 객체 91 | * @return 주소 문자열 92 | */ 93 | public String getAddressString(Address address) { 94 | String address2 = ""; 95 | if (address.getAddressLine(1) != null) { 96 | address2 = " " + address.getAddressLine(1); 97 | } 98 | return address.getAddressLine(0) + address2; 99 | } 100 | 101 | /** 102 | * 화면 중앙으로부터 화면 왼쪽까지의 거리를 반환한다. 103 | * @param map 구글 지도 객체 104 | * @return 거리(m) 105 | */ 106 | public int getDistanceMeterFromScreenCenter(GoogleMap map) { 107 | VisibleRegion vr = map.getProjection().getVisibleRegion(); 108 | double left = vr.latLngBounds.southwest.longitude; 109 | 110 | Location leftLocation = new Location("left"); 111 | leftLocation.setLatitude(vr.latLngBounds.getCenter().latitude); 112 | leftLocation.setLongitude(left); 113 | 114 | Location center=new Location("center"); 115 | center.setLatitude( vr.latLngBounds.getCenter().latitude); 116 | center.setLongitude( vr.latLngBounds.getCenter().longitude); 117 | return (int) center.distanceTo(leftLocation); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/GoLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.app.FragmentManager; 7 | 8 | import com.mobitant.bestfood.BestFoodInfoActivity; 9 | import com.mobitant.bestfood.BestFoodRegisterActivity; 10 | import com.mobitant.bestfood.ProfileActivity; 11 | 12 | /** 13 | * 액티비티나 프래그먼트 실행 라이브러리 14 | */ 15 | public class GoLib { 16 | public final String TAG = GoLib.class.getSimpleName(); 17 | private volatile static GoLib instance; 18 | 19 | public static GoLib getInstance() { 20 | if (instance == null) { 21 | synchronized (GoLib.class) { 22 | if (instance == null) { 23 | instance = new GoLib(); 24 | } 25 | } 26 | } 27 | return instance; 28 | } 29 | 30 | /** 31 | * 프래그먼트를 보여준다. 32 | * @param fragmentManager 프래그먼트 매니저 33 | * @param containerViewId 프래그먼트를 보여줄 컨테이너 뷰 아이디 34 | * @param fragment 프래그먼트 35 | */ 36 | public void goFragment(FragmentManager fragmentManager, int containerViewId, 37 | Fragment fragment) { 38 | fragmentManager.beginTransaction() 39 | .replace(containerViewId, fragment) 40 | .commit(); 41 | } 42 | 43 | /** 44 | * 뒤로가기를 할 수 있는 프래그먼트를 보여준다. 45 | * @param fragmentManager 프래그먼트 매니저 46 | * @param containerViewId 프래그먼트를 보여줄 컨테이너 뷰 아이디 47 | * @param fragment 프래그먼트 48 | */ 49 | public void goFragmentBack(FragmentManager fragmentManager, int containerViewId, 50 | Fragment fragment) { 51 | fragmentManager.beginTransaction() 52 | .replace(containerViewId, fragment) 53 | .addToBackStack(null) 54 | .commit(); 55 | } 56 | 57 | /** 58 | * 이전 프래그먼트를 보여준다. 59 | * @param fragmentManager 프래그먼트 매니저 60 | */ 61 | public void goBackFragment(FragmentManager fragmentManager) { 62 | fragmentManager.popBackStack(); 63 | } 64 | 65 | /** 66 | * 프로파일 액티비티를 실행한다. 67 | * @param context 컨텍스트 68 | */ 69 | public void goProfileActivity(Context context) { 70 | Intent intent = new Intent(context, ProfileActivity.class); 71 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 72 | context.startActivity(intent); 73 | } 74 | 75 | /** 76 | * 맛집 정보 등록 액티비티를 실행한다. 77 | * @param context 컨텍스트 78 | */ 79 | public void goBestFoodRegisterActivity(Context context) { 80 | Intent intent = new Intent(context, BestFoodRegisterActivity.class); 81 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 82 | context.startActivity(intent); 83 | } 84 | 85 | 86 | /** 87 | * 맛집 정보 액티비티를 실행한다. 88 | * @param context 컨텍스트 89 | * @param infoSeq 맛집 정보 일련번호 90 | */ 91 | public void goBestFoodInfoActivity(Context context, int infoSeq) { 92 | Intent intent = new Intent(context, BestFoodInfoActivity.class); 93 | intent.putExtra(BestFoodInfoActivity.INFO_SEQ, infoSeq); 94 | context.startActivity(intent); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/KeepLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.os.Handler; 4 | 5 | import com.mobitant.bestfood.remote.RemoteService; 6 | import com.mobitant.bestfood.remote.ServiceGenerator; 7 | 8 | import retrofit2.Call; 9 | import retrofit2.Callback; 10 | import retrofit2.Response; 11 | 12 | /** 13 | * 즐겨찾기 관련 라이브러리 14 | */ 15 | public class KeepLib { 16 | public final String TAG = KeepLib.class.getSimpleName(); 17 | private volatile static KeepLib instance; 18 | 19 | public static KeepLib getInstance() { 20 | if (instance == null) { 21 | synchronized (KeepLib.class) { 22 | if (instance == null) { 23 | instance = new KeepLib(); 24 | } 25 | } 26 | } 27 | return instance; 28 | } 29 | 30 | /** 31 | * 즐겨찾기 추가를 서버에 요청한다. 32 | * @param handler 결과를 응답할 핸들러 33 | * @param memberSeq 사용자 일련번호 34 | * @param infoSeq 맛집 정보 일련번호 35 | */ 36 | public void insertKeep(final Handler handler, int memberSeq, final int infoSeq) { 37 | RemoteService remoteService = ServiceGenerator.createService(RemoteService.class); 38 | 39 | Call call = remoteService.insertKeep(memberSeq, infoSeq); 40 | call.enqueue(new Callback() { 41 | @Override 42 | public void onResponse(Call call, Response response) { 43 | if (response.isSuccessful()) { 44 | MyLog.d(TAG, "insertKeep " + response); 45 | handler.sendEmptyMessage(infoSeq); 46 | } else { // 등록 실패 47 | MyLog.d(TAG, "response error " + response.errorBody()); 48 | } 49 | } 50 | 51 | @Override 52 | public void onFailure(Call call, Throwable t) { 53 | MyLog.d(TAG, "no internet connectivity"); 54 | } 55 | }); 56 | } 57 | 58 | /** 59 | * 즐겨찾기 삭제를 서버에 요청한다. 60 | * @param handler 결과를 응답할 핸들러 61 | * @param memberSeq 사용자 일련번호 62 | * @param infoSeq 맛집 정보 일련번호 63 | */ 64 | public void deleteKeep(final Handler handler, int memberSeq, final int infoSeq) { 65 | RemoteService remoteService = ServiceGenerator.createService(RemoteService.class); 66 | 67 | Call call = remoteService.deleteKeep(memberSeq, infoSeq); 68 | call.enqueue(new Callback() { 69 | @Override 70 | public void onResponse(Call call, Response response) { 71 | if (response.isSuccessful()) { 72 | MyLog.d(TAG, "deleteKeep " + response); 73 | handler.sendEmptyMessage(infoSeq); 74 | } else { // 등록 실패 75 | MyLog.d(TAG, "response error " + response.errorBody()); 76 | } 77 | } 78 | 79 | @Override 80 | public void onFailure(Call call, Throwable t) { 81 | MyLog.d(TAG, "no internet connectivity"); 82 | } 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/MyLog.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.util.Log; 4 | 5 | import com.mobitant.bestfood.BuildConfig; 6 | 7 | /** 8 | * 로그 편의 클래스 9 | * 기존 로그를 좀 더 편하기 사용하기 위한 메소드로 구성 10 | * BuildConfig.DEBUG 값을 통해 개발 단계에서는 로그를 출력하고 마켓 런칭 단계에서는 로그 출력 안 함 11 | */ 12 | public class MyLog { 13 | private static boolean enabled = BuildConfig.DEBUG; 14 | 15 | public static void d(String tag, String text) { 16 | if (!enabled) return; 17 | 18 | Log.d(tag, text); 19 | } 20 | 21 | public static void d(String text) { 22 | if (!enabled) return; 23 | 24 | Log.d("tag", text); 25 | } 26 | 27 | public static void d(String tag, Class cls, String text) { 28 | if (!enabled) return; 29 | 30 | Log.d(tag, cls.getName() + "." + text); 31 | } 32 | 33 | 34 | public static void e(String tag, String text) { 35 | if (!enabled) return; 36 | 37 | Log.e(tag, text); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/MyToast.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | /** 7 | * 토스트 편의 클래스 8 | * 토스트를 좀 더 편하게 사용하기 위한 메소드로 구성 9 | */ 10 | public class MyToast { 11 | public static void s(Context context, int id) { 12 | Toast.makeText(context, id, Toast.LENGTH_SHORT).show(); 13 | } 14 | 15 | public static void s(Context context, String text) { 16 | Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); 17 | } 18 | 19 | public static void l(Context context, int id) { 20 | Toast.makeText(context, id, Toast.LENGTH_LONG).show(); 21 | } 22 | 23 | public static void l(Context context, String text) { 24 | Toast.makeText(context, text, Toast.LENGTH_LONG).show(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/RemoteLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | import android.os.Handler; 7 | 8 | import com.mobitant.bestfood.remote.RemoteService; 9 | import com.mobitant.bestfood.remote.ServiceGenerator; 10 | 11 | import java.io.File; 12 | 13 | import okhttp3.MediaType; 14 | import okhttp3.MultipartBody; 15 | import okhttp3.RequestBody; 16 | import okhttp3.ResponseBody; 17 | import retrofit2.Call; 18 | import retrofit2.Callback; 19 | import retrofit2.Response; 20 | 21 | /** 22 | * 네트워크와 서버와 관련된 라이브러리 23 | */ 24 | public class RemoteLib { 25 | public static final String TAG = RemoteLib.class.getSimpleName(); 26 | 27 | private volatile static RemoteLib instance; 28 | 29 | public static RemoteLib getInstance() { 30 | if (instance == null) { 31 | synchronized (RemoteLib.class) { 32 | if (instance == null) { 33 | instance = new RemoteLib(); 34 | } 35 | } 36 | } 37 | return instance; 38 | } 39 | 40 | /** 41 | * 네트워크 연결 여부를 반환한다. 42 | * @param context 컨텍스트 43 | * @return 네트워크 연결여부 44 | */ 45 | public boolean isConnected(Context context) { 46 | try { 47 | ConnectivityManager cm = 48 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 49 | NetworkInfo info = cm.getActiveNetworkInfo(); 50 | 51 | if (info != null) { 52 | return true; 53 | } else { 54 | return false; 55 | } 56 | } catch (Exception e) { 57 | return false; 58 | } 59 | } 60 | 61 | /** 62 | * 사용자 프로필 아이콘을 서버에 업로드한다. 63 | * @param memberSeq 사용자 일련번호 64 | * @param file 파일 객체 65 | */ 66 | public void uploadMemberIcon(int memberSeq, File file) { 67 | RemoteService remoteService = ServiceGenerator.createService(RemoteService.class); 68 | 69 | RequestBody requestFile = 70 | RequestBody.create(MediaType.parse("multipart/form-data"), file); 71 | 72 | MultipartBody.Part body = 73 | MultipartBody.Part.createFormData("file", file.getName(), requestFile); 74 | 75 | RequestBody memberSeqBody = 76 | RequestBody.create( 77 | MediaType.parse("multipart/form-data"), "" + memberSeq); 78 | 79 | Call call = 80 | remoteService.uploadMemberIcon(memberSeqBody, body); 81 | call.enqueue(new Callback() { 82 | @Override 83 | public void onResponse(Call call, 84 | Response response) { 85 | MyLog.d(TAG, "uploadMemberIcon success"); 86 | } 87 | 88 | @Override 89 | public void onFailure(Call call, Throwable t) { 90 | MyLog.e(TAG, "uploadMemberIcon fail"); 91 | } 92 | }); 93 | } 94 | 95 | /** 96 | * 맛집 이미지를 서버에 업로드한다. 97 | * @param infoSeq 맛집 정보 일련번호 98 | * @param imageMemo 이미지 설명 99 | * @param file 파일 객체 100 | * @param handler 처리 결과를 응답할 핸들러 101 | */ 102 | public void uploadFoodImage(int infoSeq, String imageMemo, File file, final Handler handler) { 103 | RemoteService remoteService = ServiceGenerator.createService(RemoteService.class); 104 | 105 | RequestBody requestFile = 106 | RequestBody.create(MediaType.parse("multipart/form-data"), file); 107 | 108 | MultipartBody.Part body = 109 | MultipartBody.Part.createFormData("file", file.getName(), requestFile); 110 | 111 | RequestBody infoSeqBody = 112 | RequestBody.create( 113 | MediaType.parse("multipart/form-data"), "" + infoSeq); 114 | RequestBody imageMemoBody = 115 | RequestBody.create( 116 | MediaType.parse("multipart/form-data"), imageMemo); 117 | 118 | Call call = 119 | remoteService.uploadFoodImage(infoSeqBody, imageMemoBody, body); 120 | call.enqueue(new Callback() { 121 | @Override 122 | public void onResponse(Call call, 123 | Response response) { 124 | MyLog.d(TAG, "uploadFoodImage success"); 125 | handler.sendEmptyMessage(0); 126 | } 127 | 128 | @Override 129 | public void onFailure(Call call, Throwable t) { 130 | MyLog.e(TAG, "uploadFoodImage fail"); 131 | } 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/lib/StringLib.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.lib; 2 | 3 | import android.content.Context; 4 | 5 | import com.mobitant.bestfood.R; 6 | 7 | /** 8 | * 문자열 관련 라이브러리 9 | */ 10 | public class StringLib { 11 | public final String TAG = StringLib.class.getSimpleName(); 12 | private volatile static StringLib instance; 13 | 14 | protected StringLib() { 15 | } 16 | 17 | public static StringLib getInstance() { 18 | if (instance == null) { 19 | synchronized (StringLib.class) { 20 | if (instance == null) { 21 | instance = new StringLib(); 22 | } 23 | } 24 | } 25 | return instance; 26 | } 27 | 28 | /** 29 | * 문자열이 null이거나 ""인지를 파악한다. 30 | * @param str 문자열 객체 31 | * @return null이거나 ""이라면 true, 아니라면 false 32 | */ 33 | public boolean isBlank(String str) { 34 | if (str == null || str.equals("")) { 35 | return true; 36 | } else { 37 | return false; 38 | } 39 | } 40 | 41 | /** 42 | * 문자열를 지정된 길이로 잘라서 반환한다. 43 | * @param context 컨텍스트 44 | * @param str 문자열 객체 45 | * @param max 최대 문자열 길이 46 | * @return 변경된 문자열 객체 47 | */ 48 | public String getSubString(Context context, String str, int max) { 49 | if (str != null && str.length() > max) { 50 | return str.substring(0, max) + context.getResources().getString(R.string.skip_string); 51 | } else { 52 | return str; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/remote/RemoteService.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.remote; 2 | 3 | import com.mobitant.bestfood.item.FoodInfoItem; 4 | import com.mobitant.bestfood.item.KeepItem; 5 | import com.mobitant.bestfood.item.MemberInfoItem; 6 | 7 | import java.util.ArrayList; 8 | 9 | import okhttp3.MultipartBody; 10 | import okhttp3.RequestBody; 11 | import okhttp3.ResponseBody; 12 | import retrofit2.Call; 13 | import retrofit2.http.Body; 14 | import retrofit2.http.DELETE; 15 | import retrofit2.http.Field; 16 | import retrofit2.http.FormUrlEncoded; 17 | import retrofit2.http.GET; 18 | import retrofit2.http.Multipart; 19 | import retrofit2.http.POST; 20 | import retrofit2.http.Part; 21 | import retrofit2.http.Path; 22 | import retrofit2.http.Query; 23 | 24 | /** 25 | * 서버에 호출할 메소드를 선언하는 인터페이스 26 | */ 27 | public interface RemoteService { 28 | String BASE_URL = "http://192.168.1.188:3000"; 29 | String MEMBER_ICON_URL = BASE_URL + "/member/"; 30 | String IMAGE_URL = BASE_URL + "/img/"; 31 | 32 | //사용자 정보 33 | @GET("/member/{phone}") 34 | Call selectMemberInfo(@Path("phone") String phone); 35 | 36 | @POST("/member/info") 37 | Call insertMemberInfo(@Body MemberInfoItem memberInfoItem); 38 | 39 | @FormUrlEncoded 40 | @POST("/member/phone") 41 | Call insertMemberPhone(@Field("phone") String phone); 42 | 43 | @Multipart 44 | @POST("/member/icon_upload") 45 | Call uploadMemberIcon(@Part("member_seq") RequestBody memberSeq, 46 | @Part MultipartBody.Part file); 47 | 48 | //맛집 정보 49 | @GET("/food/info/{info_seq}") 50 | Call selectFoodInfo(@Path("info_seq") int foodInfoSeq, 51 | @Query("member_seq") int memberSeq); 52 | 53 | @POST("/food/info") 54 | Call insertFoodInfo(@Body FoodInfoItem infoItem); 55 | 56 | @Multipart 57 | @POST("/food/info/image") 58 | Call uploadFoodImage(@Part("info_seq") RequestBody infoSeq, 59 | @Part("image_memo") RequestBody imageMemo, 60 | @Part MultipartBody.Part file); 61 | 62 | @GET("/food/list") 63 | Call> listFoodInfo(@Query("member_seq") int memberSeq, 64 | @Query("user_latitude") double userLatitude, 65 | @Query("user_longitude") double userLongitude, 66 | @Query("order_type") String orderType, 67 | @Query("current_page") int currentPage); 68 | 69 | 70 | //지도 71 | @GET("/food/map/list") 72 | Call> listMap(@Query("member_seq") int memberSeq, 73 | @Query("latitude") double latitude, 74 | @Query("longitude") double longitude, 75 | @Query("distance") int distance, 76 | @Query("user_latitude") double userLatitude, 77 | @Query("user_longitude") double userLongitude); 78 | 79 | 80 | //즐겨찾기 81 | @POST("/keep/{member_seq}/{info_seq}") 82 | Call insertKeep(@Path("member_seq") int memberSeq, @Path("info_seq") int infoSeq); 83 | 84 | @DELETE("/keep/{member_seq}/{info_seq}") 85 | Call deleteKeep(@Path("member_seq") int memberSeq, @Path("info_seq") int infoSeq); 86 | 87 | @GET("/keep/list") 88 | Call> listKeep(@Query("member_seq") int memberSeq, 89 | @Query("user_latitude") double userLatitude, 90 | @Query("user_longitude") double userLongitude); 91 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobitant/bestfood/remote/ServiceGenerator.java: -------------------------------------------------------------------------------- 1 | package com.mobitant.bestfood.remote; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.mobitant.bestfood.BuildConfig; 6 | 7 | import okhttp3.OkHttpClient; 8 | import okhttp3.logging.HttpLoggingInterceptor; 9 | import retrofit2.Retrofit; 10 | import retrofit2.converter.gson.GsonConverterFactory; 11 | 12 | /** 13 | * 서버와 통신하기 위한 리트로핏을 사용하기 위한 클래스 14 | */ 15 | public class ServiceGenerator { 16 | /** 17 | * 원격 호출을 정의한 인터페이스 메소드를 호출할 수 있는 서비스를 생성 18 | * @param serviceClass 원격 호출 메소드를 정의한 인터페이스 19 | * @return 인터페이스 구현체 20 | */ 21 | public static S createService(Class serviceClass) { 22 | HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); 23 | if (BuildConfig.DEBUG) { 24 | logging.setLevel(HttpLoggingInterceptor.Level.BODY); 25 | } else { 26 | logging.setLevel(HttpLoggingInterceptor.Level.NONE); 27 | } 28 | 29 | OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); 30 | httpClient.addInterceptor(logging); 31 | 32 | Gson gson = new GsonBuilder() 33 | .setLenient() 34 | .create(); 35 | 36 | Retrofit retorfit = new Retrofit.Builder() 37 | .baseUrl(RemoteService.BASE_URL) 38 | .addConverterFactory(GsonConverterFactory.create(gson)) 39 | .client(httpClient.build()) 40 | .build(); 41 | 42 | return retorfit.create(serviceClass); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_basic_black_transparent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_basic_gray.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_bestfood_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/bg_bestfood_drawer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/bg_index.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_round_gray.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_round_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_round_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_camera.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keep_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_keep_off.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keep_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_keep_on.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_list.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_list2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_map.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_person.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_profile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_register.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kairo96/and_node/fe5de1bb882029f9afaa3e8c51dbbbad3b2be523/app/src/main/res/drawable/ic_tell.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_bestfood_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 21 | 22 | 30 | 31 | 35 | 36 | 41 | 42 | 43 | 44 | 51 | 52 | 57 | 58 | 68 | 69 | 74 | 75 | 76 | 77 | 88 | 89 | 90 | 91 | 98 | 99 | 107 | 108 | 117 | 118 | 119 | 120 | 127 | 128 | 133 | 134 | 143 | 144 | 153 | 154 | 155 | 156 | 164 | 165 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 180 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_bestfood_register.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 29 | 30 | 41 | 42 |