Skip to content
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
4aedce3
chore: 연락처 주기 설정 화면 구성에 필요한 이미지 추가
stopstone Sep 19, 2025
a9e0cfe
feat: 친구 연락 주기 화면 UI 구현
stopstone Sep 19, 2025
8b2bc90
refactor: 연락 주기 친구 목록 배경 여백 수정
stopstone Sep 23, 2025
16a07ab
feat: 친구 연락처 불러오기 UI 구현
stopstone Sep 23, 2025
d9ce4a6
chore: 불필요한 import문 제거
stopstone Sep 23, 2025
b4a1ca6
refactor: ContactCycleButtons 파라미터 추가
stopstone Sep 23, 2025
4e70734
refactor: `FriendListItem`의 친구 제거 버튼 로직 변경
stopstone Sep 23, 2025
3636490
feat: 친구 연락 주기 화면 Content 구현
stopstone Sep 23, 2025
b52b65e
feat: 연락 주기 설정 화면 구현
stopstone Sep 23, 2025
cdd10a2
fix: 친구 연락 주기 화면 UI 수정
stopstone Sep 23, 2025
8d36cbc
refactor: 친구 연락 주기 화면 아이콘 변경 및 색상 적용
stopstone Sep 23, 2025
d91e542
feat: 챙길 친구 화면 TopAppBar 동적 변경
stopstone Sep 23, 2025
33d6d21
refactor: 친구 연락 주기 화면 헤더 분리
stopstone Sep 23, 2025
66ea180
fix: 친구 이름이 길 때 레이아웃 깨짐 수정
stopstone Sep 23, 2025
0394344
Merge branch 'dev' into feat/friends-contact-cycle
stopstone Sep 23, 2025
168239e
refactor: 연락처 로드 화면 아이콘 변경
stopstone Sep 23, 2025
64bf08a
feat: 연락 주기 설정 바텀시트 UI 구현
stopstone Sep 23, 2025
b158dc3
fix: 주기설정 바텀시트 체크박스 해제 안되도록 수정
stopstone Sep 24, 2025
3c93cc0
feat: Date 확장 함수 추가
stopstone Sep 24, 2025
3614e90
feat: 다음 알림 날짜 계산 기능 추가
stopstone Sep 24, 2025
e25befc
refactor: 연락 주기 설정 UI 및 로직 개선
stopstone Sep 24, 2025
e8c0e11
feat: 친구 연락 주기 설정 기능 구현
stopstone Sep 24, 2025
69697ef
feat: 연락 주기 일괄 설정 기능 추가
stopstone Sep 24, 2025
848af59
fix: 주기 설정 바텀 시트 초기값 및 표시 텍스트 수정
stopstone Sep 24, 2025
eb2c982
refactor: 친구 연락 주기 설정 화면 UI 이벤트 및 상태 관리 개선
stopstone Sep 24, 2025
ba28f86
feat: 친구 연락처 주기 설정 화면 추가 및 NavGraph 연결
stopstone Sep 24, 2025
f623125
fix: 친구 선택 없을 시 '다음' 버튼 비활성화
stopstone Sep 24, 2025
66a61c4
feat: "나중에 하기" 버튼 클릭 즉시 홈 화면 이동
stopstone Sep 24, 2025
124e03f
feat: 모든 사람 주기 설정 후 버튼 활성화
stopstone Sep 24, 2025
27a65fa
feat: 친구 연락 주기 설정 화면에서 선택한 연락처 해제 기능 추가
stopstone Sep 24, 2025
6248747
Merge dev branch into feat/friends-contact-cycle
stopstone Sep 30, 2025
f65dccc
chore: 친구 목록 로딩 아이콘 변경
stopstone Oct 1, 2025
3454f58
feat: 연락처 권한 요청 및 화면 이동 로직 추가
stopstone Oct 1, 2025
fdd70dc
feat: 친구 연락 주기 설정 화면에 연락처 선택 기능 추가
stopstone Oct 1, 2025
ca6f100
refactor: 연락처 로딩 성능 개선 및 UI 수정
stopstone Oct 1, 2025
fc5ab12
feat: 챙길사람 화면에 불러온 연락처 반영
stopstone Oct 1, 2025
ed86d0c
refactor: 개별 연락처 주기 설정 기능 구현 및 로직 개선
stopstone Oct 1, 2025
37f144f
feat: 오늘 요일 및 리마인더 주기 변환 함수 추가
stopstone Oct 2, 2025
e41d61d
feat: 친구 연락 주기 설정 완료 시 로딩 처리 추가
stopstone Oct 2, 2025
029f446
feat: 친구 초기 설정 API 모델 및 매퍼 추가
stopstone Oct 2, 2025
fd435e0
feat: 연락처 기반 친구 추가 API 연동
stopstone Oct 2, 2025
6c2f47f
feat: 친구 초기 설정 ViewModel 및 UI 이벤트 구현
stopstone Oct 2, 2025
0407c69
refactor: 로그인 후 친구 연락 주기 설정 화면으로 이동
stopstone Oct 2, 2025
c4b137f
refactor: 친구 연락 주기 설정 화면 문자열 리소스 적용
stopstone Oct 2, 2025
cf30e6c
refactor: 연락처 정렬 스레드 IO 디스패처로 변경
stopstone Oct 2, 2025
90da47f
feat: 친구 추가 및 연락 주기 설정 화면 이동 로직 구현
stopstone Oct 2, 2025
d05063c
feat: 공용 다이얼로그 컴포저블 추가
stopstone Oct 2, 2025
f82cca6
feat: 연락처 권한 거부 시 설정 이동 다이얼로그 표시
stopstone Oct 2, 2025
b45e8e4
refactor: 주소록 권한 상태를 ViewModel에서 관리하도록 변경
stopstone Oct 2, 2025
fdbb7d1
chore: libphonenumber-android 의존성 추가
stopstone Oct 3, 2025
a3fc9ed
refactor: 전화번호 형식 변환 로직 개선
stopstone Oct 3, 2025
ec02439
fix: 전화번호 없는 연락처 처리 시 발생하는 오류 방지
stopstone Oct 3, 2025
ea72f99
refactor: 서버 응답 Enum 변환 로직 안정성 강화
stopstone Oct 3, 2025
3ed57a5
fix: 6개월 후 날짜 계산 로직 수정
stopstone Oct 3, 2025
18ae79c
fix: ContactScreen 무한루프 문제 해결
stopstone Oct 4, 2025
5c7fff2
refactor: 친구 설정 실패 시 스낵바로 에러를 표시하도록 변경
stopstone Oct 4, 2025
09c880a
refactor: 로그인 제공자 타입 조회 로직에 Enum 사용
stopstone Oct 4, 2025
727371c
refactor: 유효하지 않은 요일 값에 대해 예외 발생시키도록 변경
stopstone Oct 4, 2025
611784b
refactor: ViewModel 이벤트 처리 방식을 직접 호출로 변경
stopstone Oct 4, 2025
8a4db8a
refactor: onPermissionDenied 콜백을 non-nullable로 변경
stopstone Oct 6, 2025
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
3 changes: 3 additions & 0 deletions Near/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ dependencies {

// Splash Screen API
implementation(libs.androidx.core.splashscreen)

// libphonenumber-android for phone number formatting
implementation("io.michaelrocks:libphonenumber-android:9.0.15")
}

fun getProperty(propertyKey: String): String = gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
4 changes: 4 additions & 0 deletions Near/app/src/main/java/com/alarmy/near/NearApplication.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.alarmy.near

import android.app.Application
import com.alarmy.near.utils.PhoneNumberFormatter
import com.kakao.sdk.common.KakaoSdk
import dagger.hilt.android.HiltAndroidApp

Expand All @@ -12,5 +13,8 @@ class NearApplication : Application() {

// 카카오 SDK 초기화
KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)

// PhoneNumberFormatter 초기화
PhoneNumberFormatter.initialize(this)
}
}
104 changes: 104 additions & 0 deletions Near/app/src/main/java/com/alarmy/near/data/mapper/FriendInitMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.alarmy.near.data.mapper

import com.alarmy.near.model.Anniversary
import com.alarmy.near.model.ContactFrequency
import com.alarmy.near.model.DayOfWeek
import com.alarmy.near.model.Friend
import com.alarmy.near.model.Relation
import com.alarmy.near.model.ReminderInterval
import com.alarmy.near.network.request.ContactFrequencyInitRequest
import com.alarmy.near.network.request.FriendInitItemRequest
import com.alarmy.near.network.response.AnniversaryInitEntity
import com.alarmy.near.network.response.ContactFrequencyInitEntity
import com.alarmy.near.network.response.FriendInitItemEntity
import com.alarmy.near.presentation.feature.friendcontactcycle.model.FriendContactUIModel
import com.alarmy.near.utils.PhoneNumberFormatter
import com.alarmy.near.utils.extensions.DateExtension
import com.alarmy.near.utils.logger.NearLog

/**
* PhoneNumberFormatter를 사용하여 전화번호를 포맷팅하는 함수
* - 다양한 전화번호 형식 지원 (휴대폰, 지역번호 등)
* - 한국 전화번호 형식으로 통일 (010-0000-0000, 02-0000-0000 등)
* - 잘못된 형식의 경우 원본 반환
*/
private fun String.formatPhoneNumber(): String = PhoneNumberFormatter.formatPhoneNumber(this)

/**
* UI 모델을 서버 요청 모델로 변환
*/
fun FriendContactUIModel.toFriendInitItemRequest(providerType: String): FriendInitItemRequest =
FriendInitItemRequest(
name = name,
phone = phones.firstOrNull()?.formatPhoneNumber() ?: "",
memo = memo,
birthDay = birthDay,
source = providerType,
contactFrequency = createContactFrequencyRequest(reminderInterval!!),
imageUploadRequest = null,
anniversary = null,
relation = "FRIEND", // 기본값으로 FRIEND 설정
)

/**
* ContactFrequencyInitEntity를 모델로 변환
*/
fun ContactFrequencyInitEntity.toModel(): ContactFrequency =
ContactFrequency(
reminderInterval = contactWeek.toReminderInterval(),
dayOfWeek = dayOfWeek.toDayOfWeek(),
)

/**
* AnniversaryInitEntity를 모델로 변환
*/
fun AnniversaryInitEntity.toModel(): Anniversary =
Anniversary(
id = id,
title = title,
date = date,
)

/**
* 서버 응답을 모델로 변환
*/
fun FriendInitItemEntity.toModel(): Friend =
Friend(
friendId = friendId,
name = name,
phone = phone,
memo = memo,
birthday = null,
imageUrl = preSignedImageUrl,
relation = source.toRelation(),
contactFrequency = contactFrequency.toModel(),
anniversaryList = anniversary?.let { listOf(it.toModel()) } ?: emptyList(),
lastContactAt = nextContactAt,
)

/**
* ReminderInterval을 ContactFrequencyInitRequest로 변환
*/
private fun createContactFrequencyRequest(reminderInterval: ReminderInterval): ContactFrequencyInitRequest =
ContactFrequencyInitRequest(
contactWeek = DateExtension.toContactWeekString(reminderInterval),
dayOfWeek = DateExtension.getTodayDayOfWeekInEnglish(),
)

private fun String.toReminderInterval(): ReminderInterval =
runCatching { ReminderInterval.valueOf(this) }
.onFailure { exception ->
NearLog.w("잘못된 연락 주기 값: '$this', 기본값(EVERY_WEEK) 사용")
}.getOrDefault(ReminderInterval.EVERY_WEEK)

private fun String.toDayOfWeek(): DayOfWeek =
runCatching { DayOfWeek.valueOf(this) }
.onFailure { exception ->
NearLog.w("잘못된 요일 값: '$this', 기본값(MONDAY) 사용")
}.getOrDefault(DayOfWeek.MONDAY)

private fun String.toRelation(): Relation =
runCatching { Relation.valueOf(this) }
.onFailure { exception ->
NearLog.w("잘못된 관계 값: '$this', 기본값(FRIEND) 사용")
}.getOrDefault(Relation.FRIEND)
34 changes: 31 additions & 3 deletions Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import com.alarmy.near.network.request.FriendRequest
import com.alarmy.near.network.response.AnniversaryEntity
import com.alarmy.near.network.response.ContactFrequencyEntity
import com.alarmy.near.network.response.FriendEntity
import com.alarmy.near.utils.logger.NearLog

fun FriendEntity.toModel(): Friend =
Friend(
friendId = friendId,
imageUrl = imageUrl,
relation = Relation.valueOf(relation),
relation = relation.toRelation(),
name = name,
contactFrequency = contactFrequency.toModel(),
birthday = birthday,
Expand All @@ -29,8 +30,8 @@ fun FriendEntity.toModel(): Friend =

fun ContactFrequencyEntity.toModel(): ContactFrequency =
ContactFrequency(
reminderInterval = ReminderInterval.valueOf(contactWeek),
dayOfWeek = DayOfWeek.valueOf(dayOfWeek),
reminderInterval = contactWeek.toReminderInterval(),
dayOfWeek = dayOfWeek.toDayOfWeek(),
)

fun AnniversaryEntity.toModel(): Anniversary =
Expand Down Expand Up @@ -63,3 +64,30 @@ fun Anniversary.toRequest(): AnniversaryRequest =
title = title,
date = date,
)

/**
* 안전한 ReminderInterval 변환 (로깅 포함)
*/
private fun String.toReminderInterval(): ReminderInterval =
runCatching { ReminderInterval.valueOf(this) }
.onFailure { exception ->
NearLog.w("잘못된 연락 주기 값: '$this', 기본값(EVERY_WEEK) 사용")
}.getOrDefault(ReminderInterval.EVERY_WEEK)

/**
* 안전한 DayOfWeek 변환 (로깅 포함)
*/
private fun String.toDayOfWeek(): DayOfWeek =
runCatching { DayOfWeek.valueOf(this) }
.onFailure { exception ->
NearLog.w("잘못된 요일 값: '$this', 기본값(MONDAY) 사용")
}.getOrDefault(DayOfWeek.MONDAY)

/**
* 안전한 Relation 변환 (로깅 포함)
*/
private fun String.toRelation(): Relation =
runCatching { Relation.valueOf(this) }
.onFailure { exception ->
NearLog.w("잘못된 관계 값: '$this', 기본값(FRIEND) 사용")
}.getOrDefault(Relation.FRIEND)
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.alarmy.near.data.repository

import com.alarmy.near.data.mapper.toFriendInitItemRequest
import com.alarmy.near.data.mapper.toModel
import com.alarmy.near.data.mapper.toRequest
import com.alarmy.near.model.Friend
import com.alarmy.near.model.FriendRecord
import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.model.monthly.MonthlyFriend
import com.alarmy.near.network.request.FriendInitRequest
import com.alarmy.near.network.response.FriendInitItemEntity
import com.alarmy.near.network.service.FriendService
import com.alarmy.near.presentation.feature.friendcontactcycle.model.FriendContactUIModel
import com.alarmy.near.utils.extensions.apiCallFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
Expand Down Expand Up @@ -63,4 +68,22 @@ class DefaultFriendRepository
val response = friendService.recordContact(friendId)
emit(response.message) // CommonMessageEntity.message 라고 가정
}

override fun initFriends(
contacts: List<FriendContactUIModel>,
providerType: String,
): Flow<List<FriendInitItemEntity>> =
apiCallFlow {
// UI 모델을 Data 모델로 변환
val friendInitRequest =
FriendInitRequest(
friendList =
contacts
.filter { it.reminderInterval != null }
.map { it.toFriendInitItemRequest(providerType) },
)

// 서버 요청 및 응답 반환
friendService.initFriends(friendInitRequest).friendList
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.alarmy.near.data.repository

import com.alarmy.near.model.Friend
import com.alarmy.near.model.FriendRecord
import com.alarmy.near.network.response.FriendInitItemEntity
import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.model.monthly.MonthlyFriend
import com.alarmy.near.presentation.feature.friendcontactcycle.model.FriendContactUIModel
import kotlinx.coroutines.flow.Flow

interface FriendRepository {
Expand All @@ -23,4 +25,6 @@ interface FriendRepository {
fun fetchFriendRecord(friendId: String): Flow<List<FriendRecord>>

fun recordContact(friendId: String): Flow<String>

fun initFriends(contacts: List<FriendContactUIModel>, providerType: String): Flow<List<FriendInitItemEntity>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.alarmy.near.network.request

import kotlinx.serialization.Serializable

/**
* 친구 초기 설정 API 요청 데이터 모델
*/
@Serializable
data class FriendInitRequest(
val friendList: List<FriendInitItemRequest>,
)

/**
* 친구 초기 설정 개별 아이템 요청 데이터 모델
*/
@Serializable
data class FriendInitItemRequest(
val name: String,
val phone: String,
val memo: String? = null,
val birthDay: String? = null,
val source: String,
val contactFrequency: ContactFrequencyInitRequest,
val imageUploadRequest: ImageUploadRequest? = null,
val anniversary: AnniversaryInitRequest? = null,
val relation: String? = null,
)

/**
* 연락처 주기 설정 요청 데이터 모델
*/
@Serializable
data class ContactFrequencyInitRequest(
val contactWeek: String, // EVERY_WEEK, EVERY_TWO_WEEKS, EVERY_MONTH
val dayOfWeek: String, // MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
)

/**
* 이미지 업로드 요청 데이터 모델
*/
@Serializable
data class ImageUploadRequest(
val fileName: String? = null,
val contentType: String? = null,
val fileSize: Int? = null,
val category: String? = null,
)

/**
* 기념일 요청 데이터 모델
*/
@Serializable
data class AnniversaryInitRequest(
val title: String,
val date: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.alarmy.near.network.response

import kotlinx.serialization.Serializable

/**
* 친구 초기 설정 API 응답 데이터
*/
@Serializable
data class FriendInitEntity(
val friendList: List<FriendInitItemEntity>,
)

@Serializable
data class FriendInitItemEntity(
val friendId: String,
val name: String,
val source: String,
val contactFrequency: ContactFrequencyInitEntity,
val nextContactAt: String,
val phone: String,
val preSignedImageUrl: String? = null,
val fileName: String? = null,
val memo: String? = null,
val anniversary: AnniversaryInitEntity? = null,
)

@Serializable
data class ContactFrequencyInitEntity(
val contactWeek: String,
val dayOfWeek: String,
)

@Serializable
data class AnniversaryInitEntity(
val id: Int,
val title: String,
val date: String,
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.alarmy.near.network.service

import com.alarmy.near.network.request.FriendInitRequest
import com.alarmy.near.network.request.FriendRequest
import com.alarmy.near.network.response.CommonMessageEntity
import com.alarmy.near.network.response.FriendEntity
import com.alarmy.near.network.response.FriendInitEntity
import com.alarmy.near.network.response.FriendRecordEntity
import com.alarmy.near.network.response.FriendSummaryEntity
import com.alarmy.near.network.response.MonthlyFriendEntity
Expand Down Expand Up @@ -45,4 +47,9 @@ interface FriendService {
suspend fun recordContact(
@Path("friendId") friendId: String,
): CommonMessageEntity

@POST("/friend/init")
suspend fun initFriends(
@Body friendInitRequest: FriendInitRequest,
): FriendInitEntity
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ fun ContactRoute(

when (uiState) {
is ContactUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier =
Modifier
.fillMaxSize()
.background(NearTheme.colors.WHITE_FFFFFF),
) {
CircularProgressIndicator(
color = NearTheme.colors.BLUE01_5AA2E9,
modifier = Modifier.align(Alignment.Center),
Expand All @@ -83,7 +88,10 @@ fun ContactRoute(

is ContactUiState.Error -> {
Box(modifier = Modifier.fillMaxSize()) {
Text(modifier = Modifier.align(Alignment.Center), text = stringResource(R.string.contact_load_error))
Text(
modifier = Modifier.align(Alignment.Center),
text = stringResource(R.string.contact_load_error),
)
}
}

Expand Down
Loading