Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PC-256] 이용약관 페이지 UI 구현 #25

Merged
merged 26 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
90b90e0
[PC-256] RegistrationScreen UI 구현
tgyuuAn Jan 4, 2025
60bb46c
[PC-256] Check Component 구현
tgyuuAn Jan 4, 2025
f906476
[PC-256] RegistrationScreen UI 구현
tgyuuAn Jan 4, 2025
a0fa57a
[PC-256] 이용약관 동의 로직 추가
tgyuuAn Jan 4, 2025
2a1fb55
[PC-256] 약관을 서버에서 동적으로 불러오도록 변경
tgyuuAn Jan 5, 2025
bda08b0
[PC-256] 약관 데이터를 불러올 수 있도록 변경
tgyuuAn Jan 5, 2025
6c028dd
[PC-256] MainViewModel의 init에서 약관 데이터를 호출
tgyuuAn Jan 5, 2025
082bc81
[PC-256] ProxyMan 설정 및 서버에서 약관 정보 받아오는 것 확인
tgyuuAn Jan 5, 2025
77a7e84
[PC-256] LocalDataBase 모듈 추가 및 Room 설정
tgyuuAn Jan 5, 2025
c8b7b22
[PC-256] TermDao 추가 및 Query 추가
tgyuuAn Jan 5, 2025
3098f31
[PC-256] ApiResponse를 제네릭으로 관리하도록 변경 및 CallAdapter에서 status값을 이용하여 응답…
tgyuuAn Jan 5, 2025
8ee6aa2
[PC-256] 약관을 불러왔을경우, 이를 Room에 저장하도록 변경
tgyuuAn Jan 5, 2025
3f6b1ab
[PC-256] 약관을 로컬에서 불러오는 로직 추가
tgyuuAn Jan 5, 2025
a4845e4
[PC-256] ResponseDTO를 제네릭을 이용한 BaseResponse로 받을 수 있도록 변경
tgyuuAn Jan 5, 2025
8143032
[PC-256] ApiResponse<T> 확장 함수 정의
tgyuuAn Jan 5, 2025
62759f9
[PC-256] AndroidTest를 할 수 있도록 TestRunner 추가
tgyuuAn Jan 5, 2025
b34d8c2
[PC-256] Database 모듈 테스트 코드 추가
tgyuuAn Jan 5, 2025
854bdb0
[PC-256] TimeUtil 테스트 코드 추가
tgyuuAn Jan 5, 2025
db3ca77
[PC-256] TermsRepositoryImpl 테스트 코드 추가
tgyuuAn Jan 5, 2025
526a328
[PC-256] LocalDateBase Entity Nullalbe -> NonNullable로 변경
tgyuuAn Jan 5, 2025
9af33c8
[PC-256] agreeAllTerms -> allTermsAgreed 로 변경 및 navigate를 SideEffect로 위임
tgyuuAn Jan 7, 2025
f894dff
[PC-256] showXXX -> XXXEnabled로 네이밍 변경
tgyuuAn Jan 7, 2025
79afd27
[PC-256] getResult -> unwrapData로 네이밍 변경
tgyuuAn Jan 7, 2025
5074087
[PC-256] 데이터를 bypass시키기만 하던 UseCase 제거
tgyuuAn Jan 7, 2025
f14ff20
[PC-256] TermEntity, termId -> id로 변경
tgyuuAn Jan 7, 2025
2d236cc
[PC-256] 불필요한 Intent 제거
tgyuuAn Jan 7, 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: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ dependencies {
implementation(libs.kakao.user)

implementation(projects.presentation)
}
implementation(projects.core.data)
}
5 changes: 4 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Piece"
Expand All @@ -32,10 +33,12 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:host="oauth"
<data
android:host="oauth"
android:scheme="kakao${KAKAO_APP_KEY}" />
</intent-filter>
</activity>
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<!-- Trust user added CAs while debuggable only -->
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>

<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ internal fun Project.configureJUnitAndroid() {
unitTests.all { it.useJUnitPlatform() }
}

defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

val libs = extensions.libs
dependencies {
"androidTestImplementation"(libs.findLibrary("androidx.test.ext").get())
Expand Down
1 change: 1 addition & 0 deletions core/common/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
8 changes: 8 additions & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id("piece.kotlin.library")
id("piece.kotlin.hilt")
}

dependencies {
implementation(libs.coroutines.core)
}
18 changes: 18 additions & 0 deletions core/common/src/main/java/com/puzzle/common/TimeUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.puzzle.common

import java.time.LocalDateTime
import java.time.format.DateTimeParseException

/**
* String?을 LocalDateTime으로 변환합니다.
*
* - 문자열이 null인 경우, [LocalDateTime.MIN]을 반환합니다.
* - 잘못된 형식으로 인해 파싱에 실패할 경우, [LocalDateTime.MIN]을 반환합니다.
*/
fun String?.parseDateTime(): LocalDateTime {
return try {
this?.let { LocalDateTime.parse(it) } ?: LocalDateTime.MIN
} catch (e: DateTimeParseException) {
LocalDateTime.MIN
}
}
Comment on lines +6 to +18
Copy link
Member Author

Choose a reason for hiding this comment

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

일단 DefaultValue로 LocalDateTime.MIN을 반환하기로 하였는데,

에러를 바로 던져버릴 지 고민이에요...!

Comment on lines +12 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

  • p4 : try-catch도 좋지만, 코틀린의 runcatching을 많이 활용해보면 좋을 것 같은데 어떻게 생각하시나요?
fun String?.parseDateTime(): LocalDateTime {
    return this?.let {
        runCatching { LocalDateTime.parse(it) }
            .getOrDefault(LocalDateTime.MIN)
    } ?: LocalDateTime.MIN
}

Copy link
Contributor

Choose a reason for hiding this comment

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

  • p4 : String과 같은 범용 클래스를 global하게 확장 함수로 구현하는 대신, 도메인 모델인 Term 데이터 클래스 내부에 private한 확장 함수로 만들어 데이터 변환 시 사용하는 방법에 대해 어떻게 생각하시나요?

Copy link
Contributor

Choose a reason for hiding this comment

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

LocalDateTime.MIN 은 뭐가 나오나요?! 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

  • p4 : try-catch도 좋지만, 코틀린의 runcatching을 많이 활용해보면 좋을 것 같은데 어떻게 생각하시나요?
fun String?.parseDateTime(): LocalDateTime {
    return this?.let {
        runCatching { LocalDateTime.parse(it) }
            .getOrDefault(LocalDateTime.MIN)
    } ?: LocalDateTime.MIN
}

try-catch나 runCatching으로 묶는 것 모두 에러를 잡기위한 코드인데,

Result객체를 사용하는 이유는 에러를 잡고 �이 책임을 다른 클래스에 위임하기 위해서 사용하는 것이라고 생각해요.

하지만, 위 코드는 해당 함수에서 에러를 잡고 바로 에러를 핸들링하기 때문에 오히려 코드 수도 늘어나고 불필요한 것 같습니다.

runCatching을 사용하면 try-catch보다 직관적이지 않고 코드수도 늘어난다고 생각해요.

어떻게 생각하시나요?!

관련 레퍼런스 하나 드릴게요! 에러 핸들링을 다른 클래스에게 위임하기

Copy link
Member Author

Choose a reason for hiding this comment

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

  • p4 : String과 같은 범용 클래스를 global하게 확장 함수로 구현하는 대신, 도메인 모델인 Term 데이터 클래스 내부에 private한 확장 함수로 만들어 데이터 변환 시 사용하는 방법에 대해 어떻게 생각하시나요?

처음 이렇게 설계를 했지만,

추후에 Room을 사용하는 Database 모듈에서 해당 로직이 가짜 중복이 아닌 정말 진짜 중복으로 재사용 됨을 발견하였습니다.

Network 모듈에서는 ResponseDTO -> Domain Model로 변환,

Database 모듈에서는 RoomEntity -> Domain Model로 변환




그래서 진짜 중복이 발생한 두 코드를 위해 각 모듈에 각각 배치하는 것 보다 어차피 추후에 만들어진 common 모듈을 만들고 거기다가 넣는 것이 좋겠다고 판단하였습니다!

어떤가요?!

Copy link
Member Author

@tgyuuAn tgyuuAn Jan 7, 2025

Choose a reason for hiding this comment

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

LocalDateTime.MIN 은 뭐가 나오나요?! 🤔

아래�사진과 같은 시간이 나와요!

image

이는 ResponsDTO -> Domain Model로 파싱할 때 처럼 파싱에 실패했을 때 UNKNOWN 과 같은 효과를 주는 것 처럼 설계하였어요!

이는 Repository에서 DomainModel로 매핑할 때 필터링 되어야합니다!!

Copy link
Contributor

Choose a reason for hiding this comment

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

  • 다시 보니 runcatching 보단 try-catch가 가독성이 좋아보이는 것 같아요! 👍
  • 아하 그렇군요!! 그럼 질문이 하나 있습니다! 그럼 common은 network, database 모듈에서만 접근할 수 있는 건가요?? 아니면 다른 모듈들에서 접근할 수 있게 되는건가요??
  • 허허 그렇군요!! 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

  • 아하 그렇군요!! 그럼 질문이 하나 있습니다! 그럼 common은 network, database 모듈에서만 접근할 수 있는 건가요?? 아니면 다른 모듈들에서 접근할 수 있게 되는건가요??

common 모듈은 프레임워크 의존성이 없는 모듈이라서 어떤 모듈이라도 해당 모듈에 접근 가능하도록 설계하였습니다!

47 changes: 47 additions & 0 deletions core/common/src/test/kotlin/com/puzzle/common/TimeUtilTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.puzzle.common

import org.junit.Assert.assertEquals
import org.junit.jupiter.api.Test
import java.time.LocalDateTime

class TimeUtilTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

허허 테스트 코드의 시작을...!!!! 👍 👍


@Test
fun `올바른 형식의 문자열을 LocalDateTime으로 변환할 수 있다`() {
// given
val dateTimeString = "2024-06-01T00:00:00"
val expected = LocalDateTime.parse(dateTimeString)

// when
val actual = dateTimeString.parseDateTime()

// then
assertEquals(expected, actual)
}

@Test
fun `null 값을 파싱하려고 할 경우 LocalDateTime_MIN을 반환한다`() {
// given
val nullString: String? = null
val expected = LocalDateTime.MIN

// when
val actual = nullString.parseDateTime()

// then
assertEquals(expected, actual)
}

@Test
fun `형식에 맞지 않은 문자열을 파싱하려고 할 경우 LocalDateTime_MIN을 반환한다`() {
// given
val invalidString = "invalid-date-format"
val expected = LocalDateTime.MIN

// when
val actual = invalidString.parseDateTime()

// then
assertEquals(expected, actual)
}
}
3 changes: 2 additions & 1 deletion core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ android {
dependencies {
implementation(projects.core.domain)
implementation(projects.core.network)
}
implementation(projects.core.database)
}

This file was deleted.

10 changes: 9 additions & 1 deletion core/data/src/main/java/com/puzzle/data/di/DataModule.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.puzzle.data.di

import com.puzzle.data.repository.AuthRepositoryImpl
import com.puzzle.data.repository.TermsRepositoryImpl
import com.puzzle.domain.repository.AuthRepository
import com.puzzle.domain.repository.TermsRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -18,4 +20,10 @@ abstract class DataModule {
abstract fun bindsAuthRepository(
authRepositoryImpl: AuthRepositoryImpl,
): AuthRepository
}

@Binds
@Singleton
abstract fun bindsTermsRepository(
termsRepositoryImpl: TermsRepositoryImpl,
): TermsRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.puzzle.data.repository

import com.puzzle.database.model.terms.TermEntity
import com.puzzle.database.source.term.LocalTermDataSource
import com.puzzle.domain.model.terms.Term
import com.puzzle.domain.repository.TermsRepository
import com.puzzle.network.model.UNKNOWN_INT
import com.puzzle.network.source.TermDataSource
import javax.inject.Inject

class TermsRepositoryImpl @Inject constructor(
private val termDataSource: TermDataSource,
private val localTermDataSource: LocalTermDataSource,
) : TermsRepository {
override suspend fun loadTerms(): Result<Unit> = runCatching {
val terms = termDataSource.loadTerms()
.getOrThrow()
.toDomain()
.filter { it.termId != UNKNOWN_INT }

val termsEntity = terms.map {
TermEntity(
id = it.termId,
title = it.title,
content = it.content,
required = it.required,
startDate = it.startDate,
)
}

localTermDataSource.clearAndInsertTerms(termsEntity)
}

override suspend fun getTerms(): Result<List<Term>> = runCatching {
localTermDataSource.getTerms()
.map { it.toDomain() }
Comment on lines +34 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

  • p3 : get 보다 더 스코프한 표현이면 좋을 것 같은데 어떻게 생각하시나요?!

Copy link
Member Author

Choose a reason for hiding this comment

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

  • p3 : get 보다 더 스코프한 표현이면 좋을 것 같은데 어떻게 생각하시나요?!

헉, 스코프하다라는 것이 어떤 뜻인가요?!

좀 더 광범위 ...? 음.. 해당 함수는 약관을 불러오는 함수를 뜻하는데요,

혹시 더 직관적이고 좋은 뜻이 있을까요 ?!

Copy link
Contributor

Choose a reason for hiding this comment

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

앗 죄송합니다 표현이 명확하지 않았던 것 같아요! 제 말은 조금 더 좁은 범위, 의미를 가진 단어이면 좋을 것 같다는 뜻이었어요!!

Copy link
Contributor

Choose a reason for hiding this comment

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

저는 개인적으로 local에서 데이터를 가져올 경우엔 retrieve라는 표현을 자주 사용하는 것 같아요!
옥스포드사전을 보시면 retrieve의 사전적 정의도 그렇고,
grep에서 해당 단어를 조회해 보면 google api 문서나 다른 여러 문서에서 retrieve를 뭔가 저장되어 있는 데이터를 불러올 때 쓰는 것 같아요! 뭐 비슷한 의미를 가진 단어들이 많아 네이밍에 어려움을 겪을 때는 주로 gpt, grep, 옥스포드 사전을 참고하는 편입니다....!!!!

Copy link
Member Author

Choose a reason for hiding this comment

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

오오 새로 배웁니다! 다음 PR에서 retrieve로 바꿔놓겠습니다!

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.puzzle.data.repository

import com.puzzle.database.source.term.LocalTermDataSource
import com.puzzle.network.model.UNKNOWN_INT
import com.puzzle.network.model.terms.LoadTermsResponse
import com.puzzle.network.model.terms.TermResponse
import com.puzzle.network.source.TermDataSource
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class TermsRepositoryImplTest {

private lateinit var termDataSource: TermDataSource
private lateinit var localTermDataSource: LocalTermDataSource
private lateinit var termsRepository: TermsRepositoryImpl

@BeforeEach
fun setUp() {
termDataSource = mockk()
localTermDataSource = mockk()
termsRepository = TermsRepositoryImpl(termDataSource, localTermDataSource)
}

@Test
fun `약관을 새로 갱신할 경우 id값이 올바르게 내려오지 않은 약관은 무시한다`() = runTest {
// given
val invalidTerm = TermResponse(
termId = UNKNOWN_INT,
title = "Invalid",
content = "Invalid Content",
required = false,
startDate = "2024-06-01T00:00:00",
)
val validTerm = TermResponse(
termId = 1,
title = "Valid",
content = "Valid Content",
required = true,
startDate = "2024-06-01T00:00:00",
)

coEvery { termDataSource.loadTerms() } returns
Result.success(LoadTermsResponse(listOf(invalidTerm, validTerm)))
coEvery { localTermDataSource.clearAndInsertTerms(any()) } just Runs

// when
val result = termsRepository.loadTerms()

// then
assertTrue(result.isSuccess)
coVerify(exactly = 1) {
localTermDataSource.clearAndInsertTerms(
match {
it.size == 1 && it.first().id == validTerm.termId
}
)
}
}

@Test
fun `갱신한 데이터는 로컬 데이터베이스에 저장한다`() = runTest {
// given
val validTerms = listOf(
TermResponse(
termId = 1,
title = "Valid1",
content = "Content1",
required = true,
startDate = "2024-06-01T00:00:00"
),
TermResponse(
termId = 2,
title = "Valid2",
content = "Content2",
required = false,
startDate = "2024-06-01T00:00:00"
)
)

coEvery { termDataSource.loadTerms() } returns Result.success(LoadTermsResponse(validTerms))
coEvery { localTermDataSource.clearAndInsertTerms(any()) } just Runs

// when
termsRepository.loadTerms()

// then
coVerify(exactly = 1) {
localTermDataSource.clearAndInsertTerms(
match {
it.size == validTerms.size && it.all { entity ->
validTerms.any { term ->
term.termId == entity.id && term.title == entity.title
}
}
}
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

허허 꼼꼼한 테스트 작성 👍 👍

}
1 change: 1 addition & 0 deletions core/database/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
16 changes: 16 additions & 0 deletions core/database/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
id("piece.android.library")
id("piece.android.hilt")
}

android {
namespace = "com.puzzle.database"
}

dependencies {
implementation(projects.core.domain)
implementation(projects.core.common)

implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
}
Empty file.
21 changes: 21 additions & 0 deletions core/database/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Loading
Loading