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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
Expand All @@ -31,9 +32,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.afternote.core.ui.R
import com.afternote.core.ui.theme.AfternoteDesign
Expand Down Expand Up @@ -128,7 +131,7 @@ fun DatePickerContent(
) {
Text(
text = title,
style = AfternoteDesign.typography.h3,
style = AfternoteDesign.typography.bodyBase,
color = AfternoteDesign.colors.gray9,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
Expand All @@ -147,7 +150,7 @@ fun DatePickerContent(
) {
Text(
text = formattedDate,
style = AfternoteDesign.typography.h3,
style = AfternoteDesign.typography.textField,
color = AfternoteDesign.colors.gray9,
)
}
Expand All @@ -169,71 +172,103 @@ fun DatePickerContent(
) {
Text(
text = "${currentYear}년 ${currentMonth}월",
style = AfternoteDesign.typography.h3,
style = AfternoteDesign.typography.bodySmallR,
color = AfternoteDesign.colors.gray9,
modifier = Modifier.weight(1f),
)
Icon(
painter = painterResource(R.drawable.core_ui_arrow_left),
contentDescription = "이전 달",
Box(
modifier =
Modifier
.size(24.dp)
.height(8.dp)
.width(4.dp)
.clipToBounds()
.clickable { onPrevMonth() },
tint = AfternoteDesign.colors.gray9,
)
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(R.drawable.core_ui_arrow_left),
contentDescription = "이전 달",
modifier = Modifier.size(24.dp),
tint = AfternoteDesign.colors.gray9,
)
}
Spacer(modifier = Modifier.width(4.dp))
Icon(
painter = painterResource(R.drawable.core_ui_right),
contentDescription = "다음 달",
Box(
modifier =
Modifier
.size(24.dp)
.height(8.dp)
.width(4.dp)
.clipToBounds()
.clickable { onNextMonth() },
tint = AfternoteDesign.colors.gray9,
)
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(R.drawable.core_ui_right_arrow),
contentDescription = "다음 달",
modifier = Modifier.size(24.dp),
tint = AfternoteDesign.colors.gray9,
)
}
}

Spacer(modifier = Modifier.height(16.dp))

val dayLabels = listOf("일", "월", "화", "수", "목", "금", "토")
Row(modifier = Modifier.fillMaxWidth()) {
dayLabels.forEach { label ->
Text(
text = label,
modifier = Modifier.weight(1f),
color = Color(0xFF000000).copy(alpha = 0.3f),
style = AfternoteDesign.typography.footnoteCaption,
textAlign = TextAlign.Center,
)
}
}
CalendarGridContent(
currentYear = currentYear,
currentMonth = currentMonth,
selectedDate = selectedDate,
onDateSelect = onDateSelect,
)
}
Spacer(modifier = Modifier.height(50.dp))
}
}
}

Spacer(modifier = Modifier.height(10.dp))
@Composable
fun CalendarGridContent(
currentYear: Int,
currentMonth: Int,
selectedDate: LocalDate,
onDateSelect: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
val dayLabels = listOf("일", "월", "화", "수", "목", "금", "토")
Row(modifier = Modifier.fillMaxWidth()) {
dayLabels.forEach { label ->
Text(
text = label,
modifier = Modifier.weight(1f),
color = Color(0xFF000000).copy(alpha = 0.3f),
style = AfternoteDesign.typography.footnoteCaption,
textAlign = TextAlign.Center,
)
}
}

val days =
buildPickerDays(
year = currentYear,
month = currentMonth,
selectedDate = selectedDate,
)
days.chunked(7).forEach { week ->
Row(modifier = Modifier.fillMaxWidth()) {
week.forEach { dayModel ->
Box(modifier = Modifier.weight(1f)) {
PickerDayCell(
model = dayModel,
onSelect = { dayModel.day?.let(onDateSelect) },
)
}
}
repeat(7 - week.size) {
Spacer(modifier = Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(10.dp))

val days =
buildPickerDays(
year = currentYear,
month = currentMonth,
selectedDate = selectedDate,
)
days.chunked(7).forEach { week ->
Row(modifier = Modifier.fillMaxWidth()) {
week.forEach { dayModel ->
Box(modifier = Modifier.weight(1f)) {
PickerDayCell(
model = dayModel,
onSelect = { dayModel.day?.let(onDateSelect) },
)
}
}
repeat(7 - week.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.height(50.dp))
}
}
}
Expand Down Expand Up @@ -270,6 +305,32 @@ fun PickerDayCell(
}
}

@Preview(showBackground = true)
@Composable
private fun DatePickerContentPreview() {
DatePickerContent(
title = "발송 예정일",
currentYear = 2026,
currentMonth = 5,
selectedDate = LocalDate.of(2026, 5, 30),
onPrevMonth = {},
onNextMonth = {},
onDateSelect = {},
)
}

@Preview(showBackground = true)
@Composable
private fun CalendarGridContentPreview() {
CalendarGridContent(
currentYear = 2026,
currentMonth = 5,
selectedDate = LocalDate.of(2026, 5, 30),
onDateSelect = {},
modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp),
)
}

fun buildPickerDays(
year: Int,
month: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.afternote.feature.timeletter.data.api

import com.afternote.core.network.model.BaseResponse
import com.afternote.feature.timeletter.data.dto.ReceivedTimeLetterListResponseDto
import com.afternote.feature.timeletter.data.dto.ReceivedTimeLetterResponseDto
import retrofit2.http.GET
import retrofit2.http.Path

interface ReceiverTimeLetterApiService {
@GET("receiver-auth/time-letters")
suspend fun getReceivedTimeLetters(): BaseResponse<ReceivedTimeLetterListResponseDto>

@GET("receiver-auth/time-letters/{timeLetterReceiverId}")
suspend fun getReceivedTimeLetterDetail(
@Path("timeLetterReceiverId") timeLetterReceiverId: Long,
): BaseResponse<ReceivedTimeLetterResponseDto>
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.afternote.feature.timeletter.data.di

import android.content.Context
import com.afternote.feature.timeletter.data.api.ReceiverTimeLetterApiService
import com.afternote.feature.timeletter.data.api.TimeLetterApiService
import com.afternote.feature.timeletter.data.repositoryImpl.FileMetadataRepositoryImpl
import com.afternote.feature.timeletter.data.repositoryImpl.ReceiverTimeLetterRepositoryImpl
import com.afternote.feature.timeletter.data.repositoryImpl.TimeLetterRepositoryImpl
import com.afternote.feature.timeletter.domain.repository.FileMetadataRepository
import com.afternote.feature.timeletter.domain.repository.ReceiverTimeLetterRepository
import com.afternote.feature.timeletter.domain.repository.TimeLetterRepository
import dagger.Module
import dagger.Provides
Expand All @@ -31,4 +34,14 @@ object TimeLetterModule {
fun provideFileMetadataRepository(
@ApplicationContext context: Context,
): FileMetadataRepository = FileMetadataRepositoryImpl(context)

@Provides
@Singleton
fun provideReceiverTimeLetterApiService(retrofit: Retrofit): ReceiverTimeLetterApiService =
retrofit.create(ReceiverTimeLetterApiService::class.java)

@Provides
@Singleton
fun provideReceiverTimeLetterRepository(receiverTimeLetterApiService: ReceiverTimeLetterApiService): ReceiverTimeLetterRepository =
ReceiverTimeLetterRepositoryImpl(receiverTimeLetterApiService)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.afternote.feature.timeletter.data.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ReceivedTimeLetterResponseDto(
@SerialName("id") val id: Long,
@SerialName("timeLetterReceiverId") val timeLetterReceiverId: Long,
@SerialName("title") val title: String? = null,
@SerialName("blocks") val blocks: List<TimeLetterBlockResponseDto> = emptyList(),
@SerialName("sendAt") val sendAt: String? = null,
@SerialName("status") val status: TimeLetterStatusDto,
@SerialName("senderName") val senderName: String? = null,
@SerialName("deliveredAt") val deliveredAt: String? = null,
@SerialName("createdAt") val createdAt: String? = null,
@SerialName("isRead") val isRead: Boolean,
)

@Serializable
data class ReceivedTimeLetterListResponseDto(
@SerialName("timeLetters") val timeLetters: List<ReceivedTimeLetterResponseDto>,
@SerialName("totalCount") val totalCount: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.afternote.feature.timeletter.data.mapper

import com.afternote.feature.timeletter.data.dto.ReceivedTimeLetterListResponseDto
import com.afternote.feature.timeletter.data.dto.ReceivedTimeLetterResponseDto
import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetter
import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetterList

fun ReceivedTimeLetterResponseDto.toDomain(): ReceivedTimeLetter =
ReceivedTimeLetter(
id = id,
timeLetterReceiverId = timeLetterReceiverId,
title = title,
blocks = blocks.map { it.toDomain() },
sendAt = sendAt,
status = status.toDomain(),
senderName = senderName,
deliveredAt = deliveredAt,
createdAt = createdAt,
isRead = isRead,
)

fun ReceivedTimeLetterListResponseDto.toDomain(): ReceivedTimeLetterList =
ReceivedTimeLetterList(
timeLetters = timeLetters.map { it.toDomain() },
totalCount = totalCount,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.afternote.feature.timeletter.data.repositoryImpl

import com.afternote.core.network.model.requireData
import com.afternote.feature.timeletter.data.api.ReceiverTimeLetterApiService
import com.afternote.feature.timeletter.data.mapper.toDomain
import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetter
import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetterList
import com.afternote.feature.timeletter.domain.repository.ReceiverTimeLetterRepository
import javax.inject.Inject

class ReceiverTimeLetterRepositoryImpl
@Inject
constructor(
private val receiverTimeLetterApiService: ReceiverTimeLetterApiService,
) : ReceiverTimeLetterRepository {
override suspend fun getReceivedTimeLetters(): ReceivedTimeLetterList =
receiverTimeLetterApiService
.getReceivedTimeLetters()
.requireData()
.toDomain()

override suspend fun getReceivedTimeLetterDetail(timeLetterReceiverId: Long): ReceivedTimeLetter =
receiverTimeLetterApiService
.getReceivedTimeLetterDetail(timeLetterReceiverId)
.requireData()
.toDomain()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.afternote.feature.timeletter.domain.model

data class ReceivedTimeLetter(
val id: Long,
val timeLetterReceiverId: Long,
val title: String?,
val blocks: List<TimeLetterBlock>,
val sendAt: String?,
val status: TimeLetterStatus,
val senderName: String?,
val deliveredAt: String?,
val createdAt: String?,
val isRead: Boolean,
) {
val content: String?
get() = blocks.firstOrNull { it.blockType == TimeLetterBlockType.TEXT }?.textContent
}

data class ReceivedTimeLetterList(
val timeLetters: List<ReceivedTimeLetter>,
val totalCount: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.afternote.feature.timeletter.domain.repository

import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetter
import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetterList

interface ReceiverTimeLetterRepository {
suspend fun getReceivedTimeLetters(): ReceivedTimeLetterList

suspend fun getReceivedTimeLetterDetail(timeLetterReceiverId: Long): ReceivedTimeLetter
}
Loading
Loading