diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index b268ef3..0000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/com/example/realflo/Album.kt b/app/src/main/java/com/example/realflo/Album.kt
index d64662c..7a59048 100644
--- a/app/src/main/java/com/example/realflo/Album.kt
+++ b/app/src/main/java/com/example/realflo/Album.kt
@@ -5,12 +5,14 @@ import androidx.room.PrimaryKey
@Entity(tableName = "AlbumTable")
data class Album(
- @PrimaryKey(autoGenerate = true)
- var id: Int = 0,
+ var title: String = "",
+ var singer: String = "",
+ var coverImg: Int = 0,
+ var releaseDate: String = "",
- var title: String? = "",
- var singer: String? = "",
- var coverImg: Int? = null,
- var description: String? = null
-)
+ var isLike: Boolean = false
+) {
+ @PrimaryKey(autoGenerate = true)
+ var id: Int = 0
+}
diff --git a/app/src/main/java/com/example/realflo/AlbumFragment.kt b/app/src/main/java/com/example/realflo/AlbumFragment.kt
index b5c3ff4..50653b1 100644
--- a/app/src/main/java/com/example/realflo/AlbumFragment.kt
+++ b/app/src/main/java/com/example/realflo/AlbumFragment.kt
@@ -17,7 +17,10 @@ class AlbumFragment : Fragment() {
private lateinit var binding: FragmentAlbumBinding
private lateinit var db: FloDatabase
- private val information = arrayListOf("수록곡","상세정보","영상")
+ private val information = arrayListOf("수록곡", "상세정보", "영상")
+
+ private var currentAlbum: Album? = null
+ private var albumId: Int = -1
override fun onCreateView(
inflater: LayoutInflater,
@@ -29,29 +32,70 @@ class AlbumFragment : Fragment() {
// 뒤로가기
binding.albumBackIv.setOnClickListener {
- requireActivity().supportFragmentManager.popBackStack()
+ parentFragmentManager.popBackStack()
}
- // 1) HomeFragment에서 넘긴 albumId 받기
- val albumId = arguments?.getInt("albumId") ?: -1
+ // HomeFragment 에서 넘어온 앨범 ID
+ albumId = arguments?.getInt("albumId") ?: -1
+
+ // 1) 앨범 정보 로드
+ loadAlbumHeader()
+
+ // 2) ViewPager 설정
+ val albumAdapter = AlbumVPAdapter(this, albumId)
+ binding.albumContentVp.adapter = albumAdapter
- // 2) 앨범 헤더 채우기 (제목/가수/커버)
+ TabLayoutMediator(binding.albumContentTb, binding.albumContentVp) { tab, position ->
+ tab.text = information[position]
+ }.attach()
+
+ return binding.root
+ }
+
+ private fun loadAlbumHeader() {
viewLifecycleOwner.lifecycleScope.launch {
- val album = withContext(Dispatchers.IO) { db.albumDao().getAlbum(albumId) }
+ val album = withContext(Dispatchers.IO) {
+ db.albumDao().getAlbumById(albumId)
+ }
+
+ currentAlbum = album
+
album?.let {
binding.albumMusicTitleTv.text = it.title
binding.albumSingerNameTv.text = it.singer
it.coverImg?.let { resId -> binding.albumAlbumIv.setImageResource(resId) }
+
+ setLikeUI(it.isLike)
+
+ // 좋아요 버튼 동작
+ binding.albumLikeIv.setOnClickListener {
+ toggleAlbumLike()
+ }
}
}
+ }
- // 3) ViewPager에 albumId 넘겨주기
- val albumAdapter = AlbumVPAdapter(this, albumId)
- binding.albumContentVp.adapter = albumAdapter
- TabLayoutMediator(binding.albumContentTb, binding.albumContentVp) { tab, position ->
- tab.text = information[position]
- }.attach()
+ private fun toggleAlbumLike() {
+ val album = currentAlbum ?: return
+ val newValue = !album.isLike
- return binding.root
+ // UI 즉시 변경
+ setLikeUI(newValue)
+
+ // DB 업데이트
+ viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
+ db.albumDao().updateAlbumLike(album.id, newValue)
+ }
+
+ // 메모리도 갱신
+ album.isLike = newValue
+ }
+
+ private fun setLikeUI(isLike: Boolean) {
+ if (isLike) {
+ binding.albumLikeIv.setImageResource(R.drawable.ic_my_like_on)
+ } else {
+ binding.albumLikeIv.setImageResource(R.drawable.ic_my_like_off)
+ }
}
}
diff --git a/app/src/main/java/com/example/realflo/AlbumLockerRVAdapter.kt b/app/src/main/java/com/example/realflo/AlbumLockerRVAdapter.kt
new file mode 100644
index 0000000..1eea8f1
--- /dev/null
+++ b/app/src/main/java/com/example/realflo/AlbumLockerRVAdapter.kt
@@ -0,0 +1,56 @@
+package com.example.realflo
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.example.realflo.databinding.ItemLockerAlbumBinding
+
+class AlbumLockerRVAdapter :
+ ListAdapter(AlbumDiffCallback()) {
+
+ private var onMoreClickListener: ((Album) -> Unit)? = null
+
+ fun setOnMoreClickListener(listener: (Album) -> Unit) {
+ onMoreClickListener = listener
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = ItemLockerAlbumBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class ViewHolder(private val binding: ItemLockerAlbumBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(album: Album) {
+ binding.itemAlbumTitleTv.text = album.title
+ binding.itemAlbumSingerTv.text = album.singer
+ binding.itemAlbumImgIv.setImageResource(album.coverImg)
+
+ // (…) 버튼 클릭 시
+ binding.itemAlbumMoreIv.setOnClickListener {
+ onMoreClickListener?.invoke(album)
+ }
+ }
+ }
+}
+
+class AlbumDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: Album, newItem: Album): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: Album, newItem: Album): Boolean {
+ return oldItem == newItem
+ }
+}
diff --git a/app/src/main/java/com/example/realflo/BannerFragment.kt b/app/src/main/java/com/example/realflo/BannerFragment.kt
index ec68801..ac938a2 100644
--- a/app/src/main/java/com/example/realflo/BannerFragment.kt
+++ b/app/src/main/java/com/example/realflo/BannerFragment.kt
@@ -7,8 +7,18 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.realflo.databinding.FragmentBannerBinding
-class BannerFragment(val imgRes : Int) : Fragment(){
- lateinit var binding: FragmentBannerBinding
+class BannerFragment : Fragment() {
+
+ private lateinit var binding: FragmentBannerBinding
+
+ // onCreate에서 arguments 받기
+ private var imgRes: Int = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ imgRes = arguments?.getInt(ARG_IMG_RES) ?: 0
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -18,4 +28,17 @@ class BannerFragment(val imgRes : Int) : Fragment(){
binding.bannerImageIv.setImageResource(imgRes)
return binding.root
}
-}
\ No newline at end of file
+
+ companion object {
+
+ private const val ARG_IMG_RES = "imgRes"
+
+ fun newInstance(imgRes: Int): BannerFragment {
+ val fragment = BannerFragment()
+ val args = Bundle()
+ args.putInt(ARG_IMG_RES, imgRes)
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/realflo/DetailFragment.kt b/app/src/main/java/com/example/realflo/DetailFragment.kt
index 184a4df..85e8f28 100644
--- a/app/src/main/java/com/example/realflo/DetailFragment.kt
+++ b/app/src/main/java/com/example/realflo/DetailFragment.kt
@@ -40,16 +40,13 @@ class DetailFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch {
val album = withContext(Dispatchers.IO) {
- db.albumDao().getAlbum(albumId)
+ db.albumDao().getAlbumById(albumId)
}
album?.let {
binding.detailTitleTv.text = it.title
binding.detailSingerTv.text = it.singer
- // 설명은 별도 필드가 없으니 임시로 구성
binding.detailDescTv.text = "이 앨범은 ${it.singer}의 \"${it.title}\" 입니다."
- // 만약 fragment_detail.xml에 cover 이미지뷰가 있다면 주석 해제
- // it.coverImg?.let { resId -> binding.detailCoverIv.setImageResource(resId) }
}
}
}
diff --git a/app/src/main/java/com/example/realflo/HomeFragment.kt b/app/src/main/java/com/example/realflo/HomeFragment.kt
index 1ceb1ef..c373067 100644
--- a/app/src/main/java/com/example/realflo/HomeFragment.kt
+++ b/app/src/main/java/com/example/realflo/HomeFragment.kt
@@ -53,17 +53,16 @@ class HomeFragment : Fragment() {
}
})
- // 배너(그대로 유지)
+ // 배너
val bannerAdapter = BannerVPAdapter(this).apply {
- addFragment(BannerFragment(R.drawable.img_home_viewpager_exp))
- addFragment(BannerFragment(R.drawable.img_home_viewpager_exp2))
+ addFragment(BannerFragment.newInstance(R.drawable.img_home_viewpager_exp))
+ addFragment(BannerFragment.newInstance(R.drawable.img_home_viewpager_exp2))
}
binding.homeBannerVp.apply {
adapter = bannerAdapter
orientation = ViewPager2.ORIENTATION_HORIZONTAL
}
- // 🔥 더미 add(...) 제거하고 DB에서 로드
loadAlbumsFromDb()
return binding.root
@@ -71,7 +70,7 @@ class HomeFragment : Fragment() {
private fun loadAlbumsFromDb() {
viewLifecycleOwner.lifecycleScope.launch {
- val albums = withContext(Dispatchers.IO) { db.albumDao().getAlbums() }
+ val albums = withContext(Dispatchers.IO) { db.albumDao().getAllAlbums() }
albumDatas.clear()
albumDatas.addAll(albums)
albumRVAdapter.notifyDataSetChanged()
diff --git a/app/src/main/java/com/example/realflo/LockerAdapter.kt b/app/src/main/java/com/example/realflo/LockerAdapter.kt
index ba5615d..707a8de 100644
--- a/app/src/main/java/com/example/realflo/LockerAdapter.kt
+++ b/app/src/main/java/com/example/realflo/LockerAdapter.kt
@@ -6,9 +6,11 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.realflo.databinding.ItemSongBinding
+import java.util.Locale
class LockerAdapter(
- private val onSongClicked: (Song) -> Unit
+ private val onSongClicked: (Song) -> Unit,
+ private val onLikeClicked: (Song) -> Unit // 보관함에서 하트 클릭 콜백
) : ListAdapter(SongDiffCallback()) {
inner class SongViewHolder(private val binding: ItemSongBinding) :
@@ -16,7 +18,7 @@ class LockerAdapter(
fun bind(song: Song, order: Int) {
// 순번, 제목, 가수
- binding.songOrderTv.text = String.format("%02d", order)
+ binding.songOrderTv.text = String.format(Locale.US, "%02d", order)
binding.songTitleTv.text = song.title
binding.songSingerTv.text = song.singer
@@ -26,17 +28,16 @@ class LockerAdapter(
else R.drawable.ic_my_like_off
)
- // 클릭 동작
+ // 곡 클릭
binding.root.setOnClickListener { onSongClicked(song) }
binding.songPlayIv.setOnClickListener { onSongClicked(song) }
- // 하트 토글(화면상만 변경; DB 반영은 이후 단계에서 처리)
+ // 하트 클릭 → Fragment 로 이벤트만 전달
binding.likeIv.setOnClickListener {
- // Note: This only changes the UI state, not the database.
- // The database update should be handled in the Fragment/ViewModel.
- val currentSong = getItem(bindingAdapterPosition)
- currentSong.isLike = !currentSong.isLike
- notifyItemChanged(bindingAdapterPosition)
+ val pos = adapterPosition
+ if (pos != RecyclerView.NO_POSITION) {
+ onLikeClicked(song)
+ }
}
}
}
diff --git a/app/src/main/java/com/example/realflo/LockerFragment.kt b/app/src/main/java/com/example/realflo/LockerFragment.kt
index b541c01..8d4220c 100644
--- a/app/src/main/java/com/example/realflo/LockerFragment.kt
+++ b/app/src/main/java/com/example/realflo/LockerFragment.kt
@@ -32,31 +32,56 @@ class LockerFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+
setupRecyclerView()
+ setupSavedAlbumButton()
loadLikedSongs()
}
override fun onResume() {
super.onResume()
- // SongActivity에서 하트 토글하고 돌아왔을 때 갱신
loadLikedSongs()
}
- private fun setupRecyclerView() {
- adapter = LockerAdapter { song ->
- val intent = Intent(requireContext(), SongActivity::class.java)
- intent.putExtra("songId", song.id)
- startActivity(intent)
+ private fun setupSavedAlbumButton() {
+ // 저장앨범 버튼 클릭 시 → 저장앨범 목록 Fragment 로 이동
+ binding.lockerSavedAlbumBtn.setOnClickListener {
+ parentFragmentManager.beginTransaction()
+ .replace(R.id.main_frm, SavedAlbumFragment())
+ .addToBackStack(null)
+ .commit()
}
+ }
+
+ private fun setupRecyclerView() {
+ adapter = LockerAdapter(
+ onSongClicked = { song ->
+ // 곡 클릭하면 SongActivity 이동
+ val intent = Intent(requireContext(), SongActivity::class.java)
+ intent.putExtra("songId", song.id)
+ startActivity(intent)
+ },
+
+ onLikeClicked = { song ->
+ // 좋아요 해제
+ viewLifecycleOwner.lifecycleScope.launch {
+ withContext(Dispatchers.IO) {
+ db.songDao().updateLike(song.id, false)
+ }
+ loadLikedSongs()
+ }
+ }
+ )
binding.lockerRecyclerview.adapter = adapter
- binding.lockerRecyclerview.layoutManager = LinearLayoutManager(requireContext())
+ binding.lockerRecyclerview.layoutManager =
+ LinearLayoutManager(requireContext())
}
private fun loadLikedSongs() {
viewLifecycleOwner.lifecycleScope.launch {
val likedSongs = withContext(Dispatchers.IO) {
- db.songDao().getLikedSongs() // 좋아요만 조회
+ db.songDao().getLikedSongs()
}
adapter.submitList(likedSongs)
diff --git a/app/src/main/java/com/example/realflo/LoginActivity.kt b/app/src/main/java/com/example/realflo/LoginActivity.kt
new file mode 100644
index 0000000..739ccae
--- /dev/null
+++ b/app/src/main/java/com/example/realflo/LoginActivity.kt
@@ -0,0 +1,77 @@
+package com.example.realflo
+
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
+import androidx.lifecycle.lifecycleScope
+import com.example.realflo.databinding.ActivityLoginBinding
+import data.local.FloDatabase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class LoginActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityLoginBinding
+ private lateinit var db: FloDatabase
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityLoginBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ db = FloDatabase.getInstance(this)
+
+ // 로그인 버튼
+ binding.loginSignInBtn.setOnClickListener {
+ login()
+ }
+
+ // 회원가입 이동
+ binding.loginSignUpTv.setOnClickListener {
+ startActivity(Intent(this, SignUpActivity::class.java))
+ }
+ }
+
+ private fun login() {
+ val email = binding.loginIdEt.text.toString()
+ val password = binding.loginPasswordEt.text.toString()
+
+ if (email.isBlank() || password.isBlank()) {
+ Toast.makeText(this, "이메일과 비밀번호를 입력하세요.", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ lifecycleScope.launch {
+ // DB에서 로그인 시도
+ val user = withContext(Dispatchers.IO) {
+ db.userDao().login(email, password)
+ }
+
+ if (user == null) {
+ Toast.makeText(
+ this@LoginActivity,
+ "이메일 또는 비밀번호가 틀렸습니다.",
+ Toast.LENGTH_SHORT
+ ).show()
+ return@launch
+ }
+
+ // 로그인 성공 → sharedPreferences 저장
+ val sp = getSharedPreferences("user", MODE_PRIVATE)
+ sp.edit {
+ putInt("userId", user.id)
+ putString("userName", user.name)
+ putString("userEmail", user.email)
+ }
+
+ Toast.makeText(this@LoginActivity, "로그인 성공!", Toast.LENGTH_SHORT).show()
+
+ // 메인 화면으로 이동
+ startActivity(Intent(this@LoginActivity, MainActivity::class.java))
+ finish()
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/realflo/MainActivity.kt b/app/src/main/java/com/example/realflo/MainActivity.kt
index 02ec871..eb838f3 100644
--- a/app/src/main/java/com/example/realflo/MainActivity.kt
+++ b/app/src/main/java/com/example/realflo/MainActivity.kt
@@ -16,7 +16,7 @@ class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
- // 미니플레이어에서 표시할 현재 곡(간단 캐시)
+ // 미니플레이어에 표시할 현재 곡 (DB에 있는 실제 Song)
private var currentSong: Song? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -25,10 +25,11 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
- // 1) 최초 화면: 홈
+ // 기본 화면 = 홈
replaceFragment(HomeFragment())
+ binding.mainBnv.selectedItemId = R.id.homeFragment
- // 2) BottomNavigation 탭 전환
+ // 하단 탭
binding.mainBnv.setOnItemSelectedListener { item ->
when (item.itemId) {
R.id.homeFragment -> {
@@ -40,85 +41,148 @@ class MainActivity : AppCompatActivity() {
true
}
R.id.searchFragment -> {
- // 검색 탭이 있다면 여기에 프래그먼트 연결
- // replaceFragment(SearchFragment())
+ // 검색 프래그먼트 쓰면 여기서 교체
true
}
else -> false
}
}
- // 기본 선택 탭
- binding.mainBnv.selectedItemId = R.id.homeFragment
- // 3) 미니플레이어 클릭 → SongActivity로 이동
+ // ⭐ 미니플레이어 클릭 → SongActivity 실행
binding.mainPlayerCl.setOnClickListener {
- // 현재 저장된 songId 기반으로 이동
val sp = getSharedPreferences("song", MODE_PRIVATE)
- val songId = sp.getInt("songId", -1)
- val intent = Intent(this, SongActivity::class.java)
- if (songId != -1) {
- intent.putExtra("songId", songId)
- } else {
- // songId가 없을 때를 대비해 제목/가수만 최소 전달(선택)
- currentSong?.let {
- intent.putExtra("songId", it.id)
- intent.putExtra("title", it.title)
- intent.putExtra("singer", it.singer)
+ var songId = sp.getInt("songId", -1)
+
+ if (songId <= 0) {
+ // prefs 에 저장된 게 없으면 currentSong 기준으로라도 열어줌(라일락 포함)
+ val fallback = currentSong
+ if (fallback == null || fallback.id <= 0) {
+ android.util.Log.w("MainActivity", "열 수 있는 곡이 없습니다.")
+ return@setOnClickListener
}
+ songId = fallback.id
+ sp.edit().putInt("songId", songId).apply()
}
- startActivity(intent)
+
+ openSong(songId)
}
- // 미니플레이어 재생/일시정지 버튼(있으면) 동작 예시
- binding.mainMiniplayerBtn.setOnClickListener { togglePlay(false) }
- binding.mainPauseBtn.setOnClickListener { togglePlay(true) }
+ // 미니플레이어 재생/일시정지 버튼 (UI만 토글)
+ binding.mainMiniplayerBtn.setOnClickListener { togglePlay(true) }
+ binding.mainPauseBtn.setOnClickListener { togglePlay(false) }
}
override fun onStart() {
super.onStart()
- // 저장된 songId로 DB에서 현재 곡 로드 → 미니플레이어 렌더링
+
val sp = getSharedPreferences("song", MODE_PRIVATE)
- val songId = sp.getInt("songId", -1)
+ val savedSongId = sp.getInt("songId", -1)
lifecycleScope.launch {
+ val db = FloDatabase.getInstance(this@MainActivity)
+
val song = withContext(Dispatchers.IO) {
- if (songId != -1) {
- FloDatabase.getInstance(this@MainActivity).songDao().getSongById(songId)
- } else null
+ val dao = db.songDao()
+
+ // 1) DB에서 전체 곡 조회
+ val all = dao.getSongs()
+
+ // 1-1) DB에 곡이 하나도 없으면 → 라일락을 직접 DB에 집어넣는다
+ if (all.isEmpty()) {
+ val lilac = Song(
+ title = "라일락",
+ singer = "아이유(IU)",
+ playTime = 214,
+ music = "music_lilac", // res/raw/music_lilac.mp3 기준
+ albumIdx = 0,
+ isLike = false
+ )
+ dao.insert(lilac)
+
+ // 방금 넣은 라일락 다시 읽어오기 (id가 자동생성되니까)
+ dao.getSongs().firstOrNull()
+ } else {
+ // 2) DB에 곡은 있는데, 저장된 songId가 있으면 그 곡 우선
+ if (savedSongId > 0) {
+ dao.getSongById(savedSongId) ?: all.first()
+ } else {
+ // 3) songId 없으면 라일락 우선 선택, 없으면 첫 곡
+ all.firstOrNull {
+ it.title == "라일락" || it.title.equals("LILAC", true)
+ } ?: all.first()
+ }
+ }
+ }
+
+ currentSong = song
+
+ if (song != null) {
+ // prefs에 songId가 아직 없었다면 여기서 한 번 저장 (라일락 포함)
+ if (savedSongId <= 0 && song.id > 0) {
+ sp.edit().putInt("songId", song.id).apply()
+ }
+ setMiniPlayer(song)
+ } else {
+ // 진짜로 DB에 곡이 아무것도 없을 때만 (거의 없겠지만)
+ setEmptyMiniPlayer()
}
- currentSong = song ?: currentSong
- currentSong?.let { setMiniPlayer(it) }
}
}
- // 프래그먼트 전환 헬퍼
- private fun replaceFragment(Fragment: Fragment) {
+ // SongActivity 열기
+ private fun openSong(songId: Int) {
+ if (songId <= 0) return
+ val intent = Intent(this, SongActivity::class.java)
+ intent.putExtra("songId", songId)
+ startActivity(intent)
+ }
+
+ // 프래그먼트 전환
+ private fun replaceFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
- .replace(R.id.main_frm, Fragment)
+ .replace(R.id.main_frm, fragment)
.commit()
}
- // 미니플레이어 UI 렌더링
+ // ✅ 실제 곡 있을 때 미니플레이어 UI
private fun setMiniPlayer(song: Song) {
+ binding.mainPlayerCl.visibility = View.VISIBLE
+
binding.mainMiniplayerTitleTv.text = song.title
binding.mainMiniplayerSingerTv.text = song.singer
- // 진행바는 저장된 second/playTime 기반으로 계산 (0 나눔 방지)
val progress = if (song.playTime == 0) 0 else (song.second * 100 / song.playTime)
binding.mainMiniplayerProgressSb.progress = progress
+
+ if (song.isPlaying) {
+ binding.mainMiniplayerBtn.visibility = View.GONE
+ binding.mainPauseBtn.visibility = View.VISIBLE
+ } else {
+ binding.mainMiniplayerBtn.visibility = View.VISIBLE
+ binding.mainPauseBtn.visibility = View.GONE
+ }
+ }
+
+ // ✅ DB에 곡이 아예 없을 때만 호출 (거의 안 쓰일 것)
+ private fun setEmptyMiniPlayer() {
+ binding.mainPlayerCl.visibility = View.VISIBLE
+
+ binding.mainMiniplayerTitleTv.text = "재생할 곡이 없습니다"
+ binding.mainMiniplayerSingerTv.text = "앱에 곡 데이터를 추가해 주세요"
+ binding.mainMiniplayerProgressSb.progress = 0
+
+ binding.mainMiniplayerBtn.visibility = View.VISIBLE
+ binding.mainPauseBtn.visibility = View.GONE
}
- // 미니플레이어 버튼 샘플 동작(재생/일시정지 토글 시 아이콘만 교체)
- private fun togglePlay(toPlay: Boolean) {
- // 여기서는 UI 아이콘만 토글 (실재생 제어는 SongActivity에서 함)
- if (toPlay) {
+ // 미니플레이어 재생/일시정지 토글 (UI만)
+ private fun togglePlay(isPlaying: Boolean) {
+ if (isPlaying) {
binding.mainMiniplayerBtn.visibility = View.GONE
binding.mainPauseBtn.visibility = View.VISIBLE
} else {
binding.mainMiniplayerBtn.visibility = View.VISIBLE
binding.mainPauseBtn.visibility = View.GONE
}
- // 필요하면 isPlaying을 SharedPreferences로 저장해서 SongActivity와 동기화 가능
- // getSharedPreferences("song", MODE_PRIVATE).edit().putBoolean("isPlaying", toPlay).apply()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/realflo/SavedAlbumFragment.kt b/app/src/main/java/com/example/realflo/SavedAlbumFragment.kt
new file mode 100644
index 0000000..d9173b5
--- /dev/null
+++ b/app/src/main/java/com/example/realflo/SavedAlbumFragment.kt
@@ -0,0 +1,85 @@
+package com.example.realflo
+
+import android.app.AlertDialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.example.realflo.databinding.FragmentLockerSavedalbumBinding
+import data.local.FloDatabase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class SavedAlbumFragment : Fragment() {
+
+ private lateinit var binding: FragmentLockerSavedalbumBinding
+ private lateinit var db: FloDatabase
+ private lateinit var adapter: AlbumLockerRVAdapter
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentLockerSavedalbumBinding.inflate(inflater, container, false)
+ db = FloDatabase.getInstance(requireContext())
+
+ adapter = AlbumLockerRVAdapter()
+ binding.lockerSavedSongRecyclerView.layoutManager =
+ LinearLayoutManager(requireContext())
+ binding.lockerSavedSongRecyclerView.adapter = adapter
+
+ // (…) 버튼 클릭 콜백
+ adapter.setOnMoreClickListener { album ->
+ showDeleteDialog(album)
+ }
+
+ loadLikedAlbums()
+
+ return binding.root
+ }
+
+ private fun loadLikedAlbums() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ val likedList = withContext(Dispatchers.IO) {
+ db.albumDao().getLikedAlbums()
+ }
+ adapter.submitList(likedList)
+ }
+ }
+
+ private fun showDeleteDialog(album: Album) {
+ val builder = AlertDialog.Builder(requireContext())
+ builder.setTitle("앨범 삭제")
+ builder.setMessage("이 앨범을 저장 목록에서 삭제하시겠습니까?")
+
+ builder.setPositiveButton("삭제") { _, _ ->
+ deleteAlbum(album)
+ }
+
+ builder.setNegativeButton("취소", null)
+ builder.show()
+ }
+
+ private fun deleteAlbum(album: Album) {
+ viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
+
+ // DB에서 삭제
+ db.albumDao().deleteAlbum(album.id)
+
+ // 또는 isLike만 false로 변경하고 싶으면 ↓
+ // db.albumDao().updateAlbumLike(album.id, false)
+
+ // 삭제 후 리스트 다시 불러오기
+ val updatedList = db.albumDao().getLikedAlbums()
+
+ withContext(Dispatchers.Main) {
+ adapter.submitList(updatedList)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/realflo/SignUpActivity.kt b/app/src/main/java/com/example/realflo/SignUpActivity.kt
new file mode 100644
index 0000000..1cc28bd
--- /dev/null
+++ b/app/src/main/java/com/example/realflo/SignUpActivity.kt
@@ -0,0 +1,69 @@
+package com.example.realflo
+
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.example.realflo.databinding.ActivitySignupBinding
+import data.local.FloDatabase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class SignUpActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivitySignupBinding
+ private var db: FloDatabase? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivitySignupBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ db = FloDatabase.getInstance(this)
+
+ binding.signUpSignUpBtn.setOnClickListener {
+ signUp()
+ }
+ }
+
+ private fun signUp() {
+ val email = binding.signUpIdEt.text.toString() + "@" + binding.signUpDirectInputEt.text.toString()
+ val password = binding.signUpPasswordEt.text.toString()
+ val name = binding.signUpNameEt.text.toString()
+
+ if (email.isEmpty() || password.isEmpty() || name.isEmpty()) {
+ Toast.makeText(this, "모든 정보를 입력해주세요", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ lifecycleScope.launch {
+ val db = this@SignUpActivity.db ?: return@launch
+
+ // 이메일 중복 체크
+ val existUser = withContext(Dispatchers.IO) {
+ db.userDao().getUserByEmail(email)
+ }
+
+ if (existUser != null) {
+ Toast.makeText(this@SignUpActivity, "이미 존재하는 이메일입니다", Toast.LENGTH_SHORT).show()
+ return@launch
+ }
+
+ // 새 유저 생성
+ val newUser = User(email = email, password = password, name = name)
+
+ withContext(Dispatchers.IO) {
+ db.userDao().insert(newUser)
+ }
+
+ Toast.makeText(this@SignUpActivity, "회원가입 성공!", Toast.LENGTH_SHORT).show()
+
+ // 로그인 화면으로 이동
+ val intent = Intent(this@SignUpActivity, LoginActivity::class.java)
+ startActivity(intent)
+ finish()
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/realflo/SongActivity.kt b/app/src/main/java/com/example/realflo/SongActivity.kt
index be3d408..8c9855a 100644
--- a/app/src/main/java/com/example/realflo/SongActivity.kt
+++ b/app/src/main/java/com/example/realflo/SongActivity.kt
@@ -7,7 +7,6 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.realflo.databinding.ActivitySongBinding
-import com.google.gson.Gson
import data.local.FloDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -20,6 +19,8 @@ class SongActivity : AppCompatActivity() {
private lateinit var db: FloDatabase
private lateinit var song: Song
+ private var songLoaded = false
+
private lateinit var timer: Timer
private var mediaPlayer: MediaPlayer? = null
@@ -30,24 +31,63 @@ class SongActivity : AppCompatActivity() {
db = FloDatabase.getInstance(this)
- // songId만 받아서 DB에서 곡 로드
val songId = intent.getIntExtra("songId", -1)
+ Log.d("SongActivity", "onCreate() songId = $songId")
+
+ // ❗ songId가 0 이하이면 잘못 진입한 것 → 바로 종료
+ if (songId <= 0) {
+ Log.e("SongActivity", "잘못된 songId로 SongActivity 진입: $songId")
+ finish()
+ return
+ }
+ // DB에서 곡 로드
lifecycleScope.launch {
- song = withContext(Dispatchers.IO) {
+ val loaded: Song? = withContext(Dispatchers.IO) {
db.songDao().getSongById(songId)
- } ?: Song(title = "라일락", singer = "아이유(IU)", playTime = 214, music = "music_lilac")
+ }
+
+ if (loaded == null) {
+ Log.e("SongActivity", "DB에서 songId=$songId 를 찾지 못했습니다.")
+ finish()
+ return@launch
+ }
+
+ song = loaded
+ songLoaded = true
+
+ Log.d(
+ "SongActivity",
+ "loaded song id=${song.id}, title=${song.title}, isLike=${song.isLike}"
+ )
setPlayer(song)
startTimer()
- attachSeekBarListener() // ✅ SeekBar 스크러빙 연결
+ attachSeekBarListener()
}
// 좋아요 토글
binding.songLikeIv.setOnClickListener {
- song.isLike = !song.isLike
- lifecycleScope.launch(Dispatchers.IO) { db.songDao().updateLike(song.id, song.isLike) }
- renderLikeIcon(song.isLike)
+ if (!songLoaded) {
+ Log.w("SongActivity", "좋아요 클릭했지만 song 이 아직 로드 안 됨")
+ return@setOnClickListener
+ }
+
+ lifecycleScope.launch {
+ val newLike = !song.isLike
+
+ withContext(Dispatchers.IO) {
+ db.songDao().updateLike(song.id, newLike)
+ }
+
+ val refreshed = withContext(Dispatchers.IO) {
+ db.songDao().getSongById(song.id)
+ }
+
+ song = refreshed ?: song.copy(isLike = newLike)
+ renderLikeIcon(song.isLike)
+ Log.d("SongActivity", "좋아요 변경: songId=${song.id}, isLike=${song.isLike}")
+ }
}
// 상단 닫기
@@ -57,29 +97,45 @@ class SongActivity : AppCompatActivity() {
binding.songMiniplayerIv.setOnClickListener { setPlayerStatus(false) }
binding.songPauseIv.setOnClickListener { setPlayerStatus(true) }
- // 이전/다음 곡 버튼 (xml id 다르면 변경)
+ // 이전/다음 곡
binding.songPreviousIv.setOnClickListener {
- lifecycleScope.launch { getAdjacent(delta = -1)?.let { playNewSong(it) } }
+ if (!songLoaded) return@setOnClickListener
+ lifecycleScope.launch { getAdjacent(-1)?.let { playNewSong(it) } }
}
binding.songNextIv.setOnClickListener {
- lifecycleScope.launch { getAdjacent(delta = +1)?.let { playNewSong(it) } }
+ if (!songLoaded) return@setOnClickListener
+ lifecycleScope.launch { getAdjacent(+1)?.let { playNewSong(it) } }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (!songLoaded) return
+
+ lifecycleScope.launch {
+ val updated = withContext(Dispatchers.IO) {
+ db.songDao().getSongById(song.id)
+ }
+ if (updated != null) song = updated
+ renderLikeIcon(song.isLike)
+ Log.d("SongActivity", "onResume sync isLike=${song.isLike}")
}
}
override fun onPause() {
super.onPause()
- setPlayerStatus(false)
- // 진행도 저장
- song.second = (binding.songProgressSb.progress * song.playTime) / 100
+ if (songLoaded) {
+ setPlayerStatus(false)
- // 현재 곡 동기화(songId만 저장)
- getSharedPreferences("song", MODE_PRIVATE).edit()
- .putInt("songId", song.id)
- .apply()
+ song.second = (binding.songProgressSb.progress * song.playTime) / 100
- // 진행 상태 DB 반영(선택)
- lifecycleScope.launch(Dispatchers.IO) { db.songDao().update(song) }
+ getSharedPreferences("song", MODE_PRIVATE).edit()
+ .putInt("songId", song.id)
+ .apply()
+
+ lifecycleScope.launch(Dispatchers.IO) { db.songDao().update(song) }
+ }
}
override fun onDestroy() {
@@ -91,6 +147,8 @@ class SongActivity : AppCompatActivity() {
/** 재생/일시정지 UI & MediaPlayer 반영 */
private fun setPlayerStatus(isPlaying: Boolean) {
+ if (!songLoaded) return
+
song.isPlaying = isPlaying
if (this::timer.isInitialized) timer.isPlaying = isPlaying
@@ -106,58 +164,50 @@ class SongActivity : AppCompatActivity() {
}
private fun startTimer() {
+ if (!songLoaded) return
timer = Timer(song.playTime, song.isPlaying)
timer.start()
}
- /** 같은 앨범에서 delta(-1:이전, +1:다음) 곡을 구한다. 없으면 null */
+ /** 같은 앨범에서 이전/다음 곡 */
private suspend fun getAdjacent(delta: Int): Song? = withContext(Dispatchers.IO) {
val list = db.songDao().getSongsInAlbum(song.albumIdx)
if (list.isEmpty()) return@withContext null
+
val idx = list.indexOfFirst { it.id == song.id }.coerceAtLeast(0)
val nextIndex = (idx + delta + list.size) % list.size
list[nextIndex]
}
- /** 곡이 끝나면 자동으로 다음 곡으로 넘어가도록 타이머/플레이어 세팅 */
private fun attachAutoNextListener() {
mediaPlayer?.setOnCompletionListener {
lifecycleScope.launch {
- getAdjacent(delta = +1)?.let { playNewSong(it) }
+ getAdjacent(+1)?.let { playNewSong(it) }
}
}
}
- /** ✅ SeekBar 드래그로 재생 위치 이동 */
+ /** SeekBar 드래그 이동 */
private fun attachSeekBarListener() {
binding.songProgressSb.setOnSeekBarChangeListener(object :
android.widget.SeekBar.OnSeekBarChangeListener {
- private var userSeeking = false
-
- override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {
- userSeeking = true
- }
-
- override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {
- userSeeking = false
- }
+ override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) = Unit
+ override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) = Unit
override fun onProgressChanged(
seekBar: android.widget.SeekBar?,
progress: Int,
fromUser: Boolean
) {
- if (!fromUser) return
+ if (!fromUser || !songLoaded) return
val newSecond = (progress * song.playTime) / 100
song.second = newSecond
- // 타이머 내부 위치도 동기화
if (this@SongActivity::timer.isInitialized) {
timer.setPosition(newSecond)
}
- // 미디어플레이어 시킹 (ms)
mediaPlayer?.let {
try {
val newMs = newSecond * 1000
@@ -169,7 +219,6 @@ class SongActivity : AppCompatActivity() {
}
}
- // 현재 위치 텍스트 즉시 반영
binding.songStartTimeTv.text = String.format(
Locale.getDefault(), "%02d:%02d", newSecond / 60, newSecond % 60
)
@@ -177,31 +226,29 @@ class SongActivity : AppCompatActivity() {
})
}
- /** 새로운 곡으로 전환(타이머/플레이어/UI/저장 동기화) */
+ /** 새로운 곡 재생 */
private fun playNewSong(newSong: Song) {
- // 타이머 교체
if (this::timer.isInitialized) timer.interrupt()
- // MediaPlayer 교체
mediaPlayer?.release()
mediaPlayer = null
- song = newSong.copy(second = 0, isPlaying = true) // 다음/이전 곡은 처음부터 재생
- setPlayer(song) // UI + MediaPlayer 재설정
- startTimer() // 새 타이머 시작
+ song = newSong.copy(second = 0, isPlaying = true)
+ songLoaded = true
+
+ setPlayer(song)
+ startTimer()
- // 현재 곡 저장 (미니플레이어 동기화)
getSharedPreferences("song", MODE_PRIVATE).edit()
.putInt("songId", song.id)
.apply()
- // (선택) 상태 DB 반영
lifecycleScope.launch(Dispatchers.IO) { db.songDao().update(song) }
}
/** 타이머 스레드 */
inner class Timer(private val playTime: Int, var isPlaying: Boolean = true) : Thread() {
- // 외부에서 위치 변경 가능하도록 메서드 제공
+
@Volatile private var second: Int = song.second
@Volatile private var mills: Float = song.second * 1000f
@@ -217,10 +264,12 @@ class SongActivity : AppCompatActivity() {
if (isPlaying) {
sleep(50)
mills += 50
+
runOnUiThread {
binding.songProgressSb.progress =
((mills / (playTime * 1000)) * 100).toInt()
}
+
if (mills % 1000 == 0f) {
runOnUiThread {
binding.songStartTimeTv.text = String.format(
@@ -237,16 +286,15 @@ class SongActivity : AppCompatActivity() {
}
}
- /** 좋아요 아이콘 렌더링 */
private fun renderLikeIcon(isLike: Boolean) {
if (isLike) binding.songLikeIv.setImageResource(R.drawable.ic_my_like_on)
else binding.songLikeIv.setImageResource(R.drawable.ic_my_like_off)
}
- /** 곡 정보/플레이어 UI 렌더링 + MediaPlayer 생성 */
private fun setPlayer(song: Song) {
binding.songMusicTitleTv.text = song.title
binding.songSingerNameTv.text = song.singer
+
binding.songStartTimeTv.text =
String.format(Locale.getDefault(), "%02d:%02d", song.second / 60, song.second % 60)
binding.songEndTimeTv.text =
@@ -257,15 +305,15 @@ class SongActivity : AppCompatActivity() {
val progress = if (song.playTime == 0) 0 else (song.second * 100 / song.playTime)
binding.songProgressSb.progress = progress
- // MediaPlayer 재생 준비
mediaPlayer?.release()
val musicResId = resources.getIdentifier(song.music, "raw", packageName)
if (musicResId == 0) {
- Log.e("SongActivity", "음악 리소스를 찾을 수 없습니다: ${song.music}")
+ Log.e("SongActivity", "음악 리소스 없음: ${song.music}")
return
}
+
mediaPlayer = MediaPlayer.create(this, musicResId)
- // 시작 위치 반영(예: 재방문 시 이어듣기)
+
if (song.second > 0) {
try {
mediaPlayer?.seekTo((song.second * 1000).coerceAtLeast(0))
@@ -273,7 +321,8 @@ class SongActivity : AppCompatActivity() {
Log.e("SongActivity", "초기 seek 실패: ${e.message}")
}
}
- attachAutoNextListener() // 곡 종료 시 다음 곡 자동 재생
- setPlayerStatus(song.isPlaying) // 재생/일시정지 상태 반영
+
+ attachAutoNextListener()
+ setPlayerStatus(song.isPlaying)
}
}
diff --git a/app/src/main/java/com/example/realflo/SongFragment.kt b/app/src/main/java/com/example/realflo/SongFragment.kt
index 1436b0b..3159a82 100644
--- a/app/src/main/java/com/example/realflo/SongFragment.kt
+++ b/app/src/main/java/com/example/realflo/SongFragment.kt
@@ -8,7 +8,6 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
-import com.example.realflo.databinding.FragmentDetailBinding
import com.example.realflo.databinding.FragmentSongBinding
import data.local.FloDatabase
import kotlinx.coroutines.Dispatchers
@@ -31,11 +30,13 @@ class SongFragment : Fragment() {
): View {
binding = FragmentSongBinding.inflate(inflater, container, false)
db = FloDatabase.getInstance(requireContext())
+ return binding.root
+ }
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
- loadSongs() // 최초 로드
-
- return binding.root
+ loadSongs() // 최초 로드
}
override fun onResume() {
@@ -50,20 +51,25 @@ class SongFragment : Fragment() {
// 곡 클릭 → SongActivity
adapter.setOnItemClickListener { song ->
val intent = Intent(requireContext(), SongActivity::class.java)
- intent.putExtra("songId", song.id) // 인텐트 키 통일!
+ intent.putExtra("songId", song.id)
startActivity(intent)
}
- // 하트 클릭 → DB 토글 + UI 업데이트
+ // 하트 클릭 → DB isLike 토글 + 리스트/아이콘 갱신
adapter.setOnLikeClickListener { song, position ->
- lifecycleScope.launch {
+ viewLifecycleOwner.lifecycleScope.launch {
val newLike = !song.isLike
+
+ // DB 업데이트
withContext(Dispatchers.IO) {
db.songDao().updateLike(song.id, newLike)
}
+
// 로컬 리스트 갱신
- songs[position] = song.copy(isLike = newLike)
- adapter.notifyItemChanged(position)
+ if (position in songs.indices) {
+ songs[position] = song.copy(isLike = newLike)
+ adapter.notifyItemChanged(position)
+ }
}
}
@@ -85,6 +91,3 @@ class SongFragment : Fragment() {
}
-
-
-
diff --git a/app/src/main/java/com/example/realflo/SongRVAdapter.kt b/app/src/main/java/com/example/realflo/SongRVAdapter.kt
index 690f3c2..1c31685 100644
--- a/app/src/main/java/com/example/realflo/SongRVAdapter.kt
+++ b/app/src/main/java/com/example/realflo/SongRVAdapter.kt
@@ -17,7 +17,7 @@ class SongRVAdapter(
onItemClick = listener
}
- /** 하트만 클릭 (원하면 다음 턴에 DB 토글 연결) */
+ /** 하트 클릭 콜백: (현재 Song, position) */
private var onLikeClick: ((Song, Int) -> Unit)? = null
fun setOnLikeClickListener(listener: (Song, Int) -> Unit) {
onLikeClick = listener
@@ -46,12 +46,20 @@ class SongRVAdapter(
val song = items[position]
holder.bind(song)
- // 아이템 전체 클릭
- holder.itemView.setOnClickListener { onItemClick?.invoke(song) }
+ // 곡 전체 클릭
+ holder.itemView.setOnClickListener {
+ val pos = holder.adapterPosition
+ if (pos != RecyclerView.NO_POSITION) {
+ onItemClick?.invoke(items[pos])
+ }
+ }
- // 하트만 클릭 (다음 턴에 DB 토글 연결 예정)
+ // 하트 클릭 → Fragment 에게 Song + position 전달
holder.likeIv.setOnClickListener {
- onLikeClick?.invoke(song, position)
+ val pos = holder.adapterPosition
+ if (pos != RecyclerView.NO_POSITION) {
+ onLikeClick?.invoke(items[pos], pos)
+ }
}
}
@@ -67,11 +75,10 @@ class SongRVAdapter(
singerTv.text = song.singer
// isLike 상태에 맞춰 하트 표시
- if (song.isLike) {
- likeIv.setImageResource(R.drawable.ic_my_like_on)
- } else {
- likeIv.setImageResource(R.drawable.ic_my_like_off)
- }
+ likeIv.setImageResource(
+ if (song.isLike) R.drawable.ic_my_like_on
+ else R.drawable.ic_my_like_off
+ )
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/realflo/User.kt b/app/src/main/java/com/example/realflo/User.kt
new file mode 100644
index 0000000..bbe5654
--- /dev/null
+++ b/app/src/main/java/com/example/realflo/User.kt
@@ -0,0 +1,15 @@
+package com.example.realflo
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.google.gson.annotations.SerializedName
+
+@Entity(tableName = "UserTable")
+data class User(
+ @SerializedName(value = "email")val email: String,
+ @SerializedName(value = "password")val password: String,
+ @SerializedName(value = "name")val name: String
+) {
+ @PrimaryKey(autoGenerate = true)
+ var id: Int = 0
+}
diff --git a/app/src/main/java/data/local/AlbumDao.kt b/app/src/main/java/data/local/AlbumDao.kt
index e86671b..3793e57 100644
--- a/app/src/main/java/data/local/AlbumDao.kt
+++ b/app/src/main/java/data/local/AlbumDao.kt
@@ -2,25 +2,36 @@ package data.local
import androidx.room.Dao
import androidx.room.Insert
+import androidx.room.OnConflictStrategy
import androidx.room.Query
-import androidx.room.Update
import com.example.realflo.Album
-import com.example.realflo.Song
@Dao
interface AlbumDao {
- @Insert
- suspend fun insert(album: Album)
-
- @Insert
- suspend fun insertAll(albums: List)
-
- @Update
- suspend fun update(album: Album)
+ // 모든 앨범 가져오기
@Query("SELECT * FROM AlbumTable")
- suspend fun getAlbums(): List
+ fun getAllAlbums(): List
+ // 특정 앨범 가져오기
@Query("SELECT * FROM AlbumTable WHERE id = :albumId")
- suspend fun getAlbum(albumId: Int): Album?
-}
\ No newline at end of file
+ fun getAlbumById(albumId: Int): Album?
+
+ // 좋아요 업데이트
+ @Query("UPDATE AlbumTable SET isLike = :isLike WHERE id = :albumId")
+ fun updateAlbumLike(albumId: Int, isLike: Boolean)
+
+ // 좋아요한 앨범만 가져오기
+ @Query("SELECT * FROM AlbumTable WHERE isLike = 1")
+ fun getLikedAlbums(): List
+
+ // 초기 데이터 넣기
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertAlbums(albums: List)
+
+ @Query("DELETE FROM AlbumTable WHERE id = :albumId")
+ suspend fun deleteAlbum(albumId: Int)
+
+ @Query("SELECT * FROM AlbumTable WHERE id = :albumId LIMIT 1")
+ suspend fun getAlbums(albumId: Int): Album?
+}
diff --git a/app/src/main/java/data/local/DbSeeder.kt b/app/src/main/java/data/local/DbSeeder.kt
index 99dad2d..1137ddd 100644
--- a/app/src/main/java/data/local/DbSeeder.kt
+++ b/app/src/main/java/data/local/DbSeeder.kt
@@ -13,27 +13,25 @@ object DbSeeder {
val songDao = db.songDao()
// Album 테이블에 데이터가 이미 있는지 확인
- if (albumDao.getAlbums().isNotEmpty()) {
+ if (albumDao.getAllAlbums().isNotEmpty()) {
return@withContext
}
// 앨범 더미
- albumDao.insertAll(
+ albumDao.insertAlbums(
listOf(
Album(
title = "NewJeans",
- singer = "NewJeans",
- description = "NewJeans의 다양한 음악이 담긴 EP"
+ singer = "NewJeans"
),
Album(
title = "IU Special",
- singer = "IU",
- description = "아이유 스페셜 앨범 모음"
+ singer = "IU"
)
)
)
- val albums = albumDao.getAlbums()
+ val albums = albumDao.getAllAlbums()
val newJeansId = albums.first { it.title == "NewJeans" }.id
val iuId = albums.first { it.title == "IU Special" }.id
@@ -44,11 +42,12 @@ object DbSeeder {
title = "Hype Boy",
singer = "NewJeans",
playTime = 210,
- albumIdx = newJeansId!!,
+ albumIdx = newJeansId,
music = "music_hypeboy"
),
- Song(title = "Ditto", singer = "NewJeans", playTime = 190, albumIdx = newJeansId!!),
- Song(title = "LILAC", singer = "IU", playTime = 230, albumIdx = iuId!!, music = "music_lilac")
+
+ Song(title = "Ditto", singer = "NewJeans", playTime = 190, albumIdx = newJeansId),
+ Song(title = "LILAC", singer = "IU", playTime = 230, albumIdx = iuId, music = "music_lilac")
)
)
}
diff --git a/app/src/main/java/data/local/FloDatabase.kt b/app/src/main/java/data/local/FloDatabase.kt
index bfc8b9e..465ca4e 100644
--- a/app/src/main/java/data/local/FloDatabase.kt
+++ b/app/src/main/java/data/local/FloDatabase.kt
@@ -6,10 +6,12 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.realflo.Album
import com.example.realflo.Song
+import com.example.realflo.User
+import data.local.UserDao
@Database(
- entities = [Song::class, Album::class],
- version = 3,
+ entities = [Song::class, Album::class, User::class],
+ version = 4,
exportSchema = false
)
abstract class FloDatabase : RoomDatabase() {
@@ -17,6 +19,8 @@ abstract class FloDatabase : RoomDatabase() {
abstract fun songDao(): SongDao
abstract fun albumDao(): AlbumDao
+ abstract fun userDao(): UserDao
+
companion object {
@Volatile private var INSTANCE: FloDatabase? = null
diff --git a/app/src/main/java/data/local/SongDao.kt b/app/src/main/java/data/local/SongDao.kt
index 015ae24..4e61312 100644
--- a/app/src/main/java/data/local/SongDao.kt
+++ b/app/src/main/java/data/local/SongDao.kt
@@ -14,6 +14,9 @@ interface SongDao {
@Insert
suspend fun insertAll(songs: List)
+ @Insert
+ suspend fun insertSongs(songs: List)
+
@Update
suspend fun update(song: Song)
diff --git a/app/src/main/java/data/local/UserDao.kt b/app/src/main/java/data/local/UserDao.kt
new file mode 100644
index 0000000..c9dd125
--- /dev/null
+++ b/app/src/main/java/data/local/UserDao.kt
@@ -0,0 +1,19 @@
+package data.local
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import com.example.realflo.User
+
+@Dao
+interface UserDao {
+
+ @Insert
+ suspend fun insert(user: User)
+
+ @Query("SELECT * FROM UserTable WHERE email = :email LIMIT 1")
+ suspend fun getUserByEmail(email: String): User?
+
+ @Query("SELECT * FROM UserTable WHERE email = :email AND password = :password LIMIT 1")
+ suspend fun login(email: String, password: String): User?
+}
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
new file mode 100644
index 0000000..253a6c4
--- /dev/null
+++ b/app/src/main/res/layout/activity_login.xml
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_signup.xml b/app/src/main/res/layout/activity_signup.xml
new file mode 100644
index 0000000..b5ef854
--- /dev/null
+++ b/app/src/main/res/layout/activity_signup.xml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_album.xml b/app/src/main/res/layout/fragment_album.xml
index 98420d0..721af3c 100644
--- a/app/src/main/res/layout/fragment_album.xml
+++ b/app/src/main/res/layout/fragment_album.xml
@@ -170,7 +170,7 @@
app:layout_constraintTop_toBottomOf="@+id/linearLayout4" />
diff --git a/app/src/main/res/layout/fragment_locker.xml b/app/src/main/res/layout/fragment_locker.xml
index 494bbf0..ba2cb5a 100644
--- a/app/src/main/res/layout/fragment_locker.xml
+++ b/app/src/main/res/layout/fragment_locker.xml
@@ -1,119 +1,62 @@
+ android:layout_height="match_parent"
+ tools:context=".LockerFragment">
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/locker_title_tv" />
-
+ android:layout_marginTop="16dp"
+ android:text="저장앨범"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/locker_title_tv" />
+ app:layout_constraintEnd_toEndOf="parent"
+ tools:listitem="@layout/item_song"/>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_album.xml b/app/src/main/res/layout/item_album.xml
index 5126cc5..847f3d0 100644
--- a/app/src/main/res/layout/item_album.xml
+++ b/app/src/main/res/layout/item_album.xml
@@ -2,7 +2,7 @@
diff --git a/app/src/main/res/layout/item_locker_album.xml b/app/src/main/res/layout/item_locker_album.xml
new file mode 100644
index 0000000..abdc0c1
--- /dev/null
+++ b/app/src/main/res/layout/item_locker_album.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 65c6e09..597f286 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,7 +6,7 @@ junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
-material = "1.13.0"
+material = "1.12.0"
activity = "1.11.0"
constraintlayout = "2.2.1"