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"