diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml new file mode 100644 index 000000000..20674a023 --- /dev/null +++ b/.github/workflows/backend-test.yml @@ -0,0 +1,44 @@ +name: Kerdy Backend Build Test + +on: + pull_request: + types: [ opened, synchronize, reopened ] + branches: + - backend-main + +jobs: + build: + name: Build with Gradle + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0.28 + env: + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: 1234 + MYSQL_DATABASE: kerdy + ports: + - 13306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: backend-main + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: 'temurin' + + - name: cd backend directory + run: cd backend/emm-sale + + - name: Give permission for Gradle + run: chmod +x gradlew + + - name: Build with Gradle + run: gradlew clean build diff --git a/.gradle/8.0.2/checksums/checksums.lock b/.gradle/8.0.2/checksums/checksums.lock new file mode 100644 index 000000000..40dbc7f94 Binary files /dev/null and b/.gradle/8.0.2/checksums/checksums.lock differ diff --git a/.gradle/8.0.2/checksums/md5-checksums.bin b/.gradle/8.0.2/checksums/md5-checksums.bin new file mode 100644 index 000000000..f97566f9d Binary files /dev/null and b/.gradle/8.0.2/checksums/md5-checksums.bin differ diff --git a/.gradle/8.0.2/checksums/sha1-checksums.bin b/.gradle/8.0.2/checksums/sha1-checksums.bin new file mode 100644 index 000000000..7e2eb9696 Binary files /dev/null and b/.gradle/8.0.2/checksums/sha1-checksums.bin differ diff --git a/.gradle/8.0.2/dependencies-accessors/dependencies-accessors.lock b/.gradle/8.0.2/dependencies-accessors/dependencies-accessors.lock new file mode 100644 index 000000000..9af762783 Binary files /dev/null and b/.gradle/8.0.2/dependencies-accessors/dependencies-accessors.lock differ diff --git a/.gradle/8.0.2/dependencies-accessors/gc.properties b/.gradle/8.0.2/dependencies-accessors/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/.gradle/8.0.2/executionHistory/executionHistory.lock b/.gradle/8.0.2/executionHistory/executionHistory.lock new file mode 100644 index 000000000..67fe58ca7 Binary files /dev/null and b/.gradle/8.0.2/executionHistory/executionHistory.lock differ diff --git a/.gradle/8.0.2/fileChanges/last-build.bin b/.gradle/8.0.2/fileChanges/last-build.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/.gradle/8.0.2/fileChanges/last-build.bin differ diff --git a/.gradle/8.0.2/fileHashes/fileHashes.lock b/.gradle/8.0.2/fileHashes/fileHashes.lock new file mode 100644 index 000000000..7f135cf46 Binary files /dev/null and b/.gradle/8.0.2/fileHashes/fileHashes.lock differ diff --git a/.gradle/8.0.2/gc.properties b/.gradle/8.0.2/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 000000000..b81d38f7c Binary files /dev/null and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 000000000..921534606 --- /dev/null +++ b/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Wed Jul 19 21:44:15 KST 2023 +gradle.version=8.0.2 diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe new file mode 100644 index 000000000..918f1abaa Binary files /dev/null and b/.gradle/file-system.probe differ diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/android/2023-emmsale/app/build.gradle.kts b/android/2023-emmsale/app/build.gradle.kts index 6bf1424fc..fb08f9bdf 100644 --- a/android/2023-emmsale/app/build.gradle.kts +++ b/android/2023-emmsale/app/build.gradle.kts @@ -3,7 +3,9 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties plugins { id("org.jetbrains.kotlin.android") id("com.android.application") version "8.0.2" + id("com.google.gms.google-services") kotlin("plugin.serialization") version "1.8.21" + id("kotlin-kapt") } android { @@ -11,7 +13,7 @@ android { compileSdk = 33 defaultConfig { - applicationId = "come.emmsale" + applicationId = "com.emmsale" minSdk = 28 targetSdk = 33 versionCode = 1 @@ -40,11 +42,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } dataBinding { enable = true @@ -60,6 +62,9 @@ dependencies { implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.9.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") implementation("androidx.browser:browser:1.5.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") @@ -69,4 +74,7 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") + + implementation(platform("com.google.firebase:firebase-bom:32.2.0")) + implementation("com.google.firebase:firebase-analytics-ktx") } diff --git a/android/2023-emmsale/app/src/main/AndroidManifest.xml b/android/2023-emmsale/app/src/main/AndroidManifest.xml index 74e226b9f..0845def90 100644 --- a/android/2023-emmsale/app/src/main/AndroidManifest.xml +++ b/android/2023-emmsale/app/src/main/AndroidManifest.xml @@ -2,9 +2,8 @@ - - + + +) + +data class Activity( + val id: Int, + val name: String, +) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityRepository.kt new file mode 100644 index 000000000..aa0fe3ae6 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityRepository.kt @@ -0,0 +1,7 @@ +package com.emmsale.data.activity + +import com.emmsale.data.common.ApiResult + +interface ActivityRepository { + suspend fun getActivities(): ApiResult> +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityRepositoryImpl.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityRepositoryImpl.kt new file mode 100644 index 000000000..d95bbe4b3 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityRepositoryImpl.kt @@ -0,0 +1,19 @@ +package com.emmsale.data.activity + +import com.emmsale.data.activity.dto.ActivitiesApiModel +import com.emmsale.data.activity.dto.toData +import com.emmsale.data.common.ApiResult +import com.emmsale.data.common.handleApi +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ActivityRepositoryImpl( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, + private val activityService: ActivityService, +) : ActivityRepository { + + override suspend fun getActivities(): ApiResult> = withContext(dispatcher) { + handleApi(activityService.getActivities(), List::toData) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityService.kt new file mode 100644 index 000000000..983553671 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/ActivityService.kt @@ -0,0 +1,10 @@ +package com.emmsale.data.activity + +import com.emmsale.data.activity.dto.ActivitiesApiModel +import retrofit2.Response +import retrofit2.http.GET + +interface ActivityService { + @GET("/activities") + suspend fun getActivities(): Response> +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/dto/ActivitiesApiModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/dto/ActivitiesApiModel.kt new file mode 100644 index 000000000..0361e534a --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/activity/dto/ActivitiesApiModel.kt @@ -0,0 +1,34 @@ +package com.emmsale.data.activity.dto + +import com.emmsale.data.activity.Activities +import com.emmsale.data.activity.Activity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ActivitiesApiModel( + @SerialName("activityType") + val category: String = "-", + @SerialName("activityResponses") + val activities: List = emptyList() +) { + fun toData(): Activities = Activities( + category = category, + activities.map { it.toData() } + ) +} + +fun List.toData(): List = map { it.toData() } + +@Serializable +data class ActivityApiModel( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, +) { + fun toData(): Activity = Activity( + id = id, + name = name + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/ApiResult.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/ApiResult.kt index 6711245af..87e6d3c2a 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/ApiResult.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/ApiResult.kt @@ -8,15 +8,16 @@ class ApiSuccess(val data: T) : ApiResult class ApiError(val code: Int, val message: String?) : ApiResult class ApiException(val e: Throwable) : ApiResult -suspend fun handleApi( - execute: suspend () -> Response, -): ApiResult { +suspend inline fun handleApi( + response: Response, + mapToDomain: suspend (T) -> V +): ApiResult { return try { - val response = execute() val body = response.body() when { - response.isSuccessful && body != null -> ApiSuccess(body) + response.isSuccessful && body == null && V::class == Unit::class -> ApiSuccess(Unit as V) + response.isSuccessful && body != null -> ApiSuccess(mapToDomain(body)) else -> ApiError(code = response.code(), message = response.message()) } } catch (e: HttpException) { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/AuthInterceptor.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/AuthInterceptor.kt new file mode 100644 index 000000000..2beb468a9 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/AuthInterceptor.kt @@ -0,0 +1,21 @@ +package com.emmsale.data.common + +import com.emmsale.presentation.KerdyApplication +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val token = runBlocking { KerdyApplication.repositoryContainer.tokenRepository.getToken() } + val newRequest = chain.request().newBuilder() + .addHeader(ACCESS_TOKEN_HEADER, ACCESS_TOKEN_FORMAT.format(token?.accessToken)) + .build() + return chain.proceed(newRequest) + } + + companion object { + private const val ACCESS_TOKEN_HEADER = "authorization" + private const val ACCESS_TOKEN_FORMAT = "Bearer %s" + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/RetrofitProvider.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/ServiceFactory.kt similarity index 80% rename from android/2023-emmsale/app/src/main/java/com/emmsale/data/common/RetrofitProvider.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/data/common/ServiceFactory.kt index 776895362..829645b16 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/RetrofitProvider.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/ServiceFactory.kt @@ -1,6 +1,5 @@ package com.emmsale.data.common -import com.emmsale.data.login.LoginService import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType @@ -8,8 +7,7 @@ import okhttp3.OkHttpClient import retrofit2.Retrofit import java.util.concurrent.TimeUnit -object RetrofitProvider { - private const val BASE_URL = "https://kerdy.kro.kr/" +class ServiceFactory { private val jsonMediaType = "application/json".toMediaType() private val json = Json { coerceInputValues = true @@ -19,6 +17,7 @@ object RetrofitProvider { private val jsonConverterFactory = json.asConverterFactory(jsonMediaType) private val okhttpClient = OkHttpClient.Builder() + .addInterceptor(AuthInterceptor()) .connectTimeout(120, TimeUnit.SECONDS) .readTimeout(120, TimeUnit.SECONDS) .writeTimeout(120, TimeUnit.SECONDS) @@ -30,5 +29,9 @@ object RetrofitProvider { .client(okhttpClient) .build() - val loginService: LoginService = retrofit.create(LoginService::class.java) + fun create(service: Class): T = retrofit.create(service) + + companion object { + private const val BASE_URL = "https://kerdy.kro.kr/" + } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/Login.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/Login.kt index 086be7be6..cbc80fa73 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/Login.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/Login.kt @@ -1,17 +1,7 @@ package com.emmsale.data.login -import com.emmsale.data.login.dto.LoginApiModel - data class Login( val uid: Long, val accessToken: String, val isNewMember: Boolean, -) { - companion object { - fun from(apiModel: LoginApiModel): Login = Login( - accessToken = apiModel.accessToken, - uid = apiModel.uid, - isNewMember = apiModel.isNewMember, - ) - } -} +) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/LoginRepositoryImpl.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/LoginRepositoryImpl.kt index efc3419b6..c4352c224 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/LoginRepositoryImpl.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/LoginRepositoryImpl.kt @@ -1,29 +1,17 @@ package com.emmsale.data.login -import com.emmsale.data.common.ApiError -import com.emmsale.data.common.ApiException import com.emmsale.data.common.ApiResult -import com.emmsale.data.common.ApiSuccess import com.emmsale.data.common.handleApi +import com.emmsale.data.login.dto.LoginApiModel import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.withContext -@OptIn(DelicateCoroutinesApi::class) class LoginRepositoryImpl( private val dispatcher: CoroutineDispatcher = Dispatchers.IO, - private val coroutineScope: CoroutineScope = GlobalScope, private val loginService: LoginService, ) : LoginRepository { - override suspend fun saveGithubCode(code: String): ApiResult = - withContext(coroutineScope.coroutineContext + dispatcher) { - when (val loginResponse = handleApi { loginService.saveGithubCode(code) }) { - is ApiSuccess -> ApiSuccess(Login.from(loginResponse.data)) - is ApiError -> ApiError(loginResponse.code, loginResponse.message) - is ApiException -> ApiException(loginResponse.e) - } - } + override suspend fun saveGithubCode(code: String): ApiResult = withContext(dispatcher) { + handleApi(loginService.saveGithubCode(code), LoginApiModel::toData) + } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/dto/LoginApiModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/dto/LoginApiModel.kt index 5a059d033..87e7a6573 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/dto/LoginApiModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/login/dto/LoginApiModel.kt @@ -1,5 +1,6 @@ package com.emmsale.data.login.dto +import com.emmsale.data.login.Login import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -11,4 +12,10 @@ data class LoginApiModel( val accessToken: String, @SerialName("newMember") val isNewMember: Boolean, -) +) { + fun toData(): Login = Login( + accessToken = accessToken, + uid = uid, + isNewMember = isNewMember, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/Member.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/Member.kt new file mode 100644 index 000000000..0b1fe423a --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/Member.kt @@ -0,0 +1,6 @@ +package com.emmsale.data.member + +data class Member( + val name: String, + val activityIds: List, +) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberRepository.kt new file mode 100644 index 000000000..6dc789418 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberRepository.kt @@ -0,0 +1,7 @@ +package com.emmsale.data.member + +import com.emmsale.data.common.ApiResult + +interface MemberRepository { + suspend fun updateMember(member: Member): ApiResult +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberRepositoryImpl.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberRepositoryImpl.kt new file mode 100644 index 000000000..cbccc677a --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberRepositoryImpl.kt @@ -0,0 +1,18 @@ +package com.emmsale.data.member + +import com.emmsale.data.common.ApiResult +import com.emmsale.data.common.handleApi +import com.emmsale.data.member.dto.MemberApiModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class MemberRepositoryImpl( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, + private val memberService: MemberService, +) : MemberRepository { + override suspend fun updateMember(member: Member): ApiResult = withContext(dispatcher) { + handleApi(memberService.updateMember(MemberApiModel.from(member))) { } + } +} + diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberService.kt new file mode 100644 index 000000000..698569a0e --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/MemberService.kt @@ -0,0 +1,11 @@ +package com.emmsale.data.member + +import com.emmsale.data.member.dto.MemberApiModel +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface MemberService { + @POST("/members") + suspend fun updateMember(@Body member: MemberApiModel): Response +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/dto/MemberApiModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/dto/MemberApiModel.kt new file mode 100644 index 000000000..820c98399 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/member/dto/MemberApiModel.kt @@ -0,0 +1,20 @@ +package com.emmsale.data.member.dto + +import com.emmsale.data.member.Member +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MemberApiModel( + @SerialName("name") + val name: String, + @SerialName("activityIds") + val activityIds: List, +) { + companion object { + fun from(member: Member): MemberApiModel = MemberApiModel( + name = member.name, + activityIds = member.activityIds, + ) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/token/TokenRepositoryImpl.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/token/TokenRepositoryImpl.kt index 51f808765..2b791702f 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/token/TokenRepositoryImpl.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/token/TokenRepositoryImpl.kt @@ -9,6 +9,7 @@ class TokenRepositoryImpl( private val dispatcher: CoroutineDispatcher = Dispatchers.IO, private val preference: SharedPreferences, ) : TokenRepository { + override suspend fun saveToken(token: Token) = withContext(dispatcher) { preference.edit().putLong(UID_KEY, token.uid).apply() preference.edit().putString(ACCESS_TOKEN_KEY, token.accessToken).apply() diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/di/RepositoryContainer.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/di/RepositoryContainer.kt new file mode 100644 index 000000000..d1bb10701 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/di/RepositoryContainer.kt @@ -0,0 +1,28 @@ +package com.emmsale.di + +import com.emmsale.data.activity.ActivityRepository +import com.emmsale.data.activity.ActivityRepositoryImpl +import com.emmsale.data.login.LoginRepository +import com.emmsale.data.login.LoginRepositoryImpl +import com.emmsale.data.member.MemberRepository +import com.emmsale.data.member.MemberRepositoryImpl +import com.emmsale.data.token.TokenRepository +import com.emmsale.data.token.TokenRepositoryImpl + +class RepositoryContainer( + serviceContainer: ServiceContainer, + preferenceContainer: SharedPreferenceContainer, +) { + val loginRepository: LoginRepository by lazy { + LoginRepositoryImpl(loginService = serviceContainer.loginService) + } + val tokenRepository: TokenRepository by lazy { + TokenRepositoryImpl(preference = preferenceContainer.preference) + } + val activityRepository: ActivityRepository by lazy { + ActivityRepositoryImpl(activityService = serviceContainer.activityService) + } + val memberRepository: MemberRepository by lazy { + MemberRepositoryImpl(memberService = serviceContainer.memberService) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/di/ServiceContainer.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/di/ServiceContainer.kt new file mode 100644 index 000000000..136adc304 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/di/ServiceContainer.kt @@ -0,0 +1,12 @@ +package com.emmsale.di + +import com.emmsale.data.activity.ActivityService +import com.emmsale.data.common.ServiceFactory +import com.emmsale.data.login.LoginService +import com.emmsale.data.member.MemberService + +class ServiceContainer(serviceFactory: ServiceFactory) { + val loginService: LoginService by lazy { serviceFactory.create(LoginService::class.java) } + val activityService: ActivityService by lazy { serviceFactory.create(ActivityService::class.java) } + val memberService: MemberService by lazy { serviceFactory.create(MemberService::class.java) } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/di/SharedPreferenceContainer.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/di/SharedPreferenceContainer.kt new file mode 100644 index 000000000..c6a49c8ea --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/di/SharedPreferenceContainer.kt @@ -0,0 +1,16 @@ +package com.emmsale.di + +import android.content.Context +import android.content.SharedPreferences + +class SharedPreferenceContainer( + context: Context, +) { + val preference: SharedPreferences by lazy { + context.getSharedPreferences(KERDY_PREF_KEY, Context.MODE_PRIVATE) + } + + companion object { + private const val KERDY_PREF_KEY = "kerdy" + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/KerdyApplication.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/KerdyApplication.kt new file mode 100644 index 000000000..cbd562af4 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/KerdyApplication.kt @@ -0,0 +1,23 @@ +package com.emmsale.presentation + +import android.app.Application +import com.emmsale.data.common.ServiceFactory +import com.emmsale.di.RepositoryContainer +import com.emmsale.di.ServiceContainer +import com.emmsale.di.SharedPreferenceContainer + +class KerdyApplication : Application() { + + override fun onCreate() { + super.onCreate() + repositoryContainer = RepositoryContainer( + serviceContainer = ServiceContainer(ServiceFactory()), + preferenceContainer = SharedPreferenceContainer(this) + ) + } + + companion object { + lateinit var repositoryContainer: RepositoryContainer + private set + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/fragment/BaseFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/fragment/BaseFragment.kt new file mode 100644 index 000000000..d56f32128 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/fragment/BaseFragment.kt @@ -0,0 +1,31 @@ +package com.emmsale.presentation.base.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment + +abstract class BaseFragment : Fragment() { + abstract val layoutResId: Int + + private var _binding: V? = null + protected val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = DataBindingUtil.inflate(inflater, layoutResId, container, false) + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/ViewModelFactory.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/ViewModelFactory.kt new file mode 100644 index 000000000..c60798afa --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/ViewModelFactory.kt @@ -0,0 +1,16 @@ +package com.emmsale.presentation.common + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class ViewModelFactory( + private val create: () -> T +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + val viewModel = create() + if (modelClass.isAssignableFrom(viewModel::class.java)) { + return viewModel as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/livedata/ListLiveData.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/livedata/ListLiveData.kt new file mode 100644 index 000000000..3f07a8ecb --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/livedata/ListLiveData.kt @@ -0,0 +1,40 @@ +package com.emmsale.presentation.common.livedata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map + +class ListLiveData : MutableLiveData>() { + private val items = mutableListOf() + + fun add(item: T) { + items.add(item) + updateState() + } + + fun addAll(newItems: List) { + items.addAll(newItems) + updateState() + } + + fun remove(item: T) { + items.remove(item) + updateState() + } + + fun removeAt(position: Int) { + items.removeAt(position) + updateState() + } + + fun clear() { + items.clear() + updateState() + } + + private fun updateState() { + postValue(items) + } + + fun asLiveData(): LiveData> = this.map { it } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/ActivityTag.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/ActivityTag.kt new file mode 100644 index 000000000..5f230c415 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/ActivityTag.kt @@ -0,0 +1,38 @@ +package com.emmsale.presentation.common.views + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import androidx.appcompat.widget.AppCompatCheckBox +import androidx.core.content.ContextCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import com.emmsale.R +import com.emmsale.presentation.utils.extension.dp + +class ActivityTag : AppCompatCheckBox { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + init { + initView() + } + + private fun initView() { + isClickable = true + buttonDrawable = null + textSize = 13F + gravity = Gravity.CENTER + background = ContextCompat.getDrawable(context, R.drawable.bg_activity_tag) + setTextColor(ContextCompat.getColor(context, R.color.black)) + updatePadding(12.dp, 0, 12.dp, 0) + } +} + +fun Fragment.chipOf( + block: ActivityTag.() -> Unit +): ActivityTag = requireContext().chipOf(block) + +fun Context.chipOf( + block: ActivityTag.() -> Unit +): ActivityTag = ActivityTag(this).apply(block) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt index a88654c9e..35a9d8bcc 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt @@ -4,25 +4,25 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.browser.customtabs.CustomTabsIntent -import androidx.lifecycle.ViewModelProvider import com.emmsale.BuildConfig import com.emmsale.databinding.ActivityLoginBinding import com.emmsale.presentation.ui.login.uistate.LoginUiState -import com.emmsale.presentation.utils.binding.setContentView +import com.emmsale.presentation.ui.onboarding.OnboardingActivity import com.emmsale.presentation.utils.builder.uri import com.google.android.material.snackbar.Snackbar class LoginActivity : AppCompatActivity() { - private val viewModel: LoginViewModel by lazy { - ViewModelProvider(this, LoginViewModelFactory(this))[LoginViewModel::class.java] + private val viewModel: LoginViewModel by viewModels { LoginViewModel.factory } + private val binding: ActivityLoginBinding by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + ActivityLoginBinding.inflate(layoutInflater) } - private lateinit var binding: ActivityLoginBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityLoginBinding.inflate(layoutInflater).setContentView(this) + setContentView(binding.root) binding.viewModel = viewModel setupClickListener() setupLoginState() @@ -55,7 +55,7 @@ class LoginActivity : AppCompatActivity() { } private fun navigateToOnboarding() { - // startActivity(OnboardingActivity.getIntent(this)) + OnboardingActivity.startActivity(this) finish() } @@ -87,9 +87,12 @@ class LoginActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - intent?.data?.getQueryParameter(GITHUB_CODE_PARAMETER)?.let(viewModel::saveGithubCode) + intent?.parseGithubCode()?.let(viewModel::saveGithubCode) } + private fun Intent.parseGithubCode(): String? = + data?.getQueryParameter(GITHUB_CODE_PARAMETER) + companion object { private const val GITHUB_CODE_PARAMETER = "code" } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt index e478e8687..f332bc01c 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt @@ -2,6 +2,7 @@ package com.emmsale.presentation.ui.login import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.emmsale.data.common.ApiError import com.emmsale.data.common.ApiException @@ -10,21 +11,21 @@ import com.emmsale.data.login.Login import com.emmsale.data.login.LoginRepository import com.emmsale.data.token.Token import com.emmsale.data.token.TokenRepository -import com.emmsale.presentation.base.viewmodel.BaseViewModel -import com.emmsale.presentation.base.viewmodel.DispatcherProvider +import com.emmsale.presentation.KerdyApplication +import com.emmsale.presentation.common.ViewModelFactory import com.emmsale.presentation.ui.login.uistate.LoginUiState import kotlinx.coroutines.launch class LoginViewModel( - dispatcherProvider: DispatcherProvider, private val loginRepository: LoginRepository, private val tokenRepository: TokenRepository, -) : BaseViewModel(dispatcherProvider) { +) : ViewModel() { private val _loginState: MutableLiveData = MutableLiveData() val loginState: LiveData = _loginState fun saveGithubCode(code: String) { changeLoginState(LoginUiState.Loading) + viewModelScope.launch { when (val loginResult = loginRepository.saveGithubCode(code)) { is ApiSuccess -> handleLoginResult(loginResult.data) @@ -45,4 +46,14 @@ class LoginViewModel( private fun changeLoginState(loginState: LoginUiState) { _loginState.postValue(loginState) } + + companion object { + val factory = ViewModelFactory { + val repositoryContainer = KerdyApplication.repositoryContainer + LoginViewModel( + loginRepository = repositoryContainer.loginRepository, + tokenRepository = repositoryContainer.tokenRepository, + ) + } + } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModelFactory.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModelFactory.kt deleted file mode 100644 index 4cc76eb00..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModelFactory.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.emmsale.presentation.ui.login - -import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.emmsale.data.common.RetrofitProvider -import com.emmsale.data.login.LoginRepositoryImpl -import com.emmsale.data.token.TokenRepositoryImpl -import com.emmsale.presentation.base.viewmodel.DispatcherProviderImpl -import com.emmsale.presentation.utils.keys.KERDY_PREF_KEY - -class LoginViewModelFactory(private val context: Context) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - val kerdyPreference = context.getSharedPreferences(KERDY_PREF_KEY, Context.MODE_PRIVATE) - - if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { - return LoginViewModel( - DispatcherProviderImpl(), - LoginRepositoryImpl(loginService = RetrofitProvider.loginService), - TokenRepositoryImpl(preference = kerdyPreference) - ) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/ActivityCategory.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/ActivityCategory.kt new file mode 100644 index 000000000..c6dc907fd --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/ActivityCategory.kt @@ -0,0 +1,7 @@ +package com.emmsale.presentation.ui.onboarding + +enum class ActivityCategory(val title: String) { + EDUCATION("교육"), + CLUB("동아리"), + JOB("직무") +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingActivity.kt new file mode 100644 index 000000000..e5b75b4bf --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingActivity.kt @@ -0,0 +1,88 @@ +package com.emmsale.presentation.ui.onboarding + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.emmsale.databinding.ActivityOnboardingBinding +import com.emmsale.presentation.ui.onboarding.uistate.MemberUiState + +class OnboardingActivity : AppCompatActivity() { + private val binding: ActivityOnboardingBinding by lazy { + ActivityOnboardingBinding.inflate(layoutInflater) + } + private val viewModel: OnboardingViewModel by viewModels { OnboardingViewModel.factory } + private val fragmentStateAdapter: OnboardingFragmentStateAdapter by lazy { + OnboardingFragmentStateAdapter(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + initFragmentStateAdapter() + initBackPressedDispatcher() + setupMemberUiState() + } + + private fun setupMemberUiState() { + viewModel.memberUiState.observe(this) { memberState -> + when (memberState) { + is MemberUiState.Success -> navigateToMain() + is MemberUiState.Failed -> showMemberUpdateFailed() + is MemberUiState.Loading -> binding.progressbarLoading.visibility = View.VISIBLE + } + } + } + + private fun initFragmentStateAdapter() { + binding.vpOnboarding.adapter = fragmentStateAdapter + } + + private fun initBackPressedDispatcher() { + onBackPressedDispatcher.addCallback(this, OnboardingOnBackPressedCallback()) + } + + private fun navigateToMain() { + binding.progressbarLoading.visibility = View.GONE + // MainActivity.startActivity(this) + // finish() + } + + private fun showMemberUpdateFailed() { + binding.progressbarLoading.visibility = View.GONE + Toast.makeText(this, "회원정보 업데이트 실패", Toast.LENGTH_SHORT).show() + } + + fun navigateToNextPage() { + val currentPage = binding.vpOnboarding.currentItem + val lastPage = fragmentStateAdapter.itemCount - 1 + when { + currentPage < lastPage -> binding.vpOnboarding.currentItem += 1 + currentPage == lastPage -> viewModel.updateMember() + } + } + + private fun navigateToPrevPage() { + when { + binding.vpOnboarding.currentItem == 0 -> finish() + binding.vpOnboarding.currentItem > 0 -> binding.vpOnboarding.currentItem -= 1 + } + } + + companion object { + fun startActivity(context: Context) { + val intent = Intent(context, OnboardingActivity::class.java) + context.startActivity(intent) + } + } + + inner class OnboardingOnBackPressedCallback : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + navigateToPrevPage() + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingClubFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingClubFragment.kt new file mode 100644 index 000000000..c9523e77f --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingClubFragment.kt @@ -0,0 +1,48 @@ +package com.emmsale.presentation.ui.onboarding + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import com.emmsale.R +import com.emmsale.databinding.FragmentOnboardingClubBinding +import com.emmsale.presentation.base.fragment.BaseFragment +import com.emmsale.presentation.common.views.chipOf +import com.emmsale.presentation.ui.onboarding.uistate.ActivityUiState + +class OnboardingClubFragment : BaseFragment(), View.OnClickListener { + val viewModel: OnboardingViewModel by activityViewModels { OnboardingViewModel.factory } + override val layoutResId: Int = R.layout.fragment_onboarding_club + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = viewModel + initClickListener() + setupClubs() + } + + private fun initClickListener() { + binding.btnNext.setOnClickListener(this) + } + + private fun setupClubs() { + viewModel.clubs.observe(viewLifecycleOwner) { clubs -> + clubs?.activities?.forEach(::addClubChip) + } + } + + private fun addClubChip(clubTag: ActivityUiState) { + binding.chipgroupClubTags.addView(createChip(clubTag)) + } + + private fun createChip(clubTag: ActivityUiState) = chipOf { + text = clubTag.name + isChecked = clubTag.isSelected + setOnCheckedChangeListener { _, _ -> viewModel.toggleTagSelection(clubTag) } + } + + override fun onClick(view: View) { + when (view.id) { + R.id.btn_next -> (requireActivity() as OnboardingActivity).navigateToNextPage() + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingEducationFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingEducationFragment.kt new file mode 100644 index 000000000..2c1630aac --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingEducationFragment.kt @@ -0,0 +1,49 @@ +package com.emmsale.presentation.ui.onboarding + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import com.emmsale.R +import com.emmsale.databinding.FragmentOnboardingEducationBinding +import com.emmsale.presentation.base.fragment.BaseFragment +import com.emmsale.presentation.common.views.chipOf +import com.emmsale.presentation.ui.onboarding.uistate.ActivityUiState + +class OnboardingEducationFragment : BaseFragment(), + View.OnClickListener { + val viewModel: OnboardingViewModel by activityViewModels { OnboardingViewModel.factory } + override val layoutResId: Int = R.layout.fragment_onboarding_education + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = viewModel + initClickListener() + setupEducations() + } + + private fun initClickListener() { + binding.btnNext.setOnClickListener(this) + } + + private fun setupEducations() { + viewModel.educations.observe(viewLifecycleOwner) { educations -> + educations?.activities?.forEach(::addEducationChip) + } + } + + private fun addEducationChip(educationTag: ActivityUiState) { + binding.chipgroupEduTags.addView(createChip(educationTag)) + } + + private fun createChip(educationTag: ActivityUiState) = chipOf { + text = educationTag.name + isChecked = educationTag.isSelected + setOnCheckedChangeListener { _, _ -> viewModel.toggleTagSelection(educationTag) } + } + + override fun onClick(view: View) { + when (view.id) { + R.id.btn_next -> (requireActivity() as OnboardingActivity).navigateToNextPage() + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingFragmentStateAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingFragmentStateAdapter.kt new file mode 100644 index 000000000..d4ebc7937 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingFragmentStateAdapter.kt @@ -0,0 +1,19 @@ +package com.emmsale.presentation.ui.onboarding + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter + +class OnboardingFragmentStateAdapter(fragmentActivity: FragmentActivity) : + FragmentStateAdapter(fragmentActivity) { + private val fragments: List = listOf( + OnboardingNameFragment(), + OnboardingJobFragment(), + OnboardingEducationFragment(), + OnboardingClubFragment(), + ) + + override fun getItemCount(): Int = fragments.size + + override fun createFragment(position: Int): Fragment = fragments[position] +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingJobFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingJobFragment.kt new file mode 100644 index 000000000..676f9c1a1 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingJobFragment.kt @@ -0,0 +1,44 @@ +package com.emmsale.presentation.ui.onboarding + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import com.emmsale.R +import com.emmsale.databinding.FragmentOnboardingJobBinding +import com.emmsale.presentation.base.fragment.BaseFragment +import com.emmsale.presentation.common.views.chipOf +import com.emmsale.presentation.ui.onboarding.uistate.ActivityUiState + +class OnboardingJobFragment : BaseFragment() { + val viewModel: OnboardingViewModel by activityViewModels { OnboardingViewModel.factory } + override val layoutResId: Int = R.layout.fragment_onboarding_job + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = viewModel + initClickListener() + setupJobs() + } + + private fun initClickListener() { + binding.btnNext.setOnClickListener { + (requireActivity() as OnboardingActivity).navigateToNextPage() + } + } + + private fun setupJobs() { + viewModel.jobs.observe(viewLifecycleOwner) { jobs -> + jobs?.activities?.forEach(::addJobChip) + } + } + + private fun addJobChip(jobTag: ActivityUiState) { + binding.chipgroupJobTags.addView(createChip(jobTag)) + } + + private fun createChip(jobTag: ActivityUiState) = chipOf { + text = jobTag.name + isChecked = jobTag.isSelected + setOnCheckedChangeListener { _, _ -> viewModel.toggleTagSelection(jobTag) } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingNameFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingNameFragment.kt new file mode 100644 index 000000000..09dc98499 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingNameFragment.kt @@ -0,0 +1,29 @@ +package com.emmsale.presentation.ui.onboarding + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import com.emmsale.R +import com.emmsale.databinding.FragmentOnboardingNameBinding +import com.emmsale.presentation.base.fragment.BaseFragment + +class OnboardingNameFragment : BaseFragment(), View.OnClickListener { + val viewModel: OnboardingViewModel by activityViewModels { OnboardingViewModel.factory } + override val layoutResId: Int = R.layout.fragment_onboarding_name + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = viewModel + initClickListener() + } + + private fun initClickListener() { + binding.btnNext.setOnClickListener(this) + } + + override fun onClick(view: View) { + when (view.id) { + R.id.btn_next -> (requireActivity() as OnboardingActivity).navigateToNextPage() + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingViewModel.kt new file mode 100644 index 000000000..786bc85cb --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingViewModel.kt @@ -0,0 +1,113 @@ +package com.emmsale.presentation.ui.onboarding + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import androidx.lifecycle.viewModelScope +import com.emmsale.data.activity.ActivityRepository +import com.emmsale.data.common.ApiError +import com.emmsale.data.common.ApiException +import com.emmsale.data.common.ApiSuccess +import com.emmsale.data.member.Member +import com.emmsale.data.member.MemberRepository +import com.emmsale.presentation.KerdyApplication +import com.emmsale.presentation.common.ViewModelFactory +import com.emmsale.presentation.ui.onboarding.uistate.ActivitiesUiState +import com.emmsale.presentation.ui.onboarding.uistate.ActivityTypeContentUiState +import com.emmsale.presentation.ui.onboarding.uistate.ActivityUiState +import com.emmsale.presentation.ui.onboarding.uistate.MemberUiState +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class OnboardingViewModel( + private val activityRepository: ActivityRepository, + private val memberRepository: MemberRepository, +) : ViewModel() { + val name: MutableLiveData = MutableLiveData() + + // TODO("Error Handling on OnboardingActivity") + private val _activities: MutableLiveData = MutableLiveData() + private val selectedActivityIds: MutableList = mutableListOf() + + val educations: LiveData = _activities.map { activityTypeContent -> + when (activityTypeContent) { + is ActivityTypeContentUiState.Success -> + findActivity(activityTypeContent, ActivityCategory.EDUCATION) + + is ActivityTypeContentUiState.Error -> null + } + } + + val clubs: LiveData = _activities.map { activityTypeContent -> + when (activityTypeContent) { + is ActivityTypeContentUiState.Success -> + findActivity(activityTypeContent, ActivityCategory.CLUB) + + is ActivityTypeContentUiState.Error -> null + } + } + + val jobs: LiveData = _activities.map { activityTypeContent -> + when (activityTypeContent) { + is ActivityTypeContentUiState.Success -> + findActivity(activityTypeContent, ActivityCategory.JOB) + + is ActivityTypeContentUiState.Error -> null + } + } + + private val _memberUiState = MutableLiveData() + val memberUiState: LiveData = _memberUiState + + init { + fetchActivities() + } + + private fun fetchActivities(): Job = viewModelScope.launch { + when (val activitiesResult = activityRepository.getActivities()) { + is ApiSuccess -> + _activities.postValue(ActivityTypeContentUiState.from(activitiesResult.data)) + + is ApiError -> _activities.postValue(ActivityTypeContentUiState.Error) + is ApiException -> _activities.postValue(ActivityTypeContentUiState.Error) + } + } + + fun toggleTagSelection(tag: ActivityUiState) { + tag.isSelected = !tag.isSelected + when (tag.isSelected) { + true -> selectedActivityIds.add(tag.id) + false -> selectedActivityIds.remove(tag.id) + } + } + + fun updateMember() { + val memberName = name.value ?: return + + viewModelScope.launch { + _memberUiState.value = MemberUiState.Loading + val member = Member(memberName, selectedActivityIds) + + when (memberRepository.updateMember(member)) { + is ApiSuccess -> _memberUiState.value = MemberUiState.Success + is ApiError -> _memberUiState.value = MemberUiState.Failed + is ApiException -> _memberUiState.value = MemberUiState.Failed + } + } + } + + private fun findActivity( + activityTypeContent: ActivityTypeContentUiState.Success, + category: ActivityCategory + ): ActivitiesUiState? = activityTypeContent.activities.find { it.category == category.title } + + companion object { + val factory = ViewModelFactory { + OnboardingViewModel( + activityRepository = KerdyApplication.repositoryContainer.activityRepository, + memberRepository = KerdyApplication.repositoryContainer.memberRepository + ) + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivitiesUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivitiesUiState.kt new file mode 100644 index 000000000..c74ba1aac --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivitiesUiState.kt @@ -0,0 +1,15 @@ +package com.emmsale.presentation.ui.onboarding.uistate + +import com.emmsale.data.activity.Activities + +data class ActivitiesUiState( + val category: String, + val activities: List, +) { + companion object { + fun from(activities: Activities): ActivitiesUiState = ActivitiesUiState( + category = activities.category, + activities = activities.activities.map(ActivityUiState::from), + ) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivityTypeContentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivityTypeContentUiState.kt new file mode 100644 index 000000000..091dcdc55 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivityTypeContentUiState.kt @@ -0,0 +1,13 @@ +package com.emmsale.presentation.ui.onboarding.uistate + +import com.emmsale.data.activity.Activities + +sealed class ActivityTypeContentUiState { + data class Success(val activities: List) : ActivityTypeContentUiState() + object Error : ActivityTypeContentUiState() + + companion object { + fun from(activitiesResult: List): Success = + Success(activities = activitiesResult.map(ActivitiesUiState::from)) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivityUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivityUiState.kt new file mode 100644 index 000000000..130a9937e --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/ActivityUiState.kt @@ -0,0 +1,16 @@ +package com.emmsale.presentation.ui.onboarding.uistate + +import com.emmsale.data.activity.Activity + +data class ActivityUiState( + val id: Int, + val name: String, + var isSelected: Boolean = false, +) { + companion object { + fun from(activity: Activity): ActivityUiState = ActivityUiState( + id = activity.id, + name = activity.name + ) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/MemberUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/MemberUiState.kt new file mode 100644 index 000000000..84a633e22 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uistate/MemberUiState.kt @@ -0,0 +1,7 @@ +package com.emmsale.presentation.ui.onboarding.uistate + +sealed class MemberUiState { + object Success : MemberUiState() + object Loading : MemberUiState() + object Failed : MemberUiState() +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/binding/ViewDataBindingExt.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/binding/ViewDataBindingExt.kt deleted file mode 100644 index 6cbaa98af..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/binding/ViewDataBindingExt.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.emmsale.presentation.utils.binding - -import android.app.Activity -import androidx.databinding.ViewDataBinding - -fun T.setContentView(activity: Activity): T = run { - activity.setContentView(root) - this -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/bindingadapter/ViewPagerBindingAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/bindingadapter/ViewPagerBindingAdapter.kt new file mode 100644 index 000000000..9e7d2c5c1 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/bindingadapter/ViewPagerBindingAdapter.kt @@ -0,0 +1,9 @@ +package com.emmsale.presentation.utils.bindingadapter + +import androidx.databinding.BindingAdapter +import androidx.viewpager2.widget.ViewPager2 + +@BindingAdapter("app:isUserInputEnabled") +fun ViewPager2.setUserInput(isInputEnabled: Boolean) { + isUserInputEnabled = isInputEnabled +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/extension/IntExt.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/extension/IntExt.kt new file mode 100644 index 000000000..95d40f572 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/extension/IntExt.kt @@ -0,0 +1,10 @@ +package com.emmsale.presentation.utils.extension + +import android.content.res.Resources +import kotlin.math.roundToInt + +val Int.dp: Int + get() = (this * Resources.getSystem().displayMetrics.density).roundToInt() + +val Int.px: Int + get() = (this / Resources.getSystem().displayMetrics.density).roundToInt() diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/extensions/ViewModelExt.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/extensions/ViewModelExt.kt deleted file mode 100644 index ec47d3916..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/extensions/ViewModelExt.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.emmsale.presentation.utils.extensions - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/keys/Keys.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/keys/Keys.kt deleted file mode 100644 index 0253b0a0d..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/utils/keys/Keys.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.emmsale.presentation.utils.keys - -internal const val KERDY_PREF_KEY = "kerdy_pref_key" diff --git a/android/2023-emmsale/app/src/main/res/drawable/bg_activity_tag.xml b/android/2023-emmsale/app/src/main/res/drawable/bg_activity_tag.xml new file mode 100644 index 000000000..1bc36dc32 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/drawable/bg_activity_tag.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/drawable/bg_all_next.xml b/android/2023-emmsale/app/src/main/res/drawable/bg_all_next.xml new file mode 100644 index 000000000..895f1505d --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/drawable/bg_all_next.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/drawable/bg_edittext.xml b/android/2023-emmsale/app/src/main/res/drawable/bg_edittext.xml new file mode 100644 index 000000000..02af22b99 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/drawable/bg_edittext.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/2023-emmsale/app/src/main/res/drawable/bg_edittext_cursor.xml b/android/2023-emmsale/app/src/main/res/drawable/bg_edittext_cursor.xml new file mode 100644 index 000000000..d8d36309d --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/drawable/bg_edittext_cursor.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/2023-emmsale/app/src/main/res/drawable/ic_login_github.xml b/android/2023-emmsale/app/src/main/res/drawable/ic_login_github.xml index ea5f890c6..773fda4f6 100644 --- a/android/2023-emmsale/app/src/main/res/drawable/ic_login_github.xml +++ b/android/2023-emmsale/app/src/main/res/drawable/ic_login_github.xml @@ -3,7 +3,7 @@ android:height="29dp" android:viewportWidth="30" android:viewportHeight="29"> - + diff --git a/android/2023-emmsale/app/src/main/res/drawable/img_login_github.png b/android/2023-emmsale/app/src/main/res/drawable/img_login_github.png new file mode 100644 index 000000000..2f80870c5 Binary files /dev/null and b/android/2023-emmsale/app/src/main/res/drawable/img_login_github.png differ diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_onboarding.xml b/android/2023-emmsale/app/src/main/res/layout/activity_onboarding.xml new file mode 100644 index 000000000..bc542de2b --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/activity_onboarding.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_club.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_club.xml new file mode 100644 index 000000000..15ce27d32 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_club.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_education.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_education.xml new file mode 100644 index 000000000..e32418f7e --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_education.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_job.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_job.xml new file mode 100644 index 000000000..ecda84d03 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_job.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_name.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_name.xml new file mode 100644 index 000000000..08fc83d78 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_name.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/values-night/themes.xml b/android/2023-emmsale/app/src/main/res/values-night/themes.xml index 0c11908f4..d1b4ee30c 100644 --- a/android/2023-emmsale/app/src/main/res/values-night/themes.xml +++ b/android/2023-emmsale/app/src/main/res/values-night/themes.xml @@ -1,4 +1,4 @@ - + - diff --git a/android/2023-emmsale/build.gradle.kts b/android/2023-emmsale/build.gradle.kts index 4698a536a..47a5362ca 100644 --- a/android/2023-emmsale/build.gradle.kts +++ b/android/2023-emmsale/build.gradle.kts @@ -3,6 +3,7 @@ plugins { val agpVersion = "8.0.2" id("com.android.application") version "8.0.2" apply false id("com.android.library") version agpVersion apply false + id("com.google.gms.google-services") version "4.3.15" apply false val kotlinVersion = "1.8.20" kotlin("android") version kotlinVersion apply false