From 22c3c1d5d32eca91a94652594677bcdc241fa2c5 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Thu, 12 Feb 2026 14:42:24 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/account/view/AccountDeleteDialog1Fragment.kt | 6 ++++++ .../java/com/egobook/app/ui/account/view/AccountFragment.kt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt index 96997ab8..6771bc7c 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt @@ -1,5 +1,6 @@ package com.egobook.app.ui.account.view +import android.content.DialogInterface import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle @@ -76,6 +77,11 @@ class AccountDeleteDialog1Fragment : DialogFragment() { dismiss() } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + removeScreenBlur() + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index 6cfc1d5a..79b16339 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -184,7 +184,7 @@ class AccountFragment : Fragment() { applyScreenBlur(BlurLevel.BASE) val accountDeleteDialog1Fragment = AccountDeleteDialog1Fragment() - accountDeleteDialog1Fragment.isCancelable = false + accountDeleteDialog1Fragment.isCancelable = true accountDeleteDialog1Fragment.show(childFragmentManager, "AccountDeleteDialog1Fragment") } From a5e930be03f2c5df41af753b287dccb4e6f889cc Mon Sep 17 00:00:00 2001 From: princehw03 Date: Thu, 12 Feb 2026 15:31:08 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EB=8B=AC=EB=A0=A5=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=A0=84=ED=99=98=20=EA=B8=B0=EB=8A=A5=EB=93=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/diary/view/CalenderFragment.kt | 101 ++++++++++++++---- .../app/ui/diary/view/DiaryFragment.kt | 77 ++++++++----- .../app/ui/diary/view/MonthDialogFragment.kt | 86 ++++++++++++++- .../ui/diary/viewmodel/CalenderViewModel.kt | 40 +++++++ app/src/main/res/layout/fragment_calender.xml | 4 +- .../main/res/layout/fragment_month_dialog.xml | 4 +- .../main/res/navigation/bottom_navigation.xml | 25 ++++- 7 files changed, 284 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt index 67301d49..c3b42572 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt @@ -11,12 +11,18 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.children import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResultListener +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import com.egobook.app.BlurLevel import com.egobook.app.R import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.FragmentCalenderBinding import com.egobook.app.ui.diary.adapter.DayViewContainer +import com.egobook.app.ui.diary.viewmodel.CalenderViewModel import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.daysOfWeek @@ -26,11 +32,15 @@ import java.time.LocalDate import java.time.YearMonth import java.time.format.TextStyle import java.util.Locale +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch class CalenderFragment : Fragment() { private var _binding: FragmentCalenderBinding? = null private val binding get() = _binding!! + + private val viewModel: CalenderViewModel by viewModels() private val today = LocalDate.now() override fun onCreateView( @@ -52,28 +62,64 @@ class CalenderFragment : Fragment() { setupDayOfWeekTitles() setupCalendar() + observeViewModel() + setupMonthDialogResultListener() binding.apply { btnList.setOnClickListener { findNavController().popBackStack() } tvMonth.setOnClickListener { - applyScreenBlur(BlurLevel.BASE) // 블러 효과 적용 + applyScreenBlur(BlurLevel.BASE) - val dialog = MonthDialogFragment() + val dialog = MonthDialogFragment.newInstance( + viewModel.selectedYearMonth.value.year + ) dialog.isCancelable = true dialog.show(childFragmentManager, "MonthDialog") } btnPrevMonth.setOnClickListener { - binding.calendarView.findFirstVisibleMonth()?.let { - binding.calendarView.smoothScrollToMonth(it.yearMonth.minusMonths(1)) + viewModel.selectedYearMonth.value.minusMonths(1).let { + viewModel.setYearMonth(it) } } btnNextMonth.setOnClickListener { - binding.calendarView.findFirstVisibleMonth()?.let { - binding.calendarView.smoothScrollToMonth(it.yearMonth.plusMonths(1)) + viewModel.selectedYearMonth.value.plusMonths(1).let { + viewModel.setYearMonth(it) + } + } + } + } + + /** + * MonthDialogFragment에서 월 선택 결과 수신 + */ + private fun setupMonthDialogResultListener() { + childFragmentManager.setFragmentResultListener( + MonthDialogFragment.REQUEST_KEY_MONTH_SELECTED, + viewLifecycleOwner + ) { _, bundle -> + val year = bundle.getInt(MonthDialogFragment.BUNDLE_KEY_YEAR) + val month = bundle.getInt(MonthDialogFragment.BUNDLE_KEY_MONTH) + // 다이얼로그가 닫힌 후 ViewModel 업데이트 → 캘린더 스크롤 + viewModel.setYearMonth(YearMonth.of(year, month)) + } + } + + /** + * ViewModel 상태 관찰 - 스와이프 없이 해당 월 즉시 표시 + */ + private fun observeViewModel() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.selectedYearMonth.collectLatest { yearMonth -> + // 애니메이션 없이 해당 월 즉시 이동 + binding.calendarView.scrollToMonth(yearMonth) + // 상단 텍스트 업데이트 + binding.tvYear.text = yearMonth.year.toString() + binding.tvMonth.text = "${yearMonth.monthValue}월" } } } @@ -104,8 +150,10 @@ class CalenderFragment : Fragment() { initializeCalendarRange(currentMonth) initializeYearMonthText(currentMonth) - setupMonthScrollListener() setupDayBinder() + + // 초기 ViewModel 설정 + viewModel.setYearMonth(currentMonth) } /** @@ -127,17 +175,7 @@ class CalenderFragment : Fragment() { binding.tvYear.text = currentMonth.year.toString() binding.tvMonth.text = "${currentMonth.monthValue}월" } - - /** - * 월 스크롤 리스너 설정 - */ - private fun setupMonthScrollListener() { - binding.calendarView.monthScrollListener = { month -> - binding.tvYear.text = month.yearMonth.year.toString() - binding.tvMonth.text = "${month.yearMonth.monthValue}월" - } - } - + /** * 날짜 바인더 설정 */ @@ -160,7 +198,7 @@ class CalenderFragment : Fragment() { when { data.position != DayPosition.MonthDate -> bindOutOfMonthDate(container) - data.date == today -> bindTodayDate(container) + data.date == today -> bindTodayDate(container, data.date) else -> bindRegularDate(container, data.date) } } @@ -171,18 +209,24 @@ class CalenderFragment : Fragment() { private fun bindOutOfMonthDate(container: DayViewContainer) { container.binding.calendarDayText.visibility = View.INVISIBLE container.binding.dayEmotionImg.visibility = View.INVISIBLE + container.view.setOnClickListener(null) } /** * 오늘 날짜 바인딩 */ - private fun bindTodayDate(container: DayViewContainer) { + private fun bindTodayDate(container: DayViewContainer, date: LocalDate) { container.binding.calendarDayText.apply { visibility = View.VISIBLE setTextColor(Color.WHITE) setBackgroundResource(R.drawable.today_background) } container.binding.dayEmotionImg.visibility = View.GONE + + // 오늘 날짜 클릭 리스너 설정 + container.view.setOnClickListener { + navigateToDiaryWithDate(date) + } } /** @@ -195,6 +239,23 @@ class CalenderFragment : Fragment() { setTextColor(getDateTextColor(date)) } container.binding.dayEmotionImg.visibility = View.VISIBLE + + // 일반 날짜 클릭 리스너 설정 + container.view.setOnClickListener { + navigateToDiaryWithDate(date) + } + } + + /** + * 선택한 날짜를 DiaryFragment로 전달하고 이동 + */ + private fun navigateToDiaryWithDate(date: LocalDate) { + val action = CalenderFragmentDirections.actionCalenderFragmentToDiaryFragment( + selectedYear = date.year, + selectedMonth = date.monthValue, + selectedDay = date.dayOfMonth + ) + findNavController().navigate(action) } // ========== 색상 헬퍼 함수 ========== diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt index 4dd4ea70..96ff06ed 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt @@ -1,32 +1,30 @@ package com.egobook.app.ui.diary.view import android.os.Bundle - import android.view.Gravity - import android.view.LayoutInflater - import android.view.View - import android.view.ViewGroup - import android.widget.Toast - import android.graphics.Color - import androidx.fragment.app.Fragment - import androidx.fragment.app.activityViewModels - import androidx.lifecycle.Lifecycle - import androidx.lifecycle.lifecycleScope - import androidx.lifecycle.repeatOnLifecycle - import androidx.navigation.fragment.findNavController - import androidx.viewpager2.widget.ViewPager2 - import com.egobook.app.BlurLevel - import com.egobook.app.R - import com.egobook.app.applyScreenBlur - import com.egobook.app.databinding.FragmentDiaryBinding - import com.egobook.app.ui.diary.adapter.DiaryVPAdapter - import com.egobook.app.ui.diary.viewmodel.DiariesEvent - import com.egobook.app.ui.diary.viewmodel.DiariesViewModel - import com.google.android.material.snackbar.Snackbar - import com.google.android.material.tabs.TabLayout - import com.google.android.material.tabs.TabLayoutMediator - import kotlinx.coroutines.flow.collectLatest - import kotlinx.coroutines.launch - import kotlin.getValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.graphics.Color +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.viewpager2.widget.ViewPager2 +import com.egobook.app.BlurLevel +import com.egobook.app.R +import com.egobook.app.applyScreenBlur +import com.egobook.app.databinding.FragmentDiaryBinding +import com.egobook.app.ui.diary.adapter.DiaryVPAdapter +import com.egobook.app.ui.diary.viewmodel.DiariesEvent +import com.egobook.app.ui.diary.viewmodel.DiariesViewModel +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.getValue class DiaryFragment : Fragment() { private var _binding: FragmentDiaryBinding? = null private val binding get() = _binding!! @@ -54,11 +52,38 @@ // 초기에는 GoToTop 버튼 숨김 binding.btnGoToTop.visibility = View.GONE + // Navigation 인자로부터 선택된 날짜 확인 및 적용 + applySelectedDateFromArgs() + initViewPager() setupClickListener() observeViewModel() } + /** + * Navigation 인자로 전달된 날짜를 확인하고 적용 + */ + private fun applySelectedDateFromArgs() { + val args = arguments ?: return + + val year = args.getInt("selectedYear", -1) + val month = args.getInt("selectedMonth", -1) + val day = args.getInt("selectedDay", -1) + + // 유효한 날짜인 경우에만 적용 (-1은 기본값, 즉 인자가 전달되지 않은 경우) + if (year != -1 && month != -1 && day != -1) { + viewModel.onEvent( + DiariesEvent.ChangeDate( + year = year, + month = month, + day = day + ) + ) + // "전체" 탭으로 이동 + binding.vpDiary.setCurrentItem(0, false) + } + } + override fun onResume() { super.onResume() //다른 프래그먼트에서 돌아왔을 때 데이터 새로고침 diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/MonthDialogFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/MonthDialogFragment.kt index 02aecaa6..2f629438 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/MonthDialogFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/MonthDialogFragment.kt @@ -7,17 +7,46 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment +import androidx.fragment.app.setFragmentResult import com.egobook.app.databinding.FragmentMonthDialogBinding import com.egobook.app.removeScreenBlur +import com.egobook.app.R +import com.google.android.material.button.MaterialButton +import java.time.YearMonth class MonthDialogFragment : DialogFragment() { private var _binding: FragmentMonthDialogBinding? = null private val binding get() = _binding!! - // 부모로부터 "닫힐 때 실행할 코드"를 전달받을 람다 변수 - var onDismissListener: (() -> Unit)? = null + // 로컬 상태 - 다이얼로그 내에서만 사용 (캘린더에 반영되지 않음) + private var selectedYear: Int = YearMonth.now().year + + // 월 버튼 ID 리스트 + private val monthButtonIds = listOf( + R.id.btn_january, + R.id.btn_february, + R.id.btn_march, + R.id.btn_april, + R.id.btn_may, + R.id.btn_june, + R.id.btn_july, + R.id.btn_august, + R.id.btn_september, + R.id.btn_october, + R.id.btn_november, + R.id.btn_december + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // 부모 프래그먼트에서 현재 년도 가져오기 (인자로 전달받거나) + arguments?.getInt(ARG_INITIAL_YEAR)?.let { year -> + selectedYear = year + } + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -32,6 +61,44 @@ class MonthDialogFragment : DialogFragment() { super.onViewCreated(view, savedInstanceState) isCancelable = true dialog?.setCanceledOnTouchOutside(true) + + // 초기 년도 표시 + binding.tvDialogYear.text = selectedYear.toString() + + setupClickListeners() + } + + /** + * 클릭 리스너 설정 + */ + private fun setupClickListeners() { + // 이전 년도 버튼 - 로컬 상태만 변경 + binding.btnPrevYear.setOnClickListener { + selectedYear-- + binding.tvDialogYear.text = selectedYear.toString() + } + + // 다음 년도 버튼 - 로컬 상태만 변경 + binding.btnNextYear.setOnClickListener { + selectedYear++ + binding.tvDialogYear.text = selectedYear.toString() + } + + // 월 선택 버튼들 (1월~12월) + monthButtonIds.forEachIndexed { index, buttonId -> + binding.root.findViewById(buttonId)?.setOnClickListener { + // 다이얼로그 닫힌 후 처리를 위해 Fragment Result로 년/월 정보 전달 + setFragmentResult( + REQUEST_KEY_MONTH_SELECTED, + bundleOf( + BUNDLE_KEY_YEAR to selectedYear, + BUNDLE_KEY_MONTH to (index + 1) + ) + ) + removeScreenBlur() + dismiss() + } + } } override fun onCancel(dialog: DialogInterface) { @@ -44,4 +111,19 @@ class MonthDialogFragment : DialogFragment() { super.onDestroyView() _binding = null } + + companion object { + const val REQUEST_KEY_MONTH_SELECTED = "month_selected_key" + const val BUNDLE_KEY_YEAR = "year" + const val BUNDLE_KEY_MONTH = "month" + const val ARG_INITIAL_YEAR = "initial_year" + + fun newInstance(initialYear: Int): MonthDialogFragment { + return MonthDialogFragment().apply { + arguments = Bundle().apply { + putInt(ARG_INITIAL_YEAR, initialYear) + } + } + } + } } diff --git a/app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt new file mode 100644 index 00000000..c30e1143 --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt @@ -0,0 +1,40 @@ +package com.egobook.app.ui.diary.viewmodel + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.time.YearMonth + +class CalenderViewModel : ViewModel() { + + private val _selectedYearMonth = MutableStateFlow(YearMonth.now()) + val selectedYearMonth: StateFlow = _selectedYearMonth + + /** + * 이전 년도로 이동 + */ + fun onPreviousYear() { + _selectedYearMonth.value = _selectedYearMonth.value.minusYears(1) + } + + /** + * 다음 년도로 이동 + */ + fun onNextYear() { + _selectedYearMonth.value = _selectedYearMonth.value.plusYears(1) + } + + /** + * 특정 월 선택 (현재 선택된 년도에 해당 월 적용) + */ + fun onMonthSelected(month: Int) { + _selectedYearMonth.value = _selectedYearMonth.value.withMonth(month) + } + + /** + * 특정 년월로 설정 + */ + fun setYearMonth(yearMonth: YearMonth) { + _selectedYearMonth.value = yearMonth + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_calender.xml b/app/src/main/res/layout/fragment_calender.xml index fbf9ec67..89abf74c 100644 --- a/app/src/main/res/layout/fragment_calender.xml +++ b/app/src/main/res/layout/fragment_calender.xml @@ -116,7 +116,7 @@ android:paddingBottom="4dp" app:layout_constraintTop_toBottomOf="@id/calendar_header_layout"/> - + + + + + + + + android:label="달력 화면" > + + + + Date: Thu, 12 Feb 2026 15:44:11 +0900 Subject: [PATCH 3/8] =?UTF-8?q?refactor:=20=EB=A7=88=EC=A7=80=EB=A7=89?= =?UTF-8?q?=EC=97=90=20=EC=84=A0=ED=83=9D=ED=95=9C=20=EB=82=A0=EC=A7=9C?= =?UTF-8?q?=EC=97=90=20=ED=95=B4=EB=8B=B9=ED=95=98=EB=8A=94=20=EC=9D=BC?= =?UTF-8?q?=EA=B8=B0=EA=B0=80=20=EB=9C=A8=EB=8F=84=EB=A1=9D=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/diary/view/DiaryFragment.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt index 96ff06ed..a578d2cc 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt @@ -24,7 +24,8 @@ import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.getValue + import java.time.LocalDate + import kotlin.getValue class DiaryFragment : Fragment() { private var _binding: FragmentDiaryBinding? = null private val binding get() = _binding!! @@ -52,7 +53,7 @@ import kotlin.getValue // 초기에는 GoToTop 버튼 숨김 binding.btnGoToTop.visibility = View.GONE - // Navigation 인자로부터 선택된 날짜 확인 및 적용 + // 캘린더에서 선택한 날짜가 있으면 적용 (없으면 마지막 선택 날짜 유지) applySelectedDateFromArgs() initViewPager() @@ -61,16 +62,17 @@ import kotlin.getValue } /** - * Navigation 인자로 전달된 날짜를 확인하고 적용 + * 캘린더에서 선택한 날짜 적용 (있는 경우에만) + * 다른 화면에서는 마지막 선택된 날짜 유지 */ private fun applySelectedDateFromArgs() { - val args = arguments ?: return + val args = arguments - val year = args.getInt("selectedYear", -1) - val month = args.getInt("selectedMonth", -1) - val day = args.getInt("selectedDay", -1) + val year = args?.getInt("selectedYear", -1) ?: -1 + val month = args?.getInt("selectedMonth", -1) ?: -1 + val day = args?.getInt("selectedDay", -1) ?: -1 - // 유효한 날짜인 경우에만 적용 (-1은 기본값, 즉 인자가 전달되지 않은 경우) + // 캘린더에서 유효한 날짜가 전달된 경우에만 적용 if (year != -1 && month != -1 && day != -1) { viewModel.onEvent( DiariesEvent.ChangeDate( @@ -86,7 +88,7 @@ import kotlin.getValue override fun onResume() { super.onResume() - //다른 프래그먼트에서 돌아왔을 때 데이터 새로고침 + // 다른 화면에서 돌아왔을 때 데이터 새로고침만 수행 (날짜는 유지) viewModel.onEvent(DiariesEvent.RefreshDiaries) } From a350708144b93b954196e07187afad80f1697c1f Mon Sep 17 00:00:00 2001 From: princehw03 Date: Thu, 12 Feb 2026 15:55:03 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=EB=A7=88=EC=A7=80=EB=A7=89=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=9D=BC=EA=B8=B0=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=EA=B0=80=20=EC=BA=90=EC=8B=B1=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/egobook/app/ui/diary/view/DiaryFragment.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt index a578d2cc..6d2d114e 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt @@ -24,8 +24,7 @@ import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch - import java.time.LocalDate - import kotlin.getValue +import kotlin.getValue class DiaryFragment : Fragment() { private var _binding: FragmentDiaryBinding? = null private val binding get() = _binding!! @@ -84,6 +83,9 @@ import kotlinx.coroutines.launch // "전체" 탭으로 이동 binding.vpDiary.setCurrentItem(0, false) } + + // 인자 사용 후 초기화 (다음 진입 시 재적용 방지) + arguments = null } override fun onResume() { From d1767c2f18f3a91c26edf1ad124a076d6900c65f Mon Sep 17 00:00:00 2001 From: princehw03 Date: Thu, 12 Feb 2026 18:28:00 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EB=8B=AC=EB=A0=A5api=EC=97=B0?= =?UTF-8?q?=EB=8F=99=201=EC=B0=A8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/data/api/CalenderApiService.kt | 14 +++ .../model/diary/request/DiaryExportRequest.kt | 11 ++ .../model/diary/response/CalenderResponse.kt | 32 ++++++ .../diary/CalenderRepositoryImpl.kt | 37 ++++++ .../repository/diary/DiaryRepositoryImpl.kt | 2 - .../egobook/app/di/module/RepositoryModule.kt | 6 + .../egobook/app/di/module/ServiceModule.kt | 6 + .../egobook/app/di/module/UseCaseModule.kt | 11 ++ .../app/domain/model/calender/CalenderDate.kt | 40 +++++++ .../model/diary/mapper/CalenderMapper.kt | 31 +++++ .../repository/diary/CalenderRepository.kt | 9 ++ .../app/domain/usecase/CalenderUserCase.kt | 25 ++++ .../ui/diary/mapper/CalenderEntityMapper.kt | 48 ++++++++ .../app/ui/diary/view/CalenderFragment.kt | 83 ++++++++++++-- .../ui/diary/viewmodel/CalenderViewModel.kt | 108 ++++++++++++++++-- 15 files changed, 442 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/egobook/app/data/api/CalenderApiService.kt create mode 100644 app/src/main/java/com/egobook/app/data/model/diary/request/DiaryExportRequest.kt create mode 100644 app/src/main/java/com/egobook/app/data/model/diary/response/CalenderResponse.kt create mode 100644 app/src/main/java/com/egobook/app/data/repository/diary/CalenderRepositoryImpl.kt create mode 100644 app/src/main/java/com/egobook/app/domain/model/calender/CalenderDate.kt create mode 100644 app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt create mode 100644 app/src/main/java/com/egobook/app/domain/repository/diary/CalenderRepository.kt create mode 100644 app/src/main/java/com/egobook/app/domain/usecase/CalenderUserCase.kt create mode 100644 app/src/main/java/com/egobook/app/ui/diary/mapper/CalenderEntityMapper.kt diff --git a/app/src/main/java/com/egobook/app/data/api/CalenderApiService.kt b/app/src/main/java/com/egobook/app/data/api/CalenderApiService.kt new file mode 100644 index 00000000..f9474ada --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/api/CalenderApiService.kt @@ -0,0 +1,14 @@ +package com.egobook.app.data.api + +import com.egobook.app.data.model.ApiResponse +import com.egobook.app.data.model.diary.response.CalenderData +import retrofit2.http.GET +import retrofit2.http.Query + +interface CalenderApiService { + //선택한 연도/월의 날짜별로 가장 많이 기록된 대표 감정 단계를 확인 + @GET("/diaries/calendar") + suspend fun getCalender( + @Query("month") month: String, + ): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/model/diary/request/DiaryExportRequest.kt b/app/src/main/java/com/egobook/app/data/model/diary/request/DiaryExportRequest.kt new file mode 100644 index 00000000..16fd737f --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/diary/request/DiaryExportRequest.kt @@ -0,0 +1,11 @@ +package com.egobook.app.data.model.diary.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DiaryExportRequest ( + @SerialName("format") + val format: String, + +) \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/model/diary/response/CalenderResponse.kt b/app/src/main/java/com/egobook/app/data/model/diary/response/CalenderResponse.kt new file mode 100644 index 00000000..8b951859 --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/diary/response/CalenderResponse.kt @@ -0,0 +1,32 @@ +package com.egobook.app.data.model.diary.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CalenderResponse( + @SerialName("code") + val code: String, + @SerialName("message") + val message: String, + @SerialName("status") + val status: Int, + @SerialName("data") + val data: CalenderData? // nullable로 변경 +) + +@Serializable +data class CalenderData( + @SerialName("month") + val month: String, + @SerialName("days") + val days: List? = null // nullable + 기본값 +) + +@Serializable +data class CalenderDay( + @SerialName("date") + val date: String, + @SerialName("emotionLevel") + val emotionLevel: Int +) diff --git a/app/src/main/java/com/egobook/app/data/repository/diary/CalenderRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/diary/CalenderRepositoryImpl.kt new file mode 100644 index 00000000..891eb0e9 --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/repository/diary/CalenderRepositoryImpl.kt @@ -0,0 +1,37 @@ +package com.egobook.app.data.repository.diary + +import android.util.Log +import com.egobook.app.data.api.CalenderApiService +import com.egobook.app.data.model.diary.response.CalenderData +import com.egobook.app.data.util.safeApiCall +import com.egobook.app.domain.model.calender.CalenderDate +import com.egobook.app.domain.model.diary.mapper.CalenderMapper +import com.egobook.app.domain.repository.diary.CalenderRepository +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +class CalenderRepositoryImpl @Inject constructor( + private val apiService: CalenderApiService +) : CalenderRepository { + + override suspend fun getCalender(yearMonth: LocalDate): Result> { + val monthString = yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")) + Log.d("Repository", "Calling API with monthString: $monthString") + + val result = safeApiCall( + apiCall = { + val response = apiService.getCalender(monthString) + Log.d("Repository", "Raw API Response - code: ${response.code}, data: ${response.data}") + response + }, + transform = { calenderData: CalenderData -> + Log.d("Repository", "Transforming data: month=${calenderData.month}, days=${calenderData.days}") + CalenderMapper.dataToDomainList(calenderData) + } + ) + + Log.d("Repository", "Result: $result") + return result + } +} diff --git a/app/src/main/java/com/egobook/app/data/repository/diary/DiaryRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/diary/DiaryRepositoryImpl.kt index 56d8900d..68208d32 100644 --- a/app/src/main/java/com/egobook/app/data/repository/diary/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/diary/DiaryRepositoryImpl.kt @@ -4,13 +4,11 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.egobook.app.data.api.DiaryApiService -import com.egobook.app.data.model.diary.request.DiaryCreateRequest import com.egobook.app.data.repository.diary.paging.DiariesPagingSource import com.egobook.app.data.util.safeApiCall import com.egobook.app.domain.model.diary.entity.Diary import com.egobook.app.domain.model.diary.entity.DiaryFilter import com.egobook.app.domain.model.diary.entity.DiarySummary -import com.egobook.app.domain.model.diary.entity.DiaryType import com.egobook.app.domain.model.diary.mapper.DiaryMapper.toDiaryCreateRequest import com.egobook.app.domain.model.diary.mapper.DiaryMapper.toDiaryEntity import com.egobook.app.domain.model.diary.mapper.DiaryMapper.toDiaryUpdateRequest diff --git a/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt b/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt index 6bc50469..a95e6abb 100644 --- a/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt +++ b/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt @@ -8,6 +8,7 @@ import com.egobook.app.domain.repository.CounselingRepository import com.egobook.app.data.repository.auth.AuthRepositoryImpl import com.egobook.app.data.repository.QuestionRepositoryImpl import com.egobook.app.data.repository.account.AccountRepositoryImpl +import com.egobook.app.data.repository.diary.CalenderRepositoryImpl import com.egobook.app.data.repository.diary.DiaryRepositoryImpl import com.egobook.app.domain.repository.FriendsRepository import com.egobook.app.domain.repository.LetterRepository @@ -18,6 +19,7 @@ import com.egobook.app.ui.shop.StoreRepository import dagger.Binds import com.egobook.app.domain.repository.QuestionRepository import com.egobook.app.domain.repository.account.AccountRepository +import com.egobook.app.domain.repository.diary.CalenderRepository import com.egobook.app.domain.repository.diary.DiaryRepository import com.egobook.app.ui.home.repository.NetworkPsychologyService import com.egobook.app.ui.home.repository.HomeNotificationRepository @@ -91,4 +93,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindHomeNotificationRepository(impl: NetworkHomeNotificationRepository): HomeNotificationRepository + + @Binds + @Singleton + abstract fun bindCalenderRepository(impl: CalenderRepositoryImpl): CalenderRepository } diff --git a/app/src/main/java/com/egobook/app/di/module/ServiceModule.kt b/app/src/main/java/com/egobook/app/di/module/ServiceModule.kt index f33a3e2b..59391213 100644 --- a/app/src/main/java/com/egobook/app/di/module/ServiceModule.kt +++ b/app/src/main/java/com/egobook/app/di/module/ServiceModule.kt @@ -3,6 +3,7 @@ package com.egobook.app.di.module import com.egobook.app.data.api.AIApiService import com.egobook.app.data.api.AccountApiService import com.egobook.app.data.api.AuthApiService +import com.egobook.app.data.api.CalenderApiService import com.egobook.app.data.api.CounselingApiService import com.egobook.app.data.api.DiaryApiService import com.egobook.app.data.api.FriendsApiService @@ -73,4 +74,9 @@ object ServiceModule { @Singleton fun provideDiaryService(@BackendApi retrofit: Retrofit): DiaryApiService = retrofit.create(DiaryApiService::class.java) + + @Provides + @Singleton + fun provideCalenderService(@BackendApi retrofit: Retrofit): CalenderApiService = + retrofit.create(CalenderApiService::class.java) } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/di/module/UseCaseModule.kt b/app/src/main/java/com/egobook/app/di/module/UseCaseModule.kt index 9895dd13..cc7ba6e1 100644 --- a/app/src/main/java/com/egobook/app/di/module/UseCaseModule.kt +++ b/app/src/main/java/com/egobook/app/di/module/UseCaseModule.kt @@ -2,7 +2,10 @@ package com.egobook.app.di.module import com.egobook.app.domain.repository.diary.FakeDiaryRepository import com.egobook.app.domain.repository.auth.AuthRepository +import com.egobook.app.domain.repository.diary.CalenderRepository import com.egobook.app.domain.repository.diary.DiaryRepository +import com.egobook.app.domain.usecase.CalenderUseCases +import com.egobook.app.domain.usecase.GetCalender import com.egobook.app.domain.usecase.authusecase.AuthUseCases import com.egobook.app.domain.usecase.authusecase.GoogleAutoLogin import com.egobook.app.domain.usecase.authusecase.GoogleLogin @@ -54,4 +57,12 @@ object UseCaseModule { ) } + @Provides + @Singleton + fun provideCalenderUseCases(repository: CalenderRepository): CalenderUseCases { + return CalenderUseCases( + getCalender = GetCalender(repository) + ) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/model/calender/CalenderDate.kt b/app/src/main/java/com/egobook/app/domain/model/calender/CalenderDate.kt new file mode 100644 index 00000000..14805128 --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/model/calender/CalenderDate.kt @@ -0,0 +1,40 @@ +package com.egobook.app.domain.model.calender + +import java.time.LocalDate + +/** + * 캘린더의 각 날짜 정보 + * @param date 날짜 (ISO-8601) + * @param emotionLevel 감정 단계 (1~4, null이면 기록 없음) + * UI에서 1~4 값에 해당하는 이미지로 매핑 + * 1: 매우 나쁨, 2: 나쁨, 3: 보통, 4: 좋음 + */ +data class CalenderDate( + val date: LocalDate, //2026-02 형식 + val emotionLevel: Int? +) { + /** 해당 월의 몇 일인지 (1~31) */ + val dayOfMonth: Int + get() = date.dayOfMonth + + /** 주말 여부 (토요일=6, 일요일=7) */ + val isWeekend: Boolean + get() = date.dayOfWeek.value >= 6 + + /** 일요일 여부 (텍스트 색상용) */ + val isSunday: Boolean + get() = date.dayOfWeek.value == 7 + + /** 토요일 여부 */ + val isSaturday: Boolean + get() = date.dayOfWeek.value == 6 + + /** 오늘 날짜인지 */ + val isToday: Boolean + get() = date == LocalDate.now() + + /** 감정 기록이 있는지 (null이 아닌지) */ + val hasEmotion: Boolean + get() = emotionLevel != null + +} diff --git a/app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt b/app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt new file mode 100644 index 00000000..a179339f --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt @@ -0,0 +1,31 @@ +package com.egobook.app.domain.model.diary.mapper + +import com.egobook.app.data.model.diary.response.CalenderData +import com.egobook.app.data.model.diary.response.CalenderDay +import com.egobook.app.domain.model.calender.CalenderDate +import java.time.LocalDate + +/** + * Data Layer ↔ Domain Layer 변환 Mapper + */ +object CalenderMapper { + + /** + * CalenderData를 도메인 객체 리스트로 변환 + */ + fun dataToDomainList(calenderData: CalenderData): List { + return calenderData.days?.map { day -> + dayToDomain(day) + } ?: emptyList() + } + + /** + * 개별 날짜 데이터를 도메인 객체로 변환 + */ + private fun dayToDomain(day: CalenderDay): CalenderDate { + return CalenderDate( + date = LocalDate.parse(day.date), + emotionLevel = day.emotionLevel + ) + } +} diff --git a/app/src/main/java/com/egobook/app/domain/repository/diary/CalenderRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/diary/CalenderRepository.kt new file mode 100644 index 00000000..e5af5337 --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/repository/diary/CalenderRepository.kt @@ -0,0 +1,9 @@ +package com.egobook.app.domain.repository.diary + +import com.egobook.app.domain.model.calender.CalenderDate +import java.time.LocalDate + +interface CalenderRepository { + + suspend fun getCalender(yearMonth: LocalDate): Result> +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/usecase/CalenderUserCase.kt b/app/src/main/java/com/egobook/app/domain/usecase/CalenderUserCase.kt new file mode 100644 index 00000000..e47d019f --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/usecase/CalenderUserCase.kt @@ -0,0 +1,25 @@ +package com.egobook.app.domain.usecase + +import com.egobook.app.domain.model.calender.CalenderDate +import com.egobook.app.domain.repository.diary.CalenderRepository +import java.time.LocalDate +import javax.inject.Inject + +/** + * 캘린더 관련 유스케이스 래퍼 클래스 + * 의존성 주입을 쉽게 하기 위한 래퍼 + */ +data class CalenderUseCases @Inject constructor( + val getCalender: GetCalender +) + +/** + * 특정 월의 캘린더 데이터를 가져오는 유스케이스 + */ +class GetCalender @Inject constructor( + private val repository: CalenderRepository +) { + suspend operator fun invoke(yearMonth: LocalDate): Result> { + return repository.getCalender(yearMonth) + } +} diff --git a/app/src/main/java/com/egobook/app/ui/diary/mapper/CalenderEntityMapper.kt b/app/src/main/java/com/egobook/app/ui/diary/mapper/CalenderEntityMapper.kt new file mode 100644 index 00000000..aa53caf0 --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/diary/mapper/CalenderEntityMapper.kt @@ -0,0 +1,48 @@ +package com.egobook.app.ui.diary.mapper + +import com.egobook.app.domain.model.calender.CalenderDate +import java.time.LocalDate +import java.time.YearMonth +import java.time.format.DateTimeFormatter + +/** + * 캘린더 도메인 모델 ↔ UI 모델 변환 매퍼 + */ +object CalenderEntityMapper { + + private val YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM") + + /** + * YearMonth를 API 요청용 문자열로 변환 + * @return "2026-02" 형식의 문자열 + */ + fun yearMonthToString(yearMonth: YearMonth): String { + return yearMonth.format(YEAR_MONTH_FORMATTER) + } + + /** + * 문자열을 YearMonth로 변환 + * @param yearMonthString "2026-02" 형식의 문자열 + */ + fun stringToYearMonth(yearMonthString: String): YearMonth { + return YearMonth.parse(yearMonthString, YEAR_MONTH_FORMATTER) + } + + /** + * CalenderDate 리스트를 Map으로 변환 (UI에서 O(1) 조회용) + * Key: LocalDate, Value: emotionLevel + */ + fun toDateEmotionMap(calenderDates: List): Map { + return calenderDates.associate { it.date to it.emotionLevel } + } + + /** + * 특정 날짜의 감정 레벨 조회 (매핑된 Map에서) + */ + fun getEmotionLevel( + dateMap: Map, + date: LocalDate + ): Int? { + return dateMap[date] + } +} diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt index c3b42572..edaaa4ae 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt @@ -2,6 +2,7 @@ package com.egobook.app.ui.diary.view import android.graphics.Color import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,7 +12,6 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.children import androidx.fragment.app.Fragment -import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -23,10 +23,12 @@ import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.FragmentCalenderBinding import com.egobook.app.ui.diary.adapter.DayViewContainer import com.egobook.app.ui.diary.viewmodel.CalenderViewModel +import com.egobook.app.util.UiState import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.view.MonthDayBinder +import dagger.hilt.android.AndroidEntryPoint import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth @@ -34,7 +36,9 @@ import java.time.format.TextStyle import java.util.Locale import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import timber.log.Timber +@AndroidEntryPoint class CalenderFragment : Fragment() { private var _binding: FragmentCalenderBinding? = null @@ -73,20 +77,20 @@ class CalenderFragment : Fragment() { applyScreenBlur(BlurLevel.BASE) val dialog = MonthDialogFragment.newInstance( - viewModel.selectedYearMonth.value.year + viewModel.selectedYearMonth.year ) dialog.isCancelable = true dialog.show(childFragmentManager, "MonthDialog") } btnPrevMonth.setOnClickListener { - viewModel.selectedYearMonth.value.minusMonths(1).let { + viewModel.selectedYearMonth.minusMonths(1).let { viewModel.setYearMonth(it) } } btnNextMonth.setOnClickListener { - viewModel.selectedYearMonth.value.plusMonths(1).let { + viewModel.selectedYearMonth.plusMonths(1).let { viewModel.setYearMonth(it) } } @@ -114,12 +118,35 @@ class CalenderFragment : Fragment() { private fun observeViewModel() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.selectedYearMonth.collectLatest { yearMonth -> + viewModel.state.collectLatest { state -> // 애니메이션 없이 해당 월 즉시 이동 - binding.calendarView.scrollToMonth(yearMonth) + binding.calendarView.scrollToMonth(state.selectedYearMonth) // 상단 텍스트 업데이트 - binding.tvYear.text = yearMonth.year.toString() - binding.tvMonth.text = "${yearMonth.monthValue}월" + binding.tvYear.text = state.selectedYearMonth.year.toString() + binding.tvMonth.text = "${state.selectedYearMonth.monthValue}월" + // 중요: 감정 데이터 변경 시 해당 월만 캘린더 뷰 갱신 + binding.calendarView.notifyMonthChanged(state.selectedYearMonth) + } + } + } + + // 로딩 상태 관찰 (로딩 뷰용) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.calenderLoadState.collectLatest { loadState -> + when (loadState) { + is UiState.Loading -> { + // TODO: 로딩 뷰 표시 + } + is UiState.Success -> { + // TODO: 로딩 뷰 숨김 + // notifyCalendarChanged()는 state 관찰에서 처리 + } + is UiState.Failure -> { + // TODO: 에러 처리 + } + else -> {} + } } } } @@ -151,9 +178,7 @@ class CalenderFragment : Fragment() { initializeCalendarRange(currentMonth) initializeYearMonthText(currentMonth) setupDayBinder() - - // 초기 ViewModel 설정 - viewModel.setYearMonth(currentMonth) + // 초기 로드는 ViewModel의 init 블록에서 처리 } /** @@ -201,6 +226,42 @@ class CalenderFragment : Fragment() { data.date == today -> bindTodayDate(container, data.date) else -> bindRegularDate(container, data.date) } + + // 감정 이미지 바인딩 (API 데이터에서 조회) + bindEmotionImage(container, data.date) + } + + /** + * 감정 이미지 바인딩 + */ + private fun bindEmotionImage(container: DayViewContainer, date: LocalDate) { + val emotionLevel = viewModel.getEmotionLevel(date) + + Timber.tag("CalenderDebug").d("Date: $date, EmotionLevel: $emotionLevel, MapKeys: ${viewModel.state.value.dateEmotionMap.keys}") + + if (emotionLevel != null) { + // 감정 레벨에 따른 이미지 설정 (1~5 유효, 그 외는 기본 이미지) + val emotionDrawable = getEmotionDrawable(emotionLevel) + container.binding.dayEmotionImg.setImageResource(emotionDrawable) + container.binding.dayEmotionImg.visibility = View.VISIBLE + } else { + // 감정 기록 없음 + container.binding.dayEmotionImg.visibility = View.INVISIBLE + } + } + + /** + * 감정 레벨에 따른 Drawable 리소스 반환 + */ + private fun getEmotionDrawable(level: Int): Int { + return when (level) { + 1 -> R.drawable.img_emotion_very_sad_unselectd // 오타 있는 파일명 그대로 사용 + 2 -> R.drawable.img_emotion_sad_unselected + 3 -> R.drawable.img_emotion_neutral_unselected + 4 -> R.drawable.img_emotion_happy_unselected + 5 -> R.drawable.img_emotion_very_happy_unselected + else -> R.drawable.img_emotion_neutral_unselected + } } /** diff --git a/app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt index c30e1143..b8592ef2 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/CalenderViewModel.kt @@ -1,40 +1,132 @@ package com.egobook.app.ui.diary.viewmodel import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.egobook.app.domain.model.calender.CalenderDate +import com.egobook.app.domain.usecase.CalenderUseCases +import com.egobook.app.ui.diary.mapper.CalenderEntityMapper +import com.egobook.app.util.UiState +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.time.LocalDate import java.time.YearMonth +import javax.inject.Inject +import android.util.Log -class CalenderViewModel : ViewModel() { +@HiltViewModel +class CalenderViewModel @Inject constructor( + private val calenderUseCases: CalenderUseCases +) : ViewModel() { - private val _selectedYearMonth = MutableStateFlow(YearMonth.now()) - val selectedYearMonth: StateFlow = _selectedYearMonth + private val _state = MutableStateFlow(CalenderState()) + val state: StateFlow = _state.asStateFlow() + + // 캘린더 데이터 로딩 상태 (로딩 뷰용) + private val _calenderLoadState = MutableStateFlow>>(UiState.Idle) + val calenderLoadState: StateFlow>> = _calenderLoadState.asStateFlow() + + // UI 상태 - 선택된 년월 + val selectedYearMonth: YearMonth + get() = _state.value.selectedYearMonth + + /** + * 초기 로드 + */ + init { + Log.d("ViewModel1", "=== ViewModel INIT === selectedYearMonth=${_state.value.selectedYearMonth}") + loadCalender(_state.value.selectedYearMonth) + } + + /** + * 특정 월의 캘린더 데이터 로드 + */ + fun loadCalender(yearMonth: YearMonth) { + viewModelScope.launch { + Log.d("ViewModel1", "=== loadCalender START === yearMonth=$yearMonth") + _calenderLoadState.value = UiState.Loading + + // YearMonth의 첫날을 LocalDate로 변환하여 API 호출 + val firstDayOfMonth = yearMonth.atDay(1) + Log.d("ViewModel1", "firstDayOfMonth=$firstDayOfMonth, calling API...") + + calenderUseCases.getCalender(firstDayOfMonth) + .onSuccess { calenderDates -> + Log.d("ViewModel1", "API Success, dates count: ${calenderDates.size}") + val emotionMap = CalenderEntityMapper.toDateEmotionMap(calenderDates) + Log.d("ViewModel1", "Map created with keys: ${emotionMap.keys}") + _calenderLoadState.value = UiState.Success(calenderDates) + _state.update { state -> + state.copy( + selectedYearMonth = yearMonth, + calenderDates = calenderDates, + dateEmotionMap = emotionMap + ) + } + Log.d("ViewModel1", "State updated, current map keys: ${_state.value.dateEmotionMap.keys}") + } + .onFailure { exception -> + Log.e("ViewModel1", "API Failure: ${exception.message}", exception) + _calenderLoadState.value = UiState.Failure(exception.message) + _state.update { state -> + state.copy( + selectedYearMonth = yearMonth, + calenderDates = emptyList(), + dateEmotionMap = emptyMap() + ) + } + } + Log.d("ViewModel1", "=== loadCalender END ===") + } + } /** * 이전 년도로 이동 */ fun onPreviousYear() { - _selectedYearMonth.value = _selectedYearMonth.value.minusYears(1) + val newYearMonth = _state.value.selectedYearMonth.minusYears(1) + loadCalender(newYearMonth) } /** * 다음 년도로 이동 */ fun onNextYear() { - _selectedYearMonth.value = _selectedYearMonth.value.plusYears(1) + val newYearMonth = _state.value.selectedYearMonth.plusYears(1) + loadCalender(newYearMonth) } /** * 특정 월 선택 (현재 선택된 년도에 해당 월 적용) */ fun onMonthSelected(month: Int) { - _selectedYearMonth.value = _selectedYearMonth.value.withMonth(month) + val newYearMonth = _state.value.selectedYearMonth.withMonth(month) + loadCalender(newYearMonth) } /** * 특정 년월로 설정 */ fun setYearMonth(yearMonth: YearMonth) { - _selectedYearMonth.value = yearMonth + loadCalender(yearMonth) + } + + /** + * 특정 날짜의 감정 레벨 조회 + */ + fun getEmotionLevel(date: LocalDate): Int? { + return _state.value.dateEmotionMap[date] } -} \ No newline at end of file +} + +/** + * 캘린더 상태 + */ +data class CalenderState( + val selectedYearMonth: YearMonth = YearMonth.now(), + val calenderDates: List = emptyList(), + val dateEmotionMap: Map = emptyMap() +) \ No newline at end of file From 4f9072bf7542179ad1b267793b0188e99b77b8e2 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Thu, 12 Feb 2026 19:04:29 +0900 Subject: [PATCH 6/8] =?UTF-8?q?design:=20=EB=8B=AC=EB=A0=A5=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=B0=BD=20=EC=83=81=EB=8B=A8=EB=B0=94=20=EB=8B=A8?= =?UTF-8?q?=EC=83=89=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/domain/model/diary/mapper/CalenderMapper.kt | 2 ++ .../egobook/app/ui/diary/view/CalenderFragment.kt | 12 ++++++------ app/src/main/res/layout/fragment_diary.xml | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt b/app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt index a179339f..a025aad1 100644 --- a/app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt +++ b/app/src/main/java/com/egobook/app/domain/model/diary/mapper/CalenderMapper.kt @@ -1,5 +1,6 @@ package com.egobook.app.domain.model.diary.mapper +import android.util.Log import com.egobook.app.data.model.diary.response.CalenderData import com.egobook.app.data.model.diary.response.CalenderDay import com.egobook.app.domain.model.calender.CalenderDate @@ -15,6 +16,7 @@ object CalenderMapper { */ fun dataToDomainList(calenderData: CalenderData): List { return calenderData.days?.map { day -> + Log.d("CalenderMapper", "Mapping day: date=${day.date}, emotionLevel=${day.emotionLevel}") dayToDomain(day) } ?: emptyList() } diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt index edaaa4ae..12f210de 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt @@ -255,12 +255,12 @@ class CalenderFragment : Fragment() { */ private fun getEmotionDrawable(level: Int): Int { return when (level) { - 1 -> R.drawable.img_emotion_very_sad_unselectd // 오타 있는 파일명 그대로 사용 - 2 -> R.drawable.img_emotion_sad_unselected - 3 -> R.drawable.img_emotion_neutral_unselected - 4 -> R.drawable.img_emotion_happy_unselected - 5 -> R.drawable.img_emotion_very_happy_unselected - else -> R.drawable.img_emotion_neutral_unselected + 1 -> R.drawable.img_emotion_very_sad // 오타 있는 파일명 그대로 사용 + 2 -> R.drawable.img_emotion_sad + 3 -> R.drawable.img_emotion_neutral + 4 -> R.drawable.img_emotion_happy + 5 -> R.drawable.img_emotion_very_happy + else -> R.drawable.img_emotion_neutral } } diff --git a/app/src/main/res/layout/fragment_diary.xml b/app/src/main/res/layout/fragment_diary.xml index d477a32b..d1e6c126 100644 --- a/app/src/main/res/layout/fragment_diary.xml +++ b/app/src/main/res/layout/fragment_diary.xml @@ -15,7 +15,7 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - android:background="@drawable/topbar_background" + android:background="#F4F7EE" android:paddingBottom="12dp"> From 263c45eb9f6a5ccaefa4edd57538886d4aa682a0 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Thu, 12 Feb 2026 19:12:06 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EB=82=B4=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=84=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/diary/view/DiaryExportDialogFragment.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt index 3697095e..0ff8fb58 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt @@ -9,6 +9,7 @@ import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.DialogFragment import com.egobook.app.databinding.FragmentDiaryExportDialogBinding import com.egobook.app.removeScreenBlur @@ -31,9 +32,19 @@ class DiaryExportDialogFragment : DialogFragment() { super.onViewCreated(view, savedInstanceState) setupDateInputWatchers() + setClickListener() } + private fun setClickListener() { + binding.btnPdf.setOnClickListener { + Toast.makeText(requireContext(), "내보내기 기능은 준비중입니다!", Toast.LENGTH_SHORT).show() + } + binding.btnText.setOnClickListener { + Toast.makeText(requireContext(), "내보내기 기능은 준비중입니다!", Toast.LENGTH_SHORT).show() + } + } + private fun setupDateInputWatchers() { // 년/월/일 입력 시 자동으로 "."을 추가해주는 TextWatcher val dateWatcher = object : TextWatcher { From 40aa48be6f3b1e341e9f00ecf174ababae04b925 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Thu, 12 Feb 2026 19:40:31 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EB=8B=AC=EB=A0=A5=EC=B0=BD?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=EB=82=B4=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EA=B8=B0=20dialog=EC=B6=9C=EB=A0=A5=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/diary/view/CalenderFragment.kt | 6 ++++ .../diary/view/DiaryExportDialogFragment.kt | 35 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt index 12f210de..4bd3ba9c 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/CalenderFragment.kt @@ -94,6 +94,12 @@ class CalenderFragment : Fragment() { viewModel.setYearMonth(it) } } + btnExport.setOnClickListener { + applyScreenBlur(BlurLevel.BASE) + val dialog = DiaryExportDialogFragment() + dialog.isCancelable = true + dialog.show(childFragmentManager, "DiaryExportDialog") + } } } diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt index 0ff8fb58..a7a4908a 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryExportDialogFragment.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.DialogFragment import com.egobook.app.databinding.FragmentDiaryExportDialogBinding import com.egobook.app.removeScreenBlur @@ -33,6 +34,7 @@ class DiaryExportDialogFragment : DialogFragment() { setupDateInputWatchers() setClickListener() + updateButtonState() // 초기 버튼 상태 설정 } @@ -72,9 +74,40 @@ class DiaryExportDialogFragment : DialogFragment() { } } - // 각 EditText에 TextWatcher를 적용합니다. + // 각 EditText에 TextWatcher를 적용. binding.tvStartDate.addTextChangedListener(dateWatcher) binding.tvLastDate.addTextChangedListener(dateWatcher) + + // 날짜 유효성 검사를 위한 TextWatcher + binding.tvStartDate.doAfterTextChanged { updateButtonState() } + binding.tvLastDate.doAfterTextChanged { updateButtonState() } + } + + /** + * 날짜 입력 유효성 검사 후 버튼 상태 업데이트 + * YYYY.MM.DD 형식(10자리)이 모두 입력되었을 때만 버튼 활성화 + */ + private fun updateButtonState() { + val startDate = binding.tvStartDate.text.toString() + val lastDate = binding.tvLastDate.text.toString() + + // YYYY.MM.DD 형식 확인 (10자리) + val isStartDateValid = startDate.length == 10 && isValidDateFormat(startDate) + val isLastDateValid = lastDate.length == 10 && isValidDateFormat(lastDate) + + val isBothDatesValid = isStartDateValid && isLastDateValid + + binding.btnPdf.isEnabled = isBothDatesValid + binding.btnText.isEnabled = isBothDatesValid + } + + /** + * 날짜 형식 유효성 검사 (YYYY.MM.DD) + */ + private fun isValidDateFormat(date: String): Boolean { + // 정규식: YYYY.MM.DD 형식 + val datePattern = Regex("""\d{4}\.\d{2}\.\d{2}""") + return datePattern.matches(date) }