SSAFYcial

[6월 기획 기사] 모바일 트랙을 진행한 결과물을 공개합니다.

five2week 2023. 6. 27. 01:44

안녕하세요. 9기 SSAFYcial 오희주입니다. 

앞으로 다음 기수에 모바일 트랙에 들어오고 싶은 분, 그리고 모바일 반이 궁금하신 분들을 위해서 모바일 트랙을 진행하며 나온 결과물을 공개합니다. 싸피에서 직접적으로 진행한 프로젝트 같은 결과물은 공개할 수 없기 때문에 배운 것을 복습하는 차원에서 진행한 사이드 프로젝트 코드를 가져왔습니다.

 

https://github.com/noion0511/Log.M

 

GitHub - noion0511/Log.M

Contribute to noion0511/Log.M development by creating an account on GitHub.

github.com

https://play.google.com/store/apps/details?id=com.likewhile.meme&hl=ko&gl=US 

 

Log.M - 할 일과 중요 사항 메모장 - Google Play 앱

할 일과 중요 사항을 저장해둘 수 있는 메모 앱

play.google.com

 

1학기를 마치며 버전을 2로 업데이트하게 되었습니다. 싸피를 다니며 남는 시간에 개발을 하다보니까 많은 기능을 추가하지는 못했지만, 많은 오류들을 잡고 기능을 추가하게 되었습니다.

 

 

LogM version2(7)의 추가 및 수정된 기능은 다음과 같습니다.

1. 처음 디비를 생성했을 때, 예시 메모를 생성하기

2. 리스트 메모의 기능 수정

- 리스트 아이템은 한줄만 작성이 가능하도록 수정

- 리스트 아이템 순서를 바꾸기 위해 길게 선택했을 시, 색 변경 추가

- 리스트 아이템 삭제 스와이프 시, 선택된 아이템 색 변경 추가

- 리스트 아이템 교환 기능 수정

- 리스트 메모 저장 애니메이션 추가

3. 메모에 사진 첨부 기능 추가

4. 메모 정렬 기능 중 시간에 따른 정렬 부분에서 단위를 날짜에서 시간으로 수정

5. 달력 모드에서 메모를 생성하지 않은 날에도 마커가 찍히는 오류 수정

6. 화면 가로세로 회전 막기



일단 메모 한 개가 기본으로 앱 다운했을 때, 화면에 아무것도 없는 상황보다 환영하는 메모가 하나 있는 것이 더 좋을 것 같다는 생각이 들어서 먼저 생성되어 있도록 수정했습니다.

class MemoDBHelper(val context: Context) :
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(CREATE_TABLE_SQL)

        val memoItem = TextMemoItem(title = "first Memo", content = "hello, world")
        insertMemo(memoItem, db)
    }
    
    ...
}

배웠던 ItemTouchHelper.Callback에 관한 코드를 복습하며, 작성 리스트의 아이템들의 순서를 바꾸고, 삭제할 수 있도록 했습니다. 이러한 기능이 있는 리스트를 보며 후에 제 앱에도 똑같은 기능을 넣고 싶다고 생각한 적이 있는데 이번에 실현하게 되었습니다.

class ListItemTouchHelperCallback(private val adapter: ListAdapter) : ItemTouchHelper.Callback() {
    private var enabled: Boolean = true
    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
    //리싸이클러뷰의 아이템 타입을 확인해서 버튼이 아니라 edittext면 교환 및 삭제가 가능
        return if (enabled && viewHolder.itemViewType == ListAdapter.ITEM_TYPE_NORMAL) {
            val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
            val swipeFlags = ItemTouchHelper.END
            makeMovementFlags(dragFlags, swipeFlags)
    //아이템 타입이 플러스 버튼이라면 마지막 위치에 고정
        } else {
            makeMovementFlags(0, 0)
        }
    }
    
    //아이템을 교환하는 함수
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
    	//교환하는 a와 b 중 플러스 버튼이 있다면 교환 금지
        if(target.itemViewType == ListAdapter.ITEM_TYPE_ADD_BUTTON || viewHolder.itemViewType == ListAdapter.ITEM_TYPE_ADD_BUTTON) {
            return false
        }
        
        //아니라면 리싸이클러뷰 오댑터의 onItemMove 함수를 실행하여 교환
        adapter.onItemMove(viewHolder.layoutPosition, target.layoutPosition)
        viewHolder.itemView.setBackgroundColor(ContextCompat.getColor(viewHolder.itemView.context, R.color.teal_700))
        return true
    }

	//선택된 리스트 아이템의 색을 변화
    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)
        viewHolder?.itemView?.setBackgroundColor(ContextCompat.getColor(viewHolder.itemView.context, R.color.teal_700))
    }

	//스와이프된 아이템을 삭제
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        val position = viewHolder.layoutPosition
        adapter.removeItem(position)
    }

	//소속된 아이템의 롱클릭 드래그를 허용
    override fun isLongPressDragEnabled(): Boolean {
        return true
    }

	//소속된 아이템의 스와이프를 허용
    override fun isItemViewSwipeEnabled(): Boolean {
        return true
    }

	//선택해제 아이템의 색을 원래대로 복구
    override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
        super.clearView(recyclerView, viewHolder)
        viewHolder.itemView.setBackgroundColor(Color.WHITE)
    }

	//수정모드와 읽기모드를 구분하는 트리거
    fun setEnabled(enabled: Boolean) {
        this.enabled = enabled
    }
}

이 외에도 그전까지는 앱이 세로에서 가로로 전환되면 기존의 화면을 지우고, 새 화면을 생성한다는 생각을 해본 적이 없었습니다. 그냥 화면이 가로에서 세로로 또는 그 반대로 전환되는거라고만 생각을 했고 그래서 앱이 강제종료되는 버그를 생성해냈습니다. 이 버그를 고치기 위해서 메모앱의 가로세로 회전을 막았습니다.

    <activity
        android:name=".ui.view.MemoEditActivity"
        android:exported="false"
        android:windowSoftInputMode="adjustResize"
        android:screenOrientation="portrait">
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value=".ui.view.MainActivity" />
    </activity>

    <activity
        android:name=".ui.view.MainActivity"
        android:exported="true"
        android:screenOrientation="portrait">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

그리고 바인딩을 위해 어댑터를 만들어서 사용했습니다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <import type="android.view.View" />
        <import type="com.likewhile.meme.data.model.TextMemoItem" />
        <variable
            name="memo"
            type="com.likewhile.meme.data.model.TextMemoItem" />
    </data>

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="2dp"
        android:elevation="2dp">

        <LinearLayout
            android:id="@+id/widget_memo"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/white"
            android:orientation="vertical"
            android:padding="16dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/textViewTitle"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="5dp"
                    android:layout_weight="1"
                    android:text="@{memo.title}"
                    android:textColor="@color/black"
                    android:textSize="16sp"
                    android:singleLine="true"/>

                <ImageView
                    android:id="@+id/fixedIconImageView"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical"
                    android:contentDescription="@string/fixed_icon_description"
                    app:srcCompat="@drawable/baseline_push_pin_24"
                    app:visibility="@{memo.isFixed ? View.VISIBLE : View.GONE}" />
            </LinearLayout>

            <TextView
                android:id="@+id/textViewContent"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{memo.content}"
                android:textColor="@color/black"
                android:textSize="14sp"
                android:maxHeight="200dp"/>

            <TextView
                android:id="@+id/textViewDate"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:formattedDate="@{memo.date}"
                android:textColor="@color/black"
                android:textSize="14sp" />

        </LinearLayout>
    </androidx.cardview.widget.CardView>
</layout>
@BindingAdapter("formattedDate")
fun TextView.setFormattedDate(date: Date?) {
    date?.let {
        text = DateFormatUtil.formatDate(it)
    } ?: run {
        text = ""
    }
}

더 많은 코드는 깃허브를 참고 바랍니다.

 

싸피에서 체계적으로 모바일 개발에 대해서 배우며 독학할 때와는 달리 점처럼 찍혀있던 지식들을 이어가며 학습할 수 있었습니다. 왜 이렇게 사용하는 가에 궁금했던 부분에 대해서도 배울 수 있었고, 혼자 공부하며 알아차리지 못한 오류가 발생하는 부분에 대해서도 알 수 있었습니다.

 

대학교 4년동안 키운 실력을 두배로 곱할 수 있었던 반년인 것 같습니다. 다른 분들에게도 싸피 모바일 트랙을 추천합니다.

싸피 모바일 트랙과 로그엠에 많은 관심 부탁드립니다 ~