티스토리 뷰

개발노트

나만의 일정 관리 앱

메시에 2019. 8. 11. 22:51

최근에 포스트잇에 매일 그날 할 일들의 리스트를 적어서 벽에 붙여두면 뭔가 목표의식이 좀 더 뚜렷해지고 동기부여가 되지 않을까, 라는 생각을 해서 그렇게 하고 있긴 한데 아무래도 직접 글씨를 쓰는 건 귀찮기도 하고 그냥 해야할 일의 리스트를 적어둔 것 뿐이라 동기부여는 별로 되지 않더라. 시작한지 3일만에 써놓은 일을 미루게 되었다.

 

그래서 생각난게 이걸 대신할 앱을 만들자는 것이다. 물론 to-do list 관리할 수 있는 앱이야 앱스토어 가면 널려있겠지만, 동기부여를 위해서 '게임 같은' 기능을 좀 넣어보면 어떨까라는 생각을 했다. 운동을 하려고 해도 그냥 운동에는 아무래도 흥미가 안붙어서 펌프 같은 게임으로 대체하는게 나다.

 

- to-do list에 일을 추가하면 거기에 경험치가 일정량 붙고, 그 일을 완료하면 그만큼의 경험치를 얻는다. 하루에 일을 여러 개 완료하면 추가 경험치 보너스를 받는다.

- 일정 경험치를 얻으면 레벨업을 하며, 레벨업을 할때마다 쿠폰을 얻는다. 쿠폰을 써서 스스로에게 선물을 줄 수 있다. 이를테면 화장품을 산다던가, 하루종일 나가서 게임을 한다던가 (다시 말하면 쿠폰을 쓰지 않고는 이런 일들을 하지 않도록 스스로에게 제약을 건다)

- 부가기능으로 간단한 메모를 남길 수 있고 그날 약을 먹었는지 체크할 수 있도록 한다.

 

 

1) 전체 인터페이스 및 설계

화면 구상은 이렇다.

 

- 상단: 오늘의 날짜와 현재 레벨 및 경험치를 표시한다. 날짜를 누르면 달력이 떠서 다른 날짜를 선택할 수 있게 한다. 경험치는 숫자로만 표시할 수도 있겠지만 Bar 형태로 시각적으로 확인할 수 있으면 좋겠다.

- 일정 표시: ListView로 오늘의 일정 리스트를 표시한다. 각 일정마다 체크박스와 이 일정을 완료했을 때 얻을 수 있는 경험치를 표시한다. 일정을 추가, 삭제하는 인터페이스와 오늘의 일정을 완료하고 경험치를 얻는 인터페이스가 필요하다.

- 메모: 오늘의 메모를 써서 저장할 수 있다. EditText와 초기화 버튼, 저장 버튼이 필요하다. 

- 약 체크: 하루에 3번 약을 먹는다. 약을 먹고 버튼을 눌러서 약을 먹었음을 확인할 수 있도록 한다.

 

이러한 기능들을 구현하기 위해 DB에 저장해야 할 값들은 다음과 같다.

- 일정 테이블: 날짜별로 일정 리스트와 각 일정을 완료했을 때 얻을 수 있는 경험치, 그리고 일정 완료 여부 (앱을 껐다 켜도 체크박스 체크 여부가 유지되어야 하므로) 를 저장해야 한다.

- 메모 테이블: 날짜별 메모를 저장한다.

- 약 테이블: 날짜별로 점심, 저녁, 새벽 약을 먹었는지 저장해둔다.

- 유저 테이블: 현재 레벨과 경험치를 저장한다.

 

2) 레이아웃

일정 ListView랑 메모 EditText는 그냥 상->하로 배치하면 되긴 하지만, 중간중간 작은 버튼들이 있어야 하기 때문에 LinearLayout이나 TableLayout보다는 RelativeLayout을 쓰는 게 편했다.

 

- 버튼을 ListView 우측 하단에 두고 싶다: layout_alignParentRight 속성을 이용.

> 근데 버튼 2개에다 둘 다 이 속성을 줘버리면 두 버튼이 오른쪽 끝에 겹쳐버린다. 그러므로 둘 중에 하나에만 이 속성을 주고 나머지 하나는 그 버튼에 대해 toLeftOf를 줘야 했다.

> 그리고 또 layout_below=(ListView) 도 줘야 했다. 그렇게 안하면 리스트뷰 위쪽으로 버튼이 붕 떠버린다. toLeftOf라는게 X축으로 왼쪽에 배치하긴 하는데 Y축 위치까진 고려하지 않는 모양이다.

 

3) ListView에 체크박스 넣기

이 앱의 핵심 기능인 일정 관리 기능을 구현하기 위해서 필요한게 '체크박스 들어간 리스트뷰' 다. 이걸 어떻게 만들어야 하나 싶어서 좀 찾아봤는데, ListView를 만들 때 항상 따라오는 Adapter라는 친구를 이용해야 하는 모양이다.

기본적인 개념은 대충 이렇다.

 

- ArrayAdapter 등 기존의 Adapter에서 상속을 받는 MyAdapter 클래스를 만든다.

- MyAdapter 내의 멤버 변수로 필요한 것들 (CheckBox와 일정 이름을 표시할 String: 리스트니까 여러개를 넣을 수 있게 배열이나 ArrayList로 만든다) 과 Activity를 선언한다.

- MyAdapter 생성자를 정의한다.

- MyAdapter 안에 CheckBox나 String의 내용에 접근할 수 있는 setter/getter 등 메서드를 정의한다.

- 메인 액티비티에서 ListView.setAdapter() 메서드를 이용해서 ListView와 MyAdapter를 연결해준다.

 

예를 들어서 어떤 이슈가 있냐면, ListView의 항목 하나를 체크했으면 앱을 다시 실행했을 때도 그게 체크된 상태여야 하는데 이걸 구현하기 위해서 이렇게 한다.

 

- '일정' DB 테이블에 checkflag라는 걸 둬서 체크된 상태는 1, 안된 상태는 0이라고 한다

- 앱을 실행하고 일정 ListView를 로딩하는 함수에서 이 DB 테이블을 읽어온다

- checkflag가 1이면 MyAdapter.set_checkbox() 라는 내가 만든 메서드를 호출하게 한다. 이 메서드 안에서는 checkBoxList.get(position).setChecked() 를 실행해서 해당 항목에 대한 체크박스를 활성화시킨다

- 리스트뷰의 항목이 클릭되면 (onClick) DB의 해당 항목에 대한 checkflag를 update한다

 

 

4) 상단부: 달력과 경험치 바

- DatePickerDialog

달력은 워낙 자주 쓰이는 기능이니까 뭔가 위젯 같은걸로 미리 만들어진게 있을거라 생각하긴 했다. 찾아보니까 DatePicker라는 뷰 (XML에서 넣는 거) 도 있고, DatePickerDialog라고 팝업창에 알아서 띄워주는 기능도 있더라. DatePickerDialog를 쓰면 별도의 팝업창을 내가 구현할 필요가 없으니 좀 더 편할 것 같아서 이걸 썼다.

일단 Dialog라는 걸 되게 오랜만에 써봤는데 기본적으로 정의되어 있는 팝업창들을 사용할 수 있다. 대표적으로 많이 쓰이는 '확인/취소' 버튼이 나오는 팝업창 같은 것들. 그 중에서 달력이 뜨는 Dialog가 DatePickerDialog이다.

 

일단 DatePickerDialog를 띄우는 코드는 이렇다.

 

Dialog date_picker = new DatePickerDialog(MainActivity.this, dateSetListener, year, month-1, day);
date_picker.show();

주의할 점은 날짜를 프로그램적으로 다룰 때 항상 나오는 얘기긴 하지만 여기서도 month 값에는 -1을 해줘야 한다는 것이다.

그리고 두번째 인자로 dateSetListener라는게 들어가는데 이걸 또 만들어줘야 한다. 이름 그대로 DatePickerDialog가 뜬 다음 날짜가 선택됐을 때 어떤 행동을 할 것인지 이 리스너 안에 정의해주면 된다.

> 상단의 현재 날짜를 선택된 날짜로 바꾼다

> 일정, 메모, 약 복용 여부를 불러오는 함수를 해당 날짜에 대하여 다시 호출해서 화면을 갱신한다

 

- ProgressBar

사실 이름 때문에 처음엔 뭔가 작업을 진행중일 때 써야 하는 뷰인줄 알았는데, 그냥 게이지나 바 형식의 인터페이스가 필요하면 갖다쓸 수 있더라.

XML에서 progress 속성으로 초기 값을 %로 지정해줄 수 있고, setProgress() 함수를 이용해서 동적으로 바꿔줄 수 있다.

이 앱에선 앱을 실행할 때 DB에서 유저정보의 경험치 값을 가져와서 setProgress() 로 세팅하고, 일정이 종료되어 경험치 정산을 할 때도 오른 경험치를 반영하기 위해 또 setProgress() 를 사용한다.

 

5) DB 관련

- insertOrThrow() 메서드

지금까지 DB에 값을 insert할 때 꾸준히 써왔던 코드는 이거였다.

 

DBHelper helper = new DBHelper(MainActivity.this, "data.db", null, 1);
SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
(values.put() 을 이용해 값들을 넣은 뒤)
db.insert(tbl, null, values);

그런데 이 insert() 함수는 실행이 되더라도 다른 곳에 뭔가 문제가 있어서 실제 DB 테이블에 값이 삽입되지 않는 경우가 있다. 그래서 좀 헤맸는데 이를 보완한 메서드가 있었다. insertOrThrow() 메서드를 insert() 대신 쓰면 이런 경우에 Exception을 던져주고 무슨 문제가 있었는지 알려주므로 디버깅용으로 유용한 메서드다.

 

- db.rawQuery() 메서드: WHERE문에 변수 넣고 싶을 때

 

db.rawQuery("SELECT name, checkflag, exp FROM todolist WHERE date = ?", new String[] {current_date});

지금까지 사실 이 메서드랑 WHERE문을 같이 쓸 일이 없어서 두번째 인자가 무슨 역할을 하는건지 몰랐었는데, 알고보니 ?에 변수를 넣어줄 수 있는 부분이었다. 이렇게 하지 않고 아래와 같이 쓰면 제대로 동작하지 않더라.

db.rawQuery("SELECT name, checkflag, exp FROM todolist WHERE date = ", + current_date, null);

 

- insert or ignore into 구문: 기본적으로 insert를 하되, 삽입할 수 없을 경우엔 무시하는 SQL 구문.

테이블 설계상 메모나 약 테이블의 경우 날짜별로 1개의 레코드가 존재해야 하는데, 미리 모든 날짜 (?) 에 대한 레코드를 하나씩 넣어놓는 것은 이상하다고 생각했다. 필요한건 '그 날짜에 처음 실행했을 경우에만 레코드를 삽입하고, 그 외의 경우엔 삽입하지 않는' 기능인데 이 문제를 해결해줄 수 있는 구문이 insert or ignore into이다.

날짜 (date) 를 Unique 또는 Primary Key로 두면 한번 삽입한 이후로 또 삽입하려고 하면 오류가 날 것이다. 이런 경우 알아서 ignore를 해주므로 결과적으로 날짜별로 1개의 레코드만 넣게 되는 것이다.

 

6) 디버깅 이슈

원래 DB 테이블에 값이 제대로 들어갔는지 확인하기 위해서, (폰을 컴퓨터에 연결한 다음) 안드로이드 스튜디오의 Device File Explorer 기능을 이용해서 DB 파일을 열어보는 방법을 사용해 왔었다. 근데 얼마전에 안드로이드 업데이트를 해서 그런지 이게 안 됐다. 삼성에서 이런 식으로 앱 내부의 파일에 접근하는 걸 막아놓은 모양이더라.

그래서 DB 파일을 열어볼 수 있는 다른 방법을 찾아봤는데, 이전보단 많이 번거로운 과정을 거쳐야 한다. 안드로이드 스튜디오에 내장된 패키지 중 ADB (Android Debug Bridge) 라는걸 이용한다.

 

- ADB를 환경변수에 등록한다. 실행파일 경로는 '안드로이드 SDK 경로/platform-tools' 이다.

- 콘솔에서 adb backup -f data.ab (패키지명) 을 입력한다. 폰 화면에 백업 메시지가 뜬다. 비밀번호 입력후 백업.

- data.ab라는 파일이 콘솔상 현재 디렉토리에 생성된다. ab는 Android Backup File이다.

- 이거 콘솔에서 푸는 명령어란걸 찾아봤는데 윈도우에선 제대로 안 먹혔다. 그래서 Android Backup Toolkit이라고 누가 만든 프로그램을 받아서 썼다. 이걸 쓰면 data.ab 파일을 tar 파일로 변환할 수 있고 이 tar 파일을 압축풀면 db파일을 확인할 수 있다.

 

7) 그 외 메모

- 중간에 있는 '일정 입력' EditText 관련

> '일정 입력' 이라고 미리 회색 글씨로 써있게 하는 걸 Hint Text나 Placeholder Text라고 하는데, XML에서 'hint' 속성으로 설정해줄 수 있다. 

> 일정이 두 줄을 넘어갈 일은 어차피 없을테고 두 줄 넘게 입력을 하면 ListView가 보기 추해지기 (...) 때문에 한 줄만 입력할 수 있도록 하고 싶었다. 원래 더 직관적인 singleLine인가 하는 속성도 있었다는데 지금은 지원이 종료됐다고 들었던 것 같다. 대신 XML에서 inputType 속성을 'text' 로 설정해주면 된다.

 

- 맨 아래의 약 버튼 관련

약을 먹고 버튼을 클릭하면 그 버튼이 어둡게 변하던가 해서 이미 약을 먹었다는 걸 표시하고 싶다. 버튼의 색을 바꿔주려면 BackgroundTint 라는 속성을 바꿔주면 되는데, 액티비티에서 이걸 바꿔주기 위한 메서드를 찾아보니 setBackgroundTint'Mode' 라는게 있다. 이거 쓰는 법을 찾아보니까 뭔가 바로 이해가 안돼서 포기하고 그냥 일단 setText로 텍스트가 '먹었어요' 로 바뀌게 만들어 놨다. 저 메서드 어떻게 쓰는건지 다시 알아봐야 할 것 같다.

 

- 변수의 위치

안드로이드 프로그래밍을 하다보면 액티비티도 클래스라는 사실을 자주 까먹게 된다. 그래서 변수를 만들다보면 어떤 변수는 액티비티 클래스의 멤버 변수로 들어가있고, 어떤 변수는 onCreate() 안의 지역 변수로 들어가있고 그런 일이 자주 발생했다. 그러다보면 코드가 지저분한건 둘째치더라도 지역 변수를 바꿔놓고 왜 다른 함수에서 값을 못 가져오는지 고민하고, 왜 변수를 final로 바꾸라느니 하는 경고문이 뜨는지 고민하고... 그렇게 되더라. 한가지 기억해둘 것은 액티비티에서 계속 쓰이고 변경되는 변수는 클래스 멤버 변수로 넣자는 것이다. 

 

8) 보완해야 할 점 (2019/08/11)

- 아직 쿠폰 기능은 안 넣었다. 레벨/경험치 부분을 클릭하면 팝업창에서 남은 쿠폰의 수를 보여주고 쿠폰을 사용할 수 있는 인터페이스가 나오도록 해야 할 것.

- 위에서도 언급했던 Button.setBackgroundTintMode() 메서드의 사용 방법에 대해 알아보자.

- 메모를 할 때 EditText를 누르면 키보드가 올라오는데, 메모 EditText가 키보드에 가려서 안 보이는 문제점이 있다. 아주 불편한 UI다. 임시방편으로는 메모 구역이랑 일정 구역의 위치를 서로 바꿔서 EditText만큼은 키보드에 가려지지 않도록 만들어주는 방법도 있겠지만 이 앱의 메인 기능이 일정 리스트를 관리하는 것임을 생각하면 좀 그렇다. 근본적으로 해결하려면 키보드가 떴을 때 EditText를 임시로 위로 올려주거나 (?) 해야 할텐데 그걸 어떻게 해야하는지 찾아보자.

- 위에서 변수 위치 얘기했던 거 포함해서 코드가 굉장히 지저분하다. 어떻게든 돌아가는 것에 초점을 맞췄기 때문이다. 물론 나 혼자 쓸 앱이긴 하지만 문제는 내가 이런걸 했다고 GitHub에라도 올리려니까 도저히 쪽팔려서 (...) 못 올리겠다... 한번 코드를 좀 깔끔하게 수정, 그러니까 리팩토링을 해보자. 일단 직관적으로 생각나는 것부터 학부 때 배웠던 리팩토링 이론까지 적용해볼 수 있으면 해보자.

 

(8/23 수정)

- EditText 눌렀을 때 올라오는 키보드가 EditText를 가리는 문제 해결

매니페스트 파일에서 액티비티 옆에 다음과 같은 속성을 추가해주면 EditText의 커서에 맞춰서 화면이 자동으로 올라간다. 이 속성에 대한 자세한 정보는 공식 문서를 참고하자.

 

<activity android:name=".MainActivity" android:windowSoftInputMode="adjustPan">

근데 그냥 이것만 추가해주면 어쨌든 입력 커서를 가리지는 않지만 입력 커서 바로 밑에 키보드가 있어서 좀 답답한 느낌이 있다. 공간을 좀 더 확보해주려면 EditText에다가 layout:paddingBottom 속성으로 padding 값을 좀 주면 된다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
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
글 보관함