사전준비
주제 정하기
SNS앱 프로젝트가 시작되고 주제는 생각보다 빨리 정해졌다. 첫 아이디어를 내주신 팀원분께서 음식을 말씀하셨는데 자료 찾기도 수월하고 사용자 타겟도 까다롭지 않다는 강점이 마음에 들어 적극찬성한 결과, 메뉴를 소개하는 앱으로 결정했다. 처음에는 맛집을 소개하는 방향이었는데 음식을 추천하는 앱으로 교정했다. 점메추처럼
와이어프레임 만들기
프로젝트명과 역할분담은 화면 구성에 따라 어떤 기능이 들어갈지 알 수 있으므로 와이어프레임부터 만들어보며 결정하기로 했고 githup과 함께 pigma에 초대됐다. API는 4로 설정했고 pigma는 아이폰규격이라 안드로이드와 달라서 가장 유사한 규격에 제작하고 안드로이드 스튜디오에서는 참고해서 만들면 된다고 한다. 각 화면을 구분하고 모든 팀원이 자유롭게 구성하며 컬러, 화면별 기능 등 의견을 활발히 주고받았다. 난 이때부터 넘 신났다
프로젝트명과 역할 정하기
프로젝트명은 내가 임시로 쓸 로고를 디자인했는데 우리 조 이름이 밥조이기도 하고 모든 조원 이름에 J가 들어가서 메뉴조MENUJO로 만든게 그대로 로고가 됐고 프로젝트명이 됐다. 담당은 회원가입 페이지 디자인하다가 선호하는 음식 입력 받는 거 어떠냐고 의견을 냈더니 입력 받아서 메뉴 추천이나 마이 페이지에도 적용하면 재밌겠다고 반응이 좋았다. 어떻게 입력 받을거냐는 질문에 라디오버튼으로 입력 받자고 했는데 안써봐서 잘 모른다고 주춤하길래 내가 하겠다고 해서 로그인 페이지 SignInActivity와 회원가입 페이지 SignUpActivity 담당이 됐다.
Git & Github
팀원분께서 하나하나 친절히 알려주신 덕분에 팀장님이 올려주신 첫 프로젝트를 clone해서 feature/signin으로 local branch를 만들고 시작했다. branch는 총 3개로 구분된다.
- main : 최종본. 건들지 않는다.
- develop : 임시완성본. 문제 없으면 local을 여기로 병합하고, 다른 사람이 병합하면 local로 가져온다.
- local : 개별 원본. develop으로 merge하고나면 삭제해야한다.
프로젝트의 시작
팀장이 main을 만들고 사본인 develop을 만들면 팀원이 clone으로 시작한다.
1. local에서 제작한다.
2. origin local에 push한다.
3. develop으로 pull request한다.
4. 문제 없으면 develop에 merge한다.
처음에 default branch로 develop이 아닌 main으로 되어있어 위험했다.
주고 받고 push & pull
1. add하고 commit으로 local에 먼저 저장한다.
2. pull로 다른사람이 merged한 코드를 가져온다.
만약 내가 수정한 사항을 저장하지 않았는데 다른사람이 갱신한 내용을 가져오거나 다른 사람이 합쳐놨는데 내가 따로 또 추가하면 충돌이 일어나거나 제대로 합쳐지지 않는다. 꼬이면 깃헙이나 터미널에서 제법 번거로워지므로 안전하게 push하기 전에도 add commit pull해보는 습관을 들였다.
Github컨벤션
문제해결은 fix, 제작은 feat, 그외 수정은 chore로 commit 메시지를 작성하며 pull로 확인하고 origin local로 push한 뒤 문제 없으면 origin develop으로 merge했다. 초반에는 어차피 팀원들과 계속 대화, 채팅, 라이브코딩으로 공유하는데 commit 메시지가 왜 필요한지 의문이었는데 정리할 때도 그렇고 내가 필요한 부분 찾아볼 때도 그렇고 유용하다.
commit 메시지로 정리한 제작과정
2024. 07. 03 : SignInActivity 생성 - 로고 이미지파일 3개 업로드 - 로그인페이지 xml 제작 - SignUpActivity 생성 - string파일로 관리 - 회원가입 버튼으로 intent(SignUpActivity, SignInActivity 액티비티 간 이동) 구현 - 로그인 버튼으로 SignUpActivity에서 MainPageActivity intent 구현
2024. 07. 04 : RadioButton CheckBox로 변경 - CheckBox 예외처리 구현 - 회원가입시 미입력 항목별 toast메시지 구현 - 로그인시 미입력 항목별 toast메시지 구현 - 테마변경, 다크모드 확인, 다국어 지원을 위한 영어string 작성 - 레이아웃별 landscape 구현
회원가입 페이지 SignUpActivity
회원가입 페이지는 사용자의 닉네임, 아이디, 비밀번호, 선호하는 음식 취향 총 4가지의 답변을 요구한다. 닉네임, 아이디, 비밀번호는 EditText로 사용자에게 입력받고 선호음식은 취향에 따라 최소 1개부터 최대 3개까지 선택하여 답변한다.
처음엔 라디오 버튼으로 정했는데 라디오 그룹은 그룹 내 버튼 중에 하나만 선택가능하고 라디오 버튼을 각각 사용한다해도 버튼 취소가 되지 않아 목적에 부합하지 않았다. 이에 따라 체크박스로 변경했다.
CheckBox
체크박스는 이번이 처음이기도 하고 최대 3개만 선택 가능하도록 예외처리하는 게 어려웠다.
cbList.forEach {
it.setOnCheckedChangeListener { _, ischecked ->
if (it.isChecked) {
tagsData += it.text.toString()
if (tagsData.size == 3) {
Toast.makeText(
this,
getString(R.string.toast_signup_favorite_max3),
Toast.LENGTH_SHORT
).show()
for (i in cbList) if (!i.isChecked) i.isEnabled = false
}
} else {
tagsData -= it.text.toString()
for (i in cbList) if (!i.isChecked) i.isEnabled = true
}
}
}
처음에는 cbCount라는 변수를 만들어 개수를 셌는데 마이페이지에 태그를 넘길 수 있도록 String 리스트인 tagData로 변경됐다. cbList 체크박스를 리스트에 담고 체크 될 때마다 현재 체크된 체크박스의 태그를 tagData 리스트에 담는다. 태그리스트 사이즈가 3이 되면 토스트 메시지를 띄우고 체크박스 리스트 내 나머지 체크박스들을 비활성화하도록 했다. 체크를 취소하면 다시 활성화 된다.
트러블슈팅
문제 : 선택했다가 모두 취소하면 최소 1개의 조건을 만족하지 못했는데도 회원가입이 된다.
원인 : 취소한 태그가 리스트에 남아있어 버튼이벤트의 조건인 사이즈 == 0 에 false이다.
tagsData.size == 0 -> Toast.makeText(
this,
getString(R.string.toast_signup_favorite_min1),
Toast.LENGTH_SHORT
).show()
해결방안 : 체크를 취소하면 태그도 리스트에서 빼주도록 했다.
이벤트 처리방식
초반에는 익숙한 setOnClickListener로 작성했는데 문제없이 작동하길래 그대로 쓰려다가 setOnCheckedChangeListener를 써보니 view를 처리하는 차이가 있다. setOnCheckedChangeListener는 checkbox를 람다식 내에 it으로 받아 보다 간결하게 처리할 수 있어서 변경했다.
checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
// 체크박스가 체크된 상태일 때 할 작업들
} else {
// 체크박스가 체크 해제된 상태일 때 할 작업들
}
}
디자인 변경
기본 체크박스 디자인이 마음에 들지 않아 변경할 방법을 찾아봤다.
1. selector : 버튼만 필요할 때 쓰면 좋은 방법. 글자가 안으로 들어가는 문제점이 있다.
<!-- CheckBox에 추가하는 코드 -->
android:button="@android:color/transparent"//또는 android:button="@null"
android:background="@drawable/custom_checkbox" />
<!-- selector에 추가하는 코드 -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Unchecked -->
<item android:state_checked="false">
<shape>
<stroke android:color="#" android:width="1dp" />
<corners android:radius="10dp" />
<solid android:color="#" />
</shape>
</item>
<!-- Checked -->
<item android:state_checked="true">
<layer-list>
<!-- 배경 -->
<item>
<shape>
<corners android:radius="10dp" />
<solid android:color="#" />
</shape>
</item>
<!-- 이미지 영역 -->
<item android:drawable="@drawable/이미지파일" />
</layer-list>
</item>
</selector>
2. style 또는 buttonTint : 글자는 그대로 있는데 체크 모양이 아니라 네모 박스가 칠해진다. API 21 이상은 android:buttonTint="컬러"로 API 21 미만은 아래 코드로 가능하다.
<!-- style에 입력하는 코드 -->
<style name="CheckBox" parent="Theme.AppCompat.Light">
<item name="colorControlNormal">#000000</item>
<!-- CheckBox에 입력하는 코드 -->
android:theme="@style/CheckBox" />
두 방법 모두 기본 디자인에서 나타나는 클릭 이펙트가 없고 비활성화 시 회색으로 변하는 enable 디자인이 없어 다른 방안을 찾아보고 있던 중, 팀원분께서 내 진행 상태를 여쭤봐주셨다. 해당 문제를 말씀드렸더니 1시간 동안 서칭해도 안나오던, 내가 바라던 답을 내려주셨다. 이때 소리 질렀다.
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.MenuJo" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="colorPrimary">@color/light_pink</item>
<item name="android:colorBackground">@color/white</item>
</style>
<style name="Theme.MenuJo" parent="Base.Theme.MenuJo" />
</resources>
테마에 colorPrimary로 색상만 변경하면 되는 거였다. 더불어 테마가 적용되면 기본 배경이 white가 아니라는 점도 배웠고 다른 커스터마이징 방법별 차이도 알게 됐기에 체크박스를 통해 다양한 정보를 수집할 수 있었다.
Toast message
항목별 토스트메시지는 각각 조건별로 구성해두고 중복코드를 합쳤다. 이 부분은 다른 팀원분이 정규표현식으로 예외처리를 담당하면서 when문으로 변경됐다.
string파일로 따로 관리하는 것도 처음이라 조합도 해보고 메시지를 변수에 담아 중복을 줄여봤다. xml과 코틀린 파일은 참조하는 방법 또한 주석처럼 차이가 있어서 처음에 좀 헤맸다.
xml : android:text="@string/스트링이름"
코틀린 : getString(R.string.스트링이름)
btnSignUp.setOnClickListener {
var toastSignUp = ""
if (nameData.isBlank() || idData.isBlank() || pwdData.isBlank()) {
when {
nameData.isBlank() -> toastSignUp = getString(R.string.toast_signup_name)
idData.isBlank() -> toastSignUp = getString(R.string.common_set_id)
pwdData.isBlank() -> toastSignUp = getString(R.string.common_set_pwd)
}
Toast.makeText(this, "$toastSignUp", Toast.LENGTH_SHORT).show()
}
else {
Toast.makeText(this,getString(R.string.common_signup) + getString(R.string.common_finish), Toast.LENGTH_SHORT).show()
val intent = Intent(this, SignInActivity::class.java)
finish()
}
}
회원가입, 로그인에서 공통으로 사용되거나 또 다른 activity에서도 사용되는 문자들은 하나로 쓰면 좋을 것 같아 의견을 내보니 팀원분께서 보통 실무에서는 common으로 빼 중복코드를 관리한다고 알려주셔서 최상단으로 옮겼다.
string파일 내에서 작성할 때 중간에 공백은 그대로 반영 되는데 맨 앞에 공백을 추가하려면 앞에  를 붙이면 된다.
<!-- common : any Activity -->
<string name="common_name">닉네임</string>
<string name="common_id">아이디</string>
<string name="common_pwd">비밀번호</string>
<string name="common_set_id">아이디를 입력해주세요</string>
<string name="common_set_pwd">비밀번호를 입력해주세요</string>
<string name="common_signin">로그인</string>
<string name="common_signup">회원가입</string>
<!-- white space -->
<string name="common_finish"> 완료</string>
Photo picker
카메라나 갤러리에서 사진을 불러올 수 있는 방법으로 팀원분께서 작성하시고 알려주셔서 구현했다. 카메라나 갤러리에서 코드를 받으면 이미지 uri를 가져올 수 있고 uri를 회원정보 클래스에 넘겨줘서 마이페이지에서 꺼내올 수 있다. 이전에 배웠던 런처의 다른 행동양식을 쓸 수 있어 반가웠다. 이 부분은 추가적으로 따로 공부해볼 예정
//Gallery image upload
val pickMedia =
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
uri?.also { imageUri ->
findViewById<ImageView>(R.id.iv_signup_image)?.apply {
setPadding(0)
setImageURI(imageUri)
}
profileImageUri = imageUri.toString()
contentResolver.takePersistableUriPermission(
imageUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
}
fun openGalleryForImage() {
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
fun getGalleryImage() {
val ivUserImage = findViewById<ImageView>(R.id.iv_signup_image)
ivUserImage.setOnClickListener {
openGalleryForImage()
}
}
getGalleryImage()
로그인 페이지 SignInActivity
로그인페이지는 큰 특징이 없거나 회원가입페이지와 중복된 내용이라 따로 정리하지 않았고 7월 5일 목요일부터는 부재중이었던 팀원이 돌아와서 로그인 예외처리, 회원가입 실시간 예외처리, TextInputLayout 적용 등의 권한을 위임해 더더욱 정리할 필요를 못느꼈다가 7월 6일 금요일, 회원정보를 저장하게 되면서 새로 작성한 부분이 있다.
Object class로 회원정보를 저장하여 id, 비밀번호가 일치하면 로그인하고 그렇지 않으면 메시지를 띄운다.
if (UserManager.getUser(idData.toString()) == null) {
Toast.makeText(
this,
getString(R.string.toast_signin_non_user),
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
if (UserManager.getUser(idData.toString())?.userPwd != pwdData.toString()) {
Toast.makeText(
this,
getString(R.string.toast_signin_non_user),
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
landscape
가로모드는 new resource file - layout - orientation - ui mode - land 로 동일한 파일명의 새 파일을 만들면 세로모드와 함께 하나의 폴더로 관리된다. UI만 조금씩 손보면 되는거라 모든 페이지의 가로모드를 만들었다.
회고
시간에 쫓기고 쫓기고 또 쫓겼다.
첫 날은 괜찮았다. UI를 다 만들고 기능도 시작했고 이틀이면 충분하겠거니 했는데 둘째 날 실시간 강의를 2번 듣고 체크박스에 쏟아부었더니 저녁이 됐고 가로모드를 만들고 이것저것 만지작거리니 하루가 다 갔다. 셋째 날에는 정규표현식을 고쳐보겠다고 시간 낭비를 너무 많이했고 이때부터 발표준비를 했어야 됐는데 아무것도 못했다. 결국 주말에 피피티를 만들고 발표 당일 대본을 작성하며 너덜너덜해졌지만 발표는 성공적으로 마쳐서 다행이었다.
딱히 데드라인을 어긴 적은 없었으나 더 많이 더 잘 하고 싶은 욕심에 주어진 시간이 한없이 부족하게 느껴졌다. 팀원에 대한 이야기는 일기에 쓸 예정으로 보완점을 정리하고 마치겠다.
아쉬웠던 점
시간부족으로 못한 것들 : 속성요소에서 중복되는 모든 부분을 style로 합쳐서 관리, 로그아웃 기능 추가, 다크모드에서 툴바 색깔 변경, 영어버전에서 로그인 hint 잘리는 부분 수정
회원가입에서 버튼 클릭시 검사하는 게 아니라 실시간으로 검사 하도록 바꾸고 싶었는데 정규표현식이 내가 작성하던 방법과 달라서 건드리지 못했다.
다음에 적용할 점
다국어 지원은 string 파일만 따로 관리하면 되는 게 아니라 로직에서 String타입으로 받을 때 한 번 생각해봐야한다. 태그 리스트를 string으로 받았더니 마이페이지에서 꺼낼 때 영어까지 2중으로 작성하게 됐다. int로 처리하거나 map같은 컬렉션으로 처리해야한다.
가로모드나 다른 크기의 폰에서도 쉽게 UI를 적용시킬 수 있도록 위치의 기준이 되는 코드는 확실히 잡아두고 xml을 작성해야한다.
'안드로이드와 앱 > 프로젝트' 카테고리의 다른 글
팀프로젝트 [산타의 리스트] (0) | 2024.07.22 |
---|---|
개인 프로젝트 [에코마켓 앱] (0) | 2024.07.11 |
개인 프로젝트 [회원가입 앱] (0) | 2024.06.19 |
개인 프로젝트 [콘솔형 키오스크] (0) | 2024.06.11 |
개인 프로젝트 [콘솔형 계산기] (0) | 2024.06.05 |