Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
22dfd7b
feat: 홈 시간 포맷 유틸 추가 (#7)
JiwonLee42 Jul 17, 2026
ca61e20
feat: 홈 데이터 조회 계층 구현 (#7)
JiwonLee42 Jul 17, 2026
34b6413
feat: 홈 디자인 리소스 추가 (#7)
JiwonLee42 Jul 17, 2026
4224e4a
feat: 홈 화면 및 상태 관리 구현 (#7)
JiwonLee42 Jul 17, 2026
1ba4013
fix: 홈 Repository 주입 및 데이터 조회 오류 수정 (#7)
JiwonLee42 Jul 17, 2026
5ce2bb1
refactor: 홈 모델 구조 재설계 (#7)
JiwonLee42 Jul 17, 2026
6aba72d
Merge branch 'develop' into feat/#7-home-ui
JiwonLee42 Jul 17, 2026
22aa4d0
feat: 홈 개요 모델과 캐릭터 변경 옵션 UI 모델 추가 (#7)
JiwonLee42 Jul 17, 2026
fde5d7e
refactor: ProfileImageWithIcon 파일명을 ProfileImage로 변경
JiwonLee42 Jul 17, 2026
82794d2
feat: 헤더 고정 없이 전체 스크롤되도록 수정(#7)
JiwonLee42 Jul 18, 2026
6b864ea
Merge branch 'develop' into feat/#7-home-ui
JiwonLee42 Jul 18, 2026
f43fdef
fix: AppScreen에 Scaffold 적용 및 레이아웃 개선
JiwonLee42 Jul 18, 2026
7969c95
feat: HomeUseCase 도입 및 홈 데이터 조회 로직 통합 (#7)
JiwonLee42 Jul 18, 2026
6ddeb25
refactor: HomeUseCase를 Hilt 생성자 주입 방식으로 전환 (#7)
JiwonLee42 Jul 18, 2026
b27d72a
refactor: 홈 컴포넌트/매퍼 가시성을 public으로 변경 (#7)
JiwonLee42 Jul 18, 2026
28487cd
refactor: 홈 알림을 도메인 모델로 분리하고 리스트를 단일 LazyColumn으로 통합 (#7)
JiwonLee42 Jul 18, 2026
9f54b47
feat: HomeSideEffect에 ShowMessage 추가 (#7)
JiwonLee42 Jul 18, 2026
d933d0a
refactor: 불필요한 internal 가시성 제거 (#7)
JiwonLee42 Jul 18, 2026
f0e0429
feat: 테스트코드 제거
JiwonLee42 Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 25 additions & 11 deletions app/src/main/java/kr/co/call/callfromai/AppScreen.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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 },
)
}
}
}
87 changes: 87 additions & 0 deletions core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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

@KateteDeveloper KateteDeveloper Jul 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지나가는 안드 나그네 입니다...!
이건 core common에서 공통적으로 사용하는 유틸이기에 KDOC로 주석 달면 편하게 볼 수 있을 것 같아요:)

https://velog.io/@dudgns0507/Kotlin-KDoc%EC%9C%BC%EB%A1%9C-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%93%9C-%EB%AC%B8%EC%84%9C%ED%99%94%ED%95%98%EA%B8%B0-feat.-Dokka

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codebidoof 잠깐만 지나가겠습니다....

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㄱㅊ아영

/**
* 날짜 및 시간과 관련된 변환 및 포맷팅 처리를 담당하는 유틸리티 객체입니다.
*
* 주로 한국어 로케일을 기반으로 통화 내역, 홈 화면, 예약 시간 등에 사용되는
* 다양한 형식의 문자열 변환 기능을 제공합니다.
*/
object TimeUtil {

// LocalDateTime을 String으로 변환
fun parseLocalDateTime(value: String): LocalDateTime =
LocalDateTime.parse(value)

// LocalDate를 String으로 변환
fun toCallHistoryDateText(dateTime: LocalDateTime): String =
dateTime.format(
DateTimeFormatter.ofPattern(
"M월 d일 a h시 m분",
Locale.KOREAN,
),
)

// LocalDateTime을 "n분 전" 형식의 문자열로 변환
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)}일 전"
}
}

// LocalDate를 "M월 d일 EEEE" 형식의 문자열로 변환
fun toHomeDateText(date: LocalDate): String =
date.format(
DateTimeFormatter.ofPattern(
"M월 d일 EEEE",
Locale.KOREAN,
),
)

// LocalDateTime을 "오늘/내일 + 시간" 형식의 문자열로 변환
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"))
}
3 changes: 2 additions & 1 deletion core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ android {

dependencies {
// 다른 core 모듈 의존성
implementation(project(":core:common"))
implementation(project(":core:domain"))
implementation(project(":core:network"))
implementation(project(":core:datastore"))
Expand All @@ -20,4 +21,4 @@ dependencies {

// timber
implementation(libs.timber)
}
}
10 changes: 9 additions & 1 deletion core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package kr.co.call.data.di

import dagger.Binds
import dagger.Module
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.data.repositoryImpl.MyPageRepositoryImpl
import kr.co.call.domain.repository.MyPageRepository

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

@Binds
@Singleton
abstract fun bindHomeRepository(
homeRepositoryImpl: HomeRepositoryImpl,
): HomeRepository
@Binds
abstract fun bindMyPageRepository(
impl: MyPageRepositoryImpl
Expand Down
3 changes: 1 addition & 2 deletions core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object UseCaseModule {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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.CallReservation
import kr.co.call.domain.model.home.CallReservations
import kr.co.call.domain.model.home.HomeSummary
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,
aiSummary = aiSummary,
startedAt = parsedStartedAt,
isOutgoing = sender.toIsOutgoing(),
isMissed = status.toIsMissed(),
)
}

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(): CallReservation =
CallReservation(
id = callReservationId,
characterId = characterId,
firstName = firstName,
imageUrl = imageUrl,
scheduledAt = LocalDateTime.parse(scheduledAt),
)
Comment on lines +47 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

날짜 파싱 방식이 CallHistoryDto와 일치하지 않습니다.

CallHistoryDto.toDomain()TimeUtil.parseLocalDateTime(startedAt)를 사용하지만, ReservationDto.toDomain()LocalDateTime.parse(scheduledAt)를 직접 호출합니다. API가 ISO-8601 외 포맷(예: "2026-06-30 21:00:00")을 반환하면 예약 매핑에서 DateTimeParseException이 발생합니다. TimeUtil.parseLocalDateTime을 일관되게 사용하세요.

🔧 제안 수정
-        scheduledAt = LocalDateTime.parse(scheduledAt),
+        scheduledAt = TimeUtil.parseLocalDateTime(scheduledAt),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun ReservationDto.toDomain(): CallReservation =
CallReservation(
id = callReservationId,
characterId = characterId,
firstName = firstName,
imageUrl = imageUrl,
scheduledAt = LocalDateTime.parse(scheduledAt),
status = ReservationStatus.valueOf(status),
)
private fun ReservationDto.toDomain(): CallReservation =
CallReservation(
id = callReservationId,
characterId = characterId,
firstName = firstName,
imageUrl = imageUrl,
scheduledAt = TimeUtil.parseLocalDateTime(scheduledAt),
status = ReservationStatus.valueOf(status),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt` around
lines 48 - 56, Update ReservationDto.toDomain to parse scheduledAt through
TimeUtil.parseLocalDateTime instead of LocalDateTime.parse, matching
CallHistoryDto.toDomain and supporting the API’s non-ISO date formats.


internal fun HomeSummaryDto.toDomain(): HomeSummary =
HomeSummary(
firstName = firstName,
relationshipDays = relationshipDays,
totalCallCount = totalCallCount,
callStreakDays = callStreakDays,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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.CallReservation
import kr.co.call.domain.model.home.CallReservations
import kr.co.call.domain.model.home.HomeSummary
import kr.co.call.domain.repository.HomeRepository

class HomeRepositoryImpl @Inject constructor() : HomeRepository {

override suspend fun getReservations(): Result<CallReservations> =
Result.success(
CallReservations(
totalCount = 1,
items = listOf(
CallReservation(
id = 3L,
characterId = 2L,
firstName = "민준",
imageUrl = null,
scheduledAt = LocalDateTime.of(2026, 6, 30, 21, 0),
),
),
),
)

override suspend fun getCallHistories(): Result<List<CallHistory>> =
Result.success(
List(CALL_HISTORY_SIZE) { index ->
CallHistory(
callId = (index + 1).toLong(),
characterName = if (index % 2 == 0) "민준" else "동휘",
aiSummary = if (index % 2 == 0) {
"오늘 하루와 퇴근 후 일상 이야기"
} else {
"몸살 감기 기운과 걱정해주는 이야기"
},
startedAt = LocalDateTime.of(
2026,
6,
(28 - index).coerceAtLeast(1),
23,
2,
),
isOutgoing = index % 2 == 0,
isMissed = index % 4 == 3,
)
},
)

override suspend fun getSummary(): Result<HomeSummary> =
Result.success(
HomeSummary(
firstName = "수현",
relationshipDays = 30,
totalCallCount = 24,
callStreakDays = 12,
),
)

override suspend fun changeReservationTime(
reservationId: Long,
scheduledAt: LocalDateTime,
): Result<CallReservations> =
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
}
}
3 changes: 3 additions & 0 deletions core/designsystem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ android {
dependencies {
// Compose 의존성은 AndroidComposeConventionPlugin에서 자동 추가됨

// coil
implementation(libs.coil.compose)

// android 의존성
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.compose.material3)
Expand Down
Loading