From 22dfd7b3000f4a44c83d9b89a4c6bc0f843a7992 Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:03:43 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=ED=99=88=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EC=9C=A0=ED=8B=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/call/core/common/util/TimeUtil.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt diff --git a/core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt b/core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt new file mode 100644 index 0000000..e1ed3ca --- /dev/null +++ b/core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt @@ -0,0 +1,75 @@ +package kr.co.call.core.common.util + +import java.time.Duration +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale + +object TimeUtil { + fun parseLocalDateTime(value: String): LocalDateTime = + LocalDateTime.parse(value) + + fun toCallHistoryDateText(dateTime: LocalDateTime): String = + dateTime.format( + DateTimeFormatter.ofPattern( + "M월 d일 a h시 m분", + Locale.KOREAN, + ), + ) + + fun toTimeAgoText( + dateTime: LocalDateTime, + now: LocalDateTime = LocalDateTime.now(), + ): String { + val elapsedMinutes = Duration.between(dateTime, now) + .toMinutes() + .coerceAtLeast(0) + + return when { + elapsedMinutes < 60 -> "${elapsedMinutes}분 전" + elapsedMinutes < 60 * 24 -> "${elapsedMinutes / 60}시간 전" + else -> "${elapsedMinutes / (60 * 24)}일 전" + } + } + + fun toHomeDateText(date: LocalDate): String = + date.format( + DateTimeFormatter.ofPattern( + "M월 d일 EEEE", + Locale.KOREAN, + ), + ) + + fun toReservationTimeText( + dateTime: LocalDateTime, + today: LocalDate = LocalDate.now(), + ): String { + val dateText = when (dateTime.toLocalDate()) { + today -> "오늘" + today.plusDays(1) -> "내일" + else -> dateTime.format(DateTimeFormatter.ofPattern("M월 d일")) + } + val periodText = when (dateTime.hour) { + in 0..5 -> "새벽" + in 6..11 -> "아침" + in 12..17 -> "낮" + in 18..20 -> "저녁" + else -> "밤" + } + val hourText = when (val hourOf12 = dateTime.hour % 12) { + 0 -> 12 + else -> hourOf12 + } + val minuteText = if (dateTime.minute == 0) { + "" + } else { + " ${dateTime.minute}분" + } + + return "$dateText $periodText ${hourText}시$minuteText" + } + + fun toHourMinuteText(dateTime: LocalDateTime): String = + dateTime.format(DateTimeFormatter.ofPattern("HH:mm")) +} From ca61e2090c91fe71f116a1b45bad6b145c8637db Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:04:13 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=ED=99=88=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EA=B3=84=EC=B8=B5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/data/build.gradle.kts | 3 +- .../kr/co/call/data/di/RepositoryModule.kt | 91 ++++++++++++++++++- .../co/call/data/mapper/CallHistoryMapper.kt | 52 +++++++++++ .../data/repositoryImpl/HomeRepositoryImpl.kt | 32 +++++++ .../kr/co/call/domain/model/home/.gitkeep | 0 .../co/call/domain/model/home/CallHistory.kt | 13 +++ .../co/call/domain/model/home/CallSender.kt | 6 ++ .../co/call/domain/model/home/CallStatus.kt | 6 ++ .../kr/co/call/domain/model/home/CallType.kt | 6 ++ .../kr/co/call/domain/model/home/HomeModel.kt | 46 ++++++++++ .../call/domain/repository/HomeRepository.kt | 12 +++ .../java/kr/co/call/network/api/HomeApi.kt | 20 ++++ .../network/dto/CallHistoryListResponseDto.kt | 23 +++++ .../network/dto/HomeSummaryResponseDto.kt | 15 +++ .../network/dto/ReservationListResponseDto.kt | 22 +++++ 15 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt create mode 100644 core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt delete mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/.gitkeep create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt create mode 100644 core/network/src/main/java/kr/co/call/network/api/HomeApi.kt create mode 100644 core/network/src/main/java/kr/co/call/network/dto/CallHistoryListResponseDto.kt create mode 100644 core/network/src/main/java/kr/co/call/network/dto/HomeSummaryResponseDto.kt create mode 100644 core/network/src/main/java/kr/co/call/network/dto/ReservationListResponseDto.kt diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index bd0a7d5..7be7882 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -10,6 +10,7 @@ android { dependencies { // 다른 core 모듈 의존성 + implementation(project(":core:common")) implementation(project(":core:domain")) implementation(project(":core:network")) implementation(project(":core:datastore")) @@ -20,4 +21,4 @@ dependencies { // timber implementation(libs.timber) -} \ No newline at end of file +} diff --git a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt index 6d2944a..08eed9f 100644 --- a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt @@ -1,11 +1,98 @@ package kr.co.call.data.di import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import kr.co.call.data.repositoryImpl.HomeRepositoryImpl +import kr.co.call.domain.repository.HomeRepository +import kr.co.call.network.api.HomeApi +import kr.co.call.network.dto.CallHistoryDto +import kr.co.call.network.dto.CallHistoryListResponseDto +import kr.co.call.network.dto.CallHistoryPageDto +import kr.co.call.network.dto.HomeSummaryDto +import kr.co.call.network.dto.HomeSummaryResponseDto +import kr.co.call.network.dto.ReservationDto +import kr.co.call.network.dto.ReservationListDto +import kr.co.call.network.dto.ReservationListResponseDto @Module @InstallIn(SingletonComponent::class) -class RepositoryModule { +object RepositoryModule { -} \ No newline at end of file + @Provides + @Singleton + fun provideHomeRepository( + homeRepositoryImpl: HomeRepositoryImpl, + ): HomeRepository = homeRepositoryImpl + + // 실제 HomeApi 연결 전 사용하는 Mock 데이터 + @Provides + @Singleton + fun provideHomeApi(): HomeApi = + object : HomeApi { + override suspend fun getReservations(): ReservationListResponseDto = + ReservationListResponseDto( + isSuccess = true, + code = "COMMON200", + message = "요청에 성공하였습니다.", + result = ReservationListDto( + content = listOf( + ReservationDto( + callReservationId = 3L, + characterId = 2L, + firstName = "민준", + imageUrl = null, + scheduledAt = "2026-06-30T21:00:00", + status = "SCHEDULED", + ), + ), + count = 1, + ), + ) + + override suspend fun getCallHistories(size: Int): CallHistoryListResponseDto { + val histories = List(size.coerceAtMost(MAX_CALL_HISTORY_SIZE)) { index -> + CallHistoryDto( + callId = (index + 1).toLong(), + characterName = if (index % 2 == 0) "민준" else "동휘", + sender = if (index % 2 == 0) "USER" else "AI", + aiSummary = if (index % 2 == 0) { + "오늘 하루와 퇴근 후 일상 이야기" + } else { + "몸살 감기 기운과 걱정해주는 이야기" + }, + startedAt = "2026-06-${(28 - index).coerceAtLeast(1).toString().padStart(2, '0')}T23:02:00", + callType = if (index % 2 == 0) "DAILY" else "DEEP", + status = if (index % 4 == 3) "MISSED" else "COMPLETED", + ) + } + + return CallHistoryListResponseDto( + isSuccess = true, + code = "COMMON200", + message = "요청에 성공하였습니다.", + result = CallHistoryPageDto( + content = histories, + hasNext = false, + ), + ) + } + + override suspend fun getSummary(characterId: Long): HomeSummaryResponseDto = + HomeSummaryResponseDto( + isSuccess = true, + code = "COMMON200", + message = "요청에 성공하였습니다.", + result = HomeSummaryDto( + firstName = "수현", + relationshipDays = 30, + totalCallCount = 24, + callStreakDays = 12, + ), + ) + } + + private const val MAX_CALL_HISTORY_SIZE = 20 +} diff --git a/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt b/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt new file mode 100644 index 0000000..8edf7dd --- /dev/null +++ b/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt @@ -0,0 +1,52 @@ +package kr.co.call.data.mapper + +import java.time.LocalDateTime +import kr.co.call.core.common.util.TimeUtil +import kr.co.call.domain.model.home.CallHistory +import kr.co.call.domain.model.home.CallSender +import kr.co.call.domain.model.home.CallStatus +import kr.co.call.domain.model.home.CallType +import kr.co.call.domain.model.home.HomeModel +import kr.co.call.network.dto.CallHistoryDto +import kr.co.call.network.dto.HomeSummaryDto +import kr.co.call.network.dto.ReservationListDto +import kr.co.call.network.dto.ReservationDto + +internal fun CallHistoryDto.toDomain(): CallHistory { + val parsedStartedAt = TimeUtil.parseLocalDateTime(startedAt) + + return CallHistory( + callId = callId, + characterName = characterName, + sender = CallSender.valueOf(sender), + aiSummary = aiSummary, + startedAt = parsedStartedAt, + callType = CallType.valueOf(callType), + status = CallStatus.valueOf(status), + ) +} + +internal fun ReservationListDto.toDomain(): HomeModel.Reservations = + HomeModel.Reservations( + count = count, + items = content.map { it.toDomain() }, + ) + +private fun ReservationDto.toDomain(): HomeModel.Reservation = + HomeModel.Reservation( + callReservationId = callReservationId, + characterId = characterId, + firstName = firstName, + imageUrl = imageUrl, + scheduledAt = LocalDateTime.parse(scheduledAt), + status = HomeModel.ReservationStatus.valueOf(status), + ) + +internal fun HomeSummaryDto.toDomain(characterId: Long): HomeModel.Summary = + HomeModel.Summary( + characterId = characterId, + firstName = firstName, + relationshipDays = relationshipDays, + totalCallCount = totalCallCount, + callStreakDays = callStreakDays, + ) diff --git a/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt new file mode 100644 index 0000000..26d4d0f --- /dev/null +++ b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt @@ -0,0 +1,32 @@ +package kr.co.call.data.repositoryImpl + +import javax.inject.Inject +import kr.co.call.data.mapper.toDomain +import kr.co.call.domain.model.home.CallHistory +import kr.co.call.domain.model.home.HomeModel +import kr.co.call.domain.repository.HomeRepository +import kr.co.call.network.api.HomeApi + +class HomeRepositoryImpl @Inject constructor( + private val homeApi: HomeApi, +) : HomeRepository { + + override suspend fun getReservations(): Result = + runCatching { + homeApi.getReservations().result.toDomain() + } + + override suspend fun getCallHistories(): Result> = + runCatching { + homeApi.getCallHistories(size = CALL_HISTORY_SIZE).result.content.map { it.toDomain() } + } + + override suspend fun getSummary(characterId: Long): Result = + runCatching { + homeApi.getSummary(characterId).result.toDomain(characterId) + } + + private companion object { + const val CALL_HISTORY_SIZE = 20 + } +} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/.gitkeep b/core/domain/src/main/java/kr/co/call/domain/model/home/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt new file mode 100644 index 0000000..ca38e57 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt @@ -0,0 +1,13 @@ +package kr.co.call.domain.model.home + +import java.time.LocalDateTime + +data class CallHistory( + val callId: Long, + val characterName: String, + val aiSummary: String, + val startedAt: LocalDateTime, + val sender: CallSender, + val callType: CallType, + val status: CallStatus, +) diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt new file mode 100644 index 0000000..1abd335 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt @@ -0,0 +1,6 @@ +package kr.co.call.domain.model.home + +enum class CallSender { + USER, + AI, +} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt new file mode 100644 index 0000000..a12755c --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt @@ -0,0 +1,6 @@ +package kr.co.call.domain.model.home + +enum class CallStatus { + COMPLETED, + MISSED, +} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt new file mode 100644 index 0000000..067ca27 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt @@ -0,0 +1,6 @@ +package kr.co.call.domain.model.home + +enum class CallType { + DAILY, + DEEP, +} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt new file mode 100644 index 0000000..94fa388 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt @@ -0,0 +1,46 @@ +package kr.co.call.domain.model.home + +import java.time.LocalDateTime + +data class HomeModel( + val summary: Summary, + val reservations: Reservations, + val callHistories: List, +) { + data class Summary( + val characterId: Long, + val firstName: String, + val relationshipDays: Int, + val totalCallCount: Int, + val callStreakDays: Int, + ) + + data class Reservations( + val count: Int, + val items: List, + ) + + data class Reservation( + val callReservationId: Long, + val characterId: Long, + val firstName: String, + val imageUrl: String?, + val scheduledAt: LocalDateTime, + val status: ReservationStatus, + ) + + enum class ReservationStatus { + SCHEDULED, + } + + data class CallHistory( + val callId: Long, + val characterName: String, + val aiSummary: String, + val startedAt: LocalDateTime, + val sender: CallSender, + val callType: CallType, + val status: CallStatus, + ) + +} diff --git a/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt b/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt new file mode 100644 index 0000000..db4d04b --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt @@ -0,0 +1,12 @@ +package kr.co.call.domain.repository + +import kr.co.call.domain.model.home.CallHistory +import kr.co.call.domain.model.home.HomeModel + +interface HomeRepository { + suspend fun getReservations(): Result + + suspend fun getCallHistories(): Result> + + suspend fun getSummary(characterId: Long): Result +} diff --git a/core/network/src/main/java/kr/co/call/network/api/HomeApi.kt b/core/network/src/main/java/kr/co/call/network/api/HomeApi.kt new file mode 100644 index 0000000..b63e01e --- /dev/null +++ b/core/network/src/main/java/kr/co/call/network/api/HomeApi.kt @@ -0,0 +1,20 @@ +package kr.co.call.network.api + +import kr.co.call.network.dto.CallHistoryListResponseDto +import kr.co.call.network.dto.HomeSummaryResponseDto +import kr.co.call.network.dto.ReservationListResponseDto +import retrofit2.http.GET +import retrofit2.http.Query + +// 서버 API 경로 확정 전 임시 경로 사용 +interface HomeApi { + + @GET("api/v1/call-reservations") + suspend fun getReservations(): ReservationListResponseDto + + @GET("api/v1/calls") + suspend fun getCallHistories(@Query("size") size: Int = 20): CallHistoryListResponseDto + + @GET("api/v1/home/summary") + suspend fun getSummary(@Query("characterId") characterId: Long): HomeSummaryResponseDto +} diff --git a/core/network/src/main/java/kr/co/call/network/dto/CallHistoryListResponseDto.kt b/core/network/src/main/java/kr/co/call/network/dto/CallHistoryListResponseDto.kt new file mode 100644 index 0000000..895b8d4 --- /dev/null +++ b/core/network/src/main/java/kr/co/call/network/dto/CallHistoryListResponseDto.kt @@ -0,0 +1,23 @@ +package kr.co.call.network.dto + +data class CallHistoryListResponseDto( + val isSuccess: Boolean, + val code: String, + val message: String, + val result: CallHistoryPageDto, +) + +data class CallHistoryPageDto( + val content: List, + val hasNext: Boolean, +) + +data class CallHistoryDto( + val callId: Long, + val characterName: String, + val sender: String, + val aiSummary: String, + val startedAt: String, + val callType: String, + val status: String, +) diff --git a/core/network/src/main/java/kr/co/call/network/dto/HomeSummaryResponseDto.kt b/core/network/src/main/java/kr/co/call/network/dto/HomeSummaryResponseDto.kt new file mode 100644 index 0000000..2ce534a --- /dev/null +++ b/core/network/src/main/java/kr/co/call/network/dto/HomeSummaryResponseDto.kt @@ -0,0 +1,15 @@ +package kr.co.call.network.dto + +data class HomeSummaryResponseDto( + val isSuccess: Boolean, + val code: String, + val message: String, + val result: HomeSummaryDto, +) + +data class HomeSummaryDto( + val firstName: String, + val relationshipDays: Int, + val totalCallCount: Int, + val callStreakDays: Int, +) diff --git a/core/network/src/main/java/kr/co/call/network/dto/ReservationListResponseDto.kt b/core/network/src/main/java/kr/co/call/network/dto/ReservationListResponseDto.kt new file mode 100644 index 0000000..cc0cecf --- /dev/null +++ b/core/network/src/main/java/kr/co/call/network/dto/ReservationListResponseDto.kt @@ -0,0 +1,22 @@ +package kr.co.call.network.dto + +data class ReservationListResponseDto( + val isSuccess: Boolean, + val code: String, + val message: String, + val result: ReservationListDto, +) + +data class ReservationListDto( + val content: List, + val count: Int, +) + +data class ReservationDto( + val callReservationId: Long, + val characterId: Long, + val firstName: String, + val imageUrl: String?, + val scheduledAt: String, + val status: String, +) From 34b64139621008d73fd356030d5b62c3ca10028f Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:04:31 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=ED=99=88=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/designsystem/build.gradle.kts | 3 + .../component/ProfileImageWithIcon.kt | 79 ++++++++++++++++++ .../kr/co/call/designsystem/component/gitkeep | 1 - .../src/main/res/drawable/ic_home_alarm.xml | 13 +++ .../src/main/res/drawable/ic_home_call.xml | 14 ++++ .../main/res/drawable/ic_home_call_btn.xml | 9 ++ .../src/main/res/drawable/ic_home_record.xml | 18 ++++ .../src/main/res/drawable/img_call_logo.png | Bin 0 -> 34300 bytes .../res/drawable/img_home_call_missed.png | Bin 0 -> 1537 bytes .../res/drawable/img_home_call_received.png | Bin 0 -> 1530 bytes .../main/res/drawable/img_home_call_send.png | Bin 0 -> 1667 bytes .../res/drawable/img_profile_url_default.png | Bin 0 -> 107039 bytes 12 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt delete mode 100644 core/designsystem/src/main/java/kr/co/call/designsystem/component/gitkeep create mode 100644 core/designsystem/src/main/res/drawable/ic_home_alarm.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_home_call.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_home_call_btn.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_home_record.xml create mode 100644 core/designsystem/src/main/res/drawable/img_call_logo.png create mode 100644 core/designsystem/src/main/res/drawable/img_home_call_missed.png create mode 100644 core/designsystem/src/main/res/drawable/img_home_call_received.png create mode 100644 core/designsystem/src/main/res/drawable/img_home_call_send.png create mode 100644 core/designsystem/src/main/res/drawable/img_profile_url_default.png diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index fafa650..84d2506 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -10,6 +10,9 @@ android { dependencies { // Compose 의존성은 AndroidComposeConventionPlugin에서 자동 추가됨 + // coil + implementation(libs.coil.compose) + // android 의존성 implementation(libs.androidx.core.ktx) implementation(libs.androidx.compose.material3) diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt new file mode 100644 index 0000000..ef93700 --- /dev/null +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt @@ -0,0 +1,79 @@ +package kr.co.call.designsystem.component + + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.AsyncImage +import kr.co.call.designsystem.theme.CallFromAiTheme + +/** + * 프로필 이미지를 불러오는 컴포넌트 + * - 공통 재사용 가능 + * - URL이 없거나 로딩에 실패하면 기본 이미지 표시 + * - 통화 배지 표시 여부 설정 가능 + */ +@Composable +fun ProfileImageWithIcon( + profileImageUrl: String?, + modifier: Modifier = Modifier, + showCallBadge: Boolean = false, +) { + val defaultProfile = painterResource( + id = kr.co.call.designsystem.R.drawable.img_profile_url_default, + ) + + Box( + modifier = modifier.size( + width = 49.dp, + height = 51.dp, + ), + ) { + AsyncImage( + model = profileImageUrl?.takeIf { it.isNotBlank() }, + contentDescription = "프로필 이미지", + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(25.dp)), + placeholder = defaultProfile, + error = defaultProfile, + fallback = defaultProfile, + contentScale = ContentScale.Crop, + ) + + if (showCallBadge) { + Image( + painter = painterResource( + id = kr.co.call.designsystem.R.drawable.img_call_logo, + ), + contentDescription = null, + modifier = Modifier + .align(Alignment.BottomEnd) + .size(16.dp) + .zIndex(1f), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ProfileImagePreview() { + CallFromAiTheme { + ProfileImageWithIcon( + profileImageUrl = null, + showCallBadge = true, + ) + } +} diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/gitkeep b/core/designsystem/src/main/java/kr/co/call/designsystem/component/gitkeep deleted file mode 100644 index 945c9b4..0000000 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/gitkeep +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/core/designsystem/src/main/res/drawable/ic_home_alarm.xml b/core/designsystem/src/main/res/drawable/ic_home_alarm.xml new file mode 100644 index 0000000..3465d81 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_home_alarm.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_home_call.xml b/core/designsystem/src/main/res/drawable/ic_home_call.xml new file mode 100644 index 0000000..bb289d0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_home_call.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_home_call_btn.xml b/core/designsystem/src/main/res/drawable/ic_home_call_btn.xml new file mode 100644 index 0000000..a0ba0aa --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_home_call_btn.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_home_record.xml b/core/designsystem/src/main/res/drawable/ic_home_record.xml new file mode 100644 index 0000000..9c702cc --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_home_record.xml @@ -0,0 +1,18 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/img_call_logo.png b/core/designsystem/src/main/res/drawable/img_call_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d03833b36084c7136526594def92e9f56c75e599 GIT binary patch literal 34300 zcmV(@K-RyBP)U@11XMZl;?A0x^+!hDH>s zR0XNhBK~Bub?T>Wt@ImgZTnT)I()4izS^oq1w|$+g9w74s8NwgfdrBOfrOBGyz}r5 z=d7oDt!G?o@Au{gauX^mdG9%A@3q(Ptp78vVIw@esq6oe{&>lApS$gf$2|AZ7wp~j z)Fa1FKK=*3>y?+{`tb{ZU@su-1i-CCgxKe${tx^A011fndw;$EfmXccY`>S^T!dUJ zKd=9z0pjhl9?Z2j~dM-P4U%0IpG=67Co<)M*E9+rgg z(57kxU-I0SYPc1u1V^s?!zX186(|hOapa$oGa14i@wiRRGl*bOwF18&(0aDFeJZaaAAq2otC_=oR*=bPX9zIXreTR;AR z!y|=0B)9ka;u@eX}_Kc2)cb|1MJuau(5W{Z@l&wf9~}kecwB-yzVRmLY}U$lP@?q0GIsU72+&%h18gqU zeF(=u#U+i@GboiLALaos**0f1>0hlG{6;XBq@+#yRJS0unQzHG9BRoi+K2Xig|u&J zyD^{9;)VMWmbRwD2k&|3Z@lgo|HZ3*`KLc859hx#2KqAvcH`F1z5b7$@yI7X=~oxF zZGXb-hA%=#kUF&cE$}KSIHs;a091pc%`^6VBP~(vZX@I5)R&a`zK%f>;L`x{-4+W z$7}lA6Up|VgZ{w=doC{ndHxGO^i%u4{|A2f-P`u;eA32Ex6nd2&4Qq-s+95Ud+z8D zsaBRQ<+2Zoo2v7W_kMxZ<{ztj^MM;rWA<-d_hxmD=YuinYW=GvXw8J1X&$NKfzrOT zw|^CQAU-#$nj6XM&jAcg8EqHmZ_67|;e)(U zQa2skG!5Y?Pl>5Be6|5IT1T)h@$_kbe{J-79~|0Cjq}tC!^vfr;2k&K{O0qY^Qc!y zQ5p1eco5Qq1@_XQTi*4F=j{K6C;vgZ?v_P5abiB{aI&cxp+si2*Z@ffnSXI)MSr1i z14#iMy{w2la7r6#8LnyMrZyWCw<4vAp&n==5LGxNCQS9*-u>BD+7 zUnpxB9Wo6t?$11Yw4e7O9bDNu=^nd(cK7=~{G4-Mdc}v}K?3|&3U;&24}A1XZ`{3n z*2_0;yJ@~KT`1;^*^W_eur~BmDP9NIm~@GRAFAigRO|5fsp8G!z{1d`@q6|6rh_A) zzf(!iQ%lxC`ze|YDLfnd4`WQwl&jL0MxL$Boij`OT-SUypD*mcXmVin;9Jgm`h_op z2O(&b)}1mxT~Q**fy7kspQBK-kcv| z#z?7dY)u*p-}T#&*!qo~(rk1VNlg(Jak%&s!JJqDuqxeVh>)xdvIi;&P0Ga-yB)q^ zXhW_C2W_L!A8kinC%3T+C04bW%|-EAX&))w3hlcQmZTBd*WaxSsDZ6LGTOua(Vj{R z78a&my7SiAj-wkdrK5D~Yk%Ukec-+zX98blLiqAhvCrT8v#)#1^S}T5uU-G-wQ1T- zv&8P0e!#KS;?PhSq5?F~5U(c*V_EDMG}fS^O9#NkBl0LALM;db8aH)8v3L6HG&aw#>o%-ZBAAAAZ5|Q}+Bm2oB)^qz4N2Qo)H2-S*NgJGQ-f zcJnQ>NjI5#U%cfEkmAz~2#zME#Xq$fJB-4(hcaPjv0cD8NUiJwBP#~Qk09YPV6CFS z#lQdoPs2j}Yrq33zjk1=H9+34Qxxa(jM@$uIGx&Xlb}=>T!7?M49}V<5uFGDJWelw z0ca}*a-9k4W7wpawqZQ>-_8-lnyAWG=hs_8kkRC$n^KxhF1m2Ke(c0cmY%-vZSVjk z`~v}dDfq;P?s&YvnaV>NgLZ@TSyT2?vOUPgu7xB=B|c_pKAT>A(e&hj zV=r5|;@r2u0|5N#g1v-(RzG~l^Ov@6eeb*nI(E3w^&>=$vdp}KR0P;6#oq)P-we~(K1f)?_D?g7J82}NhDmES1{ck%l{GUtKjCTWyN>yuBnY(&kkObsKK7}CCpggKDVinNT-Taiy^;5izZfWD36<<|0|=ZvX!6*j;u$Jk znW(^!2h8CDkO-GIQ)s@9^E)}j%O!ubM&s7?+YHFTO* zP`FtZc_cDO+cc&D&rRt4W+unG8#@$Q-FOw^#E!K#>IN8G1!zI%cX%ch16vulfYZWh zACMAwrFe7Ec{gEWND4uLmJ5P(JGp-KQi}QVYm`SzY>{(LT(& zq=ZD?cfOsV^5>ER1!LcHlX`saUflEYTi*4t=KuqJRETe8vrlp*+xK03I$7Nvo)Bw?0iY6A`f6=0OT?P%7-wHY@f2vUi_26m#Nm>%sFa1qk$Enr z+c*cS6W-~v)xEa&^ z^bh`{i_dxFg)f7XCuU;zQB)nKLPM`4tQXY_cYrTq(7P0iKtrP%7E~JXHU3tq zaem%S7!Ntde5@^#M|cuXd!kk-fU?d5!yZwM3vDkMp?#*>4Nf!y$aqz4=OHkb2ba{K z#fpWbp;?*}QW#sPEvSLaLN*gEd!R)rvXpt^`24J|d(nejbakD9wmq#+GHmanfPJElh zE`aJ(s$h-gKy03`=dfP_UXv3%E*62+CM9Zns6Q)hGGM4N>h0fn;gg|}?gPpGpMUtDK6cNA`=8i54a}~}ZA8Y3Xp**wAE+>y)KLr0g)|DV zoew4lzeq!{swfha!Xs5jjU1$sA5_u?%>w4MVU+yLSe{?=rW9M|*@?;kp>)yc;W7}# z2@4v|C8qWb2A%OwQ+^aB15x|kG%W!{x^aWFd+LZ5qs`DqDUeg7e<*~%i!YAaC<51} zL9Iz|V|kU~BO*q~p!t;eeFpd1kYKV5iZ+Y29t)Nr?uqhRD!sX7ek2hTf||ACkv5XHiPET zx~pEazpDzTEJ6do zGE`2Fwa|-v?6}o7NIFjkF*?*$)CUThs2)pd z`mRe523kg8QBuk)#`f*KZu#lwJ|2Q}uO$1w|AC*l?5zF!pWH_-sggi}ca^(|bP79i zJE#`Wt`h33LNdwb++KbrEK}eqaX7$>QGXjejAr$?QEj$spmIQISp`fn;!e?0AR7P= z8pUFn#E}U^@>vwZn}ir7t(s9{ihIklCQRC7)dKpE$jqJ=W(+0vNknUcb-|^%g%wdX zg77xRF$4Vrs|#e=1;mJQxjn}~2z2|HN45Gb#40pOH55S1(hBG*c5L+k`z1Q=DFq^$ z7|{XINY8-xTsagK(!tV>-H-dFAAR*D5O@c}Cmp%+=C>~&I`DTVSkKfMfufYEc*Q@SK78;ptdTEx^U$bX_;l zMjDxETV?c7z#B_UlF=+S4|4ko6ay0| zix}RdS}Bog&0U9lZOgVmP=&Kw*aWouLfm}?OeUmMW=2p)ZHS@ELKTq#ZYEZ)Ba3sgXNY6o#e{H1ut_QnE0lYAjFZb~ z30#m44{Cpsm{fcpIUnoo;BXVu6Ld8WU(yh?1y3|cKq5@6O>ul73aNMr@>;$-Ztq_6 zihuqRh%_ztK6c>KYd7Ec*oDa?3}vln>Ji4~Q?rFYk%B%dvS5yc>+%e;bPJ&?FTNFa zXqQaMVAZ%&CTvWI!Tne*Nx!J(y;w#xG3wYDHwhtbr8AcBB_e~0o5r;LG%^sdm4=li za5`jY&6NLIW;Ah& z^S`s5d#+h}_IcS+Et0)^-cz5t6Bd^rH|-{1eM?Rw5Ja{hh_LcReN_g`O)fVVNKN zrtkh{IC2PB-w0ULA4w5Bz+i0GLn&1ZwBX=Q6of(qVf@pOg?3mlNbxV<7PCipYEW`Y zDgY9BMQUP+-D;r|nXMIbVx-K-NZuzJvc)E;#s#@oSD9^$ZRMkoJj=b`8UrmU^O0bHl7eCJ`JNVwuymi~j6W`sbX`tLaTH}N36XSJrGovo5RkBSdES@={lzl-$8^%XxOf3h3s%#o# zW!3p>4$0qr{dZjk!O;8}?-5s3&r5I~tiTmAdpn}(=*EFv0x(+|hxL+^PaR<`S|eql z4dvNENJ;*Yazr8c+N^1#P7Mh)h*lq=eFlv^1Y?hoy#lF#>Q+g?o*a2 z+{F#i{g$N8b)$+ZPg__xx$1S*rP|fw$A(IZ=8*qMWX-xhhq8uY|!ldZwgv;M2dQi9_Y94W-fYN2&Mr7 z?R_luzAdkocIw(D4PuB>YO2idIllB*CXMl0*pDQ{8SsO#XANUw1&)S3TqAXTTxQGi z%HPV@dVBqOJMoxt5Ki~+I#SJw6y`psMb&00*V@rA2~lk$M*$)D zL=_ImD3lqA`Wb?OS)-lMjOjQNHNir)Cgh!Lb-@gn|%qo^UmJ&lE*%G+w{90|I~}%uDcA&whIVt99h|M zIXpmY&6K|Jkq_ov8P1foqT!%CTj> zY!m?6kWgH(Rhc(>GfbIR_@JR7W*a=A$sDHInc>S>IHny8Gh>MyGdhSmWuQ{s&4D_y zR0gICOvQAr>mnqr_L zpip;3Vh42mw6)D35}7byp)JI<2f--wA+;B&GS67{N|Z^hhZ85@g5A5H+8tRv`8e!D zj^cB$JautWzULK`VRtr(9{$NxU+NiWc6p_Df0w;-ATksv-);kuYr zaVTwN;_0WlqRo;w05Cono^FN^hGBm2c7w(K;uE$@zXCvAZ_0BID=Y8qaP`DRlM+hQ z)!DMD&kD2~vNi(2odMP6%E=VXedToG7;RG491OLY3=mBhn->F>+A`i!xDZo1YTXoi zSS5ybi2gT|))|51w`eEPfQfW`$Q?bGCV`U}bS zjbTR8qXdBd3n`%B1S5%oj4}-6{CougP zJw6*gvDqU`cz=+$9V&sY`%RP2n0lY96x37c8$vWQWARZA`#@V08qGj#Iu&x@o?ctx z3Mp|-5-Q}#7{m%UE_CG?O+A(FlHZ;z*Ahsy$88;TMmj5xj6rn;X7Waqayro+!a8VS z(mKTAeJQ2$b~=j#kY>>kmCRO3vwd%Duo?gY<;gUH(M8wGQJ{y=#HtKHbs@nS;ufMyMZ1Hm)MlgaQ*jvUl`V8U1H)3N5Mczy_-7^I;Q(EAw2+*e47-6UP~ zzCgs%NOb1@Qp`k~{AnLcdv{Iyw(T@xftsxygSLYZ6sc&ZmxOhgh!#%?k+C3VL8Vq% zmQwC{c(0J4)Twr5AN*lJ5LP~{2NpVVtq|`QaLs#pGRu9*N8`^7GsYPy-X<<4WEA${Ve$N}IJCopJ3@1dw{Aee; zgtXOe&Xe*~&~r%zjk_p9NA{gi7@s9SsU2+{k&(oz~VYhc1z}{_$D~s&3oZJ97cmn9| zBfaf51KQSKpYoA8B{sw6M^M<&i4zA-@EhsOC-Rzpli3EJJw!J|ysbKXxg1$`A>KLG z!PaE7QWp=bDKDulD+OC6HjWvb6Pk;l=7jKUffCl?bG<0K1e!{Nv((8>}_o^vVUMP~sl zY;yKfZv0(GVg6S)0o;BFV9!<_ph`lx)RDzw5-4He)1d)bx-7#|ek7+47)SGc6c0HV zuWG_LZpmqi&J-B(M%r`8)F3PLVA|`heQCUeeVt=`u1FM&jg>AKhFYU=BShlcr)!Z! z#`hFRDMlBc2x)qPDgh9~Ls}=p*k)@ihNGQAFpC3Bt4JSQI>A${0*%SVI4@QUL2 zM`6yyhE~EmcS`elrcZPlRFMlWR{;f{FWy&h^XP;)qGrhd95_z+tV^(a(uMGVQV;go zdp`}heW{QGRp^)^s)-D$>7f=JEggaYG8523RH?%H@GQr8Pwt2L4eN7da9N5WTi7H| ztg5DWye>k7G-hq*W@=}gXH8ItnhdX%G_&$SBdu14o>QX^KUBx60N=uRqOWkR8(B?% z;yY@+6*zm#N5~rzW622KuF$@w1FF9C4#GI2g#6)RGc_Mf)S+T;lri^6J56$`K$PdR zL@G9I7r_Qre%#BO6ew10bavW$b){0>l4yEKRaKsPY~$QPjS&IA;X=i;31diR!b#j! z*_fq0HcTGcQ`X!%9czISq^`FEt#4hSp&lAZVIOETN5D?@GN6T6yD)ib8&(NDyT+~1 zD2C6flC(*GT0Yy3;_b@!dK-N5vS&VUpy#BMdcC>Rz5H(!R|QtrxdFU)l}5`?OXjbYe+$G5{6h zgtZb{SDEO^K!4@40C#M8;6RT_sFaxodS{kSDB!~+7e#Q$fa>}frj2u_5Y-|vB+;M} zT8jrSIYWf7so#?SQR@Zr>jJE5|17sT7DM2-9Hr7GV=9F*ai3A%UnOGMqbU^Do{%}o zvId1in#7O*T|kGVS`TDSOJEa2kj*}l?sPKq%Iq4UC!sE)NV zNLPs)!>C78j0cGV0k{FSL0ADV#UR9S4OEu_3xhxuSP@CNr(ywb)4cEKu{D@}|FZ$N zEq`S|w?t(<#^k%7R?5WH4GB*G*yI_34Wc=(<@*p#wtFVfgQ5^oW=T#T9dw_G$(>35|{r9M&+73OXX~J4Ini9fYIM>S5D%9YF{+jI~eWg=qYO&IJTv3L{8B+_bUr3Cj~1BLn^5ahU!<5A>}| zr=-(YnkYM|x!Vg1FnfCs_!B(?EKNjfh4!h6RnHQAvP2|D=wu8DdXH6ONC`FjXnO#l zjdH^JtdM{5Eng3K;qLqGQE7csebJM7`u`yWdP!MvA3g!p+w9Ow&Gf!&0dknL+yn3#4?K0d8gfJh zK&Qz)hMZn%PhWWjU+-=5*(Yye$9~$esW+D}T)w6ol@s@)J8u_Eo_!g6h#LuJzxkoU zFx|y_0H5}Vey(CW3=Q3o`nuC6zf}HU_X=wnAd{wHc!WBQ>~{hPAa$`{?CUs9%);J` zUTBt-jR}c>@UfDXZoK|4IH07Nt0`<{AN0n8PwG}300BT(bDnV{G-Xy-nyKsF8O5(Z zA12>)*+UO>P1)8xbPVvUZIIr5HBdiWE9#u#SNSCNkNzA==$snw>2yT{WWc`Q3D8}z zrmY>zYJ zWJ%sTzPM|vx1*omnElSjdF_!bq^tT)uDv1%9Qwe&3m7}|gQ9G0e^(6Cn%U@bCkT=l zPjE|+3fMnL!>S}l^?>OOsAeO~(86Fcg(hPZ0>xw35M>R9sD66ebm1R7_u&J&q?FB> zobEmX=>wku+`h~pHiR~6HCz^Z)Il%I5u-O0H@TEY8YNj8(F1SGB22}6NjKcpga0JI zOt-|z9prl2aGB^yLCWU__2vrf+X>Te__|)Y&seBUr)4E*y6PsN+YUk6n0cKU@VyO> zdsblbtV^N0{CqonboPc1vza&PVqYC}ZD2x;_wqsR>|=211y^w;Rb!KwS35LNMi{0J zV3njBPYEQf-xx%~m;_)ax9WEr7_!z^?8x(5$a z>Q?&b&&l5QJ$FYBxCNa*2ytlu4=O|GJ1MnM`wbQ+mKj2eSBt1K9q#lkZd?!2?ui$% zeSLhjH%m@(_%+WDWcr|fqWlRbA+Lh4aAe;Xy6pg@FWy}wbLxRS`-4xx?A@OLI?y|} zD+^^tx;VFaa&7yT657mTGW*@Xg4u6>#Kvp-Jx^!mu}%xXiJ}b++CN^Sn?x|%^fDCS zC93GAadev>lXDzNSJVpc%Iq?L&%7F&pj8c(=H%tXNoTr3WzrxD>o0s_0>Z$^bapl3 zv6i@F-4GFqFxkf$CcRU2>Apd>hb3iIoxzhodvm5pI{@$j3wbNT)n5^TDuXm+)d@Hk z2u{Cpua}8fxh*C*v6cr4dXV^oMTdk5dN8M}d)xa9_dxpHj~5MrySD-L z5?;fh#x$WkJbPCk@i_$Rum5vqo~3?_p7V7j);~xTB)jxadAXh_V&h&~$`_I`Ki|~2 zf1ry_M3R+(;Com3A7%?P&hO*s(1qgtdS)XBT{N`lcg`uN=G|4Z@Q)At7i*jqC+|& zBowEincyZ-So8*B(UI9b5LA$YTHd4_BQAWDn%PMLas*x3N4lav0$D%1aI&ynlkZ))mJ7<7?o&7S^yn|`@591#Z%S*{ zMZbjNm{w+qpS@nw5Nx%rluMR4~wHPZI+NV&RarX&!r<0xb?ozH9Z(&kT5!l$ z{%PQbS~7xX%rwa}xfUH!Fn8jB3c%5Bc-;!zT%wye80c!;z_nGn(dF&@tLV~DyPVmJ?edx&t5#$w!AnvzNlbNb~{Jm4DT9+lU zQ4D+J$#4rU^>wYaHinr)8I(YeVbFIcX|5EyBp!Pn%-;LQb{6$vPPEof(Pz`Pk1@l7 ztk(bthtjHpUQLqm`nifJK)ru(g+^^{pf(>ueKL+!tWIa74^l8cz%_v7ZzXw<@bVHZ z19&C*0fx3blx5c0fdG&QIM(00Nd5z zWKm_1XGbnsYSI0bb3;eLW;Rx}imGc9Nz^i%9`76x*h1m9N)#O1Qu43R7dOu*DH7|iQe zdI?&b~syc5J`OF<<78Ar~7mQE}Ar%xFS0aMq;$4?`$Z8fX zCJYd?x1YRJavWV{dp+ZCQb$9#to6#In@Tv9c+z(_0RUX112i^sq&olEX~!v)7qU`~A<%gY)fjD&(z-5iRhT^z zNGF%F6!&1|7gMQY8AEH<5Z%?I2iGN+?z%n8dSE6MmJJi7u7?@yJ=4u^>z%N?toXBc z^(M@51_`*MR94*z>BMP8J^Gp;E~rrBo^kIz=RT_VSI1X?U-qnG2Go5=dxt$c$sMm( ziGtQlkS+*Pm%`ME;0!{WWt~fWdn7Ol3FzHcV@4m_l+)q0^n^>BoNhFS(iHw%hm&i0$ zi9iGnB4-B9L3(^v+4)4u`l-T;Jjq%kORf8k@xW$@@o)jYSIrwY-pP{uXzz%9{7azW zKMIVG!30waCYX0JfV{_2uc5!uE(&{ooOD}^46N?;>?e}w-lOFa%xlZ%B$(2nizbGE z-pmq!+C=y3CeedALlCW&pc(XM{%1BTYvuEjV>oa$0$2NuL+Kj83}Bi+$(yyAhCu{d zEIv*~XjuTY+2goFyRS<%y#DNF@QI&f+<>ZWxd91<04lDCwM-7=CqKU<+>7OCN6J8H zyM^=keLDPbQedB0qr4=wNT7{}0qi}{XJ5FZx6>DS;6CsfDr-g{ZdPW-cQ|rVLBii9 z9|#>&6k5>cGuq4A4^BxA37SrbZo0Q@7c3hoD#wxSZH%$-Y9a~U{OG!nua?+qzF=Fn zfxxr?a*}Io&nxE^s>=#AmTXIk~T$$nRv6Yd$vQ0>MofrAPR*pvv+B zrkm~&8yY$MsUT6pRnk3N3&3UEpxGz_=$$~?$YEFVbP$G0oSJ(H<#@9gtE(3f}` zoqn$e`V`BWah~v!MiT|)r2Qu~)0ouUN3jO@ghR1XxQO_Qo01Sc`o+cMyD7YUIG>Y~ zzH|?77tC5GrAER-6DNmCdQ*`2Lj*mr(+e1^B;`8pVfTo$%Lb6tk5hJ>HOxd3 zhj!tuTrK=u=%fdp^2$*PLV(vi>kAiDDhSSm5Y9}(33|Q{X>PvOIZqqh^Rpc|7qY{Z z+NoDJzYz3H1PDMRQ$fWrsvQFm)da4h`F`Mf`<}yV>5fA@TbXV5;+UQJl8`WNWo9-t}Ro;oDH}36E~l_ zl?u@FDSZnQlkU`jzA{0~j1euGi|c~;uNVXEk}-kV?OoZ7-d(a!%#OazJ=nXY1=UpU zFe>5;S@;~K4s6RI7~>>DBNzug089r;=d6~er;!*h93T;A2RV+Fbkb~7$-xVua$*+>$Y z&IYR7o6>#Jj^>P-1tD_7W38$|%cvOdvWsXmSIJJD&{!gmCNS*8U{I~fYqmO-!l=eI z`9NuVT*@a1P>0B=F`>Tvmmt`v&IsITeL;zNz5MW`cgVhUU$LF(_!^w?lxIkDd_PO8 zyN;BjGxUu3iU|qO!-M9FqYpC$X(46v!)TF#sGTXip#;KgE99m`w8%qSmtp>q8vvJN zJKWM#W-tM2rxSuy7j%geTfz;U#?qA`g5HKkOMu_P%g-+wH^=?y#(R1GLE0$bGRpX??e64K4{U`SHnbuPB;_LfNHt!>FJiGcWcPB2HQVj;GaQxaO{LPNlQUyD zH7v_QED`H!VXDc7hFmo5Ej=s$C|Aofrl6IA%1Gr$uw_Z5zZC@%3B$6qcJ`@T%dBoO zX`)^h>1d8-skE!4JL+mk)MG2uOo0G!^6NQYF9^!o^~7<1_~gxbS%+E4IPANc6>?ud zmyHh9DrUm^YFdmPks__sX!;4+tH1_1&&n|^)|^P^7-x5BLSCwJh#l4DKqq^9ULtwz zI8@W*>PR%^It<>Q8QYeh&hgt`$`%_wPnVy`l$We#>7FHJmg?q12*6}tCmWz)bI(_MOg0eL>fVE$KM1UVqmLU$C#&_yzsv`wLVHuEFYLtE#p z{S^kG>bDs8ne^p6R|}wPQJsO0KqooL`Omy&(LmLaO8_x9jejR)EHb7D1|fJ+0J@GB z{DzT68;yFdVfpK0x7h(IC+i*?+4}Whrrrkbw)B!RuYI|APnrH^pT{=^ls4+t#R4W@ znjAV|*L>zo2G0!CA{m%Wsu4(D9ayAOZ3F|^4KP!0WeMl+|5Ok3MXn<+o+Aq6q;a!G zuY+Gl1$etOpsbzm0sg3SOGGNi(o-+dsqB=Rkeg=#8Yu*;Qj1{=v)WhbBZ6wR=-2t& z0~EoKES_Y7lcJ|jmC?bYkzjr<4M}YnMu&m=4YKg9@|w zsiGQvMRy;~hjR8n zJPv0vb$hn+3}ruQf{#9@NO#jw6zJZVTt*OT`ykhwWaBCMY`g;ybjdTuXTpqh`KOoW zd7^24_04jiX(!=GZWW5HZiNUFxak6Jlef}-@3yiDTbVNTPB9%{r@SrEIwoVzPYMui z*2f5hQRG%ec%%)D{dpk61x1Y)v|@^htxdyFo6}V8b@dPc)f=O!-cZXmsv#exb5u>7 ze636vDDysaE&?j9kA%po^ZGl1wl6_CbY{Vx)8xW^J%jc2T)3-jO2%^7kPKk8`i>x& zZVZY-r?4g<0<+Bz;kuM314*q^AgWjjPz|1ZeS5awzXaUQ8eKY+_aOvmcC=?W#ob= zSd0q5oa4D2=->+nDxu+Iw`!6s7YKK;9U#uEJB~;f*kd&;1QGbvuu*ClM^CRpsE0Sy zSsIbI=Y8=Wj_3CFbUJYc!;pQo-QMlIOd)T>yYy_^hBpAYRV~ z6(pZTZk)#C%p>J%tTy@p8Su0BUCqmom*iNw@@h=t__|7S!@jg3FBk5$W)1@6^)}gz zOSc|?*o5oR z86&hOVE`y5&HVR236PfpFN+O7Rg)&HPs&nyVSxdjHD```Wsu)>m_eS8*2^K-tl?zO zf-)Vo&ImQ8S6B=arK*}7k^dp~SZJvZD)-fV6jul)8xyKFDSmt6su5gJV5n50sO5GS z#3}fiRqaoV69GCCl=TN#t&x$&wpjPi0Qd?VHn`g69kS(Dq8^wcS`;Fr<@n(lzgCKni7Ma)JO!|-itFQ5VqV|^U1)M&Ik z)KBNH_Yxj%zK7p8l}lfcCBfOW$;*TDUzXyTA$rH3ZaoO|4D#!4FD0R#CzD)ahXaV} zEr}2{nFs(iw?YTi>}Wfy^oW)m$zxG2N>POl7$~H8Mz8>kFUkm;j#BWqd|RH$YM;+V zG0cbnV?2$*LM796aoHw_+9B;h8Unpx6bjm<>SUHOy`!4LVY9mq4Z-zbr(TOp&O3`w z6S(Z0tWAOepjrtW=-g8Np5i)GteKt21pz5>M(VEypLmka>hn4YsxTi*v7K9C;MRH3)NjY=7PV(in`_ z06{>$ztPGo>#P)4`F*p2G*m~(DhnBm$pWLsXGtWxa%V+rUAM#1#ko{~ zuB)U}az%dR$pWs>2A(Rw+eiz*-9Dwj1C-S&r5o=lZO)uXMG~@lS z^ystm>t=*Nak2vlTIpU4NSOkUj=u?nKw(|wTD@{EI_^GESdUDoPiZf?%7n(w6`X(c z3w><-CO&RDFB;41VsfbR&p!+EKl?1u?FU%uFE40>hM?g*0Tmf@g|#o`KKib<0r)9I zWU$(e=kAjmbz*!A%H}RZ9s{^4=1k zPUk0Od@zo`5{!|rS|HghA-7!Bp08+N))3A`A#!g@l*r5_XT$8~yJ4}nt#3 zE{63_+yHo9AC9}@2*Sx8@DmIhUm!Di$E&SP3fe|7wxq@S{6Wf=`YtGNeD(rA7E{! zFfL0wwjovckxwR2wAE^l++ht3LadImuA-1+rJqE}JN$F%XAkY%^fQ&R%G{-zy`opBN)M6%Cv@wTtf`DIL#v5_snBYpV+EN!7LZBVUV48y{+Js;c z9M^CGXL@uVx%sJ&c7{-5LZCSx$;nCzRI$UrEeTNtauZyWS!fts8wM?{*UincSvh+T zW;txhCRXy14L9AFUkK2v5rDnD z{kv~lejA0!p$ehO!9Xg`n0YBm02XfwJ~0ZxXRRe42HN;@4g!Lj$#QbXUPnJjNviLG z+r1*|I6s>bU;qW_4hDdeR2IRcCI*2&Tji&qDP)6+gqSEYmi7Z`(uV%KJx{}YVO)lx zambO^$fXp#qEvSMn}>{AZ7M61eGz^fV2$bM5?7ghNMVb4IkA95i1_*3<$ed`5?^d3 z#YK7e*eawi-CGXTpMCx|AILM6vNYJc6Q&pTGphF;hxDkk%GrMU(s8ry`~hZS4bDmb zgrFcaQT0k;g!r3j^=eidOkC(IWg>$a$dk$>iH}R6acUw&b|@^ys$GwOA_0y87W~sp z325$7M0+g9{S~24*{)b=6KaHqaibN*t8;xTB(!K(2bZe#M4akLB?;1loD+kA$w`*s zmTN+Ao6b$v(ZDY(Erjr32dD(Tsy#eCN-2i#$aGuYSBUvlDc9Xed4@2*ue8M7@eGQ0 zkq-+`c{ES6pXmLCuiuxC!VAlJ)rVnFs+0^WRRjS~l$*o}`zi&YC|h$tC!th?P}y;l zIIaOC2+K4w`@F(72IUTsQGo(uh^*5* z7!CE%B=-vqTgin*iu?4mZ820EI7HgrLDO|WXl8f3zA(#GQuWzXP~{`365Mgk_mhT{ zNGV|MOYmcv@};5q;S;L}U%0149A?+w*3^3jl5}GETb{%V(t0y0J$f&6t20rI-!}h5wmx2F0857mt2g&xAfOnMz_w^-dPJM0L&n!>q}hTBkmVU6`k+jrD^@8w z0)}2P1|SoLEaZ~mlfqe#?i9vE2sH3BD(BvY&v=rFgP_qWzUjhIsH^6e)-$LYjP`o4 zDOxAwr2~Awo=6HE_JSZdsnDubA!lp~TomFQ?!7lt(|8wVLo zy1>fFPNMq(f(;9*VfZs{Xdtk_06~zfA_VoDkU4oqle!q>V64-8I5lsjqt(Xou;j!T zNBfg%d1?aC++?6QTVI3BG>U7GxQh;gYN$9WE(9xt)Q!J{J`W5wF+^s2)8G%Q3an!A zbf4s2yh8)(manflm@d2LpiH`6ceG#4~_9&;#Klna~K}*HO~BiW;EmN}#Gx z)SY;q%|?TqHC*ZkBu8<5pgKonXX9xfNzQL1s-%THuN~ps!W2b4nBh->oTBnfI)F0_1;!{cr`#ei z`eIoHc>~X?=F?<1K64A4@l=-Q5=M>rSMtnC@qb|T7)x)6ij9aCDkVU204p1THS)15E7K2 zLS+e&#UNzNNaJ!Y>STYiF^~Evq-0QZMqwvRfJP^9yn)g)E?tmk>=i$18iO)iDWmg3 zy^~5o+XLw_FP8|>9(lx$5R>81%xs$N`X=d;z1fg3CFQ+uQw zChD_R$Leq;Q=y;(&W9qoAznHyd4w2Z0tS0ETdG#LM(Z|2p#DfP-U3Fs3v98V#K zwNE^CUkpRBbEvi$n@NRJNhdxsoos=F7n+IteTDK~p8|;vffH+UnV0Rxm*0#yzfI99=CZn4ZYe&dN1& z+5RohSkxwHDQ#Ye@TiHQj)2OqKmct=X1vY>u*1&EFsw;EDUh8+z^W7mfhm7O$FgVE zc8G)ObNhk&+|xGSva#99dF#AalLCWC*P?x@6frVEgg@C^D3GcfR#l`iK+r9?I)f65 z+`SEESKkPj=NE}JNfiI`4Au?A}CS877=U16y{;h>7Qeo;&`6<)PsBF<^{%( z1h98ja}0i-Upao`LuAfq>Ye4SulQz2t3AUVTZ7sD?Rh^am7_U2WV2iQ0uDk744JW2 z>|dSokbr|qFt-jut=v#K3?jpA_&L@5i~*`qWDe0^iW4pvVf0PU>XRru^!S; zG@sxhi!2b`7SjGoWJPHskLp|nGd%%BXEPOgN<ceqcPpl?i-7BMU;oeD^G^(nUu#zMSNl*S6A0||EYk1ELMXQQ z8!@VVU*`lN#A_M^$)&AFJ*w3|&Y8wEiZEpmLz_u>e~o-#K@Uk1TCRN0b71=D z3n1Ng5Yo2ZwCX|M&4JA{of_f|++d+GC#awZDti<(0x43@%?+@purl85A1_rgWs9{$ z;Mk;MIBk;gAR|a3#nIa;PGiZ$+Qk$`66aY%ZAgKEf|~+S;jAuAv^GT9Vf4KWj zRe45EX}$7!R|CP=M2YO=lf^U;OpJ;#T-{!iK*!yR1h1sq5`oTaCMUuel^0z((7N7eBnBUSn&;6Xh#=a#! z;$o&mLhuvEM5ZL!+@55~ft>zZKv2`zB~KHjz#8iq&5Je>xufsq1t=)(n#SurgsC!1 z7MP({ug)Y3sg$S+Zv^rgXu*z-B5;6e2-4JgEqd&GY8r!HMpCKIo%GXxfkCs_JBIfh1G@VN zOwQg}%o#fI5WLhNCebE~Pk9t<`}^Mt>8?ZNRI}Nh#on3jNj}$s3|;hDCouX5bR$Ev zoo@&Bi*|J9j=|~b58XnkK~E4lCtQw$60X@$gjr&Fo1#8|QNY^ptI1h_h}JCvi08Zb zd5vtB+FWRo$<_#!M$+6vYcER=^ z`%aeX_Z@}#j^3= zt@eST{%>Oy(rg{%1!U+^E^yAF2zM7$>hv`ycZmcA72K#n1PW%lnqIN((R#MpxIH1X z2|ByvOC2aW!qPVRsdO|(2zd)dPyXCHlrLEk2CY)^~eeBv}r9LLUy*cgmKNiu3DxF!N#ulj(B*H?`=F zgqtx+dWee~(CVv)&zmjACkjJ#GAYh2^i$BJBxDeVcsP!Yk4ViyEL?&dgVCgsHs*%( zV1K&^e!_9emg2GzgTvyW?)|CoXSz$N(;e)jDYXld#~L_c_p);V5kFr$M)#~j{# zwZ$qxJ8meTm-WmXL&lmJ2AZUDvne#k?v*AWgFdf&zi<~&Z@;J84#1X|d?QSr(J$9l%u16%*`-!3OG z449*;pYNqR55kdu`zGjiZ!0exTH3LP%KIZ3Tps4dXebE?qg*!W&XgYme;YO)z~7TF1;FYg51yFZM+|`7>`Pt=+;|D4{QD`DS#hI70+8$;m=t>y0Y1+M0miF?X{LFtZz{p8QLK(}YxsX=ijQ>b_CkFPHF zd|s+defzo|{L@)CVln_kB4*pXkMZ=5?~&d_yAW1>;M?G|eG8|Vy$%$ zJ;O+3Z#71g%J&5NPIz6EgCOX2Hfh|BQ9sO{uWE3sDQXbwO{5kz-GqTS>>V~rm+Z-} zl`C88w*G@}2V7Zx_yAuTDN_FUDy;UV&HB|hLU-1Va>n{(hGkqj5@6Y|b};Xy`K-6m zskg(YU$-B&{?NC7d73KseLjan|JSdU_etet56ioHz<1LgAM<9EG~ww3)yxvM(_KR# zZ+#QHLSofuW7;;0wC1V_U6$XBYHo2d4DiWSjd1=_zAAUZcaQkoq zJ|{ITdL}&aj^3XC!mSMY9(-LdWG8xf8$hwp%jrG+T*CA*7s0kK7wA?umFExs_%D}t ztmc)R%V+InCq7ZIaGT`jO%PPApq2X&4PVgMd(Z|5;7dcpAUCj$cCk9^DR3UjkmGPP z3)oLb^SMfZ*5URX5?v|Z+{Oz{r6QGJOgRg|T*^bm(xYus^=O*Wof{yiW1nTX@)jAs zG{1N^(0XqmA3X_Mf8;wD@DFzi3^(s*eoy{R5BeMLC`VE28?6hZvLo7?E{nahz4G19 zesFw`JYIdo=FpG-4|!!x5BR-jLs?{}s~;o&n)ZU-5((a11@SYgYeA@me6IQjl?2rAth>~Y{+zX^wqXE*C2GH&(6>1R6*D2z22A2Fj8XM;!% z7swl~!}@4zrDW!?cLJZL)dlZS((?1xMw#y=B@5}o9=T^#0r61_zEUnwoke;%9f2@B zql=V5-DusiWsF563?*wt0L)KFvgw<|nws2pq`X@Qwk*N%|N2fpF!iBYDS4{Y8Uxcw ztwcAApdokF&E}kW70|dl=tObeY5=bYO?Yt_DL9a(Q#1FJKQNU zj8H?DJ6cmhcT7&0;$b=|J&e2bFKlL|GR|M{CyZI{@9ZOj4mE%f66)<^ODAim=(DXN=)1Z$YFo z+0iM=K=vmK<7{mWxd4s(F+za=P;D{HYojVqSNZD+*0Wz z2B$uO9(*H~OOXI6Fh;!(vwBaxXc;a|#0~Wos2ehjt4xFDnQ;7_tt_iH^Z|vFctKP|2*a@E@i~8r zN0qs@u-`?xvS|X0^u$4VRRScM#F>Y;6eNDbvL5#-_X*;0TR@=as#sq@7*{Aw%6$Y9 zQq>;_(|b=+j!1Op?1U4)_C8qu%j-G(R@+x!8!02Br+GOLs;5WRhV#S6u)M)A89c)d zxNwvUujWM1gQ7r$A%Lp#JW=Za%GHtNX;yX0Q&ejc9c#}U?HDtQY|QyyCl8R>+1&Dq zE80~FMD@`d&g8PhldN7hNg}gKGV{~zp3oL)d~#FB%^Z8vnlJt2o$ny;B^q^5AAF`)el2V!7qNa|NUW$6$8!sJZwol+_kV z1@)>yZP{@J;1*tES2H*xm1TJWU!cyvRs8t{sde`lF3WO+nOD8u}N(2Tj zc=c1HGy=p8Q`;}|R&Tmb4zEMk$FjQ%cEkF|KM%)#`S%>`UxRl1Vn1<}BS9&LKKnlm zVF?Dj8ZL3qaFlR)NNht6LELa%=qg99m~?khYYajz>|~*^Xmqbe_bn$AA4 z(#INf%ynp-8G<-3AT>EAcv0ISwMX-sg<-e=^}5DRuT#NUX#?=(U7a#m1r7&SbumW0 zB5I>_R}f)UkSvI_2UP0XzvdSNaZUQ6lcEwzl-m*+ioQe0s&N5pZ)m%lUI3=1l*A}e z+aA>2YJzDm<;w={t;=xaXMPLTKk@mmJv%;c`p7m)o{W?8XgZ}>02ya)$Ff{1UojZ> z<#^J>?yH_NQ6>=eMGf*65>m}vYSU|xrvw+_nj_MfA-}oA7NdZ_B`~N4U^r=?or8n3 z8ZZqtN5zUsomdo}mrkH#jf5K0IT=6{yKkvJelc?K@v zlRK+?ON1^tcJvgravf>70#^5(f?+Y3Rk~KaTw(_msEWRj)>;@L>l-bG%CPJJ1f}?Ghgvw z7)vl#FFbsCSSITP(^V>4{b0*79R4?Ngw@~sSdjAY*D~cd6Q;a|D6dOhT5v6?6Of>Q zb|J+iPr>qawCnhYcEw5pK`oJ;aab_b7-7LzVM0Og(h_BpICV9H611Qrj|{pkp;NqL z8$i_|A@k6+Y_p8fj4%Qa<_l(pTo(Yxw#X3@u8F+qs2T*k!&N^h^bf?^C%4#bb*WA$ zD}&qHF@WuyzV8d`EU~g#v--zZ!I4+L3DUhsLWthFFSVD~ndZrf65--Rv~ z;2P~I&^Vfy6s2^LNH$Af50b;6(9utA4EA^pQnO(Mceh4xvFXj0e8|)KM?MR=*OT2l zU_RUMU`k%&@Ey! zLXrP1k}&I`|MN9~=k9{()|LKmYne$Mn2OL$E&_<~lrQ>0zKb*3)&nC@nZf5YSSb{o zNmh^GacqGVfvOq#giv>q$t4r5EfwN-^Ffp^S0!y;0QX)9TU@vOdtS3G8P3f^n3)IcK zvWFB_mf*-w{}vqmk8dwKq5iitZRDttgFpHUeYlazlHa9^&Ldh|w_#Cj>as$Ocs&e` zOxU1{t%~_EUM5>UrHVi?#8DDuBJ{|n0@F-cox4)t*A27-S3}+4+)=RxfbJIrjRl5B zHJk0Xax(>0;xYk%8K{!#R;@D$L0tc({l@pMsHw`VI(L zT8V>pfJYz_%`qQ3w9qZWqz6fN-mbD*@X)J%8BYAxpO(p(y2)R~Nr0Xu`uvuA0j3Dc zmt2sK*}zmz9niso5D9ZSYbPn#g4HA*qy}4@S#3!_am?fP0Fj^gErV`$@KM;3387jv zC?7K35@!#UQ!Blij*xUUf-aaI#>fQ^A>?AUhk$-5Caz-usHI?2KSBK-8qSNu?^s91 zoQ>QsA&QZhq6(q1K?xcS=g+7~WZVm&uO(cVEW+Ys3A)8;5Bfc@{->YrCFFmH)j#|M z#PYu?)0}~R~@2l`LF5!OF+E#U60uypbH zFzubvl=tdlSH?P;3`8S@l`Cd4#~h)%Jn%yD5Dkbs*6b)0yCzRCHtRry8XW<$UM=Th zu#cwG0ZIFPSd%r>S`i=IyjE#muz9h)OMubz(q8EjT3RxXwh<#Ylo-_wKs+nx&bhQWs^iRp5rpEp?lvugqNtiaL*=fJ|g zv-dJ*x>~X-=$F6!>9G8Qr$M)4rKQ+IlLY8T z|IM5FXws+pc=~o&Jby1No_jW=)%7T;bsii>ht#eBjG`FBIAsiV%pA3-uLX@r?12XH z1-c{J!y8b8Hy3~WCtn}Ra*H9Tm8Jv{nV?>G=nTa(7)jbc0!FA?n8+E+YcBJ36hi!g z)uGlV!=oEWXO`=S{63iD461Qj;D>U9^wuU0wMuwQ>d>}hWTd{o8T2Ps*)~0V5~!bw zUwYQ%u$0bz7ouGGg>qDKJXaf4e`@ zHXRhnk=K~?zw_JmX2(zTb5eb@rnif;w4XlWTv&eLGhp!>9s^xIt?W#s+zt|;|Hv=E z#tnD$Vbd+Jc=5TgxOWe~^-r`n!Rh!Z!{jG^0gR$P(hs9y;n1jM4-9-MA3kubny}GK zay#Sp=s$lwbx;*we`oa^oQ7zPK&>Wl*csLZZZ0S3l(Jo8h>)NS*o^9AzCzvZPOvD) zVTgB*CV9{n&@=!B@OM4{z^Sk-lo=Y)Lun4MnX>Y(yI91UdRUy)+=p2P{fQp*Yy6^s zl#iUwZ|-=;Ww8AGr^CYIE-og|gOcRHw!9hRz~BE*lc^)jBwHtCQJxb6* ze1sur8=yo7zBNW``F4tgL0eRpF_hOJW{_&lT(#@iKl_bi{l``rWUxtij0(%Z8mtJJ zV~O0`Hn3rkQAQGAM=fdR{zMa?5Rj#GD68_^L)qnn&!IG6z@PXK_%@omqkRY5$!+c!wc4bLT zCkH4*vbEzIJJ8~zQ5Yk1aGRV$*JHS=enuD=+>WP4N(7n*DO|P~pd8s(X$Da_RqK=` zqQ7HP;*4r3dng@9_^>Q95ECaIJ5j)&4<1eVEgJV81=zX-3y*(f*$BP(v`eAevmH(+ z<&77!Yi@$$zy1e(Xz@DeF5Fw1v3%JhpvwnCr*O=F-45 zU~|xsn+9Xe8xgivva$uyZPVUKI?%Uuf0-zhhI7-j-V~stb%q-JoIVC115UPAh$eIt zqQRs*mDFGZQk|UTE(OEDaKEF9g$HJR-@>M4>uBSnL;*1D%GTtb!aK&LXOU11^J%P^xv> zYawB1Nr(60+P`}9dsf#@zF>lrk~IZM+W`g;s2%N#7ozdcSw`55EMGK%faE`H7?uqQ zG`8{;1gtsi{L^^cG3snIRSql$b=L}n>U-1*;WwBTcTk$M?glM$S8!~jMj`LN-ElEh z@iT)$4iZH>898U@@@r{Is4ksc?aiDMg#n6HP`HdT^fWFI;=~fo!rScNwvbC-+ zY`SwhO#7ML$x1&f+Q+&B)jtZmw)XGfxo;X>y~5`r@SsDGZ>hdtXF3Vhzs# zDUmr{Rj(6USrYVB3{sEIDL|m9l2uP?HmwudQ%4Ovut?G;3}8U09<0O3@~~A+tR0+R zRbmz3mML|dPK$lLuyZ>t?AV65xKK_YE@xEcy|RnbazJZ#I=kK`pY)PGFU_6q+npD4 z!St*h+@6gYQ#B$8F^Ev}rDTj_FYY%D(JSgt4Md23Kr0`^PZwAeZktDmHO}~nN}cxk zmX^1{uYK&j@1O2mTD~UP!A2#{f&4Dux-B{|%1~0Mhg$0DM54v^g@&5OG_<2ZOI#YI zjQ|$4b!eJJD@x4XYQ^iTKSV<%ZwLB%n9{xq=zPtKMQb(zgqbKYOq}&p^6S4u*wR0R z@&KTk5b^7144=#FB@z(AjAx@$Y)Svz@&b#K<_(S&(nT7C;3E-dh~ z^F+M^jJ?&uq$~tZK`(+>q~>{0B!zNmH7%-?S6M_hQkBGFvr`5kI7>m*hevDLNgSUb zZ0N4@P@@PUI0R39KetY|Tr<7x_@R$4P8VTg^#n-N!6c%WB5GA$-kZI z6nXjO2ne=m9QOc0B{*?pukY-FTg~Vc6Fqk0dVuM7$`jbSg_n=?amm&C zNpBzTf0KnP_7G9kGz6oCQ$Ft;W7_z8HEh*sIK-_$M)VRhNLGL+ZE zvaVM30RJjbTcKrdfy}99G|vOO~+0D0C$M&wIQ=yD4Ui{WfHBpAg!68lH4NqX@bvX~_vF zIrMwbkX%P?4sibh&A7eV_fnCz2?DsvAnQ@!=fAhz_$MEjZrPS_gzL$` z%JiYwGPzHI@YYaoZ_PY~wk`=gKRUzKAW>vP{8T2AZkyDPCXaiH%8%M!xi47Oo2c@` zM1=b)BnLE!myA(!U__e{D^ein3B!jmQ{Y8{hB+E#=|#wZtHL$fDL5i+6W9aQsE`dR zYC#1g!36L~0)P*OxfoiP3#3}ZaO2PzgU<i5V0=! zy~)ZB_^oT-|3~@pVlHR9ef8+U>!O1dyNHqJ2rBAhpo>KvR6@EmR}cjUwp=$AQQo;p zHc(a}sZ$^ONA-7bLitc?9xapnh=jmas%e;rHc0wCHPm!Tr*o=!H46 zMO0%&ZzpWaB&yk@U#2bi_mEZre?}uQq@|aRbqpdv>0n-S>=#u1GcBQ7iK;GG?M^Fh zoMGE(52!w%j+L!>F_2R|t~@ws;~@p5oMtDl`TZ|^;Yhw>=f-^Q&Kv&2($W@Cs1d2k z=I>~7+oLXIMy#Gr?0BY2HQf3ek&;UrkYg-FFuo_)Y7>M8_3_-go zbAT`q1+JAvSwW(f{7E-)8hGSXjY0KVI|aF@g9EiWSZ%nbikBe>9tc9wx{|el9)TzS z#rg#AfrpqZIcl?g_c`6KUHRKTr|ng+cR%|6UwYeg$KF(e>CT6dW6|4A0Tdy2UU8od zoFGfv!|XvIp(yEw#%fV5uDk%NA+B|g0&#S#3Diae-El4huaikc5WxsWG$tqv-p`}L z!<}ufTBRoiCQ#L@T{nTNBG0)dh*1P`+kqe&ZZRlhO=TKM}jzen??z-i!evmR|_ z2JmWus0w(Xc2hYkJR5CRW@w`?vFS(lu@X-MVfTz#L;(NdF7R>dLnFj+DGw|E7*F@BjA_+?05bJnA&t2=Tw~4lmG8qKecxFzQ;|< zYp1G6s1^ii2S`Y-+E_fWC+s!ny%!7_EL#n~6m~ z>c$N+D1xsRKs6Q^6##L?XVZ_-x>T(~SP(7q3krXuz08iI=czx(y8>fm3q-4sp?zz< zw+nz2D%H-Q&=&t&j=0B7t&}0RaeFVXs0?-2E`ZaD8Kh9O;bKj3mrdRL#xmP?H z1lItyWH0NLuD$g$KfHa{x%o{6^;vhuPI8S@>KXTjzdDK`5{;_ot50Ztd%&xT1exS% zOslHM!~#>Uqd&^@qe@z15ImbV$LWpgZ>5756m1Hown5cp3>ivjLfLf!BXb!y>H&!%SMB1wbwAy4KT4VoenK*2gFCh-? zaLHx$GdlEEYZx2{63*dg`um0eL+s?f>+hH?rVUS!g@?ul& z))NM={Wcc|fW{cZXB3<3p-vTXg083~h(g`bv4vXf5I`&6twMiZi4#FjNNt%=}f8GpaC>om2J_32>Xe zomfN;!3KvM4fy=Y(KBBh5{@~PLq>h5NCh`pYpAAtB@LuKN`8<~uSdsho?y0e&Lg^C z`0(4ltN%3ts80(WlY)gCZvQ8*dBdK`;w#qY>wQVGHO~z^^;5zYxZG1&1;Jz;5sTG@ z!Ht0^@)1CzQWROZxfCC0ilMk6h-cF1b>N*s;X?hq@e4ItWm9>*N!yq?^t%XGRLL3) zaW5X+#Lzt=+!oN`Q9i=)dbQO;8x^}M&BN&ol?GRBBlK3^XS1Ol2yCp{C_O;*#l_re zn>+&7EYc9gUEANNw`jV!fCuLArt|;9D_;SPRNs^+eVV+wFXyNJ{go#V-h6Gh&`l;# zm95RFwnGg;jKizW2Z^bGCa{Irgmu$sq^VX#1b>~H!K&L5>j@1t1`-YACks%pHS5A$ zw>KjT5YO6iQ$$z8w?NjeU?tYjO+s^jTRDNDhEWi1E?&G01@LJm3^o-+P)bEidyr0x z0ErAG4GYdUPXNTeK;<`Uy&!rAF*v3h^Z9)DBOXuis=u*_EOqB?o4ZZ=l3v~a-E`9@ zzIE4*v!|3_EdlP3dhMdBxWkR}2c+wob+SebL$Fyg`EIe*5c>t2xdKxhuK!hr<+^+g zG(^agu4=S02u(pY4ylVtex)+AeXJ)#-MJ;R zK)i1KBc+4c7BWcXPsb=Z;N#L`(lWrzBaGbU-x~f2YiJ-yK(pK|6bwA2&ns}q>{_N7 zxp|1uGy`+g2xI-_ikiI1Y{=Q}-RDkjxc0-(5!zQ0eXRX$%7>H8?)ix~ym8Cw>dPr@ zl%sB)ccBs#BQnngrZPPM1mzo=@5A{3%Un*+_mA9_HgH=;W+u+B4FYwG24>fwOaQ1IU_Lbrb?(6#KIDg% zR%k8MY3tE`x0=Ly5W3>`EKCW;hgLl%oJ_jo3tN6`-_L&UOF?i8oAmHhsXR#k@z3u$ zc;nT37I2C-!Gjnvr78$O`E;b+sNIA(2{0TNzoBT6Jmw@QP<_Db=Lf74V!VKDZNoM~ zh>+`+yL>@+lLjSQ*_{~5hkC!PO0vjb(J*a0Pj!PTAEau2bo#6BZANKzdtbhfN~@|p z-p+v;*yOYVp-3UbYNsJ+_oFtHa_f5ID2@Y!Hqr*|zT~NQ;Ez9JKd@6e-=tlenlYL( zhjg#|vtPet_XU@AYx9k{ZM2Y$y*%@!=1tKm7(IDkA5TIsHwmy&kv1(3N&|U@lu~3f zwoTMQtr}f~7|a)fAiZt!BA^g>CQm~s{1loAfZiBYXQIAk^_QzT{h<=TwqI$%jfzS7 zjP=z&ArOK9LY%p}4%MDq!n%zyV+6Wh=iXP-ukn*@1})1OO(Sp+Xso>yGFn5-c1$J& z_=R6O1{IB=o&k4;i7|Kiqm3SqyDxlf_qsp&A72k64r!ZC=Kw)`*XKUD_T=p=Z+-IR z-|%B6kKQ-K{LT@RCCq)%%!69etDFM}_L@34>chwxkW4uQ9GNvV*A5W}f+(CIuC;N? z$Q>O7j}EdY9YkT`a8vQC6K%Lj zl&W(4gOsj{te!HmZ?hc5K~*-dvyW+JD*ffBxy) zpY(n2e$CBL6L-^Ao=z(2Vf9tN@f}Mm+kfxS!P{pO?55iE4(bq9LO(h|DyfJlXD~+D zVz@@PQ3egI@kOQ<>)GSlVk!Ry;TsQe1*jp@K**G6XeY>u5ZB8ggBv+bSkpiY(fj~q z&)DV~g^?d{q}5_$V`P;efz9NI#H_=8;NBF#sb%9Q*svaF18z)j>qm(Bhu1qOpeb5kn1n6sZ`vDzD3Qmq z!{~2S^g?@;=6)A3gpB2;h_?I^vyJU%oxgBo_1O39`u|@3Ryc)2+NK8r_MGyGI~zw| z_4@DLy0Y@t!w2r1_4dpZf?P5wYZZiP^|rPV?r$YvREhbYB-e~U6ro;@=PP8P5C-se zxhSrp4xwyW-&~2+n8$Qat3iT*L}x0W8zMDByr1BG9mGoPe&C{-N$R#0M$zl&<;sr% z8tYj8m~h5Mn5apNmDH(d3)WfKJn|E@Rr6IZsN8DOjM9A9p7SRsPaJ&N%FqA(w}9Xg zPBT4Fu;)}NIR2{FKYwM-F=+$uv{KVkeEc0br~U08`6*v#Xm&O}g4Zn84b6 ztw{)Sh;BA^8&fMD8LTzB{7YT}Ie4Lo2I(XUR{R=34}N~5v{BA5W1v`mopXuPYmM39 zl+$%Zo`MuOMMNxov6*E43{)ei?fk0kv3$OVqjIZ?k^-0`wQ10Zq{ry?SEgMb8kz06 z@bc-}eRqDx^3T8gz3>2m?!9>6si=-OeA^E^@fF|jJy#vL<+EvFI?X-lf=GtYeD8|$ zEyl=OEp_-`zpBy}E0P7F7oHAZTA?uzB6S*-tl|4+%<4)*)vZaoQ`V2+Vx!ybmy#?( zwwVu*s%tjW8VTZ};mjCaK-vi1+H1x)&O;IbW?_SMl^$jw6-v?&*KFg?yi{fjUM!{c zG{-&rFUQ~g#5*7RqPPCH>)-*HE=}oQb}CN6Z@=UpZhOg7U;Nn&WC#EdN zgb1Ag0OVBQh+B#c@pt^zC+5oLk#_1oJjsdWei&R;#Zt{s4n@gHt-~cZ8sWBS+Ki9l zb;gPtcJ)044~pH|0>s25P@^%81?8tkwpuSnm6wfy*A1q5(W8o$-|sUIo5>Il7@UFz z5NcHgK}a~Fw(_k$5V9~`dF12nechk@>Low;uGbxf({x71(pLiZoNQa)oj?BTZ#idi z>3a_zzGpV=r?dd$3@B#`j&)6}kVgTM1#S8=NW>$W0nuR-03e0f4DfN<(2W|6MT!ipd4hCxGB(;0R$|KdN8SlE&+8 zXJ=zr7zCpjte=+DL3yi>i7?6}UU%(7O-Q6==d-FLz|9vk9206j~#L`cd zzEZGjD%|#&@Bis5E`Q|H{%rN`no&T z0P%j*CQCUjI5#8NS<~`tutS`MxGY^Y(aJywtXWBI2ETpDiEVh3(fUsnfr_fd8Xl~9 zr)#w&54BYu)+A{rk~iAuf|2Ofi;ANon=4JbA-sj51(;Ui5iAR5LYnN%W57~ktZCquG}lL&4>^}XCVzFs4z|4 zWeIMmT~Y&t>T~oP8?H(<1=?#hQw{SvH?U&S{R+B?2h9}3O2(gBWASfgg1nBr!)@nW zjCb67^BaF<&z*no)vtbamf{Zz(6#OdGv)Hi^2&dC#S1R}@u$4#chBB^&J&N_b4x#S z&Y;&!gQuF2Xl2c2_skBpJN^Vx6j3me0~+pA1r0DJ(GcQtw!y`v_54vkw&8oROvnbKW(IhJ9nRp zM@}BS>Q_Gb?!W!du6)O>;F~W99;EbOgI!YzMfQL5pSj}iJnn~{@@>DlZ`XNGK6=m1 zWl2pp0zhrdX24_vv?>Bq4WdelFCC;F*;qThLKxo4qZl>v5J`e^zNnjj=N)kq0 z)c6m`wJTsY9l6?M24{sx4QISanc5TtLwYfjP@L3Yhu3kPx_`D%B`^$n-}3V7ymYnO zv3EbLZk+t|RkvOL!++xq|N5g4{6Kh+)0qOhCTsZ5KI89v-T(0oFZ#JXTeiL^%{IEj z2k)k7SN1Tf*|?eTu{fiSL9{E}ksSiL0se>|3^r?0?zyKKfaXOsWsg}6p$`7m_=t6i zu7&A>w(%bS1~AMII}(~WrbS}?(F7A_5yN1!AF%4XBe%E%Q_u34kEAuSiE&xTgg&7#1B6_mc+UxXrXr z!JKj})XXElL;WU=8BX@xxbqrKh^Xtlo`B4dJO102=l=w440Azyu5uE zENt19){Y;!_Oo~2`17y&^FMj(l{b|GjLta7t$py&rV6vqyX2`mU-_sfe)9{z?pZI} zwzT-{#U1<3r6c#j#%#67fD^07%UbBTcGE`QrIP&|TjPmC39K+S;FNqW%7zw4^J7y@ z0mBE!z`fb`mXQIVir{z~?Ipyc4IR`SI0s;qdm7)yL%jp00FTG3S;cpvzJ zp3@^(WpcF7gdVr;JDuk%0M};SXx7clyODB;|MAJAw!QWnBGBo0pZj1B{`(fDOSk>n zHCJB0V{z%4JJ%0<{4ekM{1*babGqL(=VvngFIe@gwPci4P5=M^07*qoM6N<$g5~MY AP000^Y1^@s6LVfqm00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP-n-lF?zZ;Fm^NTdTS;v&5^7Lt zw1^rF;lW@FF-&ufJgHkC0_ZYLEn`t>Wg5 z8|_@{aOUEtRLW}L**)F!sYIwpEyp#$N6x_=r)ck9iA_z)yf^X8>z_oE$@j8Bc6qFL z)oJhVJu98^@&iBw-4nOU=l%F?9|xs5AO>L3h5q>kEgE94%a5kPewLd7eAY110iFCgPu&_h6t_W57xDe9o&*B3hbrA?QY5rvYfY zOHIN;ia@_YN+B5*A%tPWFi42k`UE~&Ze?Ct+YmlN2GRTpd;-@+H88M8oM~NjrD1)v zZs$%vCkU-y9~A^vxG;=wf)M}YO)?4%j{?Q=Kq`78e8DhPtN}kT2@Z&ce@&SX}E|38xUp@&K$+02Qw~j18ec;Q~=rRq-GFpn0u}ep7)qlRJvsB zaQpjrs%mCf=7C2vsI@AU0q@Ypsoe^bNgXDXE?!aEcRP(p(kHtroEw=vEGU7RJmK3i zn7Gzg(DB0;hDk0U%X}$Chb$Buz;l5t$(&hVq(h==a9HVEW5dxC2RBhet%xtF9byr@ zdS@WBflwSd1{(DAYj4-&1wohL00jvWLhyZtiaO|BynHQk{6Gb) z8*JG^4+=7MET#*lUR*N~ptNj68)A6`HeVISi6V{&E?r%0KU;N%^B%x|9AX-Yj0t9u z8Z0RtD90_Ae93)=A* zkYQuwT({7N&U21GG`hq-^35O6n6j}Y<@qZ%d@f1~-a%<+gLL|^ZHvuNU7QYsPpj?r zT{N<#w=}7sp((?z$7y4mj7E_h2T)4Tbx$bKbJJ3PUEVI2R&9b~=R2XYasfP)0B|A* z$$WyIegWORTXEm^58F02+jTqN_2)1#Cb#(Ph(!@G>7no~KuhLDIf%nv!>M_)njjhj zICnvSv;$yMHC`7kc6eJgm74NnM8EvrFZb^#Z))i5W#FM5v2dt*K45n*-kg}Mx+}=+8u`yRjw;K} z7fK1s2*HW4Ve+n}F*ar*4FH;>jZ?>)`+?1kth*c2)$@w)`NzQk7(20#wiKct?BmUH zU-Oj8W>|*o%%*vf3LSzEFNU-E1YD}#KIH)(DfnvZ@I9H#N?mAvBQL-CRQ^QvxVG(x zCgc9dM^_RFwpUi4X)h`W*r8ly`D)BLvA@`4L);I@jNS|DA3x(9-yhRbthH1yW`eqG zLEK_6Gck^t^cX3=Mxdi1fH76h#(goljg?7$Duo7kaK*ZsXxd-L9jsnxaD0g!USa&> n(nDYO&eRkB@5t3|bKUy~3DME$gByO^00000NkvXXu0mjf$bh`a literal 0 HcmV?d00001 diff --git a/core/designsystem/src/main/res/drawable/img_home_call_received.png b/core/designsystem/src/main/res/drawable/img_home_call_received.png new file mode 100644 index 0000000000000000000000000000000000000000..9db0113b0f84aa8212ce8e2901435b973ba0a7da GIT binary patch literal 1530 zcmVHMQuxoilE1_+uaCTa+&EaXGx)zkz8gv%daWQtee-Vej4_%5yse%)%|zb!OB_wjbaG!*m&W>z`Rj^bLS$@Y$`W3Z9NN1 zXT<1pWn~9vxg^S3JMOa9_S^C~LXC*!v00laHhX&xY)PN_-=VH|)Vj1cqs6x=^(rB9ou zWvzN+U2YJUoCJX(nR=M~A?A*J|^ci^&YTOrKDlh>jfyc|Cyl(jJvRNs;))zGrI)cebAzdFLtVrN z1@rid4`00an?*E;qdv|tA(Z3u2&jgSsLf^*V-bOX zbY`YZpBmxu7%+^0BHK6oR6AxKX==HlFWjx4WFP4T4Aw1_5!7AbCO@&3tm2&~nA{?(>CN2mE^l&9jB; z*N5lEr=+%k+}B)MD*4xfDSQ|P96D5+e5c1v4+>uV-6HXZJlKDU_W$ z-?lGsVE3M~HwT@HSLFh;6XrNp+puT#>eYgOSq%0Z{N9kt%9BgY+4D|zU%#eCMnz$* z%y>aTa!LZt$Vd}!-5$Ilx!tGY6DNM`a*i+oVHzD}#8c9e5s!Ab`ox8T*OnLE)DkL$ z@FX5RT5c&UEOMC5xyyR{dYC~F01Fs-JTh`RJtSpf;zWZXEZ;U_rLtE6Syixuw*jYH zB3z46+1Xi}lTtE{oIY7Sh#r$=jW<$WetavBprpI2gGNP1FhiJ8YY9j&h|DO8nAe+L z$vOb94N54Oj7B>7i3!5+1IHh8a;N9x$7ESkg|K$*iqcy*J6Fw_y8ugGS&%#uSE&Mo ztH|q>dDRm}c!^07F zhaI#YBpM79bC-D+<#!UCpFbZioI72xX3eY3nsdT9;MAW3RP*zp-~0RQ`(k6mrKwY< zrB6tn6sOG_4dEy*J^_V^qUFr#nrAj{T;Jw*iu|8y3z#m|z+m0F_h#ngVV g+_GiUpP|wI19RlJ)ua7|VgLXD07*qoM6N<$g2x@)1^@s6 literal 0 HcmV?d00001 diff --git a/core/designsystem/src/main/res/drawable/img_home_call_send.png b/core/designsystem/src/main/res/drawable/img_home_call_send.png new file mode 100644 index 0000000000000000000000000000000000000000..277db0da9e03f477c357d91e0f3bdc8c703f5a51 GIT binary patch literal 1667 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%0D_X0ZUkSnI-NTsWu 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

yNE_H7rka?8~ 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 LNkvXXu0mjfp$I|M literal 0 HcmV?d00001 From 4224e4aa4ca918dc86716737db7b621af775e91e Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:04:53 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/home/impl/build.gradle.kts | 7 +- .../java/kr/co/call/impl/component/.gitkeep | 0 .../co/call/impl/component/UnreadIndicator.kt | 38 ++ .../call/impl/component/header/HomeHeader.kt | 221 ++++++++++ .../component/header/HomeReservationCard.kt | 247 ++++++++++++ .../header/RelationshipSummaryCard.kt | 136 +++++++ .../impl/component/history/CallHistoryList.kt | 201 ++++++++++ .../component/history/HomeHistorySection.kt | 50 +++ .../component/history/NotificationList.kt | 378 ++++++++++++++++++ .../main/java/kr/co/call/impl/entry/.gitkeep | 0 .../call/impl/mapper/CallHistoryUiMapper.kt | 32 ++ .../kr/co/call/impl/mapper/HomeUiMapper.kt | 25 ++ .../java/kr/co/call/impl/mock/CallMockData.kt | 45 +++ .../main/java/kr/co/call/impl/screen/.gitkeep | 0 .../kr/co/call/impl/screen/HomeMockData.kt | 114 ++++++ .../java/kr/co/call/impl/screen/HomeScreen.kt | 126 +++++- .../kr/co/call/impl/tab/HomeHistoryTab.kt | 11 + .../kr/co/call/impl/tab/HomeHistoryTabBar.kt | 82 ++++ .../java/kr/co/call/impl/viewmodel/.gitkeep | 0 .../co/call/impl/viewmodel/CallHistoryType.kt | 12 + .../kr/co/call/impl/viewmodel/HomeIntent.kt | 15 + .../co/call/impl/viewmodel/HomeSideEffect.kt | 3 + .../co/call/impl/viewmodel/HomeViewModel.kt | 103 +++++ .../call/impl/viewmodel/NotificationType.kt | 9 + .../impl/viewmodel/state/CallHistoryState.kt | 8 + .../state/HomeCallHistoryItemState.kt | 14 + .../viewmodel/state/HomeNotificationState.kt | 18 + .../viewmodel/state/HomeReservationState.kt | 13 + .../co/call/impl/viewmodel/state/HomeState.kt | 18 + .../impl/viewmodel/state/HomeSummaryState.kt | 9 + 30 files changed, 1929 insertions(+), 6 deletions(-) delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/.gitkeep create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/entry/.gitkeep create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/screen/.gitkeep create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTab.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/.gitkeep create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt diff --git a/feature/home/impl/build.gradle.kts b/feature/home/impl/build.gradle.kts index 9df30f0..3c5f710 100644 --- a/feature/home/impl/build.gradle.kts +++ b/feature/home/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(project(":feature:call:api")) // core 모듈 + implementation(project(":core:common")) implementation(project(":core:designsystem")) implementation(project(":core:domain")) @@ -29,6 +30,10 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.ktx) implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.paging.compose) + + // coil + implementation(libs.coil.compose) // timber implementation(libs.timber) @@ -37,4 +42,4 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/.gitkeep b/feature/home/impl/src/main/java/kr/co/call/impl/component/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt new file mode 100644 index 0000000..4ed9a46 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt @@ -0,0 +1,38 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +/** + * 읽지 않은 상태를 표시하는 컴포넌트 + * - 홈 헤더와 알림 목록에서 공통 사용 + */ +@Composable +internal fun UnreadIndicator( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(6.dp) + .background( + color = CallTheme.colors.subRed, + shape = CircleShape, + ), + ) +} + +@Preview(showBackground = true) +@Composable +private fun UnreadIndicatorPreview() { + CallFromAiTheme { + UnreadIndicator() + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt new file mode 100644 index 0000000..8993e94 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt @@ -0,0 +1,221 @@ +package kr.co.call.impl.component.header + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kr.co.call.designsystem.R +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.impl.component.RelationshipSummaryCard +import kr.co.call.impl.component.UnreadIndicator +import kr.co.call.impl.viewmodel.state.HomeSummaryState + +/** + * 홈 화면 상단 헤더 컴포넌트 + * - 사용자 인사말과 알림 상태 표시 + * - 전화 버튼과 관계 요약 카드 포함 + */ +@Composable +internal fun HomeHeader( + summaryState: HomeSummaryState, + hasUnreadNotification: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(312.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(265.dp) + .background(CallTheme.colors.mainVariant3), + ) + + Row( + modifier = Modifier + .align(Alignment.TopEnd) + .padding( + top = 7.dp, + end = 3.dp, + ), + horizontalArrangement = Arrangement.spacedBy(3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // 메인 연인 교체하기 버튼 + Button( + onClick = {}, + modifier = Modifier.size( + width = 93.dp, + height = 24.dp, + ), + shape = RoundedCornerShape(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = CallTheme.colors.background.copy(alpha = 0.6f), + contentColor = CallTheme.colors.gray800, + ), + contentPadding = PaddingValues( + horizontal = 4.dp, + vertical = 3.dp, + ), + ) { + Text( + text = "메인 연인 교체하기", + style = CallTheme.typography.caption.copy( + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + ), + ) + } + + Box( + modifier = Modifier.size(48.dp), + contentAlignment = Alignment.Center, + ) { + Box(modifier = Modifier.size(24.dp)) { + // 알림 아이콘 + Icon( + painter = painterResource( + id = R.drawable.ic_home_alarm, + ), + contentDescription = "알림", + modifier = Modifier + .align(Alignment.Center) + .size( + width = 19.dp, + height = 21.dp, + ), + tint = Color.Unspecified, + ) + + // 알림 읽음 여부에 따라 불러오기 + if (hasUnreadNotification) { + UnreadIndicator( + modifier = Modifier + .align(Alignment.TopEnd), + ) + } + } + } + } + + // 헤더 텍스트 영역 + Column( + modifier = Modifier.padding( + start = 16.dp, + top = 80.dp, + end = 16.dp, + ), + ) { + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle(fontWeight = FontWeight.SemiBold), + ) { + append(summaryState.firstName) + } + append("님, 반가워요! 👋🏻") + }, + color = CallTheme.colors.white, + style = CallTheme.typography.titleSmall, + ) + + Text( + text = "오늘은 어떤 이야기를\n나누게 될까요?", + color = CallTheme.colors.white, + style = CallTheme.typography.titleMediumBold, + ) + } + + // 전화하기 버튼 + FilledIconButton( + onClick = {}, + modifier = Modifier + .align(Alignment.TopEnd) + .padding( + top = 133.dp, + end = 19.dp, + ) + .size(53.dp), + shape = CircleShape, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = CallTheme.colors.background, + contentColor = CallTheme.colors.mainVariant1, + ), + ) { + Icon( + painter = painterResource( + id = R.drawable.ic_home_call, + ), + contentDescription = "전화하기", + modifier = Modifier.fillMaxSize(), + tint = Color.Unspecified, + ) + } + + // 관계 요약 + RelationshipSummaryCard( + summaryState = summaryState, + modifier = Modifier + .align(Alignment.TopCenter) + .offset(y = 217.dp) + .width(350.dp) + .height(94.dp), + ) + + + } +} + +/** + * 홈 헤더 Preview + * - 사용자와 관계 요약 샘플 데이터 표시 + * - 읽지 않은 알림 상태 확인 + */ +@Preview(showBackground = true, widthDp = 412) +@Composable +private fun HomeHeaderPreview() { + CallFromAiTheme { + HomeHeader( + summaryState = HomeSummaryState( + characterId = 1L, + firstName = "수현", + relationshipDays = 30, + totalCallCount = 24, + callStreakDays = 12, + ), + hasUnreadNotification = true, + ) + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt new file mode 100644 index 0000000..f3e69aa --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt @@ -0,0 +1,247 @@ +package kr.co.call.impl.component.header + +import androidx.compose.foundation.background +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import java.time.LocalDate +import java.time.LocalDateTime +import kr.co.call.core.common.util.TimeUtil +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.component.ProfileImageWithIcon +import kr.co.call.impl.viewmodel.state.HomeReservationState + +/** + * 홈 화면의 통화 약속 컴포넌트 + * - 날짜와 전체 약속 건수는 항상 표시 + * - 약속이 있으면 상대 정보와 시간 변경 버튼 표시 + */ +@Composable +internal fun HomeReservationCard( + reservationState: HomeReservationState, + modifier: Modifier = Modifier, + onTimeChangeClick: () -> Unit = {}, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(140.dp) + .background(CallTheme.colors.background), + ) { + // 날짜 항상 표시 + ReservationDate( + reservationCount = reservationState.reservationCount, + modifier = Modifier + .align(Alignment.TopStart) + .padding( + start = 35.dp, + top = 23.dp, + ), + ) + + // 예약이 있을 때만 카드 상세 표시 + if (reservationState.reservationCount > 0) { + ReservationContent( + reservationState = reservationState, + modifier = Modifier + .align(Alignment.TopStart) + .padding( + start = 34.dp, + top = 61.dp, + ), + ) + + reservationState.scheduledAt?.let { scheduledAt -> + ReservationTimeActions( + scheduledAt = scheduledAt, + onTimeChangeClick = onTimeChangeClick, + modifier = Modifier + .align(Alignment.TopEnd) + .padding( + top = 56.dp, + end = 24.dp, + ), + ) + } + } + } +} + +/** + * 약속 날짜 영역 컴포넌트 + * - 오늘 날짜와 전체 약속 건수 표시 + */ +@Composable +private fun ReservationDate( + reservationCount: Int, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = TimeUtil.toHomeDateText(LocalDate.now()), + color = CallTheme.colors.gray900, + style = CallTheme.typography.bodySmallBold, + ) + + Spacer(modifier = Modifier.width(10.dp)) + + VerticalDivider( + modifier = Modifier.height(21.dp), + thickness = 2.dp, + color = CallTheme.colors.gray100, + ) + + Spacer(modifier = Modifier.width(9.dp)) + + Text( + text = "약속 ${reservationCount}건", + color = CallTheme.colors.mainVariant1, + style = CallTheme.typography.bodySmallBold, + ) + } +} + +/** + * 약속 상대 정보 컴포넌트 + * - 프로필 이미지와 통화 로고 표시 + * - 이름과 시간이 모두 있을 때 안내 문구 표시 + */ +@Composable +private fun ReservationContent( + reservationState: HomeReservationState, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + ProfileImageWithIcon( + profileImageUrl = reservationState.profileImageUrl, + showCallBadge = true, + ) + + Spacer(modifier = Modifier.width(16.dp)) + + val firstName = reservationState.firstName + val scheduledAt = reservationState.scheduledAt + + if (firstName != null && scheduledAt != null) { + Text( + text = buildAnnotatedString { + withStyle(CallTheme.typography.bodyMediumMedium.toSpanStyle()) { + append(firstName) + } + append("님과 ${TimeUtil.toReservationTimeText(scheduledAt)}\n") + append("전화가 예정되어 있어요") + }, + color = CallTheme.colors.black, + style = CallTheme.typography.bodyMedium, + ) + } + } +} + +/** + * 약속 시간 변경 영역 컴포넌트 + * - 예약 시간을 24시간 형식으로 표시 + * - 시간 변경 이벤트 전달 + */ +@Composable +private fun ReservationTimeActions( + scheduledAt: LocalDateTime, + onTimeChangeClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.width(85.dp), + verticalArrangement = Arrangement.spacedBy(7.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(27.dp) + .background( + color = CallTheme.colors.mainVariant2, + shape = RoundedCornerShape(50.dp), + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = TimeUtil.toHourMinuteText(scheduledAt), + color = CallTheme.colors.mainVariant1, + style = CallTheme.typography.bodySmallBold, + ) + } + + CompositionLocalProvider( + LocalMinimumInteractiveComponentSize provides 0.dp, + ) { + Button( + onClick = onTimeChangeClick, + modifier = Modifier + .fillMaxWidth() + .height(27.dp), + shape = RoundedCornerShape(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = CallTheme.colors.white, + contentColor = CallTheme.colors.gray900, + ), + border = BorderStroke( + width = 1.dp, + color = CallTheme.colors.gray100, + ), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = "시간 변경", + style = CallTheme.typography.bodySmallBold, + ) + } + } + } +} + +/** + * 통화 약속 Preview + * - 약속이 한 건 있는 상태 확인 + */ +@Preview(showBackground = true, widthDp = 412) +@Composable +private fun HomeReservationCardPreview() { + CallFromAiTheme { + HomeReservationCard( + reservationState = HomeReservationState( + reservationCount = 1, + profileImageUrl = null, + scheduledAt = LocalDateTime.of(2026, 7, 17, 21, 0), + firstName = "민준", + ), + ) + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt new file mode 100644 index 0000000..bce5a90 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt @@ -0,0 +1,136 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.impl.viewmodel.state.HomeSummaryState + +// 관계 요약 +/** + * 관계 정보를 요약하는 컴포넌트 + * - 관계 일수와 통화 횟수 표시 + * - 연속 통화 일수 표시 + */ +@Composable +internal fun RelationshipSummaryCard( + summaryState: HomeSummaryState, + modifier: Modifier = Modifier, +) { + val cardShape = RoundedCornerShape(20.dp) + + Row( + modifier = modifier + .background( + color = CallTheme.colors.white, + shape = cardShape, + ) + .border( + width = 1.dp, + color = CallTheme.colors.gray100, + shape = cardShape, + ) + .padding(vertical = 15.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RelationshipSummaryItem( + label = "관계", + value = summaryState.relationshipDays.withSuffixOrDash("일째"), + modifier = Modifier.weight(1f), + ) + + RelationshipSummaryDivider() + + RelationshipSummaryItem( + label = "통화", + value = summaryState.totalCallCount.withSuffixOrDash("회"), + modifier = Modifier.weight(1f), + ) + + RelationshipSummaryDivider() + + RelationshipSummaryItem( + label = "연속 통화", + value = summaryState.callStreakDays.withSuffixOrDash("일"), + modifier = Modifier.weight(1f), + ) + } +} + +/** + * 관계 요약 항목 컴포넌트 + * - 항목명과 값을 세로로 표시 + */ +@Composable +private fun RelationshipSummaryItem( + label: String, + value: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = label, + color = CallTheme.colors.black, + style = CallTheme.typography.bodyMedium, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = value, + color = CallTheme.colors.mainVariant1, + style = CallTheme.typography.bodyMediumMedium, + ) + } +} + +/** + * 관계 요약 구분선 컴포넌트 + * - 항목 사이에 세로 구분선 표시 + */ +@Composable +private fun RelationshipSummaryDivider() { + VerticalDivider( + modifier = Modifier.height(63.dp), + thickness = 1.dp, + color = CallTheme.colors.gray100, + ) +} + +// 0으로 들어올 경우 마이너스로 표시하는 확장함수 +private fun Int.withSuffixOrDash(suffix: String): String = + if (this == 0) "-" else "$this$suffix" + +/** + * 관계 요약 카드 Preview + * - 관계와 통화 샘플 데이터 표시 + */ +@Preview(showBackground = true, widthDp = 350) +@Composable +private fun RelationshipSummaryCardPreview() { + CallFromAiTheme { + RelationshipSummaryCard( + summaryState = HomeSummaryState( + characterId = 1L, + relationshipDays = 30, + totalCallCount = 24, + callStreakDays = 12, + ), + modifier = Modifier.height(94.dp), + ) + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt new file mode 100644 index 0000000..6c1f6c7 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt @@ -0,0 +1,201 @@ +package kr.co.call.impl.component.history + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.impl.mock.CallMockData +import kr.co.call.impl.viewmodel.CallHistoryType +import kr.co.call.impl.viewmodel.isMissed +import kr.co.call.impl.viewmodel.state.CallHistoryState +import kr.co.call.impl.viewmodel.state.HomeCallHistoryItemState + +/** + * 통화 기록 목록을 표시하는 컴포넌트 + * - 최근 통화 기록 최대 20건 표시 + * - 완료 여부에 따라 AI 요약과 기록 아이콘 노출 + */ +@Composable +internal fun CallHistoryList( + callHistoryState: CallHistoryState, + modifier: Modifier = Modifier, + onRecordClick: (Long) -> Unit = {}, +) { + Column(modifier = modifier.fillMaxSize()) { + Text( + text = "최근 20건의 기록만 표시됩니다.", + modifier = Modifier.padding( + start = 16.dp, + top = 16.dp, + bottom = 18.dp, + ), + color = CallTheme.colors.gray800, + style = CallTheme.typography.bodySmall, + ) + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + items( + items = callHistoryState.histories, + key = { history -> history.callId }, + ) { history -> + CallHistoryCard( + callHistory = history, + onRecordClick = { onRecordClick(history.callId) }, + ) + } + } + } +} + +/** + * 통화 기록 한 건을 표시하는 컴포넌트 + * - 부재중 통화는 이름과 시간만 표시 + * - 완료 통화는 AI 요약과 기록 이동 아이콘 표시 + */ +@Composable +private fun CallHistoryCard( + callHistory: HomeCallHistoryItemState, + modifier: Modifier = Modifier, + onRecordClick: () -> Unit = {}, +) { + val isMissed = callHistory.type.isMissed + + Box( + modifier = modifier + .fillMaxWidth() + .height(if (isMissed) 70.dp else 90.dp), + ) { + CallTypeIcon( + type = callHistory.type, + modifier = Modifier + .align(Alignment.TopStart) + .padding( + start = 28.dp, + top = if (isMissed) 11.dp else 12.dp, + ), + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 68.dp, + top = if (isMissed) 11.dp else 10.dp, + end = if (isMissed) 16.dp else 74.dp, + ), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text( + text = callHistory.characterName, + color = CallTheme.colors.black, + style = CallTheme.typography.bodySmallBold, + ) + + if (!isMissed) { + Text( + text = callHistory.aiSummary, + color = CallTheme.colors.gray800, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = CallTheme.typography.bodySmall, + ) + } + + Text( + text = callHistory.startedAtText, + color = CallTheme.colors.gray600, + style = CallTheme.typography.caption, + ) + } + + // 부재중 통화가 아닐 경우 기록 버튼 표시 + if (!isMissed) { + Image( + painter = painterResource( + id = kr.co.call.designsystem.R.drawable.ic_home_record, + ), + contentDescription = "통화 기록 보기", + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 32.dp) + .size(42.dp) + .clickable(onClick = onRecordClick), + ) + } + + HorizontalDivider( + modifier = Modifier + .align(Alignment.BottomStart) + .width(380.dp), + thickness = 1.dp, + color = CallTheme.colors.gray100, + ) + } +} + +// 아이콘 타입에 따라서 표시 +@Composable +private fun CallTypeIcon( + type: CallHistoryType, + modifier: Modifier = Modifier, +) { + val iconResource = when (type) { + CallHistoryType.SEND -> + kr.co.call.designsystem.R.drawable.img_home_call_send + + CallHistoryType.RECEIVED -> + kr.co.call.designsystem.R.drawable.img_home_call_received + + CallHistoryType.SEND_MISSED, + CallHistoryType.RECEIVE_MISSED, + -> kr.co.call.designsystem.R.drawable.img_home_call_missed + } + val iconSize = when (type) { + CallHistoryType.SEND -> DpSize(26.dp, 23.dp) + CallHistoryType.RECEIVED -> DpSize(23.dp, 23.dp) + CallHistoryType.SEND_MISSED, + CallHistoryType.RECEIVE_MISSED, + -> DpSize(25.dp, 26.dp) + } + + Image( + painter = painterResource(id = iconResource), + contentDescription = null, + modifier = modifier.size(iconSize), + ) +} + +@Preview(showBackground = true, widthDp = 412, heightDp = 390) +@Composable +private fun CallHistoryListPreview() { + CallFromAiTheme { + CallHistoryList( + callHistoryState = CallMockData.state, + ) + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt new file mode 100644 index 0000000..d099b51 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt @@ -0,0 +1,50 @@ +package kr.co.call.impl.component.history + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kr.co.call.impl.tab.HomeHistoryTab +import kr.co.call.impl.tab.HomeHistoryTabBar +import kr.co.call.impl.viewmodel.state.CallHistoryState +import kr.co.call.impl.viewmodel.state.HomeNotificationState + +@Composable +internal fun HomeHistorySection( + selectedTab: HomeHistoryTab, + onTabClick: (HomeHistoryTab) -> Unit, + notifications: Flow>, + callHistoryState: CallHistoryState, + modifier: Modifier = Modifier, +) { + Spacer(modifier = Modifier.height(22.dp)) + HomeHistoryTabBar( + selectedTab = selectedTab, + onTabClick = onTabClick, + modifier = modifier, + ) + + // 탭 선택 상태에 따른 분기 + when (selectedTab) { + HomeHistoryTab.NOTIFICATION -> { + NotificationList(notifications = notifications) + } + + HomeHistoryTab.CALL_HISTORY -> { + CallHistoryList(callHistoryState = callHistoryState) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun HomeHistorySectionPreview(){ + HomeHistoryTabBar( + selectedTab = HomeHistoryTab.NOTIFICATION, + onTabClick = {}, + ) +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt new file mode 100644 index 0000000..1c7b4b3 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt @@ -0,0 +1,378 @@ +package kr.co.call.impl.component.history + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import java.time.LocalDateTime +import kotlinx.coroutines.flow.Flow +import kr.co.call.core.common.util.TimeUtil +import kr.co.call.designsystem.R +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.component.ProfileImageWithIcon +import kr.co.call.impl.component.UnreadIndicator +import kr.co.call.impl.viewmodel.state.HomeNotificationState +import kr.co.call.impl.viewmodel.NotificationType + +/** + * 알림 리스트 + * - 커서 페이징 + */ +@Composable +internal fun NotificationList( + notifications: Flow>, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxSize()) { + Text( + text = "최근 7일 간의 알림만 표시됩니다.", + modifier = Modifier.padding( + start = 16.dp, + top = 16.dp, + bottom = 18.dp, + ), + color = CallTheme.colors.gray800, + style = CallTheme.typography.bodySmall, + ) + + val notificationItems = notifications.collectAsLazyPagingItems() + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentPadding = PaddingValues( + start = 16.dp, + end = 16.dp, + bottom = 24.dp, + ), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = notificationItems.itemCount, + key = notificationItems.itemKey { it.notificationId }, + ) { index -> + notificationItems[index]?.let { notification -> + NotificationCard( + notificationState = notification, + onCallClick = {}, + ) + } + } + } + } +} + +/** + * 알림 내용 포함하는 공통 카드 컴포넌트 + */ +@Composable +private fun NotificationCard( + notificationState: HomeNotificationState, + onCallClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val cardShape = RoundedCornerShape(20.dp) + val cardHeight = when (notificationState.type) { + NotificationType.CALL_RESERVATION -> 88.dp + NotificationType.ANNIVERSARY -> 109.dp + NotificationType.MISSED_CALL -> 129.dp + } + val borderColor = when { + notificationState.type == NotificationType.ANNIVERSARY -> CallTheme.colors.gray100 + notificationState.isRead -> CallTheme.colors.gray100 + else -> CallTheme.colors.mainVariant2 + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(cardHeight) + .background( + color = CallTheme.colors.white, + shape = cardShape, + ) + .border( + width = 1.dp, + color = borderColor, + shape = cardShape, + ), + ) { + when (notificationState.type) { + NotificationType.ANNIVERSARY -> { + AnniversaryNotificationContent( + title = notificationState.type.title, + content = notificationState.content, + modifier = Modifier + .align(Alignment.TopStart) + .padding( + start = 17.dp, + top = 23.dp, + end = 17.dp, + ), + ) + } + + NotificationType.CALL_RESERVATION -> { + CallNotificationContent( + title = notificationState.type.title, + content = notificationState.content, + profileImageUrl = notificationState.profileImageUrl, + modifier = Modifier + .align(Alignment.TopStart) + .padding( + start = 17.dp, + top = 21.dp, + end = 17.dp, + ), + ) + } + + NotificationType.MISSED_CALL -> { + CallNotificationContent( + title = notificationState.type.title, + content = notificationState.content, + profileImageUrl = notificationState.profileImageUrl, + modifier = Modifier + .align(Alignment.TopStart) + .padding( + start = 17.dp, + top = 21.dp, + end = 17.dp, + ), + ) + } + } + + if (notificationState.type == NotificationType.MISSED_CALL) { + Button( + onClick = onCallClick, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding( + end = 17.dp, + bottom = 20.dp, + ) + .width(290.dp) + .height(32.dp), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = CallTheme.colors.mainVariant2, + contentColor = CallTheme.colors.black, + ), + contentPadding = PaddingValues(0.dp), + ) { + Icon( + painter = painterResource( + id = R.drawable.ic_home_call_btn, + ), + contentDescription = null, + modifier = Modifier.size( + width = 13.dp, + height = 12.dp, + ), + tint = CallTheme.colors.mainVariant1, + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = "다시 전화하기", + style = CallTheme.typography.captionBold, + ) + } + } + + // n 분 전 표시 + Text( + text = TimeUtil.toTimeAgoText(notificationState.createdAt), + modifier = Modifier + .align(Alignment.TopEnd) + .padding( + top = 20.dp, + end = 17.dp, + ), + color = CallTheme.colors.gray400, + style = CallTheme.typography.caption, + ) + + // 알림 읽음 여부 표시 + if (!notificationState.isRead) { + UnreadIndicator( + modifier = Modifier + .align(Alignment.TopEnd) + .padding( + top = 10.dp, + end = 11.dp, + ), + ) + } + + } +} + +@Composable +private fun AnniversaryNotificationContent( + title: String, + content: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(15.dp), + verticalAlignment = Alignment.Top, + ) { + // 기념일 아이콘 + Box( + modifier = Modifier + .size(41.dp) + .background( + color = CallTheme.colors.mainVariant2, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "\uD83D\uDDD3\uFE0F", + fontSize = 24.sp, + ) + } + + Column( + verticalArrangement = Arrangement.spacedBy(5.dp), + ) { + Text( + text = title, + color = CallTheme.colors.black, + style = CallTheme.typography.bodySmallBold, + ) + Text( + text = content, + color = CallTheme.colors.gray600, + style = CallTheme.typography.bodySmall, + ) + } + } +} + +/** + * 통화 약속, 예약 통화 타입 시 사용하는 공통 컴포넌트 + */ +@Composable +private fun CallNotificationContent( + title: String, + content: String, + profileImageUrl: String?, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.Top, + ) { + ProfileImageWithIcon( + profileImageUrl = profileImageUrl, + showCallBadge = true, + ) + + Column( + modifier = Modifier.padding(top = 1.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = title, + color = CallTheme.colors.black, + style = CallTheme.typography.bodySmallBold, + ) + Text( + text = content, + color = CallTheme.colors.gray600, + style = CallTheme.typography.bodySmall, + ) + } + + } +} + +@Preview(showBackground = true, widthDp = 412) +@Composable +private fun NotificationCardPreview() { + val now = LocalDateTime.now() + + CallFromAiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + NotificationCard( + notificationState = HomeNotificationState( + notificationId = 1, + type = NotificationType.CALL_RESERVATION, + title = "통화 약속", + content = "오늘 밤 9시 통화 15분 뒤에 시작됩니다.", + isRead = false, + createdAt = now.minusMinutes(1), + characterName = "민준", + profileImageUrl = null, + ), + onCallClick = {}, + ) + + NotificationCard( + notificationState = HomeNotificationState( + notificationId = 2, + type = NotificationType.MISSED_CALL, + title = "부재중 전화", + content = "민준이에게서 받지 못한 전화가 있어요.", + isRead = false, + createdAt = now.minusMinutes(30), + characterName = "민준", + profileImageUrl = null, + ), + onCallClick = {}, + ) + + NotificationCard( + notificationState = HomeNotificationState( + notificationId = 3, + type = NotificationType.ANNIVERSARY, + title = "기념일", + content = "오늘은 민준이와 함께한지 30일 째!\n작은 기념일을 함께 축하해요 💗", + isRead = true, + createdAt = now.minusHours(1), + characterName = "민준", + profileImageUrl = null, + ), + onCallClick = {}, + ) + } + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/entry/.gitkeep b/feature/home/impl/src/main/java/kr/co/call/impl/entry/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt new file mode 100644 index 0000000..750d344 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt @@ -0,0 +1,32 @@ +package kr.co.call.impl.mapper + +import kr.co.call.core.common.util.TimeUtil +import kr.co.call.domain.model.home.CallHistory +import kr.co.call.domain.model.home.CallSender +import kr.co.call.domain.model.home.CallStatus +import kr.co.call.impl.viewmodel.CallHistoryType +import kr.co.call.impl.viewmodel.state.HomeCallHistoryItemState + +internal fun CallHistory.toUiState(): HomeCallHistoryItemState = + HomeCallHistoryItemState( + callId = callId, + characterName = characterName, + aiSummary = aiSummary, + startedAtText = TimeUtil.toCallHistoryDateText(startedAt), + type = toCallHistoryType(), + ) + +private fun CallHistory.toCallHistoryType(): CallHistoryType = + when { + sender == CallSender.USER && status == CallStatus.COMPLETED -> + CallHistoryType.SEND + + sender == CallSender.AI && status == CallStatus.COMPLETED -> + CallHistoryType.RECEIVED + + sender == CallSender.USER && status == CallStatus.MISSED -> + CallHistoryType.SEND_MISSED + + else -> + CallHistoryType.RECEIVE_MISSED + } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt new file mode 100644 index 0000000..abe8cbb --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt @@ -0,0 +1,25 @@ +package kr.co.call.impl.mapper + +import kr.co.call.domain.model.home.HomeModel +import kr.co.call.impl.viewmodel.state.HomeReservationState +import kr.co.call.impl.viewmodel.state.HomeSummaryState + +internal fun HomeModel.Summary.toUiState(): HomeSummaryState = + HomeSummaryState( + characterId = characterId, + firstName = firstName, + relationshipDays = relationshipDays, + totalCallCount = totalCallCount, + callStreakDays = callStreakDays, + ) + +internal fun HomeModel.Reservations.toUiState(): HomeReservationState { + val reservation = items.firstOrNull() + + return HomeReservationState( + reservationCount = count, + profileImageUrl = reservation?.imageUrl, + scheduledAt = reservation?.scheduledAt, + firstName = reservation?.firstName, + ) +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt new file mode 100644 index 0000000..3e969c1 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt @@ -0,0 +1,45 @@ +package kr.co.call.impl.mock + +import java.time.LocalDateTime +import kr.co.call.domain.model.home.CallHistory +import kr.co.call.domain.model.home.CallSender +import kr.co.call.domain.model.home.CallStatus +import kr.co.call.domain.model.home.CallType +import kr.co.call.impl.mapper.toUiState +import kr.co.call.impl.viewmodel.state.CallHistoryState + +internal object CallMockData { + val histories: List = listOf( + CallHistory( + callId = 1L, + characterName = "민준", + aiSummary = "오늘 하루와 퇴근 후 일상 이야기", + startedAt = LocalDateTime.of(2026, 6, 28, 23, 2), + sender = CallSender.USER, + callType = CallType.DAILY, + status = CallStatus.COMPLETED, + ), + CallHistory( + callId = 2L, + characterName = "민준", + aiSummary = "출근 준비와 아침 일정 이야기", + startedAt = LocalDateTime.of(2026, 6, 27, 7, 31), + sender = CallSender.AI, + callType = CallType.DAILY, + status = CallStatus.COMPLETED, + ), + CallHistory( + callId = 3L, + characterName = "민준", + aiSummary = "", + startedAt = LocalDateTime.of(2026, 6, 26, 21, 29), + sender = CallSender.AI, + callType = CallType.DEEP, + status = CallStatus.MISSED, + ), + ) + + val state: CallHistoryState = CallHistoryState( + histories = histories.map { callHistory -> callHistory.toUiState() }, + ) +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/.gitkeep b/feature/home/impl/src/main/java/kr/co/call/impl/screen/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt new file mode 100644 index 0000000..f409a6c --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt @@ -0,0 +1,114 @@ +package kr.co.call.impl.screen + +import androidx.paging.PagingData +import java.time.LocalDateTime +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kr.co.call.domain.model.home.HomeModel +import kr.co.call.impl.mapper.toUiState +import kr.co.call.impl.mock.CallMockData +import kr.co.call.impl.viewmodel.NotificationType +import kr.co.call.impl.viewmodel.state.CallHistoryState +import kr.co.call.impl.viewmodel.state.HomeNotificationState +import kr.co.call.impl.viewmodel.state.HomeReservationState +import kr.co.call.impl.viewmodel.state.HomeSummaryState + +/** + * 홈화면에서 사용하는 Mock Data + */ +internal data class HomeMockData( + val home: HomeModel, + val hasUnreadNotification: Boolean, + val notifications: List, +) { + val summaryState: HomeSummaryState + get() = HomeSummaryState( + characterId = home.summary.characterId, + firstName = home.summary.firstName, + relationshipDays = home.summary.relationshipDays, + totalCallCount = home.summary.totalCallCount, + callStreakDays = home.summary.callStreakDays, + ) + + val reservationState: HomeReservationState + get() { + val reservation = home.reservations.items.firstOrNull() + + return HomeReservationState( + reservationCount = home.reservations.count, + profileImageUrl = reservation?.imageUrl, + scheduledAt = reservation?.scheduledAt, + firstName = reservation?.firstName, + ) + } + + val callHistoryState: CallHistoryState + get() = CallHistoryState( + histories = home.callHistories.map { callHistory -> callHistory.toUiState() }, + ) + + fun notificationFlow(): Flow> = + flowOf(PagingData.from(notifications)) +} + +internal fun createHomeMockData( + now: LocalDateTime = LocalDateTime.now(), +): HomeMockData = + HomeMockData( + home = HomeModel( + summary = HomeModel.Summary( + characterId = 2L, + firstName = "수현", + relationshipDays = 30, + totalCallCount = 24, + callStreakDays = 12, + ), + reservations = HomeModel.Reservations( + count = 2, + items = listOf( + HomeModel.Reservation( + callReservationId = 3L, + characterId = 2L, + firstName = "민준", + imageUrl = null, + scheduledAt = now.withHour(21).withMinute(0).withSecond(0), + status = HomeModel.ReservationStatus.SCHEDULED, + ), + ), + ), + callHistories = CallMockData.histories, + ), + hasUnreadNotification = true, + notifications = listOf( + HomeNotificationState( + notificationId = 42L, + type = NotificationType.CALL_RESERVATION, + title = "통화 약속", + content = "오늘 밤 9시 통화 15분 뒤에 시작됩니다.", + isRead = false, + createdAt = now.minusMinutes(1), + characterName = "민준", + profileImageUrl = "https://cdn.lovecall.com/presets/male/1.jpg", + ), + HomeNotificationState( + notificationId = 41L, + type = NotificationType.MISSED_CALL, + title = "부재중 전화", + content = "민준이에게서 받지 못한 전화가 있어요.", + isRead = false, + createdAt = now.minusMinutes(30), + characterName = "민준", + profileImageUrl = "https://cdn.lovecall.com/presets/male/1.jpg", + ), + HomeNotificationState( + notificationId = 40L, + type = NotificationType.ANNIVERSARY, + title = "기념일", + content = "오늘은 민준이와 함께한지 30일 째!\n작은 기념일을 함께 축하해요 💗", + isRead = true, + createdAt = now.minusHours(1), + characterName = "민준", + profileImageUrl = null, + ), + ), + ) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt index 0766065..772bcd0 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt @@ -1,18 +1,134 @@ package kr.co.call.impl.screen +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import java.time.LocalDateTime +import kotlinx.coroutines.flow.Flow +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.impl.component.header.HomeHeader +import kr.co.call.impl.component.history.HomeHistorySection +import kr.co.call.impl.component.header.HomeReservationCard +import kr.co.call.impl.tab.HomeHistoryTab +import kr.co.call.impl.viewmodel.state.CallHistoryState +import kr.co.call.impl.viewmodel.state.HomeSummaryState +import kr.co.call.impl.viewmodel.HomeIntent +import kr.co.call.impl.viewmodel.state.HomeNotificationState +import kr.co.call.impl.viewmodel.state.HomeReservationState +import kr.co.call.impl.viewmodel.HomeViewModel +import androidx.paging.PagingData +import org.orbitmvi.orbit.compose.collectAsState +/** + * 홈 화면 진입 컴포넌트 + * - ViewModel 상태 구독 + * - 헤더와 약속 상태를 화면 콘텐츠에 전달 + */ @Composable fun HomeScreen( modifier: Modifier = Modifier, + viewModel: HomeViewModel = hiltViewModel(), ) { - Box( - modifier = modifier.fillMaxSize() + val state by viewModel.collectAsState() + val notifications = remember { createHomeMockData().notificationFlow() } + + HomeScreenContent( + homeSummaryState = state.summary, + hasUnreadNotification = state.hasUnreadNotification, + homeReservationState = state.reservation, + callHistoryState = state.callHistory, + selectedHistoryTab = state.selectedHistoryTab, + notifications = notifications, + onHistoryTabClick = { tab -> + viewModel.handleIntent(HomeIntent.SelectHistoryTab(tab)) + }, + modifier = modifier, + ) +} + +/** + * 홈 화면 콘텐츠 컴포넌트 + * - 홈 헤더와 약속 배너 배치 + * - 배경과 영역 구분선 표시 + */ +@Composable +internal fun HomeScreenContent( + homeSummaryState: HomeSummaryState, + hasUnreadNotification: Boolean, + homeReservationState: HomeReservationState, + callHistoryState: CallHistoryState, + selectedHistoryTab: HomeHistoryTab, + notifications: Flow>, + onHistoryTabClick: (HomeHistoryTab) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(CallTheme.colors.background), ) { - Text("홈 화면") + HomeHeader( + summaryState = homeSummaryState, + hasUnreadNotification = hasUnreadNotification, + ) + + // 예약된 전화 조회 + HomeReservationCard(reservationState = homeReservationState) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(7.dp) + .background(CallTheme.colors.gray100), + ) + + // 지난 알림, 통화 기록 표시 섹션 + HomeHistorySection( + selectedTab = selectedHistoryTab, + onTabClick = onHistoryTabClick, + notifications = notifications, + callHistoryState = callHistoryState, + ) + } +} + +/** + * 홈 화면 Preview + */ +@Preview(showBackground = true, widthDp = 412) +@Composable +private fun HomeScreenContentPreview() { + val mockData = remember { + createHomeMockData( + now = LocalDateTime.of(2026, 7, 17, 15, 0), + ) + } + var selectedHistoryTab by remember { + mutableStateOf(HomeHistoryTab.CALL_HISTORY) + } + + CallFromAiTheme { + HomeScreenContent( + homeSummaryState = mockData.summaryState, + hasUnreadNotification = mockData.hasUnreadNotification, + homeReservationState = mockData.reservationState, + callHistoryState = mockData.callHistoryState, + selectedHistoryTab = selectedHistoryTab, + notifications = mockData.notificationFlow(), + onHistoryTabClick = { selectedHistoryTab = it }, + ) } -} \ No newline at end of file +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTab.kt b/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTab.kt new file mode 100644 index 0000000..04df134 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTab.kt @@ -0,0 +1,11 @@ +package kr.co.call.impl.tab + +/** + * 지난알림, 통화 기록 탭 + */ +enum class HomeHistoryTab( + val title: String, +) { + NOTIFICATION(title = "지난 알림"), + CALL_HISTORY(title = "통화 기록"), +} \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt b/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt new file mode 100644 index 0000000..98470f0 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt @@ -0,0 +1,82 @@ +package kr.co.call.impl.tab + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +@Composable +internal fun HomeHistoryTabBar( + selectedTab: HomeHistoryTab, + onTabClick: (HomeHistoryTab) -> Unit, + modifier: Modifier = Modifier, +) { + val tabs = HomeHistoryTab.entries + val selectedTabIndex = tabs.indexOf(selectedTab) + + PrimaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + modifier = modifier.fillMaxWidth(), + edgePadding = 0.dp, + minTabWidth = 80.dp, + indicator = { + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset( + selectedTabIndex = selectedTabIndex, + matchContentSize = true, + ), + width = Dp.Unspecified, + height = 2.dp, + color = CallTheme.colors.mainVariant1, + ) + }, + divider = { + HorizontalDivider( + thickness = 1.dp, + color = CallTheme.colors.gray100, + ) + }, + ) { + tabs.forEach { tab -> + Tab( + selected = tab == selectedTab, + onClick = { onTabClick(tab) }, + text = { + Text( + text = tab.title, + style = CallTheme.typography.bodyMediumMedium, + ) + }, + selectedContentColor = CallTheme.colors.mainVariant1, + unselectedContentColor = CallTheme.colors.gray800, + + ) + } + } +} + +@Preview(showBackground = true, widthDp = 412) +@Composable +private fun HomeHistoryTabBarPreview() { + var selectedTab by remember { mutableStateOf(HomeHistoryTab.NOTIFICATION) } + + CallFromAiTheme { + HomeHistoryTabBar( + selectedTab = selectedTab, + onTabClick = { selectedTab = it }, + ) + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/.gitkeep b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt new file mode 100644 index 0000000..bbd3dc3 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt @@ -0,0 +1,12 @@ +package kr.co.call.impl.viewmodel + +enum class CallHistoryType { + SEND, + RECEIVED, + SEND_MISSED, + RECEIVE_MISSED, +} + +internal val CallHistoryType.isMissed: Boolean + get() = this == CallHistoryType.SEND_MISSED || + this == CallHistoryType.RECEIVE_MISSED diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt new file mode 100644 index 0000000..36a13f8 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt @@ -0,0 +1,15 @@ +package kr.co.call.impl.viewmodel + +import kr.co.call.impl.tab.HomeHistoryTab + +internal sealed interface HomeIntent { + data class SelectHistoryTab( + val tab: HomeHistoryTab, + ) : HomeIntent + + data object ClickCall : HomeIntent + + data object ClickNotification : HomeIntent + + data object ClickChangeTime : HomeIntent +} \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt new file mode 100644 index 0000000..f399f6e --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt @@ -0,0 +1,3 @@ +package kr.co.call.impl.viewmodel + +sealed interface HomeSideEffect diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt new file mode 100644 index 0000000..debd21f --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt @@ -0,0 +1,103 @@ +package kr.co.call.impl.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kr.co.call.domain.model.home.HomeModel +import kr.co.call.domain.repository.HomeRepository +import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.mapper.toUiState +import kr.co.call.impl.tab.HomeHistoryTab +import kr.co.call.impl.viewmodel.state.CallHistoryState +import kr.co.call.impl.viewmodel.state.HomeState +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val homeRepository: HomeRepository, +) : + ViewModel(), + ContainerHost { + + override val container: Container = container( + initialState = HomeState(), + ) + + internal fun handleIntent(intent: HomeIntent) { + when (intent) { + is HomeIntent.SelectHistoryTab -> { + selectHistoryTab(intent.tab) + } + + HomeIntent.ClickCall -> { + // 전화 화면 이동 + } + + HomeIntent.ClickNotification -> { + // 알림 화면 이동 + } + + HomeIntent.ClickChangeTime -> { + // 시간 변경 화면 이동 + } + } + } + + + init { + loadHome() + } + + private fun loadHome() = intent { + reduce { + state.copy(loadStatus = LoadStatus.Loading) + } + + runCatching { + val reservations = homeRepository.getReservations().getOrThrow() + val callHistories = homeRepository.getCallHistories().getOrThrow() + + val summary = homeRepository.getSummary(state.summary.characterId).getOrThrow() + + HomeModel( + summary = summary, + reservations = reservations, + callHistories = callHistories, + ) + }.onSuccess { home -> + reduce { + state.copy( + summary = home.summary.toUiState(), + reservation = home.reservations.toUiState(), + callHistory = CallHistoryState( + histories = home.callHistories.map { history -> + history.toUiState() + }, + ), + loadStatus = LoadStatus.Idle, + ) + } + }.onFailure { throwable -> + reduce { + state.copy( + loadStatus = LoadStatus.Error( + message = throwable.message ?: "홈 정보를 불러오지 못했습니다.", + ), + ) + } + } + } + + // 기록 탭 선택 + private fun selectHistoryTab( + tab: HomeHistoryTab, + ) = intent { + reduce { + state.copy( + selectedHistoryTab = tab, + ) + } + } +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt new file mode 100644 index 0000000..10316ff --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt @@ -0,0 +1,9 @@ +package kr.co.call.impl.viewmodel + +enum class NotificationType( + val title: String, +) { + MISSED_CALL(title = "부재중 전화"), + ANNIVERSARY(title = "기념일"), + CALL_RESERVATION(title = "통화 약속"), +} \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt new file mode 100644 index 0000000..439c875 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt @@ -0,0 +1,8 @@ +package kr.co.call.impl.viewmodel.state + +/** + * 통화 기록 List + */ +data class CallHistoryState( + val histories: List = emptyList(), +) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt new file mode 100644 index 0000000..600a7ec --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt @@ -0,0 +1,14 @@ +package kr.co.call.impl.viewmodel.state + +import kr.co.call.impl.viewmodel.CallHistoryType + +/** + * 통화 기록 조회 개별 아이템 + */ +data class HomeCallHistoryItemState( + val callId: Long, + val characterName: String, + val aiSummary: String, + val startedAtText: String, + val type: CallHistoryType, +) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt new file mode 100644 index 0000000..3a8df93 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt @@ -0,0 +1,18 @@ +package kr.co.call.impl.viewmodel.state + +import kr.co.call.impl.viewmodel.NotificationType +import java.time.LocalDateTime + +/** + * 알림 + */ +data class HomeNotificationState( + val notificationId: Long, + val type: NotificationType, + val title: String, + val content: String, + val isRead: Boolean, + val createdAt: LocalDateTime, + val characterName: String?, + val profileImageUrl: String?, +) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt new file mode 100644 index 0000000..998f758 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt @@ -0,0 +1,13 @@ +package kr.co.call.impl.viewmodel.state + +import java.time.LocalDateTime + +/** + * 전화 예약 상태 + */ +data class HomeReservationState( + val reservationCount: Int = 0, + val profileImageUrl: String? = "", + val scheduledAt: LocalDateTime? = LocalDateTime.of(2026, 6, 30, 21, 0), + val firstName: String? = "민준", +) \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt new file mode 100644 index 0000000..4adb657 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt @@ -0,0 +1,18 @@ +package kr.co.call.impl.viewmodel.state + +import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.tab.HomeHistoryTab +import kr.co.call.impl.viewmodel.state.HomeReservationState +import kr.co.call.impl.viewmodel.state.HomeSummaryState + +data class HomeState( + val summary: HomeSummaryState = HomeSummaryState(), + val reservation: HomeReservationState = HomeReservationState(), + val callHistory: CallHistoryState = CallHistoryState(), + val hasUnreadNotification: Boolean = true, + val loadStatus: LoadStatus = LoadStatus.Idle, + val selectedHistoryTab: HomeHistoryTab = HomeHistoryTab.NOTIFICATION, +) { + val isLoading: Boolean + get() = loadStatus == LoadStatus.Loading +} \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt new file mode 100644 index 0000000..cd2b600 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt @@ -0,0 +1,9 @@ +package kr.co.call.impl.viewmodel.state + +data class HomeSummaryState( + val characterId: Long = 0L, + val firstName: String = "수현", + val relationshipDays: Int = 30, + val totalCallCount: Int = 24, + val callStreakDays: Int = 12, +) \ No newline at end of file From 1ba40135c90a651cfd711693b8526fb97efa433b Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:37:45 +0900 Subject: [PATCH 05/17] =?UTF-8?q?fix:=20=ED=99=88=20Repository=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/call/data/di/RepositoryModule.kt | 88 ++----------------- .../data/repositoryImpl/HomeRepositoryImpl.kt | 66 +++++++++++--- .../kr/co/call/domain/model/home/HomeModel.kt | 11 --- .../co/call/impl/viewmodel/HomeViewModel.kt | 30 +++---- 4 files changed, 69 insertions(+), 126 deletions(-) diff --git a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt index 08eed9f..3c2326b 100644 --- a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt @@ -1,98 +1,20 @@ package kr.co.call.data.di import dagger.Module -import dagger.Provides +import dagger.Binds import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton import kr.co.call.data.repositoryImpl.HomeRepositoryImpl import kr.co.call.domain.repository.HomeRepository -import kr.co.call.network.api.HomeApi -import kr.co.call.network.dto.CallHistoryDto -import kr.co.call.network.dto.CallHistoryListResponseDto -import kr.co.call.network.dto.CallHistoryPageDto -import kr.co.call.network.dto.HomeSummaryDto -import kr.co.call.network.dto.HomeSummaryResponseDto -import kr.co.call.network.dto.ReservationDto -import kr.co.call.network.dto.ReservationListDto -import kr.co.call.network.dto.ReservationListResponseDto @Module @InstallIn(SingletonComponent::class) -object RepositoryModule { +abstract class RepositoryModule { - @Provides + @Binds @Singleton - fun provideHomeRepository( + abstract fun bindHomeRepository( homeRepositoryImpl: HomeRepositoryImpl, - ): HomeRepository = homeRepositoryImpl - - // 실제 HomeApi 연결 전 사용하는 Mock 데이터 - @Provides - @Singleton - fun provideHomeApi(): HomeApi = - object : HomeApi { - override suspend fun getReservations(): ReservationListResponseDto = - ReservationListResponseDto( - isSuccess = true, - code = "COMMON200", - message = "요청에 성공하였습니다.", - result = ReservationListDto( - content = listOf( - ReservationDto( - callReservationId = 3L, - characterId = 2L, - firstName = "민준", - imageUrl = null, - scheduledAt = "2026-06-30T21:00:00", - status = "SCHEDULED", - ), - ), - count = 1, - ), - ) - - override suspend fun getCallHistories(size: Int): CallHistoryListResponseDto { - val histories = List(size.coerceAtMost(MAX_CALL_HISTORY_SIZE)) { index -> - CallHistoryDto( - callId = (index + 1).toLong(), - characterName = if (index % 2 == 0) "민준" else "동휘", - sender = if (index % 2 == 0) "USER" else "AI", - aiSummary = if (index % 2 == 0) { - "오늘 하루와 퇴근 후 일상 이야기" - } else { - "몸살 감기 기운과 걱정해주는 이야기" - }, - startedAt = "2026-06-${(28 - index).coerceAtLeast(1).toString().padStart(2, '0')}T23:02:00", - callType = if (index % 2 == 0) "DAILY" else "DEEP", - status = if (index % 4 == 3) "MISSED" else "COMPLETED", - ) - } - - return CallHistoryListResponseDto( - isSuccess = true, - code = "COMMON200", - message = "요청에 성공하였습니다.", - result = CallHistoryPageDto( - content = histories, - hasNext = false, - ), - ) - } - - override suspend fun getSummary(characterId: Long): HomeSummaryResponseDto = - HomeSummaryResponseDto( - isSuccess = true, - code = "COMMON200", - message = "요청에 성공하였습니다.", - result = HomeSummaryDto( - firstName = "수현", - relationshipDays = 30, - totalCallCount = 24, - callStreakDays = 12, - ), - ) - } - - private const val MAX_CALL_HISTORY_SIZE = 20 + ): HomeRepository } diff --git a/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt index 26d4d0f..fb8c4b6 100644 --- a/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt +++ b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt @@ -1,30 +1,68 @@ package kr.co.call.data.repositoryImpl +import java.time.LocalDateTime import javax.inject.Inject -import kr.co.call.data.mapper.toDomain import kr.co.call.domain.model.home.CallHistory +import kr.co.call.domain.model.home.CallSender +import kr.co.call.domain.model.home.CallStatus +import kr.co.call.domain.model.home.CallType import kr.co.call.domain.model.home.HomeModel import kr.co.call.domain.repository.HomeRepository -import kr.co.call.network.api.HomeApi -class HomeRepositoryImpl @Inject constructor( - private val homeApi: HomeApi, -) : HomeRepository { +class HomeRepositoryImpl @Inject constructor() : HomeRepository { override suspend fun getReservations(): Result = - runCatching { - homeApi.getReservations().result.toDomain() - } + Result.success( + HomeModel.Reservations( + count = 1, + items = listOf( + HomeModel.Reservation( + callReservationId = 3L, + characterId = 2L, + firstName = "민준", + imageUrl = null, + scheduledAt = LocalDateTime.of(2026, 6, 30, 21, 0), + status = HomeModel.ReservationStatus.SCHEDULED, + ), + ), + ), + ) override suspend fun getCallHistories(): Result> = - runCatching { - homeApi.getCallHistories(size = CALL_HISTORY_SIZE).result.content.map { it.toDomain() } - } + Result.success( + List(CALL_HISTORY_SIZE) { index -> + CallHistory( + callId = (index + 1).toLong(), + characterName = if (index % 2 == 0) "민준" else "동휘", + sender = if (index % 2 == 0) CallSender.USER else CallSender.AI, + aiSummary = if (index % 2 == 0) { + "오늘 하루와 퇴근 후 일상 이야기" + } else { + "몸살 감기 기운과 걱정해주는 이야기" + }, + startedAt = LocalDateTime.of( + 2026, + 6, + (28 - index).coerceAtLeast(1), + 23, + 2, + ), + callType = if (index % 2 == 0) CallType.DAILY else CallType.DEEP, + status = if (index % 4 == 3) CallStatus.MISSED else CallStatus.COMPLETED, + ) + }, + ) override suspend fun getSummary(characterId: Long): Result = - runCatching { - homeApi.getSummary(characterId).result.toDomain(characterId) - } + Result.success( + HomeModel.Summary( + characterId = characterId, + firstName = "수현", + relationshipDays = 30, + totalCallCount = 24, + callStreakDays = 12, + ), + ) private companion object { const val CALL_HISTORY_SIZE = 20 diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt index 94fa388..047beca 100644 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt @@ -32,15 +32,4 @@ data class HomeModel( enum class ReservationStatus { SCHEDULED, } - - data class CallHistory( - val callId: Long, - val characterName: String, - val aiSummary: String, - val startedAt: LocalDateTime, - val sender: CallSender, - val callType: CallType, - val status: CallStatus, - ) - } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt index debd21f..188f503 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt @@ -2,7 +2,7 @@ package kr.co.call.impl.viewmodel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kr.co.call.domain.model.home.HomeModel +import kotlinx.coroutines.CancellationException import kr.co.call.domain.repository.HomeRepository import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.mapper.toUiState @@ -25,6 +25,10 @@ class HomeViewModel @Inject constructor( initialState = HomeState(), ) + init { + loadHome() + } + internal fun handleIntent(intent: HomeIntent) { when (intent) { is HomeIntent.SelectHistoryTab -> { @@ -45,41 +49,31 @@ class HomeViewModel @Inject constructor( } } - - init { - loadHome() - } - private fun loadHome() = intent { reduce { state.copy(loadStatus = LoadStatus.Loading) } - runCatching { + try { val reservations = homeRepository.getReservations().getOrThrow() val callHistories = homeRepository.getCallHistories().getOrThrow() - val summary = homeRepository.getSummary(state.summary.characterId).getOrThrow() - HomeModel( - summary = summary, - reservations = reservations, - callHistories = callHistories, - ) - }.onSuccess { home -> reduce { state.copy( - summary = home.summary.toUiState(), - reservation = home.reservations.toUiState(), + summary = summary.toUiState(), + reservation = reservations.toUiState(), callHistory = CallHistoryState( - histories = home.callHistories.map { history -> + histories = callHistories.map { history -> history.toUiState() }, ), loadStatus = LoadStatus.Idle, ) } - }.onFailure { throwable -> + } catch (cancellationException: CancellationException) { + throw cancellationException + } catch (throwable: Throwable) { reduce { state.copy( loadStatus = LoadStatus.Error( From 5ce2bb13b145d1e4112fafae3a5debaed27136ef Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 03:08:58 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20=ED=99=88=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EA=B5=AC=EC=A1=B0=20=EC=9E=AC=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/call/data/mapper/CallHistoryMapper.kt | 45 ++++++---- .../data/repositoryImpl/HomeRepositoryImpl.kt | 29 +++---- .../co/call/domain/model/home/CallHistory.kt | 5 +- .../call/domain/model/home/CallReservation.kt | 16 ++++ .../domain/model/home/CallReservations.kt | 6 ++ .../co/call/domain/model/home/CallSender.kt | 6 -- .../co/call/domain/model/home/CallStatus.kt | 6 -- .../kr/co/call/domain/model/home/CallType.kt | 6 -- .../kr/co/call/domain/model/home/HomeModel.kt | 35 -------- .../co/call/domain/model/home/HomeSummary.kt | 9 ++ .../call/domain/repository/HomeRepository.kt | 7 +- .../call/impl/component/header/HomeHeader.kt | 20 ++--- .../component/header/HomeReservationCard.kt | 43 +++++----- .../header/RelationshipSummaryCard.kt | 23 ++--- .../impl/component/history/CallHistoryList.kt | 46 +++++----- .../component/history/HomeHistorySection.kt | 6 +- .../call/impl/mapper/CallHistoryUiMapper.kt | 28 ++---- .../kr/co/call/impl/mapper/HomeUiMapper.kt | 32 ++++--- .../java/kr/co/call/impl/mock/CallMockData.kt | 27 +++--- .../kr/co/call/impl/screen/HomeMockData.kt | 85 ++++++++----------- .../java/kr/co/call/impl/screen/HomeScreen.kt | 30 +++---- .../co/call/impl/viewmodel/CallHistoryType.kt | 12 --- .../co/call/impl/viewmodel/HomeViewModel.kt | 15 ++-- .../viewmodel/model/CallHistoryIconType.kt | 7 ++ .../viewmodel/model/CallHistoryUiModel.kt | 9 ++ .../viewmodel/model/HomeReservationUiModel.kt | 10 +++ .../viewmodel/model/HomeSummaryUiModel.kt | 8 ++ .../impl/viewmodel/state/CallHistoryState.kt | 8 -- .../state/HomeCallHistoryItemState.kt | 14 --- .../viewmodel/state/HomeReservationState.kt | 13 --- .../co/call/impl/viewmodel/state/HomeState.kt | 14 +-- .../impl/viewmodel/state/HomeSummaryState.kt | 9 -- 32 files changed, 281 insertions(+), 348 deletions(-) create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallReservations.kt delete mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt delete mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt delete mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt delete mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryIconType.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryUiModel.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeReservationUiModel.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeSummaryUiModel.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt diff --git a/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt b/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt index 8edf7dd..685cc77 100644 --- a/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt +++ b/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt @@ -3,10 +3,10 @@ package kr.co.call.data.mapper import java.time.LocalDateTime import kr.co.call.core.common.util.TimeUtil import kr.co.call.domain.model.home.CallHistory -import kr.co.call.domain.model.home.CallSender -import kr.co.call.domain.model.home.CallStatus -import kr.co.call.domain.model.home.CallType -import kr.co.call.domain.model.home.HomeModel +import kr.co.call.domain.model.home.CallReservation +import kr.co.call.domain.model.home.CallReservations +import kr.co.call.domain.model.home.HomeSummary +import kr.co.call.domain.model.home.ReservationStatus import kr.co.call.network.dto.CallHistoryDto import kr.co.call.network.dto.HomeSummaryDto import kr.co.call.network.dto.ReservationListDto @@ -18,32 +18,45 @@ internal fun CallHistoryDto.toDomain(): CallHistory { return CallHistory( callId = callId, characterName = characterName, - sender = CallSender.valueOf(sender), aiSummary = aiSummary, startedAt = parsedStartedAt, - callType = CallType.valueOf(callType), - status = CallStatus.valueOf(status), + isOutgoing = sender.toIsOutgoing(), + isMissed = status.toIsMissed(), ) } -internal fun ReservationListDto.toDomain(): HomeModel.Reservations = - HomeModel.Reservations( - count = count, +private fun String.toIsOutgoing(): Boolean = + when (this) { + "USER" -> true + "AI" -> false + else -> error("지원하지 않는 통화 발신자입니다: $this") + } + +private fun String.toIsMissed(): Boolean = + when (this) { + "COMPLETED" -> false + "MISSED" -> true + else -> error("지원하지 않는 통화 상태입니다: $this") + } + +internal fun ReservationListDto.toDomain(): CallReservations = + CallReservations( + totalCount = count, items = content.map { it.toDomain() }, ) -private fun ReservationDto.toDomain(): HomeModel.Reservation = - HomeModel.Reservation( - callReservationId = callReservationId, +private fun ReservationDto.toDomain(): CallReservation = + CallReservation( + id = callReservationId, characterId = characterId, firstName = firstName, imageUrl = imageUrl, scheduledAt = LocalDateTime.parse(scheduledAt), - status = HomeModel.ReservationStatus.valueOf(status), + status = ReservationStatus.valueOf(status), ) -internal fun HomeSummaryDto.toDomain(characterId: Long): HomeModel.Summary = - HomeModel.Summary( +internal fun HomeSummaryDto.toDomain(characterId: Long): HomeSummary = + HomeSummary( characterId = characterId, firstName = firstName, relationshipDays = relationshipDays, diff --git a/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt index fb8c4b6..fa571c9 100644 --- a/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt +++ b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt @@ -3,26 +3,26 @@ package kr.co.call.data.repositoryImpl import java.time.LocalDateTime import javax.inject.Inject import kr.co.call.domain.model.home.CallHistory -import kr.co.call.domain.model.home.CallSender -import kr.co.call.domain.model.home.CallStatus -import kr.co.call.domain.model.home.CallType -import kr.co.call.domain.model.home.HomeModel +import kr.co.call.domain.model.home.CallReservation +import kr.co.call.domain.model.home.CallReservations +import kr.co.call.domain.model.home.HomeSummary +import kr.co.call.domain.model.home.ReservationStatus import kr.co.call.domain.repository.HomeRepository class HomeRepositoryImpl @Inject constructor() : HomeRepository { - override suspend fun getReservations(): Result = + override suspend fun getReservations(): Result = Result.success( - HomeModel.Reservations( - count = 1, + CallReservations( + totalCount = 1, items = listOf( - HomeModel.Reservation( - callReservationId = 3L, + CallReservation( + id = 3L, characterId = 2L, firstName = "민준", imageUrl = null, scheduledAt = LocalDateTime.of(2026, 6, 30, 21, 0), - status = HomeModel.ReservationStatus.SCHEDULED, + status = ReservationStatus.SCHEDULED, ), ), ), @@ -34,7 +34,6 @@ class HomeRepositoryImpl @Inject constructor() : HomeRepository { CallHistory( callId = (index + 1).toLong(), characterName = if (index % 2 == 0) "민준" else "동휘", - sender = if (index % 2 == 0) CallSender.USER else CallSender.AI, aiSummary = if (index % 2 == 0) { "오늘 하루와 퇴근 후 일상 이야기" } else { @@ -47,15 +46,15 @@ class HomeRepositoryImpl @Inject constructor() : HomeRepository { 23, 2, ), - callType = if (index % 2 == 0) CallType.DAILY else CallType.DEEP, - status = if (index % 4 == 3) CallStatus.MISSED else CallStatus.COMPLETED, + isOutgoing = index % 2 == 0, + isMissed = index % 4 == 3, ) }, ) - override suspend fun getSummary(characterId: Long): Result = + override suspend fun getSummary(characterId: Long): Result = Result.success( - HomeModel.Summary( + HomeSummary( characterId = characterId, firstName = "수현", relationshipDays = 30, diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt index ca38e57..2fe3f0f 100644 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt @@ -7,7 +7,6 @@ data class CallHistory( val characterName: String, val aiSummary: String, val startedAt: LocalDateTime, - val sender: CallSender, - val callType: CallType, - val status: CallStatus, + val isOutgoing: Boolean, + val isMissed: Boolean, ) diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt new file mode 100644 index 0000000..a3a421c --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt @@ -0,0 +1,16 @@ +package kr.co.call.domain.model.home + +import java.time.LocalDateTime + +data class CallReservation( + val id: Long, + val characterId: Long, + val firstName: String, + val imageUrl: String?, + val scheduledAt: LocalDateTime, + val status: ReservationStatus, +) + +enum class ReservationStatus { + SCHEDULED, +} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservations.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservations.kt new file mode 100644 index 0000000..9b76a00 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservations.kt @@ -0,0 +1,6 @@ +package kr.co.call.domain.model.home + +data class CallReservations( + val totalCount: Int, + val items: List, +) diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt deleted file mode 100644 index 1abd335..0000000 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt +++ /dev/null @@ -1,6 +0,0 @@ -package kr.co.call.domain.model.home - -enum class CallSender { - USER, - AI, -} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt deleted file mode 100644 index a12755c..0000000 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt +++ /dev/null @@ -1,6 +0,0 @@ -package kr.co.call.domain.model.home - -enum class CallStatus { - COMPLETED, - MISSED, -} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt deleted file mode 100644 index 067ca27..0000000 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package kr.co.call.domain.model.home - -enum class CallType { - DAILY, - DEEP, -} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt deleted file mode 100644 index 047beca..0000000 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -package kr.co.call.domain.model.home - -import java.time.LocalDateTime - -data class HomeModel( - val summary: Summary, - val reservations: Reservations, - val callHistories: List, -) { - data class Summary( - val characterId: Long, - val firstName: String, - val relationshipDays: Int, - val totalCallCount: Int, - val callStreakDays: Int, - ) - - data class Reservations( - val count: Int, - val items: List, - ) - - data class Reservation( - val callReservationId: Long, - val characterId: Long, - val firstName: String, - val imageUrl: String?, - val scheduledAt: LocalDateTime, - val status: ReservationStatus, - ) - - enum class ReservationStatus { - SCHEDULED, - } -} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt new file mode 100644 index 0000000..2cd6c56 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt @@ -0,0 +1,9 @@ +package kr.co.call.domain.model.home + +data class HomeSummary( + val characterId: Long, + val firstName: String, + val relationshipDays: Int, + val totalCallCount: Int, + val callStreakDays: Int, +) diff --git a/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt b/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt index db4d04b..6e6f723 100644 --- a/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt +++ b/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt @@ -1,12 +1,13 @@ package kr.co.call.domain.repository import kr.co.call.domain.model.home.CallHistory -import kr.co.call.domain.model.home.HomeModel +import kr.co.call.domain.model.home.CallReservations +import kr.co.call.domain.model.home.HomeSummary interface HomeRepository { - suspend fun getReservations(): Result + suspend fun getReservations(): Result suspend fun getCallHistories(): Result> - suspend fun getSummary(characterId: Long): Result + suspend fun getSummary(characterId: Long): Result } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt index 8993e94..1dddbe6 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt @@ -38,7 +38,7 @@ import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.impl.component.RelationshipSummaryCard import kr.co.call.impl.component.UnreadIndicator -import kr.co.call.impl.viewmodel.state.HomeSummaryState +import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel /** * 홈 화면 상단 헤더 컴포넌트 @@ -47,7 +47,7 @@ import kr.co.call.impl.viewmodel.state.HomeSummaryState */ @Composable internal fun HomeHeader( - summaryState: HomeSummaryState, + summary: HomeSummaryUiModel, hasUnreadNotification: Boolean, modifier: Modifier = Modifier, ) { @@ -119,7 +119,7 @@ internal fun HomeHeader( tint = Color.Unspecified, ) - // 알림 읽음 여부에 따라 불러오기 + // 알림 읽음 여부 표시 if (hasUnreadNotification) { UnreadIndicator( modifier = Modifier @@ -143,7 +143,7 @@ internal fun HomeHeader( withStyle( style = SpanStyle(fontWeight = FontWeight.SemiBold), ) { - append(summaryState.firstName) + append(summary.firstName) } append("님, 반가워요! 👋🏻") }, @@ -174,6 +174,7 @@ internal fun HomeHeader( contentColor = CallTheme.colors.mainVariant1, ), ) { + // 전화하기 버튼 Icon( painter = painterResource( id = R.drawable.ic_home_call, @@ -186,7 +187,7 @@ internal fun HomeHeader( // 관계 요약 RelationshipSummaryCard( - summaryState = summaryState, + summary = summary, modifier = Modifier .align(Alignment.TopCenter) .offset(y = 217.dp) @@ -208,12 +209,11 @@ internal fun HomeHeader( private fun HomeHeaderPreview() { CallFromAiTheme { HomeHeader( - summaryState = HomeSummaryState( - characterId = 1L, + summary = HomeSummaryUiModel( firstName = "수현", - relationshipDays = 30, - totalCallCount = 24, - callStreakDays = 12, + relationshipDaysText = "30일째", + totalCallCountText = "24회", + callStreakDaysText = "12일", ), hasUnreadNotification = true, ) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt index f3e69aa..539ccd1 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt @@ -27,12 +27,11 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import java.time.LocalDate -import java.time.LocalDateTime import kr.co.call.core.common.util.TimeUtil import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.designsystem.component.ProfileImageWithIcon -import kr.co.call.impl.viewmodel.state.HomeReservationState +import kr.co.call.impl.viewmodel.model.HomeReservationUiModel /** * 홈 화면의 통화 약속 컴포넌트 @@ -41,7 +40,7 @@ import kr.co.call.impl.viewmodel.state.HomeReservationState */ @Composable internal fun HomeReservationCard( - reservationState: HomeReservationState, + reservation: HomeReservationUiModel, modifier: Modifier = Modifier, onTimeChangeClick: () -> Unit = {}, ) { @@ -53,7 +52,7 @@ internal fun HomeReservationCard( ) { // 날짜 항상 표시 ReservationDate( - reservationCount = reservationState.reservationCount, + reservationCountText = reservation.reservationCountText, modifier = Modifier .align(Alignment.TopStart) .padding( @@ -63,9 +62,9 @@ internal fun HomeReservationCard( ) // 예약이 있을 때만 카드 상세 표시 - if (reservationState.reservationCount > 0) { + if (reservation.hasReservation) { ReservationContent( - reservationState = reservationState, + reservation = reservation, modifier = Modifier .align(Alignment.TopStart) .padding( @@ -74,9 +73,9 @@ internal fun HomeReservationCard( ), ) - reservationState.scheduledAt?.let { scheduledAt -> + reservation.scheduledTimeText?.let { scheduledTimeText -> ReservationTimeActions( - scheduledAt = scheduledAt, + scheduledTimeText = scheduledTimeText, onTimeChangeClick = onTimeChangeClick, modifier = Modifier .align(Alignment.TopEnd) @@ -96,7 +95,7 @@ internal fun HomeReservationCard( */ @Composable private fun ReservationDate( - reservationCount: Int, + reservationCountText: String, modifier: Modifier = Modifier, ) { Row( @@ -120,7 +119,7 @@ private fun ReservationDate( Spacer(modifier = Modifier.width(9.dp)) Text( - text = "약속 ${reservationCount}건", + text = reservationCountText, color = CallTheme.colors.mainVariant1, style = CallTheme.typography.bodySmallBold, ) @@ -134,7 +133,7 @@ private fun ReservationDate( */ @Composable private fun ReservationContent( - reservationState: HomeReservationState, + reservation: HomeReservationUiModel, modifier: Modifier = Modifier, ) { Row( @@ -142,22 +141,22 @@ private fun ReservationContent( verticalAlignment = Alignment.CenterVertically, ) { ProfileImageWithIcon( - profileImageUrl = reservationState.profileImageUrl, + profileImageUrl = reservation.profileImageUrl, showCallBadge = true, ) Spacer(modifier = Modifier.width(16.dp)) - val firstName = reservationState.firstName - val scheduledAt = reservationState.scheduledAt + val firstName = reservation.firstName + val scheduledAtText = reservation.scheduledAtText - if (firstName != null && scheduledAt != null) { + if (firstName != null && scheduledAtText != null) { Text( text = buildAnnotatedString { withStyle(CallTheme.typography.bodyMediumMedium.toSpanStyle()) { append(firstName) } - append("님과 ${TimeUtil.toReservationTimeText(scheduledAt)}\n") + append("님과 $scheduledAtText\n") append("전화가 예정되어 있어요") }, color = CallTheme.colors.black, @@ -174,7 +173,7 @@ private fun ReservationContent( */ @Composable private fun ReservationTimeActions( - scheduledAt: LocalDateTime, + scheduledTimeText: String, onTimeChangeClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -193,7 +192,7 @@ private fun ReservationTimeActions( contentAlignment = Alignment.Center, ) { Text( - text = TimeUtil.toHourMinuteText(scheduledAt), + text = scheduledTimeText, color = CallTheme.colors.mainVariant1, style = CallTheme.typography.bodySmallBold, ) @@ -236,11 +235,13 @@ private fun ReservationTimeActions( private fun HomeReservationCardPreview() { CallFromAiTheme { HomeReservationCard( - reservationState = HomeReservationState( - reservationCount = 1, + reservation = HomeReservationUiModel( + hasReservation = true, + reservationCountText = "약속 1건", profileImageUrl = null, - scheduledAt = LocalDateTime.of(2026, 7, 17, 21, 0), firstName = "민준", + scheduledAtText = "오늘 오후 9시", + scheduledTimeText = "21:00", ), ) } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt index bce5a90..2f460b3 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme -import kr.co.call.impl.viewmodel.state.HomeSummaryState +import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel // 관계 요약 /** @@ -27,7 +27,7 @@ import kr.co.call.impl.viewmodel.state.HomeSummaryState */ @Composable internal fun RelationshipSummaryCard( - summaryState: HomeSummaryState, + summary: HomeSummaryUiModel, modifier: Modifier = Modifier, ) { val cardShape = RoundedCornerShape(20.dp) @@ -48,7 +48,7 @@ internal fun RelationshipSummaryCard( ) { RelationshipSummaryItem( label = "관계", - value = summaryState.relationshipDays.withSuffixOrDash("일째"), + value = summary.relationshipDaysText, modifier = Modifier.weight(1f), ) @@ -56,7 +56,7 @@ internal fun RelationshipSummaryCard( RelationshipSummaryItem( label = "통화", - value = summaryState.totalCallCount.withSuffixOrDash("회"), + value = summary.totalCallCountText, modifier = Modifier.weight(1f), ) @@ -64,7 +64,7 @@ internal fun RelationshipSummaryCard( RelationshipSummaryItem( label = "연속 통화", - value = summaryState.callStreakDays.withSuffixOrDash("일"), + value = summary.callStreakDaysText, modifier = Modifier.weight(1f), ) } @@ -111,10 +111,6 @@ private fun RelationshipSummaryDivider() { ) } -// 0으로 들어올 경우 마이너스로 표시하는 확장함수 -private fun Int.withSuffixOrDash(suffix: String): String = - if (this == 0) "-" else "$this$suffix" - /** * 관계 요약 카드 Preview * - 관계와 통화 샘플 데이터 표시 @@ -124,11 +120,10 @@ private fun Int.withSuffixOrDash(suffix: String): String = private fun RelationshipSummaryCardPreview() { CallFromAiTheme { RelationshipSummaryCard( - summaryState = HomeSummaryState( - characterId = 1L, - relationshipDays = 30, - totalCallCount = 24, - callStreakDays = 12, + summary = HomeSummaryUiModel( + relationshipDaysText = "30일째", + totalCallCountText = "24회", + callStreakDaysText = "12일", ), modifier = Modifier.height(94.dp), ) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt index 6c1f6c7..be5f2a0 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt @@ -26,10 +26,8 @@ import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.impl.mock.CallMockData -import kr.co.call.impl.viewmodel.CallHistoryType -import kr.co.call.impl.viewmodel.isMissed -import kr.co.call.impl.viewmodel.state.CallHistoryState -import kr.co.call.impl.viewmodel.state.HomeCallHistoryItemState +import kr.co.call.impl.viewmodel.model.CallHistoryIconType +import kr.co.call.impl.viewmodel.model.CallHistoryUiModel /** * 통화 기록 목록을 표시하는 컴포넌트 @@ -38,7 +36,7 @@ import kr.co.call.impl.viewmodel.state.HomeCallHistoryItemState */ @Composable internal fun CallHistoryList( - callHistoryState: CallHistoryState, + histories: List, modifier: Modifier = Modifier, onRecordClick: (Long) -> Unit = {}, ) { @@ -60,7 +58,7 @@ internal fun CallHistoryList( .weight(1f), ) { items( - items = callHistoryState.histories, + items = histories, key = { history -> history.callId }, ) { history -> CallHistoryCard( @@ -79,19 +77,20 @@ internal fun CallHistoryList( */ @Composable private fun CallHistoryCard( - callHistory: HomeCallHistoryItemState, + callHistory: CallHistoryUiModel, modifier: Modifier = Modifier, onRecordClick: () -> Unit = {}, ) { - val isMissed = callHistory.type.isMissed + // Missed type + val isMissed = callHistory.iconType == CallHistoryIconType.MISSED Box( modifier = modifier .fillMaxWidth() .height(if (isMissed) 70.dp else 90.dp), ) { - CallTypeIcon( - type = callHistory.type, + CallHistoryIcon( + iconType = callHistory.iconType, modifier = Modifier .align(Alignment.TopStart) .padding( @@ -160,27 +159,24 @@ private fun CallHistoryCard( // 아이콘 타입에 따라서 표시 @Composable -private fun CallTypeIcon( - type: CallHistoryType, +private fun CallHistoryIcon( + iconType: CallHistoryIconType, modifier: Modifier = Modifier, ) { - val iconResource = when (type) { - CallHistoryType.SEND -> + val iconResource = when (iconType) { + CallHistoryIconType.SENT -> kr.co.call.designsystem.R.drawable.img_home_call_send - CallHistoryType.RECEIVED -> + CallHistoryIconType.RECEIVED -> kr.co.call.designsystem.R.drawable.img_home_call_received - CallHistoryType.SEND_MISSED, - CallHistoryType.RECEIVE_MISSED, - -> kr.co.call.designsystem.R.drawable.img_home_call_missed + CallHistoryIconType.MISSED -> + kr.co.call.designsystem.R.drawable.img_home_call_missed } - val iconSize = when (type) { - CallHistoryType.SEND -> DpSize(26.dp, 23.dp) - CallHistoryType.RECEIVED -> DpSize(23.dp, 23.dp) - CallHistoryType.SEND_MISSED, - CallHistoryType.RECEIVE_MISSED, - -> DpSize(25.dp, 26.dp) + val iconSize = when (iconType) { + CallHistoryIconType.SENT -> DpSize(26.dp, 23.dp) + CallHistoryIconType.RECEIVED -> DpSize(23.dp, 23.dp) + CallHistoryIconType.MISSED -> DpSize(25.dp, 26.dp) } Image( @@ -195,7 +191,7 @@ private fun CallTypeIcon( private fun CallHistoryListPreview() { CallFromAiTheme { CallHistoryList( - callHistoryState = CallMockData.state, + histories = CallMockData.uiModels, ) } } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt index d099b51..7fdf03a 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt @@ -10,7 +10,7 @@ import androidx.paging.PagingData import kotlinx.coroutines.flow.Flow import kr.co.call.impl.tab.HomeHistoryTab import kr.co.call.impl.tab.HomeHistoryTabBar -import kr.co.call.impl.viewmodel.state.CallHistoryState +import kr.co.call.impl.viewmodel.model.CallHistoryUiModel import kr.co.call.impl.viewmodel.state.HomeNotificationState @Composable @@ -18,7 +18,7 @@ internal fun HomeHistorySection( selectedTab: HomeHistoryTab, onTabClick: (HomeHistoryTab) -> Unit, notifications: Flow>, - callHistoryState: CallHistoryState, + callHistories: List, modifier: Modifier = Modifier, ) { Spacer(modifier = Modifier.height(22.dp)) @@ -35,7 +35,7 @@ internal fun HomeHistorySection( } HomeHistoryTab.CALL_HISTORY -> { - CallHistoryList(callHistoryState = callHistoryState) + CallHistoryList(histories = callHistories) } } } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt index 750d344..50b60f8 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt @@ -2,31 +2,21 @@ package kr.co.call.impl.mapper import kr.co.call.core.common.util.TimeUtil import kr.co.call.domain.model.home.CallHistory -import kr.co.call.domain.model.home.CallSender -import kr.co.call.domain.model.home.CallStatus -import kr.co.call.impl.viewmodel.CallHistoryType -import kr.co.call.impl.viewmodel.state.HomeCallHistoryItemState +import kr.co.call.impl.viewmodel.model.CallHistoryIconType +import kr.co.call.impl.viewmodel.model.CallHistoryUiModel -internal fun CallHistory.toUiState(): HomeCallHistoryItemState = - HomeCallHistoryItemState( +internal fun CallHistory.toUiModel(): CallHistoryUiModel = + CallHistoryUiModel( callId = callId, characterName = characterName, aiSummary = aiSummary, startedAtText = TimeUtil.toCallHistoryDateText(startedAt), - type = toCallHistoryType(), + iconType = toIconType(), ) -private fun CallHistory.toCallHistoryType(): CallHistoryType = +private fun CallHistory.toIconType(): CallHistoryIconType = when { - sender == CallSender.USER && status == CallStatus.COMPLETED -> - CallHistoryType.SEND - - sender == CallSender.AI && status == CallStatus.COMPLETED -> - CallHistoryType.RECEIVED - - sender == CallSender.USER && status == CallStatus.MISSED -> - CallHistoryType.SEND_MISSED - - else -> - CallHistoryType.RECEIVE_MISSED + isMissed -> CallHistoryIconType.MISSED + isOutgoing -> CallHistoryIconType.SENT + else -> CallHistoryIconType.RECEIVED } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt index abe8cbb..3848c2a 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt @@ -1,25 +1,31 @@ package kr.co.call.impl.mapper -import kr.co.call.domain.model.home.HomeModel -import kr.co.call.impl.viewmodel.state.HomeReservationState -import kr.co.call.impl.viewmodel.state.HomeSummaryState +import kr.co.call.core.common.util.TimeUtil +import kr.co.call.domain.model.home.CallReservations +import kr.co.call.domain.model.home.HomeSummary +import kr.co.call.impl.viewmodel.model.HomeReservationUiModel +import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel -internal fun HomeModel.Summary.toUiState(): HomeSummaryState = - HomeSummaryState( - characterId = characterId, +internal fun HomeSummary.toUiModel(): HomeSummaryUiModel = + HomeSummaryUiModel( firstName = firstName, - relationshipDays = relationshipDays, - totalCallCount = totalCallCount, - callStreakDays = callStreakDays, + relationshipDaysText = relationshipDays.withSuffixOrDash("일째"), + totalCallCountText = totalCallCount.withSuffixOrDash("회"), + callStreakDaysText = callStreakDays.withSuffixOrDash("일"), ) -internal fun HomeModel.Reservations.toUiState(): HomeReservationState { +internal fun CallReservations.toUiModel(): HomeReservationUiModel { val reservation = items.firstOrNull() - return HomeReservationState( - reservationCount = count, + return HomeReservationUiModel( + hasReservation = reservation != null, + reservationCountText = "약속 ${totalCount}건", profileImageUrl = reservation?.imageUrl, - scheduledAt = reservation?.scheduledAt, firstName = reservation?.firstName, + scheduledAtText = reservation?.scheduledAt?.let(TimeUtil::toReservationTimeText), + scheduledTimeText = reservation?.scheduledAt?.let(TimeUtil::toHourMinuteText), ) } + +private fun Int.withSuffixOrDash(suffix: String): String = + if (this == 0) "-" else "$this$suffix" diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt index 3e969c1..60b46fc 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt @@ -2,11 +2,8 @@ package kr.co.call.impl.mock import java.time.LocalDateTime import kr.co.call.domain.model.home.CallHistory -import kr.co.call.domain.model.home.CallSender -import kr.co.call.domain.model.home.CallStatus -import kr.co.call.domain.model.home.CallType -import kr.co.call.impl.mapper.toUiState -import kr.co.call.impl.viewmodel.state.CallHistoryState +import kr.co.call.impl.mapper.toUiModel +import kr.co.call.impl.viewmodel.model.CallHistoryUiModel internal object CallMockData { val histories: List = listOf( @@ -15,31 +12,27 @@ internal object CallMockData { characterName = "민준", aiSummary = "오늘 하루와 퇴근 후 일상 이야기", startedAt = LocalDateTime.of(2026, 6, 28, 23, 2), - sender = CallSender.USER, - callType = CallType.DAILY, - status = CallStatus.COMPLETED, + isOutgoing = true, + isMissed = false, ), CallHistory( callId = 2L, characterName = "민준", aiSummary = "출근 준비와 아침 일정 이야기", startedAt = LocalDateTime.of(2026, 6, 27, 7, 31), - sender = CallSender.AI, - callType = CallType.DAILY, - status = CallStatus.COMPLETED, + isOutgoing = false, + isMissed = false, ), CallHistory( callId = 3L, characterName = "민준", aiSummary = "", startedAt = LocalDateTime.of(2026, 6, 26, 21, 29), - sender = CallSender.AI, - callType = CallType.DEEP, - status = CallStatus.MISSED, + isOutgoing = false, + isMissed = true, ), ) - val state: CallHistoryState = CallHistoryState( - histories = histories.map { callHistory -> callHistory.toUiState() }, - ) + val uiModels: List = + histories.map { callHistory -> callHistory.toUiModel() } } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt index f409a6c..7fe836a 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt @@ -4,48 +4,37 @@ import androidx.paging.PagingData import java.time.LocalDateTime import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import kr.co.call.domain.model.home.HomeModel -import kr.co.call.impl.mapper.toUiState +import kr.co.call.domain.model.home.CallHistory +import kr.co.call.domain.model.home.CallReservation +import kr.co.call.domain.model.home.CallReservations +import kr.co.call.domain.model.home.HomeSummary +import kr.co.call.domain.model.home.ReservationStatus +import kr.co.call.impl.mapper.toUiModel import kr.co.call.impl.mock.CallMockData import kr.co.call.impl.viewmodel.NotificationType -import kr.co.call.impl.viewmodel.state.CallHistoryState import kr.co.call.impl.viewmodel.state.HomeNotificationState -import kr.co.call.impl.viewmodel.state.HomeReservationState -import kr.co.call.impl.viewmodel.state.HomeSummaryState +import kr.co.call.impl.viewmodel.model.CallHistoryUiModel +import kr.co.call.impl.viewmodel.model.HomeReservationUiModel +import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel /** * 홈화면에서 사용하는 Mock Data */ internal data class HomeMockData( - val home: HomeModel, + val summary: HomeSummary, + val reservations: CallReservations, + val callHistories: List, val hasUnreadNotification: Boolean, val notifications: List, ) { - val summaryState: HomeSummaryState - get() = HomeSummaryState( - characterId = home.summary.characterId, - firstName = home.summary.firstName, - relationshipDays = home.summary.relationshipDays, - totalCallCount = home.summary.totalCallCount, - callStreakDays = home.summary.callStreakDays, - ) + val summaryUiModel: HomeSummaryUiModel + get() = summary.toUiModel() - val reservationState: HomeReservationState - get() { - val reservation = home.reservations.items.firstOrNull() + val reservationUiModel: HomeReservationUiModel + get() = reservations.toUiModel() - return HomeReservationState( - reservationCount = home.reservations.count, - profileImageUrl = reservation?.imageUrl, - scheduledAt = reservation?.scheduledAt, - firstName = reservation?.firstName, - ) - } - - val callHistoryState: CallHistoryState - get() = CallHistoryState( - histories = home.callHistories.map { callHistory -> callHistory.toUiState() }, - ) + val callHistoryUiModels: List + get() = callHistories.map { callHistory -> callHistory.toUiModel() } fun notificationFlow(): Flow> = flowOf(PagingData.from(notifications)) @@ -55,29 +44,27 @@ internal fun createHomeMockData( now: LocalDateTime = LocalDateTime.now(), ): HomeMockData = HomeMockData( - home = HomeModel( - summary = HomeModel.Summary( - characterId = 2L, - firstName = "수현", - relationshipDays = 30, - totalCallCount = 24, - callStreakDays = 12, - ), - reservations = HomeModel.Reservations( - count = 2, - items = listOf( - HomeModel.Reservation( - callReservationId = 3L, - characterId = 2L, - firstName = "민준", - imageUrl = null, - scheduledAt = now.withHour(21).withMinute(0).withSecond(0), - status = HomeModel.ReservationStatus.SCHEDULED, - ), + summary = HomeSummary( + characterId = 2L, + firstName = "수현", + relationshipDays = 30, + totalCallCount = 24, + callStreakDays = 12, + ), + reservations = CallReservations( + totalCount = 2, + items = listOf( + CallReservation( + id = 3L, + characterId = 2L, + firstName = "민준", + imageUrl = null, + scheduledAt = now.withHour(21).withMinute(0).withSecond(0), + status = ReservationStatus.SCHEDULED, ), ), - callHistories = CallMockData.histories, ), + callHistories = CallMockData.histories, hasUnreadNotification = true, notifications = listOf( HomeNotificationState( diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt index 772bcd0..493abfa 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt @@ -23,12 +23,12 @@ import kr.co.call.impl.component.header.HomeHeader import kr.co.call.impl.component.history.HomeHistorySection import kr.co.call.impl.component.header.HomeReservationCard import kr.co.call.impl.tab.HomeHistoryTab -import kr.co.call.impl.viewmodel.state.CallHistoryState -import kr.co.call.impl.viewmodel.state.HomeSummaryState import kr.co.call.impl.viewmodel.HomeIntent import kr.co.call.impl.viewmodel.state.HomeNotificationState -import kr.co.call.impl.viewmodel.state.HomeReservationState import kr.co.call.impl.viewmodel.HomeViewModel +import kr.co.call.impl.viewmodel.model.CallHistoryUiModel +import kr.co.call.impl.viewmodel.model.HomeReservationUiModel +import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel import androidx.paging.PagingData import org.orbitmvi.orbit.compose.collectAsState @@ -46,10 +46,10 @@ fun HomeScreen( val notifications = remember { createHomeMockData().notificationFlow() } HomeScreenContent( - homeSummaryState = state.summary, + homeSummary = state.summary, hasUnreadNotification = state.hasUnreadNotification, - homeReservationState = state.reservation, - callHistoryState = state.callHistory, + homeReservation = state.reservation, + callHistories = state.callHistories, selectedHistoryTab = state.selectedHistoryTab, notifications = notifications, onHistoryTabClick = { tab -> @@ -66,10 +66,10 @@ fun HomeScreen( */ @Composable internal fun HomeScreenContent( - homeSummaryState: HomeSummaryState, + homeSummary: HomeSummaryUiModel, hasUnreadNotification: Boolean, - homeReservationState: HomeReservationState, - callHistoryState: CallHistoryState, + homeReservation: HomeReservationUiModel, + callHistories: List, selectedHistoryTab: HomeHistoryTab, notifications: Flow>, onHistoryTabClick: (HomeHistoryTab) -> Unit, @@ -81,12 +81,12 @@ internal fun HomeScreenContent( .background(CallTheme.colors.background), ) { HomeHeader( - summaryState = homeSummaryState, + summary = homeSummary, hasUnreadNotification = hasUnreadNotification, ) // 예약된 전화 조회 - HomeReservationCard(reservationState = homeReservationState) + HomeReservationCard(reservation = homeReservation) Box( modifier = Modifier @@ -100,7 +100,7 @@ internal fun HomeScreenContent( selectedTab = selectedHistoryTab, onTabClick = onHistoryTabClick, notifications = notifications, - callHistoryState = callHistoryState, + callHistories = callHistories, ) } } @@ -122,10 +122,10 @@ private fun HomeScreenContentPreview() { CallFromAiTheme { HomeScreenContent( - homeSummaryState = mockData.summaryState, + homeSummary = mockData.summaryUiModel, hasUnreadNotification = mockData.hasUnreadNotification, - homeReservationState = mockData.reservationState, - callHistoryState = mockData.callHistoryState, + homeReservation = mockData.reservationUiModel, + callHistories = mockData.callHistoryUiModels, selectedHistoryTab = selectedHistoryTab, notifications = mockData.notificationFlow(), onHistoryTabClick = { selectedHistoryTab = it }, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt deleted file mode 100644 index bbd3dc3..0000000 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt +++ /dev/null @@ -1,12 +0,0 @@ -package kr.co.call.impl.viewmodel - -enum class CallHistoryType { - SEND, - RECEIVED, - SEND_MISSED, - RECEIVE_MISSED, -} - -internal val CallHistoryType.isMissed: Boolean - get() = this == CallHistoryType.SEND_MISSED || - this == CallHistoryType.RECEIVE_MISSED diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt index 188f503..3ea9d0c 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt @@ -5,9 +5,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kr.co.call.domain.repository.HomeRepository import kr.co.call.domain.util.LoadStatus -import kr.co.call.impl.mapper.toUiState +import kr.co.call.impl.mapper.toUiModel import kr.co.call.impl.tab.HomeHistoryTab -import kr.co.call.impl.viewmodel.state.CallHistoryState import kr.co.call.impl.viewmodel.state.HomeState import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -57,17 +56,13 @@ class HomeViewModel @Inject constructor( try { val reservations = homeRepository.getReservations().getOrThrow() val callHistories = homeRepository.getCallHistories().getOrThrow() - val summary = homeRepository.getSummary(state.summary.characterId).getOrThrow() + val summary = homeRepository.getSummary(state.characterId).getOrThrow() reduce { state.copy( - summary = summary.toUiState(), - reservation = reservations.toUiState(), - callHistory = CallHistoryState( - histories = callHistories.map { history -> - history.toUiState() - }, - ), + summary = summary.toUiModel(), + reservation = reservations.toUiModel(), + callHistories = callHistories.map { history -> history.toUiModel() }, loadStatus = LoadStatus.Idle, ) } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryIconType.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryIconType.kt new file mode 100644 index 0000000..ff43b39 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryIconType.kt @@ -0,0 +1,7 @@ +package kr.co.call.impl.viewmodel.model + +enum class CallHistoryIconType { + SENT, + RECEIVED, + MISSED, +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryUiModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryUiModel.kt new file mode 100644 index 0000000..1973186 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryUiModel.kt @@ -0,0 +1,9 @@ +package kr.co.call.impl.viewmodel.model + +data class CallHistoryUiModel( + val callId: Long, + val characterName: String = "", + val aiSummary: String = "", + val startedAtText: String = "", + val iconType: CallHistoryIconType, +) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeReservationUiModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeReservationUiModel.kt new file mode 100644 index 0000000..74f6eea --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeReservationUiModel.kt @@ -0,0 +1,10 @@ +package kr.co.call.impl.viewmodel.model + +data class HomeReservationUiModel( + val hasReservation: Boolean = false, + val reservationCountText: String = "약속 0건", + val profileImageUrl: String? = null, + val firstName: String? = null, + val scheduledAtText: String? = null, + val scheduledTimeText: String? = null, +) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeSummaryUiModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeSummaryUiModel.kt new file mode 100644 index 0000000..6de3715 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeSummaryUiModel.kt @@ -0,0 +1,8 @@ +package kr.co.call.impl.viewmodel.model + +data class HomeSummaryUiModel( + val firstName: String = "", + val relationshipDaysText: String = "-", + val totalCallCountText: String = "-", + val callStreakDaysText: String = "-", +) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt deleted file mode 100644 index 439c875..0000000 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package kr.co.call.impl.viewmodel.state - -/** - * 통화 기록 List - */ -data class CallHistoryState( - val histories: List = emptyList(), -) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt deleted file mode 100644 index 600a7ec..0000000 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package kr.co.call.impl.viewmodel.state - -import kr.co.call.impl.viewmodel.CallHistoryType - -/** - * 통화 기록 조회 개별 아이템 - */ -data class HomeCallHistoryItemState( - val callId: Long, - val characterName: String, - val aiSummary: String, - val startedAtText: String, - val type: CallHistoryType, -) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt deleted file mode 100644 index 998f758..0000000 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package kr.co.call.impl.viewmodel.state - -import java.time.LocalDateTime - -/** - * 전화 예약 상태 - */ -data class HomeReservationState( - val reservationCount: Int = 0, - val profileImageUrl: String? = "", - val scheduledAt: LocalDateTime? = LocalDateTime.of(2026, 6, 30, 21, 0), - val firstName: String? = "민준", -) \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt index 4adb657..43a62f6 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt @@ -2,17 +2,19 @@ package kr.co.call.impl.viewmodel.state import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.tab.HomeHistoryTab -import kr.co.call.impl.viewmodel.state.HomeReservationState -import kr.co.call.impl.viewmodel.state.HomeSummaryState +import kr.co.call.impl.viewmodel.model.CallHistoryUiModel +import kr.co.call.impl.viewmodel.model.HomeReservationUiModel +import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel data class HomeState( - val summary: HomeSummaryState = HomeSummaryState(), - val reservation: HomeReservationState = HomeReservationState(), - val callHistory: CallHistoryState = CallHistoryState(), + val characterId: Long = 0L, + val summary: HomeSummaryUiModel = HomeSummaryUiModel(), + val reservation: HomeReservationUiModel = HomeReservationUiModel(), + val callHistories: List = emptyList(), val hasUnreadNotification: Boolean = true, val loadStatus: LoadStatus = LoadStatus.Idle, val selectedHistoryTab: HomeHistoryTab = HomeHistoryTab.NOTIFICATION, ) { val isLoading: Boolean get() = loadStatus == LoadStatus.Loading -} \ No newline at end of file +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt deleted file mode 100644 index cd2b600..0000000 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package kr.co.call.impl.viewmodel.state - -data class HomeSummaryState( - val characterId: Long = 0L, - val firstName: String = "수현", - val relationshipDays: Int = 30, - val totalCallCount: Int = 24, - val callStreakDays: Int = 12, -) \ No newline at end of file From 22aa4d05bf3de66f204b7531db1ecf02a7a6d03f Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 05:43:12 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=ED=99=88=20=EA=B0=9C=EC=9A=94=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EA=B3=BC=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=98=B5=EC=85=98=20UI=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=B6=94=EA=B0=80=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/domain/model/home/HomeOverview.kt | 10 ++++++++++ .../java/kr/co/call/impl/mock/CharacterMockData.kt | 10 ++++++++++ .../impl/viewmodel/model/CharacterOptionUiModel.kt | 9 +++++++++ 3 files changed, 29 insertions(+) create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/home/HomeOverview.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CharacterOptionUiModel.kt diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeOverview.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeOverview.kt new file mode 100644 index 0000000..b49f33f --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeOverview.kt @@ -0,0 +1,10 @@ +package kr.co.call.domain.model.home + +/** + * home에서 조회할 데이터 + */ +data class HomeOverview( + val summary: HomeSummary, + val reservations: CallReservations, + val callHistories: List, +) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt new file mode 100644 index 0000000..0a0cce3 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt @@ -0,0 +1,10 @@ +package kr.co.call.impl.mock + +import kr.co.call.impl.viewmodel.model.CharacterOptionUiModel + +internal object CharacterMockData { + val uiModels: List = listOf( + CharacterOptionUiModel(characterId = 1L, name = "민준", day = 30, isSelected = true), + CharacterOptionUiModel(characterId = 2L, name = "동휘", day = 12, isSelected = false), + ) +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CharacterOptionUiModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CharacterOptionUiModel.kt new file mode 100644 index 0000000..4394ecd --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CharacterOptionUiModel.kt @@ -0,0 +1,9 @@ +package kr.co.call.impl.viewmodel.model + +data class CharacterOptionUiModel( + val characterId: Long, + val name: String, + val day: Int, + val imageUrl: String? = null, + val isSelected: Boolean = false, +) From fde5d7ea54ee2319c1170e8d6e33cde45c15102e Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 05:43:18 +0900 Subject: [PATCH 08/17] =?UTF-8?q?refactor:=20ProfileImageWithIcon=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=EC=9D=84=20ProfileImage=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/{ProfileImageWithIcon.kt => ProfileImage.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/designsystem/src/main/java/kr/co/call/designsystem/component/{ProfileImageWithIcon.kt => ProfileImage.kt} (100%) diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt similarity index 100% rename from core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt rename to core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt From 82794d24d5a5b3818d3b3ee03aebd2b6ae1af10b Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:03:05 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=ED=97=A4=EB=8D=94=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=EC=97=86=EC=9D=B4=20=EC=A0=84=EC=B2=B4=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/component/CharacterChangeDialog.kt | 11 +++ .../component/header/HomeReservationCard.kt | 45 +++++----- .../impl/component/history/CallHistoryList.kt | 24 ++--- .../component/history/HomeHistorySection.kt | 16 ---- .../component/history/NotificationList.kt | 87 +++++++++---------- .../java/kr/co/call/impl/screen/HomeScreen.kt | 73 +++++++++++----- 6 files changed, 131 insertions(+), 125 deletions(-) create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.kt diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.kt new file mode 100644 index 0000000..44cf327 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.kt @@ -0,0 +1,11 @@ +package kr.co.call.impl.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun CharacterChangeDialog( + modifier: Modifier = Modifier, +){ + +} \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt index 539ccd1..f0d7914 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt @@ -198,31 +198,30 @@ private fun ReservationTimeActions( ) } - CompositionLocalProvider( - LocalMinimumInteractiveComponentSize provides 0.dp, + // 시간 변경 버튼 + Button( + onClick = onTimeChangeClick, + modifier = Modifier + .fillMaxWidth() + .height(27.dp), + shape = RoundedCornerShape(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = CallTheme.colors.white, + contentColor = CallTheme.colors.gray900, + ), + border = BorderStroke( + width = 1.dp, + color = CallTheme.colors.gray100, + ), + contentPadding = PaddingValues(0.dp), ) { - Button( - onClick = onTimeChangeClick, - modifier = Modifier - .fillMaxWidth() - .height(27.dp), - shape = RoundedCornerShape(50.dp), - colors = ButtonDefaults.buttonColors( - containerColor = CallTheme.colors.white, - contentColor = CallTheme.colors.gray900, - ), - border = BorderStroke( - width = 1.dp, - color = CallTheme.colors.gray100, - ), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = "시간 변경", - style = CallTheme.typography.bodySmallBold, - ) - } + Text( + text = "시간 변경", + style = CallTheme.typography.bodySmallBold, + ) } + + } } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt index be5f2a0..2c59007 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt @@ -5,14 +5,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,7 +37,7 @@ internal fun CallHistoryList( modifier: Modifier = Modifier, onRecordClick: (Long) -> Unit = {}, ) { - Column(modifier = modifier.fillMaxSize()) { + Column(modifier = modifier.fillMaxWidth()) { Text( text = "최근 20건의 기록만 표시됩니다.", modifier = Modifier.padding( @@ -52,20 +49,11 @@ internal fun CallHistoryList( style = CallTheme.typography.bodySmall, ) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - ) { - items( - items = histories, - key = { history -> history.callId }, - ) { history -> - CallHistoryCard( - callHistory = history, - onRecordClick = { onRecordClick(history.callId) }, - ) - } + histories.forEach { history -> + CallHistoryCard( + callHistory = history, + onRecordClick = { onRecordClick(history.callId) }, + ) } } } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt index 7fdf03a..ff2ea1f 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt @@ -6,19 +6,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.paging.PagingData -import kotlinx.coroutines.flow.Flow import kr.co.call.impl.tab.HomeHistoryTab import kr.co.call.impl.tab.HomeHistoryTabBar -import kr.co.call.impl.viewmodel.model.CallHistoryUiModel -import kr.co.call.impl.viewmodel.state.HomeNotificationState @Composable internal fun HomeHistorySection( selectedTab: HomeHistoryTab, onTabClick: (HomeHistoryTab) -> Unit, - notifications: Flow>, - callHistories: List, modifier: Modifier = Modifier, ) { Spacer(modifier = Modifier.height(22.dp)) @@ -28,16 +22,6 @@ internal fun HomeHistorySection( modifier = modifier, ) - // 탭 선택 상태에 따른 분기 - when (selectedTab) { - HomeHistoryTab.NOTIFICATION -> { - NotificationList(notifications = notifications) - } - - HomeHistoryTab.CALL_HISTORY -> { - CallHistoryList(histories = callHistories) - } - } } @Preview(showBackground = true) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt index 1c7b4b3..edd1bdf 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt @@ -8,13 +8,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button @@ -28,11 +27,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.paging.PagingData -import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemKey import java.time.LocalDateTime -import kotlinx.coroutines.flow.Flow import kr.co.call.core.common.util.TimeUtil import kr.co.call.designsystem.R import kr.co.call.designsystem.theme.CallFromAiTheme @@ -43,52 +40,50 @@ import kr.co.call.impl.viewmodel.state.HomeNotificationState import kr.co.call.impl.viewmodel.NotificationType /** - * 알림 리스트 - * - 커서 페이징 + * 알림 리스트 안내 문구 */ @Composable -internal fun NotificationList( - notifications: Flow>, +internal fun NotificationListHeader( modifier: Modifier = Modifier, ) { - Column(modifier = modifier.fillMaxSize()) { - Text( - text = "최근 7일 간의 알림만 표시됩니다.", - modifier = Modifier.padding( - start = 16.dp, - top = 16.dp, - bottom = 18.dp, - ), - color = CallTheme.colors.gray800, - style = CallTheme.typography.bodySmall, - ) - - val notificationItems = notifications.collectAsLazyPagingItems() + Text( + text = "최근 7일 간의 알림만 표시됩니다.", + modifier = modifier.padding( + start = 16.dp, + top = 16.dp, + bottom = 18.dp, + ), + color = CallTheme.colors.gray800, + style = CallTheme.typography.bodySmall, + ) +} - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentPadding = PaddingValues( - start = 16.dp, - end = 16.dp, - bottom = 24.dp, - ), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - items( - count = notificationItems.itemCount, - key = notificationItems.itemKey { it.notificationId }, - ) { index -> - notificationItems[index]?.let { notification -> - NotificationCard( - notificationState = notification, - onCallClick = {}, - ) - } - } +/** + * 홈 LazyColumn에 Paging 알림 아이템 추가 + */ +internal fun LazyListScope.notificationItems( + notifications: LazyPagingItems, +) { + items( + count = notifications.itemCount, + key = notifications.itemKey { notification -> notification.notificationId }, + ) { index -> + notifications[index]?.let { notification -> + NotificationCard( + notificationState = notification, + onCallClick = {}, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 12.dp, + ), + ) } } + +// item { +// Spacer(modifier = Modifier.height(12.dp)) +// } } /** @@ -126,6 +121,7 @@ private fun NotificationCard( shape = cardShape, ), ) { + // 기념일, 예약통화, 부재중 통화에 따라 분기 when (notificationState.type) { NotificationType.ANNIVERSARY -> { AnniversaryNotificationContent( @@ -173,6 +169,7 @@ private fun NotificationCard( } if (notificationState.type == NotificationType.MISSED_CALL) { + // 부재중이라면 다시 전화하기 버튼 표시 Button( onClick = onCallClick, modifier = Modifier @@ -211,7 +208,7 @@ private fun NotificationCard( } } - // n 분 전 표시 + // n분 전 표시 Text( text = TimeUtil.toTimeAgoText(notificationState.createdAt), modifier = Modifier diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt index 493abfa..f3eeb40 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt @@ -2,10 +2,10 @@ package kr.co.call.impl.screen import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -15,13 +15,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems import java.time.LocalDateTime import kotlinx.coroutines.flow.Flow import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.impl.component.header.HomeHeader -import kr.co.call.impl.component.history.HomeHistorySection import kr.co.call.impl.component.header.HomeReservationCard +import kr.co.call.impl.component.history.CallHistoryList +import kr.co.call.impl.component.history.HomeHistorySection +import kr.co.call.impl.component.history.NotificationListHeader +import kr.co.call.impl.component.history.notificationItems import kr.co.call.impl.tab.HomeHistoryTab import kr.co.call.impl.viewmodel.HomeIntent import kr.co.call.impl.viewmodel.state.HomeNotificationState @@ -29,7 +34,6 @@ import kr.co.call.impl.viewmodel.HomeViewModel import kr.co.call.impl.viewmodel.model.CallHistoryUiModel import kr.co.call.impl.viewmodel.model.HomeReservationUiModel import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel -import androidx.paging.PagingData import org.orbitmvi.orbit.compose.collectAsState /** @@ -75,33 +79,56 @@ internal fun HomeScreenContent( onHistoryTabClick: (HomeHistoryTab) -> Unit, modifier: Modifier = Modifier, ) { - Column( + val lazyNotificationItems = notifications.collectAsLazyPagingItems() + + LazyColumn( modifier = modifier .fillMaxSize() .background(CallTheme.colors.background), ) { - HomeHeader( - summary = homeSummary, - hasUnreadNotification = hasUnreadNotification, - ) + item { + HomeHeader( + summary = homeSummary, + hasUnreadNotification = hasUnreadNotification, + ) + } - // 예약된 전화 조회 - HomeReservationCard(reservation = homeReservation) + item { + // 예약된 전화 조회 + HomeReservationCard(reservation = homeReservation) + } - Box( - modifier = Modifier - .fillMaxWidth() - .height(7.dp) - .background(CallTheme.colors.gray100), - ) + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(7.dp) + .background(CallTheme.colors.gray100), + ) + } - // 지난 알림, 통화 기록 표시 섹션 - HomeHistorySection( - selectedTab = selectedHistoryTab, - onTabClick = onHistoryTabClick, - notifications = notifications, - callHistories = callHistories, - ) + item { + // 지난 알림, 통화 기록 표시 섹션 + HomeHistorySection( + selectedTab = selectedHistoryTab, + onTabClick = onHistoryTabClick, + ) + } + // 탭 선택 상태에 따라서 표시 + when (selectedHistoryTab) { + HomeHistoryTab.NOTIFICATION -> { + item { + NotificationListHeader() + } + notificationItems(lazyNotificationItems) + } + + HomeHistoryTab.CALL_HISTORY -> { + item { + CallHistoryList(histories = callHistories) + } + } + } } } From f43fdef1f58707462a116c63320f434dff80c982 Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:31:14 +0900 Subject: [PATCH 10/17] =?UTF-8?q?fix:=20AppScreen=EC=97=90=20Scaffold=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index 580eeb3..a842403 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -1,8 +1,14 @@ package kr.co.call.callfromai import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -68,13 +74,30 @@ fun AppScreen(modifier: Modifier = Modifier) { with(density) { bottomBarHeightPx.toDp() } } - Box(modifier = modifier.fillMaxSize()) { + Scaffold( + modifier = modifier.fillMaxSize(), + bottomBar = { + if (showBottomBar) { + MainBottomBar( + currentTab = currentTab, + onTabSelected = appNavigator::navigateToTab, + modifier = Modifier + .onSizeChanged { bottomBarHeightPx = it.height }, + ) + } + }, + contentWindowInsets = WindowInsets.safeDrawing.only( + WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal + ), + + ) { + padding -> CompositionLocalProvider( LocalBottomBarPadding provides if (showBottomBar) bottomBarPadding else 0.dp, ) { NavDisplay( backStack = backStack, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().padding(padding), entryProvider = entryProvider { loginEntry() onboardingEntry() @@ -84,14 +107,5 @@ fun AppScreen(modifier: Modifier = Modifier) { } ) } - if (showBottomBar) { - MainBottomBar( - currentTab = currentTab, - onTabSelected = appNavigator::navigateToTab, - modifier = Modifier - .align(Alignment.BottomCenter) - .onSizeChanged { bottomBarHeightPx = it.height }, - ) - } } } From 7969c952e9e29459e99abdb7af57b9413c90f09d Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:39:55 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20HomeUseCase=20=EB=8F=84=EC=9E=85?= =?UTF-8?q?=20=EB=B0=8F=20=ED=99=88=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HomeViewModel이 HomeRepository를 직접 호출하지 않고 HomeUseCase를 경유하도록 변경. develop에서 병합된 characterId/ReservationStatus 필드는 사용하지 않아 제거. --- .../java/kr/co/call/data/di/UseCaseModule.kt | 9 ++++++- .../co/call/data/mapper/CallHistoryMapper.kt | 5 +--- .../data/repositoryImpl/HomeRepositoryImpl.kt | 24 +++++++++++++++---- .../call/domain/model/home/CallReservation.kt | 5 ---- .../co/call/domain/model/home/HomeSummary.kt | 1 - .../call/domain/repository/HomeRepository.kt | 8 ++++++- .../java/kr/co/call/domain/repository/gitkeep | 1 - .../java/kr/co/call/domain/usecase/gitkeep | 1 - .../call/domain/usecase/home/HomeUseCase.kt | 21 ++++++++++++++++ .../java/kr/co/call/network/api/HomeApi.kt | 2 +- .../kr/co/call/impl/screen/HomeMockData.kt | 3 --- .../co/call/impl/viewmodel/HomeViewModel.kt | 16 ++++++------- .../co/call/impl/viewmodel/state/HomeState.kt | 1 - 13 files changed, 65 insertions(+), 32 deletions(-) delete mode 100644 core/domain/src/main/java/kr/co/call/domain/repository/gitkeep delete mode 100644 core/domain/src/main/java/kr/co/call/domain/usecase/gitkeep create mode 100644 core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt diff --git a/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt b/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt index 39d7751..831a68b 100644 --- a/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt @@ -1,11 +1,18 @@ package kr.co.call.data.di import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kr.co.call.domain.repository.HomeRepository +import kr.co.call.domain.usecase.home.HomeUseCase @Module @InstallIn(SingletonComponent::class) object UseCaseModule { -} \ No newline at end of file + @Provides + fun provideHomeUseCase( + homeRepository: HomeRepository, + ): HomeUseCase = HomeUseCase(homeRepository) +} diff --git a/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt b/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt index 685cc77..4898d41 100644 --- a/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt +++ b/core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt @@ -6,7 +6,6 @@ import kr.co.call.domain.model.home.CallHistory import kr.co.call.domain.model.home.CallReservation import kr.co.call.domain.model.home.CallReservations import kr.co.call.domain.model.home.HomeSummary -import kr.co.call.domain.model.home.ReservationStatus import kr.co.call.network.dto.CallHistoryDto import kr.co.call.network.dto.HomeSummaryDto import kr.co.call.network.dto.ReservationListDto @@ -52,12 +51,10 @@ private fun ReservationDto.toDomain(): CallReservation = firstName = firstName, imageUrl = imageUrl, scheduledAt = LocalDateTime.parse(scheduledAt), - status = ReservationStatus.valueOf(status), ) -internal fun HomeSummaryDto.toDomain(characterId: Long): HomeSummary = +internal fun HomeSummaryDto.toDomain(): HomeSummary = HomeSummary( - characterId = characterId, firstName = firstName, relationshipDays = relationshipDays, totalCallCount = totalCallCount, diff --git a/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt index fa571c9..657cf53 100644 --- a/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt +++ b/core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt @@ -6,7 +6,6 @@ import kr.co.call.domain.model.home.CallHistory import kr.co.call.domain.model.home.CallReservation import kr.co.call.domain.model.home.CallReservations import kr.co.call.domain.model.home.HomeSummary -import kr.co.call.domain.model.home.ReservationStatus import kr.co.call.domain.repository.HomeRepository class HomeRepositoryImpl @Inject constructor() : HomeRepository { @@ -22,7 +21,6 @@ class HomeRepositoryImpl @Inject constructor() : HomeRepository { firstName = "민준", imageUrl = null, scheduledAt = LocalDateTime.of(2026, 6, 30, 21, 0), - status = ReservationStatus.SCHEDULED, ), ), ), @@ -52,10 +50,9 @@ class HomeRepositoryImpl @Inject constructor() : HomeRepository { }, ) - override suspend fun getSummary(characterId: Long): Result = + override suspend fun getSummary(): Result = Result.success( HomeSummary( - characterId = characterId, firstName = "수현", relationshipDays = 30, totalCallCount = 24, @@ -63,6 +60,25 @@ class HomeRepositoryImpl @Inject constructor() : HomeRepository { ), ) + override suspend fun changeReservationTime( + reservationId: Long, + scheduledAt: LocalDateTime, + ): Result = + Result.success( + CallReservations( + totalCount = 1, + items = listOf( + CallReservation( + id = reservationId, + characterId = 2L, + firstName = "민준", + imageUrl = null, + scheduledAt = scheduledAt, + ), + ), + ), + ) + private companion object { const val CALL_HISTORY_SIZE = 20 } diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt index a3a421c..dbcf810 100644 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt @@ -8,9 +8,4 @@ data class CallReservation( val firstName: String, val imageUrl: String?, val scheduledAt: LocalDateTime, - val status: ReservationStatus, ) - -enum class ReservationStatus { - SCHEDULED, -} diff --git a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt index 2cd6c56..ef1d9c1 100644 --- a/core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt @@ -1,7 +1,6 @@ package kr.co.call.domain.model.home data class HomeSummary( - val characterId: Long, val firstName: String, val relationshipDays: Int, val totalCallCount: Int, diff --git a/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt b/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt index 6e6f723..b5cf961 100644 --- a/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt +++ b/core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt @@ -3,11 +3,17 @@ package kr.co.call.domain.repository import kr.co.call.domain.model.home.CallHistory import kr.co.call.domain.model.home.CallReservations import kr.co.call.domain.model.home.HomeSummary +import java.time.LocalDateTime interface HomeRepository { suspend fun getReservations(): Result suspend fun getCallHistories(): Result> - suspend fun getSummary(characterId: Long): Result + suspend fun getSummary(): Result + + suspend fun changeReservationTime( + reservationId: Long, + scheduledAt: LocalDateTime, + ): Result } diff --git a/core/domain/src/main/java/kr/co/call/domain/repository/gitkeep b/core/domain/src/main/java/kr/co/call/domain/repository/gitkeep deleted file mode 100644 index 945c9b4..0000000 --- a/core/domain/src/main/java/kr/co/call/domain/repository/gitkeep +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/gitkeep b/core/domain/src/main/java/kr/co/call/domain/usecase/gitkeep deleted file mode 100644 index 945c9b4..0000000 --- a/core/domain/src/main/java/kr/co/call/domain/usecase/gitkeep +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt new file mode 100644 index 0000000..d7d6c49 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt @@ -0,0 +1,21 @@ +package kr.co.call.domain.usecase.home + +import kr.co.call.domain.model.home.HomeOverview +import kr.co.call.domain.repository.HomeRepository + +class HomeUseCase( + private val homeRepository: HomeRepository, +) { + suspend operator fun invoke(): HomeOverview { + val summary = homeRepository.getSummary().getOrThrow() + val reservations = homeRepository.getReservations().getOrThrow() + val callHistories = homeRepository.getCallHistories().getOrThrow() + + return HomeOverview( + summary = summary, + reservations = reservations, + callHistories = callHistories, + ) + } + +} diff --git a/core/network/src/main/java/kr/co/call/network/api/HomeApi.kt b/core/network/src/main/java/kr/co/call/network/api/HomeApi.kt index b63e01e..c8feff0 100644 --- a/core/network/src/main/java/kr/co/call/network/api/HomeApi.kt +++ b/core/network/src/main/java/kr/co/call/network/api/HomeApi.kt @@ -16,5 +16,5 @@ interface HomeApi { suspend fun getCallHistories(@Query("size") size: Int = 20): CallHistoryListResponseDto @GET("api/v1/home/summary") - suspend fun getSummary(@Query("characterId") characterId: Long): HomeSummaryResponseDto + suspend fun getSummary(): HomeSummaryResponseDto } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt index 7fe836a..ff95b19 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt @@ -8,7 +8,6 @@ import kr.co.call.domain.model.home.CallHistory import kr.co.call.domain.model.home.CallReservation import kr.co.call.domain.model.home.CallReservations import kr.co.call.domain.model.home.HomeSummary -import kr.co.call.domain.model.home.ReservationStatus import kr.co.call.impl.mapper.toUiModel import kr.co.call.impl.mock.CallMockData import kr.co.call.impl.viewmodel.NotificationType @@ -45,7 +44,6 @@ internal fun createHomeMockData( ): HomeMockData = HomeMockData( summary = HomeSummary( - characterId = 2L, firstName = "수현", relationshipDays = 30, totalCallCount = 24, @@ -60,7 +58,6 @@ internal fun createHomeMockData( firstName = "민준", imageUrl = null, scheduledAt = now.withHour(21).withMinute(0).withSecond(0), - status = ReservationStatus.SCHEDULED, ), ), ), diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt index 3ea9d0c..abd64f5 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt @@ -3,7 +3,7 @@ package kr.co.call.impl.viewmodel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException -import kr.co.call.domain.repository.HomeRepository +import kr.co.call.domain.usecase.home.HomeUseCase import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.mapper.toUiModel import kr.co.call.impl.tab.HomeHistoryTab @@ -15,7 +15,7 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val homeRepository: HomeRepository, + private val homeUseCase: HomeUseCase, ) : ViewModel(), ContainerHost { @@ -54,15 +54,13 @@ class HomeViewModel @Inject constructor( } try { - val reservations = homeRepository.getReservations().getOrThrow() - val callHistories = homeRepository.getCallHistories().getOrThrow() - val summary = homeRepository.getSummary(state.characterId).getOrThrow() - + // 데이터 한번에 불러오기 + val home = homeUseCase() reduce { state.copy( - summary = summary.toUiModel(), - reservation = reservations.toUiModel(), - callHistories = callHistories.map { history -> history.toUiModel() }, + summary = home.summary.toUiModel(), + reservation = home.reservations.toUiModel(), + callHistories = home.callHistories.map { history -> history.toUiModel() }, loadStatus = LoadStatus.Idle, ) } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt index 43a62f6..3dbc454 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt @@ -7,7 +7,6 @@ import kr.co.call.impl.viewmodel.model.HomeReservationUiModel import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel data class HomeState( - val characterId: Long = 0L, val summary: HomeSummaryUiModel = HomeSummaryUiModel(), val reservation: HomeReservationUiModel = HomeReservationUiModel(), val callHistories: List = emptyList(), From 6ddeb25093ab3c17008bf6aca84d896463491438 Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:26:59 +0900 Subject: [PATCH 12/17] =?UTF-8?q?refactor:=20HomeUseCase=EB=A5=BC=20Hilt?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=A3=BC=EC=9E=85=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/kr/co/call/data/di/UseCaseModule.kt | 8 -------- core/domain/build.gradle.kts | 5 ++++- .../java/kr/co/call/domain/usecase/home/HomeUseCase.kt | 3 ++- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt b/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt index 831a68b..14ac708 100644 --- a/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt @@ -1,18 +1,10 @@ package kr.co.call.data.di import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kr.co.call.domain.repository.HomeRepository -import kr.co.call.domain.usecase.home.HomeUseCase @Module @InstallIn(SingletonComponent::class) object UseCaseModule { - - @Provides - fun provideHomeUseCase( - homeRepository: HomeRepository, - ): HomeUseCase = HomeUseCase(homeRepository) } diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index ebaea55..53aaab1 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -4,4 +4,7 @@ plugins { dependencies { // Coroutines는 KotlinLibraryConventionPlugin에서 자동 추가됨 -} \ No newline at end of file + + // Hilt 생성자 주입(@Inject)에 필요한 JSR-330 어노테이션 + implementation("javax.inject:javax.inject:1") +} diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt index d7d6c49..6226d5c 100644 --- a/core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt @@ -2,8 +2,9 @@ package kr.co.call.domain.usecase.home import kr.co.call.domain.model.home.HomeOverview import kr.co.call.domain.repository.HomeRepository +import javax.inject.Inject -class HomeUseCase( +class HomeUseCase @Inject constructor( private val homeRepository: HomeRepository, ) { suspend operator fun invoke(): HomeOverview { From b27d72aa1e008758d587f32a1baebca85d47e206 Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:27:12 +0900 Subject: [PATCH 13/17] =?UTF-8?q?refactor:=20=ED=99=88=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8/=EB=A7=A4=ED=8D=BC=20=EA=B0=80?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=EC=9D=84=20public=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/kr/co/call/impl/component/header/HomeHeader.kt | 2 +- .../kr/co/call/impl/component/header/RelationshipSummaryCard.kt | 2 +- .../src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt index 1dddbe6..786c75c 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt @@ -46,7 +46,7 @@ import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel * - 전화 버튼과 관계 요약 카드 포함 */ @Composable -internal fun HomeHeader( +fun HomeHeader( summary: HomeSummaryUiModel, hasUnreadNotification: Boolean, modifier: Modifier = Modifier, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt index 2f460b3..d9d1319 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt @@ -26,7 +26,7 @@ import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel * - 연속 통화 일수 표시 */ @Composable -internal fun RelationshipSummaryCard( +fun RelationshipSummaryCard( summary: HomeSummaryUiModel, modifier: Modifier = Modifier, ) { diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt index 50b60f8..972363b 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt @@ -5,7 +5,7 @@ import kr.co.call.domain.model.home.CallHistory import kr.co.call.impl.viewmodel.model.CallHistoryIconType import kr.co.call.impl.viewmodel.model.CallHistoryUiModel -internal fun CallHistory.toUiModel(): CallHistoryUiModel = +fun CallHistory.toUiModel(): CallHistoryUiModel = CallHistoryUiModel( callId = callId, characterName = characterName, From 28487cd3e35516b0de5e96842591b3d6a77962f0 Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:29:48 +0900 Subject: [PATCH 14/17] =?UTF-8?q?refactor:=20=ED=99=88=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=EC=9D=84=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EB=8B=A8=EC=9D=BC=20Laz?= =?UTF-8?q?yColumn=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HomeNotificationState/NotificationType을 core:domain의 HomeNotification으로 이동하고, UI 모델(HomeNotificationUiModel)과 매퍼를 추가해 시간 포맷팅을 매퍼로 옮김. 알림/통화기록 리스트를 HomeScreen의 단일 LazyColumn으로 통합. ProfileImageWithIcon 함수명을 ProfileImage로 변경. --- .../kr/co/call/core/common/util/TimeUtil.kt | 12 ++++ .../designsystem/component/ProfileImage.kt | 4 +- .../domain/model/home/HomeNotification.kt | 16 ++++-- .../component/header/HomeReservationCard.kt | 6 +- .../impl/component/history/CallHistoryList.kt | 2 +- .../component/history/HomeHistorySection.kt | 2 +- .../component/history/NotificationList.kt | 41 +++++++------- .../impl/mapper/HomeNotificationUiMapper.kt | 17 ++++++ .../kr/co/call/impl/screen/HomeMockData.kt | 18 +++--- .../java/kr/co/call/impl/screen/HomeScreen.kt | 5 +- .../call/impl/viewmodel/NotificationType.kt | 9 --- .../model/HomeNotificationUiModel.kt | 14 +++++ .../HomeNotificationTimeFormatterTest.kt | 56 +++++++++++++++++++ 13 files changed, 149 insertions(+), 53 deletions(-) rename feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt => core/domain/src/main/java/kr/co/call/domain/model/home/HomeNotification.kt (51%) create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt delete mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt create mode 100644 feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeNotificationUiModel.kt create mode 100644 feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt diff --git a/core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt b/core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt index e1ed3ca..8c1fa09 100644 --- a/core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt +++ b/core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt @@ -6,10 +6,19 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.Locale +/** + * 날짜 및 시간과 관련된 변환 및 포맷팅 처리를 담당하는 유틸리티 객체입니다. + * + * 주로 한국어 로케일을 기반으로 통화 내역, 홈 화면, 예약 시간 등에 사용되는 + * 다양한 형식의 문자열 변환 기능을 제공합니다. + */ object TimeUtil { + + // LocalDateTime을 String으로 변환 fun parseLocalDateTime(value: String): LocalDateTime = LocalDateTime.parse(value) + // LocalDate를 String으로 변환 fun toCallHistoryDateText(dateTime: LocalDateTime): String = dateTime.format( DateTimeFormatter.ofPattern( @@ -18,6 +27,7 @@ object TimeUtil { ), ) + // LocalDateTime을 "n분 전" 형식의 문자열로 변환 fun toTimeAgoText( dateTime: LocalDateTime, now: LocalDateTime = LocalDateTime.now(), @@ -33,6 +43,7 @@ object TimeUtil { } } + // LocalDate를 "M월 d일 EEEE" 형식의 문자열로 변환 fun toHomeDateText(date: LocalDate): String = date.format( DateTimeFormatter.ofPattern( @@ -41,6 +52,7 @@ object TimeUtil { ), ) + // LocalDateTime을 "오늘/내일 + 시간" 형식의 문자열로 변환 fun toReservationTimeText( dateTime: LocalDateTime, today: LocalDate = LocalDate.now(), diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt index ef93700..a7ffabd 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt @@ -25,7 +25,7 @@ import kr.co.call.designsystem.theme.CallFromAiTheme * - 통화 배지 표시 여부 설정 가능 */ @Composable -fun ProfileImageWithIcon( +fun ProfileImage( profileImageUrl: String?, modifier: Modifier = Modifier, showCallBadge: Boolean = false, @@ -71,7 +71,7 @@ fun ProfileImageWithIcon( @Composable private fun ProfileImagePreview() { CallFromAiTheme { - ProfileImageWithIcon( + ProfileImage( profileImageUrl = null, showCallBadge = true, ) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeNotification.kt similarity index 51% rename from feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt rename to core/domain/src/main/java/kr/co/call/domain/model/home/HomeNotification.kt index 3a8df93..3d9b4cc 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/home/HomeNotification.kt @@ -1,12 +1,8 @@ -package kr.co.call.impl.viewmodel.state +package kr.co.call.domain.model.home -import kr.co.call.impl.viewmodel.NotificationType import java.time.LocalDateTime -/** - * 알림 - */ -data class HomeNotificationState( +data class HomeNotification( val notificationId: Long, val type: NotificationType, val title: String, @@ -16,3 +12,11 @@ data class HomeNotificationState( val characterName: String?, val profileImageUrl: String?, ) + +enum class NotificationType( + val title: String, +) { + MISSED_CALL(title = "부재중 전화"), + ANNIVERSARY(title = "기념일"), + CALL_RESERVATION(title = "통화 약속"), +} diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt index f0d7914..97534c3 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt @@ -30,7 +30,7 @@ import java.time.LocalDate import kr.co.call.core.common.util.TimeUtil import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme -import kr.co.call.designsystem.component.ProfileImageWithIcon +import kr.co.call.designsystem.component.ProfileImage import kr.co.call.impl.viewmodel.model.HomeReservationUiModel /** @@ -39,7 +39,7 @@ import kr.co.call.impl.viewmodel.model.HomeReservationUiModel * - 약속이 있으면 상대 정보와 시간 변경 버튼 표시 */ @Composable -internal fun HomeReservationCard( +fun HomeReservationCard( reservation: HomeReservationUiModel, modifier: Modifier = Modifier, onTimeChangeClick: () -> Unit = {}, @@ -140,7 +140,7 @@ private fun ReservationContent( modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { - ProfileImageWithIcon( + ProfileImage( profileImageUrl = reservation.profileImageUrl, showCallBadge = true, ) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt index 2c59007..d42b533 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt @@ -32,7 +32,7 @@ import kr.co.call.impl.viewmodel.model.CallHistoryUiModel * - 완료 여부에 따라 AI 요약과 기록 아이콘 노출 */ @Composable -internal fun CallHistoryList( +fun CallHistoryList( histories: List, modifier: Modifier = Modifier, onRecordClick: (Long) -> Unit = {}, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt index ff2ea1f..0c9b56f 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt @@ -10,7 +10,7 @@ import kr.co.call.impl.tab.HomeHistoryTab import kr.co.call.impl.tab.HomeHistoryTabBar @Composable -internal fun HomeHistorySection( +fun HomeHistorySection( selectedTab: HomeHistoryTab, onTabClick: (HomeHistoryTab) -> Unit, modifier: Modifier = Modifier, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt index edd1bdf..66adaf2 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt @@ -30,20 +30,21 @@ import androidx.compose.ui.unit.sp import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemKey import java.time.LocalDateTime -import kr.co.call.core.common.util.TimeUtil import kr.co.call.designsystem.R import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme -import kr.co.call.designsystem.component.ProfileImageWithIcon +import kr.co.call.designsystem.component.ProfileImage +import kr.co.call.domain.model.home.HomeNotification +import kr.co.call.domain.model.home.NotificationType import kr.co.call.impl.component.UnreadIndicator -import kr.co.call.impl.viewmodel.state.HomeNotificationState -import kr.co.call.impl.viewmodel.NotificationType +import kr.co.call.impl.mapper.toUiModel +import kr.co.call.impl.viewmodel.model.HomeNotificationUiModel /** * 알림 리스트 안내 문구 */ @Composable -internal fun NotificationListHeader( +fun NotificationListHeader( modifier: Modifier = Modifier, ) { Text( @@ -61,8 +62,8 @@ internal fun NotificationListHeader( /** * 홈 LazyColumn에 Paging 알림 아이템 추가 */ -internal fun LazyListScope.notificationItems( - notifications: LazyPagingItems, +fun LazyListScope.notificationItems( + notifications: LazyPagingItems, ) { items( count = notifications.itemCount, @@ -70,7 +71,7 @@ internal fun LazyListScope.notificationItems( ) { index -> notifications[index]?.let { notification -> NotificationCard( - notificationState = notification, + notificationState = notification.toUiModel(), onCallClick = {}, modifier = Modifier.padding( start = 16.dp, @@ -81,9 +82,9 @@ internal fun LazyListScope.notificationItems( } } -// item { -// Spacer(modifier = Modifier.height(12.dp)) -// } + item { + Spacer(modifier = Modifier.height(12.dp)) + } } /** @@ -91,7 +92,7 @@ internal fun LazyListScope.notificationItems( */ @Composable private fun NotificationCard( - notificationState: HomeNotificationState, + notificationState: HomeNotificationUiModel, onCallClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -210,7 +211,7 @@ private fun NotificationCard( // n분 전 표시 Text( - text = TimeUtil.toTimeAgoText(notificationState.createdAt), + text = notificationState.createdAtText, modifier = Modifier .align(Alignment.TopEnd) .padding( @@ -295,7 +296,7 @@ private fun CallNotificationContent( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top, ) { - ProfileImageWithIcon( + ProfileImage( profileImageUrl = profileImageUrl, showCallBadge = true, ) @@ -330,7 +331,7 @@ private fun NotificationCardPreview() { verticalArrangement = Arrangement.spacedBy(12.dp), ) { NotificationCard( - notificationState = HomeNotificationState( + notificationState = HomeNotification( notificationId = 1, type = NotificationType.CALL_RESERVATION, title = "통화 약속", @@ -339,12 +340,12 @@ private fun NotificationCardPreview() { createdAt = now.minusMinutes(1), characterName = "민준", profileImageUrl = null, - ), + ).toUiModel(), onCallClick = {}, ) NotificationCard( - notificationState = HomeNotificationState( + notificationState = HomeNotification( notificationId = 2, type = NotificationType.MISSED_CALL, title = "부재중 전화", @@ -353,12 +354,12 @@ private fun NotificationCardPreview() { createdAt = now.minusMinutes(30), characterName = "민준", profileImageUrl = null, - ), + ).toUiModel(), onCallClick = {}, ) NotificationCard( - notificationState = HomeNotificationState( + notificationState = HomeNotification( notificationId = 3, type = NotificationType.ANNIVERSARY, title = "기념일", @@ -367,7 +368,7 @@ private fun NotificationCardPreview() { createdAt = now.minusHours(1), characterName = "민준", profileImageUrl = null, - ), + ).toUiModel(), onCallClick = {}, ) } diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt new file mode 100644 index 0000000..4d87e10 --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt @@ -0,0 +1,17 @@ +package kr.co.call.impl.mapper + +import kr.co.call.core.common.util.TimeUtil +import kr.co.call.domain.model.home.HomeNotification +import kr.co.call.impl.viewmodel.model.HomeNotificationUiModel + +internal fun HomeNotification.toUiModel(): HomeNotificationUiModel = + HomeNotificationUiModel( + notificationId = notificationId, + type = type, + title = title, + content = content, + isRead = isRead, + createdAtText = TimeUtil.toTimeAgoText(createdAt), + characterName = characterName, + profileImageUrl = profileImageUrl, + ) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt index ff95b19..9c2ed87 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt @@ -7,11 +7,11 @@ import kotlinx.coroutines.flow.flowOf import kr.co.call.domain.model.home.CallHistory import kr.co.call.domain.model.home.CallReservation import kr.co.call.domain.model.home.CallReservations +import kr.co.call.domain.model.home.HomeNotification import kr.co.call.domain.model.home.HomeSummary +import kr.co.call.domain.model.home.NotificationType import kr.co.call.impl.mapper.toUiModel import kr.co.call.impl.mock.CallMockData -import kr.co.call.impl.viewmodel.NotificationType -import kr.co.call.impl.viewmodel.state.HomeNotificationState import kr.co.call.impl.viewmodel.model.CallHistoryUiModel import kr.co.call.impl.viewmodel.model.HomeReservationUiModel import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel @@ -19,12 +19,12 @@ import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel /** * 홈화면에서 사용하는 Mock Data */ -internal data class HomeMockData( +data class HomeMockData( val summary: HomeSummary, val reservations: CallReservations, val callHistories: List, val hasUnreadNotification: Boolean, - val notifications: List, + val notifications: List, ) { val summaryUiModel: HomeSummaryUiModel get() = summary.toUiModel() @@ -35,11 +35,11 @@ internal data class HomeMockData( val callHistoryUiModels: List get() = callHistories.map { callHistory -> callHistory.toUiModel() } - fun notificationFlow(): Flow> = + fun notificationFlow(): Flow> = flowOf(PagingData.from(notifications)) } -internal fun createHomeMockData( +fun createHomeMockData( now: LocalDateTime = LocalDateTime.now(), ): HomeMockData = HomeMockData( @@ -64,7 +64,7 @@ internal fun createHomeMockData( callHistories = CallMockData.histories, hasUnreadNotification = true, notifications = listOf( - HomeNotificationState( + HomeNotification( notificationId = 42L, type = NotificationType.CALL_RESERVATION, title = "통화 약속", @@ -74,7 +74,7 @@ internal fun createHomeMockData( characterName = "민준", profileImageUrl = "https://cdn.lovecall.com/presets/male/1.jpg", ), - HomeNotificationState( + HomeNotification( notificationId = 41L, type = NotificationType.MISSED_CALL, title = "부재중 전화", @@ -84,7 +84,7 @@ internal fun createHomeMockData( characterName = "민준", profileImageUrl = "https://cdn.lovecall.com/presets/male/1.jpg", ), - HomeNotificationState( + HomeNotification( notificationId = 40L, type = NotificationType.ANNIVERSARY, title = "기념일", diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt index f3eeb40..5f1eeb7 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt @@ -21,6 +21,7 @@ import java.time.LocalDateTime import kotlinx.coroutines.flow.Flow import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.domain.model.home.HomeNotification import kr.co.call.impl.component.header.HomeHeader import kr.co.call.impl.component.header.HomeReservationCard import kr.co.call.impl.component.history.CallHistoryList @@ -29,7 +30,6 @@ import kr.co.call.impl.component.history.NotificationListHeader import kr.co.call.impl.component.history.notificationItems import kr.co.call.impl.tab.HomeHistoryTab import kr.co.call.impl.viewmodel.HomeIntent -import kr.co.call.impl.viewmodel.state.HomeNotificationState import kr.co.call.impl.viewmodel.HomeViewModel import kr.co.call.impl.viewmodel.model.CallHistoryUiModel import kr.co.call.impl.viewmodel.model.HomeReservationUiModel @@ -75,7 +75,7 @@ internal fun HomeScreenContent( homeReservation: HomeReservationUiModel, callHistories: List, selectedHistoryTab: HomeHistoryTab, - notifications: Flow>, + notifications: Flow>, onHistoryTabClick: (HomeHistoryTab) -> Unit, modifier: Modifier = Modifier, ) { @@ -114,6 +114,7 @@ internal fun HomeScreenContent( onTabClick = onHistoryTabClick, ) } + // 탭 선택 상태에 따라서 표시 when (selectedHistoryTab) { HomeHistoryTab.NOTIFICATION -> { diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt deleted file mode 100644 index 10316ff..0000000 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt +++ /dev/null @@ -1,9 +0,0 @@ -package kr.co.call.impl.viewmodel - -enum class NotificationType( - val title: String, -) { - MISSED_CALL(title = "부재중 전화"), - ANNIVERSARY(title = "기념일"), - CALL_RESERVATION(title = "통화 약속"), -} \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeNotificationUiModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeNotificationUiModel.kt new file mode 100644 index 0000000..a890d9f --- /dev/null +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeNotificationUiModel.kt @@ -0,0 +1,14 @@ +package kr.co.call.impl.viewmodel.model + +import kr.co.call.domain.model.home.NotificationType + +data class HomeNotificationUiModel( + val notificationId: Long, + val type: NotificationType, + val title: String, + val content: String, + val isRead: Boolean, + val createdAtText: String, + val characterName: String?, + val profileImageUrl: String?, +) diff --git a/feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt b/feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt new file mode 100644 index 0000000..d1704b1 --- /dev/null +++ b/feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt @@ -0,0 +1,56 @@ +package kr.co.call.impl.screen + +import java.time.LocalDateTime +import kr.co.call.core.common.util.TimeUtil +import org.junit.Assert.assertEquals +import org.junit.Test + +class HomeNotificationTimeFormatterTest { + + private val now = LocalDateTime.of(2026, 7, 17, 15, 0) + + @Test + fun `경과한 시간을 분 단위로 내림해 표시한다`() { + val createdAt = now.minusMinutes(3).minusSeconds(59) + + val result = TimeUtil.toTimeAgoText(createdAt, now) + + assertEquals("3분 전", result) + } + + @Test + fun `1분 미만은 0분 전으로 표시한다`() { + val createdAt = now.minusSeconds(59) + + val result = TimeUtil.toTimeAgoText(createdAt, now) + + assertEquals("0분 전", result) + } + + @Test + fun `미래 시각은 음수 대신 0분 전으로 표시한다`() { + val createdAt = now.plusMinutes(1) + + val result = TimeUtil.toTimeAgoText(createdAt, now) + + assertEquals("0분 전", result) + } + + @Test + fun `60분 이상은 시간 단위로 내림해 표시한다`() { + val createdAt = now.minusHours(3).minusMinutes(59) + + val result = TimeUtil.toTimeAgoText(createdAt, now) + + assertEquals("3시간 전", result) + } + + @Test + fun `24시간 이상은 일 단위로 내림해 표시한다`() { + val createdAt = now.minusDays(2).minusHours(23) + + val result = TimeUtil.toTimeAgoText(createdAt, now) + + assertEquals("2일 전", result) + } +} From 9f54b47e6e8cccbf4c46bac58f0739b4f91528f4 Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:30:03 +0900 Subject: [PATCH 15/17] =?UTF-8?q?feat:=20HomeSideEffect=EC=97=90=20ShowMes?= =?UTF-8?q?sage=20=EC=B6=94=EA=B0=80=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/impl/viewmodel/HomeSideEffect.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt index f399f6e..b4dd835 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt @@ -1,3 +1,10 @@ package kr.co.call.impl.viewmodel -sealed interface HomeSideEffect +sealed interface HomeSideEffect { + data class ShowMessage( + val message: String, + val actionLabel: String? = null, + val onActionClick: (() -> Unit)? = null, + ) : HomeSideEffect + +} From d933d0a1711940f6fac3466181f88f5694918c5c Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:33:41 +0900 Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20internal=20=EA=B0=80=EC=8B=9C=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/kr/co/call/impl/component/UnreadIndicator.kt | 2 +- .../java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt | 2 +- .../impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt | 4 ++-- .../impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt | 2 +- .../src/main/java/kr/co/call/impl/mock/CharacterMockData.kt | 4 ++-- .../impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt | 2 +- .../src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt | 2 +- .../src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt | 2 +- .../src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt b/feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt index 4ed9a46..1dab5c2 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt @@ -16,7 +16,7 @@ import kr.co.call.designsystem.theme.CallTheme * - 홈 헤더와 알림 목록에서 공통 사용 */ @Composable -internal fun UnreadIndicator( +fun UnreadIndicator( modifier: Modifier = Modifier, ) { Box( diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt index 4d87e10..dd65375 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeNotificationUiMapper.kt @@ -4,7 +4,7 @@ import kr.co.call.core.common.util.TimeUtil import kr.co.call.domain.model.home.HomeNotification import kr.co.call.impl.viewmodel.model.HomeNotificationUiModel -internal fun HomeNotification.toUiModel(): HomeNotificationUiModel = +fun HomeNotification.toUiModel(): HomeNotificationUiModel = HomeNotificationUiModel( notificationId = notificationId, type = type, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt index 3848c2a..b62d395 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt @@ -6,7 +6,7 @@ import kr.co.call.domain.model.home.HomeSummary import kr.co.call.impl.viewmodel.model.HomeReservationUiModel import kr.co.call.impl.viewmodel.model.HomeSummaryUiModel -internal fun HomeSummary.toUiModel(): HomeSummaryUiModel = +fun HomeSummary.toUiModel(): HomeSummaryUiModel = HomeSummaryUiModel( firstName = firstName, relationshipDaysText = relationshipDays.withSuffixOrDash("일째"), @@ -14,7 +14,7 @@ internal fun HomeSummary.toUiModel(): HomeSummaryUiModel = callStreakDaysText = callStreakDays.withSuffixOrDash("일"), ) -internal fun CallReservations.toUiModel(): HomeReservationUiModel { +fun CallReservations.toUiModel(): HomeReservationUiModel { val reservation = items.firstOrNull() return HomeReservationUiModel( diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt index 60b46fc..6bfc2fe 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt @@ -5,7 +5,7 @@ import kr.co.call.domain.model.home.CallHistory import kr.co.call.impl.mapper.toUiModel import kr.co.call.impl.viewmodel.model.CallHistoryUiModel -internal object CallMockData { +object CallMockData { val histories: List = listOf( CallHistory( callId = 1L, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt index 0a0cce3..61bed57 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt @@ -2,9 +2,9 @@ package kr.co.call.impl.mock import kr.co.call.impl.viewmodel.model.CharacterOptionUiModel -internal object CharacterMockData { +object CharacterMockData { val uiModels: List = listOf( CharacterOptionUiModel(characterId = 1L, name = "민준", day = 30, isSelected = true), CharacterOptionUiModel(characterId = 2L, name = "동휘", day = 12, isSelected = false), ) -} +} \ No newline at end of file diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt index 5f1eeb7..525d544 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt @@ -69,7 +69,7 @@ fun HomeScreen( * - 배경과 영역 구분선 표시 */ @Composable -internal fun HomeScreenContent( +fun HomeScreenContent( homeSummary: HomeSummaryUiModel, hasUnreadNotification: Boolean, homeReservation: HomeReservationUiModel, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt b/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt index 98470f0..4ab815a 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt @@ -19,7 +19,7 @@ import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme @Composable -internal fun HomeHistoryTabBar( +fun HomeHistoryTabBar( selectedTab: HomeHistoryTab, onTabClick: (HomeHistoryTab) -> Unit, modifier: Modifier = Modifier, diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt index 36a13f8..0d99d3c 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt @@ -2,7 +2,7 @@ package kr.co.call.impl.viewmodel import kr.co.call.impl.tab.HomeHistoryTab -internal sealed interface HomeIntent { +sealed interface HomeIntent { data class SelectHistoryTab( val tab: HomeHistoryTab, ) : HomeIntent diff --git a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt index abd64f5..1c53592 100644 --- a/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt +++ b/feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt @@ -28,7 +28,7 @@ class HomeViewModel @Inject constructor( loadHome() } - internal fun handleIntent(intent: HomeIntent) { + fun handleIntent(intent: HomeIntent) { when (intent) { is HomeIntent.SelectHistoryTab -> { selectHistoryTab(intent.tab) From f0e0429c092920365731654afe138bad69267796 Mon Sep 17 00:00:00 2001 From: ljw42b <80021912+JiwonLee42@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:37:00 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeNotificationTimeFormatterTest.kt | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt diff --git a/feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt b/feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt deleted file mode 100644 index d1704b1..0000000 --- a/feature/home/impl/src/test/java/kr/co/call/impl/screen/HomeNotificationTimeFormatterTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package kr.co.call.impl.screen - -import java.time.LocalDateTime -import kr.co.call.core.common.util.TimeUtil -import org.junit.Assert.assertEquals -import org.junit.Test - -class HomeNotificationTimeFormatterTest { - - private val now = LocalDateTime.of(2026, 7, 17, 15, 0) - - @Test - fun `경과한 시간을 분 단위로 내림해 표시한다`() { - val createdAt = now.minusMinutes(3).minusSeconds(59) - - val result = TimeUtil.toTimeAgoText(createdAt, now) - - assertEquals("3분 전", result) - } - - @Test - fun `1분 미만은 0분 전으로 표시한다`() { - val createdAt = now.minusSeconds(59) - - val result = TimeUtil.toTimeAgoText(createdAt, now) - - assertEquals("0분 전", result) - } - - @Test - fun `미래 시각은 음수 대신 0분 전으로 표시한다`() { - val createdAt = now.plusMinutes(1) - - val result = TimeUtil.toTimeAgoText(createdAt, now) - - assertEquals("0분 전", result) - } - - @Test - fun `60분 이상은 시간 단위로 내림해 표시한다`() { - val createdAt = now.minusHours(3).minusMinutes(59) - - val result = TimeUtil.toTimeAgoText(createdAt, now) - - assertEquals("3시간 전", result) - } - - @Test - fun `24시간 이상은 일 단위로 내림해 표시한다`() { - val createdAt = now.minusDays(2).minusHours(23) - - val result = TimeUtil.toTimeAgoText(createdAt, now) - - assertEquals("2일 전", result) - } -}