Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion app/src/main/java/kr/co/call/callfromai/AppScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import kr.co.call.api.CallRecordNavKey
import kr.co.call.api.ChatRoomNavKey
import kr.co.call.api.ChattingNavKey
import kr.co.call.api.FaqNavKey
Expand Down Expand Up @@ -107,7 +108,14 @@ fun AppScreen(modifier: Modifier = Modifier) {
entryProvider = entryProvider {
loginEntry()
onboardingEntry()
homeEntry()
homeEntry(
navigateToCallRecord = { callId ->
appNavigator.navigate(CallRecordNavKey(callId = callId))
},
onCallRecordBack = {
appNavigator.popBackStack()
},
)
Comment on lines +111 to +118

@coderabbitai coderabbitai Bot Jul 24, 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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

홈→통화 기록 전환 화면 자료를 PR에 첨부해 주세요.

PR 설명에 이 변경 흐름을 확인할 스크린샷 또는 화면 녹화가 없습니다. As per coding guidelines, "*Screen.kt 변경 시 스크린샷 또는 화면 녹화가 첨부되었는지 확인하고 없으면 경고한다" 규칙에 따른 사항입니다.

🤖 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 `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt` around lines 106 - 113,
Attach a screenshot or screen recording to the PR demonstrating the
home-to-call-record transition implemented in the homeEntry navigation flow,
including navigation via CallRecordNavKey and returning with popBackStack.

Source: Coding guidelines

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.

이거왜안했엇!!!!!!

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 맞습니다, 이건 제가 코드 변경과 별개로 PR 설명의 화면 자료 첨부 여부를 체크하는 가이드라인 때문에 잡은 항목이에요. 이번 PR에서 HomeScreen.ktCallRecordScreen.kt가 변경됐는데, 설명에 홈 → 통화 기록 진입 및 뒤로가기 흐름을 보여주는 스크린샷/영상이 없어서 경고됐습니다.

번거롭게 해서 죄송합니다 🙇 PR 설명에 해당 화면 자료만 첨부하면 충분합니다.

🐰💦

chattingEntry(
navigateToChatRoom = { roomId ->
appNavigator.navigate(ChatRoomNavKey(roomId = roomId))
Expand Down
17 changes: 13 additions & 4 deletions core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kr.co.call.data.repositoryImpl.CallRecordMockRepository
import kr.co.call.data.repositoryImpl.FaqRepositoryImpl
import kr.co.call.data.repositoryImpl.ChatRepositoryImpl
import kr.co.call.data.repositoryImpl.HomeMockRepository
import kr.co.call.data.repositoryImpl.MyPageRepositoryImpl
import kr.co.call.domain.repository.CallRecordRepository
import kr.co.call.domain.repository.ChatRepository
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.FaqRepository
Expand All @@ -27,9 +30,15 @@ abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindHomeRepository(
homeRepositoryImpl: HomeRepositoryImpl,
homeMockRepository: HomeMockRepository,
): HomeRepository

@Binds
@Singleton
abstract fun bindCallRecordRepository(
callRecordMockRepository: CallRecordMockRepository,
): CallRecordRepository

@Binds
@Singleton
abstract fun bindMyPageRepository(
Expand All @@ -41,4 +50,4 @@ abstract class RepositoryModule {
abstract fun bindFaqRepository(
impl: FaqRepositoryImpl
): FaqRepository
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
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)
Expand Down Expand Up @@ -37,26 +30,3 @@ private fun String.toIsMissed(): Boolean =
"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),
)

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,21 @@
package kr.co.call.data.mapper

import kr.co.call.domain.model.home.CallTranscript
import kr.co.call.network.dto.CallTranscriptDto
import kr.co.call.network.dto.CallTranscriptResultDto

internal fun CallTranscriptResultDto.toDomain(): List<CallTranscript> =
content.map { transcript -> transcript.toDomain() }

private fun CallTranscriptDto.toDomain(): CallTranscript =
CallTranscript(
content = content,
speaker = speaker.toDomainSpeaker(),
)

private fun String.toDomainSpeaker(): CallTranscript.Speaker =
when (this) {
"사용자", "USER" -> CallTranscript.Speaker.USER
"AI" -> CallTranscript.Speaker.AI
else -> error("지원하지 않는 화자입니다: $this")
}
12 changes: 12 additions & 0 deletions core/data/src/main/java/kr/co/call/data/mapper/HomeMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kr.co.call.data.mapper

import kr.co.call.domain.model.home.HomeSummary
import kr.co.call.network.dto.HomeSummaryDto

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,22 @@
package kr.co.call.data.mapper

import java.time.LocalDateTime
import kr.co.call.domain.model.home.CallReservation
import kr.co.call.domain.model.home.CallReservations
import kr.co.call.network.dto.ReservationDto
import kr.co.call.network.dto.ReservationListDto

internal fun ReservationListDto.toDomain(): CallReservations =
CallReservations(
totalCount = count,
items = content.map { reservation -> reservation.toDomain() },
)

private fun ReservationDto.toDomain(): CallReservation =
CallReservation(
id = callReservationId,
characterId = characterId,
firstName = firstName,
imageUrl = imageUrl,
scheduledAt = LocalDateTime.parse(scheduledAt),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package kr.co.call.data.repositoryImpl

import java.time.LocalDateTime
import javax.inject.Inject
import kr.co.call.domain.model.home.CallInfo
import kr.co.call.domain.model.home.CallTranscript
import kr.co.call.domain.repository.CallRecordRepository

class CallRecordMockRepository @Inject constructor() : CallRecordRepository {

override suspend fun getCallInfo(callId: Long): Result<CallInfo> {
if (callId !in 1L..20L) {
return callNotFound()
}

return Result.success(
CallInfo(
callId = callId,
title = "출근 준비와 아침 일정 이야기",
calledAt = LocalDateTime.of(2026, 6, 27, 7, 31),
characterName = if (callId % 2L == 1L) "민준" else "동휘",
recordingUrl =
"https://storage.googleapis.com/exoplayer-test-media-0/play.mp3",
durationMillis = 0L,
),
)
}

override suspend fun getCallScript(
callId: Long,
): Result<List<CallTranscript>> {
if (callId !in 1L..20L) {
return callNotFound()
}

return Result.success(
listOf(
CallTranscript(
content = "여보세요",
speaker = CallTranscript.Speaker.USER,
),
CallTranscript(
content = "잘 일어났어? 목소리 아직 잠긴 것 같은데.",
speaker = CallTranscript.Speaker.AI,
),
CallTranscript(
content = "응 방금 일어나 준비하고 있었어",
speaker = CallTranscript.Speaker.USER,
),
CallTranscript(
content = "오늘 출근이지? 몸은 좀 괜찮아? 피곤해 보여.",
speaker = CallTranscript.Speaker.AI,
),
),
)
}

private fun <T> callNotFound(): Result<T> =
Result.failure(
IllegalArgumentException("통화 기록을 찾을 수 없습니다."),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kr.co.call.data.repositoryImpl

import javax.inject.Inject
import kr.co.call.domain.model.home.CallInfo
import kr.co.call.domain.model.home.CallTranscript
import kr.co.call.domain.repository.CallRecordRepository

class CallRecordRepositoryImpl @Inject constructor() : CallRecordRepository {

override suspend fun getCallInfo(callId: Long): Result<CallInfo> =
unsupportedApi()

override suspend fun getCallScript(
callId: Long,
): Result<List<CallTranscript>> = unsupportedApi()

private fun <T> unsupportedApi(): Result<T> =
Result.failure(
UnsupportedOperationException("통화 기록 서버 API가 아직 연결되지 않았습니다."),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package kr.co.call.data.repositoryImpl

import java.time.LocalDateTime
import javax.inject.Inject
import kr.co.call.domain.exception.CharacterChangeUnavailableException
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.HomeCharacter
import kr.co.call.domain.model.home.HomeSummary
import kr.co.call.domain.repository.HomeRepository

class HomeMockRepository @Inject constructor() : HomeRepository {

private var characters = listOf(
HomeCharacter(
id = 1L,
name = "민준",
relationshipDays = 30,
imageUrl = null,
isMain = true,
),
HomeCharacter(
id = 2L,
name = "동휘",
relationshipDays = 12,
imageUrl = null,
isMain = false,
),
)

private var reservations = CallReservations(
totalCount = 1,
items = listOf(
CallReservation(
id = 3L,
characterId = 1L,
firstName = "민준",
imageUrl = null,
scheduledAt = LocalDateTime.of(2026, 6, 30, 21, 0),
),
),
)

override suspend fun getCharacters(): Result<List<HomeCharacter>> =
Result.success(characters)

override suspend fun getReservations(): Result<CallReservations> =
Result.success(reservations)

override suspend fun getCallHistories(): Result<List<CallHistory>> =
Result.success(
List(20) { 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> {
val hasReservation = reservations.items.any { reservation ->
reservation.id == reservationId
}
if (!hasReservation) {
return Result.failure(
IllegalArgumentException("변경할 예약을 찾을 수 없습니다."),
)
}

reservations = reservations.copy(
items = reservations.items.map { reservation ->
if (reservation.id == reservationId) {
reservation.copy(scheduledAt = scheduledAt)
} else {
reservation
}
},
)

return Result.success(reservations)
}

override suspend fun changeMainCharacter(
characterId: Long,
): Result<List<HomeCharacter>> {
val hasCharacter = characters.any { character ->
character.id == characterId
}
if (!hasCharacter) {
return Result.failure(
IllegalArgumentException("변경할 캐릭터를 찾을 수 없습니다."),
)
}

return Result.failure(CharacterChangeUnavailableException())
}

override suspend fun startCall(characterId: Long): Result<Unit> {
val isMainCharacter = characters.any { character ->
character.id == characterId && character.isMain
}

return if (isMainCharacter) {
Result.success(Unit)
} else {
Result.failure(
IllegalStateException("메인 캐릭터와만 통화할 수 있습니다."),
)
}
}
}
Loading