프로젝트 진행 기간 : 2015.03.01 ~ 2015.06.08
개발 환경 :
- Android 4.4 Kitkat 기준 (Min Version : Android 4.0.3 IceCreamSandWich)
- 개발 도구 : 안드로이드 스튜디오 1.3.2 (Gradle), mySQL, Eclipse EE(PHP)
- 사용 언어 : 자바(Android), PHP (백엔드)
- 서버 정보 : Ubuntu Linux 환경 Apache Server
4학년 1학기에 들은 필수과목 종합설계 프로젝트라는 캡스톤 디자인 과목에서 진행한 프로젝트 입니다.
(기획발표 프레젠테이션)
경성대의 공대는 학교 가장 꼭대기에 위치하여 쉬이 무언가를 먹으려 내려가기 힘든 환경입니다.
그래서 연구실 생활을 하는 동안 무언가를 시켜먹는 일이 잦았습니다.
항상 비슷한 것들만 먹게 되다보니 좀 더 맛있는 집을 찾게 되었고, 여러 유명한 배달업체를 찾아봤지만 대연동이라는 검색범주 안에서는 제외되는 업체도 많고, 숨겨진 맛집을 찾기에는 힘이 들었습니다.
또한, 배달을 올때마다 뿌리고 가는 전단지들 때문에 교내 환경이 전단지로 몸살을 앓기 시작했습니다.
이러한 점들을 개선하고자 경성대만의 배달업체 정보제공 안드로이드 어플리케이션, 경성대 배달학과를 만들자는 생각을 하게 되었습니다.
종합설계 프로젝트는 강의이니 만큼 교수님의 지도 하에 프로젝트가 진행되었는데, 소프트웨어 공학에 대해 공부하면서
DFD(Data Flow Diagram), HIFO(Hierachy Plus Input Process Output) Diagram을 작성하며 프로젝트의 상세 설계를 해나갔습니다.
이러한 Diagram은 처음 그려보는 것이라 그리면서도 이게 맞나..하며 그렸지만, 교수님의 질타를 예상했던 것보단 좋은 반응을 받을 수 있었습니다.
(작성한 Diagram)
이때 프로젝트를 진행했던 인원은 본인 포함 2명이었는데, 다른 팀원 한명이 DB를 포함한 백엔드 서비스를 담당하고 저는 안드로이드 프로그래밍을 맡았습니다. 당시 사용한 서버는 연구실에서 사용하는 서버로, 이전의 프로젝트들로 서버 환경은 모두 구축되어 있었기에 DB와 PHP만 만들면 되는 상황이었습니다.
안드로이드에서 제가 구현을 맡은 것은 UI구성과 UI에 들어갈 이미지들 작성, SNS연동 기능, 서버와 http 통신을 통해 데이터를 교환하는 기능, 그 데이터들을 바탕으로 안드로이드 상에 출력해 주는 일 등 이었습니다.
따라서 여태 프로젝트를 진행할 때 그래왔듯이, 먼저 화면 구성을 프로토타입을 통해 짜게 되었습니다.
https://ovenapp.io/view/wFN7m1FXelKmy4mhgAtno1WGmAxH4m0y/T3RVd
(프로토타입 툴 oven을 사용해 만든 경성대 배달학과 프로토타입)
그리고 두번째로는 앱 내부에 들어갈 이미지들을 제작하였습니다.
버튼 이미지 또한 나인패치로 제작하였습니다.
-App Icon
-Main Logo
-Categories
-Buttons(Nine Patch)
(제작 및 편집한 이미지들)
마지막으로 안드로이드 내부에서는 SNS연동과 백엔드와의 통신 등의 어플리케이션을 기능을 구현하였습니다.
SNS 연동 같은 경우 페이스북과 카카오 연동 두가지 방식을 사용했는데, 결과 발표 당시에는 페이스북만 구현했지만 완성 이후에 카카오톡 연동 또한 구현하였습니다.
구현 시에는 페이스북 로그인 SDK와 카카오 로그인 SDK를 사용하여 사용자의 고유 카카오/페이스북 id와 이름, 프로필 사진 url 등을 받아 최초 로그인 시에 서버 DB에 저장하고, 어플리케이션 내에서는 세션을 유지시켜서 로그인을 유지할 수 있게 하였습니다.
세션을 유지하는 기능은 이전 프로젝트에서 구현했던 기능을 활용하여서 쉽게 구현할 수 있었습니다.
-LoginActivity.java
: Splash Activity 겸 세션 체크 후 로그인 상태가 아닐 때 로그인 기능이 나타나는 액티비티.
페이스북/카카오 연동 로그인 기능 및 InsertUser 객체를 통해 서버의 DB유저정보를 삽입하거나 수정한다.
연동 과정에서 액티비티가 사라지는 현상이 있어서 BaseActivity를 밑에 깔아 주었다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | package com.miclab.ksbaedal.login; import android.content.Context; import android.content.Intent; import android.support.v4.app.FragmentActivity; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.widget.Button; import android.widget.Toast; import com.facebook.*; import com.facebook.login.LoginManager; import com.facebook.login.LoginResult; import com.kakao.auth.ApiResponseCallback; import com.kakao.auth.AuthType; import com.kakao.auth.ErrorResult; import com.kakao.auth.ISessionCallback; import com.kakao.auth.KakaoSDK; import com.kakao.auth.Session; import com.kakao.usermgmt.UserManagement; import com.kakao.usermgmt.callback.MeResponseCallback; import com.kakao.usermgmt.response.model.UserProfile; import com.kakao.util.exception.KakaoException; import com.kakao.util.helper.log.Logger; import com.miclab.ksbaedal.R; import com.miclab.ksbaedal.kakao.KakaoSDKAdapter; import com.miclab.ksbaedal.main.MainActivity; import java.util.Arrays; import java.util.HashMap; import java.util.Map; public class LoginActivity extends FragmentActivity { CallbackManager callbackManager; ProfileTracker profileTracker; AccessTokenTracker accessTokenTracker; SessionCallback callback; Context con, basecon; Intent i, intent; Boolean isFirst; SessionManager session; Button fbloginButton, kakaologinButton; int delaySec; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); FacebookSdk.sdkInitialize(getApplicationContext()); try{ KakaoSDK.init(new KakaoSDKAdapter(this)); } catch (KakaoSDK.AlreadyInitializedException ex){;} setContentView(com.miclab.ksbaedal.R.layout.activity_login); final Handler handler = new Handler(); con=this; i = new Intent(LoginActivity.this, MainActivity.class); intent = getIntent(); delaySec = 1500; Boolean isDelay = intent.getBooleanExtra("startAnimation", true); if(!isDelay) { delaySec=0; } isFirst = intent.getBooleanExtra("ExistBase",false); session = new SessionManager(this.getApplicationContext()); //facebook sdk 설정 callbackManager = CallbackManager.Factory.create(); accessTokenTracker = new AccessTokenTracker() { @Override protected void onCurrentAccessTokenChanged( AccessToken oldAccessToken, AccessToken currentAccessToken) { // On AccessToken changes fetch the new profile which fires the event on // the ProfileTracker if the profile is different //Profile.fetchProfileForCurrentAccessToken(); } }; profileTracker = new ProfileTracker() { @Override protected void onCurrentProfileChanged(Profile oldProfile, Profile newProfile) { setProfile(newProfile.getId(), newProfile.getName(), "f", ""); } }; accessTokenTracker.startTracking(); profileTracker.startTracking(); //kakao sdk 설정 callback = new SessionCallback(); Session.getCurrentSession().addCallback(callback); if(session.isLogin()) Session.getCurrentSession().checkAndImplicitOpen(); fbloginButton = (Button) findViewById(R.id.fb_Login_button); kakaologinButton = (Button) findViewById(R.id.kakao_Login_button); runOnUiThread(new Runnable() { @Override public void run() { handler.postDelayed(new Runnable() { @Override public void run() { //세션체크 if (session.isLogin()) { System.out.println("login"); startActivity(i); overridePendingTransition(R.animator.in, R.animator.out); finish(); if(isFirst) BaseActivity.base.finish(); } else { Animation ani = new AlphaAnimation(0, 1); ani.setDuration(400); fbloginButton.setAnimation(ani); kakaologinButton.setAnimation(ani); fbloginButton.setVisibility(View.VISIBLE); kakaologinButton.setVisibility(View.VISIBLE); } } }, delaySec); } }); //페이스북 로그인 //fbloginButton.registerCallback(callbackManager, new FacebookCallback<LoginResult>() { LoginManager.getInstance().registerCallback(callbackManager, new FacebookCallback<LoginResult>() { @Override public void onSuccess(LoginResult loginResult) { AccessToken accessToken = loginResult.getAccessToken(); Profile profile = Profile.getCurrentProfile(); //Profile.fetchProfileForCurrentAccessToken(); //setProfile(Profile.getCurrentProfile()); setProfile(profile.getId(), profile.getName(), "f", ""); startActivity(i); overridePendingTransition(R.animator.in, R.animator.out); if(isFirst) BaseActivity.base.finish(); finish(); // } } @Override public void onCancel() { Log.d("FacebookLogin", "Canceled"); } @Override public void onError(FacebookException error) { Log.d("FacebookLogin", String.format("Error: %s", error.toString())); String title = "Facebook 로그인 에러"; String alertMessage = error.getMessage(); Toast.makeText(con, title+" : "+alertMessage, Toast.LENGTH_SHORT).show(); } }); fbloginButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { fbButtonOnClick(); } }); //카톡 로그인 부분 kakaologinButton.setOnClickListener(new Button.OnClickListener() { public void onClick(View view) { kakaoButtonOnClick(); } }); } void fbButtonOnClick(){ LoginManager.getInstance().logInWithReadPermissions(this, Arrays.asList("public_profile")); } void kakaoButtonOnClick(){ Session.getCurrentSession().open(AuthType.KAKAO_TALK_EXCLUDE_NATIVE_LOGIN, this); } void kakaoLogin(){ UserManagement.requestMe(new MeResponseCallback() { @Override public void onFailure(ErrorResult errorResult) { String message = "failed to get user info. msg=" + errorResult; Logger.d(message); } @Override public void onSessionClosed(ErrorResult errorResult) { String message = "failed to get user info. msg=" + errorResult; Logger.d(message); } @Override public void onSuccess(UserProfile userProfile) { if (userProfile != null) { userProfile.saveUserToCache(); Logger.d("UserProfile : " + userProfile); setProfile(String.valueOf(userProfile.getId()), userProfile.getNickname(), "k", userProfile.getThumbnailImagePath()); startActivity(i); overridePendingTransition(R.animator.in, R.animator.out); if (isFirst) BaseActivity.base.finish(); finish(); } } @Override public void onNotSignedUp() { session.logout(); } }); } void kakaoUpdate(){ final Map<String, String> properties = new HashMap<String, String>(); properties.put("nickname", session.getValue("userName")); properties.put("profile_image", session.getValue("profilePicture")); UserManagement.requestUpdateProfile(new ApiResponseCallback<Long>() { @Override public void onSuccess(Long userId) { UserProfile profile = UserProfile.loadFromCache(); profile.updateUserProfile(properties).saveUserToCache(); Logger.d("succeeded to update user profile" + profile); setProfile(String.valueOf(session.getLoginID()), profile.getNickname(), "k", profile.getThumbnailImagePath()); } @Override public void onNotSignedUp() { session.logout(); } @Override public void onFailure(ErrorResult errorResult) { String message = "failed to get user info. msg=" + errorResult; Logger.e(message); } @Override public void onSessionClosed(ErrorResult errorResult) { String message = "failed to get user info. msg=" + errorResult; Logger.e(message); } }, properties); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Handler handler = new Handler(); if(event.getAction()==KeyEvent.ACTION_DOWN){ if(keyCode==KeyEvent.KEYCODE_BACK){ if (isFirst) BaseActivity.base.finish(); return super.onKeyDown(keyCode, event); } } return true; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (Session.getCurrentSession().handleActivityResult(requestCode, resultCode, data)) { return; } callbackManager.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data); } @Override public void onDestroy() { Session.getCurrentSession().removeCallback(callback); profileTracker.stopTracking(); accessTokenTracker.stopTracking(); super.onDestroy(); } private void setProfile(String id, String name, String type, String picture) { Log.i("login info", "id : " + id + " name : " + name + " type : " + type); if(type=="f") { session.login(id, type, name, "https://graph.facebook.com/" + id + "/picture?type=large"); } else if(type=="k"){ session.login(id, type, name, picture); } InsertUser iu = new InsertUser(LoginActivity.this, session.getLoginID(), session.getLoginType(), session.getValue("userName"), session.getValue("profilePicture"), session); try { iu.execute(); } catch (Exception e) { e.printStackTrace(); } } private class SessionCallback implements ISessionCallback { @Override public void onSessionOpened() { if(session.isLogin()){if(session.getLoginType()=="k") LoginActivity.this.kakaoUpdate(); } else { LoginActivity.this.kakaoLogin(); } } @Override public void onSessionOpenFailed(KakaoException exception) { if(exception != null) { Logger.e(exception); } } } } | cs |
-SessionManager.java
: 어플리케이션 내의 앱 데이터, 세션을 설정하는 부분.
SharedPreferences를 사용해서 현재 로그인 된 정보를 저장하고 호출시 정보를 로드한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | package com.miclab.ksbaedal.login; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; public class SessionManager { SharedPreferences pref; Editor edit; public static final String IS_LOGIN = "isLogin"; public static final String USER_ID = "userID"; public static final String USER_TYPE = "userType"; public static final String USER_NAME = "userName"; public static final String USER_NICKNAME = "nickName"; public static final String PROFILE_PICTURE = "profilePicture"; public SessionManager(Context con){ pref=con.getSharedPreferences("LoginSession", Activity.MODE_PRIVATE); edit=pref.edit(); } public void login(String id, String type, String name, String picture){ edit.putBoolean(IS_LOGIN, true); edit.putString(USER_ID, id); edit.putString(USER_TYPE, type); edit.putString(USER_NAME, name); edit.putString(PROFILE_PICTURE, picture); edit.commit(); } public void logout(){ if(isLogin()){ edit.putBoolean(IS_LOGIN, false); edit.putString(USER_ID, null); edit.putString(USER_TYPE, null); edit.putString(USER_NAME, null); edit.putString(USER_NICKNAME, null); edit.putString(PROFILE_PICTURE, null); edit.commit(); } } public String getLoginID(){ if(isLogin()){ return pref.getString(USER_ID, null); } return null; } public String getLoginType(){ if(isLogin()){ return pref.getString(USER_TYPE, null); } return null; } public String getValue(String key){ if(isLogin()){ return pref.getString(key, ""); } return null; } public boolean isLogin(){ return pref.getBoolean(IS_LOGIN, false); } } | cs |
(SNS 연동 로그인 기능)
이외에 나머지 기능은 대부분 커스텀 리스트뷰와 http 통신을 하는 것으로 생략하겠습니다.
수정한 실제 액티비티의 구성은 이렇습니다.
-kakao : kakaoSDK 관련 파일
-login : splash 포함 로그인 기능 액티비티, 세션관리 매니저
-main : http 통신을 통해 json 데이터를 파싱하는 기능, 메인 액티비티를 중심으로 가게 카테고리 리스트, 메뉴 추천 액티비티 포함 (FavoriteAcitivity 파일은 개인의 즐겨찾기 가게를 보여주는 액티비티로 미구현 기능)
-pasing : 교내 식당 페이지 데이터 파싱 기능, 파싱한 데이터(학식 메뉴)를 표시해주는 액티비티
-store : 가게 상세 페이지 관련 액티비티/어댑터. 리뷰 작성 기능
-CTextView.java : 글자가 잘리지 않고 줄넘김이 되는 텍스트뷰 사용을 위한 파일
다음은 백엔드 서비스 구조입니다.
서버 내에 php 언어를 사용해서 json 데이터를 송출하고, 안드로이드에서는 json데이터가 있는 해당 페이지에서 파싱을 통해 데이터를 가져옵니다.
리뷰 삽입/로드
-insert_review.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | <?php header("Content-Type: application/json; charset=UTF-8"); header("Accept: application/json"); header("Cache-control: No-Cache"); header("Pragma: No-Cache"); header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept"); $mysql_hostname=""; $mysql_user=""; $mysql_password=""; $mysql_database=""; $dbc = mysql_connect($mysql_hostname,$mysql_user,$mysql_password) or die("db connect error: ".mysql_error()); mysql_select_db($mysql_database,$dbc) or die("db connect error: ".mysql_error()); mysql_query("SET NAMES UTF8"); $idx = $_POST["idx"]; $score = $_POST["score"]; $userid = $_POST["userid"]; $usertype = $_POST["usertype"]; $storeid = $_POST["storeid"]; $content = $_POST["content"]; $image = $_POST["image"]; if($userid!=null&&$usertype!=null&&$storeid!=null){ //index - null : insert new review if($idx==null){ $result = mysql_query("INSERT INTO baedal.review (score, userid, usertype, storeid, content, image) values (".$score.",'".$userid."','".$usertype."','".$storeid."','".$content."','".$image."')"); if($result) echo "review insert success. "; else die("review insert fail:".mysql_error()); } //index - not null : update review else{ //review check $result = mysql_query("SELECT * FROM review WHERE idx = ".$idx); if(!$result) die("review query fail:".mysql_error()); $numrow = mysql_num_rows($result); if($numrow!=null){ $result = mysql_query("UPDATE review SET score=".$score.", userid='".$userid."',usertype='".$usertype."',storeid='".$storeid."',content='".$content."',image='".$image."' WHERE idx=".$idx); if($result) echo "review update success. "; else die("review update fail:".mysql_error()); } else die("not exist review index"); } } //exception else{ echo "error - foreign key null"; } //score update $result = mysql_query("SELECT sum(score), count(score) FROM review WHERE storeid='".$storeid."'"); if($result) echo "review select success. "; else die("review select fail".mysql_error()); $row = mysql_fetch_row($result); $avg_score = $row[0]/$row[1]; $result = mysql_query("UPDATE store SET score=".$avg_score." WHERE storeid='".$storeid."'"); if($result) echo "score update success"; else die("score update fail".mysql_error()); mysql_close($dbc); ?> | cs |
-load_review.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | <?php header("Content-Type: application/json; charset=UTF-8"); header("Accept: application/json"); header("Cache-control: No-Cache"); header("Pragma: No-Cache"); header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept"); $mysql_hostname=""; $mysql_user=""; $mysql_password=""; $mysql_database=""; $dbc = mysql_connect($mysql_hostname,$mysql_user,$mysql_password) or die("db connect error: ".mysql_error()); mysql_select_db($mysql_database,$dbc) or die("db connect error: ".mysql_error()); mysql_query("SET NAMES UTF8"); $type = $_POST["type"]; $userid = $_POST["userid"]; $usertype = $_POST["usertype"]; $storeid = $_POST["storeid"]; if($type=='u') $selectquery = "SELECT * FROM review WHERE userid='".$userid."' AND usertype='".$usertype."'"; else if($type='s') $selectquery = "SELECT * FROM review WHERE storeid='".$storeid."'"; else die("type is null or incorrect (only u or s)"); $result = mysql_query($selectquery); if(!$result) die("review select fail:".mysql_error()); if(mysql_num_rows($result)>0){ $response = array(); echo "{\"android\":"; while($row = mysql_fetch_array($result)){ $main["idx"] = $row[0]; $main["score"] = $row[1]; $main["username"] = mysql_fetch_array(mysql_query("SELECT name FROM user WHERE userid='".$row[2]."' AND type='".$row[3]."'"))[0]; $main["storeid"] = $row[4]; $main["content"] = $row[5]; $main["datetime"] = $row[6]; $main["image"] = $row[7]; array_push($response, $main); } echo json_encode($response, JSON_UNESCAPED_UNICODE); echo "}"; } else { //not found $response["success"] = 0; $response["message"] = "Can't load or is not any review"; //echo no user JSON echo json_encode($response, JSON_UNESCAPED_UNICODE); } mysql_close($dbc); ?> | cs |
데이터베이스의 경우, 함께 프로젝트를 진행했던 팀원이 설계를 담당했었는데 가게마다 각각의 메뉴 테이블을 가지고 있는 아주 비효율적인 구조를 가지고 있었습니다. 그래서 최종발표 이후 데이터베이스를 갈아엎는 수정을 거치게 됩니다.
데이터베이스는 User 테이블, Review 테이블, Store 테이블로 구성되어 있으며
store테이블안에는 가게정보와 메뉴정보가 함께 들어있습니다.
메뉴같은 경우 parent id라는 컬럼을 추가한 메뉴와 가게 테이블을 join한 형태인데 parent id에 값이 있냐 없냐에 따라 가게항목과 메뉴항목을 구분하였습니다. 메뉴의 경우 해당 가게의 id값을 parent id로 조회하여 쿼리 결과로 나타내었는데, 데이터베이스에 설계가 미숙할 때 수정했던 것이라 지금에 와서는 메뉴만을 모아놓은 테이블과 가게테이블을 1대다 관계로 설정하면 되지 않을까 하는 생각이 듭니다.
수정한 데이터베이스는 서버가 열려있을 때 (지금은 닫혀있는 상태) DB 구성을 캡쳐하지 못하여 관련 자료가 없습니다.
이렇게 만들어진 베타 버전의 경성대 배달학과는 야침찬 부가기능 개발계획을 가지고 있었지만, 이 후의 일정에 밀려 더 이상 발전시키지 못해서 아쉬움이 많은 프로젝트입니다.
구글 플레이스토어에 등록까지 했지만, 언젠가 완성시켜서 이미 졸업한 학교이지만 후배들에게 편의를 제공해 주는 것이 목표입니다.
마지막으로 실제 실행 화면이 포함된 어플리케이션 사용설명서를 첨부합니다.
