개인과제의 늪(2)
개인 과제의 늪에 빠진지 이틀차이다. 짜증난다. 안풀린다. 검색해도 안나온다. 로직 짜는게 너무 어지럽다. 이런 생각이 점점 드는 하루였다. 하지만 끝까지 어찌어찌 해내가는 모습이 보였다. 나는 이제 액티비티로 화면전환하고 각각 맞는 데이터를 디테일 페이지에 넘겨주는 부분까지 마무리 하였다. 이제는 기능을 강화하면서 없는 기능들을 구현하면 되는데 메인 -> 디테일(데이터 파싱) 부분은 끝냈고 이제 남은 부분은 리스트를 꾹 눌러서 삭제하는 기능과 좋아요 기능 그리고 스크롤을 아래로 했을때 플로팅 버튼 하나로 최상위까지 올라가는 부분을 구현해야하는 부분이 남았다. 그럼 내가 했던 부분을 하나씩 봐보자.
물론 구현하다가 금방하게 된것도 있으니 천천히 살펴보자.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var productAdapter: ProductAdapter
private lateinit var notificationHelper: Notification
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
notificationHelper = Notification(this)
binding.localBtn.setOnClickListener {
val fragment = LocalFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.frameLayout, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
val productManager = ProductManagerImpl.getInstance()
val products = productManager.getProducts()
productAdapter = ProductAdapter(this, products)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = productAdapter
val notifiBtn: ImageView = findViewById(R.id.notifi_btn)
notifiBtn.setOnClickListener {
notificationHelper.showNotification()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d("Debug", "MainActivity - onActivityResult: requestCode=$requestCode, resultCode=$resultCode")
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ProductAdapter.DETAIL_REQUEST_CODE && resultCode == RESULT_OK) {
val itemIndex = data?.getIntExtra("itemIndex", -1)
val isLiked = data?.getBooleanExtra("isLiked", false)
if (itemIndex != -1 && isLiked != null) {
if (itemIndex != null) {
productAdapter.notifyItemChanged(itemIndex)
}
}
}
}
override fun onBackPressed() {
finish()
slideLeft()
}
}
일단 메인 페이지의 클래스와 캡쳐한 사진이다. 이건 더미 데이터로 리사이클러뷰를 이용해서 구현한 부분이다. 그럼 어댑터와 더미 데이터를 살펴보자.
class ProductManagerImpl private constructor(): ProductManager{
private val productList = mutableListOf<Product>()
init {
productList.add(
Product(
R.drawable.sample1,
"산진 한달된 선풍기 팝니다",
"이사가서 필요가 없어졌어요 급하게 내놓습니다",
"대현동",
1000,
"서울 서대문구 창천동",
13,
25,
false
)
)
productList.add(
Product(
R.drawable.sample2, "김치냉장고", "이사로인해 내놔요", "안마담", 20000, "인천 계양구 귤현동", 8, 28,false
)
)
productList.add(
Product(
R.drawable.sample3,
"샤넬 카드지갑",
"고퀄지갑이구요\n사용감이 있어서 싸게 내어둡니다",
"코코유",
10000,
"수성구 범어동",
23,
5,false
)
)
productList.add(
Product(
R.drawable.sample4,
"금고",
"금고\n떼서 가져가야함\n대우월드마크센텀\n미국이주관계로 싸게 팝니다",
"Nicole",
10000,
"해운대구 우제2동",
14,
17,false
)
)
productList.add(
Product(
R.drawable.sample5,
"갤럭시Z플립3 팝니다",
"갤럭시 Z플립3 그린 팝니다\n항시 케이스 씌워서 썻고 필름 한장챙겨드립니다\n화면에 살짝 스크래치난거 말고 크게 이상은없습니다!",
"절명",
150000,
"연제구 연산제8동",
22,
9,false
)
)
productList.add(
Product(
R.drawable.sample6,
"프라다 복조리백",
"까임 오염없고 상태 깨끗합니다\n정품여부모름",
"미니멀하게",
50000,
"수원시 영통구 원천동",
25,
16,false
)
)
productList.add(
Product(
R.drawable.sample7,
"울산 동해오션뷰 60평 복층 펜트하우스 1일 숙박권 펜션 힐링 숙소 별장",
"울산 동해바다뷰 60평 복층 펜트하우스 1일 숙박권\n(에어컨이 없기에 낮은 가격으로 변경했으며 8월 초 가장 더운날 다녀가신 분 경우 시원했다고 잘 지내다 가셨습니다)\n1. 인원: 6명 기준입니다. 1인 10,000원 추가요금\n2. 장소: 북구 블루마시티, 32-33층\n3. 취사도구, 침구류, 세면도구, 드라이기 2개, 선풍기 4대 구비\n4. 예약방법: 예약금 50,000원 하시면 저희는 명함을 드리며 입실 오전 잔금 입금하시면 저희는 동.호수를 알려드리며 고객님은 예약자분 신분증 앞면 주민번호 뒷자리 가리시거나 지우시고 문자로 보내주시면 저희는 카드키를 우편함에 놓아 둡니다.\\n5. 33층 옥상 야외 테라스 있음, 가스버너 있음\n6. 고기 굽기 가능\n7. 입실 오후 3시, 오전 11시 퇴실, 정리, 정돈 , 밸브 잠금 부탁드립니다.\n8. 층간소음 주의 부탁드립니다.\n9. 방3개, 화장실3개, 비데 3개\n10. 저희 집안이 쓰는 별장입니다.",
"굿리치",
150000,
"남구 옥동",
142,
54,false
)
)
productList.add(
Product(
R.drawable.sample8,
"샤넬 탑핸들 가방",
"샤넬 트랜디 CC 탑핸들 스몰 램스킨 블랙 금장 플랩백 !\n + \"n\" + \"색상 : 블랙\n\" + \"사이즈 : 25.5cm * 17.5cm * 8cm\n\" + \"구성 : 본품더스트\n\" + \"\n\" + \"급하게 돈이 필요해서 팝니다 ㅠ ㅠ",
"난쉽",
180000,
"동래구 온천제2동",
31,
7,false
)
)
productList.add(
Product(
R.drawable.sample9,
"4행정 엔진분무기 판매합니다.",
"3년전에 사서 한번 사용하고 그대로 둔 상태입니다. 요즘 사용은 안해봤습니다. 그래서 저렴하게 내 놓습니다. 중고라 반품은 어렵습니다.\n",
"알뜰한",
30000,
"원주시 명륜2동",
7,
28,false
)
)
productList.add(
Product(
R.drawable.sample10,
"셀린느 버킷 가방",
"22년 신세계 대전 구매입니당\n + \"셀린느 버킷백\n\" + \"구매해서 몇번사용했어요\n\" + \"까짐 스크래치 없습니다.\n\" + \"타지역에서 보내는거라 택배로 진행합니당!\"",
"똑태현",
190000,
"중구 동화동",
40, 6,false
)
)
}
override fun getProducts(): List<Product>{
return productList
}
fun addProduct(product: Product){
productList.add(product)
}
companion object{
private var instance: ProductManagerImpl? =null
fun getInstance(): ProductManagerImpl {
if(instance == null){
instance = ProductManagerImpl()
}
return instance!!
}
}
}
더미 데이터이다. 아직은 API 데이터를 파싱해서 하는 정도까지는 진도가 안나가서 더미 데이터로 대신 리스트를 구현하는 부분이고 이렇게 더미 데이터 클래스를 만들어 주고 인터페이스를 만들어줘서 list에 접근할 수 있도록 하게 하는것이 싱글톤에 대한 목적을 가지고 있고 리스트를 어디서든 쓰게 될 수가 있게 된다. 아무튼 이렇게 더미 데이터를 이용해서 리스트를 구현할 수 있었다.
class ProductAdapter(private val context: Context, products: List<Product>) :
RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() {
private val productList: MutableList<Product> = products.toMutableList()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ProductAdapter.ProductViewHolder {
val binding = ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ProductViewHolder(binding)
}
override fun onBindViewHolder(holder: ProductAdapter.ProductViewHolder, position: Int) {
Log.d("Debug", "ProductAdapter - Clicked item index: $position")
val currentProduct = productList[position]
holder.bind(currentProduct)
holder.itemView.setOnClickListener {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra("productImage", currentProduct.imageFileName)
intent.putExtra("productName", currentProduct.productName)
intent.putExtra("productInfo", currentProduct.productInfo)
intent.putExtra("userName", currentProduct.sellName)
intent.putExtra("productPrice", currentProduct.price)
intent.putExtra("userLoc", currentProduct.address)
intent.putExtra("itemIndex", position)
intent.putExtra("isLiked", currentProduct.isLike)
(context as? Activity)?.startActivityForResult(intent, DETAIL_REQUEST_CODE)
}
}
override fun getItemCount(): Int {
return productList.size
}
fun getItem(position: Int): Product {
return productList[position]
}
fun getItemIndex(product: Product): Int {
return productList.indexOf(product)
}
inner class ProductViewHolder(private val binding: ItemViewBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(product: Product) {
binding.productImage.setImageResource(product.imageFileName)
binding.productName.text = product.productName
binding.productloc.text = product.address
val priceFormat = NumberFormat.getNumberInstance(Locale.getDefault())
val formattedPrice = priceFormat.format(product.price)
val priceText = "$formattedPrice 원"
binding.productPrice.text = priceText
binding.comentTx.text = product.chatCount.toString()
binding.favariteTx.text = product.likeCount.toString()
if (product.isLike) {
binding.likeBtn.setImageResource(R.drawable.like_fill)
} else {
binding.likeBtn.setImageResource(R.drawable.like_btn)
}
binding.likeBtn.setOnClickListener {
if (!product.isLike) { // 좋아요 상태가 false일 때만 처리
product.isLike = true
product.likeCount++
binding.likeBtn.setImageResource(R.drawable.like_fill)
} else {
product.isLike = false
product.likeCount--
binding.likeBtn.setImageResource(R.drawable.like_btn)
}
}
}
init {
binding.root.setOnLongClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val currentProduct = productList[position]
showDeleteConfirmationDialog(currentProduct, position)
}
true
}
}
private fun showDeleteConfirmationDialog(product: Product, position: Int) {
val builder = AlertDialog.Builder(binding.root.context)
builder.setTitle("삭제하기")
.setMessage("${product.productName}을(를) 삭제하시겠습니까?")
.setPositiveButton("삭제하기") { _, _ ->
productList.removeAt(position)
notifyItemRemoved(position)
}
.setNegativeButton("취소", null)
.show()
}
}
companion object {
const val DETAIL_REQUEST_CODE = 0 // 원하는 값으로 설정
}
}
이렇게 어댑터도 확인을 할 수가 있다. 이렇게 해주면서 알게 데이터 이동에 대해서 더 자세하게 알게 되었던것 같다.
onBindViewHolder에 디테일뷰로 넘어가는 데이터들을 intent로 넘겨주면 리스트를 클릭했을때 해당 데이터들이 이렇게 디테일 엑티비로 넘어가게 되었다. 그러면 구현한 화면을 보자.
이렇게 더미 데이터를 활용해서 데이터 클래스를 만들고 그 데이터 틀래스들을 어댑터에서 선언해줌으로써 리스트에 해당 데이터들을 띄울 수 있었고 이렇게 띄운 데이터들은 어댑터에서 그에 맞는 데이터들을 디테일 페이지에서 intent해줌으로써 데이터데 대응되게 디테일 페이지를 띄워줄수 있었다. 그러면 추가적으로 구현한 구분들을 살펴보자.
이것은 프래그먼트로 왼쪽 상단에 동네설정을 할것 같은 버튼을 클릭하면 이렇게 하단 프래그먼트로 내 동네 선택이라는 화면이 뜬다. 이는 지금 보여지고 있는 메인 엑티비티에 framlayout을 걸어주면서 사용할 수 있다. 메인 엑티비티 위에서 사용할 프래그먼트이기 때문에 각각의 엑티비티에 대응이 될 수 있게 메인 Xml 파일 전체를 framlayout으로 감싸준다. 메인 xml을 감싸 주면서 이 프래그먼트가 메인 엑티비티위에 뜨게 할 수 있게 하는것 같다. 아직 프래그먼트를 정확하게 숙지하고 한 부분이 아니라 화면 위에만 뜨면 된다고 생각을 했기 때문에 이렇게 구현하였고 동네 선택에 있어서 버튼들은 아직 기능이 없는 그냥 버튼들이다.
프래그먼트의 특성
이런식으로 프래그먼트를 만들때 내가 알게된 사실이 하나가 있다. 이 프래그먼트들은 배경색을 지정해주지 않으면 투명색 이라는점이다.
그런 특징을 이용해 내가 하단에 띄워주고 싶은 디자인을 레이아웃으로 감싸고 뒤쪽 배경은 흐리게 처리를 하기 위해 레이아웃에 색을 넣어주면 이렇게 투명한 배경이 적용이된 프래그먼트를 띄워줄 수 있었다. 이렇게 띄워줄때 처음 겪었던 문제가 이 프래그먼트라는건 혼자 존재를 할 수 있긴하지만 이렇게 버튼을 눌렀을때 액티비티 위에 프래그먼트를 띄워주려면 이렇게 하는게 맞는것 같다. 그럼 한번 xml 파일을 살펴보자.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LocalFragment"
android:background="#6FFFFFFF"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="408dp"
android:layout_height="168dp"
app:layout_constraintBottom_toBottomOf="@+id/textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView5"
tools:ignore="MissingConstraints"
android:background="#FF9E80"/>
<TextView
android:id="@+id/textView5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="내 동네 선택"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="68dp"
android:hint="동네는 최소 1개 이상 2개까지 설정가능"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/button2"
android:layout_width="159dp"
android:layout_height="43dp"
android:layout_marginEnd="28dp"
android:layout_marginBottom="16dp"
android:text="원당동"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:layout_width="159dp"
android:layout_height="43dp"
android:layout_marginStart="28dp"
android:layout_marginBottom="16dp"
android:text="두정동"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toStartOf="@+id/button2"
app:layout_constraintHorizontal_bias="0.287"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="근처 이웃들과 이야기를 나눌 수 있어요"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
이런식으로 컨스트 레이아웃으로 처음 설정해준다음 내가 띄우고 싶은 영역은 다시 하나의 레이아웃으로 감싸준다면 이렇게 해당하는 디자인의 화면을 띄울 수 있었다. 배경화면이 투명하다는 점을 이용해서 제일 상단의 레이아웃에 흐린 색감을 넣어준다면 완성이 되는것이다.
저번 포스팅에서는 엑티비티 -> 프래그먼트를 구현한다음 데이터의 이동을 아직 능숙하게 하지 못하여서 다시 엑티비티로 전환 하는 과정을 이어갔지만 다시 엑티비티로 바꾸면서 수월하게 데이터를 엑티비티간 전환으로 다룰 수 있었다. 다음에는 이런 점을 이용해서 데이터를 실제로 전달하고 받는 부분에 대해서 더 학습하면 될것같다!
알림기능
다음은 알람기능이다 오른쪽 상단에 종 모양의 버튼을 누르면 알림이 뜬다. 원래는 동기적으로 새로 추가된 상품에 대해서 알림을 띄워주려 했지만 지금 있는 화면과 기능에서는 그냥 종을 누르면 아무런 사진이 알림으로 뜨게 구현을 해보았다. 생각보다 간단하게 구현을 할 수있는데 메인 엑티비티의 코드를 보면 엥 알림 기능이 없는데 할 수가 있다. 이건 내가 메인 엑티비티를 깔끔하게 쓰고 싶어서 따로 클래스를 만들어서 뺐다.
class Notification(private val context: Context) {
fun showNotification() {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val builder: NotificationCompat.Builder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "one-channel"
// ... (채널 설정 등)
builder = NotificationCompat.Builder(context, channelId)
} else {
builder = NotificationCompat.Builder(context)
}
val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.sample4)
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 알림의 기본 정보
builder.run {
setSmallIcon(R.mipmap.ic_launcher)
setWhen(System.currentTimeMillis())
setContentTitle("새로운 물건이 떴네요")
setContentText("구매하실건가요?")
setStyle(
NotificationCompat.BigTextStyle()
.bigText(
"새로운 물건을 둘러보실라면 들어오세요"
)
)
setLargeIcon(bitmap)
setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(bitmap)
.bigLargeIcon(null)
) // hide largeIcon while expanding
addAction(
R.mipmap.ic_launcher,
"Action",
pendingIntent
)
}
val notificationId = 1 // 알림의 고유한 ID
manager.notify(notificationId, builder.build()) // 알림 표시
}
}
이런식으로 알람의 기능을 구현한 메소드를 따로 클래스로 빼서 관리를 하게 하였고 이렇게 관리하면 필요한 부분에서 버튼에 이 메소드만 연결해주면 어떤 버튼에서나 알림 기능을 쓸 수 있게 되는것이다. 하지만 그래도 클래스가 늘어나고 아직은 뺼 필요가 없었으나 그냥 내가 보기 편하게..빼놓았다. 그럼 다음 포스팅에서는 구현의 마지막 부분을 설명하면서 개인 과제에 대한것들을 마무리 하도록 하겠다.
마무리하며
이번 과제를 하면서 많이 어려웠던 부분도 있었고 프래그먼트로 바꾸고 엑티비티로 바꾸고 데이터를 다시 연결하고 그런 점에서 삽을 팠던 부분도 많았지만 어찌어찌 원하는 결과에 대해서 도달을 하게 된것 같다. 삽을 파긴 했지만서도 이렇게 엑티비티로 전환해주면서 다시금 엑티비니의 특성에 대해서 알게되었고 다시 데이터 클래스를 만들면서 싱글톤에 대해서 생각해 볼 수 있었고 뷰홀더에 어떤 데이터를 넣어야하는지도 알게 되었던 시간들이였다. 이렇게 회고를 하면서 내가 했던 부분에 대해서 다시 생각하는 시간을 갖도록 꾸준히 나아가보자!