3주차. FLO 앱 클론 코딩 - ViewPager & TabLayout

변현섭·2023년 10월 5일
1

5th UMC Android Study

목록 보기
3/10

✅ 3주차 목표

  • ViewPager의 개념을 이해하였고, 이를 사용할 수 있다.
  • ViewPager에 Indicator를 적용할 수 있다.
  • TabLayout의 개념을 이해하였고, 이를 ViewPager와 연결할 수 있다.

1. ViewPager 사용하기

① Module 수준의 build.gradle 파일에 아래의 의존성을 추가한다.

implementation ("androidx.viewpager2:viewpager2:1.0.0")

② fragment_home.xml의 배너는 현재 ImageView로 나타나있다. 이를 ViewPager로 바꿔주기 위해 주석으로 변경한다.

<!--<ImageView-->
	<!--android:id="@+id/home_banner_vp"-->
	<!--android:layout_width="match_parent"-->
	<!--android:layout_height="0dp"-->
	<!--android:adjustViewBounds="true"-->
	<!--android:src="@drawable/img_home_viewpager_exp"-->
	<!--android:layout_marginTop="15dp"-->
	<!--app:layout_constraintStart_toStartOf="parent"-->
	<!--app:layout_constraintEnd_toEndOf="parent"-->
	<!--app:layout_constraintTop_toBottomOf="@id/home_today_music_oversea_hs" />-->

③ 기존 ImageView가 있던 자리에 ViewPager 태그를 아래와 같이 추가한다.

  • ViewPager2를 사용하기로 한다.
<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/home_banner_vp"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_marginTop="15dp"
    app:layout_constraintEnd_toEndOf="@+id/home_pannel_btn_setting_iv"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/home_today_music_oversea_hs"/>

④ default 패키지 하위로, BannerVPAdapter라는 kotlin class를 추가한다.

  • Fragment를 입력 인자로 받는다.
  • ViewPagerAdapter는 Fragment 타입 인스턴스를 인자로 받는 FragmentStateAdapter()를 상속받아야 한다.
class BannerVPAdapter(fragment : Fragment) : FragmentStateAdapter(fragment) {

}

⑤ BannerVBAdpater에 커서를 올려두고, 우클릭한 후 Show Context Actions > Implement members를 클릭하면 override 메서드를 자동 생성해준다.

⑥ BannerVPAdapter에 아래의 내용을 입력한다.

class BannerVPAdapter(fragment : Fragment) : FragmentStateAdapter(fragment) {

    private val fragmentList : ArrayList<Fragment> = ArrayList()

    override fun getItemCount(): Int {
        return fragmentList.size
    }

    override fun createFragment(position: Int): Fragment {
        return fragmentList[position]
    }
    
    fun addFragment(fragment: Fragment) {
        fragmentList.add(fragment)
        notifyItemInserted(fragmentList.size-1)
    }
}
  • fragmentList: ViewPager2에 표시할 Fragment들을 저장하는데 사용할 리스트이다.
  • getItemCount(): ViewPager2에서 표시할 Fragment의 총 개수를 반환한다.
  • createFragment(): fragmentList의 해당 position에 위치한 Fragment를 반환한다.
  • addFragment(): fragmentList에 새로운 Fragment를 추가한다.
  • notifyItemInserted(): VPAdapter에 아이템이 추가되었음을 알린다. 여기서 fragmentList.size-1은 아이템이 추가된 위치를 나타낸다.

⑦ 이제 fragmentList에 넣어줄 Fragment를 만들어야 한다. default 패키지 하위로, BannerFragment를 추가한다.

⑧ fragment_banner.xml 파일에 아래의 내용을 입력한다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:id="@+id/banner_image_iv"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:scaleType="fitXY"
        android:src="@drawable/img_home_viewpager_exp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

⑨ ViewBinding을 사용하기 위해 Module 수준의 build.gradle 파일에 아래의 내용을 추가한다.

android {
	...
    buildFeatures{
        dataBinding = true // 데이터 바인딩
    }

    buildFeatures {
        viewBinding = true // 뷰 바인딩
    }
}

⑩ BannerFragment에 아래의 내용을 입력한다.

class BannerFragment(val imgRes : Int) : Fragment() {

    lateinit var binding : FragmentBannerBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentBannerBinding.inflate(inflater, container, false)
        binding.bannerImageIv.setImageResource(imgRes)
        return binding.root
    }
}
  • setImageResource(): drawable 디렉토리에 있는 리소스로 이미지를 설정할 때 사용한다. 참고로, setImageResource()의 인자 값은 리소스 ID로 Integer이다.

⑪ 이제 HomeFragment에서 ViewPager와 VPAdapter를 연결해야 한다.

val bannerAdapter = BannerVPAdapter(this)
bannerAdapter.addFragment(BannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(BannerFragment(R.drawable.img_home_viewpager_exp2))
binding.homeBannerVp.adapter = bannerAdapter
binding.homeBannerVp.orientation = ViewPager2.ORIENTATION_HORIZONTAL

return binding.root

이제 코드를 실행해보면 배너가 ViewPager로 표시되는 것을 확인할 수 있을 것이다.

2. ViewPager에 Indicator 적용하기

1) HomeFragment에 ViewPager 적용하기

① fragment_home.xml의 배경 패널은 현재 ImageView이다. 이 ImageView를 ViewPager로 바꿔주어야 하므로 기존 ImageView 태그를 삭제한다.

② 기존 ImageView가 있던 자리에 ViewPager 태그를 아래와 같이 추가한다.

  • ImageView 태그 삭제로 인해 발생하는 에러를 해결한다. (iv를 vp로 고쳐주기만 하면 된다.)
<androidx.viewpager2.widget.ViewPager2
	android:id="@+id/home_pannel_background_vp"
	android:layout_width="match_parent"
	android:layout_height="430dp"
	android:scaleType="centerCrop"
	app:layout_constraintEnd_toEndOf="parent"
	app:layout_constraintHorizontal_bias="0.0"
	app:layout_constraintStart_toStartOf="parent"
	app:layout_constraintTop_toTopOf="parent"/>

③ PannelVPAdapter라는 kotlin class를 추가한다.

class PannelVpAdapter (fragment : Fragment) : FragmentStateAdapter(fragment) {

    private val fragmentList : ArrayList<Fragment> = ArrayList()

    override fun getItemCount(): Int {
        return fragmentList.size
    }

    override fun createFragment(position: Int): Fragment {
        return fragmentList[position]
    }

    fun addFragment(fragment: Fragment) {
        fragmentList.add(fragment)
        notifyItemInserted(fragmentList.size-1)
    }
}

④ 이제 fragmentList에 넣어줄 Fragment를 만들어야 한다. PannelFragment를 추가하자.

⑤ fragment_pannel.xml에 아래의 내용을 입력한다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:id="@+id/pannel_image_iv"
        android:layout_width="match_parent"
        android:layout_height="430dp"
        android:scaleType="fitXY"
        android:src="@drawable/img_first_album_default"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

⑥ PannelFragment에 아래의 내용을 입력한다.

class PannelFragment(val imgRes : Int) : Fragment() {

    lateinit var binding : FragmentPannelBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentPannelBinding.inflate(inflater, container, false)
        binding.pannelImageIv.setImageResource(imgRes)
        return binding.root
    }
}

⑦ 마지막으로, HomeFragment에 ViewPager와 VPAdapter를 연결하는 내용을 추가한다.

val pannelAdpater = PannelVpAdapter(this)
pannelAdpater.addFragment(PannelFragment(R.drawable.img_first_album_default))
pannelAdpater.addFragment(PannelFragment(R.drawable.img_first_album_default))
binding.homePannelBackgroundVp.adapter = pannelAdpater
binding.homePannelBackgroundVp.orientation = ViewPager2.ORIENTATION_HORIZONTAL

return binding.root

이제 코드를 실행시켜보면 HomeFragment의 Pannel에 ViewPager가 적용된 것을 확인할 수 있을 것이다.

2) ViewPager에 CircleIndicator 적용하기

① CircleIndicator 라이브러리를 이용하기 위해 Module 수준의 build.gradle 파일에 아래의 의존성을 추가한다.

implementation ("me.relex:circleindicator:2.1.6")

② drawable 디렉토리 하위로, blue_radius라는 이름의 리소스 파일을 추가한다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid
        android:color="@color/select_color"/>
</shape>

③ 이번엔 drawable 디렉토리 하위로, gray_radius라는 이름의 리소스 파일을 추가한다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid
        android:color="@color/gray_color"/>
</shape>

④ Banner와 Pannel에 모두 CircleIndicator를 적용하도록 하자. fragment_home.xml 파일의 Banner와 Pannel의 ViewPager 아래에 각각 아래와 같이 CircleIndicator3 태그를 추가한다.

  • ViewPager2와 연결해야할 때에는 CircleIndicator3을 사용한다.
<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/home_pannel_background_vp"
    android:layout_width="match_parent"
    android:layout_height="430dp"
    android:scaleType="centerCrop"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.0"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
    
<me.relex.circleindicator.CircleIndicator3
    android:id="@+id/home_pannel_indicator"
    android:layout_width="match_parent"
    android:layout_height="32dp"
    app:ci_drawable_unselected="@drawable/gray_radius"
    app:ci_drawable="@drawable/blue_radius"
    app:ci_animator="@animator/scale_with_alpha"
    app:layout_constraintTop_toBottomOf="@id/home_pannel_background_vp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

...

<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/home_banner_vp"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_marginTop="15dp"
    app:layout_constraintEnd_toEndOf="@+id/home_pannel_btn_setting_iv"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/home_today_music_oversea_hs"/>
    
<me.relex.circleindicator.CircleIndicator3
    android:id="@+id/home_banner_indicator"
    android:layout_width="match_parent"
    android:layout_height="32dp"
    app:ci_drawable_unselected="@drawable/gray_radius"
    app:ci_drawable="@drawable/blue_radius"
    app:ci_animator="@animator/scale_with_alpha"
    app:layout_constraintTop_toBottomOf="@id/home_banner_vp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

⑤ HomeFragment에서 ViewPager와 Indicator를 연결한다.

binding.homeBannerIndicator.setViewPager(binding.homeBannerVp)
binding.homePannelIndicator.setViewPager(binding.homePannelBackgroundVp)

코드를 실행시켜보자. HomeFragment의 Pannel과 Banner의 ViewPager에 Indicator가 적용된 것을 확인할 수 있다.

3) ViewPager에 자동 슬라이드 기능 추가하기

① HomeFragment를 아래와 같이 수정한다.

class HomeFragment : Fragment() {

    lateinit var binding : FragmentHomeBinding

    private val timer = Timer()
    private val handler = Handler(Looper.getMainLooper())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentHomeBinding.inflate(inflater, container, false)
        
        ...

        val bannerAdapter = BannerVPAdapter(this)
        bannerAdapter.addFragment(BannerFragment(R.drawable.img_home_viewpager_exp))
        bannerAdapter.addFragment(BannerFragment(R.drawable.img_home_viewpager_exp2))

        binding.homeBannerVp.adapter = bannerAdapter
        binding.homeBannerVp.orientation = ViewPager2.ORIENTATION_HORIZONTAL

        binding.homeBannerIndicator.setViewPager(binding.homeBannerVp)

        autoSlide(bannerAdapter)

        val pannelAdpater = PannelVpAdapter(this)
        pannelAdpater.addFragment(PannelFragment(R.drawable.img_first_album_default))
        pannelAdpater.addFragment(PannelFragment(R.drawable.img_first_album_default))
        
        binding.homePannelBackgroundVp.adapter = pannelAdpater
        binding.homePannelBackgroundVp.orientation = ViewPager2.ORIENTATION_HORIZONTAL

        binding.homePannelIndicator.setViewPager(binding.homePannelBackgroundVp)

        return binding.root
    }

    private fun autoSlide(adapter: BannerVPAdapter) {
        timer.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                handler.post {
                    val nextItem = binding.homeBannerVp.currentItem + 1
                    if (nextItem < adapter.itemCount) {
                        binding.homeBannerVp.currentItem = nextItem
                    } else {
                        binding.homeBannerVp.currentItem = 0 // 순환
                    }
                }
            }
        }, 3000, 3000)
    }
}
  • Timer: 일정한 간격으로 작업을 수행하기 위해 사용되는 Java 클래스이다. 즉, timer 객체를 이용해 주기적으로 슬라이드 변경 작업을 수행한다.
  • Handler 객체: UI 스레드와 다른 스레드 간에 통신을 위해 사용한다.
  • Looper.getMainLooper(): 메인 스레드(UI 스레드)에 대한 루퍼를 가져와 슬라이드 변경 작업을 UI 스레드에서 실행한다.
  • scheduleAtFixedRate: 주기적으로 실행할 작업을 지정한다.
    • 첫번째 파라미터: 추상 클래스인 TimerTask의 run 메서드를 오버라이드하여 주기적으로 실행할 코드를 정의한다.
    • 두 번째 파라미터: 최초 실행 딜레이 시간을 3초로 지정한다.
    • 세 번째 파라미터: 주기를 3초로 설정한다. (3초마다 TimerTask를 실행한다.)
  • handler.post: UI 스레드에서 코드를 실행한다.
  • binding.homeBannerVp.currentItem: 현재 보여지는 뷰페이저의 아이템 인덱스를 가져온다. 이 때, 다음 아이템을 계산하여 만약 다음 아이템이 뷰페이저의 아이템 수를 초과하면 첫 번째 아이템으로 돌아가도록 설정한다.

코드를 실행시켜보면 3초마다 배너 이미지가 변경되는 것을 확인할 수 있다.

3. TabLayout과 함께 ViewPager 사용하기

1) ViewPager에 Fragment 연결하기

① 기존에는 fragment_album.xml 파일에 수록곡 / 상세정보 / 영상을 TextView의 LinearLayout으로 나타냈었다. 이것을 TabLayout으로 변경해주도록 하겠다.

  • fragment_album.xml 파일에서 아래의 내용을 삭제한다.
<LinearLayout
    android:id="@+id/linearLayout4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="30dp"
    android:orientation="horizontal"
    app:layout_constraintEnd_toEndOf="@+id/album_album_iv"
    app:layout_constraintStart_toStartOf="@+id/album_album_iv"
    app:layout_constraintTop_toBottomOf="@+id/album_album_iv">
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="수록곡"
        android:textColor="@color/select_color"
        android:textStyle="bold" />
        
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="100dp"
        android:text="상세정보"
        android:textColor="@color/black" />
        
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="100dp"
        android:text="영상"
        android:textColor="@color/black" />
        
</LinearLayout>

<View
    android:id="@+id/album_line_view"
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:layout_marginTop="15dp"
    android:background="#dcdcdc"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/linearLayout4" />

② 위에서 삭제한 내용의 아래 부분은 나중에 새로운 Fragment에 넣어줄 것이기 때문에 일단은 모두 주석 처리한다.

  • 아래와 같이 종료 컨테이너만 남기고 모두 주석처리한다.
	</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

③ 주석 처리한 부분 바로 위에 아래의 내용을 추가한다.

<com.google.android.material.tabs.TabLayout
    android:id="@+id/album_content_tb"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintEnd_toEndOf="@+id/album_album_iv"
    app:layout_constraintStart_toStartOf="@+id/album_album_iv"
    app:layout_constraintTop_toBottomOf="@+id/album_album_iv"/>
    
<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/album_content_vp"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintTop_toBottomOf="@+id/album_content_tb"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

④ SongFragment, DetailFragment, VideoFragment를 추가한다.

⑤ AlbumVPAdpater라는 kotlin class를 추가하고 아래의 내용을 입력한다.

class AlbumVPAdapter(fragment : Fragment) : FragmentStateAdapter(fragment) {
    override fun getItemCount(): Int = 3

    override fun createFragment(position: Int): Fragment {
        return when(position) {
            0 -> SongFragment()
            1 -> DetailFragment()
            else -> VideoFragment()
        }
    }
}
  • ViewPager2에서 표시할 총 페이지 수를 이미 알고 있을 때에는 위의 3과 같이 바로 상수를 적어도 된다.

⑥ fragment_album.xml을 아래와 같이 수정한다.

  • 루트 컨테이너의 레이아웃을 ConstraintLayout으로 변경한다.
  • 이 때, ConstraintLayout 안에 app이라는 prefix에 대한 namespace가 추가되어야 한다.
  • 주석 처리한 부분을 ConstraintLayout 안에 붙여 넣는다.
  • NestedScrollView의 상단 제약을 "@+id/album_line_view"에서 "parent"로 변경한다.
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".SongFragment">
    	...
        <androidx.core.widget.NestedScrollView
        	android:layout_width="match_parent"
        	android:layout_height="0dp"
        	android:fillViewport="true"
        	android:orientation="vertical"
        	android:overScrollMode="never"
        	app:layout_constraintBottom_toBottomOf="parent"
        	app:layout_constraintEnd_toEndOf="parent"
        	app:layout_constraintStart_toStartOf="parent"
        	app:layout_constraintTop_toTopOf="parent">

코드를 실행시켜보면, AlbumFragment의 ViewPager 태그에 SongFragment가 나타나는 것을 확인할 수 있다. 또한 좌측으로 swipe하여 다른 Fragment로 전환할 수도 있다.

2) ViewPager와 TabLayout 연결하기

① AlbumFragment에 TabLayout에 사용될 Label을 지정하기 위해 arrayList를 만들고, TabLayout과 ViewPager를 연결해야 한다.

private val information = arrayListOf("수록곡", "상세정보", "영상")

override fun onCreateView(
    inflater: LayoutInflater, 
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
	...
    val albumAdapter = AlbumVPAdapter(this)
    binding.albumContentVp.adapter = albumAdapter

    TabLayoutMediator(binding.albumContentTb, binding.albumContentVp) { tab, position ->
        tab.text = information[position]
    }.attach()

    return binding.root
}
  • TabLayoutMediator: TabLayout과 ViewPager2를 연결하기 위해 사용한다.
  • tab: 현재 position에 해당하는 TabLayout의 Tab 객체이다.
  • attach(): TabLayoutMediator를 활성화한다.

② Indicator의 길이가 Tab을 꽉 채우는게(기본값) 아닌 글자 크기에 알맞게 채워지도록 fragment_album.xml의 TabLayout 태그에 아래의 속성을 추가한다.

app:tabIndicatorFullWidth="false"

③ 또한, Indicator의 색상을 변경하는 것도 가능하다. TabLayout 태그에 아래의 속성을 추가하자.

// 선택된 탭의 텍스트 색상 변경
app:tabSelectedTextColor="#3f3fff"
// 선택된 탭의 Indicator 색상 변경
app:tabIndicatorColor="#3f3fff"

④ 탭을 선택할 때 퍼지는 듯한 애니메이션이 나오는데, 이를 Ripple 효과라고 한다. 만약 이 효과를 제거하고 싶다면, RippleColor를 투명하게 바꿔주면 된다. TabLayout 태그에 아래의 속성을 추가하자.

app:tabRippleColor="@color/transparent"

4. 클릭 이벤트 리스너로 이미지 변경하기

SongFragment의 onCreateView 메서드를 아래와 같이 수정한다.

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    binding = FragmentSongBinding.inflate(inflater, container, false)
    binding.songMixoffTg.setOnClickListener {
        binding.songMixoffTg.visibility = View.GONE
        binding.songMixonTg.visibility = View.VISIBLE
    }
    
    binding.songMixonTg.setOnClickListener {
        binding.songMixoffTg.visibility = View.VISIBLE
        binding.songMixonTg.visibility = View.GONE
    }
    
    return binding.root
}

코드를 실행시킨 후 SongFragment의 토글 버튼을 클릭해보자. 클릭할 때마다 토글의 ON/OFF가 반복될 것이다.

profile
LG전자 Connected Service 1 Unit 연구원 변현섭입니다.

0개의 댓글